@inshorts/glint-ui 0.6.0 → 0.6.2

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.
Files changed (37) hide show
  1. package/dist/autocomplete.cjs.map +1 -1
  2. package/dist/autocomplete.js +2 -2
  3. package/dist/{chunk-PVK4WQJZ.js → chunk-5MEOZHJA.js} +2 -2
  4. package/dist/{chunk-43ZUFY63.js → chunk-A6GRLKOO.js} +3 -3
  5. package/dist/chunk-A6GRLKOO.js.map +1 -0
  6. package/dist/{chunk-YSU4VWRD.js → chunk-DS3QYIKL.js} +2 -2
  7. package/dist/{chunk-YSU4VWRD.js.map → chunk-DS3QYIKL.js.map} +1 -1
  8. package/dist/{chunk-WVNRFO5F.js → chunk-ISPICB4W.js} +2 -2
  9. package/dist/{chunk-ASU6CUTZ.js → chunk-MLRSOTFI.js} +5 -9
  10. package/dist/chunk-MLRSOTFI.js.map +1 -0
  11. package/dist/{chunk-6URKHMCF.js → chunk-OUYT6SPU.js} +2 -2
  12. package/dist/confirm-dialog.cjs +1 -1
  13. package/dist/confirm-dialog.cjs.map +1 -1
  14. package/dist/confirm-dialog.js +2 -2
  15. package/dist/index.cjs +6 -10
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +6 -6
  18. package/dist/modal.cjs +1 -1
  19. package/dist/modal.cjs.map +1 -1
  20. package/dist/modal.js +1 -1
  21. package/dist/segmented-control.cjs +4 -8
  22. package/dist/segmented-control.cjs.map +1 -1
  23. package/dist/segmented-control.d.cts +5 -7
  24. package/dist/segmented-control.d.ts +5 -7
  25. package/dist/segmented-control.js +1 -1
  26. package/dist/select.cjs.map +1 -1
  27. package/dist/select.js +3 -3
  28. package/dist/side-drawer.cjs +1 -1
  29. package/dist/side-drawer.cjs.map +1 -1
  30. package/dist/side-drawer.js +2 -2
  31. package/package.json +9 -8
  32. package/LICENSE +0 -13
  33. package/dist/chunk-43ZUFY63.js.map +0 -1
  34. package/dist/chunk-ASU6CUTZ.js.map +0 -1
  35. /package/dist/{chunk-PVK4WQJZ.js.map → chunk-5MEOZHJA.js.map} +0 -0
  36. /package/dist/{chunk-WVNRFO5F.js.map → chunk-ISPICB4W.js.map} +0 -0
  37. /package/dist/{chunk-6URKHMCF.js.map → chunk-OUYT6SPU.js.map} +0 -0
package/dist/index.js CHANGED
@@ -37,16 +37,16 @@ import {
37
37
  } from "./chunk-MUPW2TFJ.js";
38
38
  import {
39
39
  SegmentedControl
40
- } from "./chunk-ASU6CUTZ.js";
40
+ } from "./chunk-MLRSOTFI.js";
41
41
  import {
42
42
  Select
43
- } from "./chunk-6URKHMCF.js";
43
+ } from "./chunk-OUYT6SPU.js";
44
44
  import {
45
45
  Separator
46
46
  } from "./chunk-T4WCCHJQ.js";
47
47
  import {
48
48
  SideDrawer
49
- } from "./chunk-43ZUFY63.js";
49
+ } from "./chunk-A6GRLKOO.js";
50
50
  import {
51
51
  SpreadsheetGrid
52
52
  } from "./chunk-MQT5RAQV.js";
@@ -74,7 +74,7 @@ import {
74
74
  } from "./chunk-EWSBFU54.js";
75
75
  import {
76
76
  ConfirmDialog
77
- } from "./chunk-WVNRFO5F.js";
77
+ } from "./chunk-ISPICB4W.js";
78
78
  import {
79
79
  CopyButton
80
80
  } from "./chunk-PPYR3O3L.js";
@@ -103,11 +103,11 @@ import {
103
103
  } from "./chunk-ZP252OJZ.js";
104
104
  import {
105
105
  Autocomplete
106
- } from "./chunk-PVK4WQJZ.js";
106
+ } from "./chunk-5MEOZHJA.js";
107
107
  import {
108
108
  Modal,
109
109
  useHasMounted
110
- } from "./chunk-YSU4VWRD.js";
110
+ } from "./chunk-DS3QYIKL.js";
111
111
  import {
112
112
  useDialogLayer
113
113
  } from "./chunk-HBBOR43A.js";
package/dist/modal.cjs CHANGED
@@ -178,7 +178,7 @@ function Modal({
178
178
  type: "button",
179
179
  "aria-label": closeLabel,
180
180
  tabIndex: -1,
181
- className: "fixed inset-0 bg-slate-950/50",
181
+ className: "fixed inset-0 bg-overlay/50",
182
182
  onClick: () => {
183
183
  if (dismissible) onClose();
184
184
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/modal.tsx","../src/dialogLayer.ts","../src/utils.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { cn } from \"./utils\";\n\nconst emptySubscribe = () => () => {};\n\n/**\n * Returns false during SSR and the first client render, then true once mounted.\n * `useSyncExternalStore` is the React-recommended way to read client-only state\n * without a setState-in-effect (which triggers cascading renders).\n */\nexport function useHasMounted(): boolean {\n return React.useSyncExternalStore(\n emptySubscribe,\n () => true,\n () => false,\n );\n}\n\ninterface ModalProps {\n isOpen: boolean;\n /** id of the element labelling the dialog; wired to aria-labelledby. */\n labelledBy: string;\n /**\n * Optional id(s) of elements describing the dialog (APG alertdialog body).\n * Space-separated when multiple.\n */\n describedBy?: string;\n /** Accessible label for the backdrop dismiss button. */\n closeLabel: string;\n onClose: () => void;\n children: React.ReactNode;\n /**\n * When false, Escape and backdrop clicks do not close the modal (e.g. while a\n * save is in flight). Defaults to true.\n */\n dismissible?: boolean;\n /** Use \"alertdialog\" for destructive confirmations. Defaults to \"dialog\". */\n role?: \"dialog\" | \"alertdialog\";\n /** Tailwind max-width class for the panel. Defaults to \"max-w-md\". */\n panelClassName?: string;\n}\n\n/**\n * Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible\n * overlay with a centered panel. Callers provide the panel body (heading, form,\n * footer). Consolidates the boilerplate previously copied across every dialog.\n */\nexport function Modal({\n isOpen,\n labelledBy,\n describedBy,\n closeLabel,\n onClose,\n children,\n dismissible = true,\n role = \"dialog\",\n panelClassName,\n}: ModalProps) {\n const mounted = useHasMounted();\n const panelRef = React.useRef<HTMLDivElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: dismissible ? onClose : undefined,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n /*\n `overflow-y-auto` on the overlay, `my-auto` on the panel — not\n `items-center`. A flex item centred with `align-items` overflows equally\n in both directions once it is taller than its container, and the half\n above the top edge cannot be scrolled to: with the page behind it locked,\n an over-tall dialog was simply unreachable at both ends. Auto margins\n centre a short panel the same way and collapse to zero on a tall one,\n which scrolls.\n */\n <div className=\"fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4\">\n {/* `fixed`, not `absolute`: inside a scrolling overlay an absolute\n backdrop scrolls away with the content and leaves the rest of a tall\n dialog sitting on an undimmed page. */}\n <button\n type=\"button\"\n aria-label={closeLabel}\n tabIndex={-1}\n className=\"fixed inset-0 bg-slate-950/50\"\n onClick={() => {\n if (dismissible) onClose();\n }}\n />\n <div\n ref={panelRef}\n role={role}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none\",\n panelClassName ?? \"max-w-md p-6\",\n )}\n >\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n","\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * The keyboard and focus behaviour every dialog needs, in one place.\n *\n * `Modal` and `SideDrawer` each had a copy of the scroll lock and an Escape\n * handler, and neither did anything about focus: opening a dialog left focus\n * on the button behind it, Tab walked the page underneath — through the\n * full-screen backdrop button, among other things — and closing left focus\n * wherever it had wandered. That is the whole keyboard story for thirty\n * dialogs, so it is fixed once here rather than thirty times.\n */\n\n/**\n * Open dialogs, innermost last.\n *\n * Nesting is real here — \"Create audience group\" opens on top of \"Send\n * notification\" — and without an ordering both would answer the same Escape\n * and fight over the same Tab. Only the top layer acts.\n */\nconst layers: symbol[] = [];\n\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(\",\");\n\n/**\n * Deliberately not `offsetParent` or `getClientRects`: the first is null for\n * anything `position: fixed`, which a dialog panel usually is, and the second\n * is empty for *everything* under a test renderer that does no layout — which\n * would collapse the trap to \"no focusable elements\" in every test.\n */\nfunction isVisible(element: HTMLElement): boolean {\n if (element.closest(\"[hidden]\")) return false;\n if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n return typeof element.checkVisibility === \"function\"\n ? element.checkVisibility()\n : true;\n}\n\nfunction focusableWithin(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n isVisible,\n );\n}\n\nexport function useDialogLayer({\n active,\n containerRef,\n onEscape,\n}: {\n active: boolean;\n /** The panel. Focus is moved into it and kept inside it. */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Called on Escape, only while this is the top layer. Omit to ignore it. */\n onEscape?: () => void;\n}): void {\n // Read through a ref so a caller passing a fresh closure every render does\n // not tear the listener down and rebuild it on every keystroke.\n const escapeRef = React.useRef(onEscape);\n React.useEffect(() => {\n escapeRef.current = onEscape;\n });\n\n React.useEffect(() => {\n if (!active) return;\n\n const layer = Symbol(\"dialog-layer\");\n layers.push(layer);\n const isTopLayer = () => layers[layers.length - 1] === layer;\n\n const previouslyFocused =\n document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null;\n\n const container = containerRef.current;\n // Something inside may have claimed focus already — an `autoFocus` input —\n // and that choice is better than any this could make.\n if (container && !container.contains(document.activeElement)) {\n const [first] = focusableWithin(container);\n // The panel itself, not its first control: the first control is a Delete\n // button often enough that focusing it would put a destructive action\n // one Enter away from an accidental keypress.\n (container.hasAttribute(\"tabindex\") ? container : first)?.focus();\n }\n\n function handleKeyDown(event: KeyboardEvent) {\n if (!isTopLayer()) return;\n\n if (event.key === \"Escape\") {\n escapeRef.current?.();\n return;\n }\n\n if (event.key !== \"Tab\") return;\n const panel = containerRef.current;\n if (!panel) return;\n\n const items = focusableWithin(panel);\n if (items.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n\n const first = items[0]!;\n const last = items[items.length - 1]!;\n const active = document.activeElement;\n const outside = !panel.contains(active);\n\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault();\n first.focus();\n }\n }\n\n document.addEventListener(\"keydown\", handleKeyDown);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.body.style.overflow = previousOverflow;\n const index = layers.indexOf(layer);\n if (index !== -1) layers.splice(index, 1);\n // Back where it came from, so closing a dialog opened by keyboard does\n // not drop the caret at the top of the document.\n if (previouslyFocused?.isConnected) previouslyFocused.focus();\n };\n }, [active, containerRef]);\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":["React","active"]}
1
+ {"version":3,"sources":["../src/modal.tsx","../src/dialogLayer.ts","../src/utils.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { cn } from \"./utils\";\n\nconst emptySubscribe = () => () => {};\n\n/**\n * Returns false during SSR and the first client render, then true once mounted.\n * `useSyncExternalStore` is the React-recommended way to read client-only state\n * without a setState-in-effect (which triggers cascading renders).\n */\nexport function useHasMounted(): boolean {\n return React.useSyncExternalStore(\n emptySubscribe,\n () => true,\n () => false,\n );\n}\n\ninterface ModalProps {\n isOpen: boolean;\n /** id of the element labelling the dialog; wired to aria-labelledby. */\n labelledBy: string;\n /**\n * Optional id(s) of elements describing the dialog (APG alertdialog body).\n * Space-separated when multiple.\n */\n describedBy?: string;\n /** Accessible label for the backdrop dismiss button. */\n closeLabel: string;\n onClose: () => void;\n children: React.ReactNode;\n /**\n * When false, Escape and backdrop clicks do not close the modal (e.g. while a\n * save is in flight). Defaults to true.\n */\n dismissible?: boolean;\n /** Use \"alertdialog\" for destructive confirmations. Defaults to \"dialog\". */\n role?: \"dialog\" | \"alertdialog\";\n /** Tailwind max-width class for the panel. Defaults to \"max-w-md\". */\n panelClassName?: string;\n}\n\n/**\n * Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible\n * overlay with a centered panel. Callers provide the panel body (heading, form,\n * footer). Consolidates the boilerplate previously copied across every dialog.\n */\nexport function Modal({\n isOpen,\n labelledBy,\n describedBy,\n closeLabel,\n onClose,\n children,\n dismissible = true,\n role = \"dialog\",\n panelClassName,\n}: ModalProps) {\n const mounted = useHasMounted();\n const panelRef = React.useRef<HTMLDivElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: dismissible ? onClose : undefined,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n /*\n `overflow-y-auto` on the overlay, `my-auto` on the panel — not\n `items-center`. A flex item centred with `align-items` overflows equally\n in both directions once it is taller than its container, and the half\n above the top edge cannot be scrolled to: with the page behind it locked,\n an over-tall dialog was simply unreachable at both ends. Auto margins\n centre a short panel the same way and collapse to zero on a tall one,\n which scrolls.\n */\n <div className=\"fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4\">\n {/* `fixed`, not `absolute`: inside a scrolling overlay an absolute\n backdrop scrolls away with the content and leaves the rest of a tall\n dialog sitting on an undimmed page. */}\n <button\n type=\"button\"\n aria-label={closeLabel}\n tabIndex={-1}\n className=\"fixed inset-0 bg-overlay/50\"\n onClick={() => {\n if (dismissible) onClose();\n }}\n />\n <div\n ref={panelRef}\n role={role}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none\",\n panelClassName ?? \"max-w-md p-6\",\n )}\n >\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n","\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * The keyboard and focus behaviour every dialog needs, in one place.\n *\n * `Modal` and `SideDrawer` each had a copy of the scroll lock and an Escape\n * handler, and neither did anything about focus: opening a dialog left focus\n * on the button behind it, Tab walked the page underneath — through the\n * full-screen backdrop button, among other things — and closing left focus\n * wherever it had wandered. That is the whole keyboard story for thirty\n * dialogs, so it is fixed once here rather than thirty times.\n */\n\n/**\n * Open dialogs, innermost last.\n *\n * Nesting is real here — \"Create audience group\" opens on top of \"Send\n * notification\" — and without an ordering both would answer the same Escape\n * and fight over the same Tab. Only the top layer acts.\n */\nconst layers: symbol[] = [];\n\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(\",\");\n\n/**\n * Deliberately not `offsetParent` or `getClientRects`: the first is null for\n * anything `position: fixed`, which a dialog panel usually is, and the second\n * is empty for *everything* under a test renderer that does no layout — which\n * would collapse the trap to \"no focusable elements\" in every test.\n */\nfunction isVisible(element: HTMLElement): boolean {\n if (element.closest(\"[hidden]\")) return false;\n if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n return typeof element.checkVisibility === \"function\"\n ? element.checkVisibility()\n : true;\n}\n\nfunction focusableWithin(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n isVisible,\n );\n}\n\nexport function useDialogLayer({\n active,\n containerRef,\n onEscape,\n}: {\n active: boolean;\n /** The panel. Focus is moved into it and kept inside it. */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Called on Escape, only while this is the top layer. Omit to ignore it. */\n onEscape?: () => void;\n}): void {\n // Read through a ref so a caller passing a fresh closure every render does\n // not tear the listener down and rebuild it on every keystroke.\n const escapeRef = React.useRef(onEscape);\n React.useEffect(() => {\n escapeRef.current = onEscape;\n });\n\n React.useEffect(() => {\n if (!active) return;\n\n const layer = Symbol(\"dialog-layer\");\n layers.push(layer);\n const isTopLayer = () => layers[layers.length - 1] === layer;\n\n const previouslyFocused =\n document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null;\n\n const container = containerRef.current;\n // Something inside may have claimed focus already — an `autoFocus` input —\n // and that choice is better than any this could make.\n if (container && !container.contains(document.activeElement)) {\n const [first] = focusableWithin(container);\n // The panel itself, not its first control: the first control is a Delete\n // button often enough that focusing it would put a destructive action\n // one Enter away from an accidental keypress.\n (container.hasAttribute(\"tabindex\") ? container : first)?.focus();\n }\n\n function handleKeyDown(event: KeyboardEvent) {\n if (!isTopLayer()) return;\n\n if (event.key === \"Escape\") {\n escapeRef.current?.();\n return;\n }\n\n if (event.key !== \"Tab\") return;\n const panel = containerRef.current;\n if (!panel) return;\n\n const items = focusableWithin(panel);\n if (items.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n\n const first = items[0]!;\n const last = items[items.length - 1]!;\n const active = document.activeElement;\n const outside = !panel.contains(active);\n\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault();\n first.focus();\n }\n }\n\n document.addEventListener(\"keydown\", handleKeyDown);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.body.style.overflow = previousOverflow;\n const index = layers.indexOf(layer);\n if (index !== -1) layers.splice(index, 1);\n // Back where it came from, so closing a dialog opened by keyboard does\n // not drop the caret at the top of the document.\n if (previouslyFocused?.isConnected) previouslyFocused.focus();\n };\n }, [active, containerRef]);\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":["React","active"]}
package/dist/modal.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  Modal,
4
4
  useHasMounted
5
- } from "./chunk-YSU4VWRD.js";
5
+ } from "./chunk-DS3QYIKL.js";
6
6
  import "./chunk-HBBOR43A.js";
7
7
  import "./chunk-HCA5ODCD.js";
8
8
  export {
@@ -46,15 +46,11 @@ function cn(...inputs) {
46
46
 
47
47
  // src/segmented-control.tsx
48
48
  var import_jsx_runtime = require("react/jsx-runtime");
49
+ var TONE_ON = "bg-primary text-primary-foreground";
50
+ var TONE_OFF = "text-muted-foreground hover:bg-muted";
49
51
  var TONES = {
50
- emphasis: {
51
- on: "bg-slate-950 text-white dark:bg-white dark:text-slate-950",
52
- off: "text-muted-foreground hover:bg-muted"
53
- },
54
- primary: {
55
- on: "bg-primary text-primary-foreground",
56
- off: "text-muted-foreground hover:bg-muted"
57
- }
52
+ emphasis: { on: TONE_ON, off: TONE_OFF },
53
+ primary: { on: TONE_ON, off: TONE_OFF }
58
54
  };
59
55
  var TRACKS = {
60
56
  contained: "border-border inline-flex rounded-md border p-0.5",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/segmented-control.tsx","../src/utils.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"./utils\";\n\n/**\n * The one-of-N picker seven panels were each drawing by hand.\n *\n * Not `Tabs`, which is a bottom-bordered strip sitting above the content it\n * switches. This is the inline version — a bordered track with the selected\n * option filled in — used for a filter or a setting that sits among other\n * controls rather than heading a panel.\n *\n * **Roles are the reason to use this.** The seven copies had three different\n * ideas about what they were: one said `tablist`/`tab`, one said\n * `group`/`aria-pressed`, and the other five said nothing at all — so on five\n * of seven a screen reader heard a row of unlabelled buttons with no\n * indication which one was active. None of the three was right. `tablist`\n * without a `tabpanel` is a promise the markup does not keep, and\n * `aria-pressed` describes independent toggles, not a choice of one. Picking a\n * value out of a fixed set is a radio group, so that is what this is, with the\n * roving tabindex and arrow keys that come with the role.\n *\n * `tone` keeps both active colours the panels had drifted into: `emphasis` is\n * the near-black/near-white fill five of them used, `primary` the violet\n * accent the other two did. They are genuinely different colours rather than\n * one spelled two ways, so collapsing them is a visual change to two panels\n * and belongs in its own reviewable diff — the same call `DataTable` makes\n * about its two header variants.\n */\nexport interface SegmentedOption<Value extends string> {\n value: Value;\n label: React.ReactNode;\n disabled?: boolean;\n /**\n * Hover text. Worth setting on a disabled option — \"Everyone\" going grey\n * says nothing about which permission would bring it back.\n */\n title?: string;\n}\n\nexport interface SegmentedControlProps<Value extends string> {\n options: ReadonlyArray<SegmentedOption<Value>>;\n value: Value;\n onChange: (value: Value) => void;\n /**\n * Required: the group never has a visible label — the options name the\n * choice between them, which tells you nothing about what is being chosen.\n */\n ariaLabel: string;\n /**\n * `contained` — one bordered track around the options (five panels).\n * `loose` — free-standing pills with a gap between them (two panels).\n */\n variant?: \"contained\" | \"loose\";\n /** See the note above: two real colours, not one spelled twice. */\n tone?: \"emphasis\" | \"primary\";\n /** Options share the width equally, for a control that fills its column. */\n fill?: boolean;\n className?: string;\n}\n\nconst TONES = {\n emphasis: {\n on: \"bg-slate-950 text-white dark:bg-white dark:text-slate-950\",\n off: \"text-muted-foreground hover:bg-muted\",\n },\n primary: {\n on: \"bg-primary text-primary-foreground\",\n off: \"text-muted-foreground hover:bg-muted\",\n },\n} as const;\n\nconst TRACKS = {\n contained: \"border-border inline-flex rounded-md border p-0.5\",\n loose: \"flex flex-wrap gap-2\",\n} as const;\n\nexport function SegmentedControl<Value extends string>({\n options,\n value,\n onChange,\n ariaLabel,\n variant = \"contained\",\n tone = \"emphasis\",\n fill = false,\n className,\n}: SegmentedControlProps<Value>) {\n const refs = React.useRef(new Map<Value, HTMLButtonElement | null>());\n\n function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n const step =\n event.key === \"ArrowRight\" || event.key === \"ArrowDown\"\n ? 1\n : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\"\n ? -1\n : 0;\n if (step === 0) return;\n\n const selectable = options.filter((option) => !option.disabled);\n if (selectable.length < 2) return;\n\n const current = selectable.findIndex((option) => option.value === value);\n // Wraps, which is what a radio group is expected to do.\n const next =\n selectable[(current + step + selectable.length) % selectable.length];\n\n event.preventDefault();\n onChange(next.value);\n // The newly checked option has to take focus, or the next arrow press is\n // still measured from the old one.\n refs.current.get(next.value)?.focus();\n }\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n onKeyDown={onKeyDown}\n className={cn(TRACKS[variant], className)}\n >\n {options.map((option) => {\n const checked = option.value === value;\n return (\n <button\n key={option.value}\n ref={(node) => {\n refs.current.set(option.value, node);\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={checked}\n disabled={option.disabled}\n title={option.title}\n // Only the checked option is reachable by Tab; arrows move within\n // the group. Without this a keyboard user pages through every\n // option to get past the control.\n tabIndex={checked ? 0 : -1}\n onClick={() => onChange(option.value)}\n className={cn(\n \"cursor-pointer rounded px-3 py-1 text-sm font-medium transition\",\n \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n fill && \"flex-1\",\n checked ? TONES[tone].on : TONES[tone].off,\n )}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,YAAuB;;;ACFvB,kBAAsC;AACtC,4BAAwB;AAEjB,SAAS,MAAM,QAAsB;AAC1C,aAAO,mCAAQ,kBAAK,MAAM,CAAC;AAC7B;;;ADwHU;AA9DV,IAAM,QAAQ;AAAA,EACZ,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,KAAK;AAAA,EACP;AAAA,EACA,SAAS;AAAA,IACP,IAAI;AAAA,IACJ,KAAK;AAAA,EACP;AACF;AAEA,IAAM,SAAS;AAAA,EACb,WAAW;AAAA,EACX,OAAO;AACT;AAEO,SAAS,iBAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AACF,GAAiC;AAC/B,QAAM,OAAa,aAAO,oBAAI,IAAqC,CAAC;AAEpE,WAAS,UAAU,OAA4C;AAC7D,UAAM,OACJ,MAAM,QAAQ,gBAAgB,MAAM,QAAQ,cACxC,IACA,MAAM,QAAQ,eAAe,MAAM,QAAQ,YACzC,KACA;AACR,QAAI,SAAS,EAAG;AAEhB,UAAM,aAAa,QAAQ,OAAO,CAAC,WAAW,CAAC,OAAO,QAAQ;AAC9D,QAAI,WAAW,SAAS,EAAG;AAE3B,UAAM,UAAU,WAAW,UAAU,CAAC,WAAW,OAAO,UAAU,KAAK;AAEvE,UAAM,OACJ,YAAY,UAAU,OAAO,WAAW,UAAU,WAAW,MAAM;AAErE,UAAM,eAAe;AACrB,aAAS,KAAK,KAAK;AAGnB,SAAK,QAAQ,IAAI,KAAK,KAAK,GAAG,MAAM;AAAA,EACtC;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA,WAAW,GAAG,OAAO,OAAO,GAAG,SAAS;AAAA,MAEvC,kBAAQ,IAAI,CAAC,WAAW;AACvB,cAAM,UAAU,OAAO,UAAU;AACjC,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,KAAK,CAAC,SAAS;AACb,mBAAK,QAAQ,IAAI,OAAO,OAAO,IAAI;AAAA,YACrC;AAAA,YACA,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,UAAU,OAAO;AAAA,YACjB,OAAO,OAAO;AAAA,YAId,UAAU,UAAU,IAAI;AAAA,YACxB,SAAS,MAAM,SAAS,OAAO,KAAK;AAAA,YACpC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,QAAQ;AAAA,cACR,UAAU,MAAM,IAAI,EAAE,KAAK,MAAM,IAAI,EAAE;AAAA,YACzC;AAAA,YAEC,iBAAO;AAAA;AAAA,UAtBH,OAAO;AAAA,QAuBd;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/segmented-control.tsx","../src/utils.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"./utils\";\n\n/**\n * The one-of-N picker seven panels were each drawing by hand.\n *\n * Not `Tabs`, which is a bottom-bordered strip sitting above the content it\n * switches. This is the inline version — a bordered track with the selected\n * option filled in — used for a filter or a setting that sits among other\n * controls rather than heading a panel.\n *\n * **Roles are the reason to use this.** The seven copies had three different\n * ideas about what they were: one said `tablist`/`tab`, one said\n * `group`/`aria-pressed`, and the other five said nothing at all — so on five\n * of seven a screen reader heard a row of unlabelled buttons with no\n * indication which one was active. None of the three was right. `tablist`\n * without a `tabpanel` is a promise the markup does not keep, and\n * `aria-pressed` describes independent toggles, not a choice of one. Picking a\n * value out of a fixed set is a radio group, so that is what this is, with the\n * roving tabindex and arrow keys that come with the role.\n *\n * `tone` is kept for call-site compatibility. Both `emphasis` and `primary`\n * resolve to `bg-primary text-primary-foreground` — neutral brand primary is\n * already the ink fill five panels used, and a raw `slate-950` utility would\n * escape brand recoloring. Prefer `primary` in new code.\n */\nexport interface SegmentedOption<Value extends string> {\n value: Value;\n label: React.ReactNode;\n disabled?: boolean;\n /**\n * Hover text. Worth setting on a disabled option — \"Everyone\" going grey\n * says nothing about which permission would bring it back.\n */\n title?: string;\n}\n\nexport interface SegmentedControlProps<Value extends string> {\n options: ReadonlyArray<SegmentedOption<Value>>;\n value: Value;\n onChange: (value: Value) => void;\n /**\n * Required: the group never has a visible label — the options name the\n * choice between them, which tells you nothing about what is being chosen.\n */\n ariaLabel: string;\n /**\n * `contained` — one bordered track around the options (five panels).\n * `loose` — free-standing pills with a gap between them (two panels).\n */\n variant?: \"contained\" | \"loose\";\n /** Prefer `primary`. `emphasis` is an alias kept for existing call sites. */\n tone?: \"emphasis\" | \"primary\";\n /** Options share the width equally, for a control that fills its column. */\n fill?: boolean;\n className?: string;\n}\n\nconst TONE_ON = \"bg-primary text-primary-foreground\";\nconst TONE_OFF = \"text-muted-foreground hover:bg-muted\";\n\nconst TONES = {\n emphasis: { on: TONE_ON, off: TONE_OFF },\n primary: { on: TONE_ON, off: TONE_OFF },\n} as const;\n\nconst TRACKS = {\n contained: \"border-border inline-flex rounded-md border p-0.5\",\n loose: \"flex flex-wrap gap-2\",\n} as const;\n\nexport function SegmentedControl<Value extends string>({\n options,\n value,\n onChange,\n ariaLabel,\n variant = \"contained\",\n tone = \"emphasis\",\n fill = false,\n className,\n}: SegmentedControlProps<Value>) {\n const refs = React.useRef(new Map<Value, HTMLButtonElement | null>());\n\n function onKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {\n const step =\n event.key === \"ArrowRight\" || event.key === \"ArrowDown\"\n ? 1\n : event.key === \"ArrowLeft\" || event.key === \"ArrowUp\"\n ? -1\n : 0;\n if (step === 0) return;\n\n const selectable = options.filter((option) => !option.disabled);\n if (selectable.length < 2) return;\n\n const current = selectable.findIndex((option) => option.value === value);\n // Wraps, which is what a radio group is expected to do.\n const next =\n selectable[(current + step + selectable.length) % selectable.length];\n\n event.preventDefault();\n onChange(next.value);\n // The newly checked option has to take focus, or the next arrow press is\n // still measured from the old one.\n refs.current.get(next.value)?.focus();\n }\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel}\n onKeyDown={onKeyDown}\n className={cn(TRACKS[variant], className)}\n >\n {options.map((option) => {\n const checked = option.value === value;\n return (\n <button\n key={option.value}\n ref={(node) => {\n refs.current.set(option.value, node);\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={checked}\n disabled={option.disabled}\n title={option.title}\n // Only the checked option is reachable by Tab; arrows move within\n // the group. Without this a keyboard user pages through every\n // option to get past the control.\n tabIndex={checked ? 0 : -1}\n onClick={() => onChange(option.value)}\n className={cn(\n \"cursor-pointer rounded px-3 py-1 text-sm font-medium transition\",\n \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n fill && \"flex-1\",\n checked ? TONES[tone].on : TONES[tone].off,\n )}\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,YAAuB;;;ACFvB,kBAAsC;AACtC,4BAAwB;AAEjB,SAAS,MAAM,QAAsB;AAC1C,aAAO,mCAAQ,kBAAK,MAAM,CAAC;AAC7B;;;ADmHU;AA3DV,IAAM,UAAU;AAChB,IAAM,WAAW;AAEjB,IAAM,QAAQ;AAAA,EACZ,UAAU,EAAE,IAAI,SAAS,KAAK,SAAS;AAAA,EACvC,SAAS,EAAE,IAAI,SAAS,KAAK,SAAS;AACxC;AAEA,IAAM,SAAS;AAAA,EACb,WAAW;AAAA,EACX,OAAO;AACT;AAEO,SAAS,iBAAuC;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AACF,GAAiC;AAC/B,QAAM,OAAa,aAAO,oBAAI,IAAqC,CAAC;AAEpE,WAAS,UAAU,OAA4C;AAC7D,UAAM,OACJ,MAAM,QAAQ,gBAAgB,MAAM,QAAQ,cACxC,IACA,MAAM,QAAQ,eAAe,MAAM,QAAQ,YACzC,KACA;AACR,QAAI,SAAS,EAAG;AAEhB,UAAM,aAAa,QAAQ,OAAO,CAAC,WAAW,CAAC,OAAO,QAAQ;AAC9D,QAAI,WAAW,SAAS,EAAG;AAE3B,UAAM,UAAU,WAAW,UAAU,CAAC,WAAW,OAAO,UAAU,KAAK;AAEvE,UAAM,OACJ,YAAY,UAAU,OAAO,WAAW,UAAU,WAAW,MAAM;AAErE,UAAM,eAAe;AACrB,aAAS,KAAK,KAAK;AAGnB,SAAK,QAAQ,IAAI,KAAK,KAAK,GAAG,MAAM;AAAA,EACtC;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA,WAAW,GAAG,OAAO,OAAO,GAAG,SAAS;AAAA,MAEvC,kBAAQ,IAAI,CAAC,WAAW;AACvB,cAAM,UAAU,OAAO,UAAU;AACjC,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,KAAK,CAAC,SAAS;AACb,mBAAK,QAAQ,IAAI,OAAO,OAAO,IAAI;AAAA,YACrC;AAAA,YACA,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd,UAAU,OAAO;AAAA,YACjB,OAAO,OAAO;AAAA,YAId,UAAU,UAAU,IAAI;AAAA,YACxB,SAAS,MAAM,SAAS,OAAO,KAAK;AAAA,YACpC,WAAW;AAAA,cACT;AAAA,cACA;AAAA,cACA;AAAA,cACA,QAAQ;AAAA,cACR,UAAU,MAAM,IAAI,EAAE,KAAK,MAAM,IAAI,EAAE;AAAA,YACzC;AAAA,YAEC,iBAAO;AAAA;AAAA,UAtBH,OAAO;AAAA,QAuBd;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -18,12 +18,10 @@ import * as React from 'react';
18
18
  * value out of a fixed set is a radio group, so that is what this is, with the
19
19
  * roving tabindex and arrow keys that come with the role.
20
20
  *
21
- * `tone` keeps both active colours the panels had drifted into: `emphasis` is
22
- * the near-black/near-white fill five of them used, `primary` the violet
23
- * accent the other two did. They are genuinely different colours rather than
24
- * one spelled two ways, so collapsing them is a visual change to two panels
25
- * and belongs in its own reviewable diff — the same call `DataTable` makes
26
- * about its two header variants.
21
+ * `tone` is kept for call-site compatibility. Both `emphasis` and `primary`
22
+ * resolve to `bg-primary text-primary-foreground` — neutral brand primary is
23
+ * already the ink fill five panels used, and a raw `slate-950` utility would
24
+ * escape brand recoloring. Prefer `primary` in new code.
27
25
  */
28
26
  interface SegmentedOption<Value extends string> {
29
27
  value: Value;
@@ -49,7 +47,7 @@ interface SegmentedControlProps<Value extends string> {
49
47
  * `loose` — free-standing pills with a gap between them (two panels).
50
48
  */
51
49
  variant?: "contained" | "loose";
52
- /** See the note above: two real colours, not one spelled twice. */
50
+ /** Prefer `primary`. `emphasis` is an alias kept for existing call sites. */
53
51
  tone?: "emphasis" | "primary";
54
52
  /** Options share the width equally, for a control that fills its column. */
55
53
  fill?: boolean;
@@ -18,12 +18,10 @@ import * as React from 'react';
18
18
  * value out of a fixed set is a radio group, so that is what this is, with the
19
19
  * roving tabindex and arrow keys that come with the role.
20
20
  *
21
- * `tone` keeps both active colours the panels had drifted into: `emphasis` is
22
- * the near-black/near-white fill five of them used, `primary` the violet
23
- * accent the other two did. They are genuinely different colours rather than
24
- * one spelled two ways, so collapsing them is a visual change to two panels
25
- * and belongs in its own reviewable diff — the same call `DataTable` makes
26
- * about its two header variants.
21
+ * `tone` is kept for call-site compatibility. Both `emphasis` and `primary`
22
+ * resolve to `bg-primary text-primary-foreground` — neutral brand primary is
23
+ * already the ink fill five panels used, and a raw `slate-950` utility would
24
+ * escape brand recoloring. Prefer `primary` in new code.
27
25
  */
28
26
  interface SegmentedOption<Value extends string> {
29
27
  value: Value;
@@ -49,7 +47,7 @@ interface SegmentedControlProps<Value extends string> {
49
47
  * `loose` — free-standing pills with a gap between them (two panels).
50
48
  */
51
49
  variant?: "contained" | "loose";
52
- /** See the note above: two real colours, not one spelled twice. */
50
+ /** Prefer `primary`. `emphasis` is an alias kept for existing call sites. */
53
51
  tone?: "emphasis" | "primary";
54
52
  /** Options share the width equally, for a control that fills its column. */
55
53
  fill?: boolean;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  SegmentedControl
4
- } from "./chunk-ASU6CUTZ.js";
4
+ } from "./chunk-MLRSOTFI.js";
5
5
  import "./chunk-HCA5ODCD.js";
6
6
  export {
7
7
  SegmentedControl
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/select.tsx","../src/autocomplete.tsx","../src/modal.tsx","../src/utils.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nimport { Autocomplete, type AutocompleteOption } from \"./autocomplete\";\n\ntype SelectChange = {\n target: { value: string };\n currentTarget: { value: string };\n};\n\nexport type SelectProps = {\n id?: string;\n className?: string;\n disabled?: boolean;\n /** Accessible name when the control is not wrapped in a label. */\n \"aria-label\"?: string;\n /** `compact` fits a spreadsheet cell. Typing still filters the list. */\n size?: \"default\" | \"compact\";\n value?: string | number | readonly string[];\n placeholder?: string;\n children?: React.ReactNode;\n onChange?: (event: SelectChange) => void;\n};\n\nfunction optionText(node: React.ReactNode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n if (Array.isArray(node)) return node.map(optionText).join(\"\");\n if (React.isValidElement<{ children?: React.ReactNode }>(node)) {\n return optionText(node.props.children);\n }\n return \"\";\n}\n\nfunction optionsFromChildren(children: React.ReactNode): AutocompleteOption[] {\n const options: AutocompleteOption[] = [];\n React.Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return;\n if (child.type === React.Fragment) {\n options.push(\n ...optionsFromChildren(\n (child.props as { children?: React.ReactNode }).children,\n ),\n );\n return;\n }\n const props = child.props as {\n value?: string | number;\n disabled?: boolean;\n children?: React.ReactNode;\n };\n const value = props.value == null ? \"\" : String(props.value);\n options.push({\n value,\n label: optionText(props.children).trim() || value,\n disabled: Boolean(props.disabled),\n });\n });\n return options;\n}\n\n/**\n * The dropdown. It is the searchable autocomplete, not a native `<select>`,\n * so a long list can be typed into and an empty list still opens.\n */\nexport function Select({\n id,\n className,\n disabled,\n value,\n placeholder,\n children,\n onChange,\n size = \"default\",\n \"aria-label\": ariaLabel,\n}: SelectProps) {\n const options = optionsFromChildren(children);\n const current = Array.isArray(value)\n ? String(value[0] ?? \"\")\n : value == null\n ? \"\"\n : String(value);\n const blank = options.find((option) => option.value === \"\");\n\n return (\n <Autocomplete\n id={id}\n className={className}\n disabled={Boolean(disabled)}\n ariaLabel={ariaLabel}\n size={size}\n options={options}\n value={current}\n clearable={false}\n placeholder={placeholder ?? blank?.label ?? \"Search…\"}\n onChange={(next) =>\n onChange?.({\n target: { value: next },\n currentTarget: { value: next },\n })\n }\n />\n );\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Check, ChevronDown, Search, X } from \"lucide-react\";\n\nimport { useHasMounted } from \"./modal\";\nimport { cn } from \"./utils\";\n\n/** Room the list wants below the control before it gives up and flips. */\nconst PREFERRED_LIST_HEIGHT = 224;\n/** Below this there is no point opening downward at all. */\nconst MIN_LIST_HEIGHT = 120;\nconst VIEWPORT_MARGIN = 8;\nconst ANCHOR_GAP = 4;\n\ntype DropdownPosition = {\n left: number;\n width: number;\n maxHeight: number;\n /** One of the two is set: `top` opens downward, `bottom` opens upward. */\n top?: number;\n bottom?: number;\n};\n\nexport type AutocompleteOption = {\n value: string;\n label: string;\n disabled?: boolean;\n};\n\ntype BaseAutocompleteProps = {\n options: AutocompleteOption[];\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n emptyMessage?: string;\n id?: string;\n /**\n * Accessible name for the combobox.\n *\n * `Field` renders its label as a `<span>`, so without this the control is\n * announced as an unnamed combobox and cannot be queried by name — the\n * filter toolbars work around it by counting comboboxes on the page.\n */\n ariaLabel?: string;\n /**\n * `compact` is the spreadsheet cell: no search icon, no min height, and a\n * list wide enough to read a name. The closed control still shows the\n * chosen label, and typing filters the list.\n */\n size?: \"default\" | \"compact\";\n /**\n * Type a value and press Enter (or paste a list) to add it, like antd\n * `Select mode=\"tags\"`. With `multiple` a pasted list is split into several\n * values; on a single-value control it accepts one typed value the options\n * do not offer — an id for a record the list has not loaded, say.\n */\n creatable?: boolean;\n /**\n * Shows the clear button once a value is chosen. Off for dropdowns that\n * always keep one of their options, where clearing would blank a required\n * field.\n */\n clearable?: boolean;\n};\n\ntype SingleAutocompleteProps = BaseAutocompleteProps & {\n multiple?: false;\n value: string;\n onChange: (value: string) => void;\n};\n\ntype MultipleAutocompleteProps = BaseAutocompleteProps & {\n multiple: true;\n value: string[];\n onChange: (value: string[]) => void;\n};\n\nexport type AutocompleteProps =\n SingleAutocompleteProps | MultipleAutocompleteProps;\n\nfunction normalizeQuery(query: string) {\n return query.trim().toLowerCase();\n}\n\nfunction filterOptions(options: AutocompleteOption[], query: string) {\n const normalized = normalizeQuery(query);\n if (!normalized) return options;\n\n return options.filter(\n (option) =>\n option.label.toLowerCase().includes(normalized) ||\n option.value.toLowerCase().includes(normalized),\n );\n}\n\nfunction getOptionLabel(options: AutocompleteOption[], value: string) {\n return options.find((option) => option.value === value)?.label ?? value;\n}\n\nfunction parseTagTokens(raw: string): string[] {\n const seen = new Set<string>();\n const tokens: string[] = [];\n for (const part of raw.split(/[\\s,]+/)) {\n const token = part.trim();\n if (!token || seen.has(token)) continue;\n seen.add(token);\n tokens.push(token);\n }\n return tokens;\n}\n\nexport function Autocomplete(props: AutocompleteProps) {\n const {\n options,\n placeholder = \"Search…\",\n disabled = false,\n className,\n emptyMessage = \"No results found\",\n id,\n ariaLabel,\n size = \"default\",\n creatable = false,\n clearable = true,\n } = props;\n\n const multiple = props.multiple ?? false;\n const singleValue = multiple\n ? undefined\n : (props as SingleAutocompleteProps).value;\n const multipleValue = multiple\n ? (props as MultipleAutocompleteProps).value\n : undefined;\n\n // Memoized so dependent callbacks below keep stable identities across renders.\n const selectedValues = React.useMemo<string[]>(() => {\n if (multiple) return multipleValue ?? [];\n return singleValue ? [singleValue] : [];\n }, [multiple, multipleValue, singleValue]);\n\n const onSingleChange = !multiple\n ? (props as SingleAutocompleteProps).onChange\n : undefined;\n const onMultipleChange = multiple\n ? (props as MultipleAutocompleteProps).onChange\n : undefined;\n\n const containerRef = React.useRef<HTMLDivElement>(null);\n const dropdownRef = React.useRef<HTMLDivElement>(null);\n const inputRef = React.useRef<HTMLInputElement>(null);\n const listRef = React.useRef<HTMLUListElement>(null);\n const ignoreBlurRef = React.useRef(false);\n\n const mounted = useHasMounted();\n const [open, setOpen] = React.useState(false);\n const [position, setPosition] = React.useState<DropdownPosition | null>(null);\n const [query, setQuery] = React.useState(\"\");\n const [highlightedIndex, setHighlightedIndex] = React.useState(0);\n\n const filteredOptions = React.useMemo(\n () => filterOptions(options, query),\n [options, query],\n );\n\n // Clamp during render instead of via a setState-in-effect: if the option list\n // shrinks below the stored index, fall back to the last valid row.\n const safeHighlightedIndex =\n highlightedIndex >= filteredOptions.length\n ? Math.max(filteredOptions.length - 1, 0)\n : highlightedIndex;\n\n const close = React.useCallback(() => {\n setOpen(false);\n setQuery(\"\");\n setHighlightedIndex(0);\n }, []);\n\n const openDropdown = React.useCallback(() => {\n if (disabled) return;\n setOpen(true);\n setHighlightedIndex(0);\n }, [disabled]);\n\n const commitSingleValue = React.useCallback(\n (value: string) => {\n if (!multiple) {\n onSingleChange?.(value);\n close();\n }\n },\n [close, multiple, onSingleChange],\n );\n\n const toggleMultipleValue = React.useCallback(\n (value: string) => {\n if (!multiple || !onMultipleChange) return;\n\n onMultipleChange(\n selectedValues.includes(value)\n ? selectedValues.filter((item) => item !== value)\n : [...selectedValues, value],\n );\n setQuery(\"\");\n setHighlightedIndex(0);\n },\n [multiple, onMultipleChange, selectedValues],\n );\n\n const commitCustomValues = React.useCallback(\n (raw: string) => {\n if (!creatable) return;\n\n if (!multiple) {\n const value = raw.trim();\n if (!value) return;\n commitSingleValue(value);\n return;\n }\n\n if (!onMultipleChange) return;\n const tokens = parseTagTokens(raw);\n if (!tokens.length) return;\n const next = [...selectedValues];\n for (const token of tokens) {\n if (!next.includes(token)) next.push(token);\n }\n onMultipleChange(next);\n setQuery(\"\");\n setHighlightedIndex(0);\n },\n [commitSingleValue, creatable, multiple, onMultipleChange, selectedValues],\n );\n\n const removeValue = React.useCallback(\n (value: string) => {\n if (!multiple || !onMultipleChange) return;\n onMultipleChange(selectedValues.filter((item) => item !== value));\n window.requestAnimationFrame(() => inputRef.current?.focus());\n },\n [multiple, onMultipleChange, selectedValues],\n );\n\n const selectOption = React.useCallback(\n (option: AutocompleteOption) => {\n if (option.disabled) return;\n\n ignoreBlurRef.current = true;\n window.setTimeout(() => {\n ignoreBlurRef.current = false;\n }, 0);\n\n if (multiple) {\n toggleMultipleValue(option.value);\n window.requestAnimationFrame(() => inputRef.current?.focus());\n return;\n }\n\n commitSingleValue(option.value);\n },\n [commitSingleValue, multiple, toggleMultipleValue],\n );\n\n const selectHighlighted = React.useCallback(() => {\n const option = filteredOptions[safeHighlightedIndex];\n if (!option) return;\n selectOption(option);\n }, [filteredOptions, safeHighlightedIndex, selectOption]);\n\n React.useEffect(() => {\n if (!open) return;\n\n function handlePointerDown(event: PointerEvent) {\n const target = event.target as Node;\n // The list is portalled to the body, so containment has to be checked\n // against both halves or picking an option would close the control\n // before the choice registered.\n if (\n containerRef.current?.contains(target) ||\n dropdownRef.current?.contains(target)\n ) {\n return;\n }\n close();\n }\n\n document.addEventListener(\"pointerdown\", handlePointerDown);\n return () => document.removeEventListener(\"pointerdown\", handlePointerDown);\n }, [close, open]);\n\n /*\n The list is measured against the viewport and rendered in a portal.\n\n Anchored inside the control it was clipped by whatever `overflow` ancestor\n happened to be above it — inside a dialog body that is the scroll region,\n so the last field on a form showed one row of a six-hundred-row list. It\n opens downward when there is room, upward when there is not, and shrinks to\n what is left when neither side can take the whole list.\n */\n React.useLayoutEffect(() => {\n // Not cleared on close: the render is gated on `open`, and the layout\n // effect recomputes before the next paint, so a stale value is never seen.\n if (!open) return;\n\n const update = () => {\n const anchor = containerRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n const below =\n window.innerHeight - rect.bottom - ANCHOR_GAP - VIEWPORT_MARGIN;\n const above = rect.top - ANCHOR_GAP - VIEWPORT_MARGIN;\n const flip =\n below < Math.min(PREFERRED_LIST_HEIGHT, MIN_LIST_HEIGHT) &&\n above > below;\n const room = flip ? above : below;\n const width = Math.max(rect.width, size === \"compact\" ? 220 : 0);\n const left = Math.max(\n VIEWPORT_MARGIN,\n Math.min(rect.left, window.innerWidth - width - VIEWPORT_MARGIN),\n );\n setPosition({\n left,\n width,\n maxHeight: Math.max(\n MIN_LIST_HEIGHT,\n Math.min(PREFERRED_LIST_HEIGHT, room),\n ),\n ...(flip\n ? { bottom: window.innerHeight - rect.top + ANCHOR_GAP }\n : { top: rect.bottom + ANCHOR_GAP }),\n });\n };\n\n update();\n // Capture, so a scroll in any ancestor — the dialog body included — moves\n // the list with its control instead of leaving it behind.\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [open, size]);\n\n React.useEffect(() => {\n if (!open || !listRef.current) return;\n\n const highlighted = listRef.current.children[safeHighlightedIndex] as\n HTMLElement | undefined;\n\n highlighted?.scrollIntoView({ block: \"nearest\" });\n }, [safeHighlightedIndex, open]);\n\n function handleInputKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if (disabled) return;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n if (!open) {\n openDropdown();\n return;\n }\n setHighlightedIndex(\n Math.min(safeHighlightedIndex + 1, filteredOptions.length - 1),\n );\n break;\n case \"ArrowUp\":\n event.preventDefault();\n if (!open) {\n openDropdown();\n return;\n }\n setHighlightedIndex(Math.max(safeHighlightedIndex - 1, 0));\n break;\n case \"Enter\":\n event.preventDefault();\n if (creatable && query.trim()) {\n // A single-value control filters its list as you type, so a match\n // is what Enter means there; only an unmatched query is taken as a\n // new value. Tags have no such list to defer to.\n if (multiple || filteredOptions.length === 0) {\n commitCustomValues(query);\n return;\n }\n }\n if (!open) {\n openDropdown();\n return;\n }\n selectHighlighted();\n break;\n case \",\":\n if (creatable && multiple && query.trim()) {\n event.preventDefault();\n commitCustomValues(query);\n }\n break;\n case \"Escape\":\n event.preventDefault();\n close();\n break;\n case \"Backspace\":\n if (multiple && query.length === 0 && selectedValues.length > 0) {\n event.preventDefault();\n removeValue(selectedValues[selectedValues.length - 1]);\n }\n break;\n case \"Tab\":\n close();\n break;\n default:\n break;\n }\n }\n\n const displayValue =\n !multiple && !open && !query && selectedValues[0]\n ? getOptionLabel(options, selectedValues[0])\n : query;\n\n const inputPlaceholder =\n multiple && selectedValues.length > 0 ? \"\" : placeholder;\n\n const createLabel = query.trim();\n const canCreate =\n creatable && Boolean(createLabel) && !selectedValues.includes(createLabel);\n\n const listboxId = id ? `${id}-listbox` : undefined;\n\n return (\n <div\n ref={containerRef}\n className={cn(\n \"relative\",\n size === \"compact\" ? \"w-auto min-w-24\" : \"w-full\",\n className,\n )}\n >\n <div\n className={cn(\n \"border-border bg-background flex w-full cursor-pointer items-center rounded-md border transition-colors\",\n \"focus-within:border-ring focus-within:ring-ring/30 focus-within:ring-2\",\n size === \"compact\"\n ? \"min-h-0 gap-0.5 border-transparent bg-transparent px-1 py-0.5 text-xs shadow-none\"\n : \"min-h-9 gap-1.5 px-2 py-1.5 text-sm shadow-sm\",\n disabled && \"cursor-not-allowed opacity-50\",\n open && \"border-ring ring-ring/30 ring-2\",\n )}\n onPointerDown={(event) => {\n if (disabled) return;\n if ((event.target as HTMLElement).closest(\"[data-option]\")) return;\n if ((event.target as HTMLElement).closest(\"[data-chip-remove]\"))\n return;\n\n if (!(event.target as HTMLElement).closest(\"input\")) {\n event.preventDefault();\n inputRef.current?.focus();\n }\n\n openDropdown();\n }}\n >\n {size === \"compact\" ? null : (\n <Search className=\"text-muted-foreground size-4 shrink-0\" />\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1\">\n {multiple &&\n selectedValues.map((value) => (\n <span\n key={value}\n className=\"bg-primary/10 text-primary inline-flex max-w-full items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium\"\n >\n <span className=\"truncate\">\n {getOptionLabel(options, value)}\n </span>\n <button\n type=\"button\"\n data-chip-remove\n className=\"text-primary/70 hover:bg-primary/15 hover:text-primary rounded-sm transition\"\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n removeValue(value);\n }}\n aria-label={`Remove ${getOptionLabel(options, value)}`}\n >\n <X className=\"size-3\" />\n </button>\n </span>\n ))}\n\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-label={ariaLabel}\n aria-expanded={open}\n aria-controls={listboxId}\n aria-autocomplete=\"list\"\n disabled={disabled}\n value={displayValue}\n placeholder={inputPlaceholder}\n className={cn(\n \"placeholder:text-muted-foreground flex-1 bg-transparent outline-none\",\n size === \"compact\" ? \"min-w-0\" : \"min-w-[6rem]\",\n )}\n onFocus={openDropdown}\n onBlur={() => {\n if (ignoreBlurRef.current) return;\n close();\n }}\n onChange={(event) => {\n setQuery(event.target.value);\n setHighlightedIndex(0);\n if (!open) setOpen(true);\n }}\n onKeyDown={handleInputKeyDown}\n onPaste={(event) => {\n if (!creatable || !multiple) return;\n const text = event.clipboardData.getData(\"text\");\n if (!/[\\s,]/.test(text)) return;\n event.preventDefault();\n commitCustomValues(text);\n }}\n />\n </div>\n\n {clearable && !multiple && selectedValues[0] && !disabled && (\n <button\n type=\"button\"\n className=\"text-muted-foreground hover:bg-muted hover:text-foreground rounded-sm p-0.5 transition\"\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n onSingleChange?.(\"\");\n setQuery(\"\");\n inputRef.current?.focus();\n }}\n aria-label=\"Clear selection\"\n >\n <X className=\"size-3.5\" />\n </button>\n )}\n\n <ChevronDown\n className={cn(\n \"text-muted-foreground shrink-0 transition-transform\",\n size === \"compact\" ? \"size-3\" : \"size-4\",\n open && \"rotate-180\",\n )}\n />\n </div>\n\n {open && mounted && position\n ? createPortal(\n <div\n ref={dropdownRef}\n data-autocomplete-dropdown\n style={{\n position: \"fixed\",\n left: position.left,\n width: position.width,\n top: position.top,\n bottom: position.bottom,\n // Above the modal shell (z-100) it is usually opened inside.\n zIndex: 200,\n }}\n className=\"border-border bg-background overflow-hidden rounded-md border shadow-lg\"\n >\n <ul\n ref={listRef}\n id={listboxId}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n className=\"overflow-y-auto p-1\"\n style={{ maxHeight: position.maxHeight }}\n >\n {filteredOptions.length === 0 && !canCreate ? (\n <li className=\"text-muted-foreground px-3 py-6 text-center text-sm\">\n {emptyMessage}\n </li>\n ) : (\n <>\n {filteredOptions.map((option, index) => {\n const isSelected = selectedValues.includes(option.value);\n const isHighlighted = index === safeHighlightedIndex;\n\n return (\n <li\n key={option.value}\n data-option\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || undefined}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm transition-colors\",\n size === \"compact\"\n ? \"px-2 py-1 text-xs\"\n : \"px-2.5 py-2 text-sm\",\n isHighlighted && \"bg-muted\",\n option.disabled && \"cursor-not-allowed opacity-50\",\n )}\n onMouseEnter={() => setHighlightedIndex(index)}\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n selectOption(option);\n }}\n >\n {multiple ? (\n <span\n className={cn(\n \"border-border flex size-4 shrink-0 items-center justify-center rounded border\",\n isSelected &&\n \"border-primary bg-primary text-primary-foreground\",\n )}\n >\n {isSelected ? <Check className=\"size-3\" /> : null}\n </span>\n ) : (\n <Check\n className={cn(\n \"text-primary size-4 shrink-0\",\n isSelected ? \"opacity-100\" : \"opacity-0\",\n )}\n />\n )}\n <span className=\"truncate\">{option.label}</span>\n </li>\n );\n })}\n {canCreate ? (\n <li\n data-option\n role=\"option\"\n aria-selected={false}\n className=\"hover:bg-muted flex cursor-pointer items-center gap-2 rounded-sm px-2.5 py-2 text-sm transition-colors\"\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n ignoreBlurRef.current = true;\n window.setTimeout(() => {\n ignoreBlurRef.current = false;\n }, 0);\n commitCustomValues(query);\n window.requestAnimationFrame(() =>\n inputRef.current?.focus(),\n );\n }}\n >\n Add “{createLabel}”\n </li>\n ) : null}\n </>\n )}\n </ul>\n </div>,\n document.body,\n )\n : null}\n </div>\n );\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { cn } from \"./utils\";\n\nconst emptySubscribe = () => () => {};\n\n/**\n * Returns false during SSR and the first client render, then true once mounted.\n * `useSyncExternalStore` is the React-recommended way to read client-only state\n * without a setState-in-effect (which triggers cascading renders).\n */\nexport function useHasMounted(): boolean {\n return React.useSyncExternalStore(\n emptySubscribe,\n () => true,\n () => false,\n );\n}\n\ninterface ModalProps {\n isOpen: boolean;\n /** id of the element labelling the dialog; wired to aria-labelledby. */\n labelledBy: string;\n /**\n * Optional id(s) of elements describing the dialog (APG alertdialog body).\n * Space-separated when multiple.\n */\n describedBy?: string;\n /** Accessible label for the backdrop dismiss button. */\n closeLabel: string;\n onClose: () => void;\n children: React.ReactNode;\n /**\n * When false, Escape and backdrop clicks do not close the modal (e.g. while a\n * save is in flight). Defaults to true.\n */\n dismissible?: boolean;\n /** Use \"alertdialog\" for destructive confirmations. Defaults to \"dialog\". */\n role?: \"dialog\" | \"alertdialog\";\n /** Tailwind max-width class for the panel. Defaults to \"max-w-md\". */\n panelClassName?: string;\n}\n\n/**\n * Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible\n * overlay with a centered panel. Callers provide the panel body (heading, form,\n * footer). Consolidates the boilerplate previously copied across every dialog.\n */\nexport function Modal({\n isOpen,\n labelledBy,\n describedBy,\n closeLabel,\n onClose,\n children,\n dismissible = true,\n role = \"dialog\",\n panelClassName,\n}: ModalProps) {\n const mounted = useHasMounted();\n const panelRef = React.useRef<HTMLDivElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: dismissible ? onClose : undefined,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n /*\n `overflow-y-auto` on the overlay, `my-auto` on the panel — not\n `items-center`. A flex item centred with `align-items` overflows equally\n in both directions once it is taller than its container, and the half\n above the top edge cannot be scrolled to: with the page behind it locked,\n an over-tall dialog was simply unreachable at both ends. Auto margins\n centre a short panel the same way and collapse to zero on a tall one,\n which scrolls.\n */\n <div className=\"fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4\">\n {/* `fixed`, not `absolute`: inside a scrolling overlay an absolute\n backdrop scrolls away with the content and leaves the rest of a tall\n dialog sitting on an undimmed page. */}\n <button\n type=\"button\"\n aria-label={closeLabel}\n tabIndex={-1}\n className=\"fixed inset-0 bg-slate-950/50\"\n onClick={() => {\n if (dismissible) onClose();\n }}\n />\n <div\n ref={panelRef}\n role={role}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none\",\n panelClassName ?? \"max-w-md p-6\",\n )}\n >\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":["React","React","import_react_dom","import_jsx_runtime","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/select.tsx","../src/autocomplete.tsx","../src/modal.tsx","../src/utils.ts"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\nimport { Autocomplete, type AutocompleteOption } from \"./autocomplete\";\n\ntype SelectChange = {\n target: { value: string };\n currentTarget: { value: string };\n};\n\nexport type SelectProps = {\n id?: string;\n className?: string;\n disabled?: boolean;\n /** Accessible name when the control is not wrapped in a label. */\n \"aria-label\"?: string;\n /** `compact` fits a spreadsheet cell. Typing still filters the list. */\n size?: \"default\" | \"compact\";\n value?: string | number | readonly string[];\n placeholder?: string;\n children?: React.ReactNode;\n onChange?: (event: SelectChange) => void;\n};\n\nfunction optionText(node: React.ReactNode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n if (Array.isArray(node)) return node.map(optionText).join(\"\");\n if (React.isValidElement<{ children?: React.ReactNode }>(node)) {\n return optionText(node.props.children);\n }\n return \"\";\n}\n\nfunction optionsFromChildren(children: React.ReactNode): AutocompleteOption[] {\n const options: AutocompleteOption[] = [];\n React.Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return;\n if (child.type === React.Fragment) {\n options.push(\n ...optionsFromChildren(\n (child.props as { children?: React.ReactNode }).children,\n ),\n );\n return;\n }\n const props = child.props as {\n value?: string | number;\n disabled?: boolean;\n children?: React.ReactNode;\n };\n const value = props.value == null ? \"\" : String(props.value);\n options.push({\n value,\n label: optionText(props.children).trim() || value,\n disabled: Boolean(props.disabled),\n });\n });\n return options;\n}\n\n/**\n * The dropdown. It is the searchable autocomplete, not a native `<select>`,\n * so a long list can be typed into and an empty list still opens.\n */\nexport function Select({\n id,\n className,\n disabled,\n value,\n placeholder,\n children,\n onChange,\n size = \"default\",\n \"aria-label\": ariaLabel,\n}: SelectProps) {\n const options = optionsFromChildren(children);\n const current = Array.isArray(value)\n ? String(value[0] ?? \"\")\n : value == null\n ? \"\"\n : String(value);\n const blank = options.find((option) => option.value === \"\");\n\n return (\n <Autocomplete\n id={id}\n className={className}\n disabled={Boolean(disabled)}\n ariaLabel={ariaLabel}\n size={size}\n options={options}\n value={current}\n clearable={false}\n placeholder={placeholder ?? blank?.label ?? \"Search…\"}\n onChange={(next) =>\n onChange?.({\n target: { value: next },\n currentTarget: { value: next },\n })\n }\n />\n );\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Check, ChevronDown, Search, X } from \"lucide-react\";\n\nimport { useHasMounted } from \"./modal\";\nimport { cn } from \"./utils\";\n\n/** Room the list wants below the control before it gives up and flips. */\nconst PREFERRED_LIST_HEIGHT = 224;\n/** Below this there is no point opening downward at all. */\nconst MIN_LIST_HEIGHT = 120;\nconst VIEWPORT_MARGIN = 8;\nconst ANCHOR_GAP = 4;\n\ntype DropdownPosition = {\n left: number;\n width: number;\n maxHeight: number;\n /** One of the two is set: `top` opens downward, `bottom` opens upward. */\n top?: number;\n bottom?: number;\n};\n\nexport type AutocompleteOption = {\n value: string;\n label: string;\n disabled?: boolean;\n};\n\ntype BaseAutocompleteProps = {\n options: AutocompleteOption[];\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n emptyMessage?: string;\n id?: string;\n /**\n * Accessible name for the combobox.\n *\n * `Field` renders its label as a `<span>`, so without this the control is\n * announced as an unnamed combobox and cannot be queried by name — the\n * filter toolbars work around it by counting comboboxes on the page.\n */\n ariaLabel?: string;\n /**\n * `compact` is the spreadsheet cell: no search icon, no min height, and a\n * list wide enough to read a name. The closed control still shows the\n * chosen label, and typing filters the list.\n */\n size?: \"default\" | \"compact\";\n /**\n * Type a value and press Enter (or paste a list) to add it, like antd\n * `Select mode=\"tags\"`. With `multiple` a pasted list is split into several\n * values; on a single-value control it accepts one typed value the options\n * do not offer — an id for a record the list has not loaded, say.\n */\n creatable?: boolean;\n /**\n * Shows the clear button once a value is chosen. Off for dropdowns that\n * always keep one of their options, where clearing would blank a required\n * field.\n */\n clearable?: boolean;\n};\n\ntype SingleAutocompleteProps = BaseAutocompleteProps & {\n multiple?: false;\n value: string;\n onChange: (value: string) => void;\n};\n\ntype MultipleAutocompleteProps = BaseAutocompleteProps & {\n multiple: true;\n value: string[];\n onChange: (value: string[]) => void;\n};\n\nexport type AutocompleteProps =\n SingleAutocompleteProps | MultipleAutocompleteProps;\n\nfunction normalizeQuery(query: string) {\n return query.trim().toLowerCase();\n}\n\nfunction filterOptions(options: AutocompleteOption[], query: string) {\n const normalized = normalizeQuery(query);\n if (!normalized) return options;\n\n return options.filter(\n (option) =>\n option.label.toLowerCase().includes(normalized) ||\n option.value.toLowerCase().includes(normalized),\n );\n}\n\nfunction getOptionLabel(options: AutocompleteOption[], value: string) {\n return options.find((option) => option.value === value)?.label ?? value;\n}\n\nfunction parseTagTokens(raw: string): string[] {\n const seen = new Set<string>();\n const tokens: string[] = [];\n for (const part of raw.split(/[\\s,]+/)) {\n const token = part.trim();\n if (!token || seen.has(token)) continue;\n seen.add(token);\n tokens.push(token);\n }\n return tokens;\n}\n\nexport function Autocomplete(props: AutocompleteProps) {\n const {\n options,\n placeholder = \"Search…\",\n disabled = false,\n className,\n emptyMessage = \"No results found\",\n id,\n ariaLabel,\n size = \"default\",\n creatable = false,\n clearable = true,\n } = props;\n\n const multiple = props.multiple ?? false;\n const singleValue = multiple\n ? undefined\n : (props as SingleAutocompleteProps).value;\n const multipleValue = multiple\n ? (props as MultipleAutocompleteProps).value\n : undefined;\n\n // Memoized so dependent callbacks below keep stable identities across renders.\n const selectedValues = React.useMemo<string[]>(() => {\n if (multiple) return multipleValue ?? [];\n return singleValue ? [singleValue] : [];\n }, [multiple, multipleValue, singleValue]);\n\n const onSingleChange = !multiple\n ? (props as SingleAutocompleteProps).onChange\n : undefined;\n const onMultipleChange = multiple\n ? (props as MultipleAutocompleteProps).onChange\n : undefined;\n\n const containerRef = React.useRef<HTMLDivElement>(null);\n const dropdownRef = React.useRef<HTMLDivElement>(null);\n const inputRef = React.useRef<HTMLInputElement>(null);\n const listRef = React.useRef<HTMLUListElement>(null);\n const ignoreBlurRef = React.useRef(false);\n\n const mounted = useHasMounted();\n const [open, setOpen] = React.useState(false);\n const [position, setPosition] = React.useState<DropdownPosition | null>(null);\n const [query, setQuery] = React.useState(\"\");\n const [highlightedIndex, setHighlightedIndex] = React.useState(0);\n\n const filteredOptions = React.useMemo(\n () => filterOptions(options, query),\n [options, query],\n );\n\n // Clamp during render instead of via a setState-in-effect: if the option list\n // shrinks below the stored index, fall back to the last valid row.\n const safeHighlightedIndex =\n highlightedIndex >= filteredOptions.length\n ? Math.max(filteredOptions.length - 1, 0)\n : highlightedIndex;\n\n const close = React.useCallback(() => {\n setOpen(false);\n setQuery(\"\");\n setHighlightedIndex(0);\n }, []);\n\n const openDropdown = React.useCallback(() => {\n if (disabled) return;\n setOpen(true);\n setHighlightedIndex(0);\n }, [disabled]);\n\n const commitSingleValue = React.useCallback(\n (value: string) => {\n if (!multiple) {\n onSingleChange?.(value);\n close();\n }\n },\n [close, multiple, onSingleChange],\n );\n\n const toggleMultipleValue = React.useCallback(\n (value: string) => {\n if (!multiple || !onMultipleChange) return;\n\n onMultipleChange(\n selectedValues.includes(value)\n ? selectedValues.filter((item) => item !== value)\n : [...selectedValues, value],\n );\n setQuery(\"\");\n setHighlightedIndex(0);\n },\n [multiple, onMultipleChange, selectedValues],\n );\n\n const commitCustomValues = React.useCallback(\n (raw: string) => {\n if (!creatable) return;\n\n if (!multiple) {\n const value = raw.trim();\n if (!value) return;\n commitSingleValue(value);\n return;\n }\n\n if (!onMultipleChange) return;\n const tokens = parseTagTokens(raw);\n if (!tokens.length) return;\n const next = [...selectedValues];\n for (const token of tokens) {\n if (!next.includes(token)) next.push(token);\n }\n onMultipleChange(next);\n setQuery(\"\");\n setHighlightedIndex(0);\n },\n [commitSingleValue, creatable, multiple, onMultipleChange, selectedValues],\n );\n\n const removeValue = React.useCallback(\n (value: string) => {\n if (!multiple || !onMultipleChange) return;\n onMultipleChange(selectedValues.filter((item) => item !== value));\n window.requestAnimationFrame(() => inputRef.current?.focus());\n },\n [multiple, onMultipleChange, selectedValues],\n );\n\n const selectOption = React.useCallback(\n (option: AutocompleteOption) => {\n if (option.disabled) return;\n\n ignoreBlurRef.current = true;\n window.setTimeout(() => {\n ignoreBlurRef.current = false;\n }, 0);\n\n if (multiple) {\n toggleMultipleValue(option.value);\n window.requestAnimationFrame(() => inputRef.current?.focus());\n return;\n }\n\n commitSingleValue(option.value);\n },\n [commitSingleValue, multiple, toggleMultipleValue],\n );\n\n const selectHighlighted = React.useCallback(() => {\n const option = filteredOptions[safeHighlightedIndex];\n if (!option) return;\n selectOption(option);\n }, [filteredOptions, safeHighlightedIndex, selectOption]);\n\n React.useEffect(() => {\n if (!open) return;\n\n function handlePointerDown(event: PointerEvent) {\n const target = event.target as Node;\n // The list is portalled to the body, so containment has to be checked\n // against both halves or picking an option would close the control\n // before the choice registered.\n if (\n containerRef.current?.contains(target) ||\n dropdownRef.current?.contains(target)\n ) {\n return;\n }\n close();\n }\n\n document.addEventListener(\"pointerdown\", handlePointerDown);\n return () => document.removeEventListener(\"pointerdown\", handlePointerDown);\n }, [close, open]);\n\n /*\n The list is measured against the viewport and rendered in a portal.\n\n Anchored inside the control it was clipped by whatever `overflow` ancestor\n happened to be above it — inside a dialog body that is the scroll region,\n so the last field on a form showed one row of a six-hundred-row list. It\n opens downward when there is room, upward when there is not, and shrinks to\n what is left when neither side can take the whole list.\n */\n React.useLayoutEffect(() => {\n // Not cleared on close: the render is gated on `open`, and the layout\n // effect recomputes before the next paint, so a stale value is never seen.\n if (!open) return;\n\n const update = () => {\n const anchor = containerRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n const below =\n window.innerHeight - rect.bottom - ANCHOR_GAP - VIEWPORT_MARGIN;\n const above = rect.top - ANCHOR_GAP - VIEWPORT_MARGIN;\n const flip =\n below < Math.min(PREFERRED_LIST_HEIGHT, MIN_LIST_HEIGHT) &&\n above > below;\n const room = flip ? above : below;\n const width = Math.max(rect.width, size === \"compact\" ? 220 : 0);\n const left = Math.max(\n VIEWPORT_MARGIN,\n Math.min(rect.left, window.innerWidth - width - VIEWPORT_MARGIN),\n );\n setPosition({\n left,\n width,\n maxHeight: Math.max(\n MIN_LIST_HEIGHT,\n Math.min(PREFERRED_LIST_HEIGHT, room),\n ),\n ...(flip\n ? { bottom: window.innerHeight - rect.top + ANCHOR_GAP }\n : { top: rect.bottom + ANCHOR_GAP }),\n });\n };\n\n update();\n // Capture, so a scroll in any ancestor — the dialog body included — moves\n // the list with its control instead of leaving it behind.\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [open, size]);\n\n React.useEffect(() => {\n if (!open || !listRef.current) return;\n\n const highlighted = listRef.current.children[safeHighlightedIndex] as\n HTMLElement | undefined;\n\n highlighted?.scrollIntoView({ block: \"nearest\" });\n }, [safeHighlightedIndex, open]);\n\n function handleInputKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if (disabled) return;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n if (!open) {\n openDropdown();\n return;\n }\n setHighlightedIndex(\n Math.min(safeHighlightedIndex + 1, filteredOptions.length - 1),\n );\n break;\n case \"ArrowUp\":\n event.preventDefault();\n if (!open) {\n openDropdown();\n return;\n }\n setHighlightedIndex(Math.max(safeHighlightedIndex - 1, 0));\n break;\n case \"Enter\":\n event.preventDefault();\n if (creatable && query.trim()) {\n // A single-value control filters its list as you type, so a match\n // is what Enter means there; only an unmatched query is taken as a\n // new value. Tags have no such list to defer to.\n if (multiple || filteredOptions.length === 0) {\n commitCustomValues(query);\n return;\n }\n }\n if (!open) {\n openDropdown();\n return;\n }\n selectHighlighted();\n break;\n case \",\":\n if (creatable && multiple && query.trim()) {\n event.preventDefault();\n commitCustomValues(query);\n }\n break;\n case \"Escape\":\n event.preventDefault();\n close();\n break;\n case \"Backspace\":\n if (multiple && query.length === 0 && selectedValues.length > 0) {\n event.preventDefault();\n removeValue(selectedValues[selectedValues.length - 1]);\n }\n break;\n case \"Tab\":\n close();\n break;\n default:\n break;\n }\n }\n\n const displayValue =\n !multiple && !open && !query && selectedValues[0]\n ? getOptionLabel(options, selectedValues[0])\n : query;\n\n const inputPlaceholder =\n multiple && selectedValues.length > 0 ? \"\" : placeholder;\n\n const createLabel = query.trim();\n const canCreate =\n creatable && Boolean(createLabel) && !selectedValues.includes(createLabel);\n\n const listboxId = id ? `${id}-listbox` : undefined;\n\n return (\n <div\n ref={containerRef}\n className={cn(\n \"relative\",\n size === \"compact\" ? \"w-auto min-w-24\" : \"w-full\",\n className,\n )}\n >\n <div\n className={cn(\n \"border-border bg-background flex w-full cursor-pointer items-center rounded-md border transition-colors\",\n \"focus-within:border-ring focus-within:ring-ring/30 focus-within:ring-2\",\n size === \"compact\"\n ? \"min-h-0 gap-0.5 border-transparent bg-transparent px-1 py-0.5 text-xs shadow-none\"\n : \"min-h-9 gap-1.5 px-2 py-1.5 text-sm shadow-sm\",\n disabled && \"cursor-not-allowed opacity-50\",\n open && \"border-ring ring-ring/30 ring-2\",\n )}\n onPointerDown={(event) => {\n if (disabled) return;\n if ((event.target as HTMLElement).closest(\"[data-option]\")) return;\n if ((event.target as HTMLElement).closest(\"[data-chip-remove]\"))\n return;\n\n if (!(event.target as HTMLElement).closest(\"input\")) {\n event.preventDefault();\n inputRef.current?.focus();\n }\n\n openDropdown();\n }}\n >\n {size === \"compact\" ? null : (\n <Search className=\"text-muted-foreground size-4 shrink-0\" />\n )}\n\n <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-1\">\n {multiple &&\n selectedValues.map((value) => (\n <span\n key={value}\n className=\"bg-primary/10 text-primary inline-flex max-w-full items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium\"\n >\n <span className=\"truncate\">\n {getOptionLabel(options, value)}\n </span>\n <button\n type=\"button\"\n data-chip-remove\n className=\"text-primary/70 hover:bg-primary/15 hover:text-primary rounded-sm transition\"\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n removeValue(value);\n }}\n aria-label={`Remove ${getOptionLabel(options, value)}`}\n >\n <X className=\"size-3\" />\n </button>\n </span>\n ))}\n\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-label={ariaLabel}\n aria-expanded={open}\n aria-controls={listboxId}\n aria-autocomplete=\"list\"\n disabled={disabled}\n value={displayValue}\n placeholder={inputPlaceholder}\n className={cn(\n \"placeholder:text-muted-foreground flex-1 bg-transparent outline-none\",\n size === \"compact\" ? \"min-w-0\" : \"min-w-[6rem]\",\n )}\n onFocus={openDropdown}\n onBlur={() => {\n if (ignoreBlurRef.current) return;\n close();\n }}\n onChange={(event) => {\n setQuery(event.target.value);\n setHighlightedIndex(0);\n if (!open) setOpen(true);\n }}\n onKeyDown={handleInputKeyDown}\n onPaste={(event) => {\n if (!creatable || !multiple) return;\n const text = event.clipboardData.getData(\"text\");\n if (!/[\\s,]/.test(text)) return;\n event.preventDefault();\n commitCustomValues(text);\n }}\n />\n </div>\n\n {clearable && !multiple && selectedValues[0] && !disabled && (\n <button\n type=\"button\"\n className=\"text-muted-foreground hover:bg-muted hover:text-foreground rounded-sm p-0.5 transition\"\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n onSingleChange?.(\"\");\n setQuery(\"\");\n inputRef.current?.focus();\n }}\n aria-label=\"Clear selection\"\n >\n <X className=\"size-3.5\" />\n </button>\n )}\n\n <ChevronDown\n className={cn(\n \"text-muted-foreground shrink-0 transition-transform\",\n size === \"compact\" ? \"size-3\" : \"size-4\",\n open && \"rotate-180\",\n )}\n />\n </div>\n\n {open && mounted && position\n ? createPortal(\n <div\n ref={dropdownRef}\n data-autocomplete-dropdown\n style={{\n position: \"fixed\",\n left: position.left,\n width: position.width,\n top: position.top,\n bottom: position.bottom,\n // Above the modal shell (z-100) it is usually opened inside.\n zIndex: 200,\n }}\n className=\"border-border bg-background overflow-hidden rounded-md border shadow-lg\"\n >\n <ul\n ref={listRef}\n id={listboxId}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n className=\"overflow-y-auto p-1\"\n style={{ maxHeight: position.maxHeight }}\n >\n {filteredOptions.length === 0 && !canCreate ? (\n <li className=\"text-muted-foreground px-3 py-6 text-center text-sm\">\n {emptyMessage}\n </li>\n ) : (\n <>\n {filteredOptions.map((option, index) => {\n const isSelected = selectedValues.includes(option.value);\n const isHighlighted = index === safeHighlightedIndex;\n\n return (\n <li\n key={option.value}\n data-option\n role=\"option\"\n aria-selected={isSelected}\n aria-disabled={option.disabled || undefined}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm transition-colors\",\n size === \"compact\"\n ? \"px-2 py-1 text-xs\"\n : \"px-2.5 py-2 text-sm\",\n isHighlighted && \"bg-muted\",\n option.disabled && \"cursor-not-allowed opacity-50\",\n )}\n onMouseEnter={() => setHighlightedIndex(index)}\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n selectOption(option);\n }}\n >\n {multiple ? (\n <span\n className={cn(\n \"border-border flex size-4 shrink-0 items-center justify-center rounded border\",\n isSelected &&\n \"border-primary bg-primary text-primary-foreground\",\n )}\n >\n {isSelected ? <Check className=\"size-3\" /> : null}\n </span>\n ) : (\n <Check\n className={cn(\n \"text-primary size-4 shrink-0\",\n isSelected ? \"opacity-100\" : \"opacity-0\",\n )}\n />\n )}\n <span className=\"truncate\">{option.label}</span>\n </li>\n );\n })}\n {canCreate ? (\n <li\n data-option\n role=\"option\"\n aria-selected={false}\n className=\"hover:bg-muted flex cursor-pointer items-center gap-2 rounded-sm px-2.5 py-2 text-sm transition-colors\"\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n ignoreBlurRef.current = true;\n window.setTimeout(() => {\n ignoreBlurRef.current = false;\n }, 0);\n commitCustomValues(query);\n window.requestAnimationFrame(() =>\n inputRef.current?.focus(),\n );\n }}\n >\n Add “{createLabel}”\n </li>\n ) : null}\n </>\n )}\n </ul>\n </div>,\n document.body,\n )\n : null}\n </div>\n );\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { cn } from \"./utils\";\n\nconst emptySubscribe = () => () => {};\n\n/**\n * Returns false during SSR and the first client render, then true once mounted.\n * `useSyncExternalStore` is the React-recommended way to read client-only state\n * without a setState-in-effect (which triggers cascading renders).\n */\nexport function useHasMounted(): boolean {\n return React.useSyncExternalStore(\n emptySubscribe,\n () => true,\n () => false,\n );\n}\n\ninterface ModalProps {\n isOpen: boolean;\n /** id of the element labelling the dialog; wired to aria-labelledby. */\n labelledBy: string;\n /**\n * Optional id(s) of elements describing the dialog (APG alertdialog body).\n * Space-separated when multiple.\n */\n describedBy?: string;\n /** Accessible label for the backdrop dismiss button. */\n closeLabel: string;\n onClose: () => void;\n children: React.ReactNode;\n /**\n * When false, Escape and backdrop clicks do not close the modal (e.g. while a\n * save is in flight). Defaults to true.\n */\n dismissible?: boolean;\n /** Use \"alertdialog\" for destructive confirmations. Defaults to \"dialog\". */\n role?: \"dialog\" | \"alertdialog\";\n /** Tailwind max-width class for the panel. Defaults to \"max-w-md\". */\n panelClassName?: string;\n}\n\n/**\n * Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible\n * overlay with a centered panel. Callers provide the panel body (heading, form,\n * footer). Consolidates the boilerplate previously copied across every dialog.\n */\nexport function Modal({\n isOpen,\n labelledBy,\n describedBy,\n closeLabel,\n onClose,\n children,\n dismissible = true,\n role = \"dialog\",\n panelClassName,\n}: ModalProps) {\n const mounted = useHasMounted();\n const panelRef = React.useRef<HTMLDivElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: dismissible ? onClose : undefined,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n /*\n `overflow-y-auto` on the overlay, `my-auto` on the panel — not\n `items-center`. A flex item centred with `align-items` overflows equally\n in both directions once it is taller than its container, and the half\n above the top edge cannot be scrolled to: with the page behind it locked,\n an over-tall dialog was simply unreachable at both ends. Auto margins\n centre a short panel the same way and collapse to zero on a tall one,\n which scrolls.\n */\n <div className=\"fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4\">\n {/* `fixed`, not `absolute`: inside a scrolling overlay an absolute\n backdrop scrolls away with the content and leaves the rest of a tall\n dialog sitting on an undimmed page. */}\n <button\n type=\"button\"\n aria-label={closeLabel}\n tabIndex={-1}\n className=\"fixed inset-0 bg-overlay/50\"\n onClick={() => {\n if (dismissible) onClose();\n }}\n />\n <div\n ref={panelRef}\n role={role}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none\",\n panelClassName ?? \"max-w-md p-6\",\n )}\n >\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":["React","React","import_react_dom","import_jsx_runtime","import_jsx_runtime"]}
package/dist/select.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import {
3
3
  Select
4
- } from "./chunk-6URKHMCF.js";
5
- import "./chunk-PVK4WQJZ.js";
6
- import "./chunk-YSU4VWRD.js";
4
+ } from "./chunk-OUYT6SPU.js";
5
+ import "./chunk-5MEOZHJA.js";
6
+ import "./chunk-DS3QYIKL.js";
7
7
  import "./chunk-HBBOR43A.js";
8
8
  import "./chunk-HCA5ODCD.js";
9
9
  export {
@@ -277,7 +277,7 @@ function SideDrawer({
277
277
  {
278
278
  "aria-hidden": true,
279
279
  onClick: onClose,
280
- className: "fixed inset-0 z-[90] bg-slate-950/30"
280
+ className: "fixed inset-0 z-[90] bg-overlay/30"
281
281
  }
282
282
  ),
283
283
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/side-drawer.tsx","../src/button.tsx","../src/spinner.tsx","../src/utils.ts","../src/dialogLayer.ts","../src/modal.tsx"],"sourcesContent":["\"use client\";\n\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { Button } from \"./button\";\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { useHasMounted } from \"./modal\";\nimport { cn } from \"./utils\";\n\ninterface SideDrawerProps {\n isOpen: boolean;\n title: string;\n onClose: () => void;\n children: React.ReactNode;\n /** Tailwind width class for the panel. Defaults to a reading-width column. */\n panelClassName?: string;\n}\n\n/**\n * A panel that slides in from the right.\n *\n * For content that belongs beside the page rather than on top of it — a queue\n * you glance at and dismiss, not a decision you have to answer. Unlike\n * `Modal`, it does not centre or dim the whole page to a stop: the table\n * underneath stays readable, which is the point of looking at both.\n */\nexport function SideDrawer({\n isOpen,\n title,\n onClose,\n children,\n panelClassName,\n}: SideDrawerProps) {\n const mounted = useHasMounted();\n const headingId = React.useId();\n const panelRef = React.useRef<HTMLElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: onClose,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n <>\n <div\n aria-hidden\n onClick={onClose}\n className=\"fixed inset-0 z-[90] bg-slate-950/30\"\n />\n <aside\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={headingId}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background fixed inset-y-0 right-0 z-[95] flex flex-col border-l shadow-2xl outline-none\",\n panelClassName ?? \"w-full max-w-md\",\n )}\n >\n <div className=\"border-border flex items-center justify-between gap-3 border-b px-4 py-3\">\n <h2 id={headingId} className=\"truncate text-base font-semibold\">\n {title}\n </h2>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"Close panel\"\n onClick={onClose}\n >\n <X className=\"h-4 w-4\" aria-hidden />\n </Button>\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto p-4\">{children}</div>\n </aside>\n </>,\n document.body,\n );\n}\n","import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nimport { Spinner } from \"./spinner\";\nimport { cn } from \"./utils\";\n\nconst buttonVariants = cva(\n \"inline-flex h-9 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md px-3 text-sm font-medium transition duration-200 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n secondary: \"bg-muted text-foreground hover:bg-muted/80\",\n ghost: \"hover:bg-muted\",\n destructive:\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline: \"border border-border bg-background hover:bg-muted\",\n },\n size: {\n sm: \"h-8 px-2.5\",\n md: \"h-9 px-3\",\n lg: \"h-10 px-4\",\n icon: \"h-9 w-9 px-0\",\n // Square, matching `sm`'s height — for an icon-only control sitting in\n // a table row or beside `sm` buttons, where `icon` is a row taller.\n \"icon-sm\": \"h-8 w-8 px-0\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n },\n);\n\nexport interface ButtonProps\n extends\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n /**\n * Work is in flight: shows a spinner, disables the button, and sets\n * `aria-busy`.\n *\n * This exists because the panel had 272 disabled buttons and only 22 that\n * said anything about why — the rest went dead on click with no spinner and\n * no label change, which reads as a broken button rather than a busy one.\n * Making the feedback part of `disabled` rather than something each caller\n * remembers separately is the only way that ratio stays fixed.\n *\n * The label is kept rather than swapped. \"Saving…\" loses which button you\n * pressed, and the spinner already says it is working. An icon-only button\n * is the exception — there is no room beside the glyph, so the spinner takes\n * its place; the `aria-label` is what names the button either way.\n *\n * \"Icon-only\" means an icon `size`. A square button that grew a spinner\n * beside its glyph would not just look wrong, it would resize mid-request\n * and shove whatever sits next to it sideways.\n */\n loading?: boolean;\n}\n\n/** Sizes with no room for a spinner beside the glyph. */\nconst ICON_SIZES = new Set<string>([\"icon\", \"icon-sm\"]);\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant,\n size,\n asChild = false,\n loading,\n disabled,\n children,\n ...props\n },\n ref,\n ) => {\n const Comp = asChild ? Slot : \"button\";\n\n // `asChild` renders someone else's element, which may not take a spinner or\n // a disabled attribute — so the busy treatment is only applied to a real\n // button, and passing both is a mistake worth not papering over.\n if (asChild) {\n return (\n <Comp\n ref={ref}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n </Comp>\n );\n }\n\n return (\n <button\n ref={ref}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n >\n {loading ? <Spinner size=\"sm\" /> : null}\n {loading && size != null && ICON_SIZES.has(size) ? null : children}\n </button>\n );\n },\n);\n\nButton.displayName = \"Button\";\n","import * as React from \"react\";\nimport { Loader2 } from \"lucide-react\";\n\nimport { cn } from \"./utils\";\n\n/**\n * The one spinner.\n *\n * There were five spellings of `animate-spin` across twelve files — `h-4 w-4`,\n * `h-5 w-5`, `size-4`, bare, and a hand-drawn ring in `ProtectedRoute` — so the\n * same \"working\" signal came out a different size depending on which panel you\n * were on.\n *\n * It is `aria-hidden` on purpose. A spinner is decoration; what a screen reader\n * needs is the surrounding control's `aria-busy` or the panel's status text,\n * and announcing \"loading\" twice is worse than once. `Button` and `StatePanel`\n * both supply that.\n */\nconst SIZES = {\n sm: \"size-3.5\",\n md: \"size-4\",\n lg: \"size-5\",\n xl: \"size-10 border-2\",\n} as const;\n\nexport interface SpinnerProps extends React.SVGProps<SVGSVGElement> {\n size?: keyof typeof SIZES;\n}\n\nexport function Spinner({ size = \"md\", className, ...props }: SpinnerProps) {\n return (\n <Loader2\n aria-hidden\n className={cn(\"animate-spin\", SIZES[size], className)}\n {...props}\n />\n );\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * The keyboard and focus behaviour every dialog needs, in one place.\n *\n * `Modal` and `SideDrawer` each had a copy of the scroll lock and an Escape\n * handler, and neither did anything about focus: opening a dialog left focus\n * on the button behind it, Tab walked the page underneath — through the\n * full-screen backdrop button, among other things — and closing left focus\n * wherever it had wandered. That is the whole keyboard story for thirty\n * dialogs, so it is fixed once here rather than thirty times.\n */\n\n/**\n * Open dialogs, innermost last.\n *\n * Nesting is real here — \"Create audience group\" opens on top of \"Send\n * notification\" — and without an ordering both would answer the same Escape\n * and fight over the same Tab. Only the top layer acts.\n */\nconst layers: symbol[] = [];\n\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(\",\");\n\n/**\n * Deliberately not `offsetParent` or `getClientRects`: the first is null for\n * anything `position: fixed`, which a dialog panel usually is, and the second\n * is empty for *everything* under a test renderer that does no layout — which\n * would collapse the trap to \"no focusable elements\" in every test.\n */\nfunction isVisible(element: HTMLElement): boolean {\n if (element.closest(\"[hidden]\")) return false;\n if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n return typeof element.checkVisibility === \"function\"\n ? element.checkVisibility()\n : true;\n}\n\nfunction focusableWithin(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n isVisible,\n );\n}\n\nexport function useDialogLayer({\n active,\n containerRef,\n onEscape,\n}: {\n active: boolean;\n /** The panel. Focus is moved into it and kept inside it. */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Called on Escape, only while this is the top layer. Omit to ignore it. */\n onEscape?: () => void;\n}): void {\n // Read through a ref so a caller passing a fresh closure every render does\n // not tear the listener down and rebuild it on every keystroke.\n const escapeRef = React.useRef(onEscape);\n React.useEffect(() => {\n escapeRef.current = onEscape;\n });\n\n React.useEffect(() => {\n if (!active) return;\n\n const layer = Symbol(\"dialog-layer\");\n layers.push(layer);\n const isTopLayer = () => layers[layers.length - 1] === layer;\n\n const previouslyFocused =\n document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null;\n\n const container = containerRef.current;\n // Something inside may have claimed focus already — an `autoFocus` input —\n // and that choice is better than any this could make.\n if (container && !container.contains(document.activeElement)) {\n const [first] = focusableWithin(container);\n // The panel itself, not its first control: the first control is a Delete\n // button often enough that focusing it would put a destructive action\n // one Enter away from an accidental keypress.\n (container.hasAttribute(\"tabindex\") ? container : first)?.focus();\n }\n\n function handleKeyDown(event: KeyboardEvent) {\n if (!isTopLayer()) return;\n\n if (event.key === \"Escape\") {\n escapeRef.current?.();\n return;\n }\n\n if (event.key !== \"Tab\") return;\n const panel = containerRef.current;\n if (!panel) return;\n\n const items = focusableWithin(panel);\n if (items.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n\n const first = items[0]!;\n const last = items[items.length - 1]!;\n const active = document.activeElement;\n const outside = !panel.contains(active);\n\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault();\n first.focus();\n }\n }\n\n document.addEventListener(\"keydown\", handleKeyDown);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.body.style.overflow = previousOverflow;\n const index = layers.indexOf(layer);\n if (index !== -1) layers.splice(index, 1);\n // Back where it came from, so closing a dialog opened by keyboard does\n // not drop the caret at the top of the document.\n if (previouslyFocused?.isConnected) previouslyFocused.focus();\n };\n }, [active, containerRef]);\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { cn } from \"./utils\";\n\nconst emptySubscribe = () => () => {};\n\n/**\n * Returns false during SSR and the first client render, then true once mounted.\n * `useSyncExternalStore` is the React-recommended way to read client-only state\n * without a setState-in-effect (which triggers cascading renders).\n */\nexport function useHasMounted(): boolean {\n return React.useSyncExternalStore(\n emptySubscribe,\n () => true,\n () => false,\n );\n}\n\ninterface ModalProps {\n isOpen: boolean;\n /** id of the element labelling the dialog; wired to aria-labelledby. */\n labelledBy: string;\n /**\n * Optional id(s) of elements describing the dialog (APG alertdialog body).\n * Space-separated when multiple.\n */\n describedBy?: string;\n /** Accessible label for the backdrop dismiss button. */\n closeLabel: string;\n onClose: () => void;\n children: React.ReactNode;\n /**\n * When false, Escape and backdrop clicks do not close the modal (e.g. while a\n * save is in flight). Defaults to true.\n */\n dismissible?: boolean;\n /** Use \"alertdialog\" for destructive confirmations. Defaults to \"dialog\". */\n role?: \"dialog\" | \"alertdialog\";\n /** Tailwind max-width class for the panel. Defaults to \"max-w-md\". */\n panelClassName?: string;\n}\n\n/**\n * Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible\n * overlay with a centered panel. Callers provide the panel body (heading, form,\n * footer). Consolidates the boilerplate previously copied across every dialog.\n */\nexport function Modal({\n isOpen,\n labelledBy,\n describedBy,\n closeLabel,\n onClose,\n children,\n dismissible = true,\n role = \"dialog\",\n panelClassName,\n}: ModalProps) {\n const mounted = useHasMounted();\n const panelRef = React.useRef<HTMLDivElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: dismissible ? onClose : undefined,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n /*\n `overflow-y-auto` on the overlay, `my-auto` on the panel — not\n `items-center`. A flex item centred with `align-items` overflows equally\n in both directions once it is taller than its container, and the half\n above the top edge cannot be scrolled to: with the page behind it locked,\n an over-tall dialog was simply unreachable at both ends. Auto margins\n centre a short panel the same way and collapse to zero on a tall one,\n which scrolls.\n */\n <div className=\"fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4\">\n {/* `fixed`, not `absolute`: inside a scrolling overlay an absolute\n backdrop scrolls away with the content and leaves the rest of a tall\n dialog sitting on an undimmed page. */}\n <button\n type=\"button\"\n aria-label={closeLabel}\n tabIndex={-1}\n className=\"fixed inset-0 bg-slate-950/50\"\n onClick={() => {\n if (dismissible) onClose();\n }}\n />\n <div\n ref={panelRef}\n role={role}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none\",\n panelClassName ?? \"max-w-md p-6\",\n )}\n >\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":["import_lucide_react","React","import_react_dom","import_jsx_runtime","React","active","React","import_jsx_runtime","import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/side-drawer.tsx","../src/button.tsx","../src/spinner.tsx","../src/utils.ts","../src/dialogLayer.ts","../src/modal.tsx"],"sourcesContent":["\"use client\";\n\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { Button } from \"./button\";\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { useHasMounted } from \"./modal\";\nimport { cn } from \"./utils\";\n\ninterface SideDrawerProps {\n isOpen: boolean;\n title: string;\n onClose: () => void;\n children: React.ReactNode;\n /** Tailwind width class for the panel. Defaults to a reading-width column. */\n panelClassName?: string;\n}\n\n/**\n * A panel that slides in from the right.\n *\n * For content that belongs beside the page rather than on top of it — a queue\n * you glance at and dismiss, not a decision you have to answer. Unlike\n * `Modal`, it does not centre or dim the whole page to a stop: the table\n * underneath stays readable, which is the point of looking at both.\n */\nexport function SideDrawer({\n isOpen,\n title,\n onClose,\n children,\n panelClassName,\n}: SideDrawerProps) {\n const mounted = useHasMounted();\n const headingId = React.useId();\n const panelRef = React.useRef<HTMLElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: onClose,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n <>\n <div\n aria-hidden\n onClick={onClose}\n className=\"fixed inset-0 z-[90] bg-overlay/30\"\n />\n <aside\n ref={panelRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={headingId}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background fixed inset-y-0 right-0 z-[95] flex flex-col border-l shadow-2xl outline-none\",\n panelClassName ?? \"w-full max-w-md\",\n )}\n >\n <div className=\"border-border flex items-center justify-between gap-3 border-b px-4 py-3\">\n <h2 id={headingId} className=\"truncate text-base font-semibold\">\n {title}\n </h2>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"Close panel\"\n onClick={onClose}\n >\n <X className=\"h-4 w-4\" aria-hidden />\n </Button>\n </div>\n\n <div className=\"min-h-0 flex-1 overflow-y-auto p-4\">{children}</div>\n </aside>\n </>,\n document.body,\n );\n}\n","import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"@radix-ui/react-slot\";\n\nimport { Spinner } from \"./spinner\";\nimport { cn } from \"./utils\";\n\nconst buttonVariants = cva(\n \"inline-flex h-9 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md px-3 text-sm font-medium transition duration-200 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n secondary: \"bg-muted text-foreground hover:bg-muted/80\",\n ghost: \"hover:bg-muted\",\n destructive:\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline: \"border border-border bg-background hover:bg-muted\",\n },\n size: {\n sm: \"h-8 px-2.5\",\n md: \"h-9 px-3\",\n lg: \"h-10 px-4\",\n icon: \"h-9 w-9 px-0\",\n // Square, matching `sm`'s height — for an icon-only control sitting in\n // a table row or beside `sm` buttons, where `icon` is a row taller.\n \"icon-sm\": \"h-8 w-8 px-0\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"md\",\n },\n },\n);\n\nexport interface ButtonProps\n extends\n React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n /**\n * Work is in flight: shows a spinner, disables the button, and sets\n * `aria-busy`.\n *\n * This exists because the panel had 272 disabled buttons and only 22 that\n * said anything about why — the rest went dead on click with no spinner and\n * no label change, which reads as a broken button rather than a busy one.\n * Making the feedback part of `disabled` rather than something each caller\n * remembers separately is the only way that ratio stays fixed.\n *\n * The label is kept rather than swapped. \"Saving…\" loses which button you\n * pressed, and the spinner already says it is working. An icon-only button\n * is the exception — there is no room beside the glyph, so the spinner takes\n * its place; the `aria-label` is what names the button either way.\n *\n * \"Icon-only\" means an icon `size`. A square button that grew a spinner\n * beside its glyph would not just look wrong, it would resize mid-request\n * and shove whatever sits next to it sideways.\n */\n loading?: boolean;\n}\n\n/** Sizes with no room for a spinner beside the glyph. */\nconst ICON_SIZES = new Set<string>([\"icon\", \"icon-sm\"]);\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant,\n size,\n asChild = false,\n loading,\n disabled,\n children,\n ...props\n },\n ref,\n ) => {\n const Comp = asChild ? Slot : \"button\";\n\n // `asChild` renders someone else's element, which may not take a spinner or\n // a disabled attribute — so the busy treatment is only applied to a real\n // button, and passing both is a mistake worth not papering over.\n if (asChild) {\n return (\n <Comp\n ref={ref}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n >\n {children}\n </Comp>\n );\n }\n\n return (\n <button\n ref={ref}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n >\n {loading ? <Spinner size=\"sm\" /> : null}\n {loading && size != null && ICON_SIZES.has(size) ? null : children}\n </button>\n );\n },\n);\n\nButton.displayName = \"Button\";\n","import * as React from \"react\";\nimport { Loader2 } from \"lucide-react\";\n\nimport { cn } from \"./utils\";\n\n/**\n * The one spinner.\n *\n * There were five spellings of `animate-spin` across twelve files — `h-4 w-4`,\n * `h-5 w-5`, `size-4`, bare, and a hand-drawn ring in `ProtectedRoute` — so the\n * same \"working\" signal came out a different size depending on which panel you\n * were on.\n *\n * It is `aria-hidden` on purpose. A spinner is decoration; what a screen reader\n * needs is the surrounding control's `aria-busy` or the panel's status text,\n * and announcing \"loading\" twice is worse than once. `Button` and `StatePanel`\n * both supply that.\n */\nconst SIZES = {\n sm: \"size-3.5\",\n md: \"size-4\",\n lg: \"size-5\",\n xl: \"size-10 border-2\",\n} as const;\n\nexport interface SpinnerProps extends React.SVGProps<SVGSVGElement> {\n size?: keyof typeof SIZES;\n}\n\nexport function Spinner({ size = \"md\", className, ...props }: SpinnerProps) {\n return (\n <Loader2\n aria-hidden\n className={cn(\"animate-spin\", SIZES[size], className)}\n {...props}\n />\n );\n}\n","import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * The keyboard and focus behaviour every dialog needs, in one place.\n *\n * `Modal` and `SideDrawer` each had a copy of the scroll lock and an Escape\n * handler, and neither did anything about focus: opening a dialog left focus\n * on the button behind it, Tab walked the page underneath — through the\n * full-screen backdrop button, among other things — and closing left focus\n * wherever it had wandered. That is the whole keyboard story for thirty\n * dialogs, so it is fixed once here rather than thirty times.\n */\n\n/**\n * Open dialogs, innermost last.\n *\n * Nesting is real here — \"Create audience group\" opens on top of \"Send\n * notification\" — and without an ordering both would answer the same Escape\n * and fight over the same Tab. Only the top layer acts.\n */\nconst layers: symbol[] = [];\n\nconst FOCUSABLE = [\n \"a[href]\",\n \"button:not([disabled])\",\n \"input:not([disabled])\",\n \"select:not([disabled])\",\n \"textarea:not([disabled])\",\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(\",\");\n\n/**\n * Deliberately not `offsetParent` or `getClientRects`: the first is null for\n * anything `position: fixed`, which a dialog panel usually is, and the second\n * is empty for *everything* under a test renderer that does no layout — which\n * would collapse the trap to \"no focusable elements\" in every test.\n */\nfunction isVisible(element: HTMLElement): boolean {\n if (element.closest(\"[hidden]\")) return false;\n if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n return typeof element.checkVisibility === \"function\"\n ? element.checkVisibility()\n : true;\n}\n\nfunction focusableWithin(container: HTMLElement): HTMLElement[] {\n return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n isVisible,\n );\n}\n\nexport function useDialogLayer({\n active,\n containerRef,\n onEscape,\n}: {\n active: boolean;\n /** The panel. Focus is moved into it and kept inside it. */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Called on Escape, only while this is the top layer. Omit to ignore it. */\n onEscape?: () => void;\n}): void {\n // Read through a ref so a caller passing a fresh closure every render does\n // not tear the listener down and rebuild it on every keystroke.\n const escapeRef = React.useRef(onEscape);\n React.useEffect(() => {\n escapeRef.current = onEscape;\n });\n\n React.useEffect(() => {\n if (!active) return;\n\n const layer = Symbol(\"dialog-layer\");\n layers.push(layer);\n const isTopLayer = () => layers[layers.length - 1] === layer;\n\n const previouslyFocused =\n document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null;\n\n const container = containerRef.current;\n // Something inside may have claimed focus already — an `autoFocus` input —\n // and that choice is better than any this could make.\n if (container && !container.contains(document.activeElement)) {\n const [first] = focusableWithin(container);\n // The panel itself, not its first control: the first control is a Delete\n // button often enough that focusing it would put a destructive action\n // one Enter away from an accidental keypress.\n (container.hasAttribute(\"tabindex\") ? container : first)?.focus();\n }\n\n function handleKeyDown(event: KeyboardEvent) {\n if (!isTopLayer()) return;\n\n if (event.key === \"Escape\") {\n escapeRef.current?.();\n return;\n }\n\n if (event.key !== \"Tab\") return;\n const panel = containerRef.current;\n if (!panel) return;\n\n const items = focusableWithin(panel);\n if (items.length === 0) {\n event.preventDefault();\n panel.focus();\n return;\n }\n\n const first = items[0]!;\n const last = items[items.length - 1]!;\n const active = document.activeElement;\n const outside = !panel.contains(active);\n\n if (event.shiftKey && (active === first || outside)) {\n event.preventDefault();\n last.focus();\n } else if (!event.shiftKey && (active === last || outside)) {\n event.preventDefault();\n first.focus();\n }\n }\n\n document.addEventListener(\"keydown\", handleKeyDown);\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n\n return () => {\n document.removeEventListener(\"keydown\", handleKeyDown);\n document.body.style.overflow = previousOverflow;\n const index = layers.indexOf(layer);\n if (index !== -1) layers.splice(index, 1);\n // Back where it came from, so closing a dialog opened by keyboard does\n // not drop the caret at the top of the document.\n if (previouslyFocused?.isConnected) previouslyFocused.focus();\n };\n }, [active, containerRef]);\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { useDialogLayer } from \"./dialogLayer\";\nimport { cn } from \"./utils\";\n\nconst emptySubscribe = () => () => {};\n\n/**\n * Returns false during SSR and the first client render, then true once mounted.\n * `useSyncExternalStore` is the React-recommended way to read client-only state\n * without a setState-in-effect (which triggers cascading renders).\n */\nexport function useHasMounted(): boolean {\n return React.useSyncExternalStore(\n emptySubscribe,\n () => true,\n () => false,\n );\n}\n\ninterface ModalProps {\n isOpen: boolean;\n /** id of the element labelling the dialog; wired to aria-labelledby. */\n labelledBy: string;\n /**\n * Optional id(s) of elements describing the dialog (APG alertdialog body).\n * Space-separated when multiple.\n */\n describedBy?: string;\n /** Accessible label for the backdrop dismiss button. */\n closeLabel: string;\n onClose: () => void;\n children: React.ReactNode;\n /**\n * When false, Escape and backdrop clicks do not close the modal (e.g. while a\n * save is in flight). Defaults to true.\n */\n dismissible?: boolean;\n /** Use \"alertdialog\" for destructive confirmations. Defaults to \"dialog\". */\n role?: \"dialog\" | \"alertdialog\";\n /** Tailwind max-width class for the panel. Defaults to \"max-w-md\". */\n panelClassName?: string;\n}\n\n/**\n * Shared modal shell: renders a portal-backed, scroll-locked, Escape-dismissible\n * overlay with a centered panel. Callers provide the panel body (heading, form,\n * footer). Consolidates the boilerplate previously copied across every dialog.\n */\nexport function Modal({\n isOpen,\n labelledBy,\n describedBy,\n closeLabel,\n onClose,\n children,\n dismissible = true,\n role = \"dialog\",\n panelClassName,\n}: ModalProps) {\n const mounted = useHasMounted();\n const panelRef = React.useRef<HTMLDivElement>(null);\n\n useDialogLayer({\n // Gated on `mounted` too: before that the panel is not rendered, so there\n // would be nothing to move focus into.\n active: isOpen && mounted,\n containerRef: panelRef,\n onEscape: dismissible ? onClose : undefined,\n });\n\n if (!mounted || !isOpen) return null;\n\n return createPortal(\n /*\n `overflow-y-auto` on the overlay, `my-auto` on the panel — not\n `items-center`. A flex item centred with `align-items` overflows equally\n in both directions once it is taller than its container, and the half\n above the top edge cannot be scrolled to: with the page behind it locked,\n an over-tall dialog was simply unreachable at both ends. Auto margins\n centre a short panel the same way and collapse to zero on a tall one,\n which scrolls.\n */\n <div className=\"fixed inset-0 z-[100] flex justify-center overflow-y-auto p-4\">\n {/* `fixed`, not `absolute`: inside a scrolling overlay an absolute\n backdrop scrolls away with the content and leaves the rest of a tall\n dialog sitting on an undimmed page. */}\n <button\n type=\"button\"\n aria-label={closeLabel}\n tabIndex={-1}\n className=\"fixed inset-0 bg-overlay/50\"\n onClick={() => {\n if (dismissible) onClose();\n }}\n />\n <div\n ref={panelRef}\n role={role}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n tabIndex={-1}\n className={cn(\n \"border-border bg-background relative z-10 my-auto w-full rounded-lg border shadow-2xl outline-none\",\n panelClassName ?? \"max-w-md p-6\",\n )}\n >\n {children}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":["import_lucide_react","React","import_react_dom","import_jsx_runtime","React","active","React","import_jsx_runtime","import_jsx_runtime"]}