@seekalfred/primitives 0.7.4 → 0.8.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 (87) hide show
  1. package/README.md +255 -3
  2. package/dist/alert/alert.d.ts +2 -2
  3. package/dist/alert/alert.js +1 -1
  4. package/dist/alert/alert.js.map +1 -1
  5. package/dist/alert-dialog/alert-dialog.js +1 -1
  6. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  7. package/dist/avatar/avatar.d.ts +1 -1
  8. package/dist/badge/badge.d.ts +16 -1
  9. package/dist/badge/badge.js +12 -5
  10. package/dist/badge/badge.js.map +1 -1
  11. package/dist/button/button.d.ts +24 -2
  12. package/dist/button/button.js +62 -13
  13. package/dist/button/button.js.map +1 -1
  14. package/dist/card/card.js +1 -1
  15. package/dist/card/card.js.map +1 -1
  16. package/dist/checkbox/checkbox.d.ts +25 -1
  17. package/dist/checkbox/checkbox.js +103 -28
  18. package/dist/checkbox/checkbox.js.map +1 -1
  19. package/dist/checkbox/index.d.ts +2 -2
  20. package/dist/checkbox/index.js +2 -2
  21. package/dist/code-block/code-block.d.ts +45 -0
  22. package/dist/code-block/code-block.js +74 -0
  23. package/dist/code-block/code-block.js.map +1 -0
  24. package/dist/code-block/index.d.ts +2 -0
  25. package/dist/code-block/index.js +2 -0
  26. package/dist/command-palette/command-palette.d.ts +128 -0
  27. package/dist/command-palette/command-palette.js +371 -0
  28. package/dist/command-palette/command-palette.js.map +1 -0
  29. package/dist/command-palette/index.d.ts +2 -0
  30. package/dist/command-palette/index.js +2 -0
  31. package/dist/dialog/dialog.js +1 -1
  32. package/dist/dialog/dialog.js.map +1 -1
  33. package/dist/empty-state/empty-state.d.ts +17 -0
  34. package/dist/empty-state/empty-state.js +9 -7
  35. package/dist/empty-state/empty-state.js.map +1 -1
  36. package/dist/index.d.ts +8 -4
  37. package/dist/index.js +12 -8
  38. package/dist/info-tooltip/index.d.ts +2 -0
  39. package/dist/info-tooltip/index.js +2 -0
  40. package/dist/info-tooltip/info-tooltip.d.ts +35 -0
  41. package/dist/info-tooltip/info-tooltip.js +49 -0
  42. package/dist/info-tooltip/info-tooltip.js.map +1 -0
  43. package/dist/input/input.js +1 -1
  44. package/dist/lib/selection-control.js +32 -1
  45. package/dist/lib/selection-control.js.map +1 -1
  46. package/dist/line-chart/line-chart.d.ts +2 -1
  47. package/dist/line-chart/line-chart.js +2 -2
  48. package/dist/line-chart/line-chart.js.map +1 -1
  49. package/dist/mention-input/mention-input.js +1 -1
  50. package/dist/money-input/money-input.js +1 -1
  51. package/dist/multi-select/multi-select.js +1 -1
  52. package/dist/number-field/number-field.js +1 -1
  53. package/dist/password-field/password-field.js +1 -1
  54. package/dist/password-strength/password-strength.js +1 -1
  55. package/dist/phone-input/phone-input.js +1 -1
  56. package/dist/popover/popover.js +1 -1
  57. package/dist/popover/popover.js.map +1 -1
  58. package/dist/progress/progress.d.ts +3 -3
  59. package/dist/radio-group/radio-group.d.ts +12 -3
  60. package/dist/radio-group/radio-group.js +62 -52
  61. package/dist/radio-group/radio-group.js.map +1 -1
  62. package/dist/rich-text-editor/rich-text-editor.js +1 -1
  63. package/dist/scroll-area/scroll-area.d.ts +1 -1
  64. package/dist/search-field/search-field.js +1 -1
  65. package/dist/select/select.d.ts +1 -1
  66. package/dist/select/select.js +1 -1
  67. package/dist/separator/separator.d.ts +1 -1
  68. package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
  69. package/dist/stat-card/stat-card.d.ts +2 -2
  70. package/dist/styles.css +1 -1
  71. package/dist/table/index.d.ts +2 -2
  72. package/dist/table/index.js +2 -2
  73. package/dist/table/table.d.ts +99 -1
  74. package/dist/table/table.js +123 -23
  75. package/dist/table/table.js.map +1 -1
  76. package/dist/tabs/tabs.d.ts +4 -2
  77. package/dist/tabs/tabs.js +7 -5
  78. package/dist/tabs/tabs.js.map +1 -1
  79. package/dist/tag-input/tag-input.js +1 -1
  80. package/dist/textarea/textarea.js +1 -1
  81. package/dist/toast/toast.d.ts +2 -2
  82. package/dist/toggle-button/index.d.ts +2 -0
  83. package/dist/toggle-button/index.js +2 -0
  84. package/dist/toggle-button/toggle-button.d.ts +93 -0
  85. package/dist/toggle-button/toggle-button.js +199 -0
  86. package/dist/toggle-button/toggle-button.js.map +1 -0
  87. package/package.json +19 -3
@@ -0,0 +1,371 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { Dialog } from "../dialog/dialog.js";
4
+ import { Input } from "../input/input.js";
5
+ import { ShortcutKbd, ShortcutKeys } from "../shortcut-recorder/shortcut-recorder.js";
6
+ import { cva } from "class-variance-authority";
7
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
8
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
+ //#region src/command-palette/command-palette.tsx
10
+ const commandPaletteListVariants = cva(["m-0 max-h-90 list-none overflow-y-auto overscroll-contain p-1.5", "outline-none"]);
11
+ const commandPaletteOptionVariants = cva([
12
+ "flex min-h-10.5 w-full cursor-pointer items-center gap-3 rounded-control px-2.5 text-start text-sm text-foreground",
13
+ "motion-safe:transition-colors motion-safe:duration-(--duration-instant)",
14
+ "aria-selected:bg-muted",
15
+ "aria-disabled:cursor-not-allowed aria-disabled:text-muted-foreground",
16
+ "[&_svg]:pointer-events-none [&_svg]:size-4.5 [&_svg]:flex-none [&_svg]:text-muted-foreground"
17
+ ], { variants: {} });
18
+ const groupLabelClass = "px-2.5 pt-2 pb-1 text-xs font-medium text-muted-foreground";
19
+ /** True when a keydown is this chord. `mod` is either Command or Control (not both required). */
20
+ function matchesCommandPaletteShortcut(event, shortcut) {
21
+ const parts = shortcut.toLowerCase().split("+").map((part) => part.trim()).filter(Boolean);
22
+ const key = parts.pop();
23
+ if (!key) return false;
24
+ const wants = new Set(parts);
25
+ const wantsMod = wants.has("mod");
26
+ const wantsCtrl = wants.has("ctrl") || wants.has("control");
27
+ const wantsMeta = wants.has("meta") || wants.has("cmd") || wants.has("command");
28
+ if (wantsMod) {
29
+ if (!event.ctrlKey && !event.metaKey) return false;
30
+ } else if (event.ctrlKey !== wantsCtrl || event.metaKey !== wantsMeta) return false;
31
+ if (event.altKey !== (wants.has("alt") || wants.has("option"))) return false;
32
+ if (event.shiftKey !== wants.has("shift")) return false;
33
+ return matchesKey(event, key);
34
+ }
35
+ const LATIN_LETTER = /^[a-z]$/i;
36
+ function matchesKey(event, key) {
37
+ const typed = event.key.toLowerCase();
38
+ if (LATIN_LETTER.test(key)) {
39
+ if (LATIN_LETTER.test(event.key)) return typed === key;
40
+ return event.code === `Key${key.toUpperCase()}`;
41
+ }
42
+ if (/^[0-9]$/.test(key)) return typed === key || event.code === `Digit${key}`;
43
+ if (key === "space" || key === "spacebar") return event.key === " " || event.code === "Space";
44
+ if (key === "esc") return typed === "escape";
45
+ return typed === key;
46
+ }
47
+ /**
48
+ * Calls `onTrigger` when the chord is pressed anywhere in the document, and prevents the
49
+ * browser's own use of it (Ctrl+K focuses the address bar in some browsers).
50
+ *
51
+ * `CommandPalette` installs this itself, so use the hook only to open a palette from a
52
+ * different chord than the one the palette listens for, or to drive one you render conditionally.
53
+ */
54
+ function useCommandPaletteShortcut(onTrigger, { shortcut = "mod+k", enabled = true } = {}) {
55
+ const latest = useRef(onTrigger);
56
+ useEffect(() => {
57
+ latest.current = onTrigger;
58
+ });
59
+ useEffect(() => {
60
+ if (!enabled) return;
61
+ const onKeyDown = (event) => {
62
+ if (event.defaultPrevented || event.repeat) return;
63
+ if (!matchesCommandPaletteShortcut(event, shortcut)) return;
64
+ event.preventDefault();
65
+ latest.current();
66
+ };
67
+ document.addEventListener("keydown", onKeyDown);
68
+ return () => document.removeEventListener("keydown", onKeyDown);
69
+ }, [shortcut, enabled]);
70
+ }
71
+ const haystack = (item) => [
72
+ item.label,
73
+ item.group ?? "",
74
+ ...item.keywords ?? []
75
+ ].join(" ").toLowerCase();
76
+ /** Every whitespace-separated word of the query must appear in the label, group or keywords. */
77
+ function defaultFilter(item, query) {
78
+ const text = haystack(item);
79
+ return query.toLowerCase().split(/\s+/).filter(Boolean).every((word) => text.includes(word));
80
+ }
81
+ function nextEnabled(rows, from, step) {
82
+ if (rows.length === 0) return -1;
83
+ for (let i = 1; i <= rows.length; i++) {
84
+ const at = (from + step * i + rows.length * i) % rows.length;
85
+ if (!rows[at].item.isDisabled) return at;
86
+ }
87
+ return -1;
88
+ }
89
+ const defaultEmpty = (query) => `No matches for “${query}”.`;
90
+ const defaultResults = (count) => count === 0 ? "No results" : count === 1 ? "1 result" : `${count} results`;
91
+ function DefaultFooter() {
92
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
93
+ className: "inline-flex items-center gap-1.5",
94
+ children: [
95
+ /* @__PURE__ */ jsx(ShortcutKbd, {
96
+ size: "sm",
97
+ "aria-label": "Up arrow",
98
+ children: "↑"
99
+ }),
100
+ /* @__PURE__ */ jsx(ShortcutKbd, {
101
+ size: "sm",
102
+ "aria-label": "Down arrow",
103
+ children: "↓"
104
+ }),
105
+ "to move"
106
+ ]
107
+ }), /* @__PURE__ */ jsxs("span", {
108
+ className: "inline-flex items-center gap-1.5",
109
+ children: [/* @__PURE__ */ jsx(ShortcutKbd, {
110
+ size: "sm",
111
+ children: "Enter"
112
+ }), "to open"]
113
+ })] });
114
+ }
115
+ /**
116
+ * A command palette.
117
+ *
118
+ * @example
119
+ * const [open, setOpen] = useState(false);
120
+ * <CommandPalette
121
+ * items={[
122
+ * { id: "home", group: "Pages", label: "Home", hint: "Go to" },
123
+ * { id: "theme", group: "Actions", label: "Switch theme", keywords: ["dark"], shortcut: "mod+shift+l" },
124
+ * ]}
125
+ * isOpen={open}
126
+ * onOpenChange={setOpen}
127
+ * onSelect={(item) => run(item.id)}
128
+ * />
129
+ */
130
+ function CommandPalette({ items, recent, recentLabel = "Recent", onSelect, isOpen: isOpenProp, defaultOpen = false, onOpenChange, shortcut = "mod+k", label = "Search and commands", searchLabel = "Search", placeholder = "Search pages and actions", filter = defaultFilter, emptyMessage = defaultEmpty, resultsLabel = defaultResults, footer, className }) {
131
+ const baseId = useId();
132
+ const listId = `${baseId}-list`;
133
+ const [openState, setOpenState] = useState(defaultOpen);
134
+ const controlled = isOpenProp !== void 0;
135
+ const isOpen = controlled ? isOpenProp : openState;
136
+ const setOpen = useCallback((next) => {
137
+ if (!controlled) setOpenState(next);
138
+ onOpenChange?.(next);
139
+ }, [controlled, onOpenChange]);
140
+ useCommandPaletteShortcut(() => setOpen(!isOpen), {
141
+ shortcut: shortcut === false ? void 0 : shortcut,
142
+ enabled: shortcut !== false
143
+ });
144
+ return /* @__PURE__ */ jsx(Dialog, {
145
+ isOpen,
146
+ onOpenChange: setOpen,
147
+ title: /* @__PURE__ */ jsx("span", {
148
+ className: "sr-only",
149
+ children: label
150
+ }),
151
+ closeButton: false,
152
+ "data-slot": "command-palette",
153
+ overlayClassName: "items-start pt-24",
154
+ className: cn("max-w-140 gap-0 overflow-hidden p-0", className),
155
+ children: /* @__PURE__ */ jsx(PaletteBody, {
156
+ baseId,
157
+ listId,
158
+ items,
159
+ recent,
160
+ recentLabel,
161
+ searchLabel,
162
+ placeholder,
163
+ filter,
164
+ emptyMessage,
165
+ resultsLabel,
166
+ footer,
167
+ onChoose: (item) => {
168
+ setOpen(false);
169
+ onSelect?.(item);
170
+ }
171
+ })
172
+ });
173
+ }
174
+ /** Mounted only while open, so the query and the highlight reset every time without effects. */
175
+ function PaletteBody({ baseId, listId, items, recent, recentLabel, searchLabel, placeholder, filter, emptyMessage, resultsLabel, footer, onChoose }) {
176
+ const [query, setQuery] = useState("");
177
+ const [activeIndex, setActiveIndex] = useState(0);
178
+ const listRef = useRef(null);
179
+ const trimmed = query.trim();
180
+ const sections = useMemo(() => {
181
+ const out = [];
182
+ let index = 0;
183
+ const push = (key, name, list) => {
184
+ if (list.length === 0) return;
185
+ out.push({
186
+ key,
187
+ label: name,
188
+ rows: list.map((item) => ({
189
+ item,
190
+ index: index++
191
+ }))
192
+ });
193
+ };
194
+ if (!trimmed && recent?.length) push("\0recent", recentLabel, recent);
195
+ const matches = trimmed ? items.filter((item) => filter(item, trimmed)) : items;
196
+ const order = [];
197
+ const byGroup = /* @__PURE__ */ new Map();
198
+ for (const item of matches) {
199
+ const g = item.group ?? "";
200
+ if (!byGroup.has(g)) {
201
+ byGroup.set(g, []);
202
+ order.push(g);
203
+ }
204
+ byGroup.get(g).push(item);
205
+ }
206
+ for (const g of order) push(`g:${g}`, g || void 0, byGroup.get(g));
207
+ return out;
208
+ }, [
209
+ items,
210
+ recent,
211
+ recentLabel,
212
+ trimmed,
213
+ filter
214
+ ]);
215
+ const rows = useMemo(() => sections.flatMap((s) => s.rows), [sections]);
216
+ const firstEnabled = rows.findIndex((row) => !row.item.isDisabled);
217
+ const current = rows[activeIndex] && !rows[activeIndex].item.isDisabled ? activeIndex : firstEnabled;
218
+ const optionId = (index) => `${baseId}-opt-${index}`;
219
+ useEffect(() => {
220
+ if (current < 0) return;
221
+ document.getElementById(optionId(current))?.scrollIntoView?.({ block: "nearest" });
222
+ }, [current]);
223
+ const onKeyDown = (event) => {
224
+ if (event.nativeEvent.isComposing) return;
225
+ switch (event.key) {
226
+ case "ArrowDown":
227
+ case "ArrowUp": {
228
+ event.preventDefault();
229
+ const next = nextEnabled(rows, current < 0 ? 0 : current, event.key === "ArrowDown" ? 1 : -1);
230
+ if (next >= 0) setActiveIndex(next);
231
+ break;
232
+ }
233
+ case "Home":
234
+ case "End": {
235
+ if (query !== "" || rows.length === 0) break;
236
+ event.preventDefault();
237
+ const ordered = rows.map((row, i) => i).filter((i) => !rows[i].item.isDisabled);
238
+ const target = event.key === "Home" ? ordered[0] : ordered[ordered.length - 1];
239
+ if (target !== void 0) setActiveIndex(target);
240
+ break;
241
+ }
242
+ case "Enter": {
243
+ const row = rows[current];
244
+ if (!row) break;
245
+ event.preventDefault();
246
+ onChoose(row.item);
247
+ break;
248
+ }
249
+ }
250
+ };
251
+ const hasRows = rows.length > 0;
252
+ const emptyContent = typeof emptyMessage === "function" ? emptyMessage(trimmed) : emptyMessage;
253
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
254
+ /* @__PURE__ */ jsx("div", {
255
+ "data-slot": "command-palette-search",
256
+ className: "border-b-(length:--border-width) border-border",
257
+ children: /* @__PURE__ */ jsx(Input, {
258
+ autoFocus: true,
259
+ type: "text",
260
+ role: "combobox",
261
+ "aria-label": searchLabel,
262
+ "aria-expanded": hasRows,
263
+ "aria-haspopup": "listbox",
264
+ "aria-autocomplete": "list",
265
+ "aria-controls": hasRows ? listId : void 0,
266
+ "aria-activedescendant": current >= 0 ? optionId(current) : void 0,
267
+ autoComplete: "off",
268
+ autoCorrect: "off",
269
+ spellCheck: false,
270
+ placeholder,
271
+ size: "lg",
272
+ startIcon: /* @__PURE__ */ jsxs("svg", {
273
+ viewBox: "0 0 24 24",
274
+ fill: "none",
275
+ stroke: "currentColor",
276
+ strokeWidth: 2,
277
+ strokeLinecap: "round",
278
+ strokeLinejoin: "round",
279
+ "aria-hidden": "true",
280
+ className: "size-4.5 text-muted-foreground",
281
+ children: [/* @__PURE__ */ jsx("circle", {
282
+ cx: "11",
283
+ cy: "11",
284
+ r: "8"
285
+ }), /* @__PURE__ */ jsx("path", { d: "m21 21-4.34-4.34" })]
286
+ }),
287
+ value: query,
288
+ onChange: (event) => {
289
+ setQuery(event.target.value);
290
+ setActiveIndex(0);
291
+ },
292
+ onKeyDown,
293
+ className: "h-14 rounded-none border-0 bg-transparent shadow-none data-[focus-visible]:outline-none"
294
+ })
295
+ }),
296
+ hasRows ? /* @__PURE__ */ jsx("div", {
297
+ ref: listRef,
298
+ id: listId,
299
+ role: "listbox",
300
+ "aria-label": searchLabel,
301
+ "data-slot": "command-palette-list",
302
+ className: commandPaletteListVariants(),
303
+ children: sections.map((section) => {
304
+ const headingId = `${baseId}-g-${section.key.replace(/\W/g, "_")}`;
305
+ return /* @__PURE__ */ jsxs("div", {
306
+ role: "group",
307
+ "aria-labelledby": section.label ? headingId : void 0,
308
+ "data-slot": "command-palette-group",
309
+ children: [section.label ? /* @__PURE__ */ jsx("div", {
310
+ id: headingId,
311
+ role: "presentation",
312
+ "data-slot": "command-palette-group-label",
313
+ className: groupLabelClass,
314
+ children: section.label
315
+ }) : null, section.rows.map(({ item, index }) => /* @__PURE__ */ jsxs("div", {
316
+ id: optionId(index),
317
+ role: "option",
318
+ "aria-selected": index === current,
319
+ "aria-disabled": item.isDisabled || void 0,
320
+ "data-slot": "command-palette-option",
321
+ "data-item-id": item.id,
322
+ className: commandPaletteOptionVariants(),
323
+ onMouseDown: (event) => event.preventDefault(),
324
+ onMouseMove: () => {
325
+ if (!item.isDisabled && index !== current) setActiveIndex(index);
326
+ },
327
+ onClick: () => {
328
+ if (!item.isDisabled) onChoose(item);
329
+ },
330
+ children: [
331
+ item.icon,
332
+ /* @__PURE__ */ jsx("span", {
333
+ "data-slot": "command-palette-item-label",
334
+ className: "min-w-0 flex-1 truncate",
335
+ children: item.label
336
+ }),
337
+ item.shortcut ? /* @__PURE__ */ jsx(ShortcutKeys, {
338
+ shortcut: item.shortcut,
339
+ size: "sm"
340
+ }) : item.hint ? /* @__PURE__ */ jsx("small", {
341
+ "data-slot": "command-palette-item-hint",
342
+ className: "ms-auto flex-none text-xs text-muted-foreground",
343
+ children: item.hint
344
+ }) : null
345
+ ]
346
+ }, `${section.key}:${item.id}`))]
347
+ }, section.key);
348
+ })
349
+ }) : /* @__PURE__ */ jsx("div", {
350
+ role: "status",
351
+ "data-slot": "command-palette-empty",
352
+ className: "px-4 py-8 text-center text-sm text-muted-foreground",
353
+ children: emptyContent
354
+ }),
355
+ /* @__PURE__ */ jsx("div", {
356
+ "aria-live": "polite",
357
+ "data-slot": "command-palette-status",
358
+ className: "sr-only",
359
+ children: trimmed || hasRows ? resultsLabel(rows.length) : ""
360
+ }),
361
+ footer === null ? null : /* @__PURE__ */ jsx("div", {
362
+ "data-slot": "command-palette-footer",
363
+ className: "flex gap-3.5 border-t-(length:--border-width) border-border px-4 py-2.5 text-xs text-muted-foreground",
364
+ children: footer === void 0 ? /* @__PURE__ */ jsx(DefaultFooter, {}) : footer
365
+ })
366
+ ] });
367
+ }
368
+ //#endregion
369
+ export { CommandPalette, commandPaletteListVariants, commandPaletteOptionVariants, matchesCommandPaletteShortcut, useCommandPaletteShortcut };
370
+
371
+ //# sourceMappingURL=command-palette.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-palette.js","names":[],"sources":["../../src/command-palette/command-palette.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { Dialog } from \"../dialog/dialog.js\";\nimport { Input } from \"../input/input.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { ShortcutKbd, ShortcutKeys } from \"../shortcut-recorder/shortcut-recorder.js\";\n\n/**\n * CommandPalette: a modal search-and-run list opened with Cmd+K or Ctrl+K.\n *\n * Built on the existing Dialog (React Aria's modal: focus trap, focus return, Escape, scrim,\n * entrance motion that already honours prefers-reduced-motion), Input and ShortcutKeys, so it adds\n * only the combobox + listbox wiring.\n *\n * The pattern is the WAI-ARIA \"combobox with listbox popup\": DOM focus never leaves the input,\n * and the highlighted option is named by `aria-activedescendant`. Options are not tabbable.\n *\n * Every string in the cva blocks is a STATIC literal (Tailwind scans source text).\n */\n\n/** One entry in the palette. */\nexport interface CommandPaletteItem {\n /**\n * Stable identity. Unique within `items`. A `recent` entry is a copy of an item, so it uses\n * that item's `id` and `recent` need not be unique against `items`; only each list on its own.\n */\n id: string;\n /** The text shown, and the main thing the query is matched against. */\n label: string;\n /** The heading it is listed under. Items without one share an unlabelled group. */\n group?: string;\n /** A decorative glyph before the label. Mark it `aria-hidden`; it is sized by the palette. */\n icon?: ReactNode;\n /** A key combination shown as key caps, such as `\"mod+shift+p\"`. A hint only: it binds nothing. */\n shortcut?: string;\n /** Plain text at the trailing edge, such as \"Go to\" or \"Run\". Shown when there is no `shortcut`. */\n hint?: string;\n /** Extra words that also match the query but are not shown. */\n keywords?: readonly string[];\n /** Listed but skipped by the arrow keys and ignored when pressed. @default false */\n isDisabled?: boolean;\n}\n\nexport const commandPaletteListVariants = cva([\n \"m-0 max-h-90 list-none overflow-y-auto overscroll-contain p-1.5\",\n // The list is a scroll container the keyboard moves through by aria-activedescendant, so it is not a tab stop.\n \"outline-none\",\n]);\n\nexport const commandPaletteOptionVariants = cva(\n [\n \"flex min-h-10.5 w-full cursor-pointer items-center gap-3 rounded-control px-2.5 text-start text-sm text-foreground\",\n \"motion-safe:transition-colors motion-safe:duration-(--duration-instant)\",\n // aria-selected is the active descendant, not a selection: it carries the highlight.\n \"aria-selected:bg-muted\",\n \"aria-disabled:cursor-not-allowed aria-disabled:text-muted-foreground\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4.5 [&_svg]:flex-none [&_svg]:text-muted-foreground\",\n ],\n { variants: {} },\n);\n\nconst groupLabelClass = \"px-2.5 pt-2 pb-1 text-xs font-medium text-muted-foreground\";\n\n/* ------------------------------------------------ shortcut ------------------------------------------------ */\n\nexport interface CommandPaletteShortcutOptions {\n /**\n * The chord. `mod` matches Command or Control on any platform, so the default opens on both\n * Cmd+K and Ctrl+K. Other modifiers: `ctrl`, `meta`, `alt`, `shift`. Modifiers you do not name\n * must not be held.\n *\n * The last part is the key. Supported: a letter (`k`), a digit (`1`, also with `shift`), `space`,\n * and any other name equal to `KeyboardEvent.key` such as `escape`, `enter`, `/`. A letter is\n * read from what the key types, so it follows Dvorak and similar layouts. On a non-Latin layout\n * it falls back to the physical key.\n * @default \"mod+k\"\n */\n shortcut?: string;\n /** Turn the listener off. @default true */\n enabled?: boolean;\n}\n\ntype KeyLike = Pick<KeyboardEvent, \"key\" | \"code\" | \"ctrlKey\" | \"metaKey\" | \"altKey\" | \"shiftKey\">;\n\n/** True when a keydown is this chord. `mod` is either Command or Control (not both required). */\nexport function matchesCommandPaletteShortcut(event: KeyLike, shortcut: string): boolean {\n const parts = shortcut\n .toLowerCase()\n .split(\"+\")\n .map((part) => part.trim())\n .filter(Boolean);\n const key = parts.pop();\n if (!key) return false;\n const wants = new Set(parts);\n const wantsMod = wants.has(\"mod\");\n const wantsCtrl = wants.has(\"ctrl\") || wants.has(\"control\");\n const wantsMeta = wants.has(\"meta\") || wants.has(\"cmd\") || wants.has(\"command\");\n if (wantsMod) {\n // Either one of the pair, and neither if an explicit one is also named is not a thing to support.\n if (!event.ctrlKey && !event.metaKey) return false;\n } else if (event.ctrlKey !== wantsCtrl || event.metaKey !== wantsMeta) return false;\n if (event.altKey !== (wants.has(\"alt\") || wants.has(\"option\"))) return false;\n if (event.shiftKey !== wants.has(\"shift\")) return false;\n return matchesKey(event, key);\n}\n\nconst LATIN_LETTER = /^[a-z]$/i;\n\nfunction matchesKey(event: KeyLike, key: string): boolean {\n const typed = event.key.toLowerCase();\n if (LATIN_LETTER.test(key)) {\n // Trust what the key types when it is a Latin letter (Dvorak, AZERTY). Otherwise the layout\n // is non-Latin, or Alt rewrote `key` (Option+K types a symbol on macOS): use the physical key.\n if (LATIN_LETTER.test(event.key)) return typed === key;\n return event.code === `Key${key.toUpperCase()}`;\n }\n // Shift turns `key` into \"!\" and some layouts type symbols for the digit row, so use `code`.\n if (/^[0-9]$/.test(key)) return typed === key || event.code === `Digit${key}`;\n if (key === \"space\" || key === \"spacebar\") return event.key === \" \" || event.code === \"Space\";\n if (key === \"esc\") return typed === \"escape\";\n return typed === key;\n}\n\n/**\n * Calls `onTrigger` when the chord is pressed anywhere in the document, and prevents the\n * browser's own use of it (Ctrl+K focuses the address bar in some browsers).\n *\n * `CommandPalette` installs this itself, so use the hook only to open a palette from a\n * different chord than the one the palette listens for, or to drive one you render conditionally.\n */\nexport function useCommandPaletteShortcut(\n onTrigger: () => void,\n { shortcut = \"mod+k\", enabled = true }: CommandPaletteShortcutOptions = {},\n) {\n const latest = useRef(onTrigger);\n useEffect(() => {\n latest.current = onTrigger;\n });\n useEffect(() => {\n if (!enabled) return;\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || event.repeat) return;\n if (!matchesCommandPaletteShortcut(event, shortcut)) return;\n event.preventDefault();\n latest.current();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [shortcut, enabled]);\n}\n\n/* ------------------------------------------------ filtering ------------------------------------------------ */\n\nconst haystack = (item: CommandPaletteItem) =>\n [item.label, item.group ?? \"\", ...(item.keywords ?? [])].join(\" \").toLowerCase();\n\n/** Every whitespace-separated word of the query must appear in the label, group or keywords. */\nfunction defaultFilter(item: CommandPaletteItem, query: string) {\n const text = haystack(item);\n return query\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((word) => text.includes(word));\n}\n\ninterface Row {\n item: CommandPaletteItem;\n /** Flat position among the rows, which names the option's DOM id. */\n index: number;\n}\n\ninterface Section {\n key: string;\n label: string | undefined;\n rows: Row[];\n}\n\nfunction nextEnabled(rows: Row[], from: number, step: 1 | -1): number {\n if (rows.length === 0) return -1;\n for (let i = 1; i <= rows.length; i++) {\n const at = (from + step * i + rows.length * i) % rows.length;\n if (!rows[at]!.item.isDisabled) return at;\n }\n return -1;\n}\n\n/* ------------------------------------------------ component ------------------------------------------------ */\n\nexport interface CommandPaletteProps {\n /** Everything that can be run or opened. */\n items: readonly CommandPaletteItem[];\n /**\n * A group of recent or suggested items, listed first while the search box is empty. It\n * disappears once the user types, when the search runs over `items` alone.\n */\n recent?: readonly CommandPaletteItem[];\n /** Heading of the `recent` group. @default \"Recent\" */\n recentLabel?: string;\n /** Called with the chosen item, after the palette has asked to close. */\n onSelect?: (item: CommandPaletteItem) => void;\n /** Whether it is open. Pass with `onOpenChange` to control it. */\n isOpen?: boolean;\n /** Whether it starts open when uncontrolled. @default false */\n defaultOpen?: boolean;\n /** Called when it wants to open or close: the shortcut, Escape, the scrim, a selection. */\n onOpenChange?: (isOpen: boolean) => void;\n /**\n * The chord that toggles it, installed on the document while the component is mounted.\n * `false` removes the listener, for an app that binds it elsewhere (see\n * `useCommandPaletteShortcut`). `mod` is Command or Control.\n * @default \"mod+k\"\n */\n shortcut?: string | false;\n /** The dialog's accessible name. A prop so it can be translated. @default \"Search and commands\" */\n label?: string;\n /** Accessible name of the search box. @default \"Search\" */\n searchLabel?: string;\n /** Placeholder of the search box. @default \"Search pages and actions\" */\n placeholder?: string;\n /** Replaces the default filter. Receives the item and the trimmed query; the query is never empty. */\n filter?: (item: CommandPaletteItem, query: string) => boolean;\n /** What to show when nothing matches. @default ``No matches for “query”.`` */\n emptyMessage?: ReactNode | ((query: string) => ReactNode);\n /** The polite result-count announcement. @default \"1 result\" / \"n results\" / \"No results\" */\n resultsLabel?: (count: number) => string;\n /**\n * The strip of key hints under the list. `undefined` shows the default (\"↑ ↓ to move\",\n * \"Enter to open\"); `null` removes it; anything else replaces it, which is how to translate it.\n */\n footer?: ReactNode;\n /** Extra classes for the panel. Merged last. */\n className?: string;\n}\n\nconst defaultEmpty = (query: string) => `No matches for “${query}”.`;\nconst defaultResults = (count: number) =>\n count === 0 ? \"No results\" : count === 1 ? \"1 result\" : `${count} results`;\n\nfunction DefaultFooter() {\n return (\n <>\n <span className=\"inline-flex items-center gap-1.5\">\n <ShortcutKbd size=\"sm\" aria-label=\"Up arrow\">\n ↑\n </ShortcutKbd>\n <ShortcutKbd size=\"sm\" aria-label=\"Down arrow\">\n ↓\n </ShortcutKbd>\n to move\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <ShortcutKbd size=\"sm\">Enter</ShortcutKbd>\n to open\n </span>\n </>\n );\n}\n\n/**\n * A command palette.\n *\n * @example\n * const [open, setOpen] = useState(false);\n * <CommandPalette\n * items={[\n * { id: \"home\", group: \"Pages\", label: \"Home\", hint: \"Go to\" },\n * { id: \"theme\", group: \"Actions\", label: \"Switch theme\", keywords: [\"dark\"], shortcut: \"mod+shift+l\" },\n * ]}\n * isOpen={open}\n * onOpenChange={setOpen}\n * onSelect={(item) => run(item.id)}\n * />\n */\nexport function CommandPalette({\n items,\n recent,\n recentLabel = \"Recent\",\n onSelect,\n isOpen: isOpenProp,\n defaultOpen = false,\n onOpenChange,\n shortcut = \"mod+k\",\n label = \"Search and commands\",\n searchLabel = \"Search\",\n placeholder = \"Search pages and actions\",\n filter = defaultFilter,\n emptyMessage = defaultEmpty,\n resultsLabel = defaultResults,\n footer,\n className,\n}: CommandPaletteProps) {\n const baseId = useId();\n const listId = `${baseId}-list`;\n const [openState, setOpenState] = useState(defaultOpen);\n const controlled = isOpenProp !== undefined;\n const isOpen = controlled ? isOpenProp : openState;\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (!controlled) setOpenState(next);\n onOpenChange?.(next);\n },\n [controlled, onOpenChange],\n );\n\n useCommandPaletteShortcut(() => setOpen(!isOpen), {\n shortcut: shortcut === false ? undefined : shortcut,\n enabled: shortcut !== false,\n });\n\n return (\n <Dialog\n isOpen={isOpen}\n onOpenChange={setOpen}\n // A visually hidden heading is still the dialog's name (aria-labelledby), and the visible\n // chrome stays the prototype's: just the search row.\n title={<span className=\"sr-only\">{label}</span>}\n closeButton={false}\n data-slot=\"command-palette\"\n overlayClassName=\"items-start pt-24\"\n className={cn(\"max-w-140 gap-0 overflow-hidden p-0\", className)}\n >\n <PaletteBody\n baseId={baseId}\n listId={listId}\n items={items}\n recent={recent}\n recentLabel={recentLabel}\n searchLabel={searchLabel}\n placeholder={placeholder}\n filter={filter}\n emptyMessage={emptyMessage}\n resultsLabel={resultsLabel}\n footer={footer}\n onChoose={(item) => {\n setOpen(false);\n onSelect?.(item);\n }}\n />\n </Dialog>\n );\n}\n\ninterface BodyProps {\n baseId: string;\n listId: string;\n items: readonly CommandPaletteItem[];\n recent: readonly CommandPaletteItem[] | undefined;\n recentLabel: string;\n searchLabel: string;\n placeholder: string;\n filter: (item: CommandPaletteItem, query: string) => boolean;\n emptyMessage: ReactNode | ((query: string) => ReactNode);\n resultsLabel: (count: number) => string;\n footer: ReactNode;\n onChoose: (item: CommandPaletteItem) => void;\n}\n\n/** Mounted only while open, so the query and the highlight reset every time without effects. */\nfunction PaletteBody({\n baseId,\n listId,\n items,\n recent,\n recentLabel,\n searchLabel,\n placeholder,\n filter,\n emptyMessage,\n resultsLabel,\n footer,\n onChoose,\n}: BodyProps) {\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const listRef = useRef<HTMLDivElement>(null);\n\n const trimmed = query.trim();\n\n const sections = useMemo<Section[]>(() => {\n const out: Section[] = [];\n let index = 0;\n const push = (key: string, name: string | undefined, list: readonly CommandPaletteItem[]) => {\n if (list.length === 0) return;\n out.push({ key, label: name, rows: list.map((item) => ({ item, index: index++ })) });\n };\n if (!trimmed && recent?.length) push(\"\\u0000recent\", recentLabel, recent);\n // Groups in first-appearance order, with each group's items together.\n const matches = trimmed ? items.filter((item) => filter(item, trimmed)) : items;\n const order: string[] = [];\n const byGroup = new Map<string, CommandPaletteItem[]>();\n for (const item of matches) {\n const g = item.group ?? \"\";\n if (!byGroup.has(g)) {\n byGroup.set(g, []);\n order.push(g);\n }\n byGroup.get(g)!.push(item);\n }\n for (const g of order) push(`g:${g}`, g || undefined, byGroup.get(g)!);\n return out;\n }, [items, recent, recentLabel, trimmed, filter]);\n\n const rows = useMemo(() => sections.flatMap((s) => s.rows), [sections]);\n // Clamp rather than reset in an effect: the list can shrink under a stale index.\n const firstEnabled = rows.findIndex((row) => !row.item.isDisabled);\n const current =\n rows[activeIndex] && !rows[activeIndex].item.isDisabled ? activeIndex : firstEnabled;\n const optionId = (index: number) => `${baseId}-opt-${index}`;\n\n // Keep the highlighted row in view as the keyboard moves it.\n useEffect(() => {\n if (current < 0) return;\n document.getElementById(optionId(current))?.scrollIntoView?.({ block: \"nearest\" });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [current]);\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {\n if (event.nativeEvent.isComposing) return;\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowUp\": {\n event.preventDefault();\n const next = nextEnabled(\n rows,\n current < 0 ? 0 : current,\n event.key === \"ArrowDown\" ? 1 : -1,\n );\n // `current < 0` only when no row is enabled, and then `next` is -1 and nothing moves.\n if (next >= 0) setActiveIndex(next);\n break;\n }\n case \"Home\":\n case \"End\": {\n // With text in the box, Home/End move the caret as in any input; they jump rows only when empty.\n if (query !== \"\" || rows.length === 0) break;\n event.preventDefault();\n const ordered = rows.map((row, i) => i).filter((i) => !rows[i]!.item.isDisabled);\n const target = event.key === \"Home\" ? ordered[0] : ordered[ordered.length - 1];\n if (target !== undefined) setActiveIndex(target);\n break;\n }\n case \"Enter\": {\n const row = rows[current];\n if (!row) break;\n event.preventDefault();\n onChoose(row.item);\n break;\n }\n }\n };\n\n const hasRows = rows.length > 0;\n const emptyContent = typeof emptyMessage === \"function\" ? emptyMessage(trimmed) : emptyMessage;\n\n return (\n <>\n <div\n data-slot=\"command-palette-search\"\n className=\"border-b-(length:--border-width) border-border\"\n >\n <Input\n autoFocus\n type=\"text\"\n role=\"combobox\"\n aria-label={searchLabel}\n aria-expanded={hasRows}\n aria-haspopup=\"listbox\"\n aria-autocomplete=\"list\"\n aria-controls={hasRows ? listId : undefined}\n aria-activedescendant={current >= 0 ? optionId(current) : undefined}\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n placeholder={placeholder}\n size=\"lg\"\n startIcon={\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-4.5 text-muted-foreground\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.34-4.34\" />\n </svg>\n }\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setActiveIndex(0);\n }}\n onKeyDown={onKeyDown}\n // The palette's own row supplies the surface: the field drops its border, fill and ring.\n className=\"h-14 rounded-none border-0 bg-transparent shadow-none data-[focus-visible]:outline-none\"\n />\n </div>\n\n {hasRows ? (\n <div\n ref={listRef}\n id={listId}\n role=\"listbox\"\n aria-label={searchLabel}\n data-slot=\"command-palette-list\"\n className={commandPaletteListVariants()}\n >\n {sections.map((section) => {\n const headingId = `${baseId}-g-${section.key.replace(/\\W/g, \"_\")}`;\n return (\n <div\n key={section.key}\n role=\"group\"\n aria-labelledby={section.label ? headingId : undefined}\n data-slot=\"command-palette-group\"\n >\n {section.label ? (\n <div\n id={headingId}\n role=\"presentation\"\n data-slot=\"command-palette-group-label\"\n className={groupLabelClass}\n >\n {section.label}\n </div>\n ) : null}\n {section.rows.map(({ item, index }) => (\n <div\n key={`${section.key}:${item.id}`}\n id={optionId(index)}\n role=\"option\"\n aria-selected={index === current}\n aria-disabled={item.isDisabled || undefined}\n data-slot=\"command-palette-option\"\n data-item-id={item.id}\n className={commandPaletteOptionVariants()}\n // Keep DOM focus in the input: a press on a row must not steal it.\n onMouseDown={(event) => event.preventDefault()}\n onMouseMove={() => {\n if (!item.isDisabled && index !== current) setActiveIndex(index);\n }}\n onClick={() => {\n if (!item.isDisabled) onChoose(item);\n }}\n >\n {item.icon}\n <span\n data-slot=\"command-palette-item-label\"\n className=\"min-w-0 flex-1 truncate\"\n >\n {item.label}\n </span>\n {item.shortcut ? (\n <ShortcutKeys shortcut={item.shortcut} size=\"sm\" />\n ) : item.hint ? (\n <small\n data-slot=\"command-palette-item-hint\"\n className=\"ms-auto flex-none text-xs text-muted-foreground\"\n >\n {item.hint}\n </small>\n ) : null}\n </div>\n ))}\n </div>\n );\n })}\n </div>\n ) : (\n <div\n role=\"status\"\n data-slot=\"command-palette-empty\"\n className=\"px-4 py-8 text-center text-sm text-muted-foreground\"\n >\n {emptyContent}\n </div>\n )}\n\n {/* The count is announced politely; the empty state above is its own status region. */}\n <div aria-live=\"polite\" data-slot=\"command-palette-status\" className=\"sr-only\">\n {trimmed || hasRows ? resultsLabel(rows.length) : \"\"}\n </div>\n\n {footer === null ? null : (\n <div\n data-slot=\"command-palette-footer\"\n className=\"flex gap-3.5 border-t-(length:--border-width) border-border px-4 py-2.5 text-xs text-muted-foreground\"\n >\n {footer === undefined ? <DefaultFooter /> : footer}\n </div>\n )}\n </>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { CommandPalette, CommandPaletteItem, CommandPaletteProps, CommandPaletteShortcutOptions, commandPaletteListVariants, commandPaletteOptionVariants, matchesCommandPaletteShortcut, useCommandPaletteShortcut } from "./command-palette.js";
2
+ export { CommandPalette, type CommandPaletteItem, type CommandPaletteProps, type CommandPaletteShortcutOptions, commandPaletteListVariants, commandPaletteOptionVariants, matchesCommandPaletteShortcut, useCommandPaletteShortcut };
@@ -0,0 +1,2 @@
1
+ import { CommandPalette, commandPaletteListVariants, commandPaletteOptionVariants, matchesCommandPaletteShortcut, useCommandPaletteShortcut } from "./command-palette.js";
2
+ export { CommandPalette, commandPaletteListVariants, commandPaletteOptionVariants, matchesCommandPaletteShortcut, useCommandPaletteShortcut };
@@ -53,7 +53,7 @@ const dialogPanelVariants = cva([
53
53
  "outline-none",
54
54
  "max-h-full overflow-y-auto"
55
55
  ]);
56
- const dialogTitleVariants = cva(["text-lg leading-(--leading-tight) font-semibold", "m-0"]);
56
+ const dialogTitleVariants = cva(["font-display text-lg leading-(--leading-tight) font-semibold", "m-0"]);
57
57
  const dialogDescriptionVariants = cva(["text-sm leading-(--leading-normal) text-muted-foreground", "m-0"]);
58
58
  const dialogFooterVariants = cva(["flex flex-wrap items-center justify-end gap-2"]);
59
59
  const dialogCloseVariants = cva([
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-panel border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\n/** A string title or description swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\") return <>{children}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n /**\n * How the dialog comes in: `rise` fades the overlay (blurring the page) and lifts\n * the panel in on a spring; `none` leaves the entrance to the caller, as an\n * edge-anchored preset (Sheet) does.\n *\n * @default \"rise\"\n */\n entrance?: \"rise\" | \"none\";\n}\n\nexport const Dialog = forwardRef<HTMLElement, DialogProps>(function Dialog(\n {\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n // Modal is a transparent pass-through (className=\"contents\", no\n // box of its own) and ModalOverlay is the scrim -- AriaDialog is\n // the only one of the three that is the visible panel, so the ref\n // goes here and not on either ancestor.\n ref={ref}\n role={role}\n data-slot=\"dialog\"\n className={cnAll(dialogPanelVariants(), entrance === \"rise\" && \"dialog-panel-motion\", className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n <Swappable value={title}>{title}</Swappable>\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n <Swappable value={description}>{description}</Swappable>\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(function DialogFooter(\n { children, className },\n ref,\n) {\n return (\n <div ref={ref} data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n});\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,mDAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CAEA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAS,UAAU,EAAE,OAAO,YAAuD;CACjF,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAG,SAAW,CAAA;CACpD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC;CACO,CAAA;AAEd;AAiFA,MAAaA,WAAS,WAAqC,SAASA,SAClE,EACE,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IAKO;IACC;IACN,aAAU;IACV,WAAW,MAAM,oBAAoB,GAAG,aAAa,UAAU,uBAAuB,SAAS;IARjG,UAAA;KAgBG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAChF,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAQ,UAAA;MAAiB,CAAA;KAChC,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAErC,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAc,UAAA;MAAuB,CAAA;KAC/C,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC;AAgBD,MAAa,eAAe,WAA8C,SAAS,aACjF,EAAE,UAAU,aACZ,KACA;CACA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EACrF;CACE,CAAA;AAET,CAAC"}
1
+ {"version":3,"file":"dialog.js","names":["Dialog","AriaModalOverlay","AriaModal","AriaDialog","AriaHeading","AriaText","AriaButton"],"sources":["../../src/dialog/dialog.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, type ReactNode } from \"react\";\nimport {\n Button as AriaButton,\n Dialog as AriaDialog,\n Heading as AriaHeading,\n Modal as AriaModal,\n ModalOverlay as AriaModalOverlay,\n Text as AriaText,\n type ModalOverlayProps as AriaModalOverlayProps,\n} from \"react-aria-components\";\n\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * Dialog, ported from the legacy shadcn/Radix component onto React Aria's\n * Modal primitives.\n *\n * FOUR LEGACY PARTS COLLAPSE INTO TWO PROPS. The census (9 importing files,\n * but only **7** that render anything -- header.tsx and PollQuizCreator.tsx\n * import the parts and use none) reads: DialogContent 9, DialogTitle 8,\n * DialogHeader 7, DialogDescription 2. Those four always appear in the same\n * shape, so `title` and `description` are props of the dialog itself.\n *\n * That is not tidying. `title` is REQUIRED, which makes an unnamed dialog\n * impossible to write -- and the legacy shipped one: LiveQAOverlay.tsx\n * renders a DialogContent with no DialogTitle, so its `aria-labelledby`\n * points at an id that matches no element and the dialog is announced with\n * no name.\n *\n * It also fixes the dangling-description defect for free. Radix set\n * `aria-describedby` unconditionally from a `useId`, so seven of the nine\n * legacy dialogs referenced an id that did not exist. React Aria wires it\n * only when a description slot is actually rendered.\n *\n * `DialogPortal` and `DialogOverlay` are not ported: zero external call\n * sites, and React Aria's ModalOverlay does both jobs.\n *\n * NOT PORTED, and deliberately: the enter/exit animation. The legacy leans\n * on tailwindcss-animate's `animate-in` / `zoom-in-95` utilities, which this\n * repo does not ship, and the motion tokens are a separate task. React Aria\n * exposes `data-entering` / `data-exiting` for whoever adds them.\n *\n * Every string in the cva blocks below is a STATIC literal. Tailwind scans\n * source text, so a class assembled at runtime is invisible to it and would\n * simply never be emitted.\n */\nexport const dialogOverlayVariants = cva([\n // Centring by flex, not by `top-1/2 left-1/2 -translate-1/2`. The legacy's\n // translate pair is a bracket value that check:hardcoding rejects, and\n // flex centring needs no transform to undo.\n \"fixed inset-0 z-50 flex items-center justify-center\",\n // The inset that keeps a full-width panel off the viewport edge. The\n // legacy did this with max-w-[calc(100%-2rem)] -- also a bracket value.\n \"p-4\",\n \"bg-scrim\",\n]);\n\nexport const dialogPanelVariants = cva([\n // max-w-128 (32rem), NOT max-w-lg. `--container-lg` does not exist in this\n // repo -- the container scale holds only page-shell widths (max 75rem,\n // wide 90rem) -- so `max-w-lg` compiles to NOTHING and the panel spans the\n // viewport. It emitted no rule at all, tsc and ESLint were both blind to\n // it, and a screenshot is what caught it. 128 spacing units is 32rem,\n // exactly the legacy sm:max-w-lg.\n \"relative w-full max-w-128\",\n \"rounded-panel border-(length:--border-width) border-border p-6\",\n // bg-card, not bg-background: a dialog is a raised surface, and card is\n // the role that already steps up the ink ramp on dark, where a shadow made\n // of ink is invisible. Its contrast pairs are in the contract already.\n \"bg-card text-card-foreground shadow-modal\",\n \"flex flex-col gap-4\",\n // The reset in @seekalfred/styles is OPT-IN, so a consumer may load none:\n // content-box would add the 24px padding to max-w-lg, and an unstyled\n // document renders text in a serif. The Accordion shipped its panel that\n // way and the Alert overflowed by 46px.\n \"box-border font-sans\",\n // React Aria gives the dialog tabindex=-1 and focuses it when nothing\n // inside is autofocused, so it can receive a visible focus ring it does\n // not want.\n \"outline-none\",\n // A dialog taller than the viewport has to scroll, or its footer is\n // unreachable. max-h is on the panel rather than the overlay so the\n // padding above stays visible.\n \"max-h-full overflow-y-auto\",\n]);\n\nexport const dialogTitleVariants = cva([\n // leading-* AFTER text-*: tailwind-merge treats a later text-* as owning\n // line-height and silently strips a leading-* that precedes it.\n \"font-display text-lg leading-(--leading-tight) font-semibold\",\n // The user-agent <h2> ships a 1em block margin. The legacy was inside a\n // Preflight'd app and never met it.\n \"m-0\",\n]);\n\nexport const dialogDescriptionVariants = cva([\n \"text-sm leading-(--leading-normal) text-muted-foreground\",\n // Same: the user-agent <p> block margin.\n \"m-0\",\n]);\n\nexport const dialogFooterVariants = cva([\n // NOT flex-col-reverse. The legacy reversed the footer below `sm`, so a\n // DOM order of Cancel-then-Confirm rendered Confirm-then-Cancel on a\n // phone -- WCAG 1.3.2 Meaningful Sequence, and both alert-dialog call\n // sites hit it. Wrapping keeps narrow viewports working without lying\n // about the order.\n \"flex flex-wrap items-center justify-end gap-2\",\n]);\n\nconst dialogCloseVariants = cva([\n \"absolute end-4 top-4 inline-flex size-6 items-center justify-center\",\n \"rounded-2xs box-border cursor-pointer\",\n \"border-(length:--border-width) border-transparent bg-transparent\",\n \"text-muted-foreground\",\n // Fill on hover, a quick press to .96 and a spring release (see calendar-nav-motion).\n \"calendar-nav-motion\",\n // The legacy sat at opacity-70 until hover, dimming the only close\n // affordance below its already-marginal contrast. A token that is legible\n // at rest does the same job without that.\n \"data-[hovered]:bg-muted data-[hovered]:text-foreground\",\n // outline-solid is load-bearing: outline-none sets --tw-outline-style:none,\n // and the width utility resolves its style from that variable, so a focus\n // rule can apply a width and a colour and paint nothing. Button shipped\n // exactly that.\n \"outline-none\",\n \"data-[focus-visible]:outline-solid\",\n \"data-[focus-visible]:outline-(length:--focus-ring-width)\",\n \"data-[focus-visible]:outline-offset-(--focus-ring-offset)\",\n \"data-[focus-visible]:outline-(--color-ring)\",\n \"[&_svg]:pointer-events-none [&_svg]:size-4\",\n]);\n\n/**\n * A modal dialog.\n *\n * @example\n * <Dialog isOpen={open} onOpenChange={setOpen} title=\"Delete user\"\n * description=\"This cannot be undone.\">\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n * </Dialog>\n */\n/** A string title or description swaps as a whole when its text changes while the dialog is open. */\nfunction Swappable({ value, children }: { value: ReactNode; children: ReactNode }) {\n if (typeof value !== \"string\") return <>{children}</>;\n return (\n <SwapText swapKey={value} className=\"block\">\n {children}\n </SwapText>\n );\n}\n\nexport interface DialogProps\n extends Omit<AriaModalOverlayProps, \"className\" | \"style\" | \"children\"> {\n /**\n * The dialog's heading, and its accessible name.\n *\n * **Required.** The legacy allowed a dialog with no title and one call\n * site shipped that, producing an `aria-labelledby` pointing at nothing.\n * Making it a required prop turns that into something you cannot express.\n */\n title: ReactNode;\n /**\n * Rendered ABOVE the heading, inside the panel.\n *\n * Exists for Sheet's back control. A panel that replaces its own contents\n * rather than stacking a second modal needs a way back, and that control\n * cannot live in `title`: React Aria wires aria-labelledby to the heading,\n * so a button inside it becomes part of the dialog's accessible name --\n * \"Back Campaign details\" instead of \"Campaign details\".\n */\n beforeTitle?: ReactNode;\n /** Optional supporting text, wired to `aria-describedby` when present. */\n description?: ReactNode;\n /** The dialog body. */\n children?: ReactNode;\n /** Extra classes for the panel. Merged last, so they win conflicts. */\n className?: string;\n /**\n * Extra classes for the scrim, merged last.\n *\n * A centred modal never needs this. A Sheet does: it has to replace the\n * overlay's centring with an edge alignment, and it is the only way to\n * do that without a second copy of the whole overlay.\n */\n overlayClassName?: string;\n /**\n * Whether to render the corner close button.\n *\n * `true` by default. An `alertdialog` sets it `false`: a confirmation must\n * be answered, not dismissed silently.\n */\n closeButton?: boolean;\n /**\n * Accessible name for the close button.\n *\n * A prop because the legacy hard-coded the English string `Close` in an\n * `sr-only` span with no way to override it, which makes the component\n * unlocalisable.\n *\n * @default \"Close\"\n */\n closeLabel?: string;\n /** `alertdialog` for a destructive confirmation. @default \"dialog\" */\n role?: \"dialog\" | \"alertdialog\";\n /**\n * Whether clicking outside closes the dialog.\n *\n * `true`, which is NOT React Aria's default. React Aria ships\n * `isDismissable = false` (useOverlay.mjs:26); Radix, which the legacy\n * component wraps, closes on outside click unless told otherwise. Taking\n * the headless default here would have silently changed the behaviour of\n * all seven legacy call sites, and nothing in the suite would have\n * noticed.\n *\n * An `alertdialog` should set this `false`: a confirmation must be\n * answered, not clicked away.\n *\n * @default true\n */\n isDismissable?: boolean;\n /**\n * How the dialog comes in: `rise` fades the overlay (blurring the page) and lifts\n * the panel in on a spring; `none` leaves the entrance to the caller, as an\n * edge-anchored preset (Sheet) does.\n *\n * @default \"rise\"\n */\n entrance?: \"rise\" | \"none\";\n}\n\nexport const Dialog = forwardRef<HTMLElement, DialogProps>(function Dialog(\n {\n title,\n beforeTitle,\n description,\n children,\n className,\n closeButton = true,\n closeLabel = \"Close\",\n role = \"dialog\",\n isDismissable = true,\n overlayClassName,\n entrance = \"rise\",\n ...props\n },\n ref,\n) {\n return (\n // ModalOverlay explicitly, not a bare <Modal>. A bare Modal still renders\n // an overlay div, but className lands on the INNER element -- so the\n // scrim would be unstyleable and would paint nothing at all.\n <AriaModalOverlay\n {...props}\n isDismissable={isDismissable}\n data-slot=\"dialog-overlay\"\n className={cnAll(\n dialogOverlayVariants(),\n entrance === \"rise\" && \"dialog-overlay-motion\",\n overlayClassName,\n )}\n >\n <AriaModal className=\"contents\">\n <AriaDialog\n // Modal is a transparent pass-through (className=\"contents\", no\n // box of its own) and ModalOverlay is the scrim -- AriaDialog is\n // the only one of the three that is the visible panel, so the ref\n // goes here and not on either ancestor.\n ref={ref}\n role={role}\n data-slot=\"dialog\"\n className={cnAll(dialogPanelVariants(), entrance === \"rise\" && \"dialog-panel-motion\", className)}\n >\n {/*\n Above the heading, not inside it. Sheet's back control lives here:\n folding it into `title` would put a button inside the <h2> React\n Aria wires aria-labelledby to, so the dialog's accessible name\n would become \"Back Campaign details\".\n */}\n {beforeTitle}\n {/*\n slot=\"title\" is what React Aria wires aria-labelledby to. A plain\n <h2> here would render identically and leave the dialog unnamed.\n */}\n <AriaHeading slot=\"title\" data-slot=\"dialog-title\" className={dialogTitleVariants()}>\n <Swappable value={title}>{title}</Swappable>\n </AriaHeading>\n {description == null ? null : (\n <AriaText\n slot=\"description\"\n data-slot=\"dialog-description\"\n className={dialogDescriptionVariants()}\n >\n <Swappable value={description}>{description}</Swappable>\n </AriaText>\n )}\n {children}\n {closeButton ? (\n // slot=\"close\" is React Aria's own dismiss wiring; an onPress\n // that called onOpenChange by hand would not work inside an\n // uncontrolled DialogTrigger.\n <AriaButton slot=\"close\" aria-label={closeLabel} className={dialogCloseVariants()}>\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n ) : null}\n </AriaDialog>\n </AriaModal>\n </AriaModalOverlay>\n );\n});\n\n/**\n * Actions at the foot of a dialog.\n *\n * @example\n * <DialogFooter>\n * <Button slot=\"close\" variant=\"ghost\">Cancel</Button>\n * <Button variant=\"destructive\" onPress={remove}>Delete</Button>\n * </DialogFooter>\n */\nexport interface DialogFooterProps {\n children?: ReactNode;\n className?: string;\n}\n\nexport const DialogFooter = forwardRef<HTMLDivElement, DialogFooterProps>(function DialogFooter(\n { children, className },\n ref,\n) {\n return (\n <div ref={ref} data-slot=\"dialog-footer\" className={cn(dialogFooterVariants(), className)}>\n {children}\n </div>\n );\n});\n\n/**\n * Pairs a trigger with a dialog and owns the open state.\n *\n * Re-exported from React Aria unchanged. Only 3 of the 9 legacy call sites\n * use a trigger at all -- the rest drive `isOpen` themselves -- so wrapping\n * it would add a layer with nothing to do.\n */\nexport { DialogTrigger } from \"react-aria-components\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAa,wBAAwB,IAAI;CAIvC;CAGA;CACA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI;CAOrC;CACA;CAIA;CACA;CAKA;CAIA;CAIA;AACF,CAAC;AAED,MAAa,sBAAsB,IAAI,CAGrC,gEAGA,KACF,CAAC;AAED,MAAa,4BAA4B,IAAI,CAC3C,4DAEA,KACF,CAAC;AAED,MAAa,uBAAuB,IAAI,CAMtC,+CACF,CAAC;AAED,MAAM,sBAAsB,IAAI;CAC9B;CACA;CACA;CACA;CAEA;CAIA;CAKA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAS,UAAU,EAAE,OAAO,YAAuD;CACjF,IAAI,OAAO,UAAU,UAAU,OAAO,oBAAA,YAAA,EAAG,SAAW,CAAA;CACpD,OACE,oBAAC,UAAD;EAAU,SAAS;EAAO,WAAU;EACjC;CACO,CAAA;AAEd;AAiFA,MAAaA,WAAS,WAAqC,SAASA,SAClE,EACE,OACA,aACA,aACA,UACA,WACA,cAAc,MACd,aAAa,SACb,OAAO,UACP,gBAAgB,MAChB,kBACA,WAAW,QACX,GAAG,SAEL,KACA;CACA,OAIE,oBAACC,cAAD;EACE,GAAI;EACW;EACf,aAAU;EACV,WAAW,MACT,sBAAsB,GACtB,aAAa,UAAU,yBACvB,gBACF;EAEA,UAAA,oBAACC,OAAD;GAAW,WAAU;GACnB,UAAA,qBAACC,QAAD;IAKO;IACC;IACN,aAAU;IACV,WAAW,MAAM,oBAAoB,GAAG,aAAa,UAAU,uBAAuB,SAAS;IARjG,UAAA;KAgBG;KAKD,oBAACC,SAAD;MAAa,MAAK;MAAQ,aAAU;MAAe,WAAW,oBAAoB;MAChF,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAQ,UAAA;MAAiB,CAAA;KAChC,CAAA;KACZ,eAAe,OAAO,OACrB,oBAACC,MAAD;MACE,MAAK;MACL,aAAU;MACV,WAAW,0BAA0B;MAErC,UAAA,oBAAC,WAAD;OAAW,OAAO;OAAc,UAAA;MAAuB,CAAA;KAC/C,CAAA;KAEX;KACA,cAIC,oBAACC,QAAD;MAAY,MAAK;MAAQ,cAAY;MAAY,WAAW,oBAAoB;MAC9E,UAAA,oBAAC,OAAD;OACE,eAAY;OACZ,SAAQ;OACR,MAAK;OACL,QAAO;OACP,aAAY;OACZ,eAAc;OAEd,UAAA,oBAAC,QAAD,EAAM,GAAE,qBAAsB,CAAA;MAC3B,CAAA;KACK,CAAA,IACV;IACM;;EACH,CAAA;CACK,CAAA;AAEtB,CAAC;AAgBD,MAAa,eAAe,WAA8C,SAAS,aACjF,EAAE,UAAU,aACZ,KACA;CACA,OACE,oBAAC,OAAD;EAAU;EAAK,aAAU;EAAgB,WAAW,GAAG,qBAAqB,GAAG,SAAS;EACrF;CACE,CAAA;AAET,CAAC"}
@@ -68,6 +68,23 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "
68
68
  image?: ReactNode;
69
69
  /** A next step under the text: a button or a link. Omitted from the DOM when absent. */
70
70
  action?: ReactNode;
71
+ /**
72
+ * Draws the empty state as a block of its own. `dashed` gives it a dashed
73
+ * border, a rounded corner and the muted surface, for a placeholder where a
74
+ * chart or a panel will go. `none` is the bare box, as before.
75
+ *
76
+ * @default "none"
77
+ */
78
+ frame?: "none" | "dashed";
79
+ /**
80
+ * Where the content sits. `center` is the page-level empty state, as before.
81
+ * `start` aligns icon, text and action to the leading edge, for a block in a
82
+ * layout rather than a whole page; with it, and no icon or image, the title
83
+ * also drops its top margin.
84
+ *
85
+ * @default "center"
86
+ */
87
+ align?: "center" | "start";
71
88
  /** Extra classes for the box. Merged last, so they win conflicts with ours. */
72
89
  className?: string;
73
90
  }
@@ -21,7 +21,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
21
21
  * simply never be emitted.
22
22
  */
23
23
  const emptyStateVariants = cva(["flex flex-col items-center px-5 py-12 text-center", "box-border font-sans text-foreground"]);
24
- const emptyStateTitleVariants = cva("m-0 mt-5 text-base leading-(--leading-normal) font-medium text-foreground");
24
+ const emptyStateTitleVariants = cva("m-0 mt-5 font-display text-base leading-(--leading-normal) font-medium text-foreground");
25
25
  const emptyStateTextVariants = cva("mx-auto mt-0 mb-0 max-w-72 text-sm leading-(--leading-normal) text-pretty text-text-secondary");
26
26
  const emptyStateActionVariants = cva("mt-5 flex flex-wrap justify-center gap-3");
27
27
  /**
@@ -36,7 +36,7 @@ function iconKey(icon) {
36
36
  const type = icon.type;
37
37
  return typeof type === "string" ? type : type.displayName ?? type.name ?? "icon";
38
38
  }
39
- const EmptyState = forwardRef(function EmptyState({ title, description, children, icon, image, action, className, ...props }, ref) {
39
+ const EmptyState = forwardRef(function EmptyState({ title, description, children, icon, image, action, frame: frameStyle = "none", align = "center", className, ...props }, ref) {
40
40
  const frame = useRef(null);
41
41
  const copy = useRef(null);
42
42
  const message = description ?? children;
@@ -51,11 +51,13 @@ const EmptyState = forwardRef(function EmptyState({ title, description, children
51
51
  ref,
52
52
  ...props,
53
53
  "data-slot": "empty-state",
54
- className: cn(emptyStateVariants(), className),
54
+ "data-frame": frameStyle === "none" ? void 0 : frameStyle,
55
+ "data-align": align === "center" ? void 0 : align,
56
+ className: cn(cnAll(emptyStateVariants(), frameStyle === "dashed" && "rounded-panel border-(length:--border-width) border-dashed border-border bg-muted", align === "start" && "items-start text-start"), className),
55
57
  children: [
56
58
  !hasImage ? null : /* @__PURE__ */ jsx("div", {
57
59
  "data-slot": "empty-state-image",
58
- className: emptyStateImageVariants(),
60
+ className: cnAll(emptyStateImageVariants(), align === "start" && "justify-start"),
59
61
  children: image
60
62
  }),
61
63
  icon === void 0 || icon === null ? null : /* @__PURE__ */ jsx("div", {
@@ -78,18 +80,18 @@ const EmptyState = forwardRef(function EmptyState({ title, description, children
78
80
  className: "flow-root",
79
81
  children: [title == null ? null : /* @__PURE__ */ jsx("h3", {
80
82
  "data-slot": "empty-state-title",
81
- className: emptyStateTitleVariants(),
83
+ className: cnAll(emptyStateTitleVariants(), align === "start" && icon == null && !hasImage && "mt-0"),
82
84
  children: text(title)
83
85
  }), message == null ? null : /* @__PURE__ */ jsx("p", {
84
86
  "data-slot": "empty-state-text",
85
- className: cnAll(emptyStateTextVariants(), title != null ? "mt-2" : (icon != null || hasImage) && "mt-5"),
87
+ className: cnAll(emptyStateTextVariants(), align === "start" && "mx-0", title != null ? "mt-2" : (icon != null || hasImage) && "mt-5"),
86
88
  children: text(message)
87
89
  })]
88
90
  })
89
91
  }),
90
92
  action == null ? null : /* @__PURE__ */ jsx("div", {
91
93
  "data-slot": "empty-state-action",
92
- className: emptyStateActionVariants(),
94
+ className: cnAll(emptyStateActionVariants(), align === "start" && "justify-start"),
93
95
  children: action
94
96
  })
95
97
  ]