@flanksource/clicky-ui 0.2.21 → 0.3.1
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/dist/components/Combobox.cjs +5 -2
- package/dist/components/Combobox.cjs.map +1 -1
- package/dist/components/Combobox.d.ts.map +1 -1
- package/dist/components/Combobox.js +5 -2
- package/dist/components/Combobox.js.map +1 -1
- package/dist/components/FilterBar.cjs +280 -253
- package/dist/components/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar.d.ts.map +1 -1
- package/dist/components/FilterBar.js +280 -253
- package/dist/components/FilterBar.js.map +1 -1
- package/dist/components/JsonSchemaForm.cjs +3 -1
- package/dist/components/JsonSchemaForm.cjs.map +1 -1
- package/dist/components/JsonSchemaForm.d.ts.map +1 -1
- package/dist/components/JsonSchemaForm.js +4 -2
- package/dist/components/JsonSchemaForm.js.map +1 -1
- package/dist/components/MultiSelect.cjs +29 -32
- package/dist/components/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect.d.ts.map +1 -1
- package/dist/components/MultiSelect.js +30 -33
- package/dist/components/MultiSelect.js.map +1 -1
- package/dist/components/TimeRange.cjs +29 -37
- package/dist/components/TimeRange.cjs.map +1 -1
- package/dist/components/TimeRange.d.ts.map +1 -1
- package/dist/components/TimeRange.js +30 -38
- package/dist/components/TimeRange.js.map +1 -1
- package/dist/components/TreePickerField.cjs +6 -3
- package/dist/components/TreePickerField.cjs.map +1 -1
- package/dist/components/TreePickerField.d.ts.map +1 -1
- package/dist/components/TreePickerField.js +6 -3
- package/dist/components/TreePickerField.js.map +1 -1
- package/dist/components/icon-menu-picker.cjs +9 -3
- package/dist/components/icon-menu-picker.cjs.map +1 -1
- package/dist/components/icon-menu-picker.d.ts.map +1 -1
- package/dist/components/icon-menu-picker.js +9 -3
- package/dist/components/icon-menu-picker.js.map +1 -1
- package/dist/components/json-schema-form-fields.cjs +80 -2
- package/dist/components/json-schema-form-fields.cjs.map +1 -1
- package/dist/components/json-schema-form-fields.d.ts +15 -0
- package/dist/components/json-schema-form-fields.d.ts.map +1 -1
- package/dist/components/json-schema-form-fields.js +81 -3
- package/dist/components/json-schema-form-fields.js.map +1 -1
- package/dist/components/json-schema-form-refs.cjs +60 -0
- package/dist/components/json-schema-form-refs.cjs.map +1 -0
- package/dist/components/json-schema-form-refs.d.ts +3 -0
- package/dist/components/json-schema-form-refs.d.ts.map +1 -0
- package/dist/components/json-schema-form-refs.js +60 -0
- package/dist/components/json-schema-form-refs.js.map +1 -0
- package/dist/components/json-schema-form-render.cjs +14 -2
- package/dist/components/json-schema-form-render.cjs.map +1 -1
- package/dist/components/json-schema-form-render.d.ts.map +1 -1
- package/dist/components/json-schema-form-render.js +15 -3
- package/dist/components/json-schema-form-render.js.map +1 -1
- package/dist/components/json-schema-form-resolve.cjs +5 -0
- package/dist/components/json-schema-form-resolve.cjs.map +1 -1
- package/dist/components/json-schema-form-resolve.d.ts.map +1 -1
- package/dist/components/json-schema-form-resolve.js +5 -0
- package/dist/components/json-schema-form-resolve.js.map +1 -1
- package/dist/components/json-schema-form-template-menu.cjs +83 -0
- package/dist/components/json-schema-form-template-menu.cjs.map +1 -0
- package/dist/components/json-schema-form-template-menu.d.ts +15 -0
- package/dist/components/json-schema-form-template-menu.d.ts.map +1 -0
- package/dist/components/json-schema-form-template-menu.js +83 -0
- package/dist/components/json-schema-form-template-menu.js.map +1 -0
- package/dist/components/json-schema-form-template.cjs +18 -0
- package/dist/components/json-schema-form-template.cjs.map +1 -0
- package/dist/components/json-schema-form-template.d.ts +13 -0
- package/dist/components/json-schema-form-template.d.ts.map +1 -0
- package/dist/components/json-schema-form-template.js +18 -0
- package/dist/components/json-schema-form-template.js.map +1 -0
- package/dist/components/json-schema-form-types.d.ts +5 -1
- package/dist/components/json-schema-form-types.d.ts.map +1 -1
- package/dist/components.cjs +4 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.ts +3 -0
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +4 -0
- package/dist/components.js.map +1 -1
- package/dist/data/Clicky.cjs +6 -9
- package/dist/data/Clicky.cjs.map +1 -1
- package/dist/data/Clicky.d.ts.map +1 -1
- package/dist/data/Clicky.js +6 -9
- package/dist/data/Clicky.js.map +1 -1
- package/dist/data/DataTable.cjs +23 -17
- package/dist/data/DataTable.cjs.map +1 -1
- package/dist/data/DataTable.d.ts +7 -1
- package/dist/data/DataTable.d.ts.map +1 -1
- package/dist/data/DataTable.js +24 -18
- package/dist/data/DataTable.js.map +1 -1
- package/dist/data/Gauge.cjs +37 -4
- package/dist/data/Gauge.cjs.map +1 -1
- package/dist/data/Gauge.d.ts +6 -1
- package/dist/data/Gauge.d.ts.map +1 -1
- package/dist/data/Gauge.js +37 -4
- package/dist/data/Gauge.js.map +1 -1
- package/dist/data/TimeseriesCoreBars.cjs +127 -29
- package/dist/data/TimeseriesCoreBars.cjs.map +1 -1
- package/dist/data/TimeseriesCoreBars.d.ts +11 -1
- package/dist/data/TimeseriesCoreBars.d.ts.map +1 -1
- package/dist/data/TimeseriesCoreBars.js +128 -30
- package/dist/data/TimeseriesCoreBars.js.map +1 -1
- package/dist/data/TimeseriesGauge.cjs +105 -47
- package/dist/data/TimeseriesGauge.cjs.map +1 -1
- package/dist/data/TimeseriesGauge.d.ts +6 -1
- package/dist/data/TimeseriesGauge.d.ts.map +1 -1
- package/dist/data/TimeseriesGauge.js +105 -47
- package/dist/data/TimeseriesGauge.js.map +1 -1
- package/dist/data/WorkloadCard.cjs +371 -0
- package/dist/data/WorkloadCard.cjs.map +1 -0
- package/dist/data/WorkloadCard.d.ts +24 -0
- package/dist/data/WorkloadCard.d.ts.map +1 -0
- package/dist/data/WorkloadCard.js +371 -0
- package/dist/data/WorkloadCard.js.map +1 -0
- package/dist/data/WorkloadCard.model.cjs +51 -0
- package/dist/data/WorkloadCard.model.cjs.map +1 -0
- package/dist/data/WorkloadCard.model.d.ts +53 -0
- package/dist/data/WorkloadCard.model.d.ts.map +1 -0
- package/dist/data/WorkloadCard.model.js +51 -0
- package/dist/data/WorkloadCard.model.js.map +1 -0
- package/dist/data/ai/ChatFab.cjs +3 -1
- package/dist/data/ai/ChatFab.cjs.map +1 -1
- package/dist/data/ai/ChatFab.d.ts.map +1 -1
- package/dist/data/ai/ChatFab.js +3 -1
- package/dist/data/ai/ChatFab.js.map +1 -1
- package/dist/data/ai/ChatWindow.cjs +25 -7
- package/dist/data/ai/ChatWindow.cjs.map +1 -1
- package/dist/data/ai/ChatWindow.d.ts +11 -3
- package/dist/data/ai/ChatWindow.d.ts.map +1 -1
- package/dist/data/ai/ChatWindow.js +26 -8
- package/dist/data/ai/ChatWindow.js.map +1 -1
- package/dist/data/ai/chat-window-context.cjs +2 -1
- package/dist/data/ai/chat-window-context.cjs.map +1 -1
- package/dist/data/ai/chat-window-context.d.ts +1 -1
- package/dist/data/ai/chat-window-context.d.ts.map +1 -1
- package/dist/data/ai/chat-window-context.js +2 -1
- package/dist/data/ai/chat-window-context.js.map +1 -1
- package/dist/data/ai/context.cjs.map +1 -1
- package/dist/data/ai/context.d.ts +3 -0
- package/dist/data/ai/context.d.ts.map +1 -1
- package/dist/data/ai/context.js.map +1 -1
- package/dist/data/chat/Chat.cjs +9 -1
- package/dist/data/chat/Chat.cjs.map +1 -1
- package/dist/data/chat/Chat.d.ts +4 -2
- package/dist/data/chat/Chat.d.ts.map +1 -1
- package/dist/data/chat/Chat.js +10 -2
- package/dist/data/chat/Chat.js.map +1 -1
- package/dist/data/chat/Message.cjs +13 -4
- package/dist/data/chat/Message.cjs.map +1 -1
- package/dist/data/chat/Message.d.ts +4 -2
- package/dist/data/chat/Message.d.ts.map +1 -1
- package/dist/data/chat/Message.js +13 -4
- package/dist/data/chat/Message.js.map +1 -1
- package/dist/data/chat/ToolCall.cjs +9 -4
- package/dist/data/chat/ToolCall.cjs.map +1 -1
- package/dist/data/chat/ToolCall.d.ts +3 -2
- package/dist/data/chat/ToolCall.d.ts.map +1 -1
- package/dist/data/chat/ToolCall.js +9 -4
- package/dist/data/chat/ToolCall.js.map +1 -1
- package/dist/data/chat/index.d.ts +1 -1
- package/dist/data/chat/index.d.ts.map +1 -1
- package/dist/data/chat/types.cjs.map +1 -1
- package/dist/data/chat/types.d.ts +7 -0
- package/dist/data/chat/types.d.ts.map +1 -1
- package/dist/data/chat/types.js.map +1 -1
- package/dist/data/version-info.cjs +2 -2
- package/dist/data/version-info.js +2 -2
- package/dist/data.cjs +7 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +5 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +8 -1
- package/dist/data.js.map +1 -1
- package/dist/hooks/use-sort.cjs +19 -10
- package/dist/hooks/use-sort.cjs.map +1 -1
- package/dist/hooks/use-sort.d.ts +3 -0
- package/dist/hooks/use-sort.d.ts.map +1 -1
- package/dist/hooks/use-sort.js +19 -10
- package/dist/hooks/use-sort.js.map +1 -1
- package/dist/index.cjs +11 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/AppShell.cjs +6 -10
- package/dist/layout/AppShell.cjs.map +1 -1
- package/dist/layout/AppShell.d.ts +2 -0
- package/dist/layout/AppShell.d.ts.map +1 -1
- package/dist/layout/AppShell.js +6 -10
- package/dist/layout/AppShell.js.map +1 -1
- package/dist/overlay/DropdownMenu.cjs +50 -38
- package/dist/overlay/DropdownMenu.cjs.map +1 -1
- package/dist/overlay/DropdownMenu.d.ts +5 -1
- package/dist/overlay/DropdownMenu.d.ts.map +1 -1
- package/dist/overlay/DropdownMenu.js +50 -38
- package/dist/overlay/DropdownMenu.js.map +1 -1
- package/dist/overlay/HoverCard.cjs +7 -2
- package/dist/overlay/HoverCard.cjs.map +1 -1
- package/dist/overlay/HoverCard.d.ts.map +1 -1
- package/dist/overlay/HoverCard.js +7 -2
- package/dist/overlay/HoverCard.js.map +1 -1
- package/dist/overlay/Modal.cjs +20 -17
- package/dist/overlay/Modal.cjs.map +1 -1
- package/dist/overlay/Modal.d.ts.map +1 -1
- package/dist/overlay/Modal.js +21 -18
- package/dist/overlay/Modal.js.map +1 -1
- package/dist/overlay/Toast.cjs +9 -1
- package/dist/overlay/Toast.cjs.map +1 -1
- package/dist/overlay/Toast.d.ts.map +1 -1
- package/dist/overlay/Toast.js +9 -1
- package/dist/overlay/Toast.js.map +1 -1
- package/dist/overlay/modalStack.cjs +53 -1
- package/dist/overlay/modalStack.cjs.map +1 -1
- package/dist/overlay/modalStack.d.ts +12 -0
- package/dist/overlay/modalStack.d.ts.map +1 -1
- package/dist/overlay/modalStack.js +54 -2
- package/dist/overlay/modalStack.js.map +1 -1
- package/dist/overlay/zIndex.cjs +20 -0
- package/dist/overlay/zIndex.cjs.map +1 -0
- package/dist/overlay/zIndex.d.ts +39 -0
- package/dist/overlay/zIndex.d.ts.map +1 -0
- package/dist/overlay/zIndex.js +20 -0
- package/dist/overlay/zIndex.js.map +1 -0
- package/dist/rpc/AcceptPicker.cjs +28 -26
- package/dist/rpc/AcceptPicker.cjs.map +1 -1
- package/dist/rpc/AcceptPicker.d.ts.map +1 -1
- package/dist/rpc/AcceptPicker.js +29 -27
- package/dist/rpc/AcceptPicker.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.cjs","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { Button } from \"../components/button\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\nimport { useModalStack } from \"./modalStack\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Copy for the discard-confirmation prompt shown before a guarded close. */\nexport type ConfirmCloseOptions = {\n /** Heading of the confirmation prompt. */\n title?: ReactNode;\n /** Body explaining what is lost on close. */\n message?: ReactNode;\n /** Label of the button that proceeds with closing. */\n confirmLabel?: string;\n /** Label of the button that dismisses the prompt and keeps the modal open. */\n cancelLabel?: string;\n};\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /**\n * Guard every close path (close button, Escape, backdrop) behind a discard\n * confirmation. `true` uses the default copy; pass options to customise it.\n * `onClose` only fires once the user confirms. Set this while the modal holds\n * unsaved edits and clear it (or pass `false`) once there is nothing to lose.\n */\n confirmClose?: boolean | ConfirmCloseOptions;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /**\n * Secondary header content rendered on its own row directly beneath the title.\n * The title stays a single row alongside the expand/close buttons, so a tall\n * subtitle (e.g. a tab switcher) never pushes those buttons down.\n */\n subtitle?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. Off by default; close via Escape or the close button. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm max-h-[90vh]\",\n md: \"max-w-md max-h-[90vh]\",\n lg: \"max-w-2xl max-h-[90vh]\",\n xl: \"max-w-4xl max-h-[90vh]\",\n full: \"max-w-[95vw] max-h-[95vh]\",\n};\n\nconst DEFAULT_CONFIRM: Required<ConfirmCloseOptions> = {\n title: \"Discard changes?\",\n message: \"You have unsaved changes. Closing now will discard them.\",\n confirmLabel: \"Discard\",\n cancelLabel: \"Keep editing\",\n};\n\nexport function Modal({\n open,\n onClose,\n confirmClose = false,\n title,\n subtitle,\n size = \"md\",\n closeOnBackdrop = false,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const { isTop, depth } = useModalStack(open);\n const [expanded, setExpanded] = useState(false);\n // When confirmClose is active, a close request opens this prompt instead of\n // closing outright; onClose only fires once the user confirms the discard.\n const [confirming, setConfirming] = useState(false);\n\n // requestClose is the single entry point for every close affordance. With the\n // guard on, it surfaces the confirmation; otherwise it closes immediately.\n const requestClose = () => {\n if (confirmClose) setConfirming(true);\n else onClose();\n };\n\n useEffect(() => {\n if (!open) {\n setExpanded(false);\n setConfirming(false);\n }\n }, [open]);\n\n // A close guard that disappears mid-prompt (changes saved/reverted) should\n // not leave a stale confirmation dialog up.\n useEffect(() => {\n if (!confirmClose) setConfirming(false);\n }, [confirmClose]);\n\n useEffect(() => {\n // Only the topmost modal handles Escape so a keypress closes one nested\n // layer at a time instead of every open modal at once.\n if (!open || !closeOnEsc || !isTop) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n // While the prompt is up, Escape dismisses the prompt rather than the modal.\n if (confirming) setConfirming(false);\n else requestClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, closeOnEsc, isTop, confirming, confirmClose, onClose]);\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n const overlay = (\n <div\n className={cn(\n \"fixed inset-0 flex items-center justify-center\",\n // Nested modals dim less so the dialog they opened over stays visible.\n depth === 0 ? \"bg-black/40\" : \"bg-black/20\",\n )}\n // Above the z-[9999] popover/overlay layer (dropdowns, tooltips, toasts) so a\n // modal opened from one of them stacks on top; +depth keeps nested modals\n // above their parent.\n style={{ zIndex: 10000 + depth * 10 }}\n onClick={closeOnBackdrop ? requestClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative bg-background border border-border rounded-lg shadow-xl w-full flex flex-col\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || subtitle || headerSlot || expandable || !hideClose) && (\n <div className=\"px-density-4 py-density-3 border-b border-border\">\n <div className=\"flex items-center gap-density-2\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={requestClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n {subtitle ? <div className=\"mt-density-2\">{subtitle}</div> : null}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n {confirming && (\n <ConfirmClosePrompt\n options={confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose }}\n onConfirm={() => {\n setConfirming(false);\n onClose();\n }}\n onCancel={() => setConfirming(false)}\n />\n )}\n </div>\n </div>\n );\n\n // Render into document.body so the fixed-position overlay escapes any ancestor\n // transform/overflow (e.g. a dropdown menu positioned via a CSS transform),\n // which would otherwise trap position:fixed inside that ancestor's box and clip\n // the modal to it.\n return typeof document !== \"undefined\" ? createPortal(overlay, document.body) : null;\n}\n\nfunction ConfirmClosePrompt({\n options,\n onConfirm,\n onCancel,\n}: {\n options: Required<ConfirmCloseOptions>;\n onConfirm: () => void;\n onCancel: () => void;\n}) {\n return (\n <div\n className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-label={typeof options.title === \"string\" ? options.title : \"Confirm close\"}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl\">\n <h3 className=\"text-sm font-semibold\">{options.title}</h3>\n <p className=\"mt-density-2 text-sm text-muted-foreground\">{options.message}</p>\n <div className=\"mt-density-4 flex justify-end gap-density-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n {options.cancelLabel}\n </Button>\n <Button variant=\"destructive\" size=\"sm\" onClick={onConfirm}>\n {options.confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"names":["useRef","useModalStack","useState","useEffect","_a","jsx","cn","jsxs","Icon","UiFullscreenFilled","UiFullscreen","UiClose","createPortal","Button"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Modal.cjs","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { Button } from \"../components/button\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\nimport { useEscapeLayer, useModalStack } from \"./modalStack\";\nimport { zIndex } from \"./zIndex\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Copy for the discard-confirmation prompt shown before a guarded close. */\nexport type ConfirmCloseOptions = {\n /** Heading of the confirmation prompt. */\n title?: ReactNode;\n /** Body explaining what is lost on close. */\n message?: ReactNode;\n /** Label of the button that proceeds with closing. */\n confirmLabel?: string;\n /** Label of the button that dismisses the prompt and keeps the modal open. */\n cancelLabel?: string;\n};\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /**\n * Guard every close path (close button, Escape, backdrop) behind a discard\n * confirmation. `true` uses the default copy; pass options to customise it.\n * `onClose` only fires once the user confirms. Set this while the modal holds\n * unsaved edits and clear it (or pass `false`) once there is nothing to lose.\n */\n confirmClose?: boolean | ConfirmCloseOptions;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /**\n * Secondary header content rendered on its own row directly beneath the title.\n * The title stays a single row alongside the expand/close buttons, so a tall\n * subtitle (e.g. a tab switcher) never pushes those buttons down.\n */\n subtitle?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. Off by default; close via Escape or the close button. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm\",\n md: \"max-w-md\",\n lg: \"max-w-2xl\",\n xl: \"max-w-4xl\",\n full: \"max-w-[95vw]\",\n};\n\nconst DEFAULT_CONFIRM: Required<ConfirmCloseOptions> = {\n title: \"Discard changes?\",\n message: \"You have unsaved changes. Closing now will discard them.\",\n confirmLabel: \"Discard\",\n cancelLabel: \"Keep editing\",\n};\n\nexport function Modal({\n open,\n onClose,\n confirmClose = false,\n title,\n subtitle,\n size = \"md\",\n closeOnBackdrop = false,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const { depth } = useModalStack(open);\n const [expanded, setExpanded] = useState(false);\n // When confirmClose is active, a close request opens this prompt instead of\n // closing outright; onClose only fires once the user confirms the discard.\n const [confirming, setConfirming] = useState(false);\n\n // requestClose is the single entry point for every close affordance. With the\n // guard on, it surfaces the confirmation; otherwise it closes immediately.\n const requestClose = () => {\n if (confirmClose) setConfirming(true);\n else onClose();\n };\n\n useEffect(() => {\n if (!open) {\n setExpanded(false);\n setConfirming(false);\n }\n }, [open]);\n\n // A close guard that disappears mid-prompt (changes saved/reverted) should\n // not leave a stale confirmation dialog up.\n useEffect(() => {\n if (!confirmClose) setConfirming(false);\n }, [confirmClose]);\n\n useEscapeLayer(\n open,\n () => {\n // While the prompt is up, Escape dismisses the prompt rather than the modal.\n if (confirming) setConfirming(false);\n else requestClose();\n },\n closeOnEsc,\n );\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n const overlay = (\n <div\n className={cn(\n \"fixed inset-0 flex items-center justify-center p-density-2 sm:p-density-4\",\n // Nested modals dim less so the dialog they opened over stays visible.\n depth === 0 ? \"bg-black/40\" : \"bg-black/20\",\n )}\n // Sits in the modal band of the centralized z-index scale; +depth keeps\n // nested modals above their parent. Floating content (dropdowns, tooltips)\n // computes its z relative to this via useFloatingZIndex so it clears the\n // modal instead of rendering behind it.\n style={{ zIndex: zIndex.modal + depth * zIndex.modalStep }}\n onClick={closeOnBackdrop ? requestClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative flex w-full flex-col rounded-lg border border-border bg-background shadow-xl\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n // Inline maxHeight (not an arbitrary Tailwind class) so the panel stays\n // viewport-bounded even when a consumer's Tailwind build can't generate\n // the dvh utility — e.g. www aliases clicky-ui to source but scans the\n // published dist. When expanded, fill the viewport.\n style={{\n maxHeight: \"calc(100dvh - 2rem)\",\n ...(expanded ? { height: \"calc(100dvh - 2rem)\" } : {}),\n }}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || subtitle || headerSlot || expandable || !hideClose) && (\n <div className=\"px-density-4 py-density-3 border-b border-border\">\n <div className=\"flex items-center gap-density-2\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={requestClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n {subtitle ? <div className=\"mt-density-2\">{subtitle}</div> : null}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n {confirming && (\n <ConfirmClosePrompt\n options={confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose }}\n onConfirm={() => {\n setConfirming(false);\n onClose();\n }}\n onCancel={() => setConfirming(false)}\n />\n )}\n </div>\n </div>\n );\n\n // Render into document.body so the fixed-position overlay escapes any ancestor\n // transform/overflow (e.g. a dropdown menu positioned via a CSS transform),\n // which would otherwise trap position:fixed inside that ancestor's box and clip\n // the modal to it.\n return typeof document !== \"undefined\" ? createPortal(overlay, document.body) : null;\n}\n\nfunction ConfirmClosePrompt({\n options,\n onConfirm,\n onCancel,\n}: {\n options: Required<ConfirmCloseOptions>;\n onConfirm: () => void;\n onCancel: () => void;\n}) {\n return (\n <div\n className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-label={typeof options.title === \"string\" ? options.title : \"Confirm close\"}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl\">\n <h3 className=\"text-sm font-semibold\">{options.title}</h3>\n <p className=\"mt-density-2 text-sm text-muted-foreground\">{options.message}</p>\n <div className=\"mt-density-4 flex justify-end gap-density-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n {options.cancelLabel}\n </Button>\n <Button variant=\"destructive\" size=\"sm\" onClick={onConfirm}>\n {options.confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"names":["useRef","useModalStack","useState","useEffect","useEscapeLayer","_a","jsx","cn","zIndex","jsxs","Icon","UiFullscreenFilled","UiFullscreen","UiClose","createPortal","Button"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AASpE,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;AAE3D,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAAG;IAChC,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,+CAA+C;IAC/C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;IAC7C,oEAAoE;IACpE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,gGAAgG;IAChG,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,uDAAuD;IACvD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAiBF,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,OAAO,EACP,YAAoB,EACpB,KAAK,EACL,QAAQ,EACR,IAAW,EACX,eAAuB,EACvB,UAAiB,EACjB,SAAiB,EACjB,UAAiB,EACjB,SAAS,EACT,UAAU,EACV,MAAM,EACN,QAAQ,GACT,EAAE,UAAU,sCAyIZ"}
|
package/dist/overlay/Modal.js
CHANGED
|
@@ -4,15 +4,16 @@ import { createPortal } from "react-dom";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { Icon } from "../data/Icon.js";
|
|
6
6
|
import { Button } from "../components/button.js";
|
|
7
|
-
import { useModalStack } from "./modalStack.js";
|
|
7
|
+
import { useModalStack, useEscapeLayer } from "./modalStack.js";
|
|
8
|
+
import { zIndex } from "./zIndex.js";
|
|
8
9
|
import { UiFullscreenFilled, UiFullscreen } from "../icons/components/UiFullscreen.js";
|
|
9
10
|
import { UiClose } from "../icons/components/UiClose.js";
|
|
10
11
|
const sizeClass = {
|
|
11
|
-
sm: "max-w-sm
|
|
12
|
-
md: "max-w-md
|
|
13
|
-
lg: "max-w-2xl
|
|
14
|
-
xl: "max-w-4xl
|
|
15
|
-
full: "max-w-[95vw]
|
|
12
|
+
sm: "max-w-sm",
|
|
13
|
+
md: "max-w-md",
|
|
14
|
+
lg: "max-w-2xl",
|
|
15
|
+
xl: "max-w-4xl",
|
|
16
|
+
full: "max-w-[95vw]"
|
|
16
17
|
};
|
|
17
18
|
const DEFAULT_CONFIRM = {
|
|
18
19
|
title: "Discard changes?",
|
|
@@ -37,7 +38,7 @@ function Modal({
|
|
|
37
38
|
children
|
|
38
39
|
}) {
|
|
39
40
|
const dialogRef = useRef(null);
|
|
40
|
-
const {
|
|
41
|
+
const { depth } = useModalStack(open);
|
|
41
42
|
const [expanded, setExpanded] = useState(false);
|
|
42
43
|
const [confirming, setConfirming] = useState(false);
|
|
43
44
|
const requestClose = () => {
|
|
@@ -53,16 +54,14 @@ function Modal({
|
|
|
53
54
|
useEffect(() => {
|
|
54
55
|
if (!confirmClose) setConfirming(false);
|
|
55
56
|
}, [confirmClose]);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (e.key !== "Escape") return;
|
|
57
|
+
useEscapeLayer(
|
|
58
|
+
open,
|
|
59
|
+
() => {
|
|
60
60
|
if (confirming) setConfirming(false);
|
|
61
61
|
else requestClose();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}, [open, closeOnEsc, isTop, confirming, confirmClose, onClose]);
|
|
62
|
+
},
|
|
63
|
+
closeOnEsc
|
|
64
|
+
);
|
|
66
65
|
useEffect(() => {
|
|
67
66
|
var _a;
|
|
68
67
|
if (!open) return;
|
|
@@ -78,11 +77,11 @@ function Modal({
|
|
|
78
77
|
"div",
|
|
79
78
|
{
|
|
80
79
|
className: cn(
|
|
81
|
-
"fixed inset-0 flex items-center justify-center",
|
|
80
|
+
"fixed inset-0 flex items-center justify-center p-density-2 sm:p-density-4",
|
|
82
81
|
// Nested modals dim less so the dialog they opened over stays visible.
|
|
83
82
|
depth === 0 ? "bg-black/40" : "bg-black/20"
|
|
84
83
|
),
|
|
85
|
-
style: { zIndex:
|
|
84
|
+
style: { zIndex: zIndex.modal + depth * zIndex.modalStep },
|
|
86
85
|
onClick: closeOnBackdrop ? requestClose : void 0,
|
|
87
86
|
role: "presentation",
|
|
88
87
|
children: /* @__PURE__ */ jsxs(
|
|
@@ -94,10 +93,14 @@ function Modal({
|
|
|
94
93
|
"aria-modal": "true",
|
|
95
94
|
"aria-label": typeof title === "string" ? title : void 0,
|
|
96
95
|
className: cn(
|
|
97
|
-
"relative
|
|
96
|
+
"relative flex w-full flex-col rounded-lg border border-border bg-background shadow-xl",
|
|
98
97
|
expanded ? sizeClass.full : sizeClass[size],
|
|
99
98
|
className
|
|
100
99
|
),
|
|
100
|
+
style: {
|
|
101
|
+
maxHeight: "calc(100dvh - 2rem)",
|
|
102
|
+
...expanded ? { height: "calc(100dvh - 2rem)" } : {}
|
|
103
|
+
},
|
|
101
104
|
onClick: (e) => e.stopPropagation(),
|
|
102
105
|
children: [
|
|
103
106
|
(title || subtitle || headerSlot || expandable || !hideClose) && /* @__PURE__ */ jsxs("div", { className: "px-density-4 py-density-3 border-b border-border", children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.js","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { Button } from \"../components/button\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\nimport { useModalStack } from \"./modalStack\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Copy for the discard-confirmation prompt shown before a guarded close. */\nexport type ConfirmCloseOptions = {\n /** Heading of the confirmation prompt. */\n title?: ReactNode;\n /** Body explaining what is lost on close. */\n message?: ReactNode;\n /** Label of the button that proceeds with closing. */\n confirmLabel?: string;\n /** Label of the button that dismisses the prompt and keeps the modal open. */\n cancelLabel?: string;\n};\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /**\n * Guard every close path (close button, Escape, backdrop) behind a discard\n * confirmation. `true` uses the default copy; pass options to customise it.\n * `onClose` only fires once the user confirms. Set this while the modal holds\n * unsaved edits and clear it (or pass `false`) once there is nothing to lose.\n */\n confirmClose?: boolean | ConfirmCloseOptions;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /**\n * Secondary header content rendered on its own row directly beneath the title.\n * The title stays a single row alongside the expand/close buttons, so a tall\n * subtitle (e.g. a tab switcher) never pushes those buttons down.\n */\n subtitle?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. Off by default; close via Escape or the close button. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm max-h-[90vh]\",\n md: \"max-w-md max-h-[90vh]\",\n lg: \"max-w-2xl max-h-[90vh]\",\n xl: \"max-w-4xl max-h-[90vh]\",\n full: \"max-w-[95vw] max-h-[95vh]\",\n};\n\nconst DEFAULT_CONFIRM: Required<ConfirmCloseOptions> = {\n title: \"Discard changes?\",\n message: \"You have unsaved changes. Closing now will discard them.\",\n confirmLabel: \"Discard\",\n cancelLabel: \"Keep editing\",\n};\n\nexport function Modal({\n open,\n onClose,\n confirmClose = false,\n title,\n subtitle,\n size = \"md\",\n closeOnBackdrop = false,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const { isTop, depth } = useModalStack(open);\n const [expanded, setExpanded] = useState(false);\n // When confirmClose is active, a close request opens this prompt instead of\n // closing outright; onClose only fires once the user confirms the discard.\n const [confirming, setConfirming] = useState(false);\n\n // requestClose is the single entry point for every close affordance. With the\n // guard on, it surfaces the confirmation; otherwise it closes immediately.\n const requestClose = () => {\n if (confirmClose) setConfirming(true);\n else onClose();\n };\n\n useEffect(() => {\n if (!open) {\n setExpanded(false);\n setConfirming(false);\n }\n }, [open]);\n\n // A close guard that disappears mid-prompt (changes saved/reverted) should\n // not leave a stale confirmation dialog up.\n useEffect(() => {\n if (!confirmClose) setConfirming(false);\n }, [confirmClose]);\n\n useEffect(() => {\n // Only the topmost modal handles Escape so a keypress closes one nested\n // layer at a time instead of every open modal at once.\n if (!open || !closeOnEsc || !isTop) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n // While the prompt is up, Escape dismisses the prompt rather than the modal.\n if (confirming) setConfirming(false);\n else requestClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, closeOnEsc, isTop, confirming, confirmClose, onClose]);\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n const overlay = (\n <div\n className={cn(\n \"fixed inset-0 flex items-center justify-center\",\n // Nested modals dim less so the dialog they opened over stays visible.\n depth === 0 ? \"bg-black/40\" : \"bg-black/20\",\n )}\n // Above the z-[9999] popover/overlay layer (dropdowns, tooltips, toasts) so a\n // modal opened from one of them stacks on top; +depth keeps nested modals\n // above their parent.\n style={{ zIndex: 10000 + depth * 10 }}\n onClick={closeOnBackdrop ? requestClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative bg-background border border-border rounded-lg shadow-xl w-full flex flex-col\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || subtitle || headerSlot || expandable || !hideClose) && (\n <div className=\"px-density-4 py-density-3 border-b border-border\">\n <div className=\"flex items-center gap-density-2\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={requestClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n {subtitle ? <div className=\"mt-density-2\">{subtitle}</div> : null}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n {confirming && (\n <ConfirmClosePrompt\n options={confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose }}\n onConfirm={() => {\n setConfirming(false);\n onClose();\n }}\n onCancel={() => setConfirming(false)}\n />\n )}\n </div>\n </div>\n );\n\n // Render into document.body so the fixed-position overlay escapes any ancestor\n // transform/overflow (e.g. a dropdown menu positioned via a CSS transform),\n // which would otherwise trap position:fixed inside that ancestor's box and clip\n // the modal to it.\n return typeof document !== \"undefined\" ? createPortal(overlay, document.body) : null;\n}\n\nfunction ConfirmClosePrompt({\n options,\n onConfirm,\n onCancel,\n}: {\n options: Required<ConfirmCloseOptions>;\n onConfirm: () => void;\n onCancel: () => void;\n}) {\n return (\n <div\n className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-label={typeof options.title === \"string\" ? options.title : \"Confirm close\"}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl\">\n <h3 className=\"text-sm font-semibold\">{options.title}</h3>\n <p className=\"mt-density-2 text-sm text-muted-foreground\">{options.message}</p>\n <div className=\"mt-density-4 flex justify-end gap-density-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n {options.cancelLabel}\n </Button>\n <Button variant=\"destructive\" size=\"sm\" onClick={onConfirm}>\n {options.confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Modal.js","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { Button } from \"../components/button\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\nimport { useEscapeLayer, useModalStack } from \"./modalStack\";\nimport { zIndex } from \"./zIndex\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Copy for the discard-confirmation prompt shown before a guarded close. */\nexport type ConfirmCloseOptions = {\n /** Heading of the confirmation prompt. */\n title?: ReactNode;\n /** Body explaining what is lost on close. */\n message?: ReactNode;\n /** Label of the button that proceeds with closing. */\n confirmLabel?: string;\n /** Label of the button that dismisses the prompt and keeps the modal open. */\n cancelLabel?: string;\n};\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /**\n * Guard every close path (close button, Escape, backdrop) behind a discard\n * confirmation. `true` uses the default copy; pass options to customise it.\n * `onClose` only fires once the user confirms. Set this while the modal holds\n * unsaved edits and clear it (or pass `false`) once there is nothing to lose.\n */\n confirmClose?: boolean | ConfirmCloseOptions;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /**\n * Secondary header content rendered on its own row directly beneath the title.\n * The title stays a single row alongside the expand/close buttons, so a tall\n * subtitle (e.g. a tab switcher) never pushes those buttons down.\n */\n subtitle?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. Off by default; close via Escape or the close button. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm\",\n md: \"max-w-md\",\n lg: \"max-w-2xl\",\n xl: \"max-w-4xl\",\n full: \"max-w-[95vw]\",\n};\n\nconst DEFAULT_CONFIRM: Required<ConfirmCloseOptions> = {\n title: \"Discard changes?\",\n message: \"You have unsaved changes. Closing now will discard them.\",\n confirmLabel: \"Discard\",\n cancelLabel: \"Keep editing\",\n};\n\nexport function Modal({\n open,\n onClose,\n confirmClose = false,\n title,\n subtitle,\n size = \"md\",\n closeOnBackdrop = false,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const { depth } = useModalStack(open);\n const [expanded, setExpanded] = useState(false);\n // When confirmClose is active, a close request opens this prompt instead of\n // closing outright; onClose only fires once the user confirms the discard.\n const [confirming, setConfirming] = useState(false);\n\n // requestClose is the single entry point for every close affordance. With the\n // guard on, it surfaces the confirmation; otherwise it closes immediately.\n const requestClose = () => {\n if (confirmClose) setConfirming(true);\n else onClose();\n };\n\n useEffect(() => {\n if (!open) {\n setExpanded(false);\n setConfirming(false);\n }\n }, [open]);\n\n // A close guard that disappears mid-prompt (changes saved/reverted) should\n // not leave a stale confirmation dialog up.\n useEffect(() => {\n if (!confirmClose) setConfirming(false);\n }, [confirmClose]);\n\n useEscapeLayer(\n open,\n () => {\n // While the prompt is up, Escape dismisses the prompt rather than the modal.\n if (confirming) setConfirming(false);\n else requestClose();\n },\n closeOnEsc,\n );\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n const overlay = (\n <div\n className={cn(\n \"fixed inset-0 flex items-center justify-center p-density-2 sm:p-density-4\",\n // Nested modals dim less so the dialog they opened over stays visible.\n depth === 0 ? \"bg-black/40\" : \"bg-black/20\",\n )}\n // Sits in the modal band of the centralized z-index scale; +depth keeps\n // nested modals above their parent. Floating content (dropdowns, tooltips)\n // computes its z relative to this via useFloatingZIndex so it clears the\n // modal instead of rendering behind it.\n style={{ zIndex: zIndex.modal + depth * zIndex.modalStep }}\n onClick={closeOnBackdrop ? requestClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative flex w-full flex-col rounded-lg border border-border bg-background shadow-xl\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n // Inline maxHeight (not an arbitrary Tailwind class) so the panel stays\n // viewport-bounded even when a consumer's Tailwind build can't generate\n // the dvh utility — e.g. www aliases clicky-ui to source but scans the\n // published dist. When expanded, fill the viewport.\n style={{\n maxHeight: \"calc(100dvh - 2rem)\",\n ...(expanded ? { height: \"calc(100dvh - 2rem)\" } : {}),\n }}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || subtitle || headerSlot || expandable || !hideClose) && (\n <div className=\"px-density-4 py-density-3 border-b border-border\">\n <div className=\"flex items-center gap-density-2\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={requestClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n {subtitle ? <div className=\"mt-density-2\">{subtitle}</div> : null}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n {confirming && (\n <ConfirmClosePrompt\n options={confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose }}\n onConfirm={() => {\n setConfirming(false);\n onClose();\n }}\n onCancel={() => setConfirming(false)}\n />\n )}\n </div>\n </div>\n );\n\n // Render into document.body so the fixed-position overlay escapes any ancestor\n // transform/overflow (e.g. a dropdown menu positioned via a CSS transform),\n // which would otherwise trap position:fixed inside that ancestor's box and clip\n // the modal to it.\n return typeof document !== \"undefined\" ? createPortal(overlay, document.body) : null;\n}\n\nfunction ConfirmClosePrompt({\n options,\n onConfirm,\n onCancel,\n}: {\n options: Required<ConfirmCloseOptions>;\n onConfirm: () => void;\n onCancel: () => void;\n}) {\n return (\n <div\n className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-label={typeof options.title === \"string\" ? options.title : \"Confirm close\"}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl\">\n <h3 className=\"text-sm font-semibold\">{options.title}</h3>\n <p className=\"mt-density-2 text-sm text-muted-foreground\">{options.message}</p>\n <div className=\"mt-density-4 flex justify-end gap-density-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n {options.cancelLabel}\n </Button>\n <Button variant=\"destructive\" size=\"sm\" onClick={onConfirm}>\n {options.confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
|
package/dist/overlay/Toast.cjs
CHANGED
|
@@ -6,6 +6,7 @@ const reactDom = require("react-dom");
|
|
|
6
6
|
const utils = require("../lib/utils.cjs");
|
|
7
7
|
const Icon = require("../data/Icon.cjs");
|
|
8
8
|
const toastContext = require("./toast-context.cjs");
|
|
9
|
+
const zIndex = require("./zIndex.cjs");
|
|
9
10
|
const UiClose = require("../icons/components/UiClose.cjs");
|
|
10
11
|
const DEFAULT_DURATION_MS = 2200;
|
|
11
12
|
const DOT_TONE = {
|
|
@@ -54,7 +55,14 @@ function ToastProvider({ children, durationMs = DEFAULT_DURATION_MS }) {
|
|
|
54
55
|
return /* @__PURE__ */ jsxRuntime.jsxs(toastContext.ToastContext.Provider, { value, children: [
|
|
55
56
|
children,
|
|
56
57
|
typeof document !== "undefined" && reactDom.createPortal(
|
|
57
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
58
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
59
|
+
"div",
|
|
60
|
+
{
|
|
61
|
+
className: "pointer-events-none fixed inset-x-0 bottom-6 flex flex-col items-center gap-density-2",
|
|
62
|
+
style: { zIndex: zIndex.zIndex.toast },
|
|
63
|
+
children: toasts.map((entry) => /* @__PURE__ */ jsxRuntime.jsx(ToastView, { entry, onDismiss: () => dismiss(entry.id) }, entry.id))
|
|
64
|
+
}
|
|
65
|
+
),
|
|
58
66
|
document.body
|
|
59
67
|
)
|
|
60
68
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.cjs","sources":["../../src/overlay/Toast.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiClose } from \"../icons\";\nimport {\n ToastContext,\n type ToastEntry,\n type ToastOptions,\n type ToastTone,\n} from \"./toast-context\";\n\nconst DEFAULT_DURATION_MS = 2200;\n\nconst DOT_TONE: Record<ToastTone, string> = {\n success: \"bg-emerald-500\",\n danger: \"bg-rose-500\",\n warning: \"bg-amber-500\",\n info: \"bg-sky-500\",\n neutral: \"bg-primary\",\n};\n\nexport type ToastProviderProps = {\n children: ReactNode;\n /** Default auto-dismiss delay applied when a toast omits `durationMs`. */\n durationMs?: number;\n};\n\n/**\n * Provides the imperative toast API and renders the bottom-center stack into a\n * portal (the Gavel `PRDetail` confirmation toast — a tone dot plus a transient\n * message). Built on clicky tokens; auto-dismisses after `durationMs`. Consume\n * the API with `useToast()` from `./toast-context`.\n */\nexport function ToastProvider({ children, durationMs = DEFAULT_DURATION_MS }: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const idRef = useRef(0);\n const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());\n\n const dismiss = useCallback((id: number) => {\n setToasts((cur) => cur.filter((t) => t.id !== id));\n const handle = timers.current.get(id);\n if (handle !== undefined) {\n clearTimeout(handle);\n timers.current.delete(id);\n }\n }, []);\n\n const toast = useCallback(\n (options: ToastOptions | string) => {\n const normalized: ToastOptions = typeof options === \"string\" ? { message: options } : options;\n const id = (idRef.current += 1);\n setToasts((cur) => [...cur, { tone: \"neutral\", ...normalized, id }]);\n const ttl = normalized.durationMs ?? durationMs;\n if (ttl > 0) {\n timers.current.set(\n id,\n setTimeout(() => dismiss(id), ttl),\n );\n }\n return id;\n },\n [dismiss, durationMs],\n );\n\n useEffect(() => {\n const pending = timers.current;\n return () => {\n pending.forEach((handle) => clearTimeout(handle));\n pending.clear();\n };\n }, []);\n\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss]);\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n {typeof document !== \"undefined\" &&\n createPortal(\n <div
|
|
1
|
+
{"version":3,"file":"Toast.cjs","sources":["../../src/overlay/Toast.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiClose } from \"../icons\";\nimport {\n ToastContext,\n type ToastEntry,\n type ToastOptions,\n type ToastTone,\n} from \"./toast-context\";\nimport { zIndex } from \"./zIndex\";\n\nconst DEFAULT_DURATION_MS = 2200;\n\nconst DOT_TONE: Record<ToastTone, string> = {\n success: \"bg-emerald-500\",\n danger: \"bg-rose-500\",\n warning: \"bg-amber-500\",\n info: \"bg-sky-500\",\n neutral: \"bg-primary\",\n};\n\nexport type ToastProviderProps = {\n children: ReactNode;\n /** Default auto-dismiss delay applied when a toast omits `durationMs`. */\n durationMs?: number;\n};\n\n/**\n * Provides the imperative toast API and renders the bottom-center stack into a\n * portal (the Gavel `PRDetail` confirmation toast — a tone dot plus a transient\n * message). Built on clicky tokens; auto-dismisses after `durationMs`. Consume\n * the API with `useToast()` from `./toast-context`.\n */\nexport function ToastProvider({ children, durationMs = DEFAULT_DURATION_MS }: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const idRef = useRef(0);\n const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());\n\n const dismiss = useCallback((id: number) => {\n setToasts((cur) => cur.filter((t) => t.id !== id));\n const handle = timers.current.get(id);\n if (handle !== undefined) {\n clearTimeout(handle);\n timers.current.delete(id);\n }\n }, []);\n\n const toast = useCallback(\n (options: ToastOptions | string) => {\n const normalized: ToastOptions = typeof options === \"string\" ? { message: options } : options;\n const id = (idRef.current += 1);\n setToasts((cur) => [...cur, { tone: \"neutral\", ...normalized, id }]);\n const ttl = normalized.durationMs ?? durationMs;\n if (ttl > 0) {\n timers.current.set(\n id,\n setTimeout(() => dismiss(id), ttl),\n );\n }\n return id;\n },\n [dismiss, durationMs],\n );\n\n useEffect(() => {\n const pending = timers.current;\n return () => {\n pending.forEach((handle) => clearTimeout(handle));\n pending.clear();\n };\n }, []);\n\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss]);\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n {typeof document !== \"undefined\" &&\n createPortal(\n <div\n className=\"pointer-events-none fixed inset-x-0 bottom-6 flex flex-col items-center gap-density-2\"\n style={{ zIndex: zIndex.toast }}\n >\n {toasts.map((entry) => (\n <ToastView key={entry.id} entry={entry} onDismiss={() => dismiss(entry.id)} />\n ))}\n </div>,\n document.body,\n )}\n </ToastContext.Provider>\n );\n}\n\nfunction ToastView({ entry, onDismiss }: { entry: ToastEntry; onDismiss: () => void }) {\n return (\n <div\n role=\"status\"\n className={cn(\n \"pointer-events-auto flex items-center gap-density-2 rounded-md border border-border\",\n \"bg-popover px-density-3 py-density-2 text-sm font-medium text-popover-foreground shadow-lg\",\n )}\n >\n <span className={cn(\"h-2 w-2 shrink-0 rounded-full\", DOT_TONE[entry.tone ?? \"neutral\"])} />\n <span>{entry.message}</span>\n <button\n type=\"button\"\n onClick={onDismiss}\n aria-label=\"Dismiss\"\n className=\"ml-1 text-muted-foreground transition-colors hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"h-3 w-3\" />\n </button>\n </div>\n );\n}\n"],"names":["useState","useRef","useCallback","useEffect","useMemo","jsxs","ToastContext","createPortal","jsx","zIndex","cn","Icon","UiClose"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../src/overlay/Toast.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../src/overlay/Toast.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAuB1F,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,UAAgC,EAAE,EAAE,kBAAkB,2CA0D/F"}
|
package/dist/overlay/Toast.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createPortal } from "react-dom";
|
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { Icon } from "../data/Icon.js";
|
|
6
6
|
import { ToastContext } from "./toast-context.js";
|
|
7
|
+
import { zIndex } from "./zIndex.js";
|
|
7
8
|
import { UiClose } from "../icons/components/UiClose.js";
|
|
8
9
|
const DEFAULT_DURATION_MS = 2200;
|
|
9
10
|
const DOT_TONE = {
|
|
@@ -52,7 +53,14 @@ function ToastProvider({ children, durationMs = DEFAULT_DURATION_MS }) {
|
|
|
52
53
|
return /* @__PURE__ */ jsxs(ToastContext.Provider, { value, children: [
|
|
53
54
|
children,
|
|
54
55
|
typeof document !== "undefined" && createPortal(
|
|
55
|
-
/* @__PURE__ */ jsx(
|
|
56
|
+
/* @__PURE__ */ jsx(
|
|
57
|
+
"div",
|
|
58
|
+
{
|
|
59
|
+
className: "pointer-events-none fixed inset-x-0 bottom-6 flex flex-col items-center gap-density-2",
|
|
60
|
+
style: { zIndex: zIndex.toast },
|
|
61
|
+
children: toasts.map((entry) => /* @__PURE__ */ jsx(ToastView, { entry, onDismiss: () => dismiss(entry.id) }, entry.id))
|
|
62
|
+
}
|
|
63
|
+
),
|
|
56
64
|
document.body
|
|
57
65
|
)
|
|
58
66
|
] });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.js","sources":["../../src/overlay/Toast.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiClose } from \"../icons\";\nimport {\n ToastContext,\n type ToastEntry,\n type ToastOptions,\n type ToastTone,\n} from \"./toast-context\";\n\nconst DEFAULT_DURATION_MS = 2200;\n\nconst DOT_TONE: Record<ToastTone, string> = {\n success: \"bg-emerald-500\",\n danger: \"bg-rose-500\",\n warning: \"bg-amber-500\",\n info: \"bg-sky-500\",\n neutral: \"bg-primary\",\n};\n\nexport type ToastProviderProps = {\n children: ReactNode;\n /** Default auto-dismiss delay applied when a toast omits `durationMs`. */\n durationMs?: number;\n};\n\n/**\n * Provides the imperative toast API and renders the bottom-center stack into a\n * portal (the Gavel `PRDetail` confirmation toast — a tone dot plus a transient\n * message). Built on clicky tokens; auto-dismisses after `durationMs`. Consume\n * the API with `useToast()` from `./toast-context`.\n */\nexport function ToastProvider({ children, durationMs = DEFAULT_DURATION_MS }: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const idRef = useRef(0);\n const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());\n\n const dismiss = useCallback((id: number) => {\n setToasts((cur) => cur.filter((t) => t.id !== id));\n const handle = timers.current.get(id);\n if (handle !== undefined) {\n clearTimeout(handle);\n timers.current.delete(id);\n }\n }, []);\n\n const toast = useCallback(\n (options: ToastOptions | string) => {\n const normalized: ToastOptions = typeof options === \"string\" ? { message: options } : options;\n const id = (idRef.current += 1);\n setToasts((cur) => [...cur, { tone: \"neutral\", ...normalized, id }]);\n const ttl = normalized.durationMs ?? durationMs;\n if (ttl > 0) {\n timers.current.set(\n id,\n setTimeout(() => dismiss(id), ttl),\n );\n }\n return id;\n },\n [dismiss, durationMs],\n );\n\n useEffect(() => {\n const pending = timers.current;\n return () => {\n pending.forEach((handle) => clearTimeout(handle));\n pending.clear();\n };\n }, []);\n\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss]);\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n {typeof document !== \"undefined\" &&\n createPortal(\n <div
|
|
1
|
+
{"version":3,"file":"Toast.js","sources":["../../src/overlay/Toast.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiClose } from \"../icons\";\nimport {\n ToastContext,\n type ToastEntry,\n type ToastOptions,\n type ToastTone,\n} from \"./toast-context\";\nimport { zIndex } from \"./zIndex\";\n\nconst DEFAULT_DURATION_MS = 2200;\n\nconst DOT_TONE: Record<ToastTone, string> = {\n success: \"bg-emerald-500\",\n danger: \"bg-rose-500\",\n warning: \"bg-amber-500\",\n info: \"bg-sky-500\",\n neutral: \"bg-primary\",\n};\n\nexport type ToastProviderProps = {\n children: ReactNode;\n /** Default auto-dismiss delay applied when a toast omits `durationMs`. */\n durationMs?: number;\n};\n\n/**\n * Provides the imperative toast API and renders the bottom-center stack into a\n * portal (the Gavel `PRDetail` confirmation toast — a tone dot plus a transient\n * message). Built on clicky tokens; auto-dismisses after `durationMs`. Consume\n * the API with `useToast()` from `./toast-context`.\n */\nexport function ToastProvider({ children, durationMs = DEFAULT_DURATION_MS }: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const idRef = useRef(0);\n const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());\n\n const dismiss = useCallback((id: number) => {\n setToasts((cur) => cur.filter((t) => t.id !== id));\n const handle = timers.current.get(id);\n if (handle !== undefined) {\n clearTimeout(handle);\n timers.current.delete(id);\n }\n }, []);\n\n const toast = useCallback(\n (options: ToastOptions | string) => {\n const normalized: ToastOptions = typeof options === \"string\" ? { message: options } : options;\n const id = (idRef.current += 1);\n setToasts((cur) => [...cur, { tone: \"neutral\", ...normalized, id }]);\n const ttl = normalized.durationMs ?? durationMs;\n if (ttl > 0) {\n timers.current.set(\n id,\n setTimeout(() => dismiss(id), ttl),\n );\n }\n return id;\n },\n [dismiss, durationMs],\n );\n\n useEffect(() => {\n const pending = timers.current;\n return () => {\n pending.forEach((handle) => clearTimeout(handle));\n pending.clear();\n };\n }, []);\n\n const value = useMemo(() => ({ toast, dismiss }), [toast, dismiss]);\n\n return (\n <ToastContext.Provider value={value}>\n {children}\n {typeof document !== \"undefined\" &&\n createPortal(\n <div\n className=\"pointer-events-none fixed inset-x-0 bottom-6 flex flex-col items-center gap-density-2\"\n style={{ zIndex: zIndex.toast }}\n >\n {toasts.map((entry) => (\n <ToastView key={entry.id} entry={entry} onDismiss={() => dismiss(entry.id)} />\n ))}\n </div>,\n document.body,\n )}\n </ToastContext.Provider>\n );\n}\n\nfunction ToastView({ entry, onDismiss }: { entry: ToastEntry; onDismiss: () => void }) {\n return (\n <div\n role=\"status\"\n className={cn(\n \"pointer-events-auto flex items-center gap-density-2 rounded-md border border-border\",\n \"bg-popover px-density-3 py-density-2 text-sm font-medium text-popover-foreground shadow-lg\",\n )}\n >\n <span className={cn(\"h-2 w-2 shrink-0 rounded-full\", DOT_TONE[entry.tone ?? \"neutral\"])} />\n <span>{entry.message}</span>\n <button\n type=\"button\"\n onClick={onDismiss}\n aria-label=\"Dismiss\"\n className=\"ml-1 text-muted-foreground transition-colors hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"h-3 w-3\" />\n </button>\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;;;AAaA,MAAM,sBAAsB;AAE5B,MAAM,WAAsC;AAAA,EAC1C,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,MAAM;AAAA,EACN,SAAS;AACX;AAcO,SAAS,cAAc,EAAE,UAAU,aAAa,uBAA2C;AAChG,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAuB,CAAA,CAAE;AACrD,QAAM,QAAQ,OAAO,CAAC;AACtB,QAAM,SAAS,OAAmD,oBAAI,KAAK;AAE3E,QAAM,UAAU,YAAY,CAAC,OAAe;AAC1C,cAAU,CAAC,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AACjD,UAAM,SAAS,OAAO,QAAQ,IAAI,EAAE;AACpC,QAAI,WAAW,QAAW;AACxB,mBAAa,MAAM;AACnB,aAAO,QAAQ,OAAO,EAAE;AAAA,IAC1B;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAQ;AAAA,IACZ,CAAC,YAAmC;AAClC,YAAM,aAA2B,OAAO,YAAY,WAAW,EAAE,SAAS,YAAY;AACtF,YAAM,KAAM,MAAM,WAAW;AAC7B,gBAAU,CAAC,QAAQ,CAAC,GAAG,KAAK,EAAE,MAAM,WAAW,GAAG,YAAY,GAAA,CAAI,CAAC;AACnE,YAAM,MAAM,WAAW,cAAc;AACrC,UAAI,MAAM,GAAG;AACX,eAAO,QAAQ;AAAA,UACb;AAAA,UACA,WAAW,MAAM,QAAQ,EAAE,GAAG,GAAG;AAAA,QAAA;AAAA,MAErC;AACA,aAAO;AAAA,IACT;AAAA,IACA,CAAC,SAAS,UAAU;AAAA,EAAA;AAGtB,YAAU,MAAM;AACd,UAAM,UAAU,OAAO;AACvB,WAAO,MAAM;AACX,cAAQ,QAAQ,CAAC,WAAW,aAAa,MAAM,CAAC;AAChD,cAAQ,MAAA;AAAA,IACV;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,QAAQ,QAAQ,OAAO,EAAE,OAAO,YAAY,CAAC,OAAO,OAAO,CAAC;AAElE,SACE,qBAAC,aAAa,UAAb,EAAsB,OACpB,UAAA;AAAA,IAAA;AAAA,IACA,OAAO,aAAa,eACnB;AAAA,MACE;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO,EAAE,QAAQ,OAAO,MAAA;AAAA,UAEvB,UAAA,OAAO,IAAI,CAAC,8BACV,WAAA,EAAyB,OAAc,WAAW,MAAM,QAAQ,MAAM,EAAE,EAAA,GAAzD,MAAM,EAAsD,CAC7E;AAAA,QAAA;AAAA,MAAA;AAAA,MAEH,SAAS;AAAA,IAAA;AAAA,EACX,GACJ;AAEJ;AAEA,SAAS,UAAU,EAAE,OAAO,aAA2D;AACrF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,MAGF,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAW,GAAG,iCAAiC,SAAS,MAAM,QAAQ,SAAS,CAAC,EAAA,CAAG;AAAA,QACzF,oBAAC,QAAA,EAAM,UAAA,MAAM,QAAA,CAAQ;AAAA,QACrB;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,cAAW;AAAA,YACX,WAAU;AAAA,YAEV,UAAA,oBAAC,MAAA,EAAK,MAAM,SAAS,WAAU,UAAA,CAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MAC3C;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const react = require("react");
|
|
4
|
+
const zIndex = require("./zIndex.cjs");
|
|
4
5
|
const stack = [];
|
|
5
6
|
const listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
const escapeStack = [];
|
|
8
|
+
let escapeDocument = null;
|
|
6
9
|
function emit() {
|
|
7
10
|
for (const listener of listeners) listener();
|
|
8
11
|
}
|
|
@@ -15,6 +18,30 @@ function subscribe(listener) {
|
|
|
15
18
|
function getSnapshot() {
|
|
16
19
|
return stack.join("\0");
|
|
17
20
|
}
|
|
21
|
+
function onEscapeKeyDown(event) {
|
|
22
|
+
if (event.defaultPrevented || event.key !== "Escape" || event.isComposing || event.keyCode === 229) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const top = escapeStack[escapeStack.length - 1];
|
|
26
|
+
if (!top) return;
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
event.stopPropagation();
|
|
29
|
+
event.stopImmediatePropagation();
|
|
30
|
+
top.onEscape();
|
|
31
|
+
}
|
|
32
|
+
function ensureEscapeListener() {
|
|
33
|
+
if (escapeDocument || typeof document === "undefined") return;
|
|
34
|
+
escapeDocument = document;
|
|
35
|
+
escapeDocument.addEventListener("keydown", onEscapeKeyDown);
|
|
36
|
+
}
|
|
37
|
+
function removeEscapeLayer(id) {
|
|
38
|
+
const index = escapeStack.findIndex((entry) => entry.id === id);
|
|
39
|
+
if (index !== -1) escapeStack.splice(index, 1);
|
|
40
|
+
if (escapeStack.length === 0 && escapeDocument) {
|
|
41
|
+
escapeDocument.removeEventListener("keydown", onEscapeKeyDown);
|
|
42
|
+
escapeDocument = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
18
45
|
function useModalStack(open) {
|
|
19
46
|
const id = react.useId();
|
|
20
47
|
react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
@@ -29,7 +56,32 @@ function useModalStack(open) {
|
|
|
29
56
|
};
|
|
30
57
|
}, [open, id]);
|
|
31
58
|
const depth = stack.indexOf(id);
|
|
32
|
-
return {
|
|
59
|
+
return {
|
|
60
|
+
isTop: depth !== -1 && depth === stack.length - 1,
|
|
61
|
+
depth: depth === -1 ? 0 : depth
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function useEscapeLayer(open, onEscape, enabled = true) {
|
|
65
|
+
const id = react.useId();
|
|
66
|
+
const onEscapeRef = react.useRef(onEscape);
|
|
67
|
+
onEscapeRef.current = onEscape;
|
|
68
|
+
react.useEffect(() => {
|
|
69
|
+
if (!open || !enabled) return;
|
|
70
|
+
ensureEscapeListener();
|
|
71
|
+
escapeStack.push({
|
|
72
|
+
id,
|
|
73
|
+
onEscape: () => onEscapeRef.current()
|
|
74
|
+
});
|
|
75
|
+
return () => removeEscapeLayer(id);
|
|
76
|
+
}, [enabled, id, open]);
|
|
77
|
+
}
|
|
78
|
+
function useFloatingZIndex() {
|
|
79
|
+
react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
80
|
+
if (stack.length === 0) return zIndex.zIndex.popover;
|
|
81
|
+
const topModalZ = zIndex.zIndex.modal + (stack.length - 1) * zIndex.zIndex.modalStep;
|
|
82
|
+
return topModalZ + zIndex.zIndex.popoverOverModalOffset;
|
|
33
83
|
}
|
|
84
|
+
exports.useEscapeLayer = useEscapeLayer;
|
|
85
|
+
exports.useFloatingZIndex = useFloatingZIndex;
|
|
34
86
|
exports.useModalStack = useModalStack;
|
|
35
87
|
//# sourceMappingURL=modalStack.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modalStack.cjs","sources":["../../src/overlay/modalStack.ts"],"sourcesContent":["import { useEffect, useId, useSyncExternalStore } from \"react\";\n\n// A process-wide stack of the currently open modals, topmost last. Modals mount\n// independently (no shared provider), so the stack lives at module scope and is\n// shared through useSyncExternalStore. It lets nested modals cooperate: only the\n// topmost reacts to Escape, and each renders above the one it opened over.\nconst stack: string[] = [];\nconst listeners = new Set<() => void>();\n\nfunction emit() {\n for (const listener of listeners) listener();\n}\n\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n// The snapshot is the stack identity; it only changes when an entry is pushed or\n// popped, so subscribers re-render exactly when their position could have moved.\nfunction getSnapshot() {\n return stack.join(\"
|
|
1
|
+
{"version":3,"file":"modalStack.cjs","sources":["../../src/overlay/modalStack.ts"],"sourcesContent":["import { useEffect, useId, useRef, useSyncExternalStore } from \"react\";\nimport { zIndex } from \"./zIndex\";\n\n// A process-wide stack of the currently open modals, topmost last. Modals mount\n// independently (no shared provider), so the stack lives at module scope and is\n// shared through useSyncExternalStore. It lets nested modals cooperate: only the\n// topmost reacts to Escape, and each renders above the one it opened over.\nconst stack: string[] = [];\nconst listeners = new Set<() => void>();\nconst escapeStack: Array<{ id: string; onEscape: () => void }> = [];\nlet escapeDocument: Document | null = null;\n\nfunction emit() {\n for (const listener of listeners) listener();\n}\n\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n// The snapshot is the stack identity; it only changes when an entry is pushed or\n// popped, so subscribers re-render exactly when their position could have moved.\nfunction getSnapshot() {\n return stack.join(\"\\u0000\");\n}\n\nfunction onEscapeKeyDown(event: KeyboardEvent) {\n if (\n event.defaultPrevented ||\n event.key !== \"Escape\" ||\n event.isComposing ||\n (event as KeyboardEvent & { keyCode?: number }).keyCode === 229\n ) {\n return;\n }\n\n const top = escapeStack[escapeStack.length - 1];\n if (!top) return;\n\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n top.onEscape();\n}\n\nfunction ensureEscapeListener() {\n if (escapeDocument || typeof document === \"undefined\") return;\n escapeDocument = document;\n escapeDocument.addEventListener(\"keydown\", onEscapeKeyDown);\n}\n\nfunction removeEscapeLayer(id: string) {\n const index = escapeStack.findIndex((entry) => entry.id === id);\n if (index !== -1) escapeStack.splice(index, 1);\n\n if (escapeStack.length === 0 && escapeDocument) {\n escapeDocument.removeEventListener(\"keydown\", onEscapeKeyDown);\n escapeDocument = null;\n }\n}\n\nexport type ModalStackPosition = {\n /** Whether this modal is the topmost open one and should own global affordances. */\n isTop: boolean;\n /** Zero-based depth in the stack; drives the stacking order of nested modals. */\n depth: number;\n};\n\n/**\n * Registers a modal in the global stack while `open`, returning its live\n * position. Used so a nested modal sits above its opener and only the topmost\n * modal closes on Escape.\n */\nexport function useModalStack(open: boolean): ModalStackPosition {\n const id = useId();\n useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n useEffect(() => {\n if (!open) return;\n stack.push(id);\n emit();\n return () => {\n const index = stack.indexOf(id);\n if (index !== -1) stack.splice(index, 1);\n emit();\n };\n }, [open, id]);\n\n const depth = stack.indexOf(id);\n return {\n isTop: depth !== -1 && depth === stack.length - 1,\n depth: depth === -1 ? 0 : depth,\n };\n}\n\n/**\n * Registers an Escape-closeable overlay while `open`. The topmost registered\n * layer owns Escape, so a keypress dismisses one dialog/menu layer at a time.\n */\nexport function useEscapeLayer(open: boolean, onEscape: () => void, enabled = true) {\n const id = useId();\n const onEscapeRef = useRef(onEscape);\n onEscapeRef.current = onEscape;\n\n useEffect(() => {\n if (!open || !enabled) return;\n\n ensureEscapeListener();\n escapeStack.push({\n id,\n onEscape: () => onEscapeRef.current(),\n });\n\n return () => removeEscapeLayer(id);\n }, [enabled, id, open]);\n}\n\n/**\n * z-index a floating layer (dropdown / combobox / tooltip / popover) should use\n * so it renders above any open modal — and below the next nested modal — or at\n * the popover floor when no modal is open. Subscribes to the modal stack so the\n * value tracks modals opening and closing.\n */\nexport function useFloatingZIndex(): number {\n useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n if (stack.length === 0) return zIndex.popover;\n const topModalZ = zIndex.modal + (stack.length - 1) * zIndex.modalStep;\n return topModalZ + zIndex.popoverOverModalOffset;\n}\n"],"names":["useId","useSyncExternalStore","useEffect","useRef","zIndex"],"mappings":";;;;AAOA,MAAM,QAAkB,CAAA;AACxB,MAAM,gCAAgB,IAAA;AACtB,MAAM,cAA2D,CAAA;AACjE,IAAI,iBAAkC;AAEtC,SAAS,OAAO;AACd,aAAW,YAAY,UAAW,UAAA;AACpC;AAEA,SAAS,UAAU,UAAsB;AACvC,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAIA,SAAS,cAAc;AACrB,SAAO,MAAM,KAAK,IAAQ;AAC5B;AAEA,SAAS,gBAAgB,OAAsB;AAC7C,MACE,MAAM,oBACN,MAAM,QAAQ,YACd,MAAM,eACL,MAA+C,YAAY,KAC5D;AACA;AAAA,EACF;AAEA,QAAM,MAAM,YAAY,YAAY,SAAS,CAAC;AAC9C,MAAI,CAAC,IAAK;AAEV,QAAM,eAAA;AACN,QAAM,gBAAA;AACN,QAAM,yBAAA;AACN,MAAI,SAAA;AACN;AAEA,SAAS,uBAAuB;AAC9B,MAAI,kBAAkB,OAAO,aAAa,YAAa;AACvD,mBAAiB;AACjB,iBAAe,iBAAiB,WAAW,eAAe;AAC5D;AAEA,SAAS,kBAAkB,IAAY;AACrC,QAAM,QAAQ,YAAY,UAAU,CAAC,UAAU,MAAM,OAAO,EAAE;AAC9D,MAAI,UAAU,GAAI,aAAY,OAAO,OAAO,CAAC;AAE7C,MAAI,YAAY,WAAW,KAAK,gBAAgB;AAC9C,mBAAe,oBAAoB,WAAW,eAAe;AAC7D,qBAAiB;AAAA,EACnB;AACF;AAcO,SAAS,cAAc,MAAmC;AAC/D,QAAM,KAAKA,MAAAA,MAAA;AACXC,6BAAqB,WAAW,aAAa,WAAW;AAExDC,QAAAA,UAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,KAAK,EAAE;AACb,SAAA;AACA,WAAO,MAAM;AACX,YAAM,QAAQ,MAAM,QAAQ,EAAE;AAC9B,UAAI,UAAU,GAAI,OAAM,OAAO,OAAO,CAAC;AACvC,WAAA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,EAAE,CAAC;AAEb,QAAM,QAAQ,MAAM,QAAQ,EAAE;AAC9B,SAAO;AAAA,IACL,OAAO,UAAU,MAAM,UAAU,MAAM,SAAS;AAAA,IAChD,OAAO,UAAU,KAAK,IAAI;AAAA,EAAA;AAE9B;AAMO,SAAS,eAAe,MAAe,UAAsB,UAAU,MAAM;AAClF,QAAM,KAAKF,MAAAA,MAAA;AACX,QAAM,cAAcG,MAAAA,OAAO,QAAQ;AACnC,cAAY,UAAU;AAEtBD,QAAAA,UAAU,MAAM;AACd,QAAI,CAAC,QAAQ,CAAC,QAAS;AAEvB,yBAAA;AACA,gBAAY,KAAK;AAAA,MACf;AAAA,MACA,UAAU,MAAM,YAAY,QAAA;AAAA,IAAQ,CACrC;AAED,WAAO,MAAM,kBAAkB,EAAE;AAAA,EACnC,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC;AACxB;AAQO,SAAS,oBAA4B;AAC1CD,6BAAqB,WAAW,aAAa,WAAW;AACxD,MAAI,MAAM,WAAW,EAAG,QAAOG,OAAAA,OAAO;AACtC,QAAM,YAAYA,OAAAA,OAAO,SAAS,MAAM,SAAS,KAAKA,OAAAA,OAAO;AAC7D,SAAO,YAAYA,OAAAA,OAAO;AAC5B;;;;"}
|
|
@@ -10,4 +10,16 @@ export type ModalStackPosition = {
|
|
|
10
10
|
* modal closes on Escape.
|
|
11
11
|
*/
|
|
12
12
|
export declare function useModalStack(open: boolean): ModalStackPosition;
|
|
13
|
+
/**
|
|
14
|
+
* Registers an Escape-closeable overlay while `open`. The topmost registered
|
|
15
|
+
* layer owns Escape, so a keypress dismisses one dialog/menu layer at a time.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useEscapeLayer(open: boolean, onEscape: () => void, enabled?: boolean): void;
|
|
18
|
+
/**
|
|
19
|
+
* z-index a floating layer (dropdown / combobox / tooltip / popover) should use
|
|
20
|
+
* so it renders above any open modal — and below the next nested modal — or at
|
|
21
|
+
* the popover floor when no modal is open. Subscribes to the modal stack so the
|
|
22
|
+
* value tracks modals opening and closing.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useFloatingZIndex(): number;
|
|
13
25
|
//# sourceMappingURL=modalStack.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modalStack.d.ts","sourceRoot":"","sources":["../../src/overlay/modalStack.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"modalStack.d.ts","sourceRoot":"","sources":["../../src/overlay/modalStack.ts"],"names":[],"mappings":"AAgEA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,oFAAoF;IACpF,KAAK,EAAE,OAAO,CAAC;IACf,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,kBAAkB,CAoB/D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,OAAO,UAAO,QAgBjF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAK1C"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { useId, useSyncExternalStore, useEffect } from "react";
|
|
1
|
+
import { useId, useSyncExternalStore, useEffect, useRef } from "react";
|
|
2
|
+
import { zIndex } from "./zIndex.js";
|
|
2
3
|
const stack = [];
|
|
3
4
|
const listeners = /* @__PURE__ */ new Set();
|
|
5
|
+
const escapeStack = [];
|
|
6
|
+
let escapeDocument = null;
|
|
4
7
|
function emit() {
|
|
5
8
|
for (const listener of listeners) listener();
|
|
6
9
|
}
|
|
@@ -13,6 +16,30 @@ function subscribe(listener) {
|
|
|
13
16
|
function getSnapshot() {
|
|
14
17
|
return stack.join("\0");
|
|
15
18
|
}
|
|
19
|
+
function onEscapeKeyDown(event) {
|
|
20
|
+
if (event.defaultPrevented || event.key !== "Escape" || event.isComposing || event.keyCode === 229) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const top = escapeStack[escapeStack.length - 1];
|
|
24
|
+
if (!top) return;
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
event.stopPropagation();
|
|
27
|
+
event.stopImmediatePropagation();
|
|
28
|
+
top.onEscape();
|
|
29
|
+
}
|
|
30
|
+
function ensureEscapeListener() {
|
|
31
|
+
if (escapeDocument || typeof document === "undefined") return;
|
|
32
|
+
escapeDocument = document;
|
|
33
|
+
escapeDocument.addEventListener("keydown", onEscapeKeyDown);
|
|
34
|
+
}
|
|
35
|
+
function removeEscapeLayer(id) {
|
|
36
|
+
const index = escapeStack.findIndex((entry) => entry.id === id);
|
|
37
|
+
if (index !== -1) escapeStack.splice(index, 1);
|
|
38
|
+
if (escapeStack.length === 0 && escapeDocument) {
|
|
39
|
+
escapeDocument.removeEventListener("keydown", onEscapeKeyDown);
|
|
40
|
+
escapeDocument = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
16
43
|
function useModalStack(open) {
|
|
17
44
|
const id = useId();
|
|
18
45
|
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
@@ -27,9 +54,34 @@ function useModalStack(open) {
|
|
|
27
54
|
};
|
|
28
55
|
}, [open, id]);
|
|
29
56
|
const depth = stack.indexOf(id);
|
|
30
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
isTop: depth !== -1 && depth === stack.length - 1,
|
|
59
|
+
depth: depth === -1 ? 0 : depth
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function useEscapeLayer(open, onEscape, enabled = true) {
|
|
63
|
+
const id = useId();
|
|
64
|
+
const onEscapeRef = useRef(onEscape);
|
|
65
|
+
onEscapeRef.current = onEscape;
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!open || !enabled) return;
|
|
68
|
+
ensureEscapeListener();
|
|
69
|
+
escapeStack.push({
|
|
70
|
+
id,
|
|
71
|
+
onEscape: () => onEscapeRef.current()
|
|
72
|
+
});
|
|
73
|
+
return () => removeEscapeLayer(id);
|
|
74
|
+
}, [enabled, id, open]);
|
|
75
|
+
}
|
|
76
|
+
function useFloatingZIndex() {
|
|
77
|
+
useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
78
|
+
if (stack.length === 0) return zIndex.popover;
|
|
79
|
+
const topModalZ = zIndex.modal + (stack.length - 1) * zIndex.modalStep;
|
|
80
|
+
return topModalZ + zIndex.popoverOverModalOffset;
|
|
31
81
|
}
|
|
32
82
|
export {
|
|
83
|
+
useEscapeLayer,
|
|
84
|
+
useFloatingZIndex,
|
|
33
85
|
useModalStack
|
|
34
86
|
};
|
|
35
87
|
//# sourceMappingURL=modalStack.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modalStack.js","sources":["../../src/overlay/modalStack.ts"],"sourcesContent":["import { useEffect, useId, useSyncExternalStore } from \"react\";\n\n// A process-wide stack of the currently open modals, topmost last. Modals mount\n// independently (no shared provider), so the stack lives at module scope and is\n// shared through useSyncExternalStore. It lets nested modals cooperate: only the\n// topmost reacts to Escape, and each renders above the one it opened over.\nconst stack: string[] = [];\nconst listeners = new Set<() => void>();\n\nfunction emit() {\n for (const listener of listeners) listener();\n}\n\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n// The snapshot is the stack identity; it only changes when an entry is pushed or\n// popped, so subscribers re-render exactly when their position could have moved.\nfunction getSnapshot() {\n return stack.join(\"
|
|
1
|
+
{"version":3,"file":"modalStack.js","sources":["../../src/overlay/modalStack.ts"],"sourcesContent":["import { useEffect, useId, useRef, useSyncExternalStore } from \"react\";\nimport { zIndex } from \"./zIndex\";\n\n// A process-wide stack of the currently open modals, topmost last. Modals mount\n// independently (no shared provider), so the stack lives at module scope and is\n// shared through useSyncExternalStore. It lets nested modals cooperate: only the\n// topmost reacts to Escape, and each renders above the one it opened over.\nconst stack: string[] = [];\nconst listeners = new Set<() => void>();\nconst escapeStack: Array<{ id: string; onEscape: () => void }> = [];\nlet escapeDocument: Document | null = null;\n\nfunction emit() {\n for (const listener of listeners) listener();\n}\n\nfunction subscribe(listener: () => void) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n// The snapshot is the stack identity; it only changes when an entry is pushed or\n// popped, so subscribers re-render exactly when their position could have moved.\nfunction getSnapshot() {\n return stack.join(\"\\u0000\");\n}\n\nfunction onEscapeKeyDown(event: KeyboardEvent) {\n if (\n event.defaultPrevented ||\n event.key !== \"Escape\" ||\n event.isComposing ||\n (event as KeyboardEvent & { keyCode?: number }).keyCode === 229\n ) {\n return;\n }\n\n const top = escapeStack[escapeStack.length - 1];\n if (!top) return;\n\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n top.onEscape();\n}\n\nfunction ensureEscapeListener() {\n if (escapeDocument || typeof document === \"undefined\") return;\n escapeDocument = document;\n escapeDocument.addEventListener(\"keydown\", onEscapeKeyDown);\n}\n\nfunction removeEscapeLayer(id: string) {\n const index = escapeStack.findIndex((entry) => entry.id === id);\n if (index !== -1) escapeStack.splice(index, 1);\n\n if (escapeStack.length === 0 && escapeDocument) {\n escapeDocument.removeEventListener(\"keydown\", onEscapeKeyDown);\n escapeDocument = null;\n }\n}\n\nexport type ModalStackPosition = {\n /** Whether this modal is the topmost open one and should own global affordances. */\n isTop: boolean;\n /** Zero-based depth in the stack; drives the stacking order of nested modals. */\n depth: number;\n};\n\n/**\n * Registers a modal in the global stack while `open`, returning its live\n * position. Used so a nested modal sits above its opener and only the topmost\n * modal closes on Escape.\n */\nexport function useModalStack(open: boolean): ModalStackPosition {\n const id = useId();\n useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n useEffect(() => {\n if (!open) return;\n stack.push(id);\n emit();\n return () => {\n const index = stack.indexOf(id);\n if (index !== -1) stack.splice(index, 1);\n emit();\n };\n }, [open, id]);\n\n const depth = stack.indexOf(id);\n return {\n isTop: depth !== -1 && depth === stack.length - 1,\n depth: depth === -1 ? 0 : depth,\n };\n}\n\n/**\n * Registers an Escape-closeable overlay while `open`. The topmost registered\n * layer owns Escape, so a keypress dismisses one dialog/menu layer at a time.\n */\nexport function useEscapeLayer(open: boolean, onEscape: () => void, enabled = true) {\n const id = useId();\n const onEscapeRef = useRef(onEscape);\n onEscapeRef.current = onEscape;\n\n useEffect(() => {\n if (!open || !enabled) return;\n\n ensureEscapeListener();\n escapeStack.push({\n id,\n onEscape: () => onEscapeRef.current(),\n });\n\n return () => removeEscapeLayer(id);\n }, [enabled, id, open]);\n}\n\n/**\n * z-index a floating layer (dropdown / combobox / tooltip / popover) should use\n * so it renders above any open modal — and below the next nested modal — or at\n * the popover floor when no modal is open. Subscribes to the modal stack so the\n * value tracks modals opening and closing.\n */\nexport function useFloatingZIndex(): number {\n useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n if (stack.length === 0) return zIndex.popover;\n const topModalZ = zIndex.modal + (stack.length - 1) * zIndex.modalStep;\n return topModalZ + zIndex.popoverOverModalOffset;\n}\n"],"names":[],"mappings":";;AAOA,MAAM,QAAkB,CAAA;AACxB,MAAM,gCAAgB,IAAA;AACtB,MAAM,cAA2D,CAAA;AACjE,IAAI,iBAAkC;AAEtC,SAAS,OAAO;AACd,aAAW,YAAY,UAAW,UAAA;AACpC;AAEA,SAAS,UAAU,UAAsB;AACvC,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAIA,SAAS,cAAc;AACrB,SAAO,MAAM,KAAK,IAAQ;AAC5B;AAEA,SAAS,gBAAgB,OAAsB;AAC7C,MACE,MAAM,oBACN,MAAM,QAAQ,YACd,MAAM,eACL,MAA+C,YAAY,KAC5D;AACA;AAAA,EACF;AAEA,QAAM,MAAM,YAAY,YAAY,SAAS,CAAC;AAC9C,MAAI,CAAC,IAAK;AAEV,QAAM,eAAA;AACN,QAAM,gBAAA;AACN,QAAM,yBAAA;AACN,MAAI,SAAA;AACN;AAEA,SAAS,uBAAuB;AAC9B,MAAI,kBAAkB,OAAO,aAAa,YAAa;AACvD,mBAAiB;AACjB,iBAAe,iBAAiB,WAAW,eAAe;AAC5D;AAEA,SAAS,kBAAkB,IAAY;AACrC,QAAM,QAAQ,YAAY,UAAU,CAAC,UAAU,MAAM,OAAO,EAAE;AAC9D,MAAI,UAAU,GAAI,aAAY,OAAO,OAAO,CAAC;AAE7C,MAAI,YAAY,WAAW,KAAK,gBAAgB;AAC9C,mBAAe,oBAAoB,WAAW,eAAe;AAC7D,qBAAiB;AAAA,EACnB;AACF;AAcO,SAAS,cAAc,MAAmC;AAC/D,QAAM,KAAK,MAAA;AACX,uBAAqB,WAAW,aAAa,WAAW;AAExD,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,KAAK,EAAE;AACb,SAAA;AACA,WAAO,MAAM;AACX,YAAM,QAAQ,MAAM,QAAQ,EAAE;AAC9B,UAAI,UAAU,GAAI,OAAM,OAAO,OAAO,CAAC;AACvC,WAAA;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,EAAE,CAAC;AAEb,QAAM,QAAQ,MAAM,QAAQ,EAAE;AAC9B,SAAO;AAAA,IACL,OAAO,UAAU,MAAM,UAAU,MAAM,SAAS;AAAA,IAChD,OAAO,UAAU,KAAK,IAAI;AAAA,EAAA;AAE9B;AAMO,SAAS,eAAe,MAAe,UAAsB,UAAU,MAAM;AAClF,QAAM,KAAK,MAAA;AACX,QAAM,cAAc,OAAO,QAAQ;AACnC,cAAY,UAAU;AAEtB,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,CAAC,QAAS;AAEvB,yBAAA;AACA,gBAAY,KAAK;AAAA,MACf;AAAA,MACA,UAAU,MAAM,YAAY,QAAA;AAAA,IAAQ,CACrC;AAED,WAAO,MAAM,kBAAkB,EAAE;AAAA,EACnC,GAAG,CAAC,SAAS,IAAI,IAAI,CAAC;AACxB;AAQO,SAAS,oBAA4B;AAC1C,uBAAqB,WAAW,aAAa,WAAW;AACxD,MAAI,MAAM,WAAW,EAAG,QAAO,OAAO;AACtC,QAAM,YAAY,OAAO,SAAS,MAAM,SAAS,KAAK,OAAO;AAC7D,SAAO,YAAY,OAAO;AAC5B;"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const zIndex = {
|
|
4
|
+
/** Persistent chat launcher. */
|
|
5
|
+
chatFab: 60,
|
|
6
|
+
/** Floating chat windows; `ChatWindowManager` increments from this base. */
|
|
7
|
+
chatWindow: 61,
|
|
8
|
+
/** Floating transient content when NO modal is open (above app chrome). */
|
|
9
|
+
popover: 9e3,
|
|
10
|
+
/** Modal/dialog at depth 0; nested modals add `modalStep` per level. */
|
|
11
|
+
modal: 1e4,
|
|
12
|
+
/** Per-level step between stacked modals. */
|
|
13
|
+
modalStep: 10,
|
|
14
|
+
/** How far above the top modal floating content sits (below the next nest). */
|
|
15
|
+
popoverOverModalOffset: 5,
|
|
16
|
+
/** Toasts / notifications — always the topmost layer. */
|
|
17
|
+
toast: 1e5
|
|
18
|
+
};
|
|
19
|
+
exports.zIndex = zIndex;
|
|
20
|
+
//# sourceMappingURL=zIndex.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zIndex.cjs","sources":["../../src/overlay/zIndex.ts"],"sourcesContent":["/**\n * Centralized z-index scale for clicky-ui overlays — the single source of truth\n * so stacking conflicts (e.g. a dropdown opened inside a modal rendering behind\n * it) cannot recur.\n *\n * These are applied as inline `style={{ zIndex }}`, never as arbitrary `z-[N]`\n * Tailwind classes: a consumer's Tailwind build only generates the arbitrary\n * classes it can scan from clicky-ui's published dist, so a freshly chosen\n * `z-[N]` may silently emit no rule. Inline styles are immune to that.\n *\n * Floating, transient content (dropdown / combobox / menu / tooltip / popover)\n * must clear whatever modal is open, so its z-index is computed relative to the\n * active modal via `useFloatingZIndex` (see ./modalStack) rather than taken from\n * a constant here.\n *\n * Layers NOT listed here are intentionally local: they live inside their own\n * stacking context and never participate in the global ordering —\n * `Timeline` (`z-[1]`), `RangeSlider` thumbs (20/30), and `Modal`'s\n * `ConfirmClosePrompt` (`z-10`, relative to the modal panel).\n */\nexport const zIndex = {\n /** In-flow raised affordances (sticky bits). */\n raised: 30,\n /** Persistent chat launcher. */\n chatFab: 60,\n /** Floating chat windows; `ChatWindowManager` increments from this base. */\n chatWindow: 61,\n /** Floating transient content when NO modal is open (above app chrome). */\n popover: 9000,\n /** Modal/dialog at depth 0; nested modals add `modalStep` per level. */\n modal: 10000,\n /** Per-level step between stacked modals. */\n modalStep: 10,\n /** How far above the top modal floating content sits (below the next nest). */\n popoverOverModalOffset: 5,\n /** Toasts / notifications — always the topmost layer. */\n toast: 100000,\n} as const;\n"],"names":[],"mappings":";;AAoBO,MAAM,SAAS;AAAA;AAAA,EAIpB,SAAS;AAAA;AAAA,EAET,YAAY;AAAA;AAAA,EAEZ,SAAS;AAAA;AAAA,EAET,OAAO;AAAA;AAAA,EAEP,WAAW;AAAA;AAAA,EAEX,wBAAwB;AAAA;AAAA,EAExB,OAAO;AACT;;"}
|