@haruhimemoe/ui 0.7.0 → 0.9.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 (89) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/README.md +246 -11
  3. package/dist/components/basics/Disclosure.d.ts +1 -1
  4. package/dist/components/basics/Disclosure.js +2 -2
  5. package/dist/components/basics/Prose.d.ts +9 -6
  6. package/dist/components/basics/Prose.js +4 -4
  7. package/dist/components/forms/Checkbox.d.ts +1 -1
  8. package/dist/components/forms/Checkbox.js +3 -1
  9. package/dist/components/forms/RadioGroup.js +3 -1
  10. package/dist/components/mdx/Callout.d.ts +24 -0
  11. package/dist/components/mdx/Callout.js +22 -0
  12. package/dist/components/mdx/CodeBlock.d.ts +26 -0
  13. package/dist/components/mdx/CodeBlock.js +48 -0
  14. package/dist/components/mdx/CodeCopyButton.d.ts +21 -0
  15. package/dist/components/mdx/CodeCopyButton.js +34 -0
  16. package/dist/components/mdx/MdxBlockquote.d.ts +23 -0
  17. package/dist/components/mdx/MdxBlockquote.js +17 -0
  18. package/dist/components/mdx/MdxHeading.d.ts +26 -0
  19. package/dist/components/mdx/MdxHeading.js +23 -0
  20. package/dist/components/mdx/MdxLink.d.ts +29 -0
  21. package/dist/components/mdx/MdxLink.js +26 -0
  22. package/dist/components/mdx/MdxPre.d.ts +25 -0
  23. package/dist/components/mdx/MdxPre.js +39 -0
  24. package/dist/components/mdx/MdxTable.d.ts +20 -0
  25. package/dist/components/mdx/MdxTable.js +23 -0
  26. package/dist/components/mdx/highlighter.d.ts +37 -0
  27. package/dist/components/mdx/highlighter.js +66 -0
  28. package/dist/components/mdx/mdxComponents.d.ts +21 -0
  29. package/dist/components/mdx/mdxComponents.js +22 -0
  30. package/dist/components/mdx/parseCodeMeta.d.ts +21 -0
  31. package/dist/components/mdx/parseCodeMeta.js +40 -0
  32. package/dist/components/mdx/textOf.d.ts +17 -0
  33. package/dist/components/mdx/textOf.js +28 -0
  34. package/dist/components/palette/CommandPalette.d.ts +21 -0
  35. package/dist/components/palette/CommandPalette.js +315 -0
  36. package/dist/components/palette/CommandPaletteButton.d.ts +21 -0
  37. package/dist/components/palette/CommandPaletteButton.js +26 -0
  38. package/dist/components/palette/PaletteFooter.d.ts +21 -0
  39. package/dist/components/palette/PaletteFooter.js +10 -0
  40. package/dist/components/palette/PaletteInput.d.ts +30 -0
  41. package/dist/components/palette/PaletteInput.js +23 -0
  42. package/dist/components/palette/PaletteList.d.ts +29 -0
  43. package/dist/components/palette/PaletteList.js +44 -0
  44. package/dist/components/palette/PaletteRow.d.ts +30 -0
  45. package/dist/components/palette/PaletteRow.js +33 -0
  46. package/dist/components/palette/calc.d.ts +28 -0
  47. package/dist/components/palette/calc.js +226 -0
  48. package/dist/components/palette/fuzzy.d.ts +51 -0
  49. package/dist/components/palette/fuzzy.js +138 -0
  50. package/dist/components/palette/hotkeys.d.ts +57 -0
  51. package/dist/components/palette/hotkeys.js +111 -0
  52. package/dist/components/palette/paletteEvents.d.ts +22 -0
  53. package/dist/components/palette/paletteEvents.js +22 -0
  54. package/dist/components/palette/platform.d.ts +13 -0
  55. package/dist/components/palette/platform.js +21 -0
  56. package/dist/components/palette/recents.d.ts +48 -0
  57. package/dist/components/palette/recents.js +85 -0
  58. package/dist/components/palette/rows.d.ts +63 -0
  59. package/dist/components/palette/rows.js +136 -0
  60. package/dist/components/palette/siteCommands.d.ts +38 -0
  61. package/dist/components/palette/siteCommands.js +152 -0
  62. package/dist/components/palette/store.d.ts +94 -0
  63. package/dist/components/palette/store.js +135 -0
  64. package/dist/components/palette/types.d.ts +102 -0
  65. package/dist/components/palette/types.js +11 -0
  66. package/dist/components/palette/useProviderSearch.d.ts +28 -0
  67. package/dist/components/palette/useProviderSearch.js +68 -0
  68. package/dist/components/shell/HeaderMenu.d.ts +1 -1
  69. package/dist/components/shell/HeaderMenu.js +2 -2
  70. package/dist/index.d.ts +8 -1
  71. package/dist/index.js +8 -1
  72. package/dist/mdx.d.ts +17 -0
  73. package/dist/mdx.js +17 -0
  74. package/dist/remark/callouts.d.ts +20 -0
  75. package/dist/remark/callouts.js +58 -0
  76. package/dist/remark/codeMeta.d.ts +17 -0
  77. package/dist/remark/codeMeta.js +22 -0
  78. package/dist/remark/headingIds.d.ts +16 -0
  79. package/dist/remark/headingIds.js +28 -0
  80. package/dist/remark/index.d.ts +29 -0
  81. package/dist/remark/index.js +33 -0
  82. package/dist/remark/mdast.d.ts +42 -0
  83. package/dist/remark/mdast.js +37 -0
  84. package/dist/remark/slugify.d.ts +23 -0
  85. package/dist/remark/slugify.js +40 -0
  86. package/dist/shiki.d.ts +13 -0
  87. package/dist/shiki.js +38 -0
  88. package/dist/theme.css +18 -0
  89. package/package.json +34 -4
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @file src/components/palette/CommandPalette.tsx
3
+ * @desc The command palette: a native <dialog> opened by mod+k, `openCommandPalette()` or a
4
+ * command shortcut, with a combobox over the rows the store and row builder produce.
5
+ * Enter runs a command, pushes its page, or starts collecting its args; Escape and
6
+ * Backspace on an empty input go back a level, then close. Providers search as you type.
7
+ * Recents are read on open and written on every run. The calculator row copies its result.
8
+ * Mount once, inside a client component, since commands carry functions.
9
+ * @author David @dvhsh (https://dvh.sh)
10
+ * @created Sat Oct 3, 2026
11
+ * @modified Sat Oct 3, 2026
12
+ */
13
+ import type { CommandPaletteProps } from "./types.js";
14
+ export type { CommandPaletteProps } from "./types.js";
15
+ /**
16
+ * @function CommandPalette
17
+ * @param props {CommandPaletteProps} commands, root providers, storage key, hotkey, placeholder,
18
+ * label, calculator and recents switches, panel classes
19
+ * @returns {JSX.Element} the dialog, empty until opened
20
+ */
21
+ export declare function CommandPalette({ commands, providers, storageKey, hotkey, placeholder, label, calculator, recents: recentsOn, className, }: CommandPaletteProps): import("react").JSX.Element;
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @file src/components/palette/CommandPalette.tsx
3
+ * @desc The command palette: a native <dialog> opened by mod+k, `openCommandPalette()` or a
4
+ * command shortcut, with a combobox over the rows the store and row builder produce.
5
+ * Enter runs a command, pushes its page, or starts collecting its args; Escape and
6
+ * Backspace on an empty input go back a level, then close. Providers search as you type.
7
+ * Recents are read on open and written on every run. The calculator row copies its result.
8
+ * Mount once, inside a client component, since commands carry functions.
9
+ * @author David @dvhsh (https://dvh.sh)
10
+ * @created Sat Oct 3, 2026
11
+ * @modified Sat Oct 3, 2026
12
+ */
13
+ "use client";
14
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
+ import { usePathname, useRouter } from "next/navigation.js";
16
+ import { useCallback, useEffect, useId, useMemo, useReducer, useRef, useState, } from "react";
17
+ import { cx } from "../../utils/cx.js";
18
+ import { useLatestStatus } from "../actions/useLatestStatus.js";
19
+ import { isEditableTarget, matchesCombo, parseShortcut } from "./hotkeys.js";
20
+ import { PaletteFooter } from "./PaletteFooter.js";
21
+ import { PaletteInput } from "./PaletteInput.js";
22
+ import { PaletteList } from "./PaletteList.js";
23
+ import { PALETTE_EVENT } from "./paletteEvents.js";
24
+ import { isMac } from "./platform.js";
25
+ import { boostFrom, readRecents, recentIds, recordRecent } from "./recents.js";
26
+ import { buildRows, CALC_ID, optionAt, optionCount } from "./rows.js";
27
+ import { initialState, reduce, topFrame } from "./store.js";
28
+ import { useProviderSearch } from "./useProviderSearch.js";
29
+ const NONE = [];
30
+ const CHORD_MS = 800;
31
+ const DIALOG = "m-0 h-dvh max-h-none w-full max-w-none bg-transparent p-0 text-c1 backdrop:bg-b6/70 backdrop:backdrop-blur-sm sm:h-auto sm:pt-[12vh]";
32
+ const PANEL = "mx-auto flex h-full max-h-dvh w-full flex-col overflow-hidden border-b3 bg-b6 shadow-2xl sm:h-auto sm:max-h-[70vh] sm:max-w-xl sm:rounded-xl sm:border";
33
+ /**
34
+ * @function CommandPalette
35
+ * @param props {CommandPaletteProps} commands, root providers, storage key, hotkey, placeholder,
36
+ * label, calculator and recents switches, panel classes
37
+ * @returns {JSX.Element} the dialog, empty until opened
38
+ */
39
+ export function CommandPalette({ commands, providers = NONE, storageKey = "default", hotkey = "mod+k", placeholder = "Search commands…", label = "Command palette", calculator = true, recents: recentsOn = true, className, }) {
40
+ const router = useRouter();
41
+ const pathname = usePathname() ?? "";
42
+ const [state, dispatch] = useReducer(reduce, undefined, initialState);
43
+ const [recents, setRecents] = useState({});
44
+ const [mac, setMac] = useState(false);
45
+ const dialog = useRef(null);
46
+ const input = useRef(null);
47
+ const opener = useRef(null);
48
+ const chord = useRef(null);
49
+ // Where the pointer last was over a row. Chrome fires a pointermove at the same spot after the
50
+ // list scrolls under a still cursor; only a real move may take the active row from the keys.
51
+ const pointer = useRef(null);
52
+ const { status, start, settle } = useLatestStatus();
53
+ // The copy status reads until the query changes or the palette reopens, then the count again.
54
+ const [statusShown, setStatusShown] = useState(false);
55
+ const baseId = useId();
56
+ const listId = `${baseId}-list`;
57
+ const optionId = useCallback((index) => `${baseId}-option-${index}`, [baseId]);
58
+ useEffect(() => setMac(isMac()), []);
59
+ const root = useMemo(() => ({ title: label, placeholder, commands, providers }), [label, placeholder, commands, providers]);
60
+ const open = useCallback((page) => {
61
+ // Opening onto a page while already open keeps the original opener.
62
+ if (!state.open)
63
+ opener.current = document.activeElement;
64
+ if (recentsOn)
65
+ setRecents(readRecents(storageKey));
66
+ setStatusShown(false);
67
+ dispatch({ type: "open", root, page });
68
+ }, [root, recentsOn, storageKey, state.open]);
69
+ const close = useCallback(() => dispatch({ type: "close" }), []);
70
+ const ctx = useMemo(() => ({
71
+ navigate: (href) => router.push(href),
72
+ close,
73
+ push: (page) => (state.open ? dispatch({ type: "push", page }) : open(page)),
74
+ copy: async (text) => {
75
+ const run = start();
76
+ setStatusShown(true);
77
+ try {
78
+ await navigator.clipboard.writeText(text);
79
+ settle(run, "Copied");
80
+ }
81
+ catch {
82
+ settle(run, "Couldn't copy");
83
+ }
84
+ },
85
+ pathname,
86
+ commands,
87
+ }), [router, close, state.open, open, start, settle, pathname, commands]);
88
+ const frame = topFrame(state);
89
+ const depth = state.stack.length - 1;
90
+ const isRoot = depth === 0;
91
+ const query = state.arg ? state.arg.query : (frame?.query ?? "");
92
+ const rows = useMemo(() => frame
93
+ ? buildRows({
94
+ query: frame.query,
95
+ commands: frame.page.commands ?? [],
96
+ providers: frame.page.providers ?? NONE,
97
+ providerResults: frame.providers,
98
+ recentIds: isRoot && recentsOn ? recentIds(recents) : [],
99
+ boost: boostFrom(recents),
100
+ isRoot,
101
+ calculator,
102
+ arg: state.arg,
103
+ ctx,
104
+ })
105
+ : [], [frame, isRoot, recentsOn, recents, calculator, state.arg, ctx]);
106
+ const count = optionCount(rows);
107
+ const active = state.arg ? state.arg.active : (frame?.active ?? 0);
108
+ useProviderSearch({
109
+ providers: frame?.page.providers ?? NONE,
110
+ query: frame?.query ?? "",
111
+ depth,
112
+ dispatch,
113
+ enabled: state.open && state.arg === null && frame !== undefined,
114
+ });
115
+ // The dialog follows `open`: show it modally, focus the input, lock the page's scroll; on
116
+ // close, hand focus back to whatever had it.
117
+ useEffect(() => {
118
+ const element = dialog.current;
119
+ if (!element)
120
+ return;
121
+ if (state.open) {
122
+ if (!element.open)
123
+ element.showModal();
124
+ input.current?.focus();
125
+ document.documentElement.style.overflow = "hidden";
126
+ return;
127
+ }
128
+ if (element.open)
129
+ element.close();
130
+ document.documentElement.style.overflow = "";
131
+ if (opener.current instanceof HTMLElement)
132
+ opener.current.focus();
133
+ opener.current = null;
134
+ }, [state.open]);
135
+ useEffect(() => () => {
136
+ document.documentElement.style.overflow = "";
137
+ }, []);
138
+ const remember = (id) => {
139
+ if (recentsOn && id !== CALC_ID)
140
+ setRecents(recordRecent(storageKey, id));
141
+ };
142
+ const runCommand = (command, args = {}) => {
143
+ remember(command.id);
144
+ try {
145
+ const result = command.run?.(ctx, args);
146
+ if (result instanceof Promise)
147
+ result.catch((error) => console.error(`CommandPalette: "${command.id}" failed`, error));
148
+ }
149
+ catch (error) {
150
+ console.error(`CommandPalette: "${command.id}" failed`, error);
151
+ }
152
+ if (command.closeOnRun !== false)
153
+ close();
154
+ };
155
+ const acceptArg = (value) => {
156
+ const arg = state.arg;
157
+ const spec = arg?.command.args?.[arg.index];
158
+ if (!arg || !spec)
159
+ return;
160
+ const trimmed = value.trim();
161
+ if (spec.type !== "choice" && trimmed === "")
162
+ return dispatch({ type: "argError", message: "Required" });
163
+ if (spec.type === "number" && Number.isNaN(Number(trimmed)))
164
+ return dispatch({ type: "argError", message: "Enter a number" });
165
+ const message = spec.validate?.(trimmed);
166
+ if (message)
167
+ return dispatch({ type: "argError", message });
168
+ dispatch({ type: "argAccept", value: trimmed });
169
+ if (arg.index + 1 >= (arg.command.args?.length ?? 0))
170
+ runCommand(arg.command, { ...arg.values, [spec.name]: trimmed });
171
+ };
172
+ const select = (command) => {
173
+ if (state.arg)
174
+ return acceptArg(command.id);
175
+ if (command.page) {
176
+ remember(command.id);
177
+ return dispatch({ type: "push", page: command.page });
178
+ }
179
+ if (command.args && command.args.length > 0)
180
+ return dispatch({ type: "beginArgs", command });
181
+ runCommand(command);
182
+ };
183
+ const fire = (command) => {
184
+ try {
185
+ if (command.when && !command.when(ctx))
186
+ return;
187
+ }
188
+ catch {
189
+ return;
190
+ }
191
+ if (command.page)
192
+ return open(command.page);
193
+ if (command.args && command.args.length > 0) {
194
+ open();
195
+ return dispatch({ type: "beginArgs", command });
196
+ }
197
+ runCommand(command);
198
+ };
199
+ const back = () => {
200
+ if (state.arg || depth > 0)
201
+ dispatch({ type: "pop" });
202
+ else
203
+ close();
204
+ };
205
+ const onKeyDown = (event) => {
206
+ const spec = state.arg?.command.args?.[state.arg.index];
207
+ switch (event.key) {
208
+ case "ArrowDown":
209
+ case "ArrowUp":
210
+ event.preventDefault();
211
+ return dispatch({ type: "move", delta: event.key === "ArrowDown" ? 1 : -1, count });
212
+ case "Home":
213
+ case "End":
214
+ if (count === 0)
215
+ return;
216
+ event.preventDefault();
217
+ return dispatch({ type: "move", delta: event.key === "Home" ? "home" : "end", count });
218
+ case "Enter": {
219
+ event.preventDefault();
220
+ if (state.arg && spec && spec.type !== "choice")
221
+ return acceptArg(state.arg.query);
222
+ const option = optionAt(rows, active);
223
+ return option ? select(option.command) : undefined;
224
+ }
225
+ case "Escape":
226
+ event.preventDefault();
227
+ return back();
228
+ case "Backspace":
229
+ if (query !== "" || (!state.arg && depth === 0))
230
+ return;
231
+ event.preventDefault();
232
+ return dispatch({ type: "pop" });
233
+ case "Tab":
234
+ return event.preventDefault();
235
+ default:
236
+ return;
237
+ }
238
+ };
239
+ // Global keys: the opening hotkey anywhere; command shortcuts (combos and chords) only while
240
+ // closed and outside editable targets.
241
+ useEffect(() => {
242
+ const toggle = parseShortcut(hotkey);
243
+ const shortcuts = commands.flatMap((command) => {
244
+ const shortcut = command.shortcut ? parseShortcut(command.shortcut) : null;
245
+ return shortcut ? [{ command, shortcut }] : [];
246
+ });
247
+ const onKey = (event) => {
248
+ if (toggle?.kind === "combo" && matchesCombo(event, toggle.combo, mac)) {
249
+ event.preventDefault();
250
+ return state.open ? close() : open();
251
+ }
252
+ if (state.open || isEditableTarget(event.target))
253
+ return;
254
+ const bare = !event.metaKey && !event.ctrlKey && !event.altKey && event.key.length === 1;
255
+ const key = event.key.toLowerCase();
256
+ const pending = chord.current;
257
+ chord.current = null;
258
+ for (const { command, shortcut } of shortcuts) {
259
+ if (shortcut.kind === "combo") {
260
+ if (!matchesCombo(event, shortcut.combo, mac))
261
+ continue;
262
+ event.preventDefault();
263
+ return fire(command);
264
+ }
265
+ if (!bare)
266
+ continue;
267
+ if (pending &&
268
+ pending.key === shortcut.keys[0] &&
269
+ key === shortcut.keys[1] &&
270
+ Date.now() - pending.at < CHORD_MS) {
271
+ event.preventDefault();
272
+ return fire(command);
273
+ }
274
+ }
275
+ if (bare &&
276
+ shortcuts.some((entry) => entry.shortcut.kind === "chord" && entry.shortcut.keys[0] === key)) {
277
+ chord.current = { key, at: Date.now() };
278
+ }
279
+ };
280
+ window.addEventListener("keydown", onKey);
281
+ return () => window.removeEventListener("keydown", onKey);
282
+ });
283
+ useEffect(() => {
284
+ const onOpen = (event) => open(event.detail?.page);
285
+ window.addEventListener(PALETTE_EVENT, onOpen);
286
+ return () => window.removeEventListener(PALETTE_EVENT, onOpen);
287
+ }, [open]);
288
+ const argSpec = state.arg?.command.args?.[state.arg.index];
289
+ const crumbs = state.arg
290
+ ? [
291
+ state.arg.command.title,
292
+ ...(state.arg.command.args ?? [])
293
+ .slice(0, state.arg.index)
294
+ .map((a) => state.arg?.values[a.name] ?? ""),
295
+ ]
296
+ : state.stack.slice(1).map((f) => f.page.title);
297
+ return (_jsx("dialog", { ref: dialog, "data-palette": "", "aria-label": label, className: DIALOG, onCancel: (event) => {
298
+ event.preventDefault();
299
+ back();
300
+ }, onKeyDown: onKeyDown,
301
+ // The browser can close the dialog itself (a back gesture, a close watcher): follow it.
302
+ onClose: close, onClick: (event) => {
303
+ if (event.target === event.currentTarget)
304
+ close();
305
+ }, children: state.open && frame ? (_jsxs("div", { className: cx(PANEL, className), children: [_jsx(PaletteInput, { ref: input, value: query, placeholder: argSpec ? argSpec.label : (frame.page.placeholder ?? "Search…"), label: label, crumbs: crumbs, listId: listId, activeId: count > 0 ? optionId(active) : undefined, error: state.arg?.error, onChange: (value) => {
306
+ setStatusShown(false);
307
+ dispatch({ type: "query", value });
308
+ } }), _jsx(PaletteList, { id: listId, rows: rows, active: active, mac: mac, optionId: optionId, busy: Object.values(frame.providers).some((result) => result.state === "loading"), onActivate: (index, x, y) => {
309
+ const last = pointer.current;
310
+ if (last && last.x === x && last.y === y)
311
+ return;
312
+ pointer.current = { x, y };
313
+ dispatch({ type: "active", index, count });
314
+ }, onSelect: select }), _jsx(PaletteFooter, { count: count, nested: depth > 0 || state.arg !== null, status: statusShown ? (status?.result ?? null) : null, statusRun: statusShown ? (status?.run ?? null) : null })] })) : null }));
315
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @file src/components/palette/CommandPaletteButton.tsx
3
+ * @desc A header button that opens the mounted CommandPalette: a magnifier, optional text, and
4
+ * the hotkey hint ("Ctrl K", or "⌘K" once a Mac is detected after mount). Goes anywhere;
5
+ * it only dispatches the palette event.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Sat Oct 3, 2026
8
+ * @modified Sat Oct 3, 2026
9
+ */
10
+ import { type ButtonProps } from "../basics/Button.js";
11
+ /** Every Button prop except onClick, plus the accessible name. */
12
+ export type CommandPaletteButtonProps = Omit<ButtonProps, "onClick"> & {
13
+ /** The accessible name. Default "Open command palette". */
14
+ label?: string | undefined;
15
+ };
16
+ /**
17
+ * @function CommandPaletteButton
18
+ * @param props {CommandPaletteButtonProps} label, children (text beside the icon) and Button props
19
+ * @returns {JSX.Element} a ghost Button
20
+ */
21
+ export declare function CommandPaletteButton({ label, children, variant, ...props }: CommandPaletteButtonProps): import("react").JSX.Element;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @file src/components/palette/CommandPaletteButton.tsx
3
+ * @desc A header button that opens the mounted CommandPalette: a magnifier, optional text, and
4
+ * the hotkey hint ("Ctrl K", or "⌘K" once a Mac is detected after mount). Goes anywhere;
5
+ * it only dispatches the palette event.
6
+ * @author David @dvhsh (https://dvh.sh)
7
+ * @created Sat Oct 3, 2026
8
+ * @modified Sat Oct 3, 2026
9
+ */
10
+ "use client";
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ import { useEffect, useState } from "react";
13
+ import { Button } from "../basics/Button.js";
14
+ import { openCommandPalette } from "./paletteEvents.js";
15
+ import { isMac } from "./platform.js";
16
+ const KBD = "rounded border border-b3 bg-b5 px-1.5 font-sans text-c3 text-xs";
17
+ /**
18
+ * @function CommandPaletteButton
19
+ * @param props {CommandPaletteButtonProps} label, children (text beside the icon) and Button props
20
+ * @returns {JSX.Element} a ghost Button
21
+ */
22
+ export function CommandPaletteButton({ label = "Open command palette", children, variant = "ghost", ...props }) {
23
+ const [mac, setMac] = useState(false);
24
+ useEffect(() => setMac(isMac()), []);
25
+ return (_jsxs(Button, { type: "button", variant: variant, "aria-label": children === undefined || children === null ? label : undefined, onClick: () => openCommandPalette(), ...props, children: [_jsxs("svg", { "aria-hidden": "true", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "size-4", children: [_jsx("circle", { cx: "8.5", cy: "8.5", r: "5.5" }), _jsx("path", { d: "m13 13 4 4", strokeLinecap: "round" })] }), children, _jsx("kbd", { "aria-hidden": "true", className: KBD, children: mac ? "⌘K" : "Ctrl K" })] }));
26
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @file src/components/palette/PaletteFooter.tsx
3
+ * @desc The palette's bottom line (internal): the live region that announces the result count
4
+ * or the latest copy status, and the key hints (hidden on phones). No hooks, no cx.
5
+ * @author David @dvhsh (https://dvh.sh)
6
+ * @created Sat Oct 3, 2026
7
+ * @modified Sat Oct 3, 2026
8
+ */
9
+ /** The option count, whether Backspace goes back, and the latest status (keyed by its run). */
10
+ export type PaletteFooterProps = {
11
+ count: number;
12
+ nested: boolean;
13
+ status: string | null;
14
+ statusRun: number | null;
15
+ };
16
+ /**
17
+ * @function PaletteFooter
18
+ * @param props {PaletteFooterProps} count, nested, status and its run
19
+ * @returns {JSX.Element} the footer with its `<output>` live region
20
+ */
21
+ export declare function PaletteFooter({ count, nested, status, statusRun }: PaletteFooterProps): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const KBD = "rounded border border-b3 bg-b5 px-1 font-sans text-c3";
3
+ /**
4
+ * @function PaletteFooter
5
+ * @param props {PaletteFooterProps} count, nested, status and its run
6
+ * @returns {JSX.Element} the footer with its `<output>` live region
7
+ */
8
+ export function PaletteFooter({ count, nested, status, statusRun }) {
9
+ return (_jsxs("div", { className: "flex items-center justify-between gap-4 border-b4 border-t px-4 py-2 text-c4 text-xs", children: [_jsx("output", { "aria-live": "polite", className: "truncate", children: statusRun === null ? (`${count} ${count === 1 ? "result" : "results"}`) : (_jsx("span", { children: status }, statusRun)) }), _jsxs("span", { className: "hidden shrink-0 gap-3 sm:flex", children: [_jsxs("span", { children: [_jsx("kbd", { className: KBD, children: "\u2191" }), " ", _jsx("kbd", { className: KBD, children: "\u2193" }), " navigate"] }), _jsxs("span", { children: [_jsx("kbd", { className: KBD, children: "\u21B5" }), " select"] }), nested ? (_jsxs("span", { children: [_jsx("kbd", { className: KBD, children: "\u232B" }), " back"] })) : null, _jsxs("span", { children: [_jsx("kbd", { className: KBD, children: "esc" }), " ", nested ? "back" : "close"] })] })] }));
10
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file src/components/palette/PaletteInput.tsx
3
+ * @desc The palette's top line (internal): breadcrumb chips for nested pages or the argument
4
+ * being collected, the combobox input, and the validation message under it. The input is
5
+ * the dialog's one focusable thing, so its row shows focus: the bottom border turns h1 on
6
+ * focus-visible, the cue 0.7.0's fields use. The error line is a status region that is
7
+ * always mounted, so a message that appears is announced.
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Sat Oct 3, 2026
10
+ * @modified Sat Oct 3, 2026
11
+ */
12
+ import { type Ref } from "react";
13
+ /** The input's state and ids. */
14
+ export type PaletteInputProps = {
15
+ ref: Ref<HTMLInputElement>;
16
+ value: string;
17
+ placeholder: string;
18
+ label: string;
19
+ crumbs: readonly string[];
20
+ listId: string;
21
+ activeId: string | undefined;
22
+ error: string | undefined;
23
+ onChange: (value: string) => void;
24
+ };
25
+ /**
26
+ * @function PaletteInput
27
+ * @param props {PaletteInputProps} value, placeholder, name, crumbs, list and active ids, error
28
+ * @returns {JSX.Element} the crumbs, the combobox and its error line
29
+ */
30
+ export declare function PaletteInput({ ref, value, placeholder, label, crumbs, listId, activeId, error, onChange, }: PaletteInputProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @file src/components/palette/PaletteInput.tsx
3
+ * @desc The palette's top line (internal): breadcrumb chips for nested pages or the argument
4
+ * being collected, the combobox input, and the validation message under it. The input is
5
+ * the dialog's one focusable thing, so its row shows focus: the bottom border turns h1 on
6
+ * focus-visible, the cue 0.7.0's fields use. The error line is a status region that is
7
+ * always mounted, so a message that appears is announced.
8
+ * @author David @dvhsh (https://dvh.sh)
9
+ * @created Sat Oct 3, 2026
10
+ * @modified Sat Oct 3, 2026
11
+ */
12
+ "use client";
13
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
+ import { useId } from "react";
15
+ /**
16
+ * @function PaletteInput
17
+ * @param props {PaletteInputProps} value, placeholder, name, crumbs, list and active ids, error
18
+ * @returns {JSX.Element} the crumbs, the combobox and its error line
19
+ */
20
+ export function PaletteInput({ ref, value, placeholder, label, crumbs, listId, activeId, error, onChange, }) {
21
+ const errorId = `${useId()}-error`;
22
+ return (_jsxs("div", { className: "border-b4 border-b has-[input:focus-visible]:border-h1", children: [_jsxs("div", { className: "flex items-center gap-2 px-4 py-3", children: [crumbs.map((crumb, index) => (_jsx("span", { className: "shrink-0 rounded bg-b4 px-2 py-0.5 text-c3 text-xs", children: crumb }, index))), _jsx("input", { ref: ref, type: "text", role: "combobox", "aria-expanded": "true", "aria-controls": listId, "aria-activedescendant": activeId, "aria-autocomplete": "list", "aria-label": label, "aria-invalid": error ? true : undefined, "aria-describedby": errorId, autoComplete: "off", spellCheck: false, value: value, placeholder: placeholder, onChange: (event) => onChange(event.target.value), className: "min-w-0 flex-1 bg-transparent text-base text-c1 outline-hidden placeholder:text-c4 sm:text-lg" })] }), _jsx("p", { id: errorId, role: "status", className: error ? "px-4 pb-2 text-rose-300 text-xs" : "sr-only", children: error })] }));
23
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file src/components/palette/PaletteList.tsx
3
+ * @desc The palette's listbox (internal): option rows under group headings, hint rows as plain
4
+ * text, the active row scrolled into view. Hover makes a row active; click selects it.
5
+ * @author David @dvhsh (https://dvh.sh)
6
+ * @created Sat Oct 3, 2026
7
+ * @modified Sat Oct 3, 2026
8
+ */
9
+ import type { Row } from "./rows.js";
10
+ import type { Command } from "./types.js";
11
+ /** The rows, which option is active, how to id an option, and the handlers. */
12
+ export type PaletteListProps = {
13
+ id: string;
14
+ rows: readonly Row[];
15
+ active: number;
16
+ mac: boolean;
17
+ optionId: (index: number) => string;
18
+ /** A provider is searching: the listbox is aria-busy. */
19
+ busy?: boolean | undefined;
20
+ /** The pointer is over a row, at this position. */
21
+ onActivate: (index: number, x: number, y: number) => void;
22
+ onSelect: (command: Command) => void;
23
+ };
24
+ /**
25
+ * @function PaletteList
26
+ * @param props {PaletteListProps} rows, the active index, option ids and handlers
27
+ * @returns {JSX.Element} the `role="listbox"`
28
+ */
29
+ export declare function PaletteList({ id, rows, active, mac, optionId, busy, onActivate, onSelect, }: PaletteListProps): import("react").JSX.Element;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @file src/components/palette/PaletteList.tsx
3
+ * @desc The palette's listbox (internal): option rows under group headings, hint rows as plain
4
+ * text, the active row scrolled into view. Hover makes a row active; click selects it.
5
+ * @author David @dvhsh (https://dvh.sh)
6
+ * @created Sat Oct 3, 2026
7
+ * @modified Sat Oct 3, 2026
8
+ */
9
+ "use client";
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ import { useEffect } from "react";
12
+ import { PaletteRow } from "./PaletteRow.js";
13
+ const segments = (rows) => {
14
+ const out = [];
15
+ let index = 0;
16
+ for (const row of rows) {
17
+ if (row.kind === "hint") {
18
+ out.push({ kind: "hint", row });
19
+ continue;
20
+ }
21
+ const last = out[out.length - 1];
22
+ const item = { row, index: index++ };
23
+ if (last?.kind === "group" && last.group === row.group)
24
+ last.items.push(item);
25
+ else
26
+ out.push({ kind: "group", group: row.group, items: [item] });
27
+ }
28
+ return out;
29
+ };
30
+ /**
31
+ * @function PaletteList
32
+ * @param props {PaletteListProps} rows, the active index, option ids and handlers
33
+ * @returns {JSX.Element} the `role="listbox"`
34
+ */
35
+ export function PaletteList({ id, rows, active, mac, optionId, busy, onActivate, onSelect, }) {
36
+ // Runs for a new list too (its first row changes), so it scrolls back up when active stays 0.
37
+ const first = rows[0]?.id;
38
+ useEffect(() => {
39
+ if (first === undefined)
40
+ return;
41
+ document.getElementById(optionId(active))?.scrollIntoView?.({ block: "nearest" });
42
+ }, [active, optionId, first]);
43
+ return (_jsx("div", { id: id, role: "listbox", "aria-label": "Results", "aria-busy": busy ? true : undefined, className: "min-h-0 flex-1 overflow-y-auto p-2", children: segments(rows).map((segment) => segment.kind === "hint" ? (_jsx("div", { role: "option", "aria-disabled": "true", "aria-selected": "false", className: "px-3 py-2 text-c4 text-sm", children: segment.row.text }, segment.row.id)) : (_jsxs("div", { role: "group", "aria-labelledby": `${id}-${segment.items[0]?.index}-h`, children: [_jsx("div", { id: `${id}-${segment.items[0]?.index}-h`, className: "px-3 pt-3 pb-1 font-bold text-c3 text-xs", children: segment.group }), segment.items.map(({ row, index }) => (_jsx(PaletteRow, { id: optionId(index), command: row.command, ranges: row.ranges, active: index === active, mac: mac, onHover: (x, y) => onActivate(index, x, y), onSelect: () => onSelect(row.command) }, row.id)))] }, `${segment.group}:${segment.items[0]?.row.id}`))) }));
44
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @file src/components/palette/PaletteRow.tsx
3
+ * @desc One option in the palette's list (internal): icon slot, the title with its matched
4
+ * characters marked, the subtitle, and the shortcut as kbd parts. Finished class strings,
5
+ * no hooks: the list sets aria-selected and the pointer handlers. Keyboard handling lives
6
+ * on the dialog, so this isn't focusable (aria-activedescendant names it).
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ import type { FuzzyMatch } from "./fuzzy.js";
12
+ import type { Command } from "./types.js";
13
+ /** The row's id (for aria-activedescendant), its command and spans, whether it's active. */
14
+ export type PaletteRowProps = {
15
+ id: string;
16
+ command: Command;
17
+ ranges: FuzzyMatch["ranges"];
18
+ active: boolean;
19
+ mac: boolean;
20
+ /** The pointer moved over the row; gets the pointer's position, so a stale move can be told. */
21
+ onHover: (x: number, y: number) => void;
22
+ onSelect: () => void;
23
+ };
24
+ /**
25
+ * @function PaletteRow
26
+ * @param props {PaletteRowProps} the option's id, command, matched spans, active state, platform
27
+ * and pointer handlers
28
+ * @returns {JSX.Element} a `role="option"` row
29
+ */
30
+ export declare function PaletteRow({ id, command, ranges, active, mac, onHover, onSelect, }: PaletteRowProps): import("react").JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { displayShortcut, parseShortcut } from "./hotkeys.js";
3
+ // The active row shows its state with an h1 edge, not background alone: bg-b4 on b6 is only
4
+ // 1.5:1. min-h-9 keeps every row a 36px target.
5
+ const ROW = "flex min-h-9 cursor-default items-center gap-3 rounded-lg border-l-2 border-transparent px-3 py-2";
6
+ const ACTIVE = `${ROW} border-h1 bg-b4 forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]`;
7
+ const KBD = "rounded border border-b3 bg-b5 px-1.5 font-sans text-c3 text-xs";
8
+ const highlighted = (title, ranges) => {
9
+ const parts = [];
10
+ let at = 0;
11
+ for (const [start, end] of ranges) {
12
+ if (start > at)
13
+ parts.push(title.slice(at, start));
14
+ parts.push(_jsx("mark", { className: "bg-transparent text-h1", children: title.slice(start, end) }, start));
15
+ at = end;
16
+ }
17
+ if (at < title.length)
18
+ parts.push(title.slice(at));
19
+ return parts;
20
+ };
21
+ /**
22
+ * @function PaletteRow
23
+ * @param props {PaletteRowProps} the option's id, command, matched spans, active state, platform
24
+ * and pointer handlers
25
+ * @returns {JSX.Element} a `role="option"` row
26
+ */
27
+ export function PaletteRow({ id, command, ranges, active, mac, onHover, onSelect, }) {
28
+ const shortcut = command.shortcut ? parseShortcut(command.shortcut) : null;
29
+ return (_jsxs("div", { id: id, role: "option", "aria-selected": active, className: active ? ACTIVE : ROW, onPointerMove: (event) => onHover(event.clientX, event.clientY),
30
+ // Keep focus in the input: a click on a row must not blur it, so the next prompt
31
+ // (an argument, a nested page) can be typed straight away.
32
+ onPointerDown: (event) => event.preventDefault(), onClick: onSelect, children: [command.icon ? (_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center text-c3", children: command.icon })) : null, _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate font-bold text-c1 text-sm", children: highlighted(command.title, ranges) }), command.subtitle ? (_jsx("span", { className: "truncate text-c4 text-xs", children: command.subtitle })) : null] }), shortcut ? (_jsx("span", { className: "flex shrink-0 gap-1", children: displayShortcut(shortcut, mac).map((part, index) => (_jsx("kbd", { className: KBD, children: part }, index))) })) : null] }));
33
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file src/components/palette/calc.ts
3
+ * @desc The palette's calculator, pure and without eval: a tokenizer and the shunting-yard
4
+ * algorithm over + - * / % ^, unary minus, parentheses, k and m number suffixes, pi and e,
5
+ * and sqrt, abs, round, floor, ceil, min and max. Anything it can't parse, and any result
6
+ * that isn't finite (division by zero), is null.
7
+ * @author David @dvhsh (https://dvh.sh)
8
+ * @created Sat Oct 3, 2026
9
+ * @modified Sat Oct 3, 2026
10
+ */
11
+ /**
12
+ * @function evaluate
13
+ * @param expression {string} what was typed, e.g. "1.5k * (2 + sqrt(9))"
14
+ * @returns {number | null} the value, or null when it doesn't parse or isn't finite
15
+ */
16
+ export declare function evaluate(expression: string): number | null;
17
+ /**
18
+ * @function formatResult
19
+ * @param value {number} a result
20
+ * @returns {string} up to 10 significant digits, trailing zeros dropped, "-0" as "0"
21
+ */
22
+ export declare function formatResult(value: number): string;
23
+ /**
24
+ * @function isBareNumber
25
+ * @param expression {string} what was typed
26
+ * @returns {boolean} true for just a number ("2", " -2.5 "), when the palette shows no "= 2" row
27
+ */
28
+ export declare const isBareNumber: (expression: string) => boolean;