@flanksource/clicky-ui 0.2.11 → 0.2.13

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 (188) hide show
  1. package/dist/components/Combobox.cjs +47 -33
  2. package/dist/components/Combobox.cjs.map +1 -1
  3. package/dist/components/Combobox.d.ts +12 -0
  4. package/dist/components/Combobox.d.ts.map +1 -1
  5. package/dist/components/Combobox.js +48 -34
  6. package/dist/components/Combobox.js.map +1 -1
  7. package/dist/components/IconButton.cjs +26 -0
  8. package/dist/components/IconButton.cjs.map +1 -0
  9. package/dist/components/IconButton.d.ts +25 -0
  10. package/dist/components/IconButton.d.ts.map +1 -0
  11. package/dist/components/IconButton.js +26 -0
  12. package/dist/components/IconButton.js.map +1 -0
  13. package/dist/components/JsonSchemaForm.cjs +1 -1
  14. package/dist/components/JsonSchemaForm.cjs.map +1 -1
  15. package/dist/components/JsonSchemaForm.js +1 -1
  16. package/dist/components/JsonSchemaForm.js.map +1 -1
  17. package/dist/components/SecretKeySelector.cjs +151 -0
  18. package/dist/components/SecretKeySelector.cjs.map +1 -0
  19. package/dist/components/SecretKeySelector.d.ts +47 -0
  20. package/dist/components/SecretKeySelector.d.ts.map +1 -0
  21. package/dist/components/SecretKeySelector.js +151 -0
  22. package/dist/components/SecretKeySelector.js.map +1 -0
  23. package/dist/components/WorkloadPicker.cjs +144 -0
  24. package/dist/components/WorkloadPicker.cjs.map +1 -0
  25. package/dist/components/WorkloadPicker.d.ts +52 -0
  26. package/dist/components/WorkloadPicker.d.ts.map +1 -0
  27. package/dist/components/WorkloadPicker.js +144 -0
  28. package/dist/components/WorkloadPicker.js.map +1 -0
  29. package/dist/components/icon-menu-picker.cjs +83 -112
  30. package/dist/components/icon-menu-picker.cjs.map +1 -1
  31. package/dist/components/icon-menu-picker.d.ts.map +1 -1
  32. package/dist/components/icon-menu-picker.js +85 -114
  33. package/dist/components/icon-menu-picker.js.map +1 -1
  34. package/dist/components/json-schema-form-fields.cjs +9 -7
  35. package/dist/components/json-schema-form-fields.cjs.map +1 -1
  36. package/dist/components/json-schema-form-fields.d.ts.map +1 -1
  37. package/dist/components/json-schema-form-fields.js +9 -7
  38. package/dist/components/json-schema-form-fields.js.map +1 -1
  39. package/dist/components/json-schema-form-types.d.ts +8 -4
  40. package/dist/components/json-schema-form-types.d.ts.map +1 -1
  41. package/dist/components.cjs +10 -0
  42. package/dist/components.cjs.map +1 -1
  43. package/dist/components.d.ts +3 -0
  44. package/dist/components.d.ts.map +1 -1
  45. package/dist/components.js +11 -1
  46. package/dist/components.js.map +1 -1
  47. package/dist/data/Clicky.cjs +1 -1
  48. package/dist/data/Clicky.cjs.map +1 -1
  49. package/dist/data/Clicky.js +1 -1
  50. package/dist/data/Clicky.js.map +1 -1
  51. package/dist/data/LogDetails.cjs +4 -2
  52. package/dist/data/LogDetails.cjs.map +1 -1
  53. package/dist/data/LogDetails.js +4 -2
  54. package/dist/data/LogDetails.js.map +1 -1
  55. package/dist/data/Version.cjs +42 -0
  56. package/dist/data/Version.cjs.map +1 -0
  57. package/dist/data/Version.d.ts +36 -0
  58. package/dist/data/Version.d.ts.map +1 -0
  59. package/dist/data/Version.js +42 -0
  60. package/dist/data/Version.js.map +1 -0
  61. package/dist/data/test-runner/TestDetailPanel.cjs +165 -0
  62. package/dist/data/test-runner/TestDetailPanel.cjs.map +1 -0
  63. package/dist/data/test-runner/TestDetailPanel.d.ts +7 -0
  64. package/dist/data/test-runner/TestDetailPanel.d.ts.map +1 -0
  65. package/dist/data/test-runner/TestDetailPanel.js +165 -0
  66. package/dist/data/test-runner/TestDetailPanel.js.map +1 -0
  67. package/dist/data/test-runner/TestFailureDetail.cjs +78 -0
  68. package/dist/data/test-runner/TestFailureDetail.cjs.map +1 -0
  69. package/dist/data/test-runner/TestFailureDetail.d.ts +14 -0
  70. package/dist/data/test-runner/TestFailureDetail.d.ts.map +1 -0
  71. package/dist/data/test-runner/TestFailureDetail.js +78 -0
  72. package/dist/data/test-runner/TestFailureDetail.js.map +1 -0
  73. package/dist/data/test-runner/TestFilterBar.cjs +108 -0
  74. package/dist/data/test-runner/TestFilterBar.cjs.map +1 -0
  75. package/dist/data/test-runner/TestFilterBar.d.ts +15 -0
  76. package/dist/data/test-runner/TestFilterBar.d.ts.map +1 -0
  77. package/dist/data/test-runner/TestFilterBar.js +108 -0
  78. package/dist/data/test-runner/TestFilterBar.js.map +1 -0
  79. package/dist/data/test-runner/TestRunSummary.cjs +81 -0
  80. package/dist/data/test-runner/TestRunSummary.cjs.map +1 -0
  81. package/dist/data/test-runner/TestRunSummary.d.ts +19 -0
  82. package/dist/data/test-runner/TestRunSummary.d.ts.map +1 -0
  83. package/dist/data/test-runner/TestRunSummary.js +81 -0
  84. package/dist/data/test-runner/TestRunSummary.js.map +1 -0
  85. package/dist/data/test-runner/TestRunner.cjs +143 -0
  86. package/dist/data/test-runner/TestRunner.cjs.map +1 -0
  87. package/dist/data/test-runner/TestRunner.d.ts +42 -0
  88. package/dist/data/test-runner/TestRunner.d.ts.map +1 -0
  89. package/dist/data/test-runner/TestRunner.fixtures.d.ts +26 -0
  90. package/dist/data/test-runner/TestRunner.fixtures.d.ts.map +1 -0
  91. package/dist/data/test-runner/TestRunner.js +143 -0
  92. package/dist/data/test-runner/TestRunner.js.map +1 -0
  93. package/dist/data/test-runner/TestTree.cjs +42 -0
  94. package/dist/data/test-runner/TestTree.cjs.map +1 -0
  95. package/dist/data/test-runner/TestTree.d.ts +9 -0
  96. package/dist/data/test-runner/TestTree.d.ts.map +1 -0
  97. package/dist/data/test-runner/TestTree.js +42 -0
  98. package/dist/data/test-runner/TestTree.js.map +1 -0
  99. package/dist/data/test-runner/TestTreeNode.cjs +99 -0
  100. package/dist/data/test-runner/TestTreeNode.cjs.map +1 -0
  101. package/dist/data/test-runner/TestTreeNode.d.ts +12 -0
  102. package/dist/data/test-runner/TestTreeNode.d.ts.map +1 -0
  103. package/dist/data/test-runner/TestTreeNode.js +99 -0
  104. package/dist/data/test-runner/TestTreeNode.js.map +1 -0
  105. package/dist/data/test-runner/adapter.cjs +57 -0
  106. package/dist/data/test-runner/adapter.cjs.map +1 -0
  107. package/dist/data/test-runner/adapter.d.ts +60 -0
  108. package/dist/data/test-runner/adapter.d.ts.map +1 -0
  109. package/dist/data/test-runner/adapter.js +57 -0
  110. package/dist/data/test-runner/adapter.js.map +1 -0
  111. package/dist/data/test-runner/context.cjs +15 -0
  112. package/dist/data/test-runner/context.cjs.map +1 -0
  113. package/dist/data/test-runner/context.d.ts +40 -0
  114. package/dist/data/test-runner/context.d.ts.map +1 -0
  115. package/dist/data/test-runner/context.js +15 -0
  116. package/dist/data/test-runner/context.js.map +1 -0
  117. package/dist/data/test-runner/filterState.cjs +47 -0
  118. package/dist/data/test-runner/filterState.cjs.map +1 -0
  119. package/dist/data/test-runner/filterState.d.ts +14 -0
  120. package/dist/data/test-runner/filterState.d.ts.map +1 -0
  121. package/dist/data/test-runner/filterState.js +47 -0
  122. package/dist/data/test-runner/filterState.js.map +1 -0
  123. package/dist/data/test-runner/frameworkIcon.cjs +27 -0
  124. package/dist/data/test-runner/frameworkIcon.cjs.map +1 -0
  125. package/dist/data/test-runner/frameworkIcon.d.ts +3 -0
  126. package/dist/data/test-runner/frameworkIcon.d.ts.map +1 -0
  127. package/dist/data/test-runner/frameworkIcon.js +27 -0
  128. package/dist/data/test-runner/frameworkIcon.js.map +1 -0
  129. package/dist/data/test-runner/index.d.ts +14 -0
  130. package/dist/data/test-runner/index.d.ts.map +1 -0
  131. package/dist/data/test-runner/status.cjs +234 -0
  132. package/dist/data/test-runner/status.cjs.map +1 -0
  133. package/dist/data/test-runner/status.d.ts +52 -0
  134. package/dist/data/test-runner/status.d.ts.map +1 -0
  135. package/dist/data/test-runner/status.js +234 -0
  136. package/dist/data/test-runner/status.js.map +1 -0
  137. package/dist/data/test-runner/types.d.ts +148 -0
  138. package/dist/data/test-runner/types.d.ts.map +1 -0
  139. package/dist/data/version-info.cjs +26 -0
  140. package/dist/data/version-info.cjs.map +1 -0
  141. package/dist/data/version-info.d.ts +11 -0
  142. package/dist/data/version-info.d.ts.map +1 -0
  143. package/dist/data/version-info.js +26 -0
  144. package/dist/data/version-info.js.map +1 -0
  145. package/dist/data.cjs +49 -0
  146. package/dist/data.cjs.map +1 -1
  147. package/dist/data.d.ts +3 -0
  148. package/dist/data.d.ts.map +1 -1
  149. package/dist/data.js +50 -1
  150. package/dist/data.js.map +1 -1
  151. package/dist/index.cjs +59 -0
  152. package/dist/index.cjs.map +1 -1
  153. package/dist/index.js +60 -1
  154. package/dist/index.js.map +1 -1
  155. package/dist/layout/SplitPane.cjs +34 -27
  156. package/dist/layout/SplitPane.cjs.map +1 -1
  157. package/dist/layout/SplitPane.d.ts.map +1 -1
  158. package/dist/layout/SplitPane.js +34 -27
  159. package/dist/layout/SplitPane.js.map +1 -1
  160. package/dist/overlay/DropdownMenu.cjs +57 -30
  161. package/dist/overlay/DropdownMenu.cjs.map +1 -1
  162. package/dist/overlay/DropdownMenu.d.ts.map +1 -1
  163. package/dist/overlay/DropdownMenu.js +57 -30
  164. package/dist/overlay/DropdownMenu.js.map +1 -1
  165. package/dist/overlay/Modal.cjs +62 -6
  166. package/dist/overlay/Modal.cjs.map +1 -1
  167. package/dist/overlay/Modal.d.ts +19 -1
  168. package/dist/overlay/Modal.d.ts.map +1 -1
  169. package/dist/overlay/Modal.js +62 -6
  170. package/dist/overlay/Modal.js.map +1 -1
  171. package/dist/rpc/CommandForm.cjs +2 -2
  172. package/dist/rpc/CommandForm.cjs.map +1 -1
  173. package/dist/rpc/CommandForm.d.ts.map +1 -1
  174. package/dist/rpc/CommandForm.js +3 -3
  175. package/dist/rpc/CommandForm.js.map +1 -1
  176. package/dist/rpc/formMetadata.cjs +5 -4
  177. package/dist/rpc/formMetadata.cjs.map +1 -1
  178. package/dist/rpc/formMetadata.d.ts.map +1 -1
  179. package/dist/rpc/formMetadata.js +6 -5
  180. package/dist/rpc/formMetadata.js.map +1 -1
  181. package/dist/rpc/types.cjs +4 -0
  182. package/dist/rpc/types.cjs.map +1 -1
  183. package/dist/rpc/types.d.ts +10 -0
  184. package/dist/rpc/types.d.ts.map +1 -1
  185. package/dist/rpc/types.js +5 -1
  186. package/dist/rpc/types.js.map +1 -1
  187. package/dist/styles.css +1 -1
  188. package/package.json +3 -1
@@ -1,5 +1,6 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
2
  import { useState, useRef, useEffect } from "react";
3
+ import { useFloating, autoUpdate, offset, flip, shift, useClick, useDismiss, useRole, useListNavigation, useInteractions, FloatingPortal, FloatingFocusManager } from "@floating-ui/react";
3
4
  import { cn } from "../lib/utils.js";
4
5
  import { Button } from "../components/button.js";
5
6
  import { Icon } from "../data/Icon.js";
@@ -20,50 +21,61 @@ function DropdownMenu({
20
21
  menuClassName
21
22
  }) {
22
23
  const [open, setOpen] = useState(false);
23
- const wrapperRef = useRef(null);
24
+ const [activeIndex, setActiveIndex] = useState(null);
25
+ const listRef = useRef([]);
24
26
  const onOpenChangeRef = useRef(onOpenChange);
25
27
  onOpenChangeRef.current = onOpenChange;
26
28
  useEffect(() => {
27
29
  var _a;
28
30
  (_a = onOpenChangeRef.current) == null ? void 0 : _a.call(onOpenChangeRef, open);
29
31
  }, [open]);
30
- useEffect(() => {
31
- if (!open) return;
32
- const onMouseDown = (e) => {
33
- var _a;
34
- if (!((_a = wrapperRef.current) == null ? void 0 : _a.contains(e.target))) setOpen(false);
35
- };
36
- const onKeyDown = (e) => {
37
- if (e.key === "Escape") setOpen(false);
38
- };
39
- document.addEventListener("mousedown", onMouseDown);
40
- document.addEventListener("keydown", onKeyDown);
41
- return () => {
42
- document.removeEventListener("mousedown", onMouseDown);
43
- document.removeEventListener("keydown", onKeyDown);
44
- };
45
- }, [open]);
32
+ const { refs, floatingStyles, context } = useFloating({
33
+ open,
34
+ onOpenChange: setOpen,
35
+ // bottom-end / bottom-start reproduce the previous right/left alignment.
36
+ placement: align === "right" ? "bottom-end" : "bottom-start",
37
+ whileElementsMounted: autoUpdate,
38
+ // offset(4) the previous mt-1; flip opens upward at the bottom edge and
39
+ // shift slides the menu back on-screen at the horizontal edges.
40
+ middleware: [offset(4), flip({ padding: 8 }), shift({ padding: 8 })]
41
+ });
42
+ const click = useClick(context);
43
+ const dismiss = useDismiss(context);
44
+ const role = useRole(context, { role: "menu" });
45
+ const listNav = useListNavigation(context, {
46
+ listRef,
47
+ activeIndex,
48
+ onNavigate: setActiveIndex,
49
+ loop: true
50
+ });
51
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
52
+ click,
53
+ dismiss,
54
+ role,
55
+ listNav
56
+ ]);
46
57
  const closeMenu = () => setOpen(false);
47
- const toggle = () => setOpen((v) => !v);
48
- return /* @__PURE__ */ jsxs("div", { className: cn("relative inline-flex", className), ref: wrapperRef, children: [
58
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative inline-flex", className), children: [
49
59
  trigger ? /* @__PURE__ */ jsx(
50
60
  "span",
51
61
  {
52
- className: "contents",
53
- onClick: toggle,
62
+ ref: refs.setReference,
63
+ className: "inline-flex",
54
64
  "aria-haspopup": "menu",
55
65
  "aria-expanded": open,
66
+ ...getReferenceProps(),
56
67
  children: trigger
57
68
  }
58
69
  ) : /* @__PURE__ */ jsxs(
59
70
  Button,
60
71
  {
72
+ ref: refs.setReference,
61
73
  variant,
62
74
  size,
63
- onClick: toggle,
64
75
  title,
65
76
  "aria-haspopup": "menu",
66
77
  "aria-expanded": open,
78
+ ...getReferenceProps(),
67
79
  children: [
68
80
  icon && /* @__PURE__ */ jsx(Icon, { ...typeof icon === "string" ? { name: icon } : { icon } }),
69
81
  label,
@@ -71,27 +83,42 @@ function DropdownMenu({
71
83
  ]
72
84
  }
73
85
  ),
74
- open && /* @__PURE__ */ jsx(
86
+ open && /* @__PURE__ */ jsx(FloatingPortal, { children: /* @__PURE__ */ jsx(FloatingFocusManager, { context, modal: false, children: /* @__PURE__ */ jsx(
75
87
  "div",
76
88
  {
89
+ ref: refs.setFloating,
77
90
  role: "menu",
91
+ style: floatingStyles,
78
92
  className: cn(
79
- "absolute z-20 mt-1 min-w-[8rem] rounded-md border border-border bg-popover py-1 shadow-md",
80
- align === "right" ? "right-0" : "left-0",
93
+ "z-[9999] min-w-[8rem] rounded-md border border-border bg-popover py-1 shadow-md",
81
94
  menuClassName
82
95
  ),
96
+ ...getFloatingProps(),
83
97
  children: children ? children(closeMenu) : items == null ? void 0 : items.map((item, i) => /* @__PURE__ */ jsxs(
84
98
  "button",
85
99
  {
100
+ ref: (node) => {
101
+ listRef.current[i] = node;
102
+ },
86
103
  type: "button",
87
104
  role: "menuitem",
105
+ tabIndex: i === activeIndex ? 0 : -1,
88
106
  disabled: item.disabled,
89
107
  title: item.title,
90
- onClick: () => {
91
- item.onSelect();
92
- closeMenu();
93
- },
94
108
  className: "flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-popover-foreground hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50",
109
+ ...getItemProps({
110
+ onClick: () => {
111
+ item.onSelect();
112
+ closeMenu();
113
+ },
114
+ onKeyDown: (event) => {
115
+ if (event.key === "Enter" || event.key === " ") {
116
+ event.preventDefault();
117
+ item.onSelect();
118
+ closeMenu();
119
+ }
120
+ }
121
+ }),
95
122
  children: [
96
123
  item.icon && /* @__PURE__ */ jsx(
97
124
  Icon,
@@ -105,7 +132,7 @@ function DropdownMenu({
105
132
  i
106
133
  ))
107
134
  }
108
- )
135
+ ) }) })
109
136
  ] });
110
137
  }
111
138
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMenu.js","sources":["../../src/overlay/DropdownMenu.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button, type ButtonProps } from \"../components/button\";\nimport { Icon, type StaticIconComponent } from \"../data/Icon\";\nimport { UiChevronDown } from \"../icons\";\n\nexport type DropdownMenuItem = {\n /** Visible label. */\n label: ReactNode;\n /** Called when the item is chosen. */\n onSelect: () => void;\n /** Iconify name or imported icon component rendered before the label. */\n icon?: string | StaticIconComponent;\n /** Browser tooltip for the item. */\n title?: string;\n /** Disable selection. */\n disabled?: boolean;\n};\n\nexport type DropdownMenuProps = {\n /** Trigger label. Ignored when `trigger` is provided. */\n label?: ReactNode;\n /** Icon shown before the trigger label. Ignored when `trigger` is provided. */\n icon?: string | StaticIconComponent;\n /** Hide the trailing chevron on the default trigger. */\n hideChevron?: boolean;\n /** Variant forwarded to the default Button trigger. */\n variant?: ButtonProps[\"variant\"];\n /** Size forwarded to the default Button trigger. */\n size?: ButtonProps[\"size\"];\n /** Fully custom trigger. Receives no props; the menu wraps it with open state. */\n trigger?: ReactNode;\n /** Declarative menu items. Provide this or `children`, not both. */\n items?: DropdownMenuItem[];\n /** Custom menu content. Use `closeMenu` to dismiss after a selection. */\n children?: (closeMenu: () => void) => ReactNode;\n /** Horizontal alignment of the menu relative to the trigger. */\n align?: \"left\" | \"right\";\n /** Notified whenever the menu opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Browser tooltip / accessible label for the default trigger. */\n title?: string;\n /** Classes applied to the wrapper. */\n className?: string;\n /** Classes applied to the floating menu. */\n menuClassName?: string;\n};\n\nexport function DropdownMenu({\n label,\n icon,\n hideChevron = false,\n variant = \"outline\",\n size = \"sm\",\n trigger,\n items,\n children,\n align = \"right\",\n onOpenChange,\n title,\n className,\n menuClassName,\n}: DropdownMenuProps) {\n const [open, setOpen] = useState(false);\n const wrapperRef = useRef<HTMLDivElement>(null);\n\n const onOpenChangeRef = useRef(onOpenChange);\n onOpenChangeRef.current = onOpenChange;\n useEffect(() => {\n onOpenChangeRef.current?.(open);\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const onMouseDown = (e: MouseEvent) => {\n if (!wrapperRef.current?.contains(e.target as Node)) setOpen(false);\n };\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(\"mousedown\", onMouseDown);\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"mousedown\", onMouseDown);\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [open]);\n\n const closeMenu = () => setOpen(false);\n const toggle = () => setOpen((v) => !v);\n\n return (\n <div className={cn(\"relative inline-flex\", className)} ref={wrapperRef}>\n {trigger ? (\n <span\n className=\"contents\"\n onClick={toggle}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n >\n {trigger}\n </span>\n ) : (\n <Button\n variant={variant}\n size={size}\n onClick={toggle}\n title={title}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n >\n {icon && <Icon {...(typeof icon === \"string\" ? { name: icon } : { icon })} />}\n {label}\n {!hideChevron && <Icon icon={UiChevronDown} />}\n </Button>\n )}\n {open && (\n <div\n role=\"menu\"\n className={cn(\n \"absolute z-20 mt-1 min-w-[8rem] rounded-md border border-border bg-popover py-1 shadow-md\",\n align === \"right\" ? \"right-0\" : \"left-0\",\n menuClassName,\n )}\n >\n {children\n ? children(closeMenu)\n : items?.map((item, i) => (\n <button\n key={i}\n type=\"button\"\n role=\"menuitem\"\n disabled={item.disabled}\n title={item.title}\n onClick={() => {\n item.onSelect();\n closeMenu();\n }}\n className=\"flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-popover-foreground hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\"\n >\n {item.icon && (\n <Icon\n {...(typeof item.icon === \"string\"\n ? { name: item.icon }\n : { icon: item.icon })}\n />\n )}\n {item.label}\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":";;;;;;AAgDO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,UAAU;AAAA,EACV,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,aAAa,OAAuB,IAAI;AAE9C,QAAM,kBAAkB,OAAO,YAAY;AAC3C,kBAAgB,UAAU;AAC1B,YAAU,MAAM;;AACd,0BAAgB,YAAhB,yCAA0B;AAAA,EAC5B,GAAG,CAAC,IAAI,CAAC;AAET,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,UAAM,cAAc,CAAC,MAAkB;;AACrC,UAAI,GAAC,gBAAW,YAAX,mBAAoB,SAAS,EAAE,kBAAyB,KAAK;AAAA,IACpE;AACA,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,SAAU,SAAQ,KAAK;AAAA,IACvC;AACA,aAAS,iBAAiB,aAAa,WAAW;AAClD,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AACX,eAAS,oBAAoB,aAAa,WAAW;AACrD,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,YAAY,MAAM,QAAQ,KAAK;AACrC,QAAM,SAAS,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEtC,SACE,qBAAC,SAAI,WAAW,GAAG,wBAAwB,SAAS,GAAG,KAAK,YACzD,UAAA;AAAA,IAAA,UACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,SAAS;AAAA,QACT,iBAAc;AAAA,QACd,iBAAe;AAAA,QAEd,UAAA;AAAA,MAAA;AAAA,IAAA,IAGH;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT;AAAA,QACA,iBAAc;AAAA,QACd,iBAAe;AAAA,QAEd,UAAA;AAAA,UAAA,QAAQ,oBAAC,MAAA,EAAM,GAAI,OAAO,SAAS,WAAW,EAAE,MAAM,KAAA,IAAS,EAAE,KAAA,EAAK,CAAI;AAAA,UAC1E;AAAA,UACA,CAAC,eAAe,oBAAC,MAAA,EAAK,MAAM,cAAA,CAAe;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAG/C,QACC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAW;AAAA,UACT;AAAA,UACA,UAAU,UAAU,YAAY;AAAA,UAChC;AAAA,QAAA;AAAA,QAGD,UAAA,WACG,SAAS,SAAS,IAClB,+BAAO,IAAI,CAAC,MAAM,MAChB;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,UAAU,KAAK;AAAA,YACf,OAAO,KAAK;AAAA,YACZ,SAAS,MAAM;AACb,mBAAK,SAAA;AACL,wBAAA;AAAA,YACF;AAAA,YACA,WAAU;AAAA,YAET,UAAA;AAAA,cAAA,KAAK,QACJ;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACE,GAAI,OAAO,KAAK,SAAS,WACtB,EAAE,MAAM,KAAK,KAAA,IACb,EAAE,MAAM,KAAK,KAAA;AAAA,gBAAK;AAAA,cAAA;AAAA,cAGzB,KAAK;AAAA,YAAA;AAAA,UAAA;AAAA,UAlBD;AAAA,QAAA;AAAA,MAoBR;AAAA,IAAA;AAAA,EACP,GAEJ;AAEJ;"}
1
+ {"version":3,"file":"DropdownMenu.js","sources":["../../src/overlay/DropdownMenu.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n autoUpdate,\n flip,\n FloatingFocusManager,\n FloatingPortal,\n offset,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useInteractions,\n useListNavigation,\n useRole,\n} from \"@floating-ui/react\";\nimport { cn } from \"../lib/utils\";\nimport { Button, type ButtonProps } from \"../components/button\";\nimport { Icon, type StaticIconComponent } from \"../data/Icon\";\nimport { UiChevronDown } from \"../icons\";\n\nexport type DropdownMenuItem = {\n /** Visible label. */\n label: ReactNode;\n /** Called when the item is chosen. */\n onSelect: () => void;\n /** Iconify name or imported icon component rendered before the label. */\n icon?: string | StaticIconComponent;\n /** Browser tooltip for the item. */\n title?: string;\n /** Disable selection. */\n disabled?: boolean;\n};\n\nexport type DropdownMenuProps = {\n /** Trigger label. Ignored when `trigger` is provided. */\n label?: ReactNode;\n /** Icon shown before the trigger label. Ignored when `trigger` is provided. */\n icon?: string | StaticIconComponent;\n /** Hide the trailing chevron on the default trigger. */\n hideChevron?: boolean;\n /** Variant forwarded to the default Button trigger. */\n variant?: ButtonProps[\"variant\"];\n /** Size forwarded to the default Button trigger. */\n size?: ButtonProps[\"size\"];\n /** Fully custom trigger. Receives no props; the menu wraps it with open state. */\n trigger?: ReactNode;\n /** Declarative menu items. Provide this or `children`, not both. */\n items?: DropdownMenuItem[];\n /** Custom menu content. Use `closeMenu` to dismiss after a selection. */\n children?: (closeMenu: () => void) => ReactNode;\n /** Horizontal alignment of the menu relative to the trigger. */\n align?: \"left\" | \"right\";\n /** Notified whenever the menu opens or closes. */\n onOpenChange?: (open: boolean) => void;\n /** Browser tooltip / accessible label for the default trigger. */\n title?: string;\n /** Classes applied to the wrapper. */\n className?: string;\n /** Classes applied to the floating menu. */\n menuClassName?: string;\n};\n\nexport function DropdownMenu({\n label,\n icon,\n hideChevron = false,\n variant = \"outline\",\n size = \"sm\",\n trigger,\n items,\n children,\n align = \"right\",\n onOpenChange,\n title,\n className,\n menuClassName,\n}: DropdownMenuProps) {\n const [open, setOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const listRef = useRef<(HTMLElement | null)[]>([]);\n\n const onOpenChangeRef = useRef(onOpenChange);\n onOpenChangeRef.current = onOpenChange;\n useEffect(() => {\n onOpenChangeRef.current?.(open);\n }, [open]);\n\n const { refs, floatingStyles, context } = useFloating<HTMLDivElement>({\n open,\n onOpenChange: setOpen,\n // bottom-end / bottom-start reproduce the previous right/left alignment.\n placement: align === \"right\" ? \"bottom-end\" : \"bottom-start\",\n whileElementsMounted: autoUpdate,\n // offset(4) the previous mt-1; flip opens upward at the bottom edge and\n // shift slides the menu back on-screen at the horizontal edges.\n middleware: [offset(4), flip({ padding: 8 }), shift({ padding: 8 })],\n });\n\n const click = useClick(context);\n const dismiss = useDismiss(context);\n const role = useRole(context, { role: \"menu\" });\n const listNav = useListNavigation(context, {\n listRef,\n activeIndex,\n onNavigate: setActiveIndex,\n loop: true,\n });\n\n const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([\n click,\n dismiss,\n role,\n listNav,\n ]);\n\n const closeMenu = () => setOpen(false);\n\n return (\n <div className={cn(\"relative inline-flex\", className)}>\n {trigger ? (\n <span\n ref={refs.setReference}\n className=\"inline-flex\"\n aria-haspopup=\"menu\"\n aria-expanded={open}\n {...getReferenceProps()}\n >\n {trigger}\n </span>\n ) : (\n <Button\n ref={refs.setReference as React.Ref<HTMLButtonElement>}\n variant={variant}\n size={size}\n title={title}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n {...getReferenceProps()}\n >\n {icon && <Icon {...(typeof icon === \"string\" ? { name: icon } : { icon })} />}\n {label}\n {!hideChevron && <Icon icon={UiChevronDown} />}\n </Button>\n )}\n {open && (\n <FloatingPortal>\n <FloatingFocusManager context={context} modal={false}>\n <div\n ref={refs.setFloating}\n role=\"menu\"\n style={floatingStyles}\n className={cn(\n \"z-[9999] min-w-[8rem] rounded-md border border-border bg-popover py-1 shadow-md\",\n menuClassName,\n )}\n {...getFloatingProps()}\n >\n {children\n ? children(closeMenu)\n : items?.map((item, i) => (\n <button\n key={i}\n ref={(node) => {\n listRef.current[i] = node;\n }}\n type=\"button\"\n role=\"menuitem\"\n tabIndex={i === activeIndex ? 0 : -1}\n disabled={item.disabled}\n title={item.title}\n className=\"flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-popover-foreground hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50\"\n {...getItemProps({\n onClick: () => {\n item.onSelect();\n closeMenu();\n },\n onKeyDown: (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n item.onSelect();\n closeMenu();\n }\n },\n })}\n >\n {item.icon && (\n <Icon\n {...(typeof item.icon === \"string\"\n ? { name: item.icon }\n : { icon: item.icon })}\n />\n )}\n {item.label}\n </button>\n ))}\n </div>\n </FloatingFocusManager>\n </FloatingPortal>\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("react");
5
5
  const utils = require("../lib/utils.cjs");
6
6
  const Icon = require("../data/Icon.cjs");
7
+ const button = require("../components/button.cjs");
7
8
  const UiFullscreen = require("../icons/components/UiFullscreen.cjs");
8
9
  const UiClose = require("../icons/components/UiClose.cjs");
9
10
  const sizeClass = {
@@ -13,9 +14,16 @@ const sizeClass = {
13
14
  xl: "max-w-4xl max-h-[90vh]",
14
15
  full: "max-w-[95vw] max-h-[95vh]"
15
16
  };
17
+ const DEFAULT_CONFIRM = {
18
+ title: "Discard changes?",
19
+ message: "You have unsaved changes. Closing now will discard them.",
20
+ confirmLabel: "Discard",
21
+ cancelLabel: "Keep editing"
22
+ };
16
23
  function Modal({
17
24
  open,
18
25
  onClose,
26
+ confirmClose = false,
19
27
  title,
20
28
  size = "md",
21
29
  closeOnBackdrop = true,
@@ -29,17 +37,30 @@ function Modal({
29
37
  }) {
30
38
  const dialogRef = react.useRef(null);
31
39
  const [expanded, setExpanded] = react.useState(false);
40
+ const [confirming, setConfirming] = react.useState(false);
41
+ const requestClose = () => {
42
+ if (confirmClose) setConfirming(true);
43
+ else onClose();
44
+ };
32
45
  react.useEffect(() => {
33
- if (!open) setExpanded(false);
46
+ if (!open) {
47
+ setExpanded(false);
48
+ setConfirming(false);
49
+ }
34
50
  }, [open]);
51
+ react.useEffect(() => {
52
+ if (!confirmClose) setConfirming(false);
53
+ }, [confirmClose]);
35
54
  react.useEffect(() => {
36
55
  if (!open || !closeOnEsc) return;
37
56
  const onKey = (e) => {
38
- if (e.key === "Escape") onClose();
57
+ if (e.key !== "Escape") return;
58
+ if (confirming) setConfirming(false);
59
+ else requestClose();
39
60
  };
40
61
  document.addEventListener("keydown", onKey);
41
62
  return () => document.removeEventListener("keydown", onKey);
42
- }, [open, closeOnEsc, onClose]);
63
+ }, [open, closeOnEsc, confirming, confirmClose, onClose]);
43
64
  react.useEffect(() => {
44
65
  var _a;
45
66
  if (!open) return;
@@ -55,7 +76,7 @@ function Modal({
55
76
  "div",
56
77
  {
57
78
  className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40",
58
- onClick: closeOnBackdrop ? onClose : void 0,
79
+ onClick: closeOnBackdrop ? requestClose : void 0,
59
80
  role: "presentation",
60
81
  children: /* @__PURE__ */ jsxRuntime.jsxs(
61
82
  "div",
@@ -89,7 +110,7 @@ function Modal({
89
110
  "button",
90
111
  {
91
112
  type: "button",
92
- onClick: onClose,
113
+ onClick: requestClose,
93
114
  "aria-label": "Close",
94
115
  className: "text-muted-foreground hover:text-foreground",
95
116
  children: /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiClose.UiClose })
@@ -97,12 +118,47 @@ function Modal({
97
118
  )
98
119
  ] }),
99
120
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-auto px-density-4 py-density-3", children }),
100
- footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-density-4 py-density-3 border-t border-border", children: footer })
121
+ footer && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-density-4 py-density-3 border-t border-border", children: footer }),
122
+ confirming && /* @__PURE__ */ jsxRuntime.jsx(
123
+ ConfirmClosePrompt,
124
+ {
125
+ options: confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose },
126
+ onConfirm: () => {
127
+ setConfirming(false);
128
+ onClose();
129
+ },
130
+ onCancel: () => setConfirming(false)
131
+ }
132
+ )
101
133
  ]
102
134
  }
103
135
  )
104
136
  }
105
137
  );
106
138
  }
139
+ function ConfirmClosePrompt({
140
+ options,
141
+ onConfirm,
142
+ onCancel
143
+ }) {
144
+ return /* @__PURE__ */ jsxRuntime.jsx(
145
+ "div",
146
+ {
147
+ className: "absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4",
148
+ role: "alertdialog",
149
+ "aria-modal": "true",
150
+ "aria-label": typeof options.title === "string" ? options.title : "Confirm close",
151
+ onClick: (e) => e.stopPropagation(),
152
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl", children: [
153
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold", children: options.title }),
154
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-density-2 text-sm text-muted-foreground", children: options.message }),
155
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-density-4 flex justify-end gap-density-2", children: [
156
+ /* @__PURE__ */ jsxRuntime.jsx(button.Button, { variant: "ghost", size: "sm", onClick: onCancel, children: options.cancelLabel }),
157
+ /* @__PURE__ */ jsxRuntime.jsx(button.Button, { variant: "destructive", size: "sm", onClick: onConfirm, children: options.confirmLabel })
158
+ ] })
159
+ ] })
160
+ }
161
+ );
162
+ }
107
163
  exports.Modal = Modal;
108
164
  //# sourceMappingURL=Modal.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.cjs","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm max-h-[90vh]\",\n md: \"max-w-md max-h-[90vh]\",\n lg: \"max-w-2xl max-h-[90vh]\",\n xl: \"max-w-4xl max-h-[90vh]\",\n full: \"max-w-[95vw] max-h-[95vh]\",\n};\n\nexport function Modal({\n open,\n onClose,\n title,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const [expanded, setExpanded] = useState(false);\n\n useEffect(() => {\n if (!open) setExpanded(false);\n }, [open]);\n\n useEffect(() => {\n if (!open || !closeOnEsc) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, closeOnEsc, onClose]);\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/40\"\n onClick={closeOnBackdrop ? onClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative bg-background border border-border rounded-lg shadow-xl w-full flex flex-col\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || headerSlot || expandable || !hideClose) && (\n <div className=\"flex items-center gap-density-2 px-density-4 py-density-3 border-b border-border\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n </div>\n </div>\n );\n}\n"],"names":["useRef","useState","useEffect","_a","jsx","jsxs","cn","Icon","UiFullscreenFilled","UiFullscreen","UiClose"],"mappings":"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"}
1
+ {"version":3,"file":"Modal.cjs","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { Button } from \"../components/button\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Copy for the discard-confirmation prompt shown before a guarded close. */\nexport type ConfirmCloseOptions = {\n /** Heading of the confirmation prompt. */\n title?: ReactNode;\n /** Body explaining what is lost on close. */\n message?: ReactNode;\n /** Label of the button that proceeds with closing. */\n confirmLabel?: string;\n /** Label of the button that dismisses the prompt and keeps the modal open. */\n cancelLabel?: string;\n};\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /**\n * Guard every close path (close button, Escape, backdrop) behind a discard\n * confirmation. `true` uses the default copy; pass options to customise it.\n * `onClose` only fires once the user confirms. Set this while the modal holds\n * unsaved edits and clear it (or pass `false`) once there is nothing to lose.\n */\n confirmClose?: boolean | ConfirmCloseOptions;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm max-h-[90vh]\",\n md: \"max-w-md max-h-[90vh]\",\n lg: \"max-w-2xl max-h-[90vh]\",\n xl: \"max-w-4xl max-h-[90vh]\",\n full: \"max-w-[95vw] max-h-[95vh]\",\n};\n\nconst DEFAULT_CONFIRM: Required<ConfirmCloseOptions> = {\n title: \"Discard changes?\",\n message: \"You have unsaved changes. Closing now will discard them.\",\n confirmLabel: \"Discard\",\n cancelLabel: \"Keep editing\",\n};\n\nexport function Modal({\n open,\n onClose,\n confirmClose = false,\n title,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const [expanded, setExpanded] = useState(false);\n // When confirmClose is active, a close request opens this prompt instead of\n // closing outright; onClose only fires once the user confirms the discard.\n const [confirming, setConfirming] = useState(false);\n\n // requestClose is the single entry point for every close affordance. With the\n // guard on, it surfaces the confirmation; otherwise it closes immediately.\n const requestClose = () => {\n if (confirmClose) setConfirming(true);\n else onClose();\n };\n\n useEffect(() => {\n if (!open) {\n setExpanded(false);\n setConfirming(false);\n }\n }, [open]);\n\n // A close guard that disappears mid-prompt (changes saved/reverted) should\n // not leave a stale confirmation dialog up.\n useEffect(() => {\n if (!confirmClose) setConfirming(false);\n }, [confirmClose]);\n\n useEffect(() => {\n if (!open || !closeOnEsc) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n // While the prompt is up, Escape dismisses the prompt rather than the modal.\n if (confirming) setConfirming(false);\n else requestClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, closeOnEsc, confirming, confirmClose, onClose]);\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/40\"\n onClick={closeOnBackdrop ? requestClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative bg-background border border-border rounded-lg shadow-xl w-full flex flex-col\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || headerSlot || expandable || !hideClose) && (\n <div className=\"flex items-center gap-density-2 px-density-4 py-density-3 border-b border-border\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={requestClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n {confirming && (\n <ConfirmClosePrompt\n options={confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose }}\n onConfirm={() => {\n setConfirming(false);\n onClose();\n }}\n onCancel={() => setConfirming(false)}\n />\n )}\n </div>\n </div>\n );\n}\n\nfunction ConfirmClosePrompt({\n options,\n onConfirm,\n onCancel,\n}: {\n options: Required<ConfirmCloseOptions>;\n onConfirm: () => void;\n onCancel: () => void;\n}) {\n return (\n <div\n className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-label={typeof options.title === \"string\" ? options.title : \"Confirm close\"}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl\">\n <h3 className=\"text-sm font-semibold\">{options.title}</h3>\n <p className=\"mt-density-2 text-sm text-muted-foreground\">{options.message}</p>\n <div className=\"mt-density-4 flex justify-end gap-density-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n {options.cancelLabel}\n </Button>\n <Button variant=\"destructive\" size=\"sm\" onClick={onConfirm}>\n {options.confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"names":["useRef","useState","useEffect","_a","jsx","jsxs","cn","Icon","UiFullscreenFilled","UiFullscreen","UiClose","Button"],"mappings":"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"}
@@ -1,10 +1,28 @@
1
1
  import { ReactNode } from 'react';
2
2
  export type ModalSize = "sm" | "md" | "lg" | "xl" | "full";
3
+ /** Copy for the discard-confirmation prompt shown before a guarded close. */
4
+ export type ConfirmCloseOptions = {
5
+ /** Heading of the confirmation prompt. */
6
+ title?: ReactNode;
7
+ /** Body explaining what is lost on close. */
8
+ message?: ReactNode;
9
+ /** Label of the button that proceeds with closing. */
10
+ confirmLabel?: string;
11
+ /** Label of the button that dismisses the prompt and keeps the modal open. */
12
+ cancelLabel?: string;
13
+ };
3
14
  export type ModalProps = {
4
15
  /** Controls whether the modal is mounted. */
5
16
  open: boolean;
6
17
  /** Called when the modal requests to close. */
7
18
  onClose: () => void;
19
+ /**
20
+ * Guard every close path (close button, Escape, backdrop) behind a discard
21
+ * confirmation. `true` uses the default copy; pass options to customise it.
22
+ * `onClose` only fires once the user confirms. Set this while the modal holds
23
+ * unsaved edits and clear it (or pass `false`) once there is nothing to lose.
24
+ */
25
+ confirmClose?: boolean | ConfirmCloseOptions;
8
26
  /** Header title and accessible dialog label when it is a string. */
9
27
  title?: ReactNode;
10
28
  /** Width/height preset for the dialog. */
@@ -26,5 +44,5 @@ export type ModalProps = {
26
44
  /** Modal body content. */
27
45
  children: ReactNode;
28
46
  };
29
- export declare function Modal({ open, onClose, title, size, closeOnBackdrop, closeOnEsc, hideClose, expandable, className, headerSlot, footer, children, }: ModalProps): import("react/jsx-runtime").JSX.Element | null;
47
+ export declare function Modal({ open, onClose, confirmClose, title, size, closeOnBackdrop, closeOnEsc, hideClose, expandable, className, headerSlot, footer, children, }: ModalProps): import("react/jsx-runtime").JSX.Element | null;
30
48
  //# sourceMappingURL=Modal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAKpE,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;AAE3D,MAAM,MAAM,UAAU,GAAG;IACvB,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,+CAA+C;IAC/C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,uDAAuD;IACvD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAUF,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,OAAO,EACP,KAAK,EACL,IAAW,EACX,eAAsB,EACtB,UAAiB,EACjB,SAAiB,EACjB,UAAiB,EACjB,SAAS,EACT,UAAU,EACV,MAAM,EACN,QAAQ,GACT,EAAE,UAAU,kDAgFZ"}
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAMpE,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;AAE3D,6EAA6E;AAC7E,MAAM,MAAM,mBAAmB,GAAG;IAChC,0CAA0C;IAC1C,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,6CAA6C;IAC7C,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,6CAA6C;IAC7C,IAAI,EAAE,OAAO,CAAC;IACd,+CAA+C;IAC/C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;IAC7C,oEAAoE;IACpE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qCAAqC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB,uDAAuD;IACvD,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,0BAA0B;IAC1B,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAiBF,wBAAgB,KAAK,CAAC,EACpB,IAAI,EACJ,OAAO,EACP,YAAoB,EACpB,KAAK,EACL,IAAW,EACX,eAAsB,EACtB,UAAiB,EACjB,SAAiB,EACjB,UAAiB,EACjB,SAAS,EACT,UAAU,EACV,MAAM,EACN,QAAQ,GACT,EAAE,UAAU,kDAgHZ"}
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState, useEffect } from "react";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { Icon } from "../data/Icon.js";
5
+ import { Button } from "../components/button.js";
5
6
  import { UiFullscreenFilled, UiFullscreen } from "../icons/components/UiFullscreen.js";
6
7
  import { UiClose } from "../icons/components/UiClose.js";
7
8
  const sizeClass = {
@@ -11,9 +12,16 @@ const sizeClass = {
11
12
  xl: "max-w-4xl max-h-[90vh]",
12
13
  full: "max-w-[95vw] max-h-[95vh]"
13
14
  };
15
+ const DEFAULT_CONFIRM = {
16
+ title: "Discard changes?",
17
+ message: "You have unsaved changes. Closing now will discard them.",
18
+ confirmLabel: "Discard",
19
+ cancelLabel: "Keep editing"
20
+ };
14
21
  function Modal({
15
22
  open,
16
23
  onClose,
24
+ confirmClose = false,
17
25
  title,
18
26
  size = "md",
19
27
  closeOnBackdrop = true,
@@ -27,17 +35,30 @@ function Modal({
27
35
  }) {
28
36
  const dialogRef = useRef(null);
29
37
  const [expanded, setExpanded] = useState(false);
38
+ const [confirming, setConfirming] = useState(false);
39
+ const requestClose = () => {
40
+ if (confirmClose) setConfirming(true);
41
+ else onClose();
42
+ };
30
43
  useEffect(() => {
31
- if (!open) setExpanded(false);
44
+ if (!open) {
45
+ setExpanded(false);
46
+ setConfirming(false);
47
+ }
32
48
  }, [open]);
49
+ useEffect(() => {
50
+ if (!confirmClose) setConfirming(false);
51
+ }, [confirmClose]);
33
52
  useEffect(() => {
34
53
  if (!open || !closeOnEsc) return;
35
54
  const onKey = (e) => {
36
- if (e.key === "Escape") onClose();
55
+ if (e.key !== "Escape") return;
56
+ if (confirming) setConfirming(false);
57
+ else requestClose();
37
58
  };
38
59
  document.addEventListener("keydown", onKey);
39
60
  return () => document.removeEventListener("keydown", onKey);
40
- }, [open, closeOnEsc, onClose]);
61
+ }, [open, closeOnEsc, confirming, confirmClose, onClose]);
41
62
  useEffect(() => {
42
63
  var _a;
43
64
  if (!open) return;
@@ -53,7 +74,7 @@ function Modal({
53
74
  "div",
54
75
  {
55
76
  className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40",
56
- onClick: closeOnBackdrop ? onClose : void 0,
77
+ onClick: closeOnBackdrop ? requestClose : void 0,
57
78
  role: "presentation",
58
79
  children: /* @__PURE__ */ jsxs(
59
80
  "div",
@@ -87,7 +108,7 @@ function Modal({
87
108
  "button",
88
109
  {
89
110
  type: "button",
90
- onClick: onClose,
111
+ onClick: requestClose,
91
112
  "aria-label": "Close",
92
113
  className: "text-muted-foreground hover:text-foreground",
93
114
  children: /* @__PURE__ */ jsx(Icon, { icon: UiClose })
@@ -95,13 +116,48 @@ function Modal({
95
116
  )
96
117
  ] }),
97
118
  /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto px-density-4 py-density-3", children }),
98
- footer && /* @__PURE__ */ jsx("div", { className: "px-density-4 py-density-3 border-t border-border", children: footer })
119
+ footer && /* @__PURE__ */ jsx("div", { className: "px-density-4 py-density-3 border-t border-border", children: footer }),
120
+ confirming && /* @__PURE__ */ jsx(
121
+ ConfirmClosePrompt,
122
+ {
123
+ options: confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose },
124
+ onConfirm: () => {
125
+ setConfirming(false);
126
+ onClose();
127
+ },
128
+ onCancel: () => setConfirming(false)
129
+ }
130
+ )
99
131
  ]
100
132
  }
101
133
  )
102
134
  }
103
135
  );
104
136
  }
137
+ function ConfirmClosePrompt({
138
+ options,
139
+ onConfirm,
140
+ onCancel
141
+ }) {
142
+ return /* @__PURE__ */ jsx(
143
+ "div",
144
+ {
145
+ className: "absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4",
146
+ role: "alertdialog",
147
+ "aria-modal": "true",
148
+ "aria-label": typeof options.title === "string" ? options.title : "Confirm close",
149
+ onClick: (e) => e.stopPropagation(),
150
+ children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl", children: [
151
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: options.title }),
152
+ /* @__PURE__ */ jsx("p", { className: "mt-density-2 text-sm text-muted-foreground", children: options.message }),
153
+ /* @__PURE__ */ jsxs("div", { className: "mt-density-4 flex justify-end gap-density-2", children: [
154
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: onCancel, children: options.cancelLabel }),
155
+ /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", onClick: onConfirm, children: options.confirmLabel })
156
+ ] })
157
+ ] })
158
+ }
159
+ );
160
+ }
105
161
  export {
106
162
  Modal
107
163
  };
@@ -1 +1 @@
1
- {"version":3,"file":"Modal.js","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm max-h-[90vh]\",\n md: \"max-w-md max-h-[90vh]\",\n lg: \"max-w-2xl max-h-[90vh]\",\n xl: \"max-w-4xl max-h-[90vh]\",\n full: \"max-w-[95vw] max-h-[95vh]\",\n};\n\nexport function Modal({\n open,\n onClose,\n title,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const [expanded, setExpanded] = useState(false);\n\n useEffect(() => {\n if (!open) setExpanded(false);\n }, [open]);\n\n useEffect(() => {\n if (!open || !closeOnEsc) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, closeOnEsc, onClose]);\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/40\"\n onClick={closeOnBackdrop ? onClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative bg-background border border-border rounded-lg shadow-xl w-full flex flex-col\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || headerSlot || expandable || !hideClose) && (\n <div className=\"flex items-center gap-density-2 px-density-4 py-density-3 border-b border-border\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={onClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n </div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":";;;;;;AAkCA,MAAM,YAAuC;AAAA,EAC3C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,MAAM;AACR;AAEO,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,aAAa;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAe;AACb,QAAM,YAAY,OAAuB,IAAI;AAC7C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAE9C,YAAU,MAAM;AACd,QAAI,CAAC,KAAM,aAAY,KAAK;AAAA,EAC9B,GAAG,CAAC,IAAI,CAAC;AAET,YAAU,MAAM;AACd,QAAI,CAAC,QAAQ,CAAC,WAAY;AAC1B,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU,SAAA;AAAA,IAC1B;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM,SAAS,oBAAoB,WAAW,KAAK;AAAA,EAC5D,GAAG,CAAC,MAAM,YAAY,OAAO,CAAC;AAE9B,YAAU,MAAM;;AACd,QAAI,CAAC,KAAM;AACX,UAAM,OAAO,SAAS;AACtB,oBAAU,YAAV,mBAAmB;AACnB,WAAO,MAAA;;AAAM,cAAAA,MAAA,6BAAM,UAAN,gBAAAA,IAAA;AAAA;AAAA,EACf,GAAG,CAAC,IAAI,CAAC;AAET,MAAI,CAAC,KAAM,QAAO;AAElB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAU;AAAA,MACV,SAAS,kBAAkB,UAAU;AAAA,MACrC,MAAK;AAAA,MAEL,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,KAAK;AAAA,UACL,UAAU;AAAA,UACV,MAAK;AAAA,UACL,cAAW;AAAA,UACX,cAAY,OAAO,UAAU,WAAW,QAAQ;AAAA,UAChD,WAAW;AAAA,YACT;AAAA,YACA,WAAW,UAAU,OAAO,UAAU,IAAI;AAAA,YAC1C;AAAA,UAAA;AAAA,UAEF,SAAS,CAAC,MAAM,EAAE,gBAAA;AAAA,UAEhB,UAAA;AAAA,aAAA,SAAS,cAAc,cAAc,CAAC,cACtC,qBAAC,OAAA,EAAI,WAAU,oFACZ,UAAA;AAAA,cAAA,QACC,oBAAC,QAAG,WAAU,gCAAgC,iBAAM,IAEpD,oBAAC,QAAA,EAAK,WAAU,SAAA,CAAS;AAAA,cAE1B;AAAA,cACA,cACC;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;AAAA,kBACpC,cAAY,WAAW,iBAAiB;AAAA,kBACxC,WAAU;AAAA,kBAEV,UAAA,oBAAC,MAAA,EAAK,MAAM,WAAW,qBAAqB,aAAA,CAAc;AAAA,gBAAA;AAAA,cAAA;AAAA,cAG7D,CAAC,aACA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAS;AAAA,kBACT,cAAW;AAAA,kBACX,WAAU;AAAA,kBAEV,UAAA,oBAAC,MAAA,EAAK,MAAM,QAAA,CAAS;AAAA,gBAAA;AAAA,cAAA;AAAA,YACvB,GAEJ;AAAA,YAEF,oBAAC,OAAA,EAAI,WAAU,kDAAkD,SAAA,CAAS;AAAA,YACzE,UAAU,oBAAC,OAAA,EAAI,WAAU,oDAAoD,UAAA,OAAA,CAAO;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IACvF;AAAA,EAAA;AAGN;"}
1
+ {"version":3,"file":"Modal.js","sources":["../../src/overlay/Modal.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { Button } from \"../components/button\";\nimport { UiClose, UiFullscreen, UiFullscreenFilled } from \"../icons\";\n\nexport type ModalSize = \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n\n/** Copy for the discard-confirmation prompt shown before a guarded close. */\nexport type ConfirmCloseOptions = {\n /** Heading of the confirmation prompt. */\n title?: ReactNode;\n /** Body explaining what is lost on close. */\n message?: ReactNode;\n /** Label of the button that proceeds with closing. */\n confirmLabel?: string;\n /** Label of the button that dismisses the prompt and keeps the modal open. */\n cancelLabel?: string;\n};\n\nexport type ModalProps = {\n /** Controls whether the modal is mounted. */\n open: boolean;\n /** Called when the modal requests to close. */\n onClose: () => void;\n /**\n * Guard every close path (close button, Escape, backdrop) behind a discard\n * confirmation. `true` uses the default copy; pass options to customise it.\n * `onClose` only fires once the user confirms. Set this while the modal holds\n * unsaved edits and clear it (or pass `false`) once there is nothing to lose.\n */\n confirmClose?: boolean | ConfirmCloseOptions;\n /** Header title and accessible dialog label when it is a string. */\n title?: ReactNode;\n /** Width/height preset for the dialog. */\n size?: ModalSize;\n /** Close when the backdrop is clicked. */\n closeOnBackdrop?: boolean;\n /** Close when Escape is pressed. */\n closeOnEsc?: boolean;\n /** Hide the close button. */\n hideClose?: boolean;\n /** Show an expand/restore button. */\n expandable?: boolean;\n /** Classes applied to the dialog panel. */\n className?: string;\n /** Extra header content rendered before the expand/close buttons. */\n headerSlot?: ReactNode;\n /** Footer content pinned below the scrollable body. */\n footer?: ReactNode;\n /** Modal body content. */\n children: ReactNode;\n};\n\nconst sizeClass: Record<ModalSize, string> = {\n sm: \"max-w-sm max-h-[90vh]\",\n md: \"max-w-md max-h-[90vh]\",\n lg: \"max-w-2xl max-h-[90vh]\",\n xl: \"max-w-4xl max-h-[90vh]\",\n full: \"max-w-[95vw] max-h-[95vh]\",\n};\n\nconst DEFAULT_CONFIRM: Required<ConfirmCloseOptions> = {\n title: \"Discard changes?\",\n message: \"You have unsaved changes. Closing now will discard them.\",\n confirmLabel: \"Discard\",\n cancelLabel: \"Keep editing\",\n};\n\nexport function Modal({\n open,\n onClose,\n confirmClose = false,\n title,\n size = \"md\",\n closeOnBackdrop = true,\n closeOnEsc = true,\n hideClose = false,\n expandable = true,\n className,\n headerSlot,\n footer,\n children,\n}: ModalProps) {\n const dialogRef = useRef<HTMLDivElement>(null);\n const [expanded, setExpanded] = useState(false);\n // When confirmClose is active, a close request opens this prompt instead of\n // closing outright; onClose only fires once the user confirms the discard.\n const [confirming, setConfirming] = useState(false);\n\n // requestClose is the single entry point for every close affordance. With the\n // guard on, it surfaces the confirmation; otherwise it closes immediately.\n const requestClose = () => {\n if (confirmClose) setConfirming(true);\n else onClose();\n };\n\n useEffect(() => {\n if (!open) {\n setExpanded(false);\n setConfirming(false);\n }\n }, [open]);\n\n // A close guard that disappears mid-prompt (changes saved/reverted) should\n // not leave a stale confirmation dialog up.\n useEffect(() => {\n if (!confirmClose) setConfirming(false);\n }, [confirmClose]);\n\n useEffect(() => {\n if (!open || !closeOnEsc) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n // While the prompt is up, Escape dismisses the prompt rather than the modal.\n if (confirming) setConfirming(false);\n else requestClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, closeOnEsc, confirming, confirmClose, onClose]);\n\n useEffect(() => {\n if (!open) return;\n const prev = document.activeElement as HTMLElement | null;\n dialogRef.current?.focus();\n return () => prev?.focus?.();\n }, [open]);\n\n if (!open) return null;\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/40\"\n onClick={closeOnBackdrop ? requestClose : undefined}\n role=\"presentation\"\n >\n <div\n ref={dialogRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={typeof title === \"string\" ? title : undefined}\n className={cn(\n \"relative bg-background border border-border rounded-lg shadow-xl w-full flex flex-col\",\n expanded ? sizeClass.full : sizeClass[size],\n className,\n )}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || headerSlot || expandable || !hideClose) && (\n <div className=\"flex items-center gap-density-2 px-density-4 py-density-3 border-b border-border\">\n {title ? (\n <h2 className=\"text-sm font-semibold flex-1\">{title}</h2>\n ) : (\n <span className=\"flex-1\" />\n )}\n {headerSlot}\n {expandable && (\n <button\n type=\"button\"\n onClick={() => setExpanded((v) => !v)}\n aria-label={expanded ? \"Restore size\" : \"Expand to fullscreen\"}\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={expanded ? UiFullscreenFilled : UiFullscreen} />\n </button>\n )}\n {!hideClose && (\n <button\n type=\"button\"\n onClick={requestClose}\n aria-label=\"Close\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} />\n </button>\n )}\n </div>\n )}\n <div className=\"flex-1 overflow-auto px-density-4 py-density-3\">{children}</div>\n {footer && <div className=\"px-density-4 py-density-3 border-t border-border\">{footer}</div>}\n {confirming && (\n <ConfirmClosePrompt\n options={confirmClose === true ? DEFAULT_CONFIRM : { ...DEFAULT_CONFIRM, ...confirmClose }}\n onConfirm={() => {\n setConfirming(false);\n onClose();\n }}\n onCancel={() => setConfirming(false)}\n />\n )}\n </div>\n </div>\n );\n}\n\nfunction ConfirmClosePrompt({\n options,\n onConfirm,\n onCancel,\n}: {\n options: Required<ConfirmCloseOptions>;\n onConfirm: () => void;\n onCancel: () => void;\n}) {\n return (\n <div\n className=\"absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 p-density-4\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-label={typeof options.title === \"string\" ? options.title : \"Confirm close\"}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"w-full max-w-sm rounded-lg border border-border bg-background p-density-4 shadow-xl\">\n <h3 className=\"text-sm font-semibold\">{options.title}</h3>\n <p className=\"mt-density-2 text-sm text-muted-foreground\">{options.message}</p>\n <div className=\"mt-density-4 flex justify-end gap-density-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n {options.cancelLabel}\n </Button>\n <Button variant=\"destructive\" size=\"sm\" onClick={onConfirm}>\n {options.confirmLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
@@ -78,7 +78,7 @@ function ParameterField({
78
78
  {
79
79
  id: fieldId,
80
80
  value,
81
- placeholder: param.description || param.name,
81
+ placeholder: types.parameterPlaceholder(param) || param.name,
82
82
  onChange
83
83
  }
84
84
  ) });
@@ -159,7 +159,7 @@ function ParameterField({
159
159
  value,
160
160
  className: inputClassName,
161
161
  onChange: (event) => onChange(event.target.value),
162
- placeholder: (schema == null ? void 0 : schema.default) != null ? String(schema.default) : param.description || param.name
162
+ placeholder: (schema == null ? void 0 : schema.default) != null ? String(schema.default) : types.parameterPlaceholder(param) || param.name
163
163
  }
164
164
  ) });
165
165
  }