@shadow-js/ui 0.0.0-stage → 1.0.1

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 (190) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2281 -0
  5. package/dist/dev/cli/schema.d.ts +71 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +139 -0
  9. package/dist/dev/locales/ar.js +127 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +139 -0
  12. package/dist/dev/locales/en.js +127 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +27 -0
  15. package/dist/dev/registry/utils/utils.js +26 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +185 -0
  18. package/dist/dev/theme/theme-logic.js +183 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +88 -0
  21. package/dist/dev/theme/theme-provider.js +149 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. package/styles/typeset.css +251 -0
@@ -0,0 +1,757 @@
1
+ import {
2
+ type Accessor,
3
+ onCleanup,
4
+ splitProps,
5
+ untrack,
6
+ useEffect,
7
+ useSignal,
8
+ } from "@shadow-js/core";
9
+ import {
10
+ type ComboboxContentProps,
11
+ type ComboboxEmptyProps,
12
+ type ComboboxFilterItem,
13
+ type ComboboxGroupProps,
14
+ type ComboboxInputProps,
15
+ type ComboboxItemProps,
16
+ Combobox as ComboboxPrimitive,
17
+ type ComboboxSeparatorProps,
18
+ createPartContext,
19
+ type DialogProps,
20
+ normalizeSearchText,
21
+ useControllableState,
22
+ } from "@shadow-js/primitives";
23
+ import type { ComponentProps, JSX, JsxChild } from "@shadow-js/web";
24
+ import { Show } from "@shadow-js/web";
25
+ import {
26
+ Dialog,
27
+ DialogContent,
28
+ DialogDescription,
29
+ DialogHeader,
30
+ DialogTitle,
31
+ } from "@/components/ui/dialog";
32
+ import { SearchIcon } from "@/components/ui/icons";
33
+ import { useUiStrings } from "@/lib/ui-strings";
34
+ import { cn } from "@/lib/utils";
35
+
36
+ /** What an item tells the command about itself, so the shared filter and `onSelect` can find it. */
37
+ interface ItemMeta {
38
+ /** Whether the application gave a `value` (it is then what the search matches, as in cmdk). */
39
+ readonly explicit: () => boolean;
40
+ readonly keywords: () => readonly string[];
41
+ readonly select: () => void;
42
+ }
43
+
44
+ /** Where `CommandInput` hands its controlled `value` and change callback to the command. */
45
+ interface SearchSource {
46
+ value?: (() => string | undefined) | undefined;
47
+ change?: ((text: string) => void) | undefined;
48
+ }
49
+
50
+ interface CommandContextValue {
51
+ readonly label: Accessor<string | undefined>;
52
+ readonly source: SearchSource;
53
+ /** Tells the command that `source` changed (the input registered itself). */
54
+ readonly touch: () => void;
55
+ readonly register: (value: string, meta: ItemMeta) => () => void;
56
+ /** A value for an item that has none, unique within this command. */
57
+ readonly nextValue: () => string;
58
+ /** Forwards Escape from the list to the application. */
59
+ readonly escape: (event: KeyboardEvent) => void;
60
+ }
61
+
62
+ const commandContext = createPartContext<CommandContextValue>("Command");
63
+
64
+ /**
65
+ * The score function of {@link CommandProps.filter}: a number above `0` keeps the item.
66
+ * @since 1.0.0 * @see {@link CommandProps}
67
+ */
68
+ export type CommandFilter = (value: string, search: string, keywords?: readonly string[]) => number;
69
+
70
+ /**
71
+ * Props of {@link Command}: the attributes of a `div` plus the palette options.
72
+ * @see {@link Command}
73
+ * @since 1.0.0
74
+ */
75
+ export type CommandProps = ComponentProps<"div"> & {
76
+ /** Accessible name of the palette, given to the search field. Not shown. @default none */
77
+ readonly label?: string | undefined;
78
+ /**
79
+ * Hides the items that do not match the search. Set `false` when you filter the list yourself.
80
+ * @default true
81
+ */
82
+ readonly shouldFilter?: boolean | undefined;
83
+ /**
84
+ * Scores an item for the search: above `0` shows it, `0` hides it. Receives the item's `value`
85
+ * (its text when it has none) and its `keywords`. Without it every word of the search must occur
86
+ * in the value or the keywords, ignoring case and accents.
87
+ * @default none (the word match described above)
88
+ */
89
+ readonly filter?: CommandFilter | undefined;
90
+ /**
91
+ * Wraps arrow-key navigation from the last item to the first and back.
92
+ * @default false
93
+ */
94
+ readonly loop?: boolean | undefined;
95
+ /**
96
+ * Called when Escape is pressed in the palette. Calling `preventDefault()` on the event tells a
97
+ * surrounding overlay that it was handled. {@link CommandDialog} closes itself when it was not.
98
+ * @default none
99
+ */
100
+ readonly onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined;
101
+ };
102
+
103
+ /** The text a resolved child stands for, without reading anything but what is already there. */
104
+ function textOf(child: unknown): string {
105
+ if (typeof child === "string") return child;
106
+ if (typeof child === "number") return String(child);
107
+ if (Array.isArray(child)) return child.map(textOf).join("");
108
+ if (typeof child === "function") return textOf((child as () => unknown)());
109
+ if (typeof Node !== "undefined" && child instanceof Node) return child.textContent ?? "";
110
+ return "";
111
+ }
112
+
113
+ /** The words of `search` that must all occur in `haystack` (both normalized). */
114
+ function matchesEveryWord(haystack: string, search: string): boolean {
115
+ const words = normalizeSearchText(search).split(/\s+/u);
116
+ const text = normalizeSearchText(haystack);
117
+ return words.every((word) => word === "" || text.includes(word));
118
+ }
119
+
120
+ /**
121
+ * A command palette: a search field and a filtered, keyboard-navigable list of commands. The list is
122
+ * always open and sits right below the field, with no popup. Use it inline on a page, or in a
123
+ * {@link CommandDialog} for a palette opened with a shortcut.
124
+ *
125
+ * Keyboard and ARIA: the search field keeps focus. It is a `role="combobox"` with
126
+ * `aria-autocomplete="list"` and `aria-activedescendant`, and it is named by the `label` prop
127
+ * (give one; it is not shown). The list is a `role="listbox"` named by the field, and each command is
128
+ * a `role="option"`. The first enabled command is highlighted as the search changes. ArrowDown and
129
+ * ArrowUp move the highlight and skip disabled commands; Home and End jump to the first and last one.
130
+ * Enter runs the highlighted command's `onSelect`, and the search text stays. Escape does not clear
131
+ * the search: it is passed to `onEscapeKeyDown`, and {@link CommandDialog} closes on it.
132
+ *
133
+ * shadcn/ui differences: the prop is `class`, not `className`, every part has a `data-slot`
134
+ * (`command-input`, `command-list`, `command-item` and so on), and the `filter` function has the same
135
+ * `(value, search, keywords)` signature as the cmdk filter.
136
+ *
137
+ * @param props The palette props: see {@link CommandProps}.
138
+ * @returns The palette.
139
+ * @example
140
+ * import {
141
+ * Command,
142
+ * CommandEmpty,
143
+ * CommandGroup,
144
+ * CommandInput,
145
+ * CommandItem,
146
+ * CommandList,
147
+ * } from "@/components/ui/command";
148
+ *
149
+ * export function Palette() {
150
+ * return (
151
+ * <Command label="Command palette">
152
+ * <CommandInput placeholder="Type a command or search..." />
153
+ * <CommandList>
154
+ * <CommandEmpty>No results found.</CommandEmpty>
155
+ * <CommandGroup heading="Suggestions">
156
+ * <CommandItem onSelect={() => console.log("calendar")}>Calendar</CommandItem>
157
+ * </CommandGroup>
158
+ * </CommandList>
159
+ * </Command>
160
+ * );
161
+ * }
162
+ * @see {@link CommandDialog}
163
+ * @see {@link CommandInput}
164
+ * @since 1.0.0
165
+ */
166
+ function Command(props: CommandProps): JSX.Element {
167
+ const [local, rest] = splitProps(props, [
168
+ "class",
169
+ "children",
170
+ "label",
171
+ "shouldFilter",
172
+ "filter",
173
+ "loop",
174
+ "onEscapeKeyDown",
175
+ ]);
176
+ const [typed, setTyped] = useSignal("");
177
+ const [version, setVersion] = useSignal(0);
178
+ const items = new Map<string, ItemMeta>();
179
+ const source: SearchSource = {};
180
+ let generated = 0;
181
+ // The controlled `value` of the input wins over what was typed; `version` re-reads it once the
182
+ // input has registered.
183
+ const search = (): string => {
184
+ version();
185
+ return source.value?.() ?? typed();
186
+ };
187
+ const context: CommandContextValue = {
188
+ label: () => local.label,
189
+ source,
190
+ touch: () => setVersion((count) => count + 1),
191
+ register(value, meta) {
192
+ items.set(value, meta);
193
+ return () => {
194
+ if (items.get(value) === meta) items.delete(value);
195
+ };
196
+ },
197
+ nextValue: () => {
198
+ generated += 1;
199
+ return `command-item-${generated}`;
200
+ },
201
+ escape: (event) => local.onEscapeKeyDown?.(event),
202
+ };
203
+ const filter = (item: ComboboxFilterItem, query: string): boolean => {
204
+ const meta = items.get(item.value);
205
+ const keywords = meta?.keywords() ?? [];
206
+ const text = meta?.explicit() === false ? item.label : item.value;
207
+ if (local.filter !== undefined) return local.filter(text, query, keywords) > 0;
208
+ return matchesEveryWord([text, ...keywords].join(" "), query);
209
+ };
210
+ return (
211
+ <div
212
+ data-slot="command"
213
+ class={cn(
214
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
215
+ local.class,
216
+ )}
217
+ {...rest}
218
+ >
219
+ <commandContext.Provider value={context}>
220
+ <ComboboxPrimitive
221
+ inline
222
+ value={null}
223
+ onValueChange={(value) => {
224
+ // The palette keeps no selection: picking an item runs its handler.
225
+ if (value !== null) items.get(value)?.select();
226
+ }}
227
+ inputValue={search()}
228
+ onInputValueChange={(text, details) => {
229
+ // Picking an item or leaving the field would rewrite the text; the search stays.
230
+ if (details.reason !== "input" && details.reason !== "clear") return;
231
+ setTyped(text);
232
+ source.change?.(text);
233
+ }}
234
+ autoHighlight
235
+ loop={local.loop}
236
+ shouldFilter={local.shouldFilter}
237
+ filter={filter}
238
+ >
239
+ {local.children}
240
+ </ComboboxPrimitive>
241
+ </commandContext.Provider>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Props of {@link CommandDialog}: the dialog's props plus the palette's title, description and
248
+ * options.
249
+ * @see {@link CommandDialog}
250
+ * @since 1.0.0
251
+ */
252
+ export type CommandDialogProps = DialogProps & {
253
+ /**
254
+ * Accessible title of the dialog. It is visually hidden.
255
+ * @default "Command Palette"
256
+ */
257
+ readonly title?: string | undefined;
258
+ /**
259
+ * Accessible description of the dialog. It is visually hidden.
260
+ * @default "Search for a command to run..."
261
+ */
262
+ readonly description?: string | undefined;
263
+ /**
264
+ * Shows the close button in the corner of the dialog.
265
+ * @default true
266
+ */
267
+ readonly showCloseButton?: boolean | undefined;
268
+ /** Classes of the dialog panel. @default none */
269
+ readonly class?: string | undefined;
270
+ /** Props of the {@link Command} inside the dialog. @default none */
271
+ readonly commandProps?: CommandProps | undefined;
272
+ };
273
+
274
+ /**
275
+ * A {@link Command} in a modal dialog, for a palette opened with a keyboard shortcut or a button.
276
+ * Escape closes the dialog unless `commandProps.onEscapeKeyDown` prevents it. When the dialog closes,
277
+ * focus returns to the element that had focus when it opened, if it is still in the page.
278
+ *
279
+ * @param props The dialog props: see {@link CommandDialogProps}.
280
+ * @returns The dialog, rendered in a portal while it is open.
281
+ * @example
282
+ * import { useSignal } from "@shadow-js/core";
283
+ * import { Button } from "@/components/ui/button";
284
+ * import {
285
+ * CommandDialog,
286
+ * CommandEmpty,
287
+ * CommandInput,
288
+ * CommandItem,
289
+ * CommandList,
290
+ * } from "@/components/ui/command";
291
+ *
292
+ * export function PaletteButton() {
293
+ * const [open, setOpen] = useSignal(false);
294
+ * return (
295
+ * <>
296
+ * <Button variant="outline" onClick={() => setOpen(true)}>
297
+ * Open palette
298
+ * </Button>
299
+ * <CommandDialog open={open()} onOpenChange={setOpen}>
300
+ * <CommandInput placeholder="Type a command or search..." />
301
+ * <CommandList>
302
+ * <CommandEmpty>No results found.</CommandEmpty>
303
+ * <CommandItem>Calendar</CommandItem>
304
+ * </CommandList>
305
+ * </CommandDialog>
306
+ * </>
307
+ * );
308
+ * }
309
+ * @see {@link Command}
310
+ * @see {@link CommandDialogProps}
311
+ * @since 1.0.0
312
+ */
313
+ function CommandDialog(props: CommandDialogProps): JSX.Element {
314
+ const [local, rest] = splitProps(props, [
315
+ "title",
316
+ "description",
317
+ "children",
318
+ "class",
319
+ "showCloseButton",
320
+ "commandProps",
321
+ "open",
322
+ "defaultOpen",
323
+ "onOpenChange",
324
+ ]);
325
+ const t = useUiStrings();
326
+ const [open, setOpen] = useControllableState<boolean>({
327
+ value: () => local.open,
328
+ defaultValue: () => local.defaultOpen === true,
329
+ onChange: () => local.onOpenChange,
330
+ name: "CommandDialog open",
331
+ });
332
+ // A palette is usually opened by a shortcut, with no trigger for the dialog to give focus back to.
333
+ let opener: HTMLElement | undefined;
334
+ return (
335
+ <Dialog open={open()} onOpenChange={setOpen} {...rest}>
336
+ <DialogContent
337
+ class={cn("overflow-hidden p-0", local.class)}
338
+ showCloseButton={local.showCloseButton}
339
+ onOpenAutoFocus={() => {
340
+ const active = document.activeElement;
341
+ opener = active instanceof HTMLElement ? active : undefined;
342
+ }}
343
+ onCloseAutoFocus={(event: Event) => {
344
+ if (opener?.isConnected !== true) return;
345
+ event.preventDefault();
346
+ opener.focus();
347
+ }}
348
+ >
349
+ <DialogHeader class="sr-only">
350
+ <DialogTitle>{local.title ?? t("command.title", "Command Palette")}</DialogTitle>
351
+ <DialogDescription>
352
+ {local.description ?? t("command.description", "Search for a command to run...")}
353
+ </DialogDescription>
354
+ </DialogHeader>
355
+ <Command
356
+ {...local.commandProps}
357
+ onEscapeKeyDown={(event) => {
358
+ local.commandProps?.onEscapeKeyDown?.(event);
359
+ if (!event.defaultPrevented) setOpen(false);
360
+ }}
361
+ class={cn(
362
+ "[&_[data-slot=command-group-heading]]:px-2 [&_[data-slot=command-group-heading]]:font-medium [&_[data-slot=command-group-heading]]:text-muted-foreground [&_[data-slot=command-group]:not([hidden])_~[data-slot=command-group]]:pt-0 [&_[data-slot=command-group]]:px-2 [&_[data-slot=command-input-wrapper]]:h-12 [&_[data-slot=command-input-wrapper]_svg]:size-5 [&_[data-slot=command-input]]:h-12 [&_[data-slot=command-item]]:px-2 [&_[data-slot=command-item]]:py-3 [&_[data-slot=command-item]_svg]:size-5",
363
+ local.commandProps?.class,
364
+ )}
365
+ >
366
+ {local.children}
367
+ </Command>
368
+ </DialogContent>
369
+ </Dialog>
370
+ );
371
+ }
372
+
373
+ /**
374
+ * Props of {@link CommandInput}: the attributes of an `input` plus a controlled search text.
375
+ * @see {@link CommandInput}
376
+ * @since 1.0.0
377
+ */
378
+ export type CommandInputProps = Omit<ComboboxInputProps, "value"> & {
379
+ /** Controlled search text. The search is uncontrolled when it is not set. @default none */
380
+ readonly value?: string | undefined;
381
+ /** Called with the new search text whenever the user types. @default none */
382
+ readonly onValueChange?: ((search: string) => void) | undefined;
383
+ };
384
+
385
+ /**
386
+ * The search field of a {@link Command}, with a search icon. It keeps focus while the arrow keys move
387
+ * through the list. Pass `value` and `onValueChange` to control the search text from your own state.
388
+ * Its `aria-label` comes from the `label` of the {@link Command}.
389
+ *
390
+ * @param props The field props: see {@link CommandInputProps}. `class` goes to the `input`.
391
+ * @returns The search field.
392
+ * @throws Error when rendered outside a {@link Command}.
393
+ * @example
394
+ * import { useSignal } from "@shadow-js/core";
395
+ * import { Command, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
396
+ *
397
+ * export function Search() {
398
+ * const [search, setSearch] = useSignal("");
399
+ * return (
400
+ * <Command label="Search">
401
+ * <CommandInput placeholder="Search..." value={search()} onValueChange={setSearch} />
402
+ * <CommandList>
403
+ * <CommandItem>Calendar</CommandItem>
404
+ * </CommandList>
405
+ * </Command>
406
+ * );
407
+ * }
408
+ * @see {@link Command}
409
+ * @since 1.0.0
410
+ */
411
+ function CommandInput(props: CommandInputProps): JSX.Element {
412
+ const [local, rest] = splitProps(props, ["class", "value", "onValueChange"]);
413
+ const command = commandContext.use("CommandInput");
414
+ command.source.value = () => local.value;
415
+ command.source.change = (text) => local.onValueChange?.(text);
416
+ useEffect(() => {
417
+ // The first render happened before this input registered its controlled value.
418
+ untrack(command.touch);
419
+ onCleanup(() => {
420
+ command.source.value = undefined;
421
+ command.source.change = undefined;
422
+ });
423
+ });
424
+ return (
425
+ <div data-slot="command-input-wrapper" class="flex h-9 items-center gap-2 border-b px-3">
426
+ <SearchIcon class="size-4 shrink-0 opacity-50" />
427
+ <ComboboxPrimitive.Input
428
+ data-slot="command-input"
429
+ aria-label={command.label()}
430
+ class={cn(
431
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-transparent disabled:cursor-not-allowed disabled:opacity-50",
432
+ local.class,
433
+ )}
434
+ {...rest}
435
+ />
436
+ </div>
437
+ );
438
+ }
439
+
440
+ /**
441
+ * Props of {@link CommandList}: the attributes of the list element.
442
+ * @see {@link CommandList}
443
+ * @since 1.0.0
444
+ */
445
+ export type CommandListProps = ComboboxContentProps;
446
+
447
+ /**
448
+ * The scrollable list of results: a `role="listbox"` named by the search field. It is always
449
+ * rendered in place, and it scrolls when the results are taller than `max-h-[300px]`, the default
450
+ * height limit.
451
+ *
452
+ * @param props The list props: see {@link CommandListProps}.
453
+ * @returns The list.
454
+ * @throws Error when rendered outside a {@link Command}.
455
+ * @example
456
+ * import { Command, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
457
+ *
458
+ * export function Results() {
459
+ * return (
460
+ * <Command>
461
+ * <CommandInput placeholder="Search..." />
462
+ * <CommandList class="max-h-72">
463
+ * <CommandItem>Calendar</CommandItem>
464
+ * </CommandList>
465
+ * </Command>
466
+ * );
467
+ * }
468
+ * @see {@link Command}
469
+ * @since 1.0.0
470
+ */
471
+ function CommandList(props: CommandListProps): JSX.Element {
472
+ const [local, rest] = splitProps(props, ["class", "onEscapeKeyDown"]);
473
+ const command = commandContext.use("CommandList");
474
+ return (
475
+ <ComboboxPrimitive.Content
476
+ data-slot="command-list"
477
+ class={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", local.class)}
478
+ onEscapeKeyDown={(event: KeyboardEvent) => {
479
+ local.onEscapeKeyDown?.(event);
480
+ if (!event.defaultPrevented) command.escape(event);
481
+ }}
482
+ {...rest}
483
+ />
484
+ );
485
+ }
486
+
487
+ /**
488
+ * The message shown in the list while the search matches nothing.
489
+ *
490
+ * @param props The message props: the attributes of the rendered `div`.
491
+ * @returns The message, or nothing while there are matches.
492
+ * @throws Error when rendered outside a {@link Command}.
493
+ * @example
494
+ * import { Command, CommandEmpty, CommandInput, CommandList } from "@/components/ui/command";
495
+ *
496
+ * export function Empty() {
497
+ * return (
498
+ * <Command>
499
+ * <CommandInput placeholder="Search..." />
500
+ * <CommandList>
501
+ * <CommandEmpty>No results found.</CommandEmpty>
502
+ * </CommandList>
503
+ * </Command>
504
+ * );
505
+ * }
506
+ * @see {@link CommandList}
507
+ * @since 1.0.0
508
+ */
509
+ function CommandEmpty(props: ComboboxEmptyProps): JSX.Element {
510
+ const [local, rest] = splitProps(props, ["class"]);
511
+ return (
512
+ <ComboboxPrimitive.Empty
513
+ data-slot="command-empty"
514
+ class={cn("py-6 text-center text-sm", local.class)}
515
+ {...rest}
516
+ />
517
+ );
518
+ }
519
+
520
+ /**
521
+ * Props of {@link CommandGroup}: the attributes of a `div` plus the visible `heading`.
522
+ * @see {@link CommandGroup}
523
+ * @since 1.0.0
524
+ */
525
+ export type CommandGroupProps = ComboboxGroupProps & {
526
+ /** The heading above the items. It names the group for assistive technology. @default none */
527
+ readonly heading?: JsxChild | undefined;
528
+ };
529
+
530
+ /**
531
+ * A group of commands under an optional visible `heading`. The heading names the group for assistive
532
+ * technology. The group hides itself while the search matches none of its items.
533
+ *
534
+ * @param props The group props: see {@link CommandGroupProps}.
535
+ * @returns The group.
536
+ * @throws Error when rendered outside a {@link Command}.
537
+ * @example
538
+ * import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
539
+ *
540
+ * export function Settings() {
541
+ * return (
542
+ * <Command>
543
+ * <CommandInput placeholder="Search..." />
544
+ * <CommandList>
545
+ * <CommandGroup heading="Settings">
546
+ * <CommandItem>Profile</CommandItem>
547
+ * </CommandGroup>
548
+ * </CommandList>
549
+ * </Command>
550
+ * );
551
+ * }
552
+ * @see {@link CommandItem}
553
+ * @since 1.0.0
554
+ */
555
+ function CommandGroup(props: CommandGroupProps): JSX.Element {
556
+ const [local, rest] = splitProps(props, ["class", "heading", "children"]);
557
+ return (
558
+ <ComboboxPrimitive.Group
559
+ data-slot="command-group"
560
+ class={cn("overflow-hidden p-1 text-foreground", local.class)}
561
+ {...rest}
562
+ >
563
+ <Show when={local.heading !== undefined}>
564
+ <ComboboxPrimitive.Label
565
+ as="div"
566
+ data-slot="command-group-heading"
567
+ class="px-2 py-1.5 text-xs font-medium text-muted-foreground"
568
+ >
569
+ {local.heading}
570
+ </ComboboxPrimitive.Label>
571
+ </Show>
572
+ {local.children}
573
+ </ComboboxPrimitive.Group>
574
+ );
575
+ }
576
+
577
+ /**
578
+ * A line between groups of commands. It is decorative, and hidden while the user is searching.
579
+ *
580
+ * @param props The separator props: the attributes of the rendered `div`.
581
+ * @returns The separator.
582
+ * @throws Error when rendered outside a {@link Command}.
583
+ * @example
584
+ * import { Command, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command";
585
+ *
586
+ * export function Sections() {
587
+ * return (
588
+ * <Command>
589
+ * <CommandInput placeholder="Search..." />
590
+ * <CommandList>
591
+ * <CommandGroup>
592
+ * <CommandItem>Calendar</CommandItem>
593
+ * </CommandGroup>
594
+ * <CommandSeparator />
595
+ * <CommandGroup>
596
+ * <CommandItem>Settings</CommandItem>
597
+ * </CommandGroup>
598
+ * </CommandList>
599
+ * </Command>
600
+ * );
601
+ * }
602
+ * @see {@link CommandGroup}
603
+ * @since 1.0.0
604
+ */
605
+ function CommandSeparator(props: ComboboxSeparatorProps): JSX.Element {
606
+ const [local, rest] = splitProps(props, ["class"]);
607
+ return (
608
+ <ComboboxPrimitive.Separator
609
+ data-slot="command-separator"
610
+ class={cn("-mx-1 h-px bg-border", local.class)}
611
+ {...rest}
612
+ />
613
+ );
614
+ }
615
+
616
+ /**
617
+ * Props of {@link CommandItem}: the attributes of a `div` plus `value`, `keywords` and `onSelect`.
618
+ * @see {@link CommandItem}
619
+ * @since 1.0.0
620
+ */
621
+ export type CommandItemProps = Omit<ComboboxItemProps, "value" | "onSelect"> & {
622
+ /**
623
+ * What the search matches and what `onSelect` receives. It must be unique in the palette.
624
+ * @default the text of the item
625
+ */
626
+ readonly value?: string | undefined;
627
+ /**
628
+ * Extra words the search matches, such as synonyms.
629
+ * @default []
630
+ */
631
+ readonly keywords?: readonly string[] | undefined;
632
+ /**
633
+ * Called with the item's value when it is picked by a click or by Enter.
634
+ * @default none
635
+ */
636
+ readonly onSelect?: ((value: string) => void) | undefined;
637
+ };
638
+
639
+ /**
640
+ * One command of a {@link Command}. Picking it, by a click or by Enter while it is highlighted, calls
641
+ * `onSelect`. The highlighted item has `data-highlighted`; a disabled item cannot be highlighted or
642
+ * picked. Its text is what the search matches, unless it has an explicit `value`.
643
+ *
644
+ * @param props The item props: see {@link CommandItemProps}.
645
+ * @returns The command, or nothing while the search rejects it.
646
+ * @throws Error when rendered outside a {@link Command}.
647
+ * @example
648
+ * import { Command, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
649
+ *
650
+ * export function Calendar() {
651
+ * return (
652
+ * <Command>
653
+ * <CommandInput placeholder="Search..." />
654
+ * <CommandList>
655
+ * <CommandItem value="calendar" keywords={["date"]} onSelect={(value) => console.log(value)}>
656
+ * Calendar
657
+ * </CommandItem>
658
+ * </CommandList>
659
+ * </Command>
660
+ * );
661
+ * }
662
+ * @see {@link CommandShortcut}
663
+ * @since 1.0.0
664
+ */
665
+ function CommandItem(props: CommandItemProps): JSX.Element {
666
+ const [local, rest] = splitProps(props, ["class", "value", "keywords", "onSelect", "children"]);
667
+ const command = commandContext.use("CommandItem");
668
+ const generated = command.nextValue();
669
+ // Resolved once: the primitive reads the text of its children to label the item, and building
670
+ // rich children twice would claim the same server nodes twice while hydrating.
671
+ const content: JsxChild = untrack(() => local.children);
672
+ const label = (): string | undefined => {
673
+ const text = textOf(content).trim();
674
+ return text === "" ? undefined : text;
675
+ };
676
+ const value = (): string => local.value ?? generated;
677
+ const meta: ItemMeta = {
678
+ explicit: () => local.value !== undefined,
679
+ keywords: () => local.keywords ?? [],
680
+ select: () => {
681
+ const text = local.value ?? label() ?? "";
682
+ untrack(() => local.onSelect?.(text));
683
+ },
684
+ };
685
+ // Registered while rendering: the item's own visibility is computed before any effect runs.
686
+ let release = untrack(() => command.register(value(), meta));
687
+ useEffect(() => {
688
+ const current = value();
689
+ release();
690
+ release = command.register(current, meta);
691
+ });
692
+ onCleanup(() => release());
693
+ return (
694
+ <ComboboxPrimitive.Item
695
+ data-slot="command-item"
696
+ value={value()}
697
+ label={label()}
698
+ class={cn(
699
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground forced-colors:data-highlighted:bg-[Highlight] forced-colors:data-highlighted:text-[HighlightText] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
700
+ local.class,
701
+ )}
702
+ {...rest}
703
+ >
704
+ {content}
705
+ </ComboboxPrimitive.Item>
706
+ );
707
+ }
708
+
709
+ /**
710
+ * The keyboard shortcut hint at the end of a {@link CommandItem}. It is part of the item's text, so
711
+ * the search matches it too.
712
+ *
713
+ * @param props The hint props: the attributes of the rendered `span`, and `children` (the shortcut).
714
+ * @returns The shortcut hint.
715
+ * @example
716
+ * import { Command, CommandInput, CommandItem, CommandList, CommandShortcut } from "@/components/ui/command";
717
+ *
718
+ * export function Shortcuts() {
719
+ * return (
720
+ * <Command>
721
+ * <CommandInput placeholder="Search..." />
722
+ * <CommandList>
723
+ * <CommandItem>
724
+ * Settings
725
+ * <CommandShortcut>Ctrl+S</CommandShortcut>
726
+ * </CommandItem>
727
+ * </CommandList>
728
+ * </Command>
729
+ * );
730
+ * }
731
+ * @see {@link CommandItem}
732
+ * @since 1.0.0
733
+ */
734
+ function CommandShortcut(props: ComponentProps<"span">): JSX.Element {
735
+ const [local, rest] = splitProps(props, ["class", "children"]);
736
+ return (
737
+ <span
738
+ data-slot="command-shortcut"
739
+ class={cn("ms-auto text-xs tracking-widest text-muted-foreground", local.class)}
740
+ {...rest}
741
+ >
742
+ {local.children}
743
+ </span>
744
+ );
745
+ }
746
+
747
+ export {
748
+ Command,
749
+ CommandDialog,
750
+ CommandEmpty,
751
+ CommandGroup,
752
+ CommandInput,
753
+ CommandItem,
754
+ CommandList,
755
+ CommandSeparator,
756
+ CommandShortcut,
757
+ };