@seekalfred/primitives 0.7.3 → 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 (149) hide show
  1. package/README.md +315 -4
  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.d.ts +14 -0
  6. package/dist/alert-dialog/alert-dialog.js +52 -27
  7. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  8. package/dist/announcement-bar/announcement-bar.d.ts +15 -4
  9. package/dist/announcement-bar/announcement-bar.js +31 -6
  10. package/dist/announcement-bar/announcement-bar.js.map +1 -1
  11. package/dist/avatar/avatar.d.ts +7 -0
  12. package/dist/avatar/avatar.js +33 -12
  13. package/dist/avatar/avatar.js.map +1 -1
  14. package/dist/badge/badge.d.ts +16 -1
  15. package/dist/badge/badge.js +12 -5
  16. package/dist/badge/badge.js.map +1 -1
  17. package/dist/bar-chart/bar-chart-series.d.ts +75 -0
  18. package/dist/bar-chart/bar-chart-series.js +401 -0
  19. package/dist/bar-chart/bar-chart-series.js.map +1 -0
  20. package/dist/bar-chart/bar-chart.d.ts +25 -1
  21. package/dist/bar-chart/bar-chart.js +26 -1
  22. package/dist/bar-chart/bar-chart.js.map +1 -1
  23. package/dist/bar-chart/index.d.ts +2 -1
  24. package/dist/button/button.d.ts +24 -2
  25. package/dist/button/button.js +62 -13
  26. package/dist/button/button.js.map +1 -1
  27. package/dist/card/card.js +1 -1
  28. package/dist/card/card.js.map +1 -1
  29. package/dist/checkbox/checkbox.d.ts +25 -1
  30. package/dist/checkbox/checkbox.js +103 -28
  31. package/dist/checkbox/checkbox.js.map +1 -1
  32. package/dist/checkbox/index.d.ts +2 -2
  33. package/dist/checkbox/index.js +2 -2
  34. package/dist/chip-group/chip-group.d.ts +11 -0
  35. package/dist/chip-group/chip-group.js +50 -34
  36. package/dist/chip-group/chip-group.js.map +1 -1
  37. package/dist/chip-group/index.d.ts +2 -2
  38. package/dist/code-block/code-block.d.ts +45 -0
  39. package/dist/code-block/code-block.js +74 -0
  40. package/dist/code-block/code-block.js.map +1 -0
  41. package/dist/code-block/index.d.ts +2 -0
  42. package/dist/code-block/index.js +2 -0
  43. package/dist/command-palette/command-palette.d.ts +128 -0
  44. package/dist/command-palette/command-palette.js +371 -0
  45. package/dist/command-palette/command-palette.js.map +1 -0
  46. package/dist/command-palette/index.d.ts +2 -0
  47. package/dist/command-palette/index.js +2 -0
  48. package/dist/copy-button/copy-button.d.ts +19 -3
  49. package/dist/copy-button/copy-button.js +7 -4
  50. package/dist/copy-button/copy-button.js.map +1 -1
  51. package/dist/dialog/dialog.js +1 -1
  52. package/dist/dialog/dialog.js.map +1 -1
  53. package/dist/empty-state/empty-state.d.ts +17 -0
  54. package/dist/empty-state/empty-state.js +9 -7
  55. package/dist/empty-state/empty-state.js.map +1 -1
  56. package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
  57. package/dist/file-drop-zone/file-drop-zone.js +26 -8
  58. package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
  59. package/dist/gauge/gauge.d.ts +18 -1
  60. package/dist/gauge/gauge.js +35 -21
  61. package/dist/gauge/gauge.js.map +1 -1
  62. package/dist/gauge/index.d.ts +2 -2
  63. package/dist/index.d.ts +14 -9
  64. package/dist/index.js +14 -10
  65. package/dist/info-tooltip/index.d.ts +2 -0
  66. package/dist/info-tooltip/index.js +2 -0
  67. package/dist/info-tooltip/info-tooltip.d.ts +35 -0
  68. package/dist/info-tooltip/info-tooltip.js +49 -0
  69. package/dist/info-tooltip/info-tooltip.js.map +1 -0
  70. package/dist/input/input.d.ts +7 -0
  71. package/dist/input/input.js +4 -2
  72. package/dist/input/input.js.map +1 -1
  73. package/dist/lib/field-shell.js +2 -6
  74. package/dist/lib/field-shell.js.map +1 -1
  75. package/dist/lib/required-marker.js +20 -0
  76. package/dist/lib/required-marker.js.map +1 -0
  77. package/dist/lib/selection-control.js +32 -1
  78. package/dist/lib/selection-control.js.map +1 -1
  79. package/dist/line-chart/index.d.ts +2 -2
  80. package/dist/line-chart/line-chart.d.ts +106 -3
  81. package/dist/line-chart/line-chart.js +185 -46
  82. package/dist/line-chart/line-chart.js.map +1 -1
  83. package/dist/mention-input/mention-input.js +1 -1
  84. package/dist/money-input/index.d.ts +2 -2
  85. package/dist/money-input/money-input.d.ts +14 -0
  86. package/dist/money-input/money-input.js +17 -7
  87. package/dist/money-input/money-input.js.map +1 -1
  88. package/dist/morph-select/morph-select.d.ts +8 -1
  89. package/dist/morph-select/morph-select.js +8 -5
  90. package/dist/morph-select/morph-select.js.map +1 -1
  91. package/dist/multi-select/multi-select.d.ts +31 -0
  92. package/dist/multi-select/multi-select.js +123 -6
  93. package/dist/multi-select/multi-select.js.map +1 -1
  94. package/dist/number-field/number-field.js +1 -1
  95. package/dist/password-field/password-field.js +1 -1
  96. package/dist/password-strength/password-strength.js +1 -1
  97. package/dist/phone-input/phone-input.js +1 -1
  98. package/dist/popover/popover.js +1 -1
  99. package/dist/popover/popover.js.map +1 -1
  100. package/dist/progress/index.d.ts +2 -2
  101. package/dist/progress/index.js +2 -2
  102. package/dist/progress/progress.d.ts +53 -2
  103. package/dist/progress/progress.js +146 -36
  104. package/dist/progress/progress.js.map +1 -1
  105. package/dist/radio-group/radio-group.d.ts +12 -3
  106. package/dist/radio-group/radio-group.js +62 -52
  107. package/dist/radio-group/radio-group.js.map +1 -1
  108. package/dist/rich-text-editor/rich-text-editor.js +1 -1
  109. package/dist/scroll-area/scroll-area.d.ts +1 -1
  110. package/dist/scrub-input/scrub-input.d.ts +11 -1
  111. package/dist/scrub-input/scrub-input.js +22 -10
  112. package/dist/scrub-input/scrub-input.js.map +1 -1
  113. package/dist/search-field/search-field.js +1 -1
  114. package/dist/select/select.d.ts +10 -1
  115. package/dist/select/select.js +4 -3
  116. package/dist/select/select.js.map +1 -1
  117. package/dist/separator/separator.d.ts +1 -1
  118. package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
  119. package/dist/sparkline/sparkline.d.ts +27 -2
  120. package/dist/sparkline/sparkline.js +115 -18
  121. package/dist/sparkline/sparkline.js.map +1 -1
  122. package/dist/stat-card/index.d.ts +2 -2
  123. package/dist/stat-card/index.js +2 -2
  124. package/dist/stat-card/stat-card.d.ts +32 -1
  125. package/dist/stat-card/stat-card.js +52 -9
  126. package/dist/stat-card/stat-card.js.map +1 -1
  127. package/dist/styles.css +1 -1
  128. package/dist/table/index.d.ts +2 -2
  129. package/dist/table/index.js +2 -2
  130. package/dist/table/table.d.ts +211 -3
  131. package/dist/table/table.js +473 -93
  132. package/dist/table/table.js.map +1 -1
  133. package/dist/tabs/tabs.d.ts +4 -2
  134. package/dist/tabs/tabs.js +7 -5
  135. package/dist/tabs/tabs.js.map +1 -1
  136. package/dist/tag-input/tag-input.js +1 -1
  137. package/dist/text-shimmer/text-shimmer.d.ts +18 -2
  138. package/dist/text-shimmer/text-shimmer.js +5 -2
  139. package/dist/text-shimmer/text-shimmer.js.map +1 -1
  140. package/dist/textarea/textarea.d.ts +7 -0
  141. package/dist/textarea/textarea.js +4 -2
  142. package/dist/textarea/textarea.js.map +1 -1
  143. package/dist/toast/toast.d.ts +2 -2
  144. package/dist/toggle-button/index.d.ts +2 -0
  145. package/dist/toggle-button/index.js +2 -0
  146. package/dist/toggle-button/toggle-button.d.ts +93 -0
  147. package/dist/toggle-button/toggle-button.js +199 -0
  148. package/dist/toggle-button/toggle-button.js.map +1 -0
  149. 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 };
@@ -2,9 +2,23 @@
2
2
  export interface CopyButtonProps {
3
3
  /** What goes on the clipboard. */
4
4
  value: string;
5
- /** The label at rest, and the button's accessible name. @default "Copy" */
5
+ /**
6
+ * The label at rest. Unless `aria-label` is set it is also the button's accessible name.
7
+ * @default "Copy"
8
+ */
6
9
  children?: string;
7
- /** Only the icon, a square button. The label is still its accessible name. */
10
+ /**
11
+ * An accessible name that differs from the visible label, for when the short label leans on its
12
+ * surroundings: visible "Copy" next to a code block, named "Copy code". It also prefixes the
13
+ * live-region announcement ("Copy code: Copied"). It stays constant while the visible label
14
+ * shows "Copied" or "Failed".
15
+ *
16
+ * Keep the visible label inside it, as written and ideally at the start ("Copy" → "Copy code",
17
+ * "Copy page" → "Copy page draft"), so a speech-input user can say what they see
18
+ * (WCAG 2.5.3 Label in Name). Unset or empty, the accessible name is `children`, as before.
19
+ */
20
+ "aria-label"?: string;
21
+ /** Only the icon, a square button. The label (or `aria-label`) is still its accessible name. */
8
22
  iconOnly?: boolean;
9
23
  /**
10
24
  * `plain` has no border until hovered. `primary` is the filled brand button, for a copy that is the
@@ -34,11 +48,13 @@ export interface CopyButtonProps {
34
48
  * its label cell reserves the widest of the three texts.
35
49
  *
36
50
  * It goes back to rest by itself after a moment, and a live region announces the
37
- * result. The accessible name is always the resting label.
51
+ * result. The accessible name is the resting label, or `aria-label` when the visible
52
+ * label is too short to stand alone; it never changes with the state.
38
53
  *
39
54
  * @example
40
55
  * <CopyButton value={token} />
41
56
  * <CopyButton value={url} iconOnly variant="plain" />
57
+ * <CopyButton value={code} aria-label="Copy code">Copy</CopyButton>
42
58
  * <CopyButton value={ticket} variant="primary" size="md" onError={() => toast("Copy failed")}>
43
59
  * Copy as ticket
44
60
  * </CopyButton>
@@ -82,17 +82,20 @@ function StateIcon({ state }) {
82
82
  * its label cell reserves the widest of the three texts.
83
83
  *
84
84
  * It goes back to rest by itself after a moment, and a live region announces the
85
- * result. The accessible name is always the resting label.
85
+ * result. The accessible name is the resting label, or `aria-label` when the visible
86
+ * label is too short to stand alone; it never changes with the state.
86
87
  *
87
88
  * @example
88
89
  * <CopyButton value={token} />
89
90
  * <CopyButton value={url} iconOnly variant="plain" />
91
+ * <CopyButton value={code} aria-label="Copy code">Copy</CopyButton>
90
92
  * <CopyButton value={ticket} variant="primary" size="md" onError={() => toast("Copy failed")}>
91
93
  * Copy as ticket
92
94
  * </CopyButton>
93
95
  */
94
- const CopyButton = forwardRef(function CopyButton({ value, children: label = "Copy", iconOnly = false, variant = "outline", size = "sm", isDisabled, onCopied, onError, className }, ref) {
96
+ const CopyButton = forwardRef(function CopyButton({ value, children: label = "Copy", "aria-label": ariaLabel, iconOnly = false, variant = "outline", size = "sm", isDisabled, onCopied, onError, className }, ref) {
95
97
  const { state, copy } = useCopyFeedback();
98
+ const name = ariaLabel || label;
96
99
  const text = state === "copied" ? "Copied" : state === "error" ? "Failed" : label;
97
100
  const pressStart = (event) => {
98
101
  event.target.style.setProperty("--press-scale", iconOnly ? "0.96" : "0.97");
@@ -106,7 +109,7 @@ const CopyButton = forwardRef(function CopyButton({ value, children: label = "Co
106
109
  ref,
107
110
  "data-slot": "button",
108
111
  "data-copy-state": state,
109
- "aria-label": label,
112
+ "aria-label": name,
110
113
  isDisabled,
111
114
  onPressStart: pressStart,
112
115
  onPress: () => {
@@ -146,7 +149,7 @@ const CopyButton = forwardRef(function CopyButton({ value, children: label = "Co
146
149
  className: "sr-only",
147
150
  role: "status",
148
151
  "aria-live": "polite",
149
- children: state === "idle" ? "" : state === "error" ? label + ": Could not copy" : label + ": Copied"
152
+ children: state === "idle" ? "" : state === "error" ? name + ": Could not copy" : name + ": Copied"
150
153
  })] });
151
154
  });
152
155
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"copy-button.js","names":["AriaButton"],"sources":["../../src/copy-button/copy-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Button as AriaButton, type PressEvent } from \"react-aria-components\";\n\nimport { buttonVariants } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { IconSwap } from \"../lib/icon-swap.js\";\nimport { MorphText } from \"../lib/morph-text.js\";\nimport { callSafely, useCopyFeedback, type CopyFeedbackState } from \"../lib/use-copy-feedback.js\";\n\nexport interface CopyButtonProps {\n /** What goes on the clipboard. */\n value: string;\n /** The label at rest, and the button's accessible name. @default \"Copy\" */\n children?: string;\n /** Only the icon, a square button. The label is still its accessible name. */\n iconOnly?: boolean;\n /**\n * `plain` has no border until hovered. `primary` is the filled brand button, for a copy that is the\n * main action of its row or dialog. @default \"outline\"\n */\n variant?: \"outline\" | \"plain\" | \"primary\";\n /**\n * Control height, from the density scale: `sm` sits in a row, `md` matches a default Button.\n * @default \"sm\"\n */\n size?: \"sm\" | \"md\";\n isDisabled?: boolean;\n /** Called once the text has reached the clipboard. */\n onCopied?: () => void;\n /**\n * Called when the clipboard refused the text, with what it rejected with. The button already shows\n * \"Failed\" and announces it; use this to keep a toast or log of your own.\n */\n onError?: (error: unknown) => void;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nconst ICON = \"size-4\";\n\n/** CopyButton's variant names, mapped onto Button's. */\nconst BUTTON_VARIANT = {\n outline: \"outlineNeutral\",\n plain: \"ghost\",\n primary: \"primary\",\n} as const;\n\n/** An icon-only button is square: as wide as its rung is tall. */\nconst ICON_ONLY_WIDTH = {\n sm: \" w-(--density-control-h-sm) px-0\",\n md: \" w-(--density-control-h-md) px-0\",\n} as const;\n\n/** The result icon's tone on a page-surface button; at rest it takes the label's ink. */\nconst RESULT_TONE: Record<CopyFeedbackState, string | undefined> = {\n idle: undefined,\n copied: \"text-success-strong\",\n error: \"text-destructive-strong\",\n};\n\n/** The tick draws itself from its short stroke, the way a hand would write it: quick to start, then easing into place. */\nfunction DrawnCheck() {\n return (\n <svg\n className={ICON}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 12l5 5L20 6\"\n pathLength={1}\n className=\"[stroke-dasharray:1] motion-safe:animate-check-draw-calm\"\n />\n </svg>\n );\n}\n\nfunction StateIcon({ state }: { state: CopyFeedbackState }) {\n if (state === \"copied\") return <DrawnCheck />;\n const common = {\n className: ICON,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 1.75,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n \"aria-hidden\": true,\n } as const;\n if (state === \"error\") {\n return (\n <svg {...common}>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 8v4M12 16h.01\" />\n </svg>\n );\n }\n return (\n <svg {...common}>\n <rect x=\"8\" y=\"8\" width=\"14\" height=\"14\" rx=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </svg>\n );\n}\n\n/**\n * Copies a value and says so, calmly. The icon trades places in one soft breath\n * (copy, then a tick that draws itself, or an alert if the clipboard refused)\n * while the label's changed letters rise in, and the button never changes width:\n * its label cell reserves the widest of the three texts.\n *\n * It goes back to rest by itself after a moment, and a live region announces the\n * result. The accessible name is always the resting label.\n *\n * @example\n * <CopyButton value={token} />\n * <CopyButton value={url} iconOnly variant=\"plain\" />\n * <CopyButton value={ticket} variant=\"primary\" size=\"md\" onError={() => toast(\"Copy failed\")}>\n * Copy as ticket\n * </CopyButton>\n */\nexport const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(function CopyButton(\n {\n value,\n children: label = \"Copy\",\n iconOnly = false,\n variant = \"outline\",\n size = \"sm\",\n isDisabled,\n onCopied,\n onError,\n className,\n },\n ref,\n) {\n const { state, copy } = useCopyFeedback();\n const text = state === \"copied\" ? \"Copied\" : state === \"error\" ? \"Failed\" : label;\n\n // Not width-based like Button's press: an icon button presses a little deeper, every other one the same.\n const pressStart = (event: PressEvent) => {\n (event.target as HTMLElement).style.setProperty(\"--press-scale\", iconOnly ? \"0.96\" : \"0.97\");\n };\n\n const classes = cn(\n buttonVariants({ variant: BUTTON_VARIANT[variant], size }) +\n \" [--release-duration:var(--duration-gentle)] [--release-ease:var(--ease-gentle)]\" +\n (iconOnly ? ICON_ONLY_WIDTH[size] : \"\"),\n className,\n );\n // On the brand fill the result icon keeps the label's ink: the success and destructive tones are set\n // against the page surface, not the orange.\n const onBrandFill = variant === \"primary\";\n\n return (\n <>\n <AriaButton\n ref={ref}\n data-slot=\"button\"\n data-copy-state={state}\n aria-label={label}\n isDisabled={isDisabled}\n onPressStart={pressStart}\n onPress={() => {\n void copy(value, onError).then((ok) => {\n if (ok) callSafely(() => onCopied?.());\n });\n }}\n className={classes}\n >\n <IconSwap iconKey={state} calm className={cn(ICON, \"relative\")}>\n <span className={onBrandFill ? undefined : RESULT_TONE[state]}>\n <StateIcon state={state} />\n </span>\n </IconSwap>\n {iconOnly ? null : (\n // The cell reserves the widest state, so nothing moves but the letters. The inline padding gives blurred\n // edge letters room before the horizontal clip; the negative margin keeps the box the same size.\n <span\n className=\"-mx-1 grid min-w-0 overflow-x-clip px-1 text-start whitespace-nowrap\"\n aria-hidden=\"true\"\n >\n {[label, \"Copied\", \"Failed\"].map((measure) => (\n <span key={measure} className=\"invisible col-start-1 row-start-1\">\n {measure}\n </span>\n ))}\n <span className=\"col-start-1 row-start-1 justify-self-start\">\n <MorphText text={text} morphWidth={false} calm />\n </span>\n </span>\n )}\n </AriaButton>\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {state === \"idle\"\n ? \"\"\n : state === \"error\"\n ? label + \": Could not copy\"\n : label + \": Copied\"}\n </span>\n </>\n );\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"copy-button.js","names":["AriaButton"],"sources":["../../src/copy-button/copy-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Button as AriaButton, type PressEvent } from \"react-aria-components\";\n\nimport { buttonVariants } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { IconSwap } from \"../lib/icon-swap.js\";\nimport { MorphText } from \"../lib/morph-text.js\";\nimport { callSafely, useCopyFeedback, type CopyFeedbackState } from \"../lib/use-copy-feedback.js\";\n\nexport interface CopyButtonProps {\n /** What goes on the clipboard. */\n value: string;\n /**\n * The label at rest. Unless `aria-label` is set it is also the button's accessible name.\n * @default \"Copy\"\n */\n children?: string;\n /**\n * An accessible name that differs from the visible label, for when the short label leans on its\n * surroundings: visible \"Copy\" next to a code block, named \"Copy code\". It also prefixes the\n * live-region announcement (\"Copy code: Copied\"). It stays constant while the visible label\n * shows \"Copied\" or \"Failed\".\n *\n * Keep the visible label inside it, as written and ideally at the start (\"Copy\" → \"Copy code\",\n * \"Copy page\" → \"Copy page draft\"), so a speech-input user can say what they see\n * (WCAG 2.5.3 Label in Name). Unset or empty, the accessible name is `children`, as before.\n */\n \"aria-label\"?: string;\n /** Only the icon, a square button. The label (or `aria-label`) is still its accessible name. */\n iconOnly?: boolean;\n /**\n * `plain` has no border until hovered. `primary` is the filled brand button, for a copy that is the\n * main action of its row or dialog. @default \"outline\"\n */\n variant?: \"outline\" | \"plain\" | \"primary\";\n /**\n * Control height, from the density scale: `sm` sits in a row, `md` matches a default Button.\n * @default \"sm\"\n */\n size?: \"sm\" | \"md\";\n isDisabled?: boolean;\n /** Called once the text has reached the clipboard. */\n onCopied?: () => void;\n /**\n * Called when the clipboard refused the text, with what it rejected with. The button already shows\n * \"Failed\" and announces it; use this to keep a toast or log of your own.\n */\n onError?: (error: unknown) => void;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nconst ICON = \"size-4\";\n\n/** CopyButton's variant names, mapped onto Button's. */\nconst BUTTON_VARIANT = {\n outline: \"outlineNeutral\",\n plain: \"ghost\",\n primary: \"primary\",\n} as const;\n\n/** An icon-only button is square: as wide as its rung is tall. */\nconst ICON_ONLY_WIDTH = {\n sm: \" w-(--density-control-h-sm) px-0\",\n md: \" w-(--density-control-h-md) px-0\",\n} as const;\n\n/** The result icon's tone on a page-surface button; at rest it takes the label's ink. */\nconst RESULT_TONE: Record<CopyFeedbackState, string | undefined> = {\n idle: undefined,\n copied: \"text-success-strong\",\n error: \"text-destructive-strong\",\n};\n\n/** The tick draws itself from its short stroke, the way a hand would write it: quick to start, then easing into place. */\nfunction DrawnCheck() {\n return (\n <svg\n className={ICON}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 12l5 5L20 6\"\n pathLength={1}\n className=\"[stroke-dasharray:1] motion-safe:animate-check-draw-calm\"\n />\n </svg>\n );\n}\n\nfunction StateIcon({ state }: { state: CopyFeedbackState }) {\n if (state === \"copied\") return <DrawnCheck />;\n const common = {\n className: ICON,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 1.75,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n \"aria-hidden\": true,\n } as const;\n if (state === \"error\") {\n return (\n <svg {...common}>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 8v4M12 16h.01\" />\n </svg>\n );\n }\n return (\n <svg {...common}>\n <rect x=\"8\" y=\"8\" width=\"14\" height=\"14\" rx=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </svg>\n );\n}\n\n/**\n * Copies a value and says so, calmly. The icon trades places in one soft breath\n * (copy, then a tick that draws itself, or an alert if the clipboard refused)\n * while the label's changed letters rise in, and the button never changes width:\n * its label cell reserves the widest of the three texts.\n *\n * It goes back to rest by itself after a moment, and a live region announces the\n * result. The accessible name is the resting label, or `aria-label` when the visible\n * label is too short to stand alone; it never changes with the state.\n *\n * @example\n * <CopyButton value={token} />\n * <CopyButton value={url} iconOnly variant=\"plain\" />\n * <CopyButton value={code} aria-label=\"Copy code\">Copy</CopyButton>\n * <CopyButton value={ticket} variant=\"primary\" size=\"md\" onError={() => toast(\"Copy failed\")}>\n * Copy as ticket\n * </CopyButton>\n */\nexport const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(function CopyButton(\n {\n value,\n children: label = \"Copy\",\n \"aria-label\": ariaLabel,\n iconOnly = false,\n variant = \"outline\",\n size = \"sm\",\n isDisabled,\n onCopied,\n onError,\n className,\n },\n ref,\n) {\n const { state, copy } = useCopyFeedback();\n // Empty falls back too: an empty aria-label would leave the button with no name at all.\n const name = ariaLabel || label;\n const text = state === \"copied\" ? \"Copied\" : state === \"error\" ? \"Failed\" : label;\n\n // Not width-based like Button's press: an icon button presses a little deeper, every other one the same.\n const pressStart = (event: PressEvent) => {\n (event.target as HTMLElement).style.setProperty(\"--press-scale\", iconOnly ? \"0.96\" : \"0.97\");\n };\n\n const classes = cn(\n buttonVariants({ variant: BUTTON_VARIANT[variant], size }) +\n \" [--release-duration:var(--duration-gentle)] [--release-ease:var(--ease-gentle)]\" +\n (iconOnly ? ICON_ONLY_WIDTH[size] : \"\"),\n className,\n );\n // On the brand fill the result icon keeps the label's ink: the success and destructive tones are set\n // against the page surface, not the orange.\n const onBrandFill = variant === \"primary\";\n\n return (\n <>\n <AriaButton\n ref={ref}\n data-slot=\"button\"\n data-copy-state={state}\n aria-label={name}\n isDisabled={isDisabled}\n onPressStart={pressStart}\n onPress={() => {\n void copy(value, onError).then((ok) => {\n if (ok) callSafely(() => onCopied?.());\n });\n }}\n className={classes}\n >\n <IconSwap iconKey={state} calm className={cn(ICON, \"relative\")}>\n <span className={onBrandFill ? undefined : RESULT_TONE[state]}>\n <StateIcon state={state} />\n </span>\n </IconSwap>\n {iconOnly ? null : (\n // The cell reserves the widest state, so nothing moves but the letters. The inline padding gives blurred\n // edge letters room before the horizontal clip; the negative margin keeps the box the same size.\n <span\n className=\"-mx-1 grid min-w-0 overflow-x-clip px-1 text-start whitespace-nowrap\"\n aria-hidden=\"true\"\n >\n {[label, \"Copied\", \"Failed\"].map((measure) => (\n <span key={measure} className=\"invisible col-start-1 row-start-1\">\n {measure}\n </span>\n ))}\n <span className=\"col-start-1 row-start-1 justify-self-start\">\n <MorphText text={text} morphWidth={false} calm />\n </span>\n </span>\n )}\n </AriaButton>\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {state === \"idle\" ? \"\" : state === \"error\" ? name + \": Could not copy\" : name + \": Copied\"}\n </span>\n </>\n );\n});\n"],"mappings":"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"}
@@ -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([