@dowel-ui/react 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/blocks/admin-dashboard/meta.js +31 -0
- package/dist/blocks/admin-dashboard/meta.js.map +1 -0
- package/dist/blocks/agent-console/meta.js +27 -0
- package/dist/blocks/agent-console/meta.js.map +1 -0
- package/dist/blocks/ai-dashboard/meta.js +25 -0
- package/dist/blocks/ai-dashboard/meta.js.map +1 -0
- package/dist/blocks/ai-workspace/meta.js +33 -0
- package/dist/blocks/ai-workspace/meta.js.map +1 -0
- package/dist/blocks/analytics/meta.js +24 -0
- package/dist/blocks/analytics/meta.js.map +1 -0
- package/dist/blocks/billing/meta.js +25 -0
- package/dist/blocks/billing/meta.js.map +1 -0
- package/dist/blocks/command-center/meta.js +28 -0
- package/dist/blocks/command-center/meta.js.map +1 -0
- package/dist/blocks/crm/meta.js +32 -0
- package/dist/blocks/crm/meta.js.map +1 -0
- package/dist/blocks/onboarding/meta.js +23 -0
- package/dist/blocks/onboarding/meta.js.map +1 -0
- package/dist/components/accordion/accordion.js +1 -1
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/activity-feed/activity-feed.js +1 -1
- package/dist/components/activity-feed/activity-feed.js.map +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -1
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.js +91 -0
- package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
- package/dist/components/ai-extraction-review/index.d.ts +3 -0
- package/dist/components/ai-extraction-review/index.js +3 -0
- package/dist/components/ai-extraction-review/meta.js +17 -0
- package/dist/components/ai-extraction-review/meta.js.map +1 -0
- package/dist/components/ai-prompt-input/ai-prompt-input.js +1 -1
- package/dist/components/ai-prompt-input/ai-prompt-input.js.map +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js.map +1 -1
- package/dist/components/ai-response/ai-response.js +2 -2
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
- package/dist/components/ai-suggested-value/index.d.ts +2 -0
- package/dist/components/ai-suggested-value/index.js +2 -0
- package/dist/components/ai-suggested-value/meta.js +17 -0
- package/dist/components/ai-suggested-value/meta.js.map +1 -0
- package/dist/components/ai-tool/ai-tool.js +2 -2
- package/dist/components/ai-tool/ai-tool.js.map +1 -1
- package/dist/components/breadcrumb/breadcrumb.d.ts +52 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb/breadcrumb.js +141 -0
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
- package/dist/components/breadcrumb/index.d.ts +2 -0
- package/dist/components/breadcrumb/index.js +2 -0
- package/dist/components/breadcrumb/meta.js +17 -0
- package/dist/components/breadcrumb/meta.js.map +1 -0
- package/dist/components/calendar/calendar.js +2 -2
- package/dist/components/calendar/calendar.js.map +1 -1
- package/dist/components/code-block/code-block.js +1 -1
- package/dist/components/code-block/code-block.js.map +1 -1
- package/dist/components/collapsible/collapsible.d.ts +30 -0
- package/dist/components/collapsible/collapsible.d.ts.map +1 -0
- package/dist/components/collapsible/collapsible.js +38 -0
- package/dist/components/collapsible/collapsible.js.map +1 -0
- package/dist/components/collapsible/index.d.ts +2 -0
- package/dist/components/collapsible/index.js +2 -0
- package/dist/components/collapsible/meta.js +17 -0
- package/dist/components/collapsible/meta.js.map +1 -0
- package/dist/components/combobox/combobox.js +2 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/command/command.js +1 -1
- package/dist/components/command/command.js.map +1 -1
- package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
- package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
- package/dist/components/confirm-typed/confirm-typed.js +106 -0
- package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
- package/dist/components/confirm-typed/index.d.ts +2 -0
- package/dist/components/confirm-typed/index.js +2 -0
- package/dist/components/confirm-typed/meta.js +17 -0
- package/dist/components/confirm-typed/meta.js.map +1 -0
- package/dist/components/cron-editor/cron-editor.d.ts +36 -0
- package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-editor.js +493 -0
- package/dist/components/cron-editor/cron-editor.js.map +1 -0
- package/dist/components/cron-editor/cron-expression.d.ts +82 -0
- package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-expression.js +352 -0
- package/dist/components/cron-editor/cron-expression.js.map +1 -0
- package/dist/components/cron-editor/index.d.ts +3 -0
- package/dist/components/cron-editor/index.js +3 -0
- package/dist/components/cron-editor/meta.js +17 -0
- package/dist/components/cron-editor/meta.js.map +1 -0
- package/dist/components/data-table/data-table.d.ts.map +1 -1
- package/dist/components/data-table/data-table.js +4 -1
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/dialog/dialog.js +2 -2
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/diff-viewer/diff-viewer.js +4 -4
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -1
- package/dist/components/direction/direction.d.ts +35 -0
- package/dist/components/direction/direction.d.ts.map +1 -0
- package/dist/components/direction/direction.js +14 -0
- package/dist/components/direction/direction.js.map +1 -0
- package/dist/components/direction/index.d.ts +2 -0
- package/dist/components/direction/index.js +2 -0
- package/dist/components/direction/meta.js +17 -0
- package/dist/components/direction/meta.js.map +1 -0
- package/dist/components/dns-record/dns-record.d.ts +61 -0
- package/dist/components/dns-record/dns-record.d.ts.map +1 -0
- package/dist/components/dns-record/dns-record.js +227 -0
- package/dist/components/dns-record/dns-record.js.map +1 -0
- package/dist/components/dns-record/index.d.ts +2 -0
- package/dist/components/dns-record/index.js +2 -0
- package/dist/components/dns-record/meta.js +17 -0
- package/dist/components/dns-record/meta.js.map +1 -0
- package/dist/components/drawer/drawer.js +1 -1
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.js +9 -8
- package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/components/input/input.js +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/log-viewer/log-viewer.js +2 -2
- package/dist/components/log-viewer/log-viewer.js.map +1 -1
- package/dist/components/meter/meter.js +1 -1
- package/dist/components/meter/meter.js.map +1 -1
- package/dist/components/pagination/pagination.d.ts.map +1 -1
- package/dist/components/pagination/pagination.js +3 -2
- package/dist/components/pagination/pagination.js.map +1 -1
- package/dist/components/permission-matrix/index.d.ts +3 -0
- package/dist/components/permission-matrix/index.js +3 -0
- package/dist/components/permission-matrix/meta.js +17 -0
- package/dist/components/permission-matrix/meta.js.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.js +199 -0
- package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
- package/dist/components/permission-matrix/permission-model.d.ts +53 -0
- package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-model.js +64 -0
- package/dist/components/permission-matrix/permission-model.js.map +1 -0
- package/dist/components/record-diff/record-diff.js +4 -4
- package/dist/components/record-diff/record-diff.js.map +1 -1
- package/dist/components/secret-field/index.d.ts +2 -0
- package/dist/components/secret-field/index.js +2 -0
- package/dist/components/secret-field/meta.js +17 -0
- package/dist/components/secret-field/meta.js.map +1 -0
- package/dist/components/secret-field/secret-field.d.ts +58 -0
- package/dist/components/secret-field/secret-field.d.ts.map +1 -0
- package/dist/components/secret-field/secret-field.js +168 -0
- package/dist/components/secret-field/secret-field.js.map +1 -0
- package/dist/components/select/select.js +2 -2
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/session-expiry/index.d.ts +2 -0
- package/dist/components/session-expiry/index.js +2 -0
- package/dist/components/session-expiry/meta.js +17 -0
- package/dist/components/session-expiry/meta.js.map +1 -0
- package/dist/components/session-expiry/session-expiry.d.ts +64 -0
- package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
- package/dist/components/session-expiry/session-expiry.js +185 -0
- package/dist/components/session-expiry/session-expiry.js.map +1 -0
- package/dist/components/sheet/sheet.d.ts.map +1 -1
- package/dist/components/sheet/sheet.js +2 -2
- package/dist/components/sheet/sheet.js.map +1 -1
- package/dist/components/shortcut-recorder/index.d.ts +3 -0
- package/dist/components/shortcut-recorder/index.js +3 -0
- package/dist/components/shortcut-recorder/meta.js +17 -0
- package/dist/components/shortcut-recorder/meta.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
- package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/components/sidebar/index.d.ts +2 -0
- package/dist/components/sidebar/index.js +2 -0
- package/dist/components/sidebar/meta.js +17 -0
- package/dist/components/sidebar/meta.js.map +1 -0
- package/dist/components/sidebar/sidebar.d.ts +69 -0
- package/dist/components/sidebar/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar/sidebar.js +256 -0
- package/dist/components/sidebar/sidebar.js.map +1 -0
- package/dist/components/sync-status/index.d.ts +2 -0
- package/dist/components/sync-status/index.js +2 -0
- package/dist/components/sync-status/meta.js +17 -0
- package/dist/components/sync-status/meta.js.map +1 -0
- package/dist/components/sync-status/sync-status.d.ts +65 -0
- package/dist/components/sync-status/sync-status.d.ts.map +1 -0
- package/dist/components/sync-status/sync-status.js +103 -0
- package/dist/components/sync-status/sync-status.js.map +1 -0
- package/dist/components/table/table.js +2 -2
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/tags-input/tags-input.js +1 -1
- package/dist/components/tags-input/tags-input.js.map +1 -1
- package/dist/components/textarea/index.d.ts +2 -0
- package/dist/components/textarea/index.js +2 -0
- package/dist/components/textarea/meta.js +17 -0
- package/dist/components/textarea/meta.js.map +1 -0
- package/dist/components/textarea/textarea.d.ts +43 -0
- package/dist/components/textarea/textarea.d.ts.map +1 -0
- package/dist/components/textarea/textarea.js +119 -0
- package/dist/components/textarea/textarea.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +3 -3
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -1
- package/dist/components/toast/toast.d.ts.map +1 -1
- package/dist/components/toast/toast.js +2 -2
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/index.d.ts +36 -2
- package/dist/index.js +22 -3
- package/dist/lib/styles.d.ts +16 -1
- package/dist/lib/styles.d.ts.map +1 -1
- package/dist/lib/styles.js +16 -1
- package/dist/lib/styles.js.map +1 -1
- package/dist/registry/blocks.d.ts.map +1 -1
- package/dist/registry/blocks.js +27 -9
- package/dist/registry/blocks.js.map +1 -1
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +84 -54
- package/dist/registry/components.js.map +1 -1
- package/dist/registry/schema.d.ts +8 -0
- package/dist/registry/schema.d.ts.map +1 -1
- package/dist/registry/schema.js.map +1 -1
- package/package.json +5 -4
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/activity-feed/activity-feed.tsx +1 -1
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
- package/src/components/ai-extraction-review/extraction-model.ts +159 -0
- package/src/components/ai-extraction-review/index.ts +13 -0
- package/src/components/ai-extraction-review/meta.ts +24 -0
- package/src/components/ai-prompt-input/ai-prompt-input.tsx +1 -1
- package/src/components/ai-reasoning/ai-reasoning.tsx +1 -1
- package/src/components/ai-response/ai-response.tsx +2 -2
- package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
- package/src/components/ai-suggested-value/index.ts +6 -0
- package/src/components/ai-suggested-value/meta.ts +24 -0
- package/src/components/ai-tool/ai-tool.tsx +2 -2
- package/src/components/breadcrumb/breadcrumb.tsx +157 -0
- package/src/components/breadcrumb/index.ts +10 -0
- package/src/components/breadcrumb/meta.ts +19 -0
- package/src/components/calendar/calendar.tsx +2 -2
- package/src/components/code-block/code-block.tsx +1 -1
- package/src/components/collapsible/collapsible.tsx +63 -0
- package/src/components/collapsible/index.ts +6 -0
- package/src/components/collapsible/meta.ts +16 -0
- package/src/components/combobox/combobox.tsx +2 -2
- package/src/components/command/command.tsx +1 -1
- package/src/components/confirm-typed/confirm-typed.tsx +191 -0
- package/src/components/confirm-typed/index.ts +1 -0
- package/src/components/confirm-typed/meta.ts +22 -0
- package/src/components/cron-editor/cron-editor.tsx +676 -0
- package/src/components/cron-editor/cron-expression.ts +552 -0
- package/src/components/cron-editor/index.ts +23 -0
- package/src/components/cron-editor/meta.ts +23 -0
- package/src/components/data-table/data-table.tsx +14 -3
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/diff-viewer/diff-viewer.tsx +4 -4
- package/src/components/direction/direction.tsx +38 -0
- package/src/components/direction/index.ts +1 -0
- package/src/components/direction/meta.ts +19 -0
- package/src/components/dns-record/dns-record.tsx +315 -0
- package/src/components/dns-record/index.ts +7 -0
- package/src/components/dns-record/meta.ts +21 -0
- package/src/components/drawer/drawer.tsx +1 -1
- package/src/components/dropdown-menu/dropdown-menu.tsx +14 -8
- package/src/components/input/input.tsx +1 -1
- package/src/components/log-viewer/log-viewer.tsx +2 -2
- package/src/components/meter/meter.tsx +1 -1
- package/src/components/pagination/pagination.tsx +13 -2
- package/src/components/permission-matrix/index.ts +14 -0
- package/src/components/permission-matrix/meta.ts +23 -0
- package/src/components/permission-matrix/permission-matrix.tsx +368 -0
- package/src/components/permission-matrix/permission-model.ts +120 -0
- package/src/components/record-diff/record-diff.tsx +4 -4
- package/src/components/secret-field/index.ts +6 -0
- package/src/components/secret-field/meta.ts +21 -0
- package/src/components/secret-field/secret-field.tsx +284 -0
- package/src/components/select/select.tsx +2 -2
- package/src/components/session-expiry/index.ts +9 -0
- package/src/components/session-expiry/meta.ts +23 -0
- package/src/components/session-expiry/session-expiry.tsx +321 -0
- package/src/components/sheet/sheet.tsx +8 -4
- package/src/components/shortcut-recorder/index.ts +19 -0
- package/src/components/shortcut-recorder/meta.ts +22 -0
- package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
- package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
- package/src/components/sidebar/index.ts +19 -0
- package/src/components/sidebar/meta.ts +25 -0
- package/src/components/sidebar/sidebar.tsx +374 -0
- package/src/components/sync-status/index.ts +9 -0
- package/src/components/sync-status/meta.ts +21 -0
- package/src/components/sync-status/sync-status.tsx +217 -0
- package/src/components/table/table.tsx +3 -3
- package/src/components/tags-input/tags-input.tsx +1 -1
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/meta.ts +21 -0
- package/src/components/textarea/textarea.tsx +210 -0
- package/src/components/time-range-picker/time-range-picker.tsx +3 -3
- package/src/components/toast/toast.tsx +14 -6
- package/src/index.ts +15 -0
- package/src/lib/styles.ts +16 -0
- package/src/registry/blocks.ts +18 -0
- package/src/registry/components.ts +30 -0
- package/src/registry/schema.ts +8 -0
- package/src/blocks/admin-users/admin-users.tsx +0 -251
- package/src/blocks/admin-users/index.ts +0 -1
- package/src/blocks/admin-users/meta.ts +0 -27
- package/src/blocks/ai-chat/ai-chat.tsx +0 -280
- package/src/blocks/ai-chat/index.ts +0 -7
- package/src/blocks/ai-chat/meta.ts +0 -32
- package/src/blocks/dashboard/dashboard.tsx +0 -192
- package/src/blocks/dashboard/index.ts +0 -6
- package/src/blocks/dashboard/meta.ts +0 -18
- package/src/blocks/forgot-password/forgot-password.tsx +0 -158
- package/src/blocks/forgot-password/index.ts +0 -1
- package/src/blocks/forgot-password/meta.ts +0 -18
- package/src/blocks/login/index.ts +0 -1
- package/src/blocks/login/login.tsx +0 -178
- package/src/blocks/login/meta.ts +0 -18
- package/src/blocks/pricing/index.ts +0 -1
- package/src/blocks/pricing/meta.ts +0 -18
- package/src/blocks/pricing/pricing.tsx +0 -176
- package/src/blocks/settings/index.ts +0 -6
- package/src/blocks/settings/meta.ts +0 -29
- package/src/blocks/settings/settings.tsx +0 -317
- package/src/blocks/signup/index.ts +0 -1
- package/src/blocks/signup/meta.ts +0 -18
- package/src/blocks/signup/signup.tsx +0 -240
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drawer.js","names":["DrawerPrimitive"],"sources":["../../../src/components/drawer/drawer.tsx"],"sourcesContent":["\"use client\";\n\nimport { Dialog as DrawerPrimitive } from \"radix-ui\";\nimport {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type PointerEvent as ReactPointerEvent,\n} from \"react\";\n\nimport { focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A bottom sheet that can be dismissed by dragging it down.\n *\n * Drag is a pointer-only affordance. Escape, the overlay and the close button\n * are the accessible equivalents and are always present, so keyboard and screen\n * reader users are never dependent on the gesture.\n *\n * Deliberately bottom-anchored only. A drawer that enters from the side with no\n * gesture is a Sheet, and having two components that differ by nothing but a\n * name is worse than having one. Snap points are not supported in this version.\n */\n\ninterface DrawerContextValue {\n close: () => void;\n}\n\nconst DrawerContext = createContext<DrawerContextValue | null>(null);\n\nfunction useDrawerContext(component: string): DrawerContextValue {\n const context = useContext(DrawerContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <Drawer>.`);\n }\n return context;\n}\n\nexport interface DrawerProps extends Omit<\n ComponentPropsWithRef<typeof DrawerPrimitive.Root>,\n \"onOpenChange\"\n> {\n /**\n * Redeclared as a property rather than inherited as a method signature. The\n * inherited shorthand reads as a method, which makes destructuring it look\n * like an unbound `this` hazard to static analysis; a plain function property\n * says what this actually is.\n */\n onOpenChange?: (open: boolean) => void;\n}\n\nexport function Drawer({ open, defaultOpen, onOpenChange, children, ...props }: DrawerProps) {\n // State is owned here (while still supporting the controlled form) because\n // dismissing by drag has to close the drawer, and the primitive exposes no\n // imperative close.\n const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen ?? false);\n const isControlled = open !== undefined;\n const isOpen = isControlled ? open : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n const contextValue = useMemo<DrawerContextValue>(\n () => ({ close: () => setOpen(false) }),\n [setOpen],\n );\n\n return (\n <DrawerPrimitive.Root open={isOpen} onOpenChange={setOpen} {...props}>\n <DrawerContext.Provider value={contextValue}>{children}</DrawerContext.Provider>\n </DrawerPrimitive.Root>\n );\n}\n\nexport const DrawerTrigger = DrawerPrimitive.Trigger;\nexport const DrawerPortal = DrawerPrimitive.Portal;\nexport const DrawerClose = DrawerPrimitive.Close;\n\nexport function DrawerOverlay({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Overlay>) {\n return (\n <DrawerPrimitive.Overlay\n data-slot=\"drawer-overlay\"\n className={cn(\n \"fixed inset-0 z-[var(--z-overlay)] bg-overlay\",\n \"data-[state=closed]:animate-overlay-out data-[state=open]:animate-overlay-in\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Fraction of the drawer's height past which release dismisses it. */\nconst DISMISS_DISTANCE_RATIO = 0.25;\n/** Downward speed (px/ms) that dismisses a short but deliberate flick. */\nconst DISMISS_VELOCITY = 0.5;\n/**\n * Travel below which nothing dismisses, however fast.\n *\n * A tap on the handle registers as a few pixels of movement over a couple of\n * milliseconds, which is an enormous velocity. Without a floor, touching the\n * drawer would close it.\n */\nconst MIN_FLICK_DISTANCE = 24;\n\nexport interface DrawerContentProps extends ComponentPropsWithRef<\n typeof DrawerPrimitive.Content\n> {\n /** Shows the grab handle. Turn off only if the drawer cannot be dragged. */\n showHandle?: boolean;\n}\n\nexport function DrawerContent({\n className,\n children,\n showHandle = true,\n ...props\n}: DrawerContentProps) {\n const { close } = useDrawerContext(\"DrawerContent\");\n const contentRef = useRef<HTMLDivElement | null>(null);\n const gesture = useRef<{ startY: number; startTime: number } | null>(null);\n const [offset, setOffset] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n const handlePointerDown = (event: ReactPointerEvent<HTMLElement>) => {\n if (event.button !== 0) return;\n gesture.current = { startY: event.clientY, startTime: event.timeStamp };\n setIsDragging(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handlePointerMove = (event: ReactPointerEvent<HTMLElement>) => {\n if (!gesture.current) return;\n // Downward only: dragging up must not detach the drawer from the edge.\n setOffset(Math.max(0, event.clientY - gesture.current.startY));\n };\n\n const handlePointerUp = (event: ReactPointerEvent<HTMLElement>) => {\n if (!gesture.current) return;\n\n const distance = Math.max(0, event.clientY - gesture.current.startY);\n const elapsed = Math.max(1, event.timeStamp - gesture.current.startTime);\n const velocity = distance / elapsed;\n const height = contentRef.current?.getBoundingClientRect().height ?? 0;\n\n gesture.current = null;\n setIsDragging(false);\n setOffset(0);\n\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n\n const draggedPastThreshold = height > 0 && distance > height * DISMISS_DISTANCE_RATIO;\n const flicked = distance >= MIN_FLICK_DISTANCE && velocity > DISMISS_VELOCITY;\n\n if (draggedPastThreshold || flicked) {\n close();\n }\n };\n\n const dragHandlers = {\n onPointerDown: handlePointerDown,\n onPointerMove: handlePointerMove,\n onPointerUp: handlePointerUp,\n onPointerCancel: handlePointerUp,\n };\n\n return (\n <DrawerPortal>\n <DrawerOverlay />\n <DrawerPrimitive.Content\n ref={contentRef}\n data-slot=\"drawer-content\"\n data-dragging={isDragging || undefined}\n style={offset > 0 ? { transform: `translate3d(0, ${String(offset)}px, 0)` } : undefined}\n className={cn(\n \"fixed inset-x-0 bottom-0 z-[var(--z-drawer)] flex max-h-[92svh] flex-col\",\n \"rounded-t-2xl border-t border-border bg-card text-card-foreground shadow-xl\",\n \"[--slide-y:100%]\",\n \"data-[state=closed]:animate-slide-out data-[state=open]:animate-slide-in\",\n // While a finger is down the transform is driven directly; afterwards\n // it springs back. Suppressing the entry animation mid-drag stops the\n // two from fighting over the same property.\n \"data-[dragging]:animate-none data-[dragging]:transition-none\",\n \"transition-transform duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]\",\n className,\n )}\n {...props}\n >\n {showHandle ? (\n <div\n data-slot=\"drawer-handle\"\n aria-hidden=\"true\"\n className=\"flex shrink-0 cursor-grab touch-none justify-center pt-3 pb-1 active:cursor-grabbing\"\n {...dragHandlers}\n >\n <div className=\"h-1.5 w-10 rounded-full bg-border-strong\" />\n </div>\n ) : null}\n {children}\n </DrawerPrimitive.Content>\n </DrawerPortal>\n );\n}\n\nexport function DrawerHeader({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"drawer-header\"\n className={cn(\"flex shrink-0 flex-col gap-1.5 px-6 pt-4 pb-2 text-left\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerBody({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"drawer-body\"\n className={cn(\"overflow-y-auto overscroll-contain px-6 py-2\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerFooter({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"drawer-footer\"\n className={cn(\"flex shrink-0 flex-col gap-2 px-6 pt-2 pb-6\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerTitle({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Title>) {\n return (\n <DrawerPrimitive.Title\n data-slot=\"drawer-title\"\n className={cn(\"text-lg leading-tight font-semibold tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerDescription({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Description>) {\n return (\n <DrawerPrimitive.Description\n data-slot=\"drawer-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\n/** Close control for the drawer footer, styled as a full-width secondary action. */\nexport function DrawerCancel({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Close>) {\n return (\n <DrawerPrimitive.Close\n data-slot=\"drawer-cancel\"\n className={cn(\n \"inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-5 text-sm font-medium\",\n \"transition-colors duration-[var(--duration-fast)] hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n className,\n )}\n {...props}\n />\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drawer.js","names":["DrawerPrimitive"],"sources":["../../../src/components/drawer/drawer.tsx"],"sourcesContent":["\"use client\";\n\nimport { Dialog as DrawerPrimitive } from \"radix-ui\";\nimport {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type PointerEvent as ReactPointerEvent,\n} from \"react\";\n\nimport { focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A bottom sheet that can be dismissed by dragging it down.\n *\n * Drag is a pointer-only affordance. Escape, the overlay and the close button\n * are the accessible equivalents and are always present, so keyboard and screen\n * reader users are never dependent on the gesture.\n *\n * Deliberately bottom-anchored only. A drawer that enters from the side with no\n * gesture is a Sheet, and having two components that differ by nothing but a\n * name is worse than having one. Snap points are not supported in this version.\n */\n\ninterface DrawerContextValue {\n close: () => void;\n}\n\nconst DrawerContext = createContext<DrawerContextValue | null>(null);\n\nfunction useDrawerContext(component: string): DrawerContextValue {\n const context = useContext(DrawerContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <Drawer>.`);\n }\n return context;\n}\n\nexport interface DrawerProps extends Omit<\n ComponentPropsWithRef<typeof DrawerPrimitive.Root>,\n \"onOpenChange\"\n> {\n /**\n * Redeclared as a property rather than inherited as a method signature. The\n * inherited shorthand reads as a method, which makes destructuring it look\n * like an unbound `this` hazard to static analysis; a plain function property\n * says what this actually is.\n */\n onOpenChange?: (open: boolean) => void;\n}\n\nexport function Drawer({ open, defaultOpen, onOpenChange, children, ...props }: DrawerProps) {\n // State is owned here (while still supporting the controlled form) because\n // dismissing by drag has to close the drawer, and the primitive exposes no\n // imperative close.\n const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen ?? false);\n const isControlled = open !== undefined;\n const isOpen = isControlled ? open : uncontrolledOpen;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!isControlled) setUncontrolledOpen(next);\n onOpenChange?.(next);\n },\n [isControlled, onOpenChange],\n );\n\n const contextValue = useMemo<DrawerContextValue>(\n () => ({ close: () => setOpen(false) }),\n [setOpen],\n );\n\n return (\n <DrawerPrimitive.Root open={isOpen} onOpenChange={setOpen} {...props}>\n <DrawerContext.Provider value={contextValue}>{children}</DrawerContext.Provider>\n </DrawerPrimitive.Root>\n );\n}\n\nexport const DrawerTrigger = DrawerPrimitive.Trigger;\nexport const DrawerPortal = DrawerPrimitive.Portal;\nexport const DrawerClose = DrawerPrimitive.Close;\n\nexport function DrawerOverlay({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Overlay>) {\n return (\n <DrawerPrimitive.Overlay\n data-slot=\"drawer-overlay\"\n className={cn(\n \"fixed inset-0 z-[var(--z-overlay)] bg-overlay\",\n \"data-[state=closed]:animate-overlay-out data-[state=open]:animate-overlay-in\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Fraction of the drawer's height past which release dismisses it. */\nconst DISMISS_DISTANCE_RATIO = 0.25;\n/** Downward speed (px/ms) that dismisses a short but deliberate flick. */\nconst DISMISS_VELOCITY = 0.5;\n/**\n * Travel below which nothing dismisses, however fast.\n *\n * A tap on the handle registers as a few pixels of movement over a couple of\n * milliseconds, which is an enormous velocity. Without a floor, touching the\n * drawer would close it.\n */\nconst MIN_FLICK_DISTANCE = 24;\n\nexport interface DrawerContentProps extends ComponentPropsWithRef<\n typeof DrawerPrimitive.Content\n> {\n /** Shows the grab handle. Turn off only if the drawer cannot be dragged. */\n showHandle?: boolean;\n}\n\nexport function DrawerContent({\n className,\n children,\n showHandle = true,\n ...props\n}: DrawerContentProps) {\n const { close } = useDrawerContext(\"DrawerContent\");\n const contentRef = useRef<HTMLDivElement | null>(null);\n const gesture = useRef<{ startY: number; startTime: number } | null>(null);\n const [offset, setOffset] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n const handlePointerDown = (event: ReactPointerEvent<HTMLElement>) => {\n if (event.button !== 0) return;\n gesture.current = { startY: event.clientY, startTime: event.timeStamp };\n setIsDragging(true);\n event.currentTarget.setPointerCapture(event.pointerId);\n };\n\n const handlePointerMove = (event: ReactPointerEvent<HTMLElement>) => {\n if (!gesture.current) return;\n // Downward only: dragging up must not detach the drawer from the edge.\n setOffset(Math.max(0, event.clientY - gesture.current.startY));\n };\n\n const handlePointerUp = (event: ReactPointerEvent<HTMLElement>) => {\n if (!gesture.current) return;\n\n const distance = Math.max(0, event.clientY - gesture.current.startY);\n const elapsed = Math.max(1, event.timeStamp - gesture.current.startTime);\n const velocity = distance / elapsed;\n const height = contentRef.current?.getBoundingClientRect().height ?? 0;\n\n gesture.current = null;\n setIsDragging(false);\n setOffset(0);\n\n if (event.currentTarget.hasPointerCapture(event.pointerId)) {\n event.currentTarget.releasePointerCapture(event.pointerId);\n }\n\n const draggedPastThreshold = height > 0 && distance > height * DISMISS_DISTANCE_RATIO;\n const flicked = distance >= MIN_FLICK_DISTANCE && velocity > DISMISS_VELOCITY;\n\n if (draggedPastThreshold || flicked) {\n close();\n }\n };\n\n const dragHandlers = {\n onPointerDown: handlePointerDown,\n onPointerMove: handlePointerMove,\n onPointerUp: handlePointerUp,\n onPointerCancel: handlePointerUp,\n };\n\n return (\n <DrawerPortal>\n <DrawerOverlay />\n <DrawerPrimitive.Content\n ref={contentRef}\n data-slot=\"drawer-content\"\n data-dragging={isDragging || undefined}\n style={offset > 0 ? { transform: `translate3d(0, ${String(offset)}px, 0)` } : undefined}\n className={cn(\n \"fixed inset-x-0 bottom-0 z-[var(--z-drawer)] flex max-h-[92svh] flex-col\",\n \"rounded-t-2xl border-t border-border bg-card text-card-foreground shadow-xl\",\n \"[--slide-y:100%]\",\n \"data-[state=closed]:animate-slide-out data-[state=open]:animate-slide-in\",\n // While a finger is down the transform is driven directly; afterwards\n // it springs back. Suppressing the entry animation mid-drag stops the\n // two from fighting over the same property.\n \"data-[dragging]:animate-none data-[dragging]:transition-none\",\n \"transition-transform duration-[var(--duration-normal)] ease-[var(--ease-out-quint)]\",\n className,\n )}\n {...props}\n >\n {showHandle ? (\n <div\n data-slot=\"drawer-handle\"\n aria-hidden=\"true\"\n className=\"flex shrink-0 cursor-grab touch-none justify-center pt-3 pb-1 active:cursor-grabbing\"\n {...dragHandlers}\n >\n <div className=\"h-1.5 w-10 rounded-full bg-border-strong\" />\n </div>\n ) : null}\n {children}\n </DrawerPrimitive.Content>\n </DrawerPortal>\n );\n}\n\nexport function DrawerHeader({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"drawer-header\"\n className={cn(\"flex shrink-0 flex-col gap-1.5 px-6 pt-4 pb-2 text-start\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerBody({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"drawer-body\"\n className={cn(\"overflow-y-auto overscroll-contain px-6 py-2\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerFooter({ className, ...props }: ComponentPropsWithRef<\"div\">) {\n return (\n <div\n data-slot=\"drawer-footer\"\n className={cn(\"flex shrink-0 flex-col gap-2 px-6 pt-2 pb-6\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerTitle({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Title>) {\n return (\n <DrawerPrimitive.Title\n data-slot=\"drawer-title\"\n className={cn(\"text-lg leading-tight font-semibold tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nexport function DrawerDescription({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Description>) {\n return (\n <DrawerPrimitive.Description\n data-slot=\"drawer-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\n/** Close control for the drawer footer, styled as a full-width secondary action. */\nexport function DrawerCancel({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DrawerPrimitive.Close>) {\n return (\n <DrawerPrimitive.Close\n data-slot=\"drawer-cancel\"\n className={cn(\n \"inline-flex h-10 items-center justify-center rounded-md border border-input bg-background px-5 text-sm font-medium\",\n \"transition-colors duration-[var(--duration-fast)] hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n className,\n )}\n {...props}\n />\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown-menu.d.ts","names":[],"sources":["../../../src/components/dropdown-menu/dropdown-menu.tsx"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"dropdown-menu.d.ts","names":[],"sources":["../../../src/components/dropdown-menu/dropdown-menu.tsx"],"mappings":";;;;;;;;;;cAea,gCAAY,GAAA,aAAA;cACZ,qCAAmB,0BAAA,aAAA,2CAAA,cAAA;cACnB,mCAAiB,0BAAA,aAAA,yCAAA,cAAA;cACjB,oCAAkB,GAAA,aAAA;cAClB,iCAAe,GAAA,aAAA;cACf,wCAAsB,0BAAA,aAAA,8CAAA,cAAA;KAqBvB,2BAA2B,6BAC9B,aAAsB;iBAGf,sBACd,WACA,eACG,SACF,2CAAwB,IAAA;UAaV,8BAA8B,6BACtC,aAAsB;;EAG7B;;EAEA;;iBAGc,mBACd,WACA,SACA,UACG,SACF,wCAAqB,IAAA;iBAyCR,2BACd,WACA,aACG,SACF,6BAA6B,aAAsB,gCAAa,IAAA;iBAanD,wBACd,WACA,aACG,SACF,6BAA6B,aAAsB,6BAAU,IAAA;UAa/C,+BAA+B,6BACvC,aAAsB;EAE7B;;iBAGc,oBAAoB,WAAW,UAAU,SAAS,yCAAsB,IAAA;iBAcxE,wBACd,cACG,SACF,6BAA6B,aAAsB,6BAAU,IAAA;;;iBAYhD,uBAAuB,cAAc,SAAS,gDAA6B,IAAA;UAW1E,oCAAoC,6BAC5C,aAAsB;EAE7B;;iBAGc,yBACd,WACA,OACA,aACG,SACF,8CAA2B,IAAA;iBA0Bd,yBACd,cACG,SACF,6BAA6B,aAAsB,8BAAW,IAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { mirrorForDirection } from "../../lib/styles.js";
|
|
2
3
|
import { cn } from "../../lib/utils.js";
|
|
3
4
|
import { DropdownMenu } from "radix-ui";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -33,7 +34,7 @@ function DropdownMenuItem({ className, variant = "default", inset, ...props }) {
|
|
|
33
34
|
return /* @__PURE__ */ jsx(DropdownMenu.Item, {
|
|
34
35
|
"data-slot": "dropdown-menu-item",
|
|
35
36
|
"data-variant": variant,
|
|
36
|
-
className: cn(menuItem, inset && "
|
|
37
|
+
className: cn(menuItem, inset && "ps-8", variant === "destructive" && "text-destructive data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive", className),
|
|
37
38
|
...props
|
|
38
39
|
});
|
|
39
40
|
}
|
|
@@ -41,7 +42,7 @@ function DropdownMenuItem({ className, variant = "default", inset, ...props }) {
|
|
|
41
42
|
* aria-checked, so announcing it twice would be noise. */
|
|
42
43
|
function ItemIndicator({ radio = false }) {
|
|
43
44
|
return /* @__PURE__ */ jsx("span", {
|
|
44
|
-
className: "pointer-events-none absolute
|
|
45
|
+
className: "pointer-events-none absolute start-2 grid size-4 place-items-center",
|
|
45
46
|
children: /* @__PURE__ */ jsx(DropdownMenu.ItemIndicator, { children: radio ? /* @__PURE__ */ jsx("span", { className: "size-2 rounded-full bg-current" }) : /* @__PURE__ */ jsx("svg", {
|
|
46
47
|
viewBox: "0 0 24 24",
|
|
47
48
|
fill: "none",
|
|
@@ -60,7 +61,7 @@ function ItemIndicator({ radio = false }) {
|
|
|
60
61
|
function DropdownMenuCheckboxItem({ className, children, ...props }) {
|
|
61
62
|
return /* @__PURE__ */ jsxs(DropdownMenu.CheckboxItem, {
|
|
62
63
|
"data-slot": "dropdown-menu-checkbox-item",
|
|
63
|
-
className: cn(menuItem, "
|
|
64
|
+
className: cn(menuItem, "ps-8", className),
|
|
64
65
|
...props,
|
|
65
66
|
children: [/* @__PURE__ */ jsx(ItemIndicator, {}), children]
|
|
66
67
|
});
|
|
@@ -68,7 +69,7 @@ function DropdownMenuCheckboxItem({ className, children, ...props }) {
|
|
|
68
69
|
function DropdownMenuRadioItem({ className, children, ...props }) {
|
|
69
70
|
return /* @__PURE__ */ jsxs(DropdownMenu.RadioItem, {
|
|
70
71
|
"data-slot": "dropdown-menu-radio-item",
|
|
71
|
-
className: cn(menuItem, "
|
|
72
|
+
className: cn(menuItem, "ps-8", className),
|
|
72
73
|
...props,
|
|
73
74
|
children: [/* @__PURE__ */ jsx(ItemIndicator, { radio: true }), children]
|
|
74
75
|
});
|
|
@@ -76,7 +77,7 @@ function DropdownMenuRadioItem({ className, children, ...props }) {
|
|
|
76
77
|
function DropdownMenuLabel({ className, inset, ...props }) {
|
|
77
78
|
return /* @__PURE__ */ jsx(DropdownMenu.Label, {
|
|
78
79
|
"data-slot": "dropdown-menu-label",
|
|
79
|
-
className: cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", inset && "
|
|
80
|
+
className: cn("px-2 py-1.5 text-xs font-medium text-muted-foreground", inset && "ps-8", className),
|
|
80
81
|
...props
|
|
81
82
|
});
|
|
82
83
|
}
|
|
@@ -93,20 +94,20 @@ function DropdownMenuShortcut({ className, ...props }) {
|
|
|
93
94
|
return /* @__PURE__ */ jsx("span", {
|
|
94
95
|
"data-slot": "dropdown-menu-shortcut",
|
|
95
96
|
"aria-hidden": "true",
|
|
96
|
-
className: cn("
|
|
97
|
+
className: cn("ms-auto text-2xs tracking-wide text-muted-foreground", className),
|
|
97
98
|
...props
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
|
|
101
102
|
return /* @__PURE__ */ jsxs(DropdownMenu.SubTrigger, {
|
|
102
103
|
"data-slot": "dropdown-menu-sub-trigger",
|
|
103
|
-
className: cn(menuItem, "data-[state=open]:bg-accent", inset && "
|
|
104
|
+
className: cn(menuItem, "data-[state=open]:bg-accent", inset && "ps-8", className),
|
|
104
105
|
...props,
|
|
105
106
|
children: [children, /* @__PURE__ */ jsx("svg", {
|
|
106
107
|
viewBox: "0 0 24 24",
|
|
107
108
|
fill: "none",
|
|
108
109
|
"aria-hidden": "true",
|
|
109
|
-
className: "
|
|
110
|
+
className: cn("ms-auto size-3.5", mirrorForDirection),
|
|
110
111
|
children: /* @__PURE__ */ jsx("path", {
|
|
111
112
|
d: "m9 18 6-6-6-6",
|
|
112
113
|
stroke: "currentColor",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dropdown-menu.js","names":["DropdownMenu","DropdownMenuPrimitive"],"sources":["../../../src/components/dropdown-menu/dropdown-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A menu of actions revealed from a trigger.\n *\n * Actions, not navigation and not form state: a menu item does something. For a\n * list of links use a nav, and for a value the user picks from use Select\n * (Phase 3) — the roles differ and assistive technology treats them differently.\n */\nexport const DropdownMenu = DropdownMenuPrimitive.Root;\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nexport const DropdownMenuPortal = DropdownMenuPrimitive.Portal;\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\n/** Shared surface styling for the root menu and every submenu. */\nconst menuSurface = cn(\n \"z-[var(--z-popover)] min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-lg border border-border\",\n \"bg-popover p-1 text-popover-foreground shadow-lg outline-none\",\n \"max-h-[var(--radix-dropdown-menu-content-available-height)]\",\n \"origin-[var(--radix-dropdown-menu-content-transform-origin)]\",\n \"data-[state=closed]:animate-float-out data-[state=open]:animate-float-in\",\n);\n\n/** Shared item styling. Highlight follows data-highlighted, which the primitive\n * drives from both pointer and keyboard, so the two never disagree. */\nconst menuItem = cn(\n \"relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none select-none\",\n \"transition-colors duration-[var(--duration-instant)]\",\n \"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-55\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n);\n\nexport type DropdownMenuContentProps = ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.Content\n>;\n\nexport function DropdownMenuContent({\n className,\n sideOffset = 6,\n ...props\n}: DropdownMenuContentProps) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(menuSurface, className)}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n}\n\nexport interface DropdownMenuItemProps extends ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.Item\n> {\n /** Styles the item as destructive. Pair with a label that says what is destroyed. */\n variant?: \"default\" | \"destructive\";\n /** Indents the item to align with items that have a leading indicator. */\n inset?: boolean;\n}\n\nexport function DropdownMenuItem({\n className,\n variant = \"default\",\n inset,\n ...props\n}: DropdownMenuItemProps) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-variant={variant}\n className={cn(\n menuItem,\n inset && \"pl-8\",\n variant === \"destructive\" &&\n \"text-destructive data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Tick shown by checkbox and radio items. Decorative: the state is already in\n * aria-checked, so announcing it twice would be noise. */\nfunction ItemIndicator({ radio = false }: { radio?: boolean }) {\n return (\n <span className=\"pointer-events-none absolute left-2 grid size-4 place-items-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n {radio ? (\n <span className=\"size-2 rounded-full bg-current\" />\n ) : (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <path\n d=\"m5 13 4 4L19 7\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n );\n}\n\nexport function DropdownMenuCheckboxItem({\n className,\n children,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n className={cn(menuItem, \"pl-8\", className)}\n {...props}\n >\n <ItemIndicator />\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n}\n\nexport function DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(menuItem, \"pl-8\", className)}\n {...props}\n >\n <ItemIndicator radio />\n {children}\n </DropdownMenuPrimitive.RadioItem>\n );\n}\n\nexport interface DropdownMenuLabelProps extends ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.Label\n> {\n inset?: boolean;\n}\n\nexport function DropdownMenuLabel({ className, inset, ...props }: DropdownMenuLabelProps) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n className={cn(\n \"px-2 py-1.5 text-xs font-medium text-muted-foreground\",\n inset && \"pl-8\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuSeparator({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n );\n}\n\n/** Keyboard hint. aria-hidden because the shortcut is decoration here — bind it\n * for real at the application level. */\nexport function DropdownMenuShortcut({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n aria-hidden=\"true\"\n className={cn(\"ml-auto text-2xs tracking-wide text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport interface DropdownMenuSubTriggerProps extends ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.SubTrigger\n> {\n inset?: boolean;\n}\n\nexport function DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: DropdownMenuSubTriggerProps) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n className={cn(menuItem, \"data-[state=open]:bg-accent\", inset && \"pl-8\", className)}\n {...props}\n >\n {children}\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"ml-auto size-3.5\">\n <path\n d=\"m9 18 6-6-6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </DropdownMenuPrimitive.SubTrigger>\n );\n}\n\nexport function DropdownMenuSubContent({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(menuSurface, className)}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dropdown-menu.js","names":["DropdownMenu","DropdownMenuPrimitive"],"sources":["../../../src/components/dropdown-menu/dropdown-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { mirrorForDirection } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A menu of actions revealed from a trigger.\n *\n * Actions, not navigation and not form state: a menu item does something. For a\n * list of links use a nav, and for a value the user picks from use Select\n * (Phase 3) — the roles differ and assistive technology treats them differently.\n */\nexport const DropdownMenu = DropdownMenuPrimitive.Root;\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nexport const DropdownMenuPortal = DropdownMenuPrimitive.Portal;\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\n/** Shared surface styling for the root menu and every submenu. */\nconst menuSurface = cn(\n \"z-[var(--z-popover)] min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-lg border border-border\",\n \"bg-popover p-1 text-popover-foreground shadow-lg outline-none\",\n \"max-h-[var(--radix-dropdown-menu-content-available-height)]\",\n \"origin-[var(--radix-dropdown-menu-content-transform-origin)]\",\n \"data-[state=closed]:animate-float-out data-[state=open]:animate-float-in\",\n);\n\n/** Shared item styling. Highlight follows data-highlighted, which the primitive\n * drives from both pointer and keyboard, so the two never disagree. */\nconst menuItem = cn(\n \"relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none select-none\",\n \"transition-colors duration-[var(--duration-instant)]\",\n \"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-55\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n);\n\nexport type DropdownMenuContentProps = ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.Content\n>;\n\nexport function DropdownMenuContent({\n className,\n sideOffset = 6,\n ...props\n}: DropdownMenuContentProps) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(menuSurface, className)}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n}\n\nexport interface DropdownMenuItemProps extends ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.Item\n> {\n /** Styles the item as destructive. Pair with a label that says what is destroyed. */\n variant?: \"default\" | \"destructive\";\n /** Indents the item to align with items that have a leading indicator. */\n inset?: boolean;\n}\n\nexport function DropdownMenuItem({\n className,\n variant = \"default\",\n inset,\n ...props\n}: DropdownMenuItemProps) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-variant={variant}\n className={cn(\n menuItem,\n inset && \"ps-8\",\n variant === \"destructive\" &&\n \"text-destructive data-[highlighted]:bg-destructive/10 data-[highlighted]:text-destructive\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Tick shown by checkbox and radio items. Decorative: the state is already in\n * aria-checked, so announcing it twice would be noise. */\nfunction ItemIndicator({ radio = false }: { radio?: boolean }) {\n return (\n <span className=\"pointer-events-none absolute start-2 grid size-4 place-items-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n {radio ? (\n <span className=\"size-2 rounded-full bg-current\" />\n ) : (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" className=\"size-3.5\">\n <path\n d=\"m5 13 4 4L19 7\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n );\n}\n\nexport function DropdownMenuCheckboxItem({\n className,\n children,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n className={cn(menuItem, \"ps-8\", className)}\n {...props}\n >\n <ItemIndicator />\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n}\n\nexport function DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(menuItem, \"ps-8\", className)}\n {...props}\n >\n <ItemIndicator radio />\n {children}\n </DropdownMenuPrimitive.RadioItem>\n );\n}\n\nexport interface DropdownMenuLabelProps extends ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.Label\n> {\n inset?: boolean;\n}\n\nexport function DropdownMenuLabel({ className, inset, ...props }: DropdownMenuLabelProps) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n className={cn(\n \"px-2 py-1.5 text-xs font-medium text-muted-foreground\",\n inset && \"ps-8\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function DropdownMenuSeparator({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n {...props}\n />\n );\n}\n\n/** Keyboard hint. aria-hidden because the shortcut is decoration here — bind it\n * for real at the application level. */\nexport function DropdownMenuShortcut({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n aria-hidden=\"true\"\n className={cn(\"ms-auto text-2xs tracking-wide text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport interface DropdownMenuSubTriggerProps extends ComponentPropsWithRef<\n typeof DropdownMenuPrimitive.SubTrigger\n> {\n inset?: boolean;\n}\n\nexport function DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: DropdownMenuSubTriggerProps) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n className={cn(menuItem, \"data-[state=open]:bg-accent\", inset && \"ps-8\", className)}\n {...props}\n >\n {children}\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\"ms-auto size-3.5\", mirrorForDirection)}\n >\n <path\n d=\"m9 18 6-6-6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </DropdownMenuPrimitive.SubTrigger>\n );\n}\n\nexport function DropdownMenuSubContent({\n className,\n ...props\n}: ComponentPropsWithRef<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(menuSurface, className)}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
//#region src/components/input/input.tsx
|
|
6
|
-
const inputVariants = cva(cn("flex w-full min-w-0 rounded-md border border-input bg-background text-foreground shadow-xs", "transition-[border-color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-out-quint)]", "placeholder:text-muted-foreground", "file:
|
|
6
|
+
const inputVariants = cva(cn("flex w-full min-w-0 rounded-md border border-input bg-background text-foreground shadow-xs", "transition-[border-color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-out-quint)]", "placeholder:text-muted-foreground", "file:me-3 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground", "focus-visible:border-ring", "disabled:cursor-not-allowed disabled:opacity-55", focusRingInset, invalidStyles), {
|
|
7
7
|
variants: { inputSize: {
|
|
8
8
|
sm: "h-8 px-2.5 text-sm",
|
|
9
9
|
md: "h-9 px-3 text-sm",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { focusRingInset, invalidStyles } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nconst inputVariants = cva(\n cn(\n \"flex w-full min-w-0 rounded-md border border-input bg-background text-foreground shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-out-quint)]\",\n \"placeholder:text-muted-foreground\",\n \"file:
|
|
1
|
+
{"version":3,"file":"input.js","names":[],"sources":["../../../src/components/input/input.tsx"],"sourcesContent":["import { cva, type VariantProps } from \"class-variance-authority\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { focusRingInset, invalidStyles } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nconst inputVariants = cva(\n cn(\n \"flex w-full min-w-0 rounded-md border border-input bg-background text-foreground shadow-xs\",\n \"transition-[border-color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-out-quint)]\",\n \"placeholder:text-muted-foreground\",\n \"file:me-3 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground\",\n \"focus-visible:border-ring\",\n \"disabled:cursor-not-allowed disabled:opacity-55\",\n focusRingInset,\n invalidStyles,\n ),\n {\n variants: {\n inputSize: {\n sm: \"h-8 px-2.5 text-sm\",\n md: \"h-9 px-3 text-sm\",\n lg: \"h-10 px-3.5 text-base\",\n },\n },\n defaultVariants: {\n inputSize: \"md\",\n },\n },\n);\n\nexport interface InputProps\n extends ComponentPropsWithRef<\"input\">, VariantProps<typeof inputVariants> {}\n\n/**\n * Single-line text field.\n *\n * The visual size prop is named `inputSize` rather than `size` because `size`\n * is a native `<input>` attribute with unrelated semantics (a character-count\n * width hint). Naming it apart keeps both available instead of shadowing one.\n *\n * Error styling is driven by the native `aria-invalid` attribute rather than a\n * bespoke prop, so form libraries wire it up without an adapter.\n */\nexport function Input({ className, inputSize, type = \"text\", ...props }: InputProps) {\n return (\n <input type={type} className={cn(inputVariants({ inputSize }), className)} {...props} />\n );\n}\n\nexport { inputVariants };\n"],"mappings":";;;;;AAMA,MAAM,gBAAgB,IACpB,GACE,8FACA,qGACA,qCACA,kGACA,6BACA,mDACA,gBACA,aACF,GACA;CACE,UAAU,EACR,WAAW;EACT,IAAI;EACJ,IAAI;EACJ,IAAI;CACN,EACF;CACA,iBAAiB,EACf,WAAW,KACb;AACF,CACF;;;;;;;;;;;AAeA,SAAgB,MAAM,EAAE,WAAW,WAAW,OAAO,QAAQ,GAAG,SAAqB;CACnF,OACE,oBAAC,SAAD;EAAa;EAAM,WAAW,GAAG,cAAc,EAAE,UAAU,CAAC,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AAE3F"}
|
|
@@ -134,7 +134,7 @@ function LogViewer({ className, lines, label, rowHeight = 22, height = 360, anno
|
|
|
134
134
|
"data-index": item.index,
|
|
135
135
|
"data-slot": "log-viewer-row",
|
|
136
136
|
"data-level": line.level,
|
|
137
|
-
className: "absolute
|
|
137
|
+
className: "absolute start-0 top-0 w-full",
|
|
138
138
|
style: { transform: `translateY(${String(item.start)}px)` },
|
|
139
139
|
children: /* @__PURE__ */ jsx(LogViewerRow, {
|
|
140
140
|
line,
|
|
@@ -192,7 +192,7 @@ function LogViewerRow({ line, expanded = false, onToggle }) {
|
|
|
192
192
|
]
|
|
193
193
|
}), expanded && line.fields ? /* @__PURE__ */ jsx("dl", {
|
|
194
194
|
"data-slot": "log-viewer-fields",
|
|
195
|
-
className: "mt-1 mb-1
|
|
195
|
+
className: "ms-4 mt-1 mb-1 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-s border-border ps-3",
|
|
196
196
|
children: Object.entries(line.fields).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
197
197
|
className: "contents",
|
|
198
198
|
children: [/* @__PURE__ */ jsx("dt", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-viewer.js","names":[],"sources":["../../../src/components/log-viewer/log-viewer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport { LOG_LEVELS, segment, type LogLevel, type VisibleLine } from \"./log-stream\";\n\n/**\n * A streaming console: filter, follow, expand.\n *\n * The incumbent is react-lazylog, which was last published in 2022, is built on\n * react-virtualized, and cannot run on React 19 — while still taking about\n * 15,000 downloads a week. This is for those people.\n *\n * Two accessibility decisions worth stating rather than discovering.\n *\n * `role=\"log\"` implies `aria-live=\"polite\"`, which is right for a handful of\n * events and catastrophic for a console: a screen reader would read every line\n * of a build as it scrolls past, and nothing else would be audible. Announcing\n * is therefore off by default and opt-in through `announce`. The role stays,\n * because it still describes what the region is.\n *\n * And virtualization means most rows are not in the DOM. That is a real trade,\n * not an implementation detail: assistive technology cannot reach what is not\n * rendered, so a reader who needs the whole log needs an escape — which is what\n * `onDownload` is for. A log viewer with no way out of the virtual window is\n * not accessible however good its ARIA is.\n */\n\nconst LEVEL_STYLES: Record<LogLevel, string> = {\n trace: \"text-muted-foreground\",\n debug: \"text-muted-foreground\",\n info: \"text-foreground\",\n warn: \"text-warning\",\n error: \"text-destructive\",\n fatal: \"text-destructive font-semibold\",\n};\n\n/** Distance from the bottom, in pixels, that still counts as \"at the end\". */\nconst FOLLOW_THRESHOLD = 24;\n\nexport interface LogViewerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n lines: VisibleLine[];\n /** Names the region. */\n label: string;\n /** Row height in pixels. Rows are one line; expansion is measured. */\n rowHeight?: number;\n height?: number | string;\n /**\n * Read new lines aloud. Off by default — a console that announces every line\n * makes a screen reader useless for anything else.\n */\n announce?: boolean;\n /** The way out of the virtual window, for anyone who needs the whole log. */\n onDownload?: () => void;\n children?: ReactNode;\n}\n\nexport function LogViewer({\n className,\n lines,\n label,\n rowHeight = 22,\n height = 360,\n announce = false,\n onDownload,\n children,\n ...props\n}: LogViewerProps) {\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const [following, setFollowing] = useState(true);\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const labelId = useId();\n\n const virtualizer = useVirtualizer({\n count: lines.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 12,\n // Without a starting rect the virtualizer renders nothing until a\n // ResizeObserver fires, so the first paint is an empty box. Seeding it from\n // the declared height means rows are there immediately, and it is measured\n // properly a moment later.\n initialRect: { width: 0, height: typeof height === \"number\" ? height : 360 },\n });\n\n // Follow mode, hand-written rather than delegated. A spring-anchoring library\n // wants to own the scroll container and so does the virtualizer, and the two\n // fight; this is scrollToIndex on append plus a proximity check.\n useEffect(() => {\n if (!following || lines.length === 0) return;\n virtualizer.scrollToIndex(lines.length - 1, { align: \"end\" });\n }, [following, lines.length, virtualizer]);\n\n function handleScroll() {\n const element = scrollRef.current;\n if (!element) return;\n\n const distance = element.scrollHeight - element.scrollTop - element.clientHeight;\n // Scrolling up detaches, because the reader is looking at something and\n // yanking them back to the tail would lose it. Returning to the bottom\n // re-attaches, which is the gesture people already expect.\n setFollowing(distance <= FOLLOW_THRESHOLD);\n }\n\n function toggleExpanded(id: string) {\n setExpanded((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n }\n\n const items = virtualizer.getVirtualItems();\n\n return (\n <div data-slot=\"log-viewer\" className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span id={labelId} className=\"text-sm font-medium\">\n {label}\n </span>\n <div className=\"flex items-center gap-2\">\n {!following ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-jump\"\n onClick={() => {\n setFollowing(true);\n }}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Jump to latest\n </button>\n ) : (\n <span className=\"text-xs text-muted-foreground\">Following</span>\n )}\n {onDownload ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-download\"\n onClick={onDownload}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Download full log\n </button>\n ) : null}\n </div>\n </div>\n\n {children}\n\n <div\n ref={scrollRef}\n onScroll={handleScroll}\n role=\"log\"\n aria-labelledby={labelId}\n // Explicitly off unless asked for: role=\"log\" implies polite, and a\n // console that reads every line aloud drowns out everything else.\n aria-live={announce ? \"polite\" : \"off\"}\n // A scrollable region must be focusable, or its content is unreachable\n // without a pointer — WCAG 2.1.1, and axe's scrollable-region-focusable\n // requires exactly this. jsx-a11y's heuristic cannot see that this\n // element scrolls, so here the requirement outranks the rule.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n style={{ height }}\n className={cn(\n \"overflow-auto rounded-lg border border-border bg-muted/30 font-mono text-xs\",\n focusRing,\n )}\n >\n {lines.length === 0 ? (\n <p className=\"p-3 text-muted-foreground\">No lines match the current filter.</p>\n ) : (\n <div style={{ height: virtualizer.getTotalSize(), position: \"relative\" }}>\n {items.map((item) => {\n const line = lines[item.index];\n if (!line) return null;\n\n return (\n <div\n key={line.id}\n ref={virtualizer.measureElement}\n data-index={item.index}\n data-slot=\"log-viewer-row\"\n data-level={line.level}\n className=\"absolute top-0 left-0 w-full\"\n style={{ transform: `translateY(${String(item.start)}px)` }}\n >\n <LogViewerRow\n line={line}\n expanded={expanded.has(line.id)}\n onToggle={() => {\n toggleExpanded(line.id);\n }}\n />\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport interface LogViewerRowProps {\n line: VisibleLine;\n expanded?: boolean;\n onToggle?: () => void;\n}\n\n/**\n * One line.\n *\n * Exported because the virtualized container cannot be measured outside a real\n * browser — jsdom reports every box as zero — so the row's behaviour would\n * otherwise be untestable. It is also genuinely useful on its own for anyone\n * rendering a short log without virtualization.\n */\nexport function LogViewerRow({ line, expanded = false, onToggle }: LogViewerRowProps) {\n const hasFields = line.fields !== undefined && Object.keys(line.fields).length > 0;\n const parts = segment(line.message, line.matches);\n\n return (\n <div className=\"px-3 py-0.5 hover:bg-accent/40\">\n <div className=\"flex items-baseline gap-2\">\n {line.timestamp ? (\n <time className=\"shrink-0 text-muted-foreground tabular-nums\">{line.timestamp}</time>\n ) : null}\n\n {line.level ? (\n // The level in text, not a coloured bar. Colour alone cannot\n // distinguish warn from error for everyone reading.\n <span className={cn(\"w-11 shrink-0 uppercase\", LEVEL_STYLES[line.level])}>\n {line.level}\n </span>\n ) : null}\n\n <span\n className={cn(\n \"min-w-0 flex-1 break-all whitespace-pre-wrap\",\n line.level && LEVEL_STYLES[line.level],\n )}\n >\n {parts.map((part, index) =>\n part.match ? (\n // A mark element, so the match is conveyed structurally rather\n // than only as a background colour.\n <mark key={index} className=\"rounded-[2px] bg-warning/35 text-inherit\">\n {part.text}\n </mark>\n ) : (\n <span key={index}>{part.text}</span>\n ),\n )}\n </span>\n\n {hasFields ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-expand\"\n aria-expanded={expanded}\n onClick={onToggle}\n className={cn(\n \"shrink-0 rounded px-1 text-2xs text-muted-foreground\",\n \"transition-colors hover:text-foreground\",\n focusRing,\n disabledStyles,\n )}\n >\n {expanded ? \"Hide fields\" : \"Fields\"}\n </button>\n ) : null}\n </div>\n\n {expanded && line.fields ? (\n <dl\n data-slot=\"log-viewer-fields\"\n className=\"mt-1 mb-1 ml-4 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-l border-border pl-3\"\n >\n {Object.entries(line.fields).map(([key, value]) => (\n <div key={key} className=\"contents\">\n <dt className=\"text-muted-foreground\">{key}</dt>\n <dd className=\"m-0 break-all\">\n {typeof value === \"string\" ? value : JSON.stringify(value)}\n </dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n );\n}\n\nexport interface LogViewerToolbarProps extends Omit<ComponentPropsWithRef<\"div\">, \"onChange\"> {\n query: string;\n onQueryChange: (query: string) => void;\n regex: boolean;\n onRegexChange: (regex: boolean) => void;\n levels: Set<LogLevel>;\n onToggleLevel: (level: LogLevel) => void;\n counts: Map<LogLevel, number>;\n invalidPattern?: boolean;\n /** Shown as \"N of M lines\". */\n showing: number;\n total: number;\n}\n\nexport function LogViewerToolbar({\n className,\n query,\n onQueryChange,\n regex,\n onRegexChange,\n levels,\n onToggleLevel,\n counts,\n invalidPattern = false,\n showing,\n total,\n ...props\n}: LogViewerToolbarProps) {\n const filterId = useId();\n const errorId = useId();\n\n return (\n <div\n data-slot=\"log-viewer-toolbar\"\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <label htmlFor={filterId} className=\"sr-only\">\n Filter log\n </label>\n <input\n id={filterId}\n type=\"search\"\n value={query}\n placeholder={regex ? \"Pattern…\" : \"Filter…\"}\n aria-invalid={invalidPattern || undefined}\n aria-describedby={invalidPattern ? errorId : undefined}\n onChange={(event) => {\n onQueryChange(event.target.value);\n }}\n className={cn(\n \"min-w-32 flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm\",\n invalidPattern && \"border-destructive\",\n focusRing,\n )}\n />\n <label className=\"flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={regex}\n onChange={(event) => {\n onRegexChange(event.target.checked);\n }}\n className={cn(\"size-3.5 rounded border-input\", focusRing)}\n />\n Regex\n </label>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1\">\n {LOG_LEVELS.filter((level) => counts.has(level)).map((level) => {\n const on = levels.size === 0 || levels.has(level);\n return (\n <button\n key={level}\n type=\"button\"\n data-slot=\"log-viewer-facet\"\n aria-pressed={on}\n onClick={() => {\n onToggleLevel(level);\n }}\n className={cn(\n \"rounded-md border px-1.5 py-0.5 font-mono text-2xs uppercase transition-colors\",\n on\n ? \"border-transparent bg-secondary text-secondary-foreground\"\n : \"border-input bg-background text-muted-foreground line-through\",\n focusRing,\n )}\n >\n {level} {counts.get(level) ?? 0}\n </button>\n );\n })}\n </div>\n\n <p className=\"w-full text-xs text-muted-foreground tabular-nums\">\n {invalidPattern ? (\n // Said out loud rather than shown as an empty log, which reads as\n // \"nothing matched\" and sends the reader looking for the wrong thing.\n <span id={errorId} className=\"text-destructive\">\n Incomplete pattern — showing everything until it is valid\n </span>\n ) : (\n `${String(showing)} of ${String(total)} lines`\n )}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"log-viewer.js","names":[],"sources":["../../../src/components/log-viewer/log-viewer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport { LOG_LEVELS, segment, type LogLevel, type VisibleLine } from \"./log-stream\";\n\n/**\n * A streaming console: filter, follow, expand.\n *\n * The incumbent is react-lazylog, which was last published in 2022, is built on\n * react-virtualized, and cannot run on React 19 — while still taking about\n * 15,000 downloads a week. This is for those people.\n *\n * Two accessibility decisions worth stating rather than discovering.\n *\n * `role=\"log\"` implies `aria-live=\"polite\"`, which is right for a handful of\n * events and catastrophic for a console: a screen reader would read every line\n * of a build as it scrolls past, and nothing else would be audible. Announcing\n * is therefore off by default and opt-in through `announce`. The role stays,\n * because it still describes what the region is.\n *\n * And virtualization means most rows are not in the DOM. That is a real trade,\n * not an implementation detail: assistive technology cannot reach what is not\n * rendered, so a reader who needs the whole log needs an escape — which is what\n * `onDownload` is for. A log viewer with no way out of the virtual window is\n * not accessible however good its ARIA is.\n */\n\nconst LEVEL_STYLES: Record<LogLevel, string> = {\n trace: \"text-muted-foreground\",\n debug: \"text-muted-foreground\",\n info: \"text-foreground\",\n warn: \"text-warning\",\n error: \"text-destructive\",\n fatal: \"text-destructive font-semibold\",\n};\n\n/** Distance from the bottom, in pixels, that still counts as \"at the end\". */\nconst FOLLOW_THRESHOLD = 24;\n\nexport interface LogViewerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n lines: VisibleLine[];\n /** Names the region. */\n label: string;\n /** Row height in pixels. Rows are one line; expansion is measured. */\n rowHeight?: number;\n height?: number | string;\n /**\n * Read new lines aloud. Off by default — a console that announces every line\n * makes a screen reader useless for anything else.\n */\n announce?: boolean;\n /** The way out of the virtual window, for anyone who needs the whole log. */\n onDownload?: () => void;\n children?: ReactNode;\n}\n\nexport function LogViewer({\n className,\n lines,\n label,\n rowHeight = 22,\n height = 360,\n announce = false,\n onDownload,\n children,\n ...props\n}: LogViewerProps) {\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const [following, setFollowing] = useState(true);\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const labelId = useId();\n\n const virtualizer = useVirtualizer({\n count: lines.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 12,\n // Without a starting rect the virtualizer renders nothing until a\n // ResizeObserver fires, so the first paint is an empty box. Seeding it from\n // the declared height means rows are there immediately, and it is measured\n // properly a moment later.\n initialRect: { width: 0, height: typeof height === \"number\" ? height : 360 },\n });\n\n // Follow mode, hand-written rather than delegated. A spring-anchoring library\n // wants to own the scroll container and so does the virtualizer, and the two\n // fight; this is scrollToIndex on append plus a proximity check.\n useEffect(() => {\n if (!following || lines.length === 0) return;\n virtualizer.scrollToIndex(lines.length - 1, { align: \"end\" });\n }, [following, lines.length, virtualizer]);\n\n function handleScroll() {\n const element = scrollRef.current;\n if (!element) return;\n\n const distance = element.scrollHeight - element.scrollTop - element.clientHeight;\n // Scrolling up detaches, because the reader is looking at something and\n // yanking them back to the tail would lose it. Returning to the bottom\n // re-attaches, which is the gesture people already expect.\n setFollowing(distance <= FOLLOW_THRESHOLD);\n }\n\n function toggleExpanded(id: string) {\n setExpanded((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n }\n\n const items = virtualizer.getVirtualItems();\n\n return (\n <div data-slot=\"log-viewer\" className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span id={labelId} className=\"text-sm font-medium\">\n {label}\n </span>\n <div className=\"flex items-center gap-2\">\n {!following ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-jump\"\n onClick={() => {\n setFollowing(true);\n }}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Jump to latest\n </button>\n ) : (\n <span className=\"text-xs text-muted-foreground\">Following</span>\n )}\n {onDownload ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-download\"\n onClick={onDownload}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Download full log\n </button>\n ) : null}\n </div>\n </div>\n\n {children}\n\n <div\n ref={scrollRef}\n onScroll={handleScroll}\n role=\"log\"\n aria-labelledby={labelId}\n // Explicitly off unless asked for: role=\"log\" implies polite, and a\n // console that reads every line aloud drowns out everything else.\n aria-live={announce ? \"polite\" : \"off\"}\n // A scrollable region must be focusable, or its content is unreachable\n // without a pointer — WCAG 2.1.1, and axe's scrollable-region-focusable\n // requires exactly this. jsx-a11y's heuristic cannot see that this\n // element scrolls, so here the requirement outranks the rule.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n style={{ height }}\n className={cn(\n \"overflow-auto rounded-lg border border-border bg-muted/30 font-mono text-xs\",\n focusRing,\n )}\n >\n {lines.length === 0 ? (\n <p className=\"p-3 text-muted-foreground\">No lines match the current filter.</p>\n ) : (\n <div style={{ height: virtualizer.getTotalSize(), position: \"relative\" }}>\n {items.map((item) => {\n const line = lines[item.index];\n if (!line) return null;\n\n return (\n <div\n key={line.id}\n ref={virtualizer.measureElement}\n data-index={item.index}\n data-slot=\"log-viewer-row\"\n data-level={line.level}\n className=\"absolute start-0 top-0 w-full\"\n style={{ transform: `translateY(${String(item.start)}px)` }}\n >\n <LogViewerRow\n line={line}\n expanded={expanded.has(line.id)}\n onToggle={() => {\n toggleExpanded(line.id);\n }}\n />\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport interface LogViewerRowProps {\n line: VisibleLine;\n expanded?: boolean;\n onToggle?: () => void;\n}\n\n/**\n * One line.\n *\n * Exported because the virtualized container cannot be measured outside a real\n * browser — jsdom reports every box as zero — so the row's behaviour would\n * otherwise be untestable. It is also genuinely useful on its own for anyone\n * rendering a short log without virtualization.\n */\nexport function LogViewerRow({ line, expanded = false, onToggle }: LogViewerRowProps) {\n const hasFields = line.fields !== undefined && Object.keys(line.fields).length > 0;\n const parts = segment(line.message, line.matches);\n\n return (\n <div className=\"px-3 py-0.5 hover:bg-accent/40\">\n <div className=\"flex items-baseline gap-2\">\n {line.timestamp ? (\n <time className=\"shrink-0 text-muted-foreground tabular-nums\">{line.timestamp}</time>\n ) : null}\n\n {line.level ? (\n // The level in text, not a coloured bar. Colour alone cannot\n // distinguish warn from error for everyone reading.\n <span className={cn(\"w-11 shrink-0 uppercase\", LEVEL_STYLES[line.level])}>\n {line.level}\n </span>\n ) : null}\n\n <span\n className={cn(\n \"min-w-0 flex-1 break-all whitespace-pre-wrap\",\n line.level && LEVEL_STYLES[line.level],\n )}\n >\n {parts.map((part, index) =>\n part.match ? (\n // A mark element, so the match is conveyed structurally rather\n // than only as a background colour.\n <mark key={index} className=\"rounded-[2px] bg-warning/35 text-inherit\">\n {part.text}\n </mark>\n ) : (\n <span key={index}>{part.text}</span>\n ),\n )}\n </span>\n\n {hasFields ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-expand\"\n aria-expanded={expanded}\n onClick={onToggle}\n className={cn(\n \"shrink-0 rounded px-1 text-2xs text-muted-foreground\",\n \"transition-colors hover:text-foreground\",\n focusRing,\n disabledStyles,\n )}\n >\n {expanded ? \"Hide fields\" : \"Fields\"}\n </button>\n ) : null}\n </div>\n\n {expanded && line.fields ? (\n <dl\n data-slot=\"log-viewer-fields\"\n className=\"ms-4 mt-1 mb-1 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-s border-border ps-3\"\n >\n {Object.entries(line.fields).map(([key, value]) => (\n <div key={key} className=\"contents\">\n <dt className=\"text-muted-foreground\">{key}</dt>\n <dd className=\"m-0 break-all\">\n {typeof value === \"string\" ? value : JSON.stringify(value)}\n </dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n );\n}\n\nexport interface LogViewerToolbarProps extends Omit<ComponentPropsWithRef<\"div\">, \"onChange\"> {\n query: string;\n onQueryChange: (query: string) => void;\n regex: boolean;\n onRegexChange: (regex: boolean) => void;\n levels: Set<LogLevel>;\n onToggleLevel: (level: LogLevel) => void;\n counts: Map<LogLevel, number>;\n invalidPattern?: boolean;\n /** Shown as \"N of M lines\". */\n showing: number;\n total: number;\n}\n\nexport function LogViewerToolbar({\n className,\n query,\n onQueryChange,\n regex,\n onRegexChange,\n levels,\n onToggleLevel,\n counts,\n invalidPattern = false,\n showing,\n total,\n ...props\n}: LogViewerToolbarProps) {\n const filterId = useId();\n const errorId = useId();\n\n return (\n <div\n data-slot=\"log-viewer-toolbar\"\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <label htmlFor={filterId} className=\"sr-only\">\n Filter log\n </label>\n <input\n id={filterId}\n type=\"search\"\n value={query}\n placeholder={regex ? \"Pattern…\" : \"Filter…\"}\n aria-invalid={invalidPattern || undefined}\n aria-describedby={invalidPattern ? errorId : undefined}\n onChange={(event) => {\n onQueryChange(event.target.value);\n }}\n className={cn(\n \"min-w-32 flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm\",\n invalidPattern && \"border-destructive\",\n focusRing,\n )}\n />\n <label className=\"flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={regex}\n onChange={(event) => {\n onRegexChange(event.target.checked);\n }}\n className={cn(\"size-3.5 rounded border-input\", focusRing)}\n />\n Regex\n </label>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1\">\n {LOG_LEVELS.filter((level) => counts.has(level)).map((level) => {\n const on = levels.size === 0 || levels.has(level);\n return (\n <button\n key={level}\n type=\"button\"\n data-slot=\"log-viewer-facet\"\n aria-pressed={on}\n onClick={() => {\n onToggleLevel(level);\n }}\n className={cn(\n \"rounded-md border px-1.5 py-0.5 font-mono text-2xs uppercase transition-colors\",\n on\n ? \"border-transparent bg-secondary text-secondary-foreground\"\n : \"border-input bg-background text-muted-foreground line-through\",\n focusRing,\n )}\n >\n {level} {counts.get(level) ?? 0}\n </button>\n );\n })}\n </div>\n\n <p className=\"w-full text-xs text-muted-foreground tabular-nums\">\n {invalidPattern ? (\n // Said out loud rather than shown as an empty log, which reads as\n // \"nothing matched\" and sends the reader looking for the wrong thing.\n <span id={errorId} className=\"text-destructive\">\n Incomplete pattern — showing everything until it is valid\n </span>\n ) : (\n `${String(showing)} of ${String(total)} lines`\n )}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -78,7 +78,7 @@ function Meter({ className, segments, max, label, warnAt = .9, threshold, format
|
|
|
78
78
|
return /* @__PURE__ */ jsx("div", {
|
|
79
79
|
"data-slot": "meter-segment",
|
|
80
80
|
"data-tone": segment.tone ?? "primary",
|
|
81
|
-
className: cn("h-full first:rounded-
|
|
81
|
+
className: cn("h-full first:rounded-s-full last:rounded-e-full", "transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]", TONES[segment.tone ?? "primary"]),
|
|
82
82
|
style: { width: `${String(value / denominator * 100)}%` }
|
|
83
83
|
}, segment.id);
|
|
84
84
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meter.js","names":[],"sources":["../../../src/components/meter/meter.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useMemo, type ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A measurement against a known capacity: storage against a quota, seats\n * against a plan, spend against a budget.\n *\n * Not a Progress. Progress reports how far along a task is and is expected to\n * reach the end; a meter reports a level that has no reason to move at all, and\n * where being near the top is the thing worth noticing. They carry different\n * ARIA roles for exactly that reason, and screen readers phrase them\n * differently.\n *\n * The bar is a single `role=\"meter\"` and the segments inside it are decoration.\n * Making each segment its own widget would put five unlabelled meters in the\n * tab order to describe one quantity; the legend carries the per-segment detail\n * as text instead, where it can actually be read.\n */\n\nconst TONES = {\n primary: \"bg-primary\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n destructive: \"bg-destructive\",\n neutral: \"bg-border-strong\",\n} as const;\n\nexport type MeterTone = keyof typeof TONES;\n\nexport interface MeterSegment {\n /** Stable identity, so a re-order does not re-animate every segment. */\n id: string;\n /** Named in the legend and in the accessible summary. */\n label: string;\n value: number;\n tone?: MeterTone;\n}\n\ninterface MeterContextValue {\n segments: MeterSegment[];\n total: number;\n max: number;\n format: (value: number) => string;\n over: boolean;\n}\n\nconst MeterContext = createContext<MeterContextValue | null>(null);\n\nfunction useMeterContext(component: string): MeterContextValue {\n const context = useContext(MeterContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <Meter>.`);\n }\n return context;\n}\n\nfunction defaultFormat(value: number): string {\n return new Intl.NumberFormat().format(value);\n}\n\nexport interface MeterProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** Parts making up the measurement. One segment is the ordinary case. */\n segments: MeterSegment[];\n /** The capacity being measured against. */\n max: number;\n /** Names the meter. Required — an unnamed measurement is unreadable. */\n label: string;\n /**\n * Fraction of `max` past which the meter reports strain, between 0 and 1.\n * Presentation only: it never changes the reported value.\n */\n warnAt?: number;\n /** Optional marker on the track, for a soft limit or an alert threshold. */\n threshold?: { value: number; label: string };\n /** Formats every number that reaches the reader. */\n format?: (value: number) => string;\n /** Shown after the bar. Compose `MeterLegend` here, or your own summary. */\n children?: React.ReactNode;\n}\n\nexport function Meter({\n className,\n segments,\n max,\n label,\n warnAt = 0.9,\n threshold,\n format = defaultFormat,\n children,\n ...props\n}: MeterProps) {\n const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);\n const over = total > max;\n const strained = max > 0 && total / max >= warnAt;\n\n // Widths are a share of capacity, not of the total, so a half-full meter\n // renders half full. Dividing by the total instead would make every meter\n // look completely full, which is the usual bug in hand-rolled versions.\n const denominator = over ? total : max;\n\n const context = useMemo<MeterContextValue>(\n () => ({ segments, total, max, format, over }),\n [segments, total, max, format, over],\n );\n\n return (\n <MeterContext.Provider value={context}>\n <div\n data-slot=\"meter\"\n data-over={over || undefined}\n data-strained={strained || undefined}\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n >\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuenow={total}\n aria-valuemin={0}\n aria-valuemax={max}\n // Without this a screen reader announces a bare number with no unit\n // and no capacity — \"4\" tells the reader nothing about 4 of what.\n aria-valuetext={`${format(total)} of ${format(max)}${over ? \", over capacity\" : \"\"}`}\n data-slot=\"meter-track\"\n className=\"relative h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div className=\"flex h-full w-full\">\n {segments.map((segment) => {\n const value = Math.max(0, segment.value);\n // A zero segment renders nothing at all. A one-pixel sliver reads\n // as a small amount rather than as none.\n if (value === 0 || denominator === 0) return null;\n\n return (\n <div\n key={segment.id}\n data-slot=\"meter-segment\"\n data-tone={segment.tone ?? \"primary\"}\n className={cn(\n \"h-full first:rounded-
|
|
1
|
+
{"version":3,"file":"meter.js","names":[],"sources":["../../../src/components/meter/meter.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext, useMemo, type ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A measurement against a known capacity: storage against a quota, seats\n * against a plan, spend against a budget.\n *\n * Not a Progress. Progress reports how far along a task is and is expected to\n * reach the end; a meter reports a level that has no reason to move at all, and\n * where being near the top is the thing worth noticing. They carry different\n * ARIA roles for exactly that reason, and screen readers phrase them\n * differently.\n *\n * The bar is a single `role=\"meter\"` and the segments inside it are decoration.\n * Making each segment its own widget would put five unlabelled meters in the\n * tab order to describe one quantity; the legend carries the per-segment detail\n * as text instead, where it can actually be read.\n */\n\nconst TONES = {\n primary: \"bg-primary\",\n info: \"bg-info\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n destructive: \"bg-destructive\",\n neutral: \"bg-border-strong\",\n} as const;\n\nexport type MeterTone = keyof typeof TONES;\n\nexport interface MeterSegment {\n /** Stable identity, so a re-order does not re-animate every segment. */\n id: string;\n /** Named in the legend and in the accessible summary. */\n label: string;\n value: number;\n tone?: MeterTone;\n}\n\ninterface MeterContextValue {\n segments: MeterSegment[];\n total: number;\n max: number;\n format: (value: number) => string;\n over: boolean;\n}\n\nconst MeterContext = createContext<MeterContextValue | null>(null);\n\nfunction useMeterContext(component: string): MeterContextValue {\n const context = useContext(MeterContext);\n if (!context) {\n throw new Error(`${component} must be rendered inside <Meter>.`);\n }\n return context;\n}\n\nfunction defaultFormat(value: number): string {\n return new Intl.NumberFormat().format(value);\n}\n\nexport interface MeterProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** Parts making up the measurement. One segment is the ordinary case. */\n segments: MeterSegment[];\n /** The capacity being measured against. */\n max: number;\n /** Names the meter. Required — an unnamed measurement is unreadable. */\n label: string;\n /**\n * Fraction of `max` past which the meter reports strain, between 0 and 1.\n * Presentation only: it never changes the reported value.\n */\n warnAt?: number;\n /** Optional marker on the track, for a soft limit or an alert threshold. */\n threshold?: { value: number; label: string };\n /** Formats every number that reaches the reader. */\n format?: (value: number) => string;\n /** Shown after the bar. Compose `MeterLegend` here, or your own summary. */\n children?: React.ReactNode;\n}\n\nexport function Meter({\n className,\n segments,\n max,\n label,\n warnAt = 0.9,\n threshold,\n format = defaultFormat,\n children,\n ...props\n}: MeterProps) {\n const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.value), 0);\n const over = total > max;\n const strained = max > 0 && total / max >= warnAt;\n\n // Widths are a share of capacity, not of the total, so a half-full meter\n // renders half full. Dividing by the total instead would make every meter\n // look completely full, which is the usual bug in hand-rolled versions.\n const denominator = over ? total : max;\n\n const context = useMemo<MeterContextValue>(\n () => ({ segments, total, max, format, over }),\n [segments, total, max, format, over],\n );\n\n return (\n <MeterContext.Provider value={context}>\n <div\n data-slot=\"meter\"\n data-over={over || undefined}\n data-strained={strained || undefined}\n className={cn(\"flex w-full flex-col gap-1.5\", className)}\n {...props}\n >\n <div\n role=\"meter\"\n aria-label={label}\n aria-valuenow={total}\n aria-valuemin={0}\n aria-valuemax={max}\n // Without this a screen reader announces a bare number with no unit\n // and no capacity — \"4\" tells the reader nothing about 4 of what.\n aria-valuetext={`${format(total)} of ${format(max)}${over ? \", over capacity\" : \"\"}`}\n data-slot=\"meter-track\"\n className=\"relative h-2 w-full overflow-hidden rounded-full bg-muted\"\n >\n <div className=\"flex h-full w-full\">\n {segments.map((segment) => {\n const value = Math.max(0, segment.value);\n // A zero segment renders nothing at all. A one-pixel sliver reads\n // as a small amount rather than as none.\n if (value === 0 || denominator === 0) return null;\n\n return (\n <div\n key={segment.id}\n data-slot=\"meter-segment\"\n data-tone={segment.tone ?? \"primary\"}\n className={cn(\n \"h-full first:rounded-s-full last:rounded-e-full\",\n \"transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-out-quint)]\",\n TONES[segment.tone ?? \"primary\"],\n )}\n style={{ width: `${String((value / denominator) * 100)}%` }}\n />\n );\n })}\n </div>\n\n {threshold && max > 0 && threshold.value > 0 && threshold.value < max ? (\n <div\n data-slot=\"meter-threshold\"\n // The marker repeats what the legend states in words, so it is\n // decoration. Announcing it would interrupt the value.\n aria-hidden=\"true\"\n className=\"absolute inset-y-0 w-px bg-foreground/45\"\n style={{ left: `${String((threshold.value / denominator) * 100)}%` }}\n />\n ) : null}\n </div>\n\n {children}\n </div>\n </MeterContext.Provider>\n );\n}\n\n/**\n * The per-segment breakdown, as text.\n *\n * This is where the detail lives. The bar communicates one number; anyone who\n * needs to know that images account for most of it reads it here.\n */\nexport function MeterLegend({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n const { segments, format } = useMeterContext(\"MeterLegend\");\n\n return (\n <ul\n data-slot=\"meter-legend\"\n className={cn(\"flex flex-wrap gap-x-4 gap-y-1 text-xs\", className)}\n {...props}\n >\n {segments.map((segment) => (\n <li key={segment.id} className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className={cn(\"size-2 shrink-0 rounded-full\", TONES[segment.tone ?? \"primary\"])}\n />\n <span className=\"text-muted-foreground\">{segment.label}</span>\n <span className=\"tabular-nums\">{format(segment.value)}</span>\n </li>\n ))}\n </ul>\n );\n}\n\n/** The headline \"4.2 GB of 10 GB\" line, with an over-capacity state. */\nexport function MeterValue({ className, ...props }: ComponentPropsWithRef<\"p\">) {\n const { total, max, format, over } = useMeterContext(\"MeterValue\");\n\n return (\n <p\n data-slot=\"meter-value\"\n className={cn(\n \"text-xs tabular-nums\",\n over ? \"text-destructive\" : \"text-muted-foreground\",\n className,\n )}\n {...props}\n >\n {format(total)} of {format(max)}\n {over ? \" — over capacity\" : null}\n </p>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagination.d.ts","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"mappings":";;;;;;;;;;iBAegB,aAAa,cAAc,SAAS,+CAA4B,IAAA;iBAahE,oBAAoB,cAAc,SAAS,8CAA2B,IAAA;iBAUtE,iBAAiB,cAAc,SAAS,8CAA2B,IAAA;UAIlE,4BAA4B;;EAE3C;EACA;EACA;;iBAGc,iBACd,WACA,UACA,MACA,YACG,SACF,sCAAmB,IAAA;iBAoBN,qBAAqB,cAAc,SAAS,sCAAmB,IAAA;iBA2B/D,iBAAiB,cAAc,SAAS,sCAAmB,IAAA;;;;;;;;iBAkC3D,qBAAqB,cAAc,SAAS,gDAA6B,IAAA"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mirrorForDirection } from "../../lib/styles.js";
|
|
1
2
|
import { cn } from "../../lib/utils.js";
|
|
2
3
|
import { buttonVariants } from "../button/button.js";
|
|
3
4
|
import { Slot } from "radix-ui";
|
|
@@ -56,7 +57,7 @@ function PaginationPrevious({ className, ...props }) {
|
|
|
56
57
|
viewBox: "0 0 24 24",
|
|
57
58
|
fill: "none",
|
|
58
59
|
"aria-hidden": "true",
|
|
59
|
-
className: "size-4",
|
|
60
|
+
className: cn("size-4", mirrorForDirection),
|
|
60
61
|
children: /* @__PURE__ */ jsx("path", {
|
|
61
62
|
d: "m15 18-6-6 6-6",
|
|
62
63
|
stroke: "currentColor",
|
|
@@ -83,7 +84,7 @@ function PaginationNext({ className, ...props }) {
|
|
|
83
84
|
viewBox: "0 0 24 24",
|
|
84
85
|
fill: "none",
|
|
85
86
|
"aria-hidden": "true",
|
|
86
|
-
className: "size-4",
|
|
87
|
+
className: cn("size-4", mirrorForDirection),
|
|
87
88
|
children: /* @__PURE__ */ jsx("path", {
|
|
88
89
|
d: "m9 18 6-6-6-6",
|
|
89
90
|
stroke: "currentColor",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagination.js","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"sourcesContent":["import { Slot } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { buttonVariants } from \"@/components/button\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Navigation between pages of a list.\n *\n * Rendered as links by default, not buttons: a page of results is a place, so\n * it should be shareable, openable in a new tab and part of history. Use\n * `asChild` to hand off to your router's link. For pagination that only changes\n * client state, render buttons instead — `PaginationLink` accepts either.\n */\nexport function Pagination({ className, ...props }: ComponentPropsWithRef<\"nav\">) {\n return (\n <nav\n // A named landmark, so it can be jumped to and is distinguishable from\n // the page's main navigation.\n aria-label=\"Pagination\"\n data-slot=\"pagination\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationContent({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n return (\n <ul\n data-slot=\"pagination-content\"\n className={cn(\"flex flex-row items-center gap-1\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationItem({ className, ...props }: ComponentPropsWithRef<\"li\">) {\n return <li data-slot=\"pagination-item\" className={cn(className)} {...props} />;\n}\n\nexport interface PaginationLinkProps extends ComponentPropsWithRef<\"a\"> {\n /** Marks the page the user is on. Sets aria-current=\"page\". */\n isActive?: boolean;\n size?: \"sm\" | \"md\" | \"icon\" | \"icon-sm\";\n asChild?: boolean;\n}\n\nexport function PaginationLink({\n className,\n isActive,\n size = \"icon\",\n asChild,\n ...props\n}: PaginationLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n\n return (\n <Comp\n data-slot=\"pagination-link\"\n // aria-current is what tells a screen reader user where they are; styling\n // the active page differently does nothing on its own.\n aria-current={isActive ? \"page\" : undefined}\n data-active={isActive || undefined}\n className={cn(\n buttonVariants({ variant: isActive ? \"outline\" : \"ghost\", size }),\n isActive && \"border-border-strong font-medium\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function PaginationPrevious({ className, ...props }: PaginationLinkProps) {\n return (\n <PaginationLink\n aria-label=\"Go to previous page\"\n size=\"sm\"\n className={cn(\"gap-1 px-2.5\", className)}\n {...props}\n >\n <svg
|
|
1
|
+
{"version":3,"file":"pagination.js","names":[],"sources":["../../../src/components/pagination/pagination.tsx"],"sourcesContent":["import { Slot } from \"radix-ui\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { buttonVariants } from \"@/components/button\";\nimport { mirrorForDirection } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Navigation between pages of a list.\n *\n * Rendered as links by default, not buttons: a page of results is a place, so\n * it should be shareable, openable in a new tab and part of history. Use\n * `asChild` to hand off to your router's link. For pagination that only changes\n * client state, render buttons instead — `PaginationLink` accepts either.\n */\nexport function Pagination({ className, ...props }: ComponentPropsWithRef<\"nav\">) {\n return (\n <nav\n // A named landmark, so it can be jumped to and is distinguishable from\n // the page's main navigation.\n aria-label=\"Pagination\"\n data-slot=\"pagination\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationContent({ className, ...props }: ComponentPropsWithRef<\"ul\">) {\n return (\n <ul\n data-slot=\"pagination-content\"\n className={cn(\"flex flex-row items-center gap-1\", className)}\n {...props}\n />\n );\n}\n\nexport function PaginationItem({ className, ...props }: ComponentPropsWithRef<\"li\">) {\n return <li data-slot=\"pagination-item\" className={cn(className)} {...props} />;\n}\n\nexport interface PaginationLinkProps extends ComponentPropsWithRef<\"a\"> {\n /** Marks the page the user is on. Sets aria-current=\"page\". */\n isActive?: boolean;\n size?: \"sm\" | \"md\" | \"icon\" | \"icon-sm\";\n asChild?: boolean;\n}\n\nexport function PaginationLink({\n className,\n isActive,\n size = \"icon\",\n asChild,\n ...props\n}: PaginationLinkProps) {\n const Comp = asChild ? Slot.Root : \"a\";\n\n return (\n <Comp\n data-slot=\"pagination-link\"\n // aria-current is what tells a screen reader user where they are; styling\n // the active page differently does nothing on its own.\n aria-current={isActive ? \"page\" : undefined}\n data-active={isActive || undefined}\n className={cn(\n buttonVariants({ variant: isActive ? \"outline\" : \"ghost\", size }),\n isActive && \"border-border-strong font-medium\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport function PaginationPrevious({ className, ...props }: PaginationLinkProps) {\n return (\n <PaginationLink\n aria-label=\"Go to previous page\"\n size=\"sm\"\n className={cn(\"gap-1 px-2.5\", className)}\n {...props}\n >\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\"size-4\", mirrorForDirection)}\n >\n <path\n d=\"m15 18-6-6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n <span className=\"hidden sm:inline\">Previous</span>\n </PaginationLink>\n );\n}\n\nexport function PaginationNext({ className, ...props }: PaginationLinkProps) {\n return (\n <PaginationLink\n aria-label=\"Go to next page\"\n size=\"sm\"\n className={cn(\"gap-1 px-2.5\", className)}\n {...props}\n >\n <span className=\"hidden sm:inline\">Next</span>\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n className={cn(\"size-4\", mirrorForDirection)}\n >\n <path\n d=\"m9 18 6-6-6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </PaginationLink>\n );\n}\n\n/**\n * Stands in for a run of skipped pages.\n *\n * Hidden from assistive technology and given screen-reader text, because \"…\"\n * is announced as nothing useful and the gap is already implied by the page\n * numbers either side.\n */\nexport function PaginationEllipsis({ className, ...props }: ComponentPropsWithRef<\"span\">) {\n return (\n <span\n data-slot=\"pagination-ellipsis\"\n aria-hidden=\"true\"\n className={cn(\"grid size-9 place-items-center text-muted-foreground\", className)}\n {...props}\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-4\">\n <circle cx=\"5\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n <circle cx=\"19\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" />\n </svg>\n <span className=\"sr-only\">More pages</span>\n </span>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAgB,WAAW,EAAE,WAAW,GAAG,SAAuC;CAChF,OACE,oBAAC,OAAD;EAGE,cAAW;EACX,aAAU;EACV,WAAW,GAAG,sCAAsC,SAAS;EAC7D,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,kBAAkB,EAAE,WAAW,GAAG,SAAsC;CACtF,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GAAG,oCAAoC,SAAS;EAC3D,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAAsC;CACnF,OAAO,oBAAC,MAAD;EAAI,aAAU;EAAkB,WAAW,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AAC/E;AASA,SAAgB,eAAe,EAC7B,WACA,UACA,OAAO,QACP,SACA,GAAG,SACmB;CACtB,MAAM,OAAO,UAAU,KAAK,OAAO;CAEnC,OACE,oBAAC,MAAD;EACE,aAAU;EAGV,gBAAc,WAAW,SAAS,KAAA;EAClC,eAAa,YAAY,KAAA;EACzB,WAAW,GACT,eAAe;GAAE,SAAS,WAAW,YAAY;GAAS;EAAK,CAAC,GAChE,YAAY,oCACZ,SACF;EACA,GAAI;CACL,CAAA;AAEL;AAEA,SAAgB,mBAAmB,EAAE,WAAW,GAAG,SAA8B;CAC/E,OACE,qBAAC,gBAAD;EACE,cAAW;EACX,MAAK;EACL,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EAJN,UAAA,CAME,oBAAC,OAAD;GACE,SAAQ;GACR,MAAK;GACL,eAAY;GACZ,WAAW,GAAG,UAAU,kBAAkB;GAE1C,UAAA,oBAAC,QAAD;IACE,GAAE;IACF,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;GAChB,CAAA;EACE,CAAA,GACL,oBAAC,QAAD;GAAM,WAAU;GAAmB,UAAA;EAAc,CAAA,CACnC;;AAEpB;AAEA,SAAgB,eAAe,EAAE,WAAW,GAAG,SAA8B;CAC3E,OACE,qBAAC,gBAAD;EACE,cAAW;EACX,MAAK;EACL,WAAW,GAAG,gBAAgB,SAAS;EACvC,GAAI;EAJN,UAAA,CAME,oBAAC,QAAD;GAAM,WAAU;GAAmB,UAAA;EAAU,CAAA,GAC7C,oBAAC,OAAD;GACE,SAAQ;GACR,MAAK;GACL,eAAY;GACZ,WAAW,GAAG,UAAU,kBAAkB;GAE1C,UAAA,oBAAC,QAAD;IACE,GAAE;IACF,QAAO;IACP,aAAY;IACZ,eAAc;IACd,gBAAe;GAChB,CAAA;EACE,CAAA,CACS;;AAEpB;;;;;;;;AASA,SAAgB,mBAAmB,EAAE,WAAW,GAAG,SAAwC;CACzF,OACE,qBAAC,QAAD;EACE,aAAU;EACV,eAAY;EACZ,WAAW,GAAG,wDAAwD,SAAS;EAC/E,GAAI;EAJN,UAAA,CAME,qBAAC,OAAD;GAAK,SAAQ;GAAY,MAAK;GAAO,WAAU;GAA/C,UAAA;IACE,oBAAC,UAAD;KAAQ,IAAG;KAAI,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACpD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;IACrD,oBAAC,UAAD;KAAQ,IAAG;KAAK,IAAG;KAAK,GAAE;KAAM,MAAK;IAAgB,CAAA;GAClD;EACL,CAAA,GAAA,oBAAC,QAAD;GAAM,WAAU;GAAU,UAAA;EAAgB,CAAA,CACtC;;AAEV"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { CellState, Grants, GroupState, Permission, PermissionGroup, Role, cellState, countGranted, groupPermissions, groupState, inheritedFrom } from "./permission-model.js";
|
|
2
|
+
import { PermissionMatrix, PermissionMatrixProps } from "./permission-matrix.js";
|
|
3
|
+
export { type CellState, type Grants, type GroupState, type Permission, type PermissionGroup, PermissionMatrix, type PermissionMatrixProps, type Role, cellState, countGranted, groupPermissions, groupState, inheritedFrom };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/permission-matrix/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "permission-matrix",
|
|
5
|
+
title: "Permission Matrix",
|
|
6
|
+
description: "Roles across, permissions down, with inheritance, locked roles and one tab stop.",
|
|
7
|
+
category: "data",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: ["checkbox"],
|
|
11
|
+
files: ["permission-model.ts", "permission-matrix.tsx"],
|
|
12
|
+
a11y: "A grid in the WAI-ARIA sense: role=\"grid\" with one tab stop, arrow keys between cells, Home and End along a row and Ctrl with them across the whole matrix — the right call for a matrix that is operated, where it was the wrong one for a diff that is read. Every checkbox is named by both coordinates, \"Delete projects for Editor\", so a reader arriving by arrow key knows where they are without re-reading the headers. An inherited grant is a checked box that cannot be changed here, marked aria-disabled rather than disabled so it stays in the keyboard path, with the role it came from as its description. A locked role says in each cell's description that nothing can be changed. Group toggles are tri-state, and the column header counts each role's grants in text."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/permission-matrix/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"permission-matrix\",\n title: \"Permission Matrix\",\n description:\n \"Roles across, permissions down, with inheritance, locked roles and one tab stop.\",\n category: \"data\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [\"checkbox\"],\n files: [\"permission-model.ts\", \"permission-matrix.tsx\"],\n a11y:\n 'A grid in the WAI-ARIA sense: role=\"grid\" with one tab stop, arrow keys between cells, Home ' +\n \"and End along a row and Ctrl with them across the whole matrix — the right call for a \" +\n \"matrix that is operated, where it was the wrong one for a diff that is read. Every checkbox \" +\n 'is named by both coordinates, \"Delete projects for Editor\", so a reader arriving by arrow ' +\n \"key knows where they are without re-reading the headers. An inherited grant is a checked \" +\n \"box that cannot be changed here, marked aria-disabled rather than disabled so it stays in \" +\n \"the keyboard path, with the role it came from as its description. A locked role says in \" +\n \"each cell's description that nothing can be changed. Group toggles are tri-state, and the \" +\n \"column header counts each role's grants in text.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC,UAAU;CACjC,OAAO,CAAC,uBAAuB,uBAAuB;CACtD,MACE;AASJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Grants, Permission, Role } from "./permission-model.js";
|
|
2
|
+
import { ComponentPropsWithRef } from "react";
|
|
3
|
+
//#region src/components/permission-matrix/permission-matrix.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Roles across, permissions down, a checkbox at every crossing.
|
|
6
|
+
*
|
|
7
|
+
* Every admin panel has one and every admin panel builds it, because the
|
|
8
|
+
* hard part is not the checkboxes. It is that a role can inherit from
|
|
9
|
+
* another, so a box can be ticked without anyone having ticked it; that an
|
|
10
|
+
* Owner has everything and none of it can be unticked; that a section of
|
|
11
|
+
* eight permissions wants one control for all of them; and that sixty
|
|
12
|
+
* checkboxes are sixty tab stops unless something is done about it.
|
|
13
|
+
*
|
|
14
|
+
* Something is done about it. This is a grid in the WAI-ARIA sense — one tab
|
|
15
|
+
* stop, arrow keys between cells, Home and End along a row — which is the
|
|
16
|
+
* right call here and was the wrong one for the diff viewer: a diff is read,
|
|
17
|
+
* a matrix is operated. Every checkbox is named by both coordinates, "Delete
|
|
18
|
+
* projects for Editor", so a reader arriving by arrow key knows where they
|
|
19
|
+
* are without re-reading the headers.
|
|
20
|
+
*
|
|
21
|
+
* A grant that arrived by inheritance is shown as a checked box that cannot
|
|
22
|
+
* be unchecked here, with the role it came from said beside it and in the
|
|
23
|
+
* box's description. A disabled control would be the obvious rendering, and
|
|
24
|
+
* it would take the box out of the tab order and the arrow-key path, so a
|
|
25
|
+
* keyboard user would step over the one cell whose state needs explaining.
|
|
26
|
+
*
|
|
27
|
+
* Changes are reported, never applied. A group toggle reports every
|
|
28
|
+
* permission it touched in one call, so an application can save it as one
|
|
29
|
+
* change rather than eight.
|
|
30
|
+
*/
|
|
31
|
+
interface PermissionMatrixProps extends Omit<ComponentPropsWithRef<"table">, "onChange"> {
|
|
32
|
+
/** Names the matrix: "Permissions for Acme". */
|
|
33
|
+
label: string;
|
|
34
|
+
roles: Role[];
|
|
35
|
+
permissions: Permission[];
|
|
36
|
+
grants: Grants;
|
|
37
|
+
/**
|
|
38
|
+
* A change to make. Several ids arrive together when a group toggle was
|
|
39
|
+
* used. Omit to render the matrix as a record.
|
|
40
|
+
*/
|
|
41
|
+
onChange?: (roleId: string, permissionIds: string[], granted: boolean) => void;
|
|
42
|
+
}
|
|
43
|
+
declare function PermissionMatrix({ className, label, roles, permissions, grants, onChange, ...props }: PermissionMatrixProps): import("react").JSX.Element;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { PermissionMatrix, PermissionMatrixProps };
|
|
46
|
+
//# sourceMappingURL=permission-matrix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-matrix.d.ts","names":[],"sources":["../../../src/components/permission-matrix/permission-matrix.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmDiB,8BAA8B,KAC7C;;EAIA;EACA,OAAO;EACP,aAAa;EACb,QAAQ;;;;;EAKR,YAAY,gBAAgB,yBAAyB;;iBAcvC,mBACd,WACA,OACA,OACA,aACA,QACA,aACG,SACF,wCAAqB,IAAA"}
|