diff --git a/web/package-lock.json b/web/package-lock.json index 39895ed..72c0774 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -111,6 +111,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -738,6 +739,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2008,6 +2010,7 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -4280,6 +4283,7 @@ "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4290,6 +4294,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4300,6 +4305,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4316,8 +4322,7 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@types/unist": { "version": "3.0.3", @@ -4383,6 +4388,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -4909,6 +4915,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5295,6 +5302,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -5389,6 +5397,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6352,7 +6361,6 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", "license": "(MPL-2.0 OR Apache-2.0)", - "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -6691,6 +6699,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6831,6 +6840,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7299,6 +7309,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -8091,6 +8102,7 @@ "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -9510,7 +9522,6 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -11407,6 +11418,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -11437,6 +11449,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11456,6 +11469,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -11594,7 +11608,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -12808,6 +12823,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13075,6 +13091,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13806,6 +13823,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/src/app/(dashboard)/admins/page.tsx b/web/src/app/(dashboard)/admins/page.tsx index 5ff2054..abee1b8 100644 --- a/web/src/app/(dashboard)/admins/page.tsx +++ b/web/src/app/(dashboard)/admins/page.tsx @@ -9,15 +9,15 @@ import { SiteHeader } from "@/components/site-header"; import { Button } from "@/components/ui/button"; import { Trash2 } from "lucide-react"; import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; + ResponsiveDialog, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, + ResponsiveDialogTrigger, +} from "@/components/ui/responsive-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -144,15 +144,15 @@ function AddAdminDialog({ } return ( - - + + - - - - Add Admin - Add a new admin by their DID. - + + + + Add Admin + Add a new admin by their DID. +
{error &&

{error}

}
@@ -165,13 +165,13 @@ function AddAdminDialog({ />
- - + + - + - -
-
+ + + ); } diff --git a/web/src/app/(dashboard)/backfill/page.tsx b/web/src/app/(dashboard)/backfill/page.tsx index 6ee4dea..d950f6d 100644 --- a/web/src/app/(dashboard)/backfill/page.tsx +++ b/web/src/app/(dashboard)/backfill/page.tsx @@ -28,15 +28,15 @@ import { ComboboxList, } from "@/components/ui/combobox"; import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; + ResponsiveDialog, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, + ResponsiveDialogTrigger, +} from "@/components/ui/responsive-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -213,18 +213,18 @@ function CreateDialog({ } return ( - - + + - - - - Create Backfill Job - + + + + Create Backfill Job + Start a backfill for a collection or specific DID. Leave both empty to backfill all collections. - - + +
{error &&

{error}

}
@@ -256,13 +256,13 @@ function CreateDialog({ />
- - + + - + - -
-
+ + + ); } diff --git a/web/src/app/(dashboard)/events/page.tsx b/web/src/app/(dashboard)/events/page.tsx index 6c03b83..55cb9b8 100644 --- a/web/src/app/(dashboard)/events/page.tsx +++ b/web/src/app/(dashboard)/events/page.tsx @@ -22,11 +22,11 @@ import { SiteHeader } from "@/components/site-header"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; + ResponsiveDialog, + ResponsiveDialogContent, + ResponsiveDialogHeader, + ResponsiveDialogTitle, +} from "@/components/ui/responsive-dialog"; import { ChevronLeft, ChevronRight } from "lucide-react"; function severityBadge(severity: string) { @@ -333,16 +333,16 @@ export default function EventsPage() { {viewEvent && ( - setViewEvent(null)}> - - - + setViewEvent(null)}> + + + {severityBadge(viewEvent.severity)} {viewEvent.event_type} - - + +
Subject @@ -370,8 +370,8 @@ export default function EventsPage() { className="mt-1 rounded-md" />
- -
+ + )} diff --git a/web/src/app/(dashboard)/records/page.tsx b/web/src/app/(dashboard)/records/page.tsx index ab573cd..7612b7f 100644 --- a/web/src/app/(dashboard)/records/page.tsx +++ b/web/src/app/(dashboard)/records/page.tsx @@ -20,15 +20,14 @@ import { import type { CollectionStat } from "@/types/stats"; import type { AdminRecord } from "@/types/records"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; + ResponsiveDialog, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, +} from "@/components/ui/responsive-dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { CodeBlock } from "@/components/code-block"; import { DataTable } from "@/components/data-table/data-table"; @@ -41,12 +40,6 @@ import { } from "@/components/ui/dropdown-menu"; import { SiteHeader } from "@/components/site-header"; import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; import { Select, SelectContent, @@ -422,13 +415,13 @@ export default function RecordsPage() { )} {viewRecord && ( - setViewRecord(null)}> - - - + setViewRecord(null)}> + + + {viewRecord.uri} - - + +
-
-
+ + )} - { if (!open) setDeleteUri(null); }} > - - - Delete record? - + + + Delete record? + This will permanently delete the record. This action cannot be undone. - - + + {deleteUri && ( {deleteUri} )} - - Cancel - + + + + + + + - { if (!open) { @@ -487,7 +482,7 @@ export default function RecordsPage() { } }} > - + {(() => { const selectedCount = Object.keys(rowSelection).length; const totalCount = @@ -500,19 +495,19 @@ export default function RecordsPage() { if (allInCollection) { return ( <> - - + + Delete all records in collection? - - + + This will permanently delete all {totalCount} record(s) in{" "} {selectedCollection} . This action cannot be undone. - - + +
- - - Cancel - - + + + + + ); } @@ -553,38 +550,40 @@ export default function RecordsPage() { if (!table.getIsAllPageRowsSelected()) { return ( <> - - + + Delete {selectedCount} record(s)? - - + + This action cannot be undone. - - - - - Cancel - - + + + + + + + ); } return ( <> - - Delete records? - + + Delete records? + This action cannot be undone. - - + + { @@ -641,11 +640,13 @@ export default function RecordsPage() { /> )} - - - Cancel - - + + + + + ); })()} -
-
+ + ); diff --git a/web/src/app/(dashboard)/settings/page.tsx b/web/src/app/(dashboard)/settings/page.tsx index 3cbc138..78ebd63 100644 --- a/web/src/app/(dashboard)/settings/page.tsx +++ b/web/src/app/(dashboard)/settings/page.tsx @@ -17,26 +17,15 @@ import type { ApiKeySummary, CreateApiKeyResponse } from "@/types/api-keys"; import { SiteHeader } from "@/components/site-header"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; + ResponsiveDialog, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, + ResponsiveDialogTrigger, +} from "@/components/ui/responsive-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; @@ -229,33 +218,38 @@ export default function SettingsPage() { {!key.revoked_at && ( - - + + - - - - + + + + Revoke API Key - - + + This will permanently revoke the key “ {key.name}”. Any services using this key will lose access. - - - - Cancel - handleRevoke(key.id)} - > - Revoke - - - - + + + + + + + + + + + + )} @@ -303,7 +297,7 @@ function UpsertVariableDialog({ } return ( - { setOpen(o); @@ -314,7 +308,7 @@ function UpsertVariableDialog({ } }} > - + {isEdit ? ( )} - - - - + + + + {isEdit ? "Edit Variable" : "Add Variable"} - - + + {isEdit ? "Update the value for this script variable." : "Add a new script variable accessible via env.KEY in Lua scripts."} - - + +
{error &&

{error}

}
@@ -364,14 +358,14 @@ function UpsertVariableDialog({ />
- - + + - + - -
-
+ + + ); } @@ -421,21 +415,21 @@ function CreateApiKeyDialog({ } return ( - - + + - - - - + + + + {createdKey ? "API Key Created" : "Create API Key"} - - + + {createdKey ? "Copy your API key now. It won\u2019t be shown again." : "Give this key a name to identify its purpose."} - - + + {createdKey ? (
@@ -480,19 +474,19 @@ function CreateApiKeyDialog({
)} - - + + - + {!createdKey && ( )} - -
-
+ + + ); } diff --git a/web/src/components/ui/responsive-dialog.tsx b/web/src/components/ui/responsive-dialog.tsx new file mode 100644 index 0000000..544265a --- /dev/null +++ b/web/src/components/ui/responsive-dialog.tsx @@ -0,0 +1,280 @@ +"use client"; + +import * as React from "react"; + +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + DrawerPortal, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import { cn } from "@/lib/utils"; +import { useAsRef } from "@/hooks/use-as-ref"; +import { useIsomorphicLayoutEffect } from "@/hooks/use-isomorphic-layout-effect"; +import { useLazyRef } from "@/hooks/use-lazy-ref"; +import { useIsMobile } from "@/hooks/use-mobile"; + +const ROOT_NAME = "ResponsiveDialog"; + +interface StoreState { + open: boolean; + isMobile: boolean; +} + +interface Store { + subscribe: (callback: () => void) => () => void; + getState: () => StoreState; + setState: (key: K, value: StoreState[K]) => void; + notify: () => void; +} + +const StoreContext = React.createContext(null); + +function useStore( + selector: (state: StoreState) => T, + ogStore?: Store | null, +): T { + const contextStore = React.useContext(StoreContext); + const store = ogStore ?? contextStore; + + if (!store) { + throw new Error(`\`useStore\` must be used within \`${ROOT_NAME}\``); + } + + const getSnapshot = React.useCallback( + () => selector(store.getState()), + [store, selector], + ); + + return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot); +} + +interface ResponsiveDialogProps extends React.ComponentProps { + breakpoint?: number; +} + +function ResponsiveDialog({ + breakpoint = 768, + open: openProp, + defaultOpen = false, + onOpenChange: onOpenChangeProp, + ...props +}: ResponsiveDialogProps) { + const isMobile = useIsMobile(breakpoint); + + const listenersRef = useLazyRef(() => new Set<() => void>()); + const stateRef = useLazyRef(() => ({ + open: openProp ?? defaultOpen, + isMobile, + })); + + const onOpenChangeRef = useAsRef(onOpenChangeProp); + + const store = React.useMemo(() => { + return { + subscribe: (cb) => { + listenersRef.current.add(cb); + return () => listenersRef.current.delete(cb); + }, + getState: () => stateRef.current, + setState: (key, value) => { + if (Object.is(stateRef.current[key], value)) return; + + if (key === "open" && typeof value === "boolean") { + stateRef.current.open = value; + onOpenChangeRef.current?.(value); + } else { + stateRef.current[key] = value; + } + + store.notify(); + }, + notify: () => { + for (const cb of listenersRef.current) { + cb(); + } + }, + }; + }, [listenersRef, stateRef, onOpenChangeRef]); + + const open = useStore((state) => state.open, store); + + useIsomorphicLayoutEffect(() => { + if (openProp !== undefined) { + store.setState("open", openProp); + } + }, [openProp]); + + useIsomorphicLayoutEffect(() => { + store.setState("isMobile", isMobile); + }, [isMobile]); + + const onOpenChange = React.useCallback( + (value: boolean) => { + store.setState("open", value); + }, + [store], + ); + + if (isMobile) { + return ( + + + + ); + } + + return ( + + + + ); +} + +function ResponsiveDialogTrigger({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +function ResponsiveDialogClose({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +function ResponsiveDialogPortal({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +function ResponsiveDialogOverlay({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +function ResponsiveDialogContent({ + className, + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ( + + ); + } + + return ( + + ); +} + +function ResponsiveDialogHeader({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +function ResponsiveDialogFooter({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +function ResponsiveDialogTitle({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +function ResponsiveDialogDescription({ + ...props +}: React.ComponentProps) { + const isMobile = useStore((state) => state.isMobile); + + if (isMobile) { + return ; + } + + return ; +} + +export { + ResponsiveDialog, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogOverlay, + ResponsiveDialogPortal, + ResponsiveDialogTitle, + ResponsiveDialogTrigger, +}; diff --git a/web/src/hooks/use-as-ref.ts b/web/src/hooks/use-as-ref.ts new file mode 100644 index 0000000..985e51c --- /dev/null +++ b/web/src/hooks/use-as-ref.ts @@ -0,0 +1,15 @@ +import * as React from "react"; + +import { useIsomorphicLayoutEffect } from "@/hooks/use-isomorphic-layout-effect"; + +function useAsRef(props: T) { + const ref = React.useRef(props); + + useIsomorphicLayoutEffect(() => { + ref.current = props; + }); + + return ref; +} + +export { useAsRef }; diff --git a/web/src/hooks/use-isomorphic-layout-effect.ts b/web/src/hooks/use-isomorphic-layout-effect.ts new file mode 100644 index 0000000..08d7411 --- /dev/null +++ b/web/src/hooks/use-isomorphic-layout-effect.ts @@ -0,0 +1,6 @@ +import * as React from "react"; + +const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; + +export { useIsomorphicLayoutEffect }; diff --git a/web/src/hooks/use-lazy-ref.ts b/web/src/hooks/use-lazy-ref.ts new file mode 100644 index 0000000..535c509 --- /dev/null +++ b/web/src/hooks/use-lazy-ref.ts @@ -0,0 +1,13 @@ +import * as React from "react"; + +function useLazyRef(fn: () => T) { + const ref = React.useRef(null); + + if (ref.current === null) { + ref.current = fn(); + } + + return ref as React.RefObject; +} + +export { useLazyRef }; diff --git a/web/src/hooks/use-mobile.ts b/web/src/hooks/use-mobile.ts index 2b0fe1d..c444b4c 100644 --- a/web/src/hooks/use-mobile.ts +++ b/web/src/hooks/use-mobile.ts @@ -2,18 +2,18 @@ import * as React from "react" const MOBILE_BREAKPOINT = 768 -export function useIsMobile() { +export function useIsMobile(breakpoint: number = MOBILE_BREAKPOINT) { const [isMobile, setIsMobile] = React.useState(undefined) React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) + const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`) const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + setIsMobile(window.innerWidth < breakpoint) } mql.addEventListener("change", onChange) - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) + setIsMobile(window.innerWidth < breakpoint) return () => mql.removeEventListener("change", onChange) - }, []) + }, [breakpoint]) return !!isMobile }