@motir/design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +126 -0
  2. package/dist/appearance.d.ts +47 -0
  3. package/dist/appearance.js +3 -0
  4. package/dist/appearance.js.map +1 -0
  5. package/dist/components/theme/AppearancePickers.d.ts +97 -0
  6. package/dist/components/theme/AppearancePickers.js +542 -0
  7. package/dist/components/theme/AppearancePickers.js.map +1 -0
  8. package/dist/components/theme/HandDrawnFilter.d.ts +23 -0
  9. package/dist/components/theme/HandDrawnFilter.js +41 -0
  10. package/dist/components/theme/HandDrawnFilter.js.map +1 -0
  11. package/dist/components/theme/ImmersiveTilt.d.ts +3 -0
  12. package/dist/components/theme/ImmersiveTilt.js +113 -0
  13. package/dist/components/theme/ImmersiveTilt.js.map +1 -0
  14. package/dist/components/theme/StyleVignette.d.ts +60 -0
  15. package/dist/components/theme/StyleVignette.js +382 -0
  16. package/dist/components/theme/StyleVignette.js.map +1 -0
  17. package/dist/components/ui/Button.d.ts +32 -0
  18. package/dist/components/ui/Button.js +92 -0
  19. package/dist/components/ui/Button.js.map +1 -0
  20. package/dist/components/ui/Card.d.ts +33 -0
  21. package/dist/components/ui/Card.js +64 -0
  22. package/dist/components/ui/Card.js.map +1 -0
  23. package/dist/components/ui/ColorSwatchPicker.d.ts +19 -0
  24. package/dist/components/ui/ColorSwatchPicker.js +71 -0
  25. package/dist/components/ui/ColorSwatchPicker.js.map +1 -0
  26. package/dist/components/ui/Combobox.d.ts +84 -0
  27. package/dist/components/ui/Combobox.js +374 -0
  28. package/dist/components/ui/Combobox.js.map +1 -0
  29. package/dist/components/ui/EmptyState.d.ts +38 -0
  30. package/dist/components/ui/EmptyState.js +73 -0
  31. package/dist/components/ui/EmptyState.js.map +1 -0
  32. package/dist/components/ui/ErrorState.d.ts +43 -0
  33. package/dist/components/ui/ErrorState.js +169 -0
  34. package/dist/components/ui/ErrorState.js.map +1 -0
  35. package/dist/components/ui/FormField.d.ts +31 -0
  36. package/dist/components/ui/FormField.js +43 -0
  37. package/dist/components/ui/FormField.js.map +1 -0
  38. package/dist/components/ui/Input.d.ts +29 -0
  39. package/dist/components/ui/Input.js +105 -0
  40. package/dist/components/ui/Input.js.map +1 -0
  41. package/dist/components/ui/Modal.d.ts +84 -0
  42. package/dist/components/ui/Modal.js +153 -0
  43. package/dist/components/ui/Modal.js.map +1 -0
  44. package/dist/components/ui/MultiSelectPicker.d.ts +73 -0
  45. package/dist/components/ui/MultiSelectPicker.js +358 -0
  46. package/dist/components/ui/MultiSelectPicker.js.map +1 -0
  47. package/dist/components/ui/Pill.d.ts +49 -0
  48. package/dist/components/ui/Pill.js +107 -0
  49. package/dist/components/ui/Pill.js.map +1 -0
  50. package/dist/components/ui/Popover.d.ts +65 -0
  51. package/dist/components/ui/Popover.js +57 -0
  52. package/dist/components/ui/Popover.js.map +1 -0
  53. package/dist/components/ui/SectionLabel.d.ts +28 -0
  54. package/dist/components/ui/SectionLabel.js +27 -0
  55. package/dist/components/ui/SectionLabel.js.map +1 -0
  56. package/dist/components/ui/Segmented.d.ts +30 -0
  57. package/dist/components/ui/Segmented.js +65 -0
  58. package/dist/components/ui/Segmented.js.map +1 -0
  59. package/dist/components/ui/Spinner.d.ts +22 -0
  60. package/dist/components/ui/Spinner.js +39 -0
  61. package/dist/components/ui/Spinner.js.map +1 -0
  62. package/dist/components/ui/Switch.d.ts +34 -0
  63. package/dist/components/ui/Switch.js +50 -0
  64. package/dist/components/ui/Switch.js.map +1 -0
  65. package/dist/components/ui/Textarea.d.ts +19 -0
  66. package/dist/components/ui/Textarea.js +72 -0
  67. package/dist/components/ui/Textarea.js.map +1 -0
  68. package/dist/components/ui/Toast.d.ts +45 -0
  69. package/dist/components/ui/Toast.js +116 -0
  70. package/dist/components/ui/Toast.js.map +1 -0
  71. package/dist/components/ui/Tooltip.d.ts +25 -0
  72. package/dist/components/ui/Tooltip.js +43 -0
  73. package/dist/components/ui/Tooltip.js.map +1 -0
  74. package/dist/contexts/theme-context.d.ts +95 -0
  75. package/dist/contexts/theme-context.js +561 -0
  76. package/dist/contexts/theme-context.js.map +1 -0
  77. package/dist/index.d.ts +41 -0
  78. package/dist/index.js +67 -0
  79. package/dist/specimen/TokensSpecimen.d.ts +13 -0
  80. package/dist/specimen/TokensSpecimen.js +1090 -0
  81. package/dist/specimen/TokensSpecimen.js.map +1 -0
  82. package/dist/theme/appearance-resolution.d.ts +59 -0
  83. package/dist/theme/appearance-resolution.js +379 -0
  84. package/dist/theme/appearance-resolution.js.map +1 -0
  85. package/dist/theme/init-script.d.ts +48 -0
  86. package/dist/theme/init-script.js +391 -0
  87. package/dist/theme/init-script.js.map +1 -0
  88. package/dist/theme/palettes.d.ts +146 -0
  89. package/dist/theme/palettes.js +85 -0
  90. package/dist/theme/palettes.js.map +1 -0
  91. package/dist/theme/styles.d.ts +351 -0
  92. package/dist/theme/styles.js +264 -0
  93. package/dist/theme/styles.js.map +1 -0
  94. package/dist/theme/tilt.d.ts +41 -0
  95. package/dist/theme/tilt.js +18 -0
  96. package/dist/theme/tilt.js.map +1 -0
  97. package/dist/theme/types.d.ts +61 -0
  98. package/dist/theme/types.js +234 -0
  99. package/dist/theme/types.js.map +1 -0
  100. package/dist/theme/typography.d.ts +144 -0
  101. package/dist/theme/typography.js +57 -0
  102. package/dist/theme/typography.js.map +1 -0
  103. package/dist/utils/cn.d.ts +16 -0
  104. package/dist/utils/cn.js +11 -0
  105. package/dist/utils/cn.js.map +1 -0
  106. package/package.json +60 -0
  107. package/theme.css +3924 -0
@@ -0,0 +1,374 @@
1
+ 'use client';
2
+ import { useState, useId, useRef, useMemo, useCallback, useEffect, Fragment as Fragment$1, useSyncExternalStore, useLayoutEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { Check, ChevronsUpDown } from 'lucide-react';
5
+ import { clsx } from 'clsx';
6
+ import { twMerge } from 'tailwind-merge';
7
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
8
+
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
13
+ var subscribeNoop = () => () => {
14
+ };
15
+ function useMounted() {
16
+ return useSyncExternalStore(
17
+ subscribeNoop,
18
+ () => true,
19
+ () => false
20
+ );
21
+ }
22
+ function nearestClipBox(el) {
23
+ let node = el.parentElement;
24
+ while (node && node !== document.body) {
25
+ const oy = getComputedStyle(node).overflowY;
26
+ if (oy === "auto" || oy === "scroll") return { box: node, scrolls: true };
27
+ if (oy === "hidden" || oy === "clip") return { box: node, scrolls: false };
28
+ node = node.parentElement;
29
+ }
30
+ return null;
31
+ }
32
+ function Combobox({
33
+ options,
34
+ value,
35
+ onChange,
36
+ label,
37
+ placeholder = "Select\u2026",
38
+ searchable = false,
39
+ searchPlaceholder = "Search\u2026",
40
+ emptyText = "No matches",
41
+ loading = false,
42
+ loadingText = "Loading\u2026",
43
+ disabled = false,
44
+ id,
45
+ className,
46
+ autoOpen = false,
47
+ onClose,
48
+ query: controlledQuery,
49
+ onQueryChange
50
+ }) {
51
+ const [open, setOpen] = useState(autoOpen);
52
+ const serverFiltered = onQueryChange !== void 0;
53
+ const [internalQuery, setInternalQuery] = useState("");
54
+ const query = serverFiltered ? controlledQuery ?? "" : internalQuery;
55
+ const setQuery = (q) => serverFiltered ? onQueryChange(q) : setInternalQuery(q);
56
+ const [activeIndex, setActiveIndex] = useState(0);
57
+ const mounted = useMounted();
58
+ const [menuStyle, setMenuStyle] = useState(null);
59
+ const [listMaxHeight, setListMaxHeight] = useState(256);
60
+ const [inlineAbove, setInlineAbove] = useState(false);
61
+ const baseId = useId();
62
+ const listId = `${baseId}-listbox`;
63
+ const optionId = (i) => `${baseId}-opt-${i}`;
64
+ const containerRef = useRef(null);
65
+ const triggerRef = useRef(null);
66
+ const inputRef = useRef(null);
67
+ const listRef = useRef(null);
68
+ const menuRef = useRef(null);
69
+ const filtered = useMemo(() => {
70
+ if (serverFiltered || !searchable || query.trim() === "") return options;
71
+ const needle = query.trim().toLowerCase();
72
+ return options.filter(
73
+ (o) => o.label.toLowerCase().includes(needle) || (o.secondary ? o.secondary.toLowerCase().includes(needle) : false) || (o.keywords ? o.keywords.toLowerCase().includes(needle) : false)
74
+ );
75
+ }, [options, query, searchable, serverFiltered]);
76
+ const selected = value != null ? options.find((o) => o.value === value) ?? null : null;
77
+ const active = filtered.length > 0 ? Math.min(activeIndex, filtered.length - 1) : 0;
78
+ const updatePosition = useCallback(() => {
79
+ const trigger = triggerRef.current;
80
+ if (!trigger) return;
81
+ const rect = trigger.getBoundingClientRect();
82
+ const gap = 4;
83
+ const viewportH = window.innerHeight;
84
+ const spaceBelow = viewportH - rect.bottom - gap;
85
+ const spaceAbove = rect.top - gap;
86
+ const placeBelow = spaceBelow >= spaceAbove;
87
+ const avail = Math.max(120, placeBelow ? spaceBelow : spaceAbove);
88
+ setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));
89
+ const style = {
90
+ position: "fixed",
91
+ left: Math.round(rect.left),
92
+ minWidth: Math.round(rect.width)
93
+ };
94
+ if (placeBelow) style.top = Math.round(rect.bottom + gap);
95
+ else style.bottom = Math.round(viewportH - rect.top + gap);
96
+ setMenuStyle(style);
97
+ }, [searchable]);
98
+ const updateInlinePosition = useCallback(() => {
99
+ const trigger = triggerRef.current;
100
+ if (!trigger) return;
101
+ const clip = nearestClipBox(trigger);
102
+ if (!clip || clip.scrolls) {
103
+ setInlineAbove(false);
104
+ setListMaxHeight(256);
105
+ return;
106
+ }
107
+ const rect = trigger.getBoundingClientRect();
108
+ const clipRect = clip.box.getBoundingClientRect();
109
+ const gap = 4;
110
+ const inset = 8;
111
+ const spaceBelow = clipRect.bottom - rect.bottom - gap - inset;
112
+ const spaceAbove = rect.top - clipRect.top - gap - inset;
113
+ const placeAbove = spaceAbove > spaceBelow;
114
+ setInlineAbove(placeAbove);
115
+ const avail = Math.max(80, placeAbove ? spaceAbove : spaceBelow);
116
+ setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));
117
+ }, [searchable]);
118
+ useEffect(() => {
119
+ if (!(open && mounted)) return;
120
+ const t = setTimeout(() => {
121
+ if (searchable) inputRef.current?.focus();
122
+ else listRef.current?.focus();
123
+ }, 0);
124
+ return () => clearTimeout(t);
125
+ }, [open, mounted, searchable]);
126
+ useIsomorphicLayoutEffect(() => {
127
+ if (!(open && mounted)) return;
128
+ if (triggerRef.current?.closest('[data-surface="modal"],[aria-modal="true"]')) return;
129
+ updatePosition();
130
+ const onReflow = () => updatePosition();
131
+ window.addEventListener("scroll", onReflow, true);
132
+ window.addEventListener("resize", onReflow);
133
+ return () => {
134
+ window.removeEventListener("scroll", onReflow, true);
135
+ window.removeEventListener("resize", onReflow);
136
+ };
137
+ }, [open, mounted, updatePosition, filtered.length]);
138
+ useIsomorphicLayoutEffect(() => {
139
+ if (!(open && mounted)) return;
140
+ if (!triggerRef.current?.closest('[data-surface="modal"],[aria-modal="true"]')) return;
141
+ updateInlinePosition();
142
+ const onReflow = () => updateInlinePosition();
143
+ window.addEventListener("scroll", onReflow, true);
144
+ window.addEventListener("resize", onReflow);
145
+ return () => {
146
+ window.removeEventListener("scroll", onReflow, true);
147
+ window.removeEventListener("resize", onReflow);
148
+ };
149
+ }, [open, mounted, updateInlinePosition, filtered.length]);
150
+ useEffect(() => {
151
+ if (!open) return;
152
+ function onDocMouseDown(e) {
153
+ const target = e.target;
154
+ if (containerRef.current?.contains(target) || menuRef.current?.contains(target)) return;
155
+ closeMenu();
156
+ }
157
+ document.addEventListener("mousedown", onDocMouseDown);
158
+ return () => document.removeEventListener("mousedown", onDocMouseDown);
159
+ }, [open]);
160
+ useEffect(() => {
161
+ if (open) document.getElementById(optionId(active))?.scrollIntoView({ block: "nearest" });
162
+ }, [active, open]);
163
+ function openMenu() {
164
+ if (!serverFiltered) setInternalQuery("");
165
+ const sel = value != null ? options.findIndex((o) => o.value === value) : -1;
166
+ setActiveIndex(sel >= 0 ? sel : 0);
167
+ setOpen(true);
168
+ }
169
+ function closeMenu() {
170
+ setOpen(false);
171
+ onClose?.();
172
+ }
173
+ function closeAndRefocus() {
174
+ closeMenu();
175
+ triggerRef.current?.focus();
176
+ }
177
+ function commit(i) {
178
+ const opt = filtered[i];
179
+ if (!opt) return;
180
+ onChange(opt.value);
181
+ closeAndRefocus();
182
+ }
183
+ function onListKeyDown(e) {
184
+ if (e.key === "ArrowDown") {
185
+ e.preventDefault();
186
+ setActiveIndex(Math.min(active + 1, filtered.length - 1));
187
+ } else if (e.key === "ArrowUp") {
188
+ e.preventDefault();
189
+ setActiveIndex(Math.max(active - 1, 0));
190
+ } else if (e.key === "Home") {
191
+ e.preventDefault();
192
+ setActiveIndex(0);
193
+ } else if (e.key === "End") {
194
+ e.preventDefault();
195
+ setActiveIndex(filtered.length - 1);
196
+ } else if (e.key === "Enter") {
197
+ e.preventDefault();
198
+ commit(active);
199
+ } else if (e.key === "Escape") {
200
+ e.preventDefault();
201
+ closeAndRefocus();
202
+ }
203
+ }
204
+ function onTriggerKeyDown(e) {
205
+ if (!open && (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")) {
206
+ e.preventDefault();
207
+ openMenu();
208
+ }
209
+ }
210
+ const activeId = filtered.length > 0 ? optionId(active) : void 0;
211
+ const hasOptions = !loading && filtered.length > 0;
212
+ const inDialog = mounted ? !!triggerRef.current?.closest('[data-surface="modal"],[aria-modal="true"]') : false;
213
+ const menuInner = /* @__PURE__ */ jsxs(Fragment, { children: [
214
+ searchable ? /* @__PURE__ */ jsx(
215
+ "input",
216
+ {
217
+ ref: inputRef,
218
+ type: "text",
219
+ role: "combobox",
220
+ "aria-expanded": true,
221
+ "aria-controls": listId,
222
+ "aria-activedescendant": activeId,
223
+ "aria-label": searchPlaceholder,
224
+ value: query,
225
+ onChange: (e) => {
226
+ setQuery(e.target.value);
227
+ setActiveIndex(0);
228
+ },
229
+ onKeyDown: onListKeyDown,
230
+ placeholder: searchPlaceholder,
231
+ className: "border-(--el-border) bg-(--el-page-bg) mb-1 w-full rounded-(--radius-input) border px-(--spacing-control-x) py-(--spacing-control-y) text-sm focus-visible:outline-none"
232
+ }
233
+ ) : null,
234
+ /* @__PURE__ */ jsx(
235
+ "div",
236
+ {
237
+ ref: listRef,
238
+ id: listId,
239
+ role: hasOptions ? "listbox" : "status",
240
+ "aria-label": label,
241
+ tabIndex: hasOptions && !searchable ? 0 : -1,
242
+ "aria-activedescendant": hasOptions && !searchable ? activeId : void 0,
243
+ onKeyDown: hasOptions && !searchable ? onListKeyDown : void 0,
244
+ style: { maxHeight: listMaxHeight },
245
+ className: "max-h-64 overflow-y-auto focus:outline-none",
246
+ children: loading ? /* @__PURE__ */ jsx("p", { className: "text-(--el-text-muted) px-2.5 py-2 text-sm", children: loadingText }) : filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-(--el-text-muted) px-2.5 py-2 text-sm", children: emptyText }) : filtered.map((opt, i) => {
247
+ const isSelected = opt.value === value;
248
+ const isActive = i === active;
249
+ const header = opt.group !== void 0 && opt.group !== filtered[i - 1]?.group ? /* @__PURE__ */ jsx(
250
+ "div",
251
+ {
252
+ role: "presentation",
253
+ className: "px-(--spacing-control-x) pt-2 pb-1 font-mono text-[11px] font-semibold tracking-wider text-(--el-text-eyebrow) uppercase",
254
+ children: opt.group
255
+ },
256
+ `group-${opt.group}`
257
+ ) : null;
258
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
259
+ header,
260
+ /* @__PURE__ */ jsxs(
261
+ "div",
262
+ {
263
+ id: optionId(i),
264
+ role: "option",
265
+ "aria-selected": isSelected,
266
+ onMouseEnter: () => setActiveIndex(i),
267
+ onClick: () => commit(i),
268
+ className: cn(
269
+ "flex cursor-pointer items-center gap-2 rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) text-sm",
270
+ isActive ? "bg-(--el-option-active-bg) text-(--el-text)" : "text-(--el-text)"
271
+ ),
272
+ children: [
273
+ opt.icon ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: opt.icon }) : null,
274
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: opt.label }),
275
+ opt.secondary ? (
276
+ // --el-text-identifier (= --color-slate, the -secondary
277
+ // weight), NOT -muted: muted (#787671) fails AA (4.16:1) on
278
+ // the panel surface (#f6f5f4) at this 12px size (the
279
+ // sidebar-caption contrast lesson) — identifier/slate clears it.
280
+ /* @__PURE__ */ jsx("span", { className: "text-(--el-text-identifier) ml-auto truncate text-xs", children: opt.secondary })
281
+ ) : null,
282
+ isSelected ? (
283
+ // The selected-row check uses the accent (matching
284
+ // MultiSelectPicker) — unifies the check-icon colour across
285
+ // the two pickers (was bare ink here, accent there).
286
+ /* @__PURE__ */ jsx(
287
+ Check,
288
+ {
289
+ className: "ml-1 h-4 w-4 shrink-0 text-(--el-accent-on-surface)",
290
+ "aria-hidden": true
291
+ }
292
+ )
293
+ ) : null
294
+ ]
295
+ }
296
+ )
297
+ ] }, opt.value);
298
+ })
299
+ }
300
+ )
301
+ ] });
302
+ const menu = inDialog ? /* @__PURE__ */ jsx(
303
+ "div",
304
+ {
305
+ ref: menuRef,
306
+ "data-menu-surface": "",
307
+ className: cn(
308
+ "absolute left-0 z-50 w-max min-w-full max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1",
309
+ "shadow-(--shadow-elevated) border border-(--el-border)",
310
+ // Flip above the trigger when the dialog has more room there, so a tall
311
+ // list near the modal's bottom edge isn't clipped (clamped + scrolled).
312
+ inlineAbove ? "bottom-full mb-1" : "top-full mt-1"
313
+ ),
314
+ children: menuInner
315
+ }
316
+ ) : /* @__PURE__ */ jsx(
317
+ "div",
318
+ {
319
+ ref: menuRef,
320
+ "data-menu-surface": "",
321
+ style: menuStyle ?? { position: "fixed", visibility: "hidden" },
322
+ className: cn(
323
+ "z-50 w-max max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1",
324
+ "shadow-(--shadow-elevated) border border-(--el-border)"
325
+ ),
326
+ children: menuInner
327
+ }
328
+ );
329
+ return (
330
+ // `data-inner-dismiss` while the menu is open — see MultiSelectPicker:
331
+ // an enclosing Radix layer's onEscapeKeyDown checks it so Esc closes the
332
+ // MENU (our own handler) instead of dismissing the whole layer.
333
+ /* @__PURE__ */ jsxs("div", { ref: containerRef, "data-inner-dismiss": open ? true : void 0, className: "relative", children: [
334
+ /* @__PURE__ */ jsxs(
335
+ "button",
336
+ {
337
+ ref: triggerRef,
338
+ type: "button",
339
+ id,
340
+ role: "combobox",
341
+ "aria-haspopup": "listbox",
342
+ "aria-expanded": open,
343
+ "aria-controls": open ? listId : void 0,
344
+ "aria-label": label,
345
+ disabled,
346
+ onClick: () => open ? closeMenu() : openMenu(),
347
+ onKeyDown: onTriggerKeyDown,
348
+ className: cn(
349
+ "border-(--el-border) bg-(--el-page-bg) flex h-(--height-control) w-full items-center gap-2 rounded-(--radius-input) border px-(--spacing-control-x) text-sm",
350
+ "focus-visible:ring-(--focus-ring-color) focus-visible:outline-none focus-visible:ring-2",
351
+ "disabled:opacity-50",
352
+ className
353
+ ),
354
+ children: [
355
+ selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
356
+ selected.icon ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: selected.icon }) : null,
357
+ /* @__PURE__ */ jsx("span", { className: "text-(--el-text) truncate", children: selected.label }),
358
+ selected.secondary ? (
359
+ // identifier (= slate, the -secondary weight): AA on the trigger surface at 12px (as above).
360
+ /* @__PURE__ */ jsx("span", { className: "text-(--el-text-identifier) ml-auto truncate text-xs", children: selected.secondary })
361
+ ) : null
362
+ ] }) : /* @__PURE__ */ jsx("span", { className: "text-(--el-text-muted) truncate", children: placeholder }),
363
+ /* @__PURE__ */ jsx(ChevronsUpDown, { className: "text-(--el-icon-muted) ml-auto h-4 w-4 shrink-0", "aria-hidden": true })
364
+ ]
365
+ }
366
+ ),
367
+ open ? inDialog ? menu : mounted ? createPortal(menu, document.body) : null : null
368
+ ] })
369
+ );
370
+ }
371
+
372
+ export { Combobox };
373
+ //# sourceMappingURL=Combobox.js.map
374
+ //# sourceMappingURL=Combobox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Combobox.tsx"],"names":["Fragment"],"mappings":"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","file":"Combobox.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport {\n Fragment,\n useCallback,\n useEffect,\n useId,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { Check, ChevronsUpDown } from 'lucide-react';\nimport { cn } from '../../utils/cn';\n\n// Run layout effects on the client, fall back to useEffect during SSR (the menu\n// only mounts client-side anyway — see the `mounted` gate below).\nconst useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;\n\n// `false` during SSR / first hydration render, `true` on the client thereafter —\n// gates the createPortal call so document.body is guaranteed present. Hydration-\n// safe (matches the server's `false`) and avoids setState-in-effect.\nconst subscribeNoop = () => () => {};\nfunction useMounted() {\n return useSyncExternalStore(\n subscribeNoop,\n () => true,\n () => false,\n );\n}\n\n/**\n * Combobox — an accessible select/combobox primitive (Subtask 2.3.4). A trigger\n * button (`role=\"combobox\"`, `aria-haspopup=\"listbox\"`) opens an anchored panel\n * holding an optional type-ahead filter and a `role=\"listbox\"` of\n * `role=\"option\"` rows. The active option is tracked with `aria-activedescendant`\n * on whichever control holds focus (the filter input when `searchable`, else the\n * listbox) — the CommandPalette pattern that already clears the STRICT axe sweep.\n *\n * Deliberately NOT built on the Radix Popover primitive: that injects\n * `aria-haspopup=\"dialog\"` + dialog focus semantics onto its trigger, which\n * conflicts with the listbox combobox pattern. This is a self-contained anchored\n * dropdown (click-outside + Escape + focus return handled here) so the ARIA is\n * exactly the WAI-ARIA combobox shape.\n *\n * Composed by `components/issues/TypePicker` (searchable=false, 5 options) and\n * `ParentPicker` (searchable, async candidate list).\n */\nexport interface ComboboxOption<T extends string> {\n value: T;\n /** Primary text — also the accessible name of the option. */\n label: string;\n /** Extra text matched by the filter (e.g. a PROD-N identifier). */\n keywords?: string;\n /** Leading visual (a kind icon); decorative — label carries the name. */\n icon?: ReactNode;\n /** Trailing muted text (e.g. the identifier). */\n secondary?: string;\n /**\n * Optional section label. When consecutive options carry different `group`\n * values, a non-interactive header row is rendered at each transition (the\n * advanced filter's \"Fields\" / \"Custom fields\" / \"Other\" field menu). Pass\n * options pre-sorted by group; headers follow list order, so a group whose\n * every option is filtered out disappears with it. Decorative — not part of\n * the listbox's option indices, so keyboard nav is unaffected.\n */\n group?: string;\n}\n\nexport interface ComboboxProps<T extends string> {\n options: ComboboxOption<T>[];\n value: T | null;\n onChange: (value: T) => void;\n /** Accessible name for the trigger + listbox. */\n label: string;\n /** Trigger text when nothing is selected. */\n placeholder?: string;\n searchable?: boolean;\n searchPlaceholder?: string;\n emptyText?: string;\n loading?: boolean;\n loadingText?: string;\n disabled?: boolean;\n /** id for the trigger button. */\n id?: string;\n className?: string;\n /**\n * Mount already-open (Subtask 2.5.5) — for an inline-edit cell that renders the\n * picker on a single click, so the menu is open immediately (mirrors\n * `DatePicker`'s `autoOpen`). Focus lands on the search input / listbox.\n */\n autoOpen?: boolean;\n /**\n * Fired whenever the menu transitions open → closed (a pick, Escape,\n * click-outside, or toggling the trigger). The inline-edit cell uses it to\n * leave edit mode and return to the static pill/avatar (Subtask 2.5.5).\n */\n onClose?: () => void;\n /**\n * Server-driven search mode (Subtask 6.9.2 — the link picker). When\n * `onQueryChange` is provided the type-ahead query is CONTROLLED by the parent\n * (`query`) and client-side filtering is SKIPPED — `options` is taken as the\n * server's authoritative result for the current query (the parent debounces\n * its fetch off `onQueryChange`, the same contract as `MultiSelectPicker`).\n * Omit both for the default self-filtering behaviour. The parent owns the\n * query lifecycle, so opening the menu does NOT reset it.\n */\n query?: string;\n onQueryChange?: (query: string) => void;\n}\n\n// Walk up from a node to the nearest ancestor that clips overflow, classifying\n// it as scrolling (overflow auto/scroll — the menu can be scrolled into view)\n// or a hard `hidden`/`clip` box (no scroll — the menu is trapped). Used by the\n// inline (in-dialog) branch to decide whether to clamp + flip the menu (only a\n// hard non-scrolling clip needs it). Stops at <body>; returns null if nothing\n// up the chain clips.\nfunction nearestClipBox(el: HTMLElement): { box: HTMLElement; scrolls: boolean } | null {\n let node = el.parentElement;\n while (node && node !== document.body) {\n const oy = getComputedStyle(node).overflowY;\n if (oy === 'auto' || oy === 'scroll') return { box: node, scrolls: true };\n if (oy === 'hidden' || oy === 'clip') return { box: node, scrolls: false };\n node = node.parentElement;\n }\n return null;\n}\n\nexport function Combobox<T extends string>({\n options,\n value,\n onChange,\n label,\n placeholder = 'Select…',\n searchable = false,\n searchPlaceholder = 'Search…',\n emptyText = 'No matches',\n loading = false,\n loadingText = 'Loading…',\n disabled = false,\n id,\n className,\n autoOpen = false,\n onClose,\n query: controlledQuery,\n onQueryChange,\n}: ComboboxProps<T>) {\n const [open, setOpen] = useState(autoOpen);\n // Server-driven mode (6.9.2): when `onQueryChange` is provided the query is\n // controlled by the parent and client filtering is bypassed (the server's\n // `options` are authoritative). Otherwise the query is internal state.\n const serverFiltered = onQueryChange !== undefined;\n const [internalQuery, setInternalQuery] = useState('');\n const query = serverFiltered ? (controlledQuery ?? '') : internalQuery;\n const setQuery = (q: string) => (serverFiltered ? onQueryChange(q) : setInternalQuery(q));\n const [activeIndex, setActiveIndex] = useState(0);\n // The menu is portaled to <body> by default, so it escapes any clipping\n // ancestor — a short table's overflow:hidden\n // (bug-inline-edit-clipped-when-table-short) OR the Advanced-filter popover's\n // overflow-hidden + overflow-y-auto body (MOTIR-1346). We render INLINE only\n // inside a focus-trapping MODAL (the shared Modal primitive — `data-surface=\n // \"modal\"`; `aria-modal` covers any other), where a portaled menu would land\n // outside the dialog's focus scope (focus-trap war → a click on it hangs /\n // dismisses) AND the modal centers with a CSS transform that breaks a fixed-\n // positioned child's coords. A NON-modal Radix Popover (the filter builder,\n // promote-sprint, mark-duplicate — `data-surface=\"popover\"`) carries\n // `role=\"dialog\"` for a11y but does NOT trap focus, so it portals safely —\n // keyed on the modal surface, NOT `role`, so those popovers escape the clip\n // too. Only render the portal once mounted, since createPortal needs document.body.\n const mounted = useMounted();\n // Viewport-anchored position for the portaled menu + the listbox's available\n // height, recomputed from the trigger rect on open / scroll / resize.\n const [menuStyle, setMenuStyle] = useState<CSSProperties | null>(null);\n const [listMaxHeight, setListMaxHeight] = useState(256);\n // Inline (in-dialog) branch only: whether the menu is flipped ABOVE the\n // trigger because there's more room there than below within the dialog's\n // overflow-hidden clip box (bug-combobox-menu-clipped-inside-modal).\n const [inlineAbove, setInlineAbove] = useState(false);\n const baseId = useId();\n const listId = `${baseId}-listbox`;\n const optionId = (i: number) => `${baseId}-opt-${i}`;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const menuRef = useRef<HTMLDivElement>(null);\n\n const filtered = useMemo(() => {\n // Server-driven mode: `options` is already the query's result — never filter\n // it again client-side (a substring re-filter would hide trigram / token\n // matches the server legitimately returned).\n if (serverFiltered || !searchable || query.trim() === '') return options;\n const needle = query.trim().toLowerCase();\n return options.filter(\n (o) =>\n o.label.toLowerCase().includes(needle) ||\n (o.secondary ? o.secondary.toLowerCase().includes(needle) : false) ||\n (o.keywords ? o.keywords.toLowerCase().includes(needle) : false),\n );\n }, [options, query, searchable, serverFiltered]);\n\n const selected = value != null ? (options.find((o) => o.value === value) ?? null) : null;\n\n // The active row, clamped to the (possibly just-filtered) list — DERIVED, not\n // an effect, so a shrinking filter never needs a setState-in-effect.\n const active = filtered.length > 0 ? Math.min(activeIndex, filtered.length - 1) : 0;\n\n // Anchor the portaled menu to the trigger in VIEWPORT coordinates (position:\n // fixed), flipping above the trigger when there's more room there, and capping\n // the listbox height to the available space so it never runs off-screen.\n const updatePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n const rect = trigger.getBoundingClientRect();\n const gap = 4; // matches the old mt-1\n const viewportH = window.innerHeight;\n const spaceBelow = viewportH - rect.bottom - gap;\n const spaceAbove = rect.top - gap;\n const placeBelow = spaceBelow >= spaceAbove;\n const avail = Math.max(120, placeBelow ? spaceBelow : spaceAbove);\n // Reserve room for the optional search input + container padding.\n setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));\n const style: CSSProperties = {\n position: 'fixed',\n left: Math.round(rect.left),\n minWidth: Math.round(rect.width),\n };\n if (placeBelow) style.top = Math.round(rect.bottom + gap);\n else style.bottom = Math.round(viewportH - rect.top + gap);\n setMenuStyle(style);\n }, [searchable]);\n\n // The in-dialog branch renders INLINE (an absolute child of the trigger),\n // so — unlike the body-portaled branch — it cannot escape the modal's\n // `overflow-hidden` panel: an absolute child contributes nothing to the\n // dialog body's scroll height, so a menu taller than the room below the\n // trigger is simply CLIPPED, not scrolled-to (bug-combobox-menu-clipped-\n // inside-modal). Mirror the portaled branch's logic against the DIALOG's\n // clip box instead of the viewport: measure the space above/below the\n // trigger inside the dialog, flip to whichever side is taller, and clamp the\n // listbox height to it so the menu scrolls INTERNALLY and always fits.\n const updateInlinePosition = useCallback(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n // Find the nearest ancestor that CLIPS the inline menu. If that ancestor\n // SCROLLS (overflow auto/scroll — e.g. the Advanced-filter popover's\n // `overflow-y-auto` body, or a `Modal.Body`), the menu can be scrolled into\n // view, so leave it exactly as before (top-full, max-h-64) — flipping/\n // clamping against a scroll box would wrongly shove the menu up under the\n // panel's header (the regression the first cut caused). Only a NON-scrolling\n // `overflow: hidden` clip — the centered Modal panel's `overflow-hidden\n // max-h-[90vh]` box — actually traps the menu (bug-combobox-menu-clipped-\n // inside-modal); clamp + flip against THAT box.\n const clip = nearestClipBox(trigger);\n if (!clip || clip.scrolls) {\n setInlineAbove(false);\n setListMaxHeight(256); // == the max-h-64 fallback (original behaviour)\n return;\n }\n const rect = trigger.getBoundingClientRect();\n const clipRect = clip.box.getBoundingClientRect();\n const gap = 4; // matches mt-1 / mb-1\n const inset = 8; // stay a hair inside the panel's rounded clip edge\n const spaceBelow = clipRect.bottom - rect.bottom - gap - inset;\n const spaceAbove = rect.top - clipRect.top - gap - inset;\n const placeAbove = spaceAbove > spaceBelow;\n setInlineAbove(placeAbove);\n const avail = Math.max(80, placeAbove ? spaceAbove : spaceBelow);\n // Reserve room for the optional search input + container padding (same\n // budget as the portaled branch).\n setListMaxHeight(Math.max(80, Math.min(256, avail - (searchable ? 52 : 12))));\n }, [searchable]);\n\n // On open: focus the right control (the only side effect — query/active reset\n // happens in openMenu so this effect never calls setState).\n useEffect(() => {\n if (!(open && mounted)) return;\n const t = setTimeout(() => {\n if (searchable) inputRef.current?.focus();\n else listRef.current?.focus();\n }, 0);\n return () => clearTimeout(t);\n }, [open, mounted, searchable]);\n\n // Position the menu before paint, and keep it glued to the trigger while open\n // (ancestor scroll uses capture so a scrolling table re-anchors the menu).\n // Skip entirely for the inline (in-dialog) branch — it positions via CSS.\n useIsomorphicLayoutEffect(() => {\n if (!(open && mounted)) return;\n if (triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]')) return;\n updatePosition();\n const onReflow = () => updatePosition();\n window.addEventListener('scroll', onReflow, true);\n window.addEventListener('resize', onReflow);\n return () => {\n window.removeEventListener('scroll', onReflow, true);\n window.removeEventListener('resize', onReflow);\n };\n }, [open, mounted, updatePosition, filtered.length]);\n\n // Inline (in-dialog) counterpart: measure the menu against the dialog's clip\n // box before paint and keep it clamped while open (re-measure on resize, and\n // on capture-phase scroll so a scrolling dialog body re-anchors it).\n useIsomorphicLayoutEffect(() => {\n if (!(open && mounted)) return;\n if (!triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]')) return;\n updateInlinePosition();\n const onReflow = () => updateInlinePosition();\n window.addEventListener('scroll', onReflow, true);\n window.addEventListener('resize', onReflow);\n return () => {\n window.removeEventListener('scroll', onReflow, true);\n window.removeEventListener('resize', onReflow);\n };\n }, [open, mounted, updateInlinePosition, filtered.length]);\n\n // Click-outside closes, restoring focus to the trigger. The menu is portaled\n // out of containerRef, so a click on it must also count as \"inside\".\n useEffect(() => {\n if (!open) return;\n function onDocMouseDown(e: MouseEvent) {\n const target = e.target as Node;\n if (containerRef.current?.contains(target) || menuRef.current?.contains(target)) return;\n closeMenu();\n }\n document.addEventListener('mousedown', onDocMouseDown);\n return () => document.removeEventListener('mousedown', onDocMouseDown);\n }, [open]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Keep the active row in view.\n useEffect(() => {\n if (open) document.getElementById(optionId(active))?.scrollIntoView({ block: 'nearest' });\n }, [active, open]); // eslint-disable-line react-hooks/exhaustive-deps\n\n function openMenu() {\n // Uncontrolled mode resets its own type-ahead on open; server-driven mode\n // leaves the query to the parent (it owns the lifecycle — resets on form\n // open / relationship change), so reopening keeps the current results.\n if (!serverFiltered) setInternalQuery('');\n const sel = value != null ? options.findIndex((o) => o.value === value) : -1;\n setActiveIndex(sel >= 0 ? sel : 0);\n setOpen(true);\n }\n\n // Close the menu, notifying the consumer (Subtask 2.5.5's inline-edit cell\n // leaves edit mode on close). Every close path funnels through here.\n function closeMenu() {\n setOpen(false);\n onClose?.();\n }\n\n function closeAndRefocus() {\n closeMenu();\n triggerRef.current?.focus();\n }\n\n function commit(i: number) {\n const opt = filtered[i];\n if (!opt) return;\n onChange(opt.value);\n closeAndRefocus();\n }\n\n function onListKeyDown(e: React.KeyboardEvent) {\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setActiveIndex(Math.min(active + 1, filtered.length - 1));\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setActiveIndex(Math.max(active - 1, 0));\n } else if (e.key === 'Home') {\n e.preventDefault();\n setActiveIndex(0);\n } else if (e.key === 'End') {\n e.preventDefault();\n setActiveIndex(filtered.length - 1);\n } else if (e.key === 'Enter') {\n e.preventDefault();\n commit(active);\n } else if (e.key === 'Escape') {\n e.preventDefault();\n closeAndRefocus();\n }\n }\n\n function onTriggerKeyDown(e: React.KeyboardEvent) {\n if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n openMenu();\n }\n }\n\n const activeId = filtered.length > 0 ? optionId(active) : undefined;\n // A `role=\"listbox\"` MUST own `role=\"option\"` children, so the listbox role\n // only applies when options are actually rendered. While loading or with no\n // matches the panel holds a status message instead — present it as a\n // `role=\"status\"` live region (announced to AT, and not an empty/childless\n // listbox, which is a critical `aria-required-children` axe violation). The\n // empty state is the link picker's DEFAULT on-open state since the 6.9.2\n // query-driven retrofit, so this is now reachable on every open.\n const hasOptions = !loading && filtered.length > 0;\n\n // Portal out to escape a short table's overflow:hidden — UNLESS we're inside a\n // focus-trapping dialog, where an inline menu is required (see the `mounted`\n // comment above). Reading the ref in render is safe: by the time `open` flips\n // true via a click the trigger is mounted; the only ref-null case is autoOpen,\n // used solely by inline-edit cells (always in a table, never a dialog) → portal.\n const inDialog = mounted\n ? !!triggerRef.current?.closest('[data-surface=\"modal\"],[aria-modal=\"true\"]')\n : false;\n\n const menuInner = (\n <>\n {searchable ? (\n <input\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={activeId}\n aria-label={searchPlaceholder}\n value={query}\n onChange={(e) => {\n setQuery(e.target.value);\n setActiveIndex(0);\n }}\n onKeyDown={onListKeyDown}\n placeholder={searchPlaceholder}\n className=\"border-(--el-border) bg-(--el-page-bg) mb-1 w-full rounded-(--radius-input) border px-(--spacing-control-x) py-(--spacing-control-y) text-sm focus-visible:outline-none\"\n />\n ) : null}\n <div\n ref={listRef}\n id={listId}\n role={hasOptions ? 'listbox' : 'status'}\n aria-label={label}\n tabIndex={hasOptions && !searchable ? 0 : -1}\n aria-activedescendant={hasOptions && !searchable ? activeId : undefined}\n onKeyDown={hasOptions && !searchable ? onListKeyDown : undefined}\n // Both branches cap height to their measured available space (inline\n // style wins over the max-h-64 fallback): the portaled menu against the\n // viewport, the inline (in-dialog) menu against the dialog's clip box —\n // so an over-tall list scrolls internally instead of being clipped.\n style={{ maxHeight: listMaxHeight }}\n className=\"max-h-64 overflow-y-auto focus:outline-none\"\n >\n {loading ? (\n <p className=\"text-(--el-text-muted) px-2.5 py-2 text-sm\">{loadingText}</p>\n ) : filtered.length === 0 ? (\n <p className=\"text-(--el-text-muted) px-2.5 py-2 text-sm\">{emptyText}</p>\n ) : (\n filtered.map((opt, i) => {\n const isSelected = opt.value === value;\n const isActive = i === active;\n // A non-interactive group header at each group transition (in list\n // order, so a fully-filtered group's header drops with it). Not an\n // option row — keyboard nav over `filtered` is unaffected.\n const header =\n opt.group !== undefined && opt.group !== filtered[i - 1]?.group ? (\n <div\n key={`group-${opt.group}`}\n role=\"presentation\"\n className=\"px-(--spacing-control-x) pt-2 pb-1 font-mono text-[11px] font-semibold tracking-wider text-(--el-text-eyebrow) uppercase\"\n >\n {opt.group}\n </div>\n ) : null;\n return (\n <Fragment key={opt.value}>\n {header}\n <div\n id={optionId(i)}\n role=\"option\"\n aria-selected={isSelected}\n onMouseEnter={() => setActiveIndex(i)}\n onClick={() => commit(i)}\n className={cn(\n 'flex cursor-pointer items-center gap-2 rounded-(--radius-control) px-(--spacing-control-x) py-(--spacing-control-y) text-sm',\n isActive ? 'bg-(--el-option-active-bg) text-(--el-text)' : 'text-(--el-text)',\n )}\n >\n {opt.icon ? <span aria-hidden>{opt.icon}</span> : null}\n <span className=\"truncate\">{opt.label}</span>\n {opt.secondary ? (\n // --el-text-identifier (= --color-slate, the -secondary\n // weight), NOT -muted: muted (#787671) fails AA (4.16:1) on\n // the panel surface (#f6f5f4) at this 12px size (the\n // sidebar-caption contrast lesson) — identifier/slate clears it.\n <span className=\"text-(--el-text-identifier) ml-auto truncate text-xs\">\n {opt.secondary}\n </span>\n ) : null}\n {isSelected ? (\n // The selected-row check uses the accent (matching\n // MultiSelectPicker) — unifies the check-icon colour across\n // the two pickers (was bare ink here, accent there).\n <Check\n className=\"ml-1 h-4 w-4 shrink-0 text-(--el-accent-on-surface)\"\n aria-hidden\n />\n ) : null}\n </div>\n </Fragment>\n );\n })\n )}\n </div>\n </>\n );\n\n // The shared menu panel. In a dialog: an inline absolute panel (the original,\n // proven layout). Otherwise: a body-portaled panel with viewport-anchored\n // fixed positioning that escapes every overflow ancestor.\n const menu = inDialog ? (\n <div\n ref={menuRef}\n data-menu-surface=\"\"\n className={cn(\n 'absolute left-0 z-50 w-max min-w-full max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1',\n 'shadow-(--shadow-elevated) border border-(--el-border)',\n // Flip above the trigger when the dialog has more room there, so a tall\n // list near the modal's bottom edge isn't clipped (clamped + scrolled).\n inlineAbove ? 'bottom-full mb-1' : 'top-full mt-1',\n )}\n >\n {menuInner}\n </div>\n ) : (\n <div\n ref={menuRef}\n data-menu-surface=\"\"\n // Width sizes to the widest option but never narrower than the trigger\n // (minWidth set in updatePosition) and is capped so a long label can't run\n // off-screen. Hidden until positioned to avoid a first-paint flash at 0,0.\n style={menuStyle ?? { position: 'fixed', visibility: 'hidden' }}\n className={cn(\n 'z-50 w-max max-w-[18rem] rounded-(--radius-card) bg-(--el-page-bg) p-1',\n 'shadow-(--shadow-elevated) border border-(--el-border)',\n )}\n >\n {menuInner}\n </div>\n );\n\n return (\n // `data-inner-dismiss` while the menu is open — see MultiSelectPicker:\n // an enclosing Radix layer's onEscapeKeyDown checks it so Esc closes the\n // MENU (our own handler) instead of dismissing the whole layer.\n <div ref={containerRef} data-inner-dismiss={open ? true : undefined} className=\"relative\">\n <button\n ref={triggerRef}\n type=\"button\"\n id={id}\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n aria-controls={open ? listId : undefined}\n aria-label={label}\n disabled={disabled}\n onClick={() => (open ? closeMenu() : openMenu())}\n onKeyDown={onTriggerKeyDown}\n className={cn(\n 'border-(--el-border) bg-(--el-page-bg) flex h-(--height-control) w-full items-center gap-2 rounded-(--radius-input) border px-(--spacing-control-x) text-sm',\n 'focus-visible:ring-(--focus-ring-color) focus-visible:outline-none focus-visible:ring-2',\n 'disabled:opacity-50',\n className,\n )}\n >\n {selected ? (\n <>\n {selected.icon ? <span aria-hidden>{selected.icon}</span> : null}\n <span className=\"text-(--el-text) truncate\">{selected.label}</span>\n {selected.secondary ? (\n // identifier (= slate, the -secondary weight): AA on the trigger surface at 12px (as above).\n <span className=\"text-(--el-text-identifier) ml-auto truncate text-xs\">\n {selected.secondary}\n </span>\n ) : null}\n </>\n ) : (\n <span className=\"text-(--el-text-muted) truncate\">{placeholder}</span>\n )}\n <ChevronsUpDown className=\"text-(--el-icon-muted) ml-auto h-4 w-4 shrink-0\" aria-hidden />\n </button>\n\n {open ? (inDialog ? menu : mounted ? createPortal(menu, document.body) : null) : null}\n </div>\n );\n}\n"]}
@@ -0,0 +1,38 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ /**
5
+ * EmptyState — composed pattern for "there's nothing to show yet" screens.
6
+ *
7
+ * Composes [`Card`](./Card.tsx) + a lucide icon + an optional action button.
8
+ * Use this whenever a list, table, board, or detail panel has no data —
9
+ * the common mistake is leaving the screen blank, which reads as broken.
10
+ * Always give the user (a) a sense of what would appear here, and (b) a
11
+ * clear next step.
12
+ *
13
+ * Icons come from `lucide-react`. The default `<Inbox />` is appropriate for
14
+ * generic "no items" cases; pass `icon` to specialize (FolderOpen,
15
+ * MessageSquareOff, Users, etc.).
16
+ *
17
+ * @example
18
+ * <EmptyState
19
+ * title="No projects yet"
20
+ * description="Create your first project to get started."
21
+ * action={<Button leftIcon={<Plus />}>New project</Button>}
22
+ * />
23
+ */
24
+ interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {
25
+ /** Headline — what's missing. Required. */
26
+ title: string;
27
+ /** Optional supporting copy explaining what would appear here. Accepts a
28
+ * node so the copy can carry an inline link (e.g. the API-tokens empty
29
+ * state's "Read the MCP setup guide", 7.8.3); a plain string still works. */
30
+ description?: ReactNode;
31
+ /** Lucide-style icon; defaults to `<Inbox />`. */
32
+ icon?: ReactNode;
33
+ /** Typically a `<Button>` representing the next action. */
34
+ action?: ReactNode;
35
+ }
36
+ declare const EmptyState: react.ForwardRefExoticComponent<EmptyStateProps & react.RefAttributes<HTMLDivElement>>;
37
+
38
+ export { EmptyState, type EmptyStateProps };
@@ -0,0 +1,73 @@
1
+ import { forwardRef } from 'react';
2
+ import { Inbox } from 'lucide-react';
3
+ import { cva } from 'class-variance-authority';
4
+ import { clsx } from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { jsxs, jsx } from 'react/jsx-runtime';
7
+
8
+ // src/components/ui/EmptyState.tsx
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+ var cardVariants = cva(
13
+ cn(
14
+ "rounded-(--radius-card) border border-(--el-border)",
15
+ "p-(--spacing-card-padding)",
16
+ "transition-shadow duration-(--transition-duration)"
17
+ ),
18
+ {
19
+ variants: {
20
+ tint: {
21
+ none: "bg-(--el-card)",
22
+ peach: "bg-(--el-tint-peach) border-transparent",
23
+ rose: "bg-(--el-tint-rose) border-transparent",
24
+ mint: "bg-(--el-tint-mint) border-transparent",
25
+ lavender: "bg-(--el-tint-lavender) border-transparent",
26
+ sky: "bg-(--el-tint-sky) border-transparent",
27
+ yellow: "bg-(--el-tint-yellow) border-transparent"
28
+ },
29
+ clickable: {
30
+ true: "cursor-pointer hover:shadow-(--shadow-card) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-background",
31
+ false: ""
32
+ }
33
+ },
34
+ defaultVariants: { tint: "none", clickable: false }
35
+ }
36
+ );
37
+ var Card = forwardRef(function Card2({ tint, clickable, header, footer, className, children, ...rest }, ref) {
38
+ return /* @__PURE__ */ jsxs(
39
+ "div",
40
+ {
41
+ ref,
42
+ className: cn(cardVariants({ tint, clickable }), className),
43
+ "data-tilt": "",
44
+ ...tint && tint !== "none" ? {} : { "data-surface": "card" },
45
+ ...clickable ? { tabIndex: 0, role: "button" } : {},
46
+ ...rest,
47
+ children: [
48
+ header ? /* @__PURE__ */ jsx("div", { "data-tilt-layer": "front", className: "mb-(--spacing-md)", children: header }) : null,
49
+ /* @__PURE__ */ jsx("div", { children }),
50
+ footer ? /* @__PURE__ */ jsx(
51
+ "div",
52
+ {
53
+ "data-tilt-layer": "back",
54
+ className: "border-(--el-border) mt-(--spacing-md) border-t pt-(--spacing-md)",
55
+ children: footer
56
+ }
57
+ ) : null
58
+ ]
59
+ }
60
+ );
61
+ });
62
+ var EmptyState = forwardRef(function EmptyState2({ title, description, icon, action, className, ...rest }, ref) {
63
+ return /* @__PURE__ */ jsxs(Card, { ref, className: cn("flex flex-col items-center text-center", className), ...rest, children: [
64
+ /* @__PURE__ */ jsx("div", { className: "text-(--el-icon-muted) mb-(--spacing-md) inline-flex h-12 w-12 items-center justify-center", children: icon ?? /* @__PURE__ */ jsx(Inbox, { className: "h-12 w-12", "aria-hidden": true }) }),
65
+ /* @__PURE__ */ jsx("h2", { className: "font-serif text-xl text-(--el-text)", children: title }),
66
+ description ? /* @__PURE__ */ jsx("p", { className: "text-(--el-text-subtitle) mt-(--spacing-sm) max-w-prose font-sans text-sm", children: description }) : null,
67
+ action ? /* @__PURE__ */ jsx("div", { className: "mt-(--spacing-md)", children: action }) : null
68
+ ] });
69
+ });
70
+
71
+ export { EmptyState };
72
+ //# sourceMappingURL=EmptyState.js.map
73
+ //# sourceMappingURL=EmptyState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Card.tsx","../../../src/components/ui/EmptyState.tsx"],"names":["Card","forwardRef","EmptyState","jsxs","jsx"],"mappings":"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","file":"EmptyState.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Card — content container with optional header/footer slots and tint variants.\n *\n * Compose with `header` / `footer` props for the common pattern, or just\n * pass children for a bare card. `clickable` makes the whole card a button\n * (use `onClick` + Enter/Space; pass `href` to render as `<a>` instead).\n *\n * Tint variants use the pastel feature tints from the palette\n * (peach/rose/mint/lavender/sky/yellow) — apply sparingly, never to\n * page-level surfaces.\n *\n * @example\n * <Card header={<h3>Title</h3>}>Body content</Card>\n * <Card tint=\"lavender\" clickable onClick={open}>Pastel card</Card>\n * <Card tint=\"mint\"><a href=\"/x\">Link wrapper</a></Card>\n */\nconst cardVariants = cva(\n cn(\n 'rounded-(--radius-card) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'transition-shadow duration-(--transition-duration)',\n ),\n {\n variants: {\n tint: {\n none: 'bg-(--el-card)',\n peach: 'bg-(--el-tint-peach) border-transparent',\n rose: 'bg-(--el-tint-rose) border-transparent',\n mint: 'bg-(--el-tint-mint) border-transparent',\n lavender: 'bg-(--el-tint-lavender) border-transparent',\n sky: 'bg-(--el-tint-sky) border-transparent',\n yellow: 'bg-(--el-tint-yellow) border-transparent',\n },\n clickable: {\n true: 'cursor-pointer hover:shadow-(--shadow-card) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n false: '',\n },\n },\n defaultVariants: { tint: 'none', clickable: false },\n },\n);\n\nexport interface CardProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, VariantProps<typeof cardVariants> {\n header?: ReactNode;\n footer?: ReactNode;\n children?: ReactNode;\n}\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n { tint, clickable, header, footer, className, children, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cn(cardVariants({ tint, clickable }), className)}\n // `data-tilt` is the hook the 3D / Immersive style's pointer-parallax\n // engine (ImmersiveTilt, 7.3.39) uses to tip this tile toward the cursor.\n // Inert for every other style + under reduced motion; harmless otherwise.\n data-tilt=\"\"\n // `data-surface` is the hook a surface-MATERIAL style (e.g. glassmorphism)\n // uses to frost this panel — see globals.css's material layer. Only the\n // default (untinted) card opts in; a pastel-tint feature card keeps its\n // opaque tint, never a frosted overlay that would wash the hue out.\n {...(tint && tint !== 'none' ? {} : { 'data-surface': 'card' })}\n {...(clickable ? { tabIndex: 0, role: 'button' } : {})}\n {...rest}\n >\n {/* `data-tilt-layer` lifts the slot onto a translateZ plane under the 3D /\n Immersive style (7.3.39) so it PARALLAXES above the card face as the\n card tilts — `front` (the header/title pops most), `back` (the footer\n sits nearer the face). Inert under every other style + reduced motion.\n The body stays on the face so arbitrary content never renders on a\n skewed Z-plane. */}\n {header ? (\n <div data-tilt-layer=\"front\" className=\"mb-(--spacing-md)\">\n {header}\n </div>\n ) : null}\n <div>{children}</div>\n {footer ? (\n <div\n data-tilt-layer=\"back\"\n className=\"border-(--el-border) mt-(--spacing-md) border-t pt-(--spacing-md)\"\n >\n {footer}\n </div>\n ) : null}\n </div>\n );\n});\n","import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';\nimport { Inbox } from 'lucide-react';\nimport { Card } from './Card';\nimport { cn } from '../../utils/cn';\n\n/**\n * EmptyState — composed pattern for \"there's nothing to show yet\" screens.\n *\n * Composes [`Card`](./Card.tsx) + a lucide icon + an optional action button.\n * Use this whenever a list, table, board, or detail panel has no data —\n * the common mistake is leaving the screen blank, which reads as broken.\n * Always give the user (a) a sense of what would appear here, and (b) a\n * clear next step.\n *\n * Icons come from `lucide-react`. The default `<Inbox />` is appropriate for\n * generic \"no items\" cases; pass `icon` to specialize (FolderOpen,\n * MessageSquareOff, Users, etc.).\n *\n * @example\n * <EmptyState\n * title=\"No projects yet\"\n * description=\"Create your first project to get started.\"\n * action={<Button leftIcon={<Plus />}>New project</Button>}\n * />\n */\nexport interface EmptyStateProps extends HTMLAttributes<HTMLDivElement> {\n /** Headline — what's missing. Required. */\n title: string;\n /** Optional supporting copy explaining what would appear here. Accepts a\n * node so the copy can carry an inline link (e.g. the API-tokens empty\n * state's \"Read the MCP setup guide\", 7.8.3); a plain string still works. */\n description?: ReactNode;\n /** Lucide-style icon; defaults to `<Inbox />`. */\n icon?: ReactNode;\n /** Typically a `<Button>` representing the next action. */\n action?: ReactNode;\n}\n\nexport const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(\n { title, description, icon, action, className, ...rest },\n ref,\n) {\n return (\n <Card ref={ref} className={cn('flex flex-col items-center text-center', className)} {...rest}>\n <div className=\"text-(--el-icon-muted) mb-(--spacing-md) inline-flex h-12 w-12 items-center justify-center\">\n {icon ?? <Inbox className=\"h-12 w-12\" aria-hidden />}\n </div>\n <h2 className=\"font-serif text-xl text-(--el-text)\">{title}</h2>\n {description ? (\n <p className=\"text-(--el-text-subtitle) mt-(--spacing-sm) max-w-prose font-sans text-sm\">\n {description}\n </p>\n ) : null}\n {action ? <div className=\"mt-(--spacing-md)\">{action}</div> : null}\n </Card>\n );\n});\n"]}
@@ -0,0 +1,43 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * ErrorState — composed pattern for "something failed" screens.
6
+ *
7
+ * Use this whenever a request, mutation, or background job has failed in a
8
+ * way the user needs to acknowledge or recover from. Prefer this over a
9
+ * silent log line whenever the failure prevents the user from progressing.
10
+ *
11
+ * Has `role="alert"` so screen readers announce the failure when it's
12
+ * inserted into the DOM. The `error.message` debug detail renders only in
13
+ * non-production builds (Next.js statically replaces `process.env.NODE_ENV`
14
+ * at build time, so the block is dead-code-eliminated in production).
15
+ *
16
+ * @example
17
+ * <ErrorState
18
+ * title="Couldn't load workspace"
19
+ * description="We couldn't reach the server. Check your connection and try again."
20
+ * error={err}
21
+ * retry={() => refetch()}
22
+ * />
23
+ */
24
+ interface ErrorStateProps extends HTMLAttributes<HTMLDivElement> {
25
+ /** Short, plain-language summary of what failed. Required. */
26
+ title: string;
27
+ /** Recovery hint or context. Avoid restating the title. */
28
+ description?: string;
29
+ /** Real Error object — rendered as a mono code block in dev only. */
30
+ error?: Error;
31
+ /** Callback for the "Try again" button; omit to hide the button. */
32
+ retry?: () => void;
33
+ /**
34
+ * Label for the retry button. Defaults to `'Try again'`. i18n is the consuming
35
+ * app's concern — the design-system package is framework-agnostic (no
36
+ * `next-intl` dependency), so a localized app passes the translated string
37
+ * (motir-core wires it from its `common.retry`).
38
+ */
39
+ retryLabel?: string;
40
+ }
41
+ declare const ErrorState: react.ForwardRefExoticComponent<ErrorStateProps & react.RefAttributes<HTMLDivElement>>;
42
+
43
+ export { ErrorState, type ErrorStateProps };