diff --git a/app/src/ui/components/display/Alert.tsx b/app/src/ui/components/display/Alert.tsx index de2d366..0cd4240 100644 --- a/app/src/ui/components/display/Alert.tsx +++ b/app/src/ui/components/display/Alert.tsx @@ -18,13 +18,7 @@ const Base: React.FC = ({ ...props }) => visible ? ( -
+

{title && {title}: } {message || children} @@ -33,19 +27,19 @@ const Base: React.FC = ({ ) : null; const Warning: React.FC = ({ className, ...props }) => ( - + ); const Exception: React.FC = ({ className, ...props }) => ( - + ); const Success: React.FC = ({ className, ...props }) => ( - + ); const Info: React.FC = ({ className, ...props }) => ( - + ); export const Alert = { diff --git a/app/src/ui/components/form/Formy/components.tsx b/app/src/ui/components/form/Formy/components.tsx index 9996139..4eb8cb4 100644 --- a/app/src/ui/components/form/Formy/components.tsx +++ b/app/src/ui/components/form/Formy/components.tsx @@ -3,7 +3,6 @@ import { getBrowser } from "core/utils"; import type { Field } from "data"; import { Switch as RadixSwitch } from "radix-ui"; import { - type ChangeEventHandler, type ComponentPropsWithoutRef, type ElementType, forwardRef, @@ -12,7 +11,7 @@ import { useRef, useState, } from "react"; -import { TbCalendar, TbChevronDown, TbInfoCircle } from "react-icons/tb"; +import { TbCalendar, TbChevronDown, TbEye, TbEyeOff, TbInfoCircle } from "react-icons/tb"; import { twMerge } from "tailwind-merge"; import { IconButton } from "ui/components/buttons/IconButton"; import { useEvent } from "ui/hooks/use-event"; @@ -89,7 +88,7 @@ export const Input = forwardRef> {...props} ref={ref} className={twMerge( - "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none", + "bg-muted/40 h-11 rounded-md py-2.5 px-4 outline-none w-full", disabledOrReadonly && "bg-muted/50 text-primary/50", !disabledOrReadonly && "focus:bg-muted focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all", @@ -99,6 +98,40 @@ export const Input = forwardRef> ); }); +export const TypeAwareInput = forwardRef>( + (props, ref) => { + if (props.type === "password") { + return ; + } + + return ; + }, +); + +export const Password = forwardRef>( + (props, ref) => { + const [visible, setVisible] = useState(false); + + function handleToggle() { + setVisible((v) => !v); + } + + return ( +

+ +
+ +
+
+ ); + }, +); + export const Textarea = forwardRef>( (props, ref) => { return ( diff --git a/app/src/ui/components/form/json-schema-form/Field.tsx b/app/src/ui/components/form/json-schema-form/Field.tsx index 955d882..e511977 100644 --- a/app/src/ui/components/form/json-schema-form/Field.tsx +++ b/app/src/ui/components/form/json-schema-form/Field.tsx @@ -1,5 +1,5 @@ import type { JsonSchema } from "json-schema-library"; -import type { ChangeEvent, ComponentPropsWithoutRef } from "react"; +import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react"; import ErrorBoundary from "ui/components/display/ErrorBoundary"; import * as Formy from "ui/components/form/Formy"; import { useEvent } from "ui/hooks/use-event"; @@ -13,6 +13,7 @@ export type FieldProps = { onChange?: (e: ChangeEvent) => void; placeholder?: string; disabled?: boolean; + inputProps?: Partial; } & Omit; export const Field = (props: FieldProps) => { @@ -31,7 +32,14 @@ const fieldErrorBoundary = ); -const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props }: FieldProps) => { +const FieldImpl = ({ + name, + onChange, + placeholder, + required: _required, + inputProps, + ...props +}: FieldProps) => { const { path, setValue, schema, ...ctx } = useDerivedFieldContext(name); const required = typeof _required === "boolean" ? _required : ctx.required; //console.log("Field", { name, path, schema }); @@ -64,6 +72,7 @@ const FieldImpl = ({ name, onChange, placeholder, required: _required, ...props return ( ( ); -export const FieldComponent = ({ - schema, - ..._props -}: { schema: JsonSchema } & ComponentPropsWithoutRef<"input">) => { +export type FieldComponentProps = { + schema: JsonSchema; + render?: (props: Omit) => ReactNode; +} & ComponentPropsWithoutRef<"input">; + +export const FieldComponent = ({ schema, render, ..._props }: FieldComponentProps) => { const { value } = useFormValue(_props.name!, { strict: true }); if (!isTypeSchema(schema)) return null; const props = { @@ -97,6 +108,8 @@ export const FieldComponent = ({ : "", }; + if (render) return render({ schema, ...props }); + if (schema.enum) { return ; } @@ -158,5 +171,7 @@ export const FieldComponent = ({ } } - return ; + return ( + + ); }; diff --git a/app/src/ui/layouts/AppShell/AppShell.tsx b/app/src/ui/layouts/AppShell/AppShell.tsx index 25110d7..16cfc8b 100644 --- a/app/src/ui/layouts/AppShell/AppShell.tsx +++ b/app/src/ui/layouts/AppShell/AppShell.tsx @@ -1,5 +1,6 @@ import { useClickOutside, useHotkeys } from "@mantine/hooks"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; +import { clampNumber } from "core/utils/numbers"; import { throttle } from "lodash-es"; import { ScrollArea } from "radix-ui"; import { @@ -12,13 +13,20 @@ import { import type { IconType } from "react-icons"; import { twMerge } from "tailwind-merge"; import { IconButton } from "ui/components/buttons/IconButton"; -import { useEvent } from "ui/hooks/use-event"; import { AppShellProvider, useAppShell } from "ui/layouts/AppShell/use-appshell"; +import { appShellStore } from "ui/store"; +import { useLocation } from "wouter"; -export function Root({ children }) { +export function Root({ children }: { children: React.ReactNode }) { + const sidebarWidth = appShellStore((store) => store.sidebarWidth); return ( -
+
{children}
@@ -80,7 +88,7 @@ export function Main({ children }) { data-shell="main" className={twMerge( "flex flex-col flex-grow w-1 flex-shrink-1", - sidebar.open && "md:max-w-[calc(100%-350px)]", + sidebar.open && "md:max-w-[calc(100%-var(--sidebar-width))]", )} > {children} @@ -89,47 +97,38 @@ export function Main({ children }) { } export function Sidebar({ children }) { - const ctx = useAppShell(); + const open = appShellStore((store) => store.sidebarOpen); + const close = appShellStore((store) => store.closeSidebar); + const ref = useClickOutside(close, null, [document.getElementById("header")]); + const [location] = useLocation(); - const ref = useClickOutside(ctx.sidebar?.handler?.close); + const closeHandler = () => { + open && close(); + }; - const onClickBackdrop = useEvent((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - ctx?.sidebar?.handler.close(); - }); - - const onEscape = useEvent(() => { - if (ctx?.sidebar?.open) { - ctx?.sidebar?.handler.close(); - } - }); + // listen for window location change + useEffect(closeHandler, [location]); // @todo: potentially has to be added to the root, as modals could be opened - useHotkeys([["Escape", onEscape]]); - - if (!ctx) { - console.warn("AppShell.Sidebar: missing AppShellContext"); - return null; - } + useHotkeys([["Escape", closeHandler]]); return ( <> +
@@ -138,6 +137,59 @@ export function Sidebar({ children }) { ); } +const SidebarResize = () => { + const setSidebarWidth = appShellStore((store) => store.setSidebarWidth); + const [isResizing, setIsResizing] = useState(false); + const [startX, setStartX] = useState(0); + const [startWidth, setStartWidth] = useState(0); + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + setIsResizing(true); + setStartX(e.clientX); + setStartWidth( + Number.parseInt( + getComputedStyle(document.getElementById("app-shell")!) + .getPropertyValue("--sidebar-width") + .replace("px", ""), + ), + ); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing) return; + + const diff = e.clientX - startX; + const newWidth = clampNumber(startWidth + diff, 250, window.innerWidth * 0.5); + setSidebarWidth(newWidth); + }; + + const handleMouseUp = () => { + setIsResizing(false); + }; + + useEffect(() => { + if (isResizing) { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + } + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isResizing, startX, startWidth]); + + return ( +
+ ); +}; + export function SectionHeaderTitle({ children, className, ...props }: ComponentProps<"h2">) { return (

- ); + const toggle = appShellStore((store) => store.toggleSidebar); + const open = appShellStore((store) => store.sidebarOpen); + return ; } export function Header({ hasSidebar = true }) { @@ -118,6 +117,7 @@ export function Header({ hasSidebar = true }) { return (

)}
- {todos?.reverse().map((todo) => ( -
-
- { - await $q.update({ done: !todo.done }, todo.id); + {todos && + [...todos].reverse().map((todo) => ( +
+
+ { + await $q.update({ done: !todo.done }, todo.id); + }} + /> +
{todo.title}
+
+
- -
- ))} + ))}
*/}
+ ); } + +function Debug({ app }: { app: App }) { + const [info, setInfo] = useState(); + const connection = app.em.connection as SQLocalConnection; + + useEffect(() => { + (async () => { + setInfo(await connection.client.getDatabaseInfo()); + app.emgr.onAny( + async () => { + setInfo(await connection.client.getDatabaseInfo()); + }, + { mode: "sync", id: "debug" }, + ); + })(); + }, []); + + async function download() { + const databaseFile = await connection.client.getDatabaseFile(); + const fileUrl = URL.createObjectURL(databaseFile); + + const a = document.createElement("a"); + a.href = fileUrl; + a.download = "database.sqlite3"; + a.click(); + a.remove(); + + URL.revokeObjectURL(fileUrl); + } + + return ( +
+ +
{JSON.stringify(info, null, 2)}
+
+ ); +}