@eifi1/ui-kit 0.9.0 → 0.10.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.
- package/README.md +21 -11
- package/dist/components/alert-banner.d.ts +32 -5
- package/dist/components/alert-banner.js +57 -14
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +4 -0
- package/dist/components/breadcrumbs.d.ts +60 -0
- package/dist/components/breadcrumbs.js +88 -0
- package/dist/components/breadcrumbs.js.map +1 -0
- package/dist/components/bulk-action-bar.d.ts +66 -0
- package/dist/components/bulk-action-bar.js +149 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/calculator.d.ts +4 -0
- package/dist/components/chip.d.ts +31 -5
- package/dist/components/chip.js +85 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/copy-button.d.ts +13 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/date-picker.d.ts +22 -2
- package/dist/components/date-picker.js +11 -3
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +43 -6
- package/dist/components/description-list.js +91 -11
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +7 -0
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +57 -2
- package/dist/components/disclosure.js +24 -12
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -4
- package/dist/components/file-dropzone.js +2 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +23 -4
- package/dist/components/full-bleed-dialog.js +11 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/list.d.ts +174 -0
- package/dist/components/list.js +215 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +99 -0
- package/dist/components/menu-item.js +83 -0
- package/dist/components/menu-item.js.map +1 -0
- package/dist/components/modal.d.ts +23 -1
- package/dist/components/modal.js +36 -10
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-field.d.ts +4 -0
- package/dist/components/number-input.d.ts +4 -0
- package/dist/components/numpad-sheet.d.ts +4 -0
- package/dist/components/page-contents.js +1 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.d.ts +37 -0
- package/dist/components/page-header.js +36 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/series-chart-labels.d.ts +3 -0
- package/dist/components/series-chart-labels.js +2 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +137 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/status-dot.d.ts +49 -0
- package/dist/components/status-dot.js +52 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +39 -6
- package/dist/components/table.js +43 -4
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +63 -0
- package/dist/components/text.js +23 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/toast.d.ts +167 -0
- package/dist/components/toast.js +226 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle-group.d.ts +29 -1
- package/dist/components/toggle-group.js +52 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +38 -15
- package/dist/components/tooltip.js +85 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +104 -4
- package/dist/components/ui.js +112 -8
- package/dist/components/ui.js.map +1 -1
- package/dist/hooks/use-close-transition.d.ts +7 -1
- package/dist/hooks/use-close-transition.js +5 -2
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.js +1 -1
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
- package/dist/i18n/defaults.d.ts +4 -0
- package/dist/i18n/defaults.js +9 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +8 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +4 -0
- package/dist/i18n/locales/de-CH.d.ts +4 -0
- package/dist/i18n/locales/de-informal.d.ts +4 -0
- package/dist/i18n/locales/de.d.ts +4 -0
- package/dist/i18n/locales/de.js +21 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +4 -0
- package/dist/i18n/locales/es.js +21 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +4 -0
- package/dist/i18n/locales/fr.js +21 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +4 -0
- package/dist/i18n/locales/hu.js +21 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +4 -0
- package/dist/i18n/locales/it.js +21 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +4 -0
- package/dist/i18n/locales/zh.js +21 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +15 -7
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +9 -0
- package/dist/lib/clipping.js +14 -0
- package/dist/lib/clipping.js.map +1 -0
- package/dist/search/command-palette.d.ts +19 -2
- package/dist/search/command-palette.js +16 -5
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +26 -1
- package/dist/search/global-search.js +29 -13
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +1 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +2 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +1 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +16 -12
- package/src/components/alert-banner.tsx +117 -18
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +256 -0
- package/src/components/chip.tsx +128 -7
- package/src/components/copy-button.tsx +29 -7
- package/src/components/date-picker.tsx +49 -1
- package/src/components/description-list.tsx +168 -15
- package/src/components/dialog-frame.tsx +7 -0
- package/src/components/disclosure.tsx +93 -21
- package/src/components/file-dropzone.tsx +9 -10
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +417 -0
- package/src/components/menu-item.tsx +214 -0
- package/src/components/modal.tsx +80 -17
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +253 -3
- package/src/components/status-dot.tsx +99 -0
- package/src/components/table.tsx +91 -6
- package/src/components/text.tsx +89 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +82 -3
- package/src/components/tooltip.tsx +186 -100
- package/src/components/ui.tsx +237 -8
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/kit-labels.tsx +8 -0
- package/src/i18n/locales/de.ts +20 -0
- package/src/i18n/locales/es.ts +20 -0
- package/src/i18n/locales/fr.ts +20 -0
- package/src/i18n/locales/hu.ts +20 -0
- package/src/i18n/locales/it.ts +20 -0
- package/src/i18n/locales/zh.ts +20 -0
- package/src/index.ts +26 -0
- package/src/lib/clipping.ts +16 -0
- package/src/search/command-palette.tsx +42 -5
- package/src/search/global-search.tsx +59 -18
- package/src/wizard/stepper-nav.tsx +2 -1
- package/src/wizard/types.ts +2 -2
- package/src/wizard/use-wizard.ts +3 -3
- package/src/wizard/wizard-summary.tsx +14 -10
- package/tokens.css +97 -0
|
@@ -35,6 +35,7 @@ const DEFAULT_COMMAND_PALETTE_LABELS = {
|
|
|
35
35
|
submit: "Search",
|
|
36
36
|
close: "Close"
|
|
37
37
|
};
|
|
38
|
+
const isThenable = (value) => typeof value?.then === "function";
|
|
38
39
|
const { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;
|
|
39
40
|
function useCommandKey(onOpen) {
|
|
40
41
|
useEffect(() => {
|
|
@@ -58,6 +59,7 @@ function CommandPalette({
|
|
|
58
59
|
searchOn = "input",
|
|
59
60
|
redactLabels = false,
|
|
60
61
|
fullScreenOnPhone = true,
|
|
62
|
+
density = "compact",
|
|
61
63
|
labels
|
|
62
64
|
}) {
|
|
63
65
|
const l = useKitLabels("commandPalette", DEFAULTS_WITHOUT_DIALOG, labels);
|
|
@@ -97,6 +99,7 @@ function CommandPalette({
|
|
|
97
99
|
const searchRef = useRef(search);
|
|
98
100
|
searchRef.current = search;
|
|
99
101
|
const reqId = useRef(0);
|
|
102
|
+
const syncRef = useRef(void 0);
|
|
100
103
|
const shownQuery = useRef(null);
|
|
101
104
|
const activeIdRef = useRef(void 0);
|
|
102
105
|
useOverlayHistory(open, onClose);
|
|
@@ -112,10 +115,13 @@ function CommandPalette({
|
|
|
112
115
|
useEffect(() => {
|
|
113
116
|
if (!open) return;
|
|
114
117
|
const id = ++reqId.current;
|
|
115
|
-
setLoading(true);
|
|
118
|
+
if (syncRef.current === false) setLoading(true);
|
|
116
119
|
const run = async () => {
|
|
117
120
|
try {
|
|
118
|
-
const
|
|
121
|
+
const answer = searchRef.current(query);
|
|
122
|
+
syncRef.current = !isThenable(answer);
|
|
123
|
+
if (!syncRef.current) setLoading(true);
|
|
124
|
+
const r = await answer;
|
|
119
125
|
if (reqId.current === id) {
|
|
120
126
|
const keep = shownQuery.current === query ? activeIdRef.current : void 0;
|
|
121
127
|
const at = keep === void 0 ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);
|
|
@@ -169,9 +175,10 @@ function CommandPalette({
|
|
|
169
175
|
};
|
|
170
176
|
useEffect(() => {
|
|
171
177
|
const el = listRef.current?.querySelector(`[data-index="${active}"]`);
|
|
172
|
-
el?.scrollIntoView({ block: "nearest" });
|
|
178
|
+
el?.scrollIntoView?.({ block: "nearest" });
|
|
173
179
|
}, [active]);
|
|
174
180
|
if (!open) return null;
|
|
181
|
+
const comfortable = density === "comfortable";
|
|
175
182
|
let flatIndex = -1;
|
|
176
183
|
return createPortal(
|
|
177
184
|
/* @__PURE__ */ jsx(
|
|
@@ -282,7 +289,10 @@ function CommandPalette({
|
|
|
282
289
|
"li",
|
|
283
290
|
{
|
|
284
291
|
role: "presentation",
|
|
285
|
-
className:
|
|
292
|
+
className: cn(
|
|
293
|
+
"flex items-center gap-2.5 px-3 text-xs text-[var(--text-placeholder)]",
|
|
294
|
+
comfortable ? "min-h-11 py-2.5" : "py-2"
|
|
295
|
+
),
|
|
286
296
|
children: [
|
|
287
297
|
item.icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center", children: item.icon }),
|
|
288
298
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label })
|
|
@@ -302,7 +312,8 @@ function CommandPalette({
|
|
|
302
312
|
"data-index": idx,
|
|
303
313
|
onMouseMove: () => setActive(idx),
|
|
304
314
|
className: cn(
|
|
305
|
-
"flex w-full items-center gap-2.5 px-3
|
|
315
|
+
"flex w-full items-center gap-2.5 px-3 text-start",
|
|
316
|
+
comfortable ? "min-h-11 py-2.5 text-base" : "py-2 text-sm",
|
|
306
317
|
isActive ? "bg-[var(--bg-active)] text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
|
|
307
318
|
)
|
|
308
319
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CornerDownLeft, Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { PHONE_QUERY } from \"../components/ui\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n /** Marks this row's label and hint `data-private`, so session replay and screenshot\n * tooling masks them — a payee, an account name, an amount. Overrides the palette's\n * {@link CommandPaletteProps.redactLabels} either way, so `false` un-masks one\n * harmless row (\"Settings\") in a palette that masks by default. */\n redact?: boolean;\n /**\n * `\"status\"` makes the row a line of text rather than a choice — a group's\n * \"Searching…\" while its source is still out, or its error. It is skipped by the\n * arrow keys and ↵, has no hover highlight, and its `onSelect` is never called.\n * Default `\"option\"`.\n */\n kind?: \"option\" | \"status\";\n}\n\n/** Group items preserving first-seen group order; `flat` is the navigable rows only. */\nfunction groupItems(items: CommandItem[]) {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of items) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat: order.flatMap((g) => byGroup.get(g)!).filter((item) => item.kind !== \"status\"),\n };\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n /**\n * Shown in place of the results when the `search` provider throws or its promise\n * rejects. Optional for the same reason as `dialog`; the default always carries it.\n */\n error?: string;\n /** Names the \"×\" that empties the field. Optional like `dialog`. */\n clear?: string;\n /** Names the button that commits the typed text when `searchOn=\"submit\"`. */\n submit?: string;\n /** Names the close button of the full-screen phone presentation, which has no\n * backdrop to tap and, on a phone, no Escape key. */\n close?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n error: \"Search failed. Try again.\",\n clear: \"Clear search\",\n submit: \"Search\",\n close: \"Close\",\n};\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n /**\n * The search text, CONTROLLED. Keksdose binds it to the URL's `q`, so a search\n * survives a reload, can be shared as a link, and Back walks through it — none of\n * which state inside the palette can do.\n *\n * Leave it out and the palette keeps its own, as before, starting empty on every\n * open. Given, it is never reset on open: the owner decides what an open shows, and\n * a palette that blanked the URL's `q` each time would fight its own address bar.\n * `onQueryChange` is then how a keystroke gets anywhere, since it is the only writer.\n */\n query?: string;\n /** Every edit of the field's text. Fires in both modes — uncontrolled, it only\n * observes. */\n onQueryChange?: (query: string) => void;\n /**\n * When the typed text becomes THE query — the one `search` runs on and\n * `onQueryChange` reports.\n *\n * - `\"input\"` (default): every keystroke, debounced for the search, as before.\n * - `\"submit\"`: typing edits a draft; the query is committed by ↵ or the submit\n * button beside the field. Until then the results stay on the last committed query\n * and `onQueryChange` stays quiet — which is what an owner binding `query` to the\n * URL's `q` needs, or every keystroke re-filters the page behind and pushes history.\n * ↵ with a draft that differs from the committed query COMMITS; ↵ with nothing new\n * typed chooses the highlighted row, as in `\"input\"` mode. The draft follows any\n * outside change of the committed query (Back through the URL), and an uncommitted\n * draft is dropped when the palette is closed.\n *\n * The clear \"×\" commits the empty query in both modes: an emptied box over results\n * (or a page) still filtered by the old text is the stuck state Keksdose #423 was.\n */\n searchOn?: \"input\" | \"submit\";\n /** Mark every row's label and hint `data-private` (see {@link CommandItem.redact},\n * which overrides it per row). Off by default. */\n redactLabels?: boolean;\n /**\n * Below the phone breakpoint ({@link PHONE_QUERY}) the palette fills the screen:\n * no inset, no rounded panel, the field pinned at the top inside the safe areas and\n * the list scrolling under it, with a close button since there is no backdrop to tap.\n * On by default — a top-centred card at 70vh leaves little room once the keyboard is\n * up. `false` keeps the card on every viewport.\n */\n fullScreenOnPhone?: boolean;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n query: queryProp,\n onQueryChange,\n searchOn = \"input\",\n redactLabels = false,\n fullScreenOnPhone = true,\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [ownQuery, setOwnQuery] = useState(\"\");\n const controlled = queryProp !== undefined;\n const query = controlled ? queryProp : ownQuery;\n const setQuery = (next: string) => {\n if (!controlled) setOwnQuery(next);\n onQueryChange?.(next);\n };\n const submitMode = searchOn === \"submit\";\n // The field's text in `\"submit\"` mode. It follows the committed query whenever that\n // changes — adjusted during render, React's pattern for state derived from a prop,\n // rather than in an effect that would paint the stale draft first.\n const [draft, setDraft] = useState(query);\n const [draftBase, setDraftBase] = useState(query);\n if (draftBase !== query) {\n setDraftBase(query);\n setDraft(query);\n }\n const fieldText = submitMode ? draft : query;\n const commit = (next: string) => {\n setDraft(next);\n setQuery(next);\n };\n const dirty = submitMode && draft !== query;\n const isPhone = useMediaQuery(PHONE_QUERY, false) && fullScreenOnPhone;\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const [active, setActive] = useState(0);\n // Per instance: these were the literal \"command-palette-list\" / \"command-item-<id>\",\n // so two palettes on one page (an app-wide one and a scoped one) pointed each\n // other's `aria-controls` and `aria-activedescendant` at the wrong list.\n const uid = useId();\n const listId = `${uid}-list`;\n const itemId = (id: string) => `${uid}-item-${id}`;\n const groupId = (index: number) => `${uid}-group-${index}`;\n const dialogRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const searchRef = useRef(search);\n searchRef.current = search;\n const reqId = useRef(0);\n /** The query the results on screen answer, and the row highlighted in them — so a\n * re-run for the SAME query (a `revision` change: a source streaming in) keeps the\n * highlight on its row instead of throwing it back to the top mid-arrow. */\n const shownQuery = useRef<string | null>(null);\n const activeIdRef = useRef<string | undefined>(undefined);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // A modal like any other: Tab stays inside, the page behind does not scroll, and\n // focus goes back to whatever opened it — the palette used to drop it on <body>.\n // Initial focus is the field, not the container: typing is the only reason to open it.\n useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });\n useBodyScrollLock(open);\n\n // Reset when opened — the palette's OWN text only; a controlled query belongs to\n // its owner (see the prop).\n useEffect(() => {\n if (!open) return;\n if (!controlled) setOwnQuery(\"\");\n // An uncommitted draft does not outlive the open it was typed in.\n setDraft(controlled ? query : \"\");\n setActive(0);\n shownQuery.current = null;\n // `controlled` is read at the open, not watched: a caller switching modes while\n // the palette is open is not a reason to wipe what is typed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n setLoading(true);\n const run = async () => {\n try {\n const r = await Promise.resolve(searchRef.current(query));\n if (reqId.current === id) {\n const keep = shownQuery.current === query ? activeIdRef.current : undefined;\n const at = keep === undefined ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);\n shownQuery.current = query;\n setResults(r);\n setFailed(false);\n setActive(Math.max(at, 0));\n }\n } catch (err) {\n // There was no catch: the previous query's results stayed on screen as if they\n // answered this one, and the rejection went unhandled. Clear them and say so.\n if (reqId.current === id) {\n shownQuery.current = null;\n setResults([]);\n setFailed(true);\n setActive(0);\n }\n console.error(\"CommandPalette: search failed\", err);\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat } = useMemo(() => groupItems(results), [results]);\n useEffect(() => {\n activeIdRef.current = flat[active]?.id;\n });\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (dirty) commit(draft);\n else choose(flat[active]);\n }\n };\n\n // Escape on the whole dialog, not only the field: once Tab has moved focus to a row\n // (or a footer control), Escape used to do nothing at all.\n const onDialogKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n el?.scrollIntoView({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n let flatIndex = -1;\n return createPortal(\n <div\n className={cn(\n \"fixed inset-0 z-[70] flex items-start justify-center bg-black/40\",\n isPhone ? \"p-0\" : \"p-4 pt-[10vh]\",\n )}\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n // The trap's fallback target when the field is not there to take focus.\n tabIndex={-1}\n // Escape bubbling up from anything inside — a dialog is where it belongs.\n onKeyDown={onDialogKeyDown}\n data-fullscreen={isPhone || undefined}\n className={cn(\n \"flex w-full flex-col overflow-hidden bg-[var(--bg-surface)] outline-none\",\n isPhone\n ? \"h-full\"\n : \"max-h-[70vh] max-w-xl rounded-xl border border-[var(--border)] shadow-2xl\",\n )}\n // Full screen means edge to edge, so the notch, the home indicator and the\n // rounded corners are the panel's to keep clear of.\n style={\n isPhone\n ? {\n paddingTop: \"max(0px, env(safe-area-inset-top))\",\n paddingBottom: \"max(0px, env(safe-area-inset-bottom))\",\n paddingLeft: \"max(0px, env(safe-area-inset-left))\",\n paddingRight: \"max(0px, env(safe-area-inset-right))\",\n }\n : undefined\n }\n >\n <div className=\"flex shrink-0 items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}\n value={fieldText}\n onChange={(e) => (submitMode ? setDraft(e.target.value) : setQuery(e.target.value))}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n enterKeyHint={submitMode ? \"search\" : undefined}\n className={cn(\n \"w-full bg-transparent py-3 outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\",\n // 16px on a phone, or iOS zooms the page in on focus.\n isPhone ? \"text-base\" : \"text-sm\",\n )}\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n {fieldText !== \"\" && (\n <button\n type=\"button\"\n aria-label={l.clear ?? DEFAULT_COMMAND_PALETTE_LABELS.clear}\n onClick={() => {\n commit(\"\");\n // Clearing starts the next query far more often than it ends this one.\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n {dirty && (\n <button\n type=\"button\"\n aria-label={l.submit ?? DEFAULT_COMMAND_PALETTE_LABELS.submit}\n onClick={() => {\n commit(draft);\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <CornerDownLeft className=\"size-4\" />\n </button>\n )}\n {isPhone && (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"-me-1 shrink-0 rounded px-1 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n {l.close ?? DEFAULT_COMMAND_PALETTE_LABELS.close}\n </button>\n )}\n </div>\n\n {/* The status lines sit outside the listbox: a listbox owns options and groups,\n and \"No results\" is neither. */}\n {failed && !loading && (\n <div role=\"alert\" className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">\n {l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}\n </div>\n )}\n {/* `results`, not `flat`: a group whose only row is its \"Searching…\" line is not\n an empty result. */}\n {!failed && results.length === 0 && !loading && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</div>\n )}\n {/* Listbox grouping: each group is a `role=\"group\"` named by its heading, and\n the list markup in between is presentational so the listbox's children are\n groups and options only — as a plain <li>/<ul> tree a reader announced\n \"list, 2 items\" between options and never tied the heading to its rows. */}\n <ul id={listId} ref={listRef} role=\"listbox\" aria-label={dialogName} className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {groups.map(({ group, items }, groupIndex) => (\n <li key={group} role=\"presentation\">\n <div\n id={groupId(groupIndex)}\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\"\n >\n {group}\n </div>\n <ul role=\"group\" aria-labelledby={groupId(groupIndex)}>\n {items.map((item) => {\n if (item.kind === \"status\") {\n return (\n <li\n key={item.id}\n role=\"presentation\"\n className=\"flex items-center gap-2.5 px-3 py-2 text-xs text-[var(--text-placeholder)]\"\n >\n {item.icon && <span className=\"flex size-4 shrink-0 items-center justify-center\">{item.icon}</span>}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n </li>\n );\n }\n flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\n const redact = item.redact ?? redactLabels;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: itemId(item.id),\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 py-2 text-start text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span data-private={redact || undefined} className=\"min-w-0 flex-1 truncate\">\n {item.label}\n </span>\n {item.hint && (\n <span data-private={redact || undefined} className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">\n {item.hint}\n </span>\n )}\n </>\n );\n return (\n <li key={item.id} role=\"presentation\">\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/search/command-palette.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { CornerDownLeft, Search, X } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { PHONE_QUERY } from \"../components/ui\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useOverlayHistory } from \"../hooks/use-overlay-history\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { useBodyScrollLock } from \"../hooks/use-body-scroll-lock\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\nexport interface CommandItem {\n id: string;\n /** Primary text. */\n label: string;\n /** Secondary text on the right (e.g. an amount, a date, a group hint). */\n hint?: string;\n /** Heading this item is grouped under. */\n group: string;\n /** Leading icon / element. */\n icon?: ReactNode;\n /** Invoked when the item is chosen (navigate, run an action, …). */\n onSelect: () => void;\n /** Where this item leads, when it is a navigation rather than an action. Renders\n * the row as a real anchor so it can be middle-/⌘-clicked into a new tab\n * (Keksdose feedback #451); `onSelect` still runs for a plain click and for ↵, so\n * in-app navigation stays client-side. `href` only adds what the browser can do\n * with a link and JavaScript cannot fake — the rejected alternative, an\n * `onAuxClick` calling `window.open`, gets the middle click and nothing else. */\n href?: string;\n /** Marks this row's label and hint `data-private`, so session replay and screenshot\n * tooling masks them — a payee, an account name, an amount. Overrides the palette's\n * {@link CommandPaletteProps.redactLabels} either way, so `false` un-masks one\n * harmless row (\"Settings\") in a palette that masks by default. */\n redact?: boolean;\n /**\n * `\"status\"` makes the row a line of text rather than a choice — a group's\n * \"Searching…\" while its source is still out, or its error. It is skipped by the\n * arrow keys and ↵, has no hover highlight, and its `onSelect` is never called.\n * Default `\"option\"`.\n */\n kind?: \"option\" | \"status\";\n}\n\n/** Group items preserving first-seen group order; `flat` is the navigable rows only. */\nfunction groupItems(items: CommandItem[]) {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of items) {\n if (!byGroup.has(item.group)) {\n byGroup.set(item.group, []);\n order.push(item.group);\n }\n byGroup.get(item.group)!.push(item);\n }\n return {\n groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),\n flat: order.flatMap((g) => byGroup.get(g)!).filter((item) => item.kind !== \"status\"),\n };\n}\n\nexport interface CommandPaletteLabels {\n placeholder: string;\n empty: string;\n loading: string;\n /** Hint shown in the footer, e.g. \"↑↓ to navigate · ↵ to select · esc to close\". */\n hint?: string;\n /**\n * Accessible name of the dialog itself. It used to borrow `placeholder`, which is\n * written to sit greyed-out in an empty box (\"Search…\", \"Type a command\"), not to\n * name a window — and a reader heard the ellipsis read out as part of the name.\n *\n * Optional, unlike `placeholder`, so a translation annotated as\n * `CommandPaletteLabels` keeps compiling; the default always carries it.\n */\n dialog?: string;\n /**\n * Shown in place of the results when the `search` provider throws or its promise\n * rejects. Optional for the same reason as `dialog`; the default always carries it.\n */\n error?: string;\n /** Names the \"×\" that empties the field. Optional like `dialog`. */\n clear?: string;\n /** Names the button that commits the typed text when `searchOn=\"submit\"`. */\n submit?: string;\n /** Names the close button of the full-screen phone presentation, which has no\n * backdrop to tap and, on a phone, no Escape key. */\n close?: string;\n}\n\nexport const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {\n placeholder: \"Search…\",\n empty: \"No results\",\n loading: \"Searching…\",\n dialog: \"Search\",\n error: \"Search failed. Try again.\",\n clear: \"Clear search\",\n submit: \"Search\",\n close: \"Close\",\n};\n\n/**\n * How tall the palette's rows are. `\"compact\"` (the default) is the desktop list the\n * palette always drew, 36px a row; `\"comfortable\"` gives every row the kit's 44px touch\n * target (`min-h-11`) and body-size text — keksdose's phone search, where a 36px row\n * under a thumb picks its neighbour.\n */\nexport type CommandPaletteDensity = \"compact\" | \"comfortable\";\n\n/** A provider's answer is a promise — the only case with anything to wait for. */\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof (value as PromiseLike<unknown> | null)?.then === \"function\";\n\n/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —\n * see the dialog's `aria-label` for why that distinction matters. */\nconst { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;\n\ninterface CommandPaletteProps {\n open: boolean;\n onClose: () => void;\n /**\n * Returns results for a query (called debounced as the user types; an empty\n * query is allowed — return defaults like pages/recent). Sync or async; stale\n * async responses are ignored.\n *\n * A SYNCHRONOUS provider (one returning an array, not a promise) never shows the\n * \"Searching…\" hint: its answer is there the moment it is asked, and the hint only\n * flashed through the debounce before results that were never late — keksdose's\n * transaction search filters a list already in memory. The palette tells the two\n * apart by what the provider RETURNS, call by call, so there is no prop to keep in\n * step with the implementation: an async provider shows the hint from the first\n * promise on (and during the debounce of every later query), a sync one never.\n *\n * Its IDENTITY is not a re-run signal — it is read through a ref, so the palette\n * never re-searches merely because the caller rebuilt the callback. That is\n * deliberate: a provider closing over a list rendered from a query allocates a new\n * function on most renders, and re-running the search on each would make typing\n * stutter. Use {@link CommandPaletteProps.revision} when the DATA behind the\n * provider changes.\n */\n search: (query: string) => CommandItem[] | Promise<CommandItem[]>;\n /**\n * Anything whose change means the current results are out of date — typically the\n * arrays the provider searches over. Compared by identity (as an effect dependency\n * would be), so pass stable references: a `data = []` default allocates a fresh\n * array on every render while a query is pending and would re-search forever.\n *\n * Without it, a provider whose data arrives AFTER the palette opened never gets\n * asked again: the user's last keystroke settles the debounce, the queries resolve a\n * moment later, and the groups they would have contributed are simply absent until\n * another keystroke. That is the ordinary first open on a cold cache.\n */\n revision?: unknown;\n /**\n * The search text, CONTROLLED. Keksdose binds it to the URL's `q`, so a search\n * survives a reload, can be shared as a link, and Back walks through it — none of\n * which state inside the palette can do.\n *\n * Leave it out and the palette keeps its own, as before, starting empty on every\n * open. Given, it is never reset on open: the owner decides what an open shows, and\n * a palette that blanked the URL's `q` each time would fight its own address bar.\n * `onQueryChange` is then how a keystroke gets anywhere, since it is the only writer.\n */\n query?: string;\n /** Every edit of the field's text. Fires in both modes — uncontrolled, it only\n * observes. */\n onQueryChange?: (query: string) => void;\n /**\n * When the typed text becomes THE query — the one `search` runs on and\n * `onQueryChange` reports.\n *\n * - `\"input\"` (default): every keystroke, debounced for the search, as before.\n * - `\"submit\"`: typing edits a draft; the query is committed by ↵ or the submit\n * button beside the field. Until then the results stay on the last committed query\n * and `onQueryChange` stays quiet — which is what an owner binding `query` to the\n * URL's `q` needs, or every keystroke re-filters the page behind and pushes history.\n * ↵ with a draft that differs from the committed query COMMITS; ↵ with nothing new\n * typed chooses the highlighted row, as in `\"input\"` mode. The draft follows any\n * outside change of the committed query (Back through the URL), and an uncommitted\n * draft is dropped when the palette is closed.\n *\n * The clear \"×\" commits the empty query in both modes: an emptied box over results\n * (or a page) still filtered by the old text is the stuck state Keksdose #423 was.\n */\n searchOn?: \"input\" | \"submit\";\n /** Mark every row's label and hint `data-private` (see {@link CommandItem.redact},\n * which overrides it per row). Off by default. */\n redactLabels?: boolean;\n /**\n * Below the phone breakpoint ({@link PHONE_QUERY}) the palette fills the screen:\n * no inset, no rounded panel, the field pinned at the top inside the safe areas and\n * the list scrolling under it, with a close button since there is no backdrop to tap.\n * On by default — a top-centred card at 70vh leaves little room once the keyboard is\n * up. `false` keeps the card on every viewport.\n */\n fullScreenOnPhone?: boolean;\n /** Row height — see {@link CommandPaletteDensity}. Default `\"compact\"`. */\n density?: CommandPaletteDensity;\n labels?: Partial<CommandPaletteLabels>;\n}\n\n/** Registers a global ⌘K / Ctrl-K shortcut that calls `onOpen`. */\nexport function useCommandKey(onOpen: () => void): void {\n useEffect(() => {\n const handler = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && (e.key === \"k\" || e.key === \"K\")) {\n e.preventDefault();\n onOpen();\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [onOpen]);\n}\n\n/**\n * A ⌘K-style command palette: a top-centred modal with a search field and a\n * grouped, keyboard-navigable result list. Domain-free — the app supplies\n * results via the `search` provider (pages, actions, records, …); each result\n * carries its own `onSelect`.\n */\nexport function CommandPalette({\n open,\n onClose,\n search,\n revision,\n query: queryProp,\n onQueryChange,\n searchOn = \"input\",\n redactLabels = false,\n fullScreenOnPhone = true,\n density = \"compact\",\n labels,\n}: CommandPaletteProps) {\n const l = useKitLabels(\"commandPalette\", DEFAULTS_WITHOUT_DIALOG, labels);\n // `dialog` is new, and until it existed the dialog was named by `placeholder`. An\n // app that translated `placeholder` and has not heard of `dialog` would otherwise\n // go from a German name to an English \"Search\" on update — so a translated\n // placeholder keeps doing the job until a `dialog` is supplied.\n const dialogName =\n l.dialog ??\n (l.placeholder !== DEFAULT_COMMAND_PALETTE_LABELS.placeholder ? l.placeholder : DEFAULT_DIALOG_NAME);\n const [ownQuery, setOwnQuery] = useState(\"\");\n const controlled = queryProp !== undefined;\n const query = controlled ? queryProp : ownQuery;\n const setQuery = (next: string) => {\n if (!controlled) setOwnQuery(next);\n onQueryChange?.(next);\n };\n const submitMode = searchOn === \"submit\";\n // The field's text in `\"submit\"` mode. It follows the committed query whenever that\n // changes — adjusted during render, React's pattern for state derived from a prop,\n // rather than in an effect that would paint the stale draft first.\n const [draft, setDraft] = useState(query);\n const [draftBase, setDraftBase] = useState(query);\n if (draftBase !== query) {\n setDraftBase(query);\n setDraft(query);\n }\n const fieldText = submitMode ? draft : query;\n const commit = (next: string) => {\n setDraft(next);\n setQuery(next);\n };\n const dirty = submitMode && draft !== query;\n const isPhone = useMediaQuery(PHONE_QUERY, false) && fullScreenOnPhone;\n const [results, setResults] = useState<CommandItem[]>([]);\n const [loading, setLoading] = useState(false);\n const [failed, setFailed] = useState(false);\n const [active, setActive] = useState(0);\n // Per instance: these were the literal \"command-palette-list\" / \"command-item-<id>\",\n // so two palettes on one page (an app-wide one and a scoped one) pointed each\n // other's `aria-controls` and `aria-activedescendant` at the wrong list.\n const uid = useId();\n const listId = `${uid}-list`;\n const itemId = (id: string) => `${uid}-item-${id}`;\n const groupId = (index: number) => `${uid}-group-${index}`;\n const dialogRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLUListElement>(null);\n const searchRef = useRef(search);\n searchRef.current = search;\n const reqId = useRef(0);\n /** What the provider's last answer was: a promise (`false`), an array (`true`), or\n * not known yet. Only a provider known to be async shows \"Searching…\" while the\n * debounce runs — see `search`. */\n const syncRef = useRef<boolean | undefined>(undefined);\n /** The query the results on screen answer, and the row highlighted in them — so a\n * re-run for the SAME query (a `revision` change: a source streaming in) keeps the\n * highlight on its row instead of throwing it back to the top mid-arrow. */\n const shownQuery = useRef<string | null>(null);\n const activeIdRef = useRef<string | undefined>(undefined);\n\n // Back dismisses the palette (Keksdose feedback #172). Declared with the other\n // hooks, above the `if (!open) return null` below — a hook past a conditional\n // return changes the hook order between renders. Passing `open` rather than\n // mounting-while-open is what lets it sit here.\n useOverlayHistory(open, onClose);\n\n // A modal like any other: Tab stays inside, the page behind does not scroll, and\n // focus goes back to whatever opened it — the palette used to drop it on <body>.\n // Initial focus is the field, not the container: typing is the only reason to open it.\n useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });\n useBodyScrollLock(open);\n\n // Reset when opened — the palette's OWN text only; a controlled query belongs to\n // its owner (see the prop).\n useEffect(() => {\n if (!open) return;\n if (!controlled) setOwnQuery(\"\");\n // An uncommitted draft does not outlive the open it was typed in.\n setDraft(controlled ? query : \"\");\n setActive(0);\n shownQuery.current = null;\n // `controlled` is read at the open, not watched: a caller switching modes while\n // the palette is open is not a reason to wipe what is typed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open]);\n\n // Debounced, race-safe search.\n useEffect(() => {\n if (!open) return;\n const id = ++reqId.current;\n if (syncRef.current === false) setLoading(true);\n const run = async () => {\n try {\n const answer = searchRef.current(query);\n syncRef.current = !isThenable(answer);\n if (!syncRef.current) setLoading(true);\n const r = await answer;\n if (reqId.current === id) {\n const keep = shownQuery.current === query ? activeIdRef.current : undefined;\n const at = keep === undefined ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);\n shownQuery.current = query;\n setResults(r);\n setFailed(false);\n setActive(Math.max(at, 0));\n }\n } catch (err) {\n // There was no catch: the previous query's results stayed on screen as if they\n // answered this one, and the rejection went unhandled. Clear them and say so.\n if (reqId.current === id) {\n shownQuery.current = null;\n setResults([]);\n setFailed(true);\n setActive(0);\n }\n console.error(\"CommandPalette: search failed\", err);\n } finally {\n if (reqId.current === id) setLoading(false);\n }\n };\n const t = setTimeout(run, query ? 150 : 0);\n return () => clearTimeout(t);\n // `revision`, not `search`: see the prop's docstring. The debounce is re-armed on a\n // revision change, which is right — data arriving is not a keystroke and there is\n // nothing to feel laggy about.\n }, [query, open, revision]);\n\n // Group results, preserving first-seen group order; keep a flat list for nav.\n const { groups, flat } = useMemo(() => groupItems(results), [results]);\n useEffect(() => {\n activeIdRef.current = flat[active]?.id;\n });\n\n const choose = (item: CommandItem | undefined) => {\n if (!item) return;\n onClose();\n item.onSelect();\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActive((i) => Math.min(i + 1, flat.length - 1));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActive((i) => Math.max(i - 1, 0));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (dirty) commit(draft);\n else choose(flat[active]);\n }\n };\n\n // Escape on the whole dialog, not only the field: once Tab has moved focus to a row\n // (or a footer control), Escape used to do nothing at all.\n const onDialogKeyDown = (e: React.KeyboardEvent) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n onClose();\n }\n };\n\n // Keep the active row scrolled into view.\n useEffect(() => {\n const el = listRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`);\n // Optional-called, as in combobox-core: jsdom has no scrollIntoView, and every\n // consumer's tests crashed on the first highlighted row (kastlan stubbed it).\n el?.scrollIntoView?.({ block: \"nearest\" });\n }, [active]);\n\n if (!open) return null;\n\n const comfortable = density === \"comfortable\";\n let flatIndex = -1;\n return createPortal(\n <div\n className={cn(\n \"fixed inset-0 z-[70] flex items-start justify-center bg-black/40\",\n isPhone ? \"p-0\" : \"p-4 pt-[10vh]\",\n )}\n onMouseDown={(e) => {\n if (e.target === e.currentTarget) onClose();\n }}\n >\n {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Escape from anywhere in the dialog */}\n <div\n ref={dialogRef}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={dialogName}\n // The trap's fallback target when the field is not there to take focus.\n tabIndex={-1}\n // Escape bubbling up from anything inside — a dialog is where it belongs.\n onKeyDown={onDialogKeyDown}\n data-fullscreen={isPhone || undefined}\n className={cn(\n \"flex w-full flex-col overflow-hidden bg-[var(--bg-surface)] outline-none\",\n isPhone\n ? \"h-full\"\n : \"max-h-[70vh] max-w-xl rounded-xl border border-[var(--border)] shadow-2xl\",\n )}\n // Full screen means edge to edge, so the notch, the home indicator and the\n // rounded corners are the panel's to keep clear of.\n style={\n isPhone\n ? {\n paddingTop: \"max(0px, env(safe-area-inset-top))\",\n paddingBottom: \"max(0px, env(safe-area-inset-bottom))\",\n paddingLeft: \"max(0px, env(safe-area-inset-left))\",\n paddingRight: \"max(0px, env(safe-area-inset-right))\",\n }\n : undefined\n }\n >\n <div className=\"flex shrink-0 items-center gap-2 border-b border-[var(--border)] px-3\">\n <Search className=\"size-4 shrink-0 text-[var(--text-placeholder)]\" />\n <input\n ref={inputRef}\n role=\"combobox\"\n aria-expanded\n aria-controls={listId}\n aria-activedescendant={flat[active] ? itemId(flat[active].id) : undefined}\n value={fieldText}\n onChange={(e) => (submitMode ? setDraft(e.target.value) : setQuery(e.target.value))}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n enterKeyHint={submitMode ? \"search\" : undefined}\n className={cn(\n \"w-full bg-transparent py-3 outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\",\n // 16px on a phone, or iOS zooms the page in on focus.\n isPhone ? \"text-base\" : \"text-sm\",\n )}\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\n {fieldText !== \"\" && (\n <button\n type=\"button\"\n aria-label={l.clear ?? DEFAULT_COMMAND_PALETTE_LABELS.clear}\n onClick={() => {\n commit(\"\");\n // Clearing starts the next query far more often than it ends this one.\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n )}\n {dirty && (\n <button\n type=\"button\"\n aria-label={l.submit ?? DEFAULT_COMMAND_PALETTE_LABELS.submit}\n onClick={() => {\n commit(draft);\n inputRef.current?.focus();\n }}\n className=\"shrink-0 rounded p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n <CornerDownLeft className=\"size-4\" />\n </button>\n )}\n {isPhone && (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"-me-1 shrink-0 rounded px-1 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]\"\n >\n {l.close ?? DEFAULT_COMMAND_PALETTE_LABELS.close}\n </button>\n )}\n </div>\n\n {/* The status lines sit outside the listbox: a listbox owns options and groups,\n and \"No results\" is neither. */}\n {failed && !loading && (\n <div role=\"alert\" className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">\n {l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error}\n </div>\n )}\n {/* `results`, not `flat`: a group whose only row is its \"Searching…\" line is not\n an empty result. */}\n {!failed && results.length === 0 && !loading && (\n <div className=\"px-3 py-6 text-center text-sm text-[var(--text-muted)]\">{l.empty}</div>\n )}\n {/* Listbox grouping: each group is a `role=\"group\"` named by its heading, and\n the list markup in between is presentational so the listbox's children are\n groups and options only — as a plain <li>/<ul> tree a reader announced\n \"list, 2 items\" between options and never tied the heading to its rows. */}\n <ul id={listId} ref={listRef} role=\"listbox\" aria-label={dialogName} className=\"min-h-0 flex-1 overflow-y-auto py-1\">\n {groups.map(({ group, items }, groupIndex) => (\n <li key={group} role=\"presentation\">\n <div\n id={groupId(groupIndex)}\n role=\"presentation\"\n className=\"px-3 pb-0.5 pt-2 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]\"\n >\n {group}\n </div>\n <ul role=\"group\" aria-labelledby={groupId(groupIndex)}>\n {items.map((item) => {\n if (item.kind === \"status\") {\n return (\n <li\n key={item.id}\n role=\"presentation\"\n className={cn(\n \"flex items-center gap-2.5 px-3 text-xs text-[var(--text-placeholder)]\",\n comfortable ? \"min-h-11 py-2.5\" : \"py-2\",\n )}\n >\n {item.icon && <span className=\"flex size-4 shrink-0 items-center justify-center\">{item.icon}</span>}\n <span className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n </li>\n );\n }\n flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\n const redact = item.redact ?? redactLabels;\n // Written once and worn by either tag below, so the anchor and the\n // button can never drift apart in looks or in listbox semantics.\n // `role=\"option\"` on an <a href> is fine: the browser's middle-click\n // behaviour keys off the element, not the ARIA role.\n const shared = {\n id: itemId(item.id),\n role: \"option\" as const,\n \"aria-selected\": isActive,\n \"data-index\": idx,\n onMouseMove: () => setActive(idx),\n className: cn(\n \"flex w-full items-center gap-2.5 px-3 text-start\",\n comfortable ? \"min-h-11 py-2.5 text-base\" : \"py-2 text-sm\",\n isActive\n ? \"bg-[var(--bg-active)] text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n ),\n };\n const inner = (\n <>\n {item.icon && (\n <span className=\"flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]\">\n {item.icon}\n </span>\n )}\n <span data-private={redact || undefined} className=\"min-w-0 flex-1 truncate\">\n {item.label}\n </span>\n {item.hint && (\n <span data-private={redact || undefined} className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">\n {item.hint}\n </span>\n )}\n </>\n );\n return (\n <li key={item.id} role=\"presentation\">\n {item.href ? (\n <a\n {...shared}\n href={item.href}\n draggable={false}\n onClick={(e) => {\n // A modified click belongs to the browser (feedback\n // #451) — and the palette deliberately stays OPEN for\n // it: ⌘-clicking three results in a row is the reason to\n // want this, and closing after the first would undo it.\n if (\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey\n )\n return;\n e.preventDefault();\n choose(item);\n }}\n >\n {inner}\n </a>\n ) : (\n <button type=\"button\" {...shared} onClick={() => choose(item)}>\n {inner}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n </li>\n ))}\n </ul>\n\n {l.hint && (\n <div className=\"border-t border-[var(--border)] px-3 py-1.5 text-[11px] text-[var(--text-placeholder)]\">\n {l.hint}\n </div>\n )}\n </div>\n </div>,\n document.body,\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CommandPaletteLabels } from './command-palette.js';
|
|
1
|
+
import { CommandPaletteDensity, CommandPaletteLabels } from './command-palette.js';
|
|
2
2
|
import { SearchEntry, SearchIndexOptions } from './search-index.js';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
@@ -77,6 +77,14 @@ interface GlobalSearchProps {
|
|
|
77
77
|
sources?: readonly GlobalSearchSource[];
|
|
78
78
|
/** What the empty query shows, under the `suggestions` heading. */
|
|
79
79
|
suggestions?: readonly GlobalSearchSuggestion[];
|
|
80
|
+
/**
|
|
81
|
+
* Show each suggested ENTRY under its own `group` instead of all of them under the
|
|
82
|
+
* one `suggestions` heading — keksdose's empty palette listed "Pages" and "Actions"
|
|
83
|
+
* apart, and one "Try" heading over both lost the difference. An entry with no
|
|
84
|
+
* `group` and a query suggestion (`{ query }`) stay under `suggestions`. `groupOrder`
|
|
85
|
+
* orders these groups as it orders the results'. Off by default.
|
|
86
|
+
*/
|
|
87
|
+
suggestionsKeepGroups?: boolean;
|
|
80
88
|
/** Groups listed here come first, in this order; the rest follow in ranking order. */
|
|
81
89
|
groupOrder?: readonly string[];
|
|
82
90
|
/** Ranking and capping — `groupLimit` (default 8) applies to every group. */
|
|
@@ -113,6 +121,23 @@ interface GlobalSearchProps {
|
|
|
113
121
|
hideTriggerOnPhone?: boolean;
|
|
114
122
|
/** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */
|
|
115
123
|
triggerClassName?: string;
|
|
124
|
+
/**
|
|
125
|
+
* The default trigger's magnifier, in px. Default 16 (`size-4`), as it always was;
|
|
126
|
+
* 20 matches the kit's other top-bar triggers (`TopbarIconButton` draws `size-5`) and
|
|
127
|
+
* keksdose's top bar, which reached into the button with `[&_svg]:size-5` for it.
|
|
128
|
+
*/
|
|
129
|
+
triggerIconSize?: number;
|
|
130
|
+
/**
|
|
131
|
+
* Whether the trigger's accessible name includes the shortcut: `"plain"` (default)
|
|
132
|
+
* names it "Search"; `"withShortcut"` names it by `labels.shortcut(keys)` —
|
|
133
|
+
* "Search (⌘K)" on Apple platforms, "Search (Ctrl K)" elsewhere — the same text its
|
|
134
|
+
* tooltip shows, so a speech-control user ("click Search") and a sighted keyboard user
|
|
135
|
+
* meet the shortcut in the name itself (keksdose C25). `aria-keyshortcuts` is set
|
|
136
|
+
* either way. Also reaches a custom trigger as `triggerProps.label`.
|
|
137
|
+
*/
|
|
138
|
+
triggerName?: "plain" | "withShortcut";
|
|
139
|
+
/** The palette's row height — `CommandPalette`'s `density`. Default `"compact"`. */
|
|
140
|
+
density?: CommandPaletteDensity;
|
|
116
141
|
labels?: Partial<GlobalSearchLabels>;
|
|
117
142
|
/** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */
|
|
118
143
|
paletteLabels?: Partial<CommandPaletteLabels>;
|
|
@@ -52,6 +52,7 @@ function GlobalSearchImpl({
|
|
|
52
52
|
entries,
|
|
53
53
|
sources,
|
|
54
54
|
suggestions,
|
|
55
|
+
suggestionsKeepGroups = false,
|
|
55
56
|
groupOrder,
|
|
56
57
|
indexOptions,
|
|
57
58
|
navigate,
|
|
@@ -67,6 +68,9 @@ function GlobalSearchImpl({
|
|
|
67
68
|
trigger = "button",
|
|
68
69
|
hideTriggerOnPhone = false,
|
|
69
70
|
triggerClassName,
|
|
71
|
+
triggerIconSize,
|
|
72
|
+
triggerName = "plain",
|
|
73
|
+
density,
|
|
70
74
|
labels,
|
|
71
75
|
paletteLabels
|
|
72
76
|
}) {
|
|
@@ -165,12 +169,21 @@ function GlobalSearchImpl({
|
|
|
165
169
|
[hrefFor, redactLabels]
|
|
166
170
|
);
|
|
167
171
|
const items = useMemo(() => {
|
|
172
|
+
const ordered = (out2) => {
|
|
173
|
+
if (!order.length) return out2;
|
|
174
|
+
const rank = (group) => {
|
|
175
|
+
const i = order.indexOf(group);
|
|
176
|
+
return i === -1 ? order.length : i;
|
|
177
|
+
};
|
|
178
|
+
return out2.map((item, i) => ({ item, i })).sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i).map(({ item }) => item);
|
|
179
|
+
};
|
|
168
180
|
if (!q) {
|
|
169
181
|
const out2 = [];
|
|
182
|
+
const groupOf = (entry) => suggestionsKeepGroups ? entry.group ?? l.suggestions : l.suggestions;
|
|
170
183
|
suggestionList.forEach((s, i) => {
|
|
171
184
|
if (typeof s === "string") {
|
|
172
185
|
const entry = byId.get(s);
|
|
173
|
-
if (entry) out2.push(toItem("try:", entry,
|
|
186
|
+
if (entry) out2.push(toItem("try:", entry, groupOf(entry)));
|
|
174
187
|
} else if ("query" in s) {
|
|
175
188
|
out2.push({
|
|
176
189
|
id: `try:q${i}`,
|
|
@@ -183,9 +196,9 @@ function GlobalSearchImpl({
|
|
|
183
196
|
refocusField();
|
|
184
197
|
}
|
|
185
198
|
});
|
|
186
|
-
} else out2.push(toItem("try:", s,
|
|
199
|
+
} else out2.push(toItem("try:", s, groupOf(s)));
|
|
187
200
|
});
|
|
188
|
-
return out2;
|
|
201
|
+
return suggestionsKeepGroups ? ordered(out2) : out2;
|
|
189
202
|
}
|
|
190
203
|
const out = index.search(q).map((hit) => toItem("s:", hit.entry, hit.entry.group ?? l.results));
|
|
191
204
|
for (const source of sourceList) {
|
|
@@ -209,13 +222,8 @@ function GlobalSearchImpl({
|
|
|
209
222
|
}
|
|
210
223
|
}
|
|
211
224
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const i = order.indexOf(group);
|
|
215
|
-
return i === -1 ? order.length : i;
|
|
216
|
-
};
|
|
217
|
-
return out.map((item, i) => ({ item, i })).sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i).map(({ item }) => item);
|
|
218
|
-
}, [q, suggestionList, byId, index, sourceList, sourceResults, order, groupLimit, toItem, l.suggestions, l.results, pl.loading, pl.error, setOpen, setQuery, noop, redactLabels]);
|
|
225
|
+
return ordered(out);
|
|
226
|
+
}, [q, suggestionList, suggestionsKeepGroups, byId, index, sourceList, sourceResults, order, groupLimit, toItem, l.suggestions, l.results, pl.loading, pl.error, setOpen, setQuery, noop, redactLabels]);
|
|
219
227
|
const provider = useCallback(() => items, [items]);
|
|
220
228
|
const resolvedPaletteLabels = useMemo(
|
|
221
229
|
() => ({ dialog: l.trigger, placeholder: l.placeholder, ...paletteLabels }),
|
|
@@ -223,9 +231,10 @@ function GlobalSearchImpl({
|
|
|
223
231
|
);
|
|
224
232
|
const apple = useMemo(() => isApplePlatform(), []);
|
|
225
233
|
const keys = apple ? "\u2318K" : "Ctrl K";
|
|
234
|
+
const triggerLabel = triggerName === "withShortcut" ? l.shortcut(keys) : l.trigger;
|
|
226
235
|
const triggerProps = {
|
|
227
236
|
open: openSearch,
|
|
228
|
-
label:
|
|
237
|
+
label: triggerLabel,
|
|
229
238
|
tooltip: l.shortcut(keys),
|
|
230
239
|
keys,
|
|
231
240
|
ariaKeyShortcuts: "Meta+K Control+K"
|
|
@@ -243,11 +252,17 @@ function GlobalSearchImpl({
|
|
|
243
252
|
{
|
|
244
253
|
type: "button",
|
|
245
254
|
onClick: openSearch,
|
|
246
|
-
"aria-label":
|
|
255
|
+
"aria-label": triggerLabel,
|
|
247
256
|
"aria-keyshortcuts": triggerProps.ariaKeyShortcuts,
|
|
248
257
|
"aria-haspopup": "dialog",
|
|
249
258
|
className: cn(TOPBAR_TRIGGER_CLASS, triggerClassName),
|
|
250
|
-
children: /* @__PURE__ */ jsx(
|
|
259
|
+
children: /* @__PURE__ */ jsx(
|
|
260
|
+
Search,
|
|
261
|
+
{
|
|
262
|
+
className: triggerIconSize === void 0 ? "size-4" : void 0,
|
|
263
|
+
style: triggerIconSize === void 0 ? void 0 : { width: triggerIconSize, height: triggerIconSize }
|
|
264
|
+
}
|
|
265
|
+
)
|
|
251
266
|
}
|
|
252
267
|
)
|
|
253
268
|
}
|
|
@@ -264,6 +279,7 @@ function GlobalSearchImpl({
|
|
|
264
279
|
searchOn,
|
|
265
280
|
redactLabels,
|
|
266
281
|
fullScreenOnPhone,
|
|
282
|
+
density,
|
|
267
283
|
labels: resolvedPaletteLabels
|
|
268
284
|
}
|
|
269
285
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/search/global-search.tsx"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useHref, useInRouterContext, useNavigate } from \"react-router\";\nimport { AlertCircle, Loader2, Search } from \"lucide-react\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { TOPBAR_TRIGGER_CLASS } from \"../shell/topbar-controls\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { CommandPalette, DEFAULT_COMMAND_PALETTE_LABELS, useCommandKey } from \"./command-palette\";\nimport type { CommandItem, CommandPaletteLabels } from \"./command-palette\";\nimport { createSearchIndex } from \"./search-index\";\nimport type { SearchEntry, SearchIndexOptions } from \"./search-index\";\n\nexport interface GlobalSearchLabels {\n /** The trigger's accessible name, and the dialog's. */\n trigger: string;\n /** The field's placeholder — says WHAT can be found, which \"Search…\" does not. */\n placeholder: string;\n /** The trigger's tooltip, given the shortcut as the platform writes it (\"⌘K\",\n * \"Ctrl K\"). A function so a language can put the keys where its grammar wants. */\n shortcut: (keys: string) => string;\n /** Heading of the suggestions shown for an empty query. */\n suggestions: string;\n /** Heading for entries that name no `group`. */\n results: string;\n}\n\nexport const DEFAULT_GLOBAL_SEARCH_LABELS: GlobalSearchLabels = {\n trigger: \"Search\",\n placeholder: \"Search or jump to…\",\n shortcut: (keys) => `Search (${keys})`,\n suggestions: \"Try\",\n results: \"Results\",\n};\n\n/**\n * A place results come from that is not the static index — a server search, a\n * local-first store too large to hand over whole. Each source is its own group, with its\n * own debounce, its own \"Searching…\" line and its own failure: the static groups never\n * wait for it, and one source failing never takes another's results with it.\n */\nexport interface GlobalSearchSource {\n /** Stable key — the effect that runs the source is keyed by it, not by identity. */\n id: string;\n /** The group its results are shown under (an entry's own `group` is ignored). */\n group: string;\n /** Called with the trimmed query. Its results are shown AS GIVEN — the source ranked\n * them — and never re-filtered by the index's matcher. `signal` aborts when the query\n * moves on or the palette closes; a stale answer is dropped either way. */\n search: (query: string, signal: AbortSignal) => Promise<readonly SearchEntry[]> | readonly SearchEntry[];\n /** Shortest query it is asked for. Default 2 — one letter matches half a database. */\n minChars?: number;\n /** Quiet period after the last keystroke before it is asked. Default 200ms. */\n debounceMs?: number;\n /** Rows kept. Default: `indexOptions.groupLimit` (8). */\n limit?: number;\n /** Mask every row of this source for session replay, hints (amounts) included. An\n * entry's own `redact` still wins. */\n redact?: boolean;\n /** Told when `search` throws or rejects (not when it is aborted). The group then\n * shows the palette's `error` line instead of rows. */\n onError?: (error: unknown) => void;\n}\n\n/** What a custom trigger gets: everything the default button uses. */\nexport interface GlobalSearchTriggerProps {\n open: () => void;\n /** Resolved `trigger` label — the accessible name. */\n label: string;\n /** Resolved `shortcut(keys)` — the tooltip. */\n tooltip: string;\n /** \"⌘K\" on Apple platforms, \"Ctrl K\" elsewhere. */\n keys: string;\n /** For the trigger's `aria-keyshortcuts`. */\n ariaKeyShortcuts: string;\n}\n\n/** A suggestion for the empty query: an entry, the `id` of one in `entries`, or a query\n * to type into the field. */\nexport type GlobalSearchSuggestion = SearchEntry | string | { query: string };\n\nexport interface GlobalSearchProps {\n /**\n * The static index: pages, actions, settings rows — already filtered by the app (by\n * role, by privacy mode). Matched and ranked by {@link createSearchIndex}.\n *\n * Changing it re-runs the open search by itself. Nothing has to be stable: the index is\n * rebuilt only when the entries' TEXT changes (ids, titles, keywords, …), so a list\n * rebuilt on every render — `data = []` while a query is pending, icons as fresh JSX —\n * neither re-indexes nor loops, and the icons and handlers used are always the latest.\n */\n entries?: readonly SearchEntry[];\n /** The async sources, each streamed into its own group. */\n sources?: readonly GlobalSearchSource[];\n /** What the empty query shows, under the `suggestions` heading. */\n suggestions?: readonly GlobalSearchSuggestion[];\n /** Groups listed here come first, in this order; the rest follow in ranking order. */\n groupOrder?: readonly string[];\n /** Ranking and capping — `groupLimit` (default 8) applies to every group. */\n indexOptions?: SearchIndexOptions;\n /**\n * How an entry's `href` is followed on a plain click or ↵. Default: react-router's\n * `navigate` when the search is rendered inside a router, else a full page load.\n */\n navigate?: (href: string) => void;\n /**\n * The browser URL of an `href`, for the row's real link (middle-/⌘-click opens a tab).\n * Default inside a router: the router's own (`#/page` under a hash router, the\n * basename prefixed under a browser router); outside one, the href itself.\n */\n hrefFor?: (href: string) => string;\n /** Register ⌘K / Ctrl K. Default true; turn it off for a second, scoped search on a\n * page that already has the app-wide one. */\n shortcut?: boolean;\n /** Controlled open state — so a button anywhere can open it. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Controlled query, as on `CommandPalette` (keksdose binds it to the URL's `q`). */\n query?: string;\n onQueryChange?: (query: string) => void;\n searchOn?: \"input\" | \"submit\";\n /** Mask every row for session replay (see `CommandPalette`). A source's or an entry's\n * own `redact` wins. */\n redactLabels?: boolean;\n fullScreenOnPhone?: boolean;\n /** `\"button\"` (default): the top-bar icon button. `\"none\"`: no trigger — the shortcut\n * or a controlled `open` opens it. A function renders a trigger of your own. */\n trigger?: \"button\" | \"none\" | ((props: GlobalSearchTriggerProps) => ReactNode);\n /** Hide the default trigger below `sm` (kastlan's phone top bar has no room). */\n hideTriggerOnPhone?: boolean;\n /** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */\n triggerClassName?: string;\n labels?: Partial<GlobalSearchLabels>;\n /** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */\n paletteLabels?: Partial<CommandPaletteLabels>;\n}\n\nfunction isApplePlatform(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || nav.userAgent);\n}\n\nconst pageLoad = (href: string) => window.location.assign(href);\nconst identity = (href: string) => href;\n\n/** The entries' text, which is all the index reads — see `entries`. */\nfunction signature(entries: readonly SearchEntry[]): string {\n return entries\n .map((e) =>\n [e.id, e.title, (e.keywords ?? []).join(\"\\u0003\"), e.description, e.group, e.hint, e.href, e.weight, e.redact].join(\n \"\\u0001\",\n ),\n )\n .join(\"\\u0002\");\n}\n\n/** Focus the open palette's field again — after a suggestion typed itself into it, the\n * row that was clicked (and held focus) is gone. */\nfunction refocusField() {\n requestAnimationFrame(() => {\n const fields = document.querySelectorAll<HTMLInputElement>('[role=\"dialog\"][aria-modal=\"true\"] input[role=\"combobox\"]');\n fields[fields.length - 1]?.focus();\n });\n}\n\ninterface SourceResult {\n query: string;\n status: \"loading\" | \"done\" | \"error\";\n entries: readonly SearchEntry[];\n}\n\n/**\n * The app-wide ⌘K search, whole: the top-bar trigger, the shortcut, and a\n * {@link CommandPalette} fed from a ranked index ({@link createSearchIndex}) and any\n * number of async sources.\n *\n * Every consumer had built this out of `CommandPalette` by hand, and each copy had the\n * same holes — a substring match, no ranking, no typo tolerance, results in declaration\n * order, one monolithic provider in which the static groups waited on the slowest\n * request. This is that wrapper once:\n *\n * <GlobalSearch\n * entries={pages} // { id, title, keywords, group, href, icon }\n * sources={[{ id: \"tx\", group: \"Transactions\", search: (q, signal) => api.search(q, { signal }) }]}\n * suggestions={[\"page-budget\", \"action-new-account\"]}\n * />\n *\n * Inside a react-router tree it navigates with the router and builds its links with it;\n * outside one, pass `navigate` (or accept full page loads).\n */\nexport function GlobalSearch(props: GlobalSearchProps) {\n // Two components rather than a conditional hook: `useNavigate` throws outside a\n // router, and whether we are in one does not change over a component's life.\n const inRouter = useInRouterContext();\n if (inRouter && (!props.navigate || !props.hrefFor)) return <RoutedGlobalSearch {...props} />;\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? pageLoad} hrefFor={props.hrefFor ?? identity} />;\n}\n\nfunction RoutedGlobalSearch(props: GlobalSearchProps) {\n const routerNavigate = useNavigate();\n // The router's href for \"/\" is its base (\"#/\" under a hash router, \"/app/\" under a\n // basename); every href is that base plus the path.\n const base = useHref(\"/\").replace(/\\/$/, \"\");\n const navigate = useCallback((href: string) => void routerNavigate(href), [routerNavigate]);\n const hrefFor = useCallback((href: string) => (href.startsWith(\"/\") ? base + href : href), [base]);\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? navigate} hrefFor={props.hrefFor ?? hrefFor} />;\n}\n\nfunction GlobalSearchImpl({\n entries,\n sources,\n suggestions,\n groupOrder,\n indexOptions,\n navigate,\n hrefFor,\n shortcut = true,\n open: openProp,\n onOpenChange,\n query: queryProp,\n onQueryChange,\n searchOn,\n redactLabels = false,\n fullScreenOnPhone,\n trigger = \"button\",\n hideTriggerOnPhone = false,\n triggerClassName,\n labels,\n paletteLabels,\n}: GlobalSearchProps & { navigate: (href: string) => void; hrefFor: (href: string) => string }) {\n const l = useKitLabels(\"globalSearch\", DEFAULT_GLOBAL_SEARCH_LABELS, labels);\n const pl = useKitLabels(\"commandPalette\", DEFAULT_COMMAND_PALETTE_LABELS, paletteLabels);\n const [ownOpen, setOwnOpen] = useState(false);\n const open = openProp ?? ownOpen;\n const [ownQuery, setOwnQuery] = useState(\"\");\n const queryControlled = queryProp !== undefined;\n const query = queryControlled ? queryProp : ownQuery;\n const q = query.trim();\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (openProp === undefined) setOwnOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n const setQuery = useCallback(\n (next: string) => {\n if (!queryControlled) setOwnQuery(next);\n onQueryChange?.(next);\n },\n [queryControlled, onQueryChange],\n );\n\n // Each open starts blank — unless the owner controls the query (a URL `q`). Adjusted\n // during render, React's pattern for state that follows a prop, so a controlled `open`\n // gets the same reset as the trigger does.\n const [wasOpen, setWasOpen] = useState(open);\n if (wasOpen !== open) {\n setWasOpen(open);\n if (open && !queryControlled) setOwnQuery(\"\");\n }\n\n const openSearch = useCallback(() => setOpen(true), [setOpen]);\n const noop = useCallback(() => {}, []);\n useCommandKey(shortcut ? openSearch : noop);\n\n /* ── the static index ─────────────────────────────────────────────────── */\n\n // Everything below is keyed by CONTENT, never by the identity of what the app passed:\n // inline arrays and `data = []` defaults are the norm, and an identity key would\n // re-index and re-search on every render of the app. Handlers, which can close over\n // anything, are read from refs at the moment a row is chosen — never stale.\n const list = entries ?? EMPTY;\n const sig = signature(list);\n const optionsKey = JSON.stringify(indexOptions ?? {});\n const groupLimit = indexOptions?.groupLimit ?? 8;\n // `sig` and `optionsKey` stand for `list` and `indexOptions`: same text, same index.\n /* eslint-disable react-hooks/exhaustive-deps */\n const index = useMemo(() => createSearchIndex(list, indexOptions), [sig, optionsKey]);\n const byId = useMemo(() => new Map(list.map((e) => [e.id, e])), [sig]);\n\n const sourceKey = (sources ?? [])\n .map((s) => [s.id, s.group, s.minChars ?? 2, s.debounceMs ?? 200, s.limit, s.redact].join(\"\\u0001\"))\n .join(\"\\u0002\");\n const sourceList = useMemo(() => sources ?? [], [sourceKey]);\n\n const suggestionsKey = (suggestions ?? [])\n .map((s) => (typeof s === \"string\" ? `i:${s}` : \"query\" in s ? `q:${s.query}` : `e:${signature([s])}`))\n .join(\"\\u0002\");\n const suggestionList = useMemo(() => suggestions ?? [], [suggestionsKey]);\n\n const groupOrderKey = (groupOrder ?? []).join(\"\\u0002\");\n const order = useMemo(() => groupOrder ?? [], [groupOrderKey]);\n /* eslint-enable react-hooks/exhaustive-deps */\n\n // The live objects, for the moment a row is chosen or a source is asked.\n const live = useRef({ entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate });\n useLayoutEffect(() => {\n live.current = { entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate };\n });\n\n /* ── the async sources ────────────────────────────────────────────────── */\n\n const [sourceResults, setSourceResults] = useState<Record<string, SourceResult>>({});\n useEffect(() => {\n const controllers: AbortController[] = [];\n const timers: ReturnType<typeof setTimeout>[] = [];\n const asked = open ? live.current.sources.filter((s) => q.length >= (s.minChars ?? 2)) : [];\n for (const source of asked) {\n const id = source.id;\n const controller = new AbortController();\n controllers.push(controller);\n timers.push(\n setTimeout(async () => {\n const settle = (result: SourceResult) => {\n if (!controller.signal.aborted) setSourceResults((prev) => ({ ...prev, [id]: result }));\n };\n try {\n const found = await source.search(q, controller.signal);\n settle({ query: q, status: \"done\", entries: found });\n } catch (error) {\n if (controller.signal.aborted) return;\n source.onError?.(error);\n settle({ query: q, status: \"error\", entries: [] });\n }\n }, source.debounceMs ?? 200),\n );\n }\n return () => {\n timers.forEach(clearTimeout);\n controllers.forEach((c) => c.abort());\n };\n }, [open, q, sourceKey]);\n\n /* ── rows ─────────────────────────────────────────────────────────────── */\n\n const toItem = useCallback(\n (prefix: string, entry: SearchEntry, group: string, redact = redactLabels): CommandItem => ({\n id: `${prefix}${entry.id}`,\n label: entry.title,\n hint: entry.hint,\n group,\n icon: entry.icon,\n redact: entry.redact ?? redact,\n href: entry.href !== undefined ? hrefFor(entry.href) : undefined,\n onSelect: () => {\n // The entry as the app has it NOW — its handler may close over newer state\n // than the one this row was built from.\n const current = live.current.entries.find((e) => e.id === entry.id) ?? entry;\n if (current.onSelect) current.onSelect();\n else if (current.href !== undefined) live.current.navigate(current.href);\n },\n }),\n [hrefFor, redactLabels],\n );\n\n // `toItem` only BUILDS `onSelect` closures here; `live` is read when a row is chosen,\n // which the compiler lint cannot tell from a read during render.\n /* eslint-disable react-hooks/refs */\n const items = useMemo((): CommandItem[] => {\n if (!q) {\n const out: CommandItem[] = [];\n suggestionList.forEach((s, i) => {\n if (typeof s === \"string\") {\n const entry = byId.get(s);\n if (entry) out.push(toItem(\"try:\", entry, l.suggestions));\n } else if (\"query\" in s) {\n out.push({\n id: `try:q${i}`,\n label: s.query,\n group: l.suggestions,\n icon: <Search className=\"size-4\" />,\n onSelect: () => {\n // The palette closes before it runs a row; a query suggestion keeps it\n // open and types itself in instead.\n setOpen(true);\n setQuery(s.query);\n refocusField();\n },\n });\n } else out.push(toItem(\"try:\", s, l.suggestions));\n });\n return out;\n }\n\n const out = index.search(q).map((hit) => toItem(\"s:\", hit.entry, hit.entry.group ?? l.results));\n for (const source of sourceList) {\n if (q.length < (source.minChars ?? 2)) continue;\n const result = sourceResults[source.id];\n const status = (label: string, icon: ReactNode): CommandItem => ({\n id: `src:${source.id}:status`,\n label,\n group: source.group,\n icon,\n kind: \"status\",\n onSelect: noop,\n });\n // Anything but an answer to THIS query is still loading — including the previous\n // query's rows, which would otherwise sit under the new query as if they matched.\n if (!result || result.query !== q) {\n out.push(status(pl.loading, <Loader2 className=\"size-3.5 animate-spin\" />));\n } else if (result.status === \"error\") {\n out.push(status(pl.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error!, <AlertCircle className=\"size-3.5\" />));\n } else {\n for (const entry of result.entries.slice(0, source.limit ?? groupLimit)) {\n out.push(toItem(`src:${source.id}:`, entry, source.group, source.redact ?? redactLabels));\n }\n }\n }\n\n if (!order.length) return out;\n const rank = (group: string) => {\n const i = order.indexOf(group);\n return i === -1 ? order.length : i;\n };\n // Stable: within a rank, groups keep the order their best hit gave them.\n return out\n .map((item, i) => ({ item, i }))\n .sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i)\n .map(({ item }) => item);\n }, [q, suggestionList, byId, index, sourceList, sourceResults, order, groupLimit, toItem, l.suggestions, l.results, pl.loading, pl.error, setOpen, setQuery, noop, redactLabels]);\n /* eslint-enable react-hooks/refs */\n\n // The palette re-runs its provider on a `revision` change, never on the provider's\n // identity — and `items` is everything the provider answers with.\n const provider = useCallback(() => items, [items]);\n\n const resolvedPaletteLabels = useMemo(\n () => ({ dialog: l.trigger, placeholder: l.placeholder, ...paletteLabels }),\n [l.trigger, l.placeholder, paletteLabels],\n );\n\n const apple = useMemo(() => isApplePlatform(), []);\n const keys = apple ? \"⌘K\" : \"Ctrl K\";\n const triggerProps: GlobalSearchTriggerProps = {\n open: openSearch,\n label: l.trigger,\n tooltip: l.shortcut(keys),\n keys,\n ariaKeyShortcuts: \"Meta+K Control+K\",\n };\n\n return (\n <>\n {typeof trigger === \"function\"\n ? trigger(triggerProps)\n : trigger === \"button\" && (\n <Tooltip\n label={triggerProps.tooltip}\n side=\"bottom\"\n portal\n className={hideTriggerOnPhone ? \"hidden sm:block\" : undefined}\n >\n <button\n type=\"button\"\n onClick={openSearch}\n aria-label={l.trigger}\n aria-keyshortcuts={triggerProps.ariaKeyShortcuts}\n aria-haspopup=\"dialog\"\n className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}\n >\n <Search className=\"size-4\" />\n </button>\n </Tooltip>\n )}\n <CommandPalette\n open={open}\n onClose={() => setOpen(false)}\n search={provider}\n revision={items}\n query={query}\n onQueryChange={setQuery}\n searchOn={searchOn}\n redactLabels={redactLabels}\n fullScreenOnPhone={fullScreenOnPhone}\n labels={resolvedPaletteLabels}\n />\n </>\n );\n}\n\nconst EMPTY: readonly SearchEntry[] = [];\n"],"mappings":"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","names":["out"]}
|
|
1
|
+
{"version":3,"sources":["../../src/search/global-search.tsx"],"sourcesContent":["import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { useHref, useInRouterContext, useNavigate } from \"react-router\";\nimport { AlertCircle, Loader2, Search } from \"lucide-react\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { TOPBAR_TRIGGER_CLASS } from \"../shell/topbar-controls\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { CommandPalette, DEFAULT_COMMAND_PALETTE_LABELS, useCommandKey } from \"./command-palette\";\nimport type { CommandItem, CommandPaletteDensity, CommandPaletteLabels } from \"./command-palette\";\nimport { createSearchIndex } from \"./search-index\";\nimport type { SearchEntry, SearchIndexOptions } from \"./search-index\";\n\nexport interface GlobalSearchLabels {\n /** The trigger's accessible name, and the dialog's. */\n trigger: string;\n /** The field's placeholder — says WHAT can be found, which \"Search…\" does not. */\n placeholder: string;\n /** The trigger's tooltip, given the shortcut as the platform writes it (\"⌘K\",\n * \"Ctrl K\"). A function so a language can put the keys where its grammar wants. */\n shortcut: (keys: string) => string;\n /** Heading of the suggestions shown for an empty query. */\n suggestions: string;\n /** Heading for entries that name no `group`. */\n results: string;\n}\n\nexport const DEFAULT_GLOBAL_SEARCH_LABELS: GlobalSearchLabels = {\n trigger: \"Search\",\n placeholder: \"Search or jump to…\",\n shortcut: (keys) => `Search (${keys})`,\n suggestions: \"Try\",\n results: \"Results\",\n};\n\n/**\n * A place results come from that is not the static index — a server search, a\n * local-first store too large to hand over whole. Each source is its own group, with its\n * own debounce, its own \"Searching…\" line and its own failure: the static groups never\n * wait for it, and one source failing never takes another's results with it.\n */\nexport interface GlobalSearchSource {\n /** Stable key — the effect that runs the source is keyed by it, not by identity. */\n id: string;\n /** The group its results are shown under (an entry's own `group` is ignored). */\n group: string;\n /** Called with the trimmed query. Its results are shown AS GIVEN — the source ranked\n * them — and never re-filtered by the index's matcher. `signal` aborts when the query\n * moves on or the palette closes; a stale answer is dropped either way. */\n search: (query: string, signal: AbortSignal) => Promise<readonly SearchEntry[]> | readonly SearchEntry[];\n /** Shortest query it is asked for. Default 2 — one letter matches half a database. */\n minChars?: number;\n /** Quiet period after the last keystroke before it is asked. Default 200ms. */\n debounceMs?: number;\n /** Rows kept. Default: `indexOptions.groupLimit` (8). */\n limit?: number;\n /** Mask every row of this source for session replay, hints (amounts) included. An\n * entry's own `redact` still wins. */\n redact?: boolean;\n /** Told when `search` throws or rejects (not when it is aborted). The group then\n * shows the palette's `error` line instead of rows. */\n onError?: (error: unknown) => void;\n}\n\n/** What a custom trigger gets: everything the default button uses. */\nexport interface GlobalSearchTriggerProps {\n open: () => void;\n /** Resolved `trigger` label — the accessible name. */\n label: string;\n /** Resolved `shortcut(keys)` — the tooltip. */\n tooltip: string;\n /** \"⌘K\" on Apple platforms, \"Ctrl K\" elsewhere. */\n keys: string;\n /** For the trigger's `aria-keyshortcuts`. */\n ariaKeyShortcuts: string;\n}\n\n/** A suggestion for the empty query: an entry, the `id` of one in `entries`, or a query\n * to type into the field. */\nexport type GlobalSearchSuggestion = SearchEntry | string | { query: string };\n\nexport interface GlobalSearchProps {\n /**\n * The static index: pages, actions, settings rows — already filtered by the app (by\n * role, by privacy mode). Matched and ranked by {@link createSearchIndex}.\n *\n * Changing it re-runs the open search by itself. Nothing has to be stable: the index is\n * rebuilt only when the entries' TEXT changes (ids, titles, keywords, …), so a list\n * rebuilt on every render — `data = []` while a query is pending, icons as fresh JSX —\n * neither re-indexes nor loops, and the icons and handlers used are always the latest.\n */\n entries?: readonly SearchEntry[];\n /** The async sources, each streamed into its own group. */\n sources?: readonly GlobalSearchSource[];\n /** What the empty query shows, under the `suggestions` heading. */\n suggestions?: readonly GlobalSearchSuggestion[];\n /**\n * Show each suggested ENTRY under its own `group` instead of all of them under the\n * one `suggestions` heading — keksdose's empty palette listed \"Pages\" and \"Actions\"\n * apart, and one \"Try\" heading over both lost the difference. An entry with no\n * `group` and a query suggestion (`{ query }`) stay under `suggestions`. `groupOrder`\n * orders these groups as it orders the results'. Off by default.\n */\n suggestionsKeepGroups?: boolean;\n /** Groups listed here come first, in this order; the rest follow in ranking order. */\n groupOrder?: readonly string[];\n /** Ranking and capping — `groupLimit` (default 8) applies to every group. */\n indexOptions?: SearchIndexOptions;\n /**\n * How an entry's `href` is followed on a plain click or ↵. Default: react-router's\n * `navigate` when the search is rendered inside a router, else a full page load.\n */\n navigate?: (href: string) => void;\n /**\n * The browser URL of an `href`, for the row's real link (middle-/⌘-click opens a tab).\n * Default inside a router: the router's own (`#/page` under a hash router, the\n * basename prefixed under a browser router); outside one, the href itself.\n */\n hrefFor?: (href: string) => string;\n /** Register ⌘K / Ctrl K. Default true; turn it off for a second, scoped search on a\n * page that already has the app-wide one. */\n shortcut?: boolean;\n /** Controlled open state — so a button anywhere can open it. */\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Controlled query, as on `CommandPalette` (keksdose binds it to the URL's `q`). */\n query?: string;\n onQueryChange?: (query: string) => void;\n searchOn?: \"input\" | \"submit\";\n /** Mask every row for session replay (see `CommandPalette`). A source's or an entry's\n * own `redact` wins. */\n redactLabels?: boolean;\n fullScreenOnPhone?: boolean;\n /** `\"button\"` (default): the top-bar icon button. `\"none\"`: no trigger — the shortcut\n * or a controlled `open` opens it. A function renders a trigger of your own. */\n trigger?: \"button\" | \"none\" | ((props: GlobalSearchTriggerProps) => ReactNode);\n /** Hide the default trigger below `sm` (kastlan's phone top bar has no room). */\n hideTriggerOnPhone?: boolean;\n /** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */\n triggerClassName?: string;\n /**\n * The default trigger's magnifier, in px. Default 16 (`size-4`), as it always was;\n * 20 matches the kit's other top-bar triggers (`TopbarIconButton` draws `size-5`) and\n * keksdose's top bar, which reached into the button with `[&_svg]:size-5` for it.\n */\n triggerIconSize?: number;\n /**\n * Whether the trigger's accessible name includes the shortcut: `\"plain\"` (default)\n * names it \"Search\"; `\"withShortcut\"` names it by `labels.shortcut(keys)` —\n * \"Search (⌘K)\" on Apple platforms, \"Search (Ctrl K)\" elsewhere — the same text its\n * tooltip shows, so a speech-control user (\"click Search\") and a sighted keyboard user\n * meet the shortcut in the name itself (keksdose C25). `aria-keyshortcuts` is set\n * either way. Also reaches a custom trigger as `triggerProps.label`.\n */\n triggerName?: \"plain\" | \"withShortcut\";\n /** The palette's row height — `CommandPalette`'s `density`. Default `\"compact\"`. */\n density?: CommandPaletteDensity;\n labels?: Partial<GlobalSearchLabels>;\n /** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */\n paletteLabels?: Partial<CommandPaletteLabels>;\n}\n\nfunction isApplePlatform(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const nav = navigator as Navigator & { userAgentData?: { platform?: string } };\n return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || nav.userAgent);\n}\n\nconst pageLoad = (href: string) => window.location.assign(href);\nconst identity = (href: string) => href;\n\n/** The entries' text, which is all the index reads — see `entries`. */\nfunction signature(entries: readonly SearchEntry[]): string {\n return entries\n .map((e) =>\n [e.id, e.title, (e.keywords ?? []).join(\"\\u0003\"), e.description, e.group, e.hint, e.href, e.weight, e.redact].join(\n \"\\u0001\",\n ),\n )\n .join(\"\\u0002\");\n}\n\n/** Focus the open palette's field again — after a suggestion typed itself into it, the\n * row that was clicked (and held focus) is gone. */\nfunction refocusField() {\n requestAnimationFrame(() => {\n const fields = document.querySelectorAll<HTMLInputElement>('[role=\"dialog\"][aria-modal=\"true\"] input[role=\"combobox\"]');\n fields[fields.length - 1]?.focus();\n });\n}\n\ninterface SourceResult {\n query: string;\n status: \"loading\" | \"done\" | \"error\";\n entries: readonly SearchEntry[];\n}\n\n/**\n * The app-wide ⌘K search, whole: the top-bar trigger, the shortcut, and a\n * {@link CommandPalette} fed from a ranked index ({@link createSearchIndex}) and any\n * number of async sources.\n *\n * Every consumer had built this out of `CommandPalette` by hand, and each copy had the\n * same holes — a substring match, no ranking, no typo tolerance, results in declaration\n * order, one monolithic provider in which the static groups waited on the slowest\n * request. This is that wrapper once:\n *\n * <GlobalSearch\n * entries={pages} // { id, title, keywords, group, href, icon }\n * sources={[{ id: \"tx\", group: \"Transactions\", search: (q, signal) => api.search(q, { signal }) }]}\n * suggestions={[\"page-budget\", \"action-new-account\"]}\n * />\n *\n * Inside a react-router tree it navigates with the router and builds its links with it;\n * outside one, pass `navigate` (or accept full page loads).\n */\nexport function GlobalSearch(props: GlobalSearchProps) {\n // Two components rather than a conditional hook: `useNavigate` throws outside a\n // router, and whether we are in one does not change over a component's life.\n const inRouter = useInRouterContext();\n if (inRouter && (!props.navigate || !props.hrefFor)) return <RoutedGlobalSearch {...props} />;\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? pageLoad} hrefFor={props.hrefFor ?? identity} />;\n}\n\nfunction RoutedGlobalSearch(props: GlobalSearchProps) {\n const routerNavigate = useNavigate();\n // The router's href for \"/\" is its base (\"#/\" under a hash router, \"/app/\" under a\n // basename); every href is that base plus the path.\n const base = useHref(\"/\").replace(/\\/$/, \"\");\n const navigate = useCallback((href: string) => void routerNavigate(href), [routerNavigate]);\n const hrefFor = useCallback((href: string) => (href.startsWith(\"/\") ? base + href : href), [base]);\n return <GlobalSearchImpl {...props} navigate={props.navigate ?? navigate} hrefFor={props.hrefFor ?? hrefFor} />;\n}\n\nfunction GlobalSearchImpl({\n entries,\n sources,\n suggestions,\n suggestionsKeepGroups = false,\n groupOrder,\n indexOptions,\n navigate,\n hrefFor,\n shortcut = true,\n open: openProp,\n onOpenChange,\n query: queryProp,\n onQueryChange,\n searchOn,\n redactLabels = false,\n fullScreenOnPhone,\n trigger = \"button\",\n hideTriggerOnPhone = false,\n triggerClassName,\n triggerIconSize,\n triggerName = \"plain\",\n density,\n labels,\n paletteLabels,\n}: GlobalSearchProps & { navigate: (href: string) => void; hrefFor: (href: string) => string }) {\n const l = useKitLabels(\"globalSearch\", DEFAULT_GLOBAL_SEARCH_LABELS, labels);\n const pl = useKitLabels(\"commandPalette\", DEFAULT_COMMAND_PALETTE_LABELS, paletteLabels);\n const [ownOpen, setOwnOpen] = useState(false);\n const open = openProp ?? ownOpen;\n const [ownQuery, setOwnQuery] = useState(\"\");\n const queryControlled = queryProp !== undefined;\n const query = queryControlled ? queryProp : ownQuery;\n const q = query.trim();\n\n const setOpen = useCallback(\n (next: boolean) => {\n if (openProp === undefined) setOwnOpen(next);\n onOpenChange?.(next);\n },\n [openProp, onOpenChange],\n );\n const setQuery = useCallback(\n (next: string) => {\n if (!queryControlled) setOwnQuery(next);\n onQueryChange?.(next);\n },\n [queryControlled, onQueryChange],\n );\n\n // Each open starts blank — unless the owner controls the query (a URL `q`). Adjusted\n // during render, React's pattern for state that follows a prop, so a controlled `open`\n // gets the same reset as the trigger does.\n const [wasOpen, setWasOpen] = useState(open);\n if (wasOpen !== open) {\n setWasOpen(open);\n if (open && !queryControlled) setOwnQuery(\"\");\n }\n\n const openSearch = useCallback(() => setOpen(true), [setOpen]);\n const noop = useCallback(() => {}, []);\n useCommandKey(shortcut ? openSearch : noop);\n\n /* ── the static index ─────────────────────────────────────────────────── */\n\n // Everything below is keyed by CONTENT, never by the identity of what the app passed:\n // inline arrays and `data = []` defaults are the norm, and an identity key would\n // re-index and re-search on every render of the app. Handlers, which can close over\n // anything, are read from refs at the moment a row is chosen — never stale.\n const list = entries ?? EMPTY;\n const sig = signature(list);\n const optionsKey = JSON.stringify(indexOptions ?? {});\n const groupLimit = indexOptions?.groupLimit ?? 8;\n // `sig` and `optionsKey` stand for `list` and `indexOptions`: same text, same index.\n /* eslint-disable react-hooks/exhaustive-deps */\n const index = useMemo(() => createSearchIndex(list, indexOptions), [sig, optionsKey]);\n const byId = useMemo(() => new Map(list.map((e) => [e.id, e])), [sig]);\n\n const sourceKey = (sources ?? [])\n .map((s) => [s.id, s.group, s.minChars ?? 2, s.debounceMs ?? 200, s.limit, s.redact].join(\"\\u0001\"))\n .join(\"\\u0002\");\n const sourceList = useMemo(() => sources ?? [], [sourceKey]);\n\n const suggestionsKey = (suggestions ?? [])\n .map((s) => (typeof s === \"string\" ? `i:${s}` : \"query\" in s ? `q:${s.query}` : `e:${signature([s])}`))\n .join(\"\\u0002\");\n const suggestionList = useMemo(() => suggestions ?? [], [suggestionsKey]);\n\n const groupOrderKey = (groupOrder ?? []).join(\"\\u0002\");\n const order = useMemo(() => groupOrder ?? [], [groupOrderKey]);\n /* eslint-enable react-hooks/exhaustive-deps */\n\n // The live objects, for the moment a row is chosen or a source is asked.\n const live = useRef({ entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate });\n useLayoutEffect(() => {\n live.current = { entries: list, sources: sources ?? [], suggestions: suggestions ?? [], navigate };\n });\n\n /* ── the async sources ────────────────────────────────────────────────── */\n\n const [sourceResults, setSourceResults] = useState<Record<string, SourceResult>>({});\n useEffect(() => {\n const controllers: AbortController[] = [];\n const timers: ReturnType<typeof setTimeout>[] = [];\n const asked = open ? live.current.sources.filter((s) => q.length >= (s.minChars ?? 2)) : [];\n for (const source of asked) {\n const id = source.id;\n const controller = new AbortController();\n controllers.push(controller);\n timers.push(\n setTimeout(async () => {\n const settle = (result: SourceResult) => {\n if (!controller.signal.aborted) setSourceResults((prev) => ({ ...prev, [id]: result }));\n };\n try {\n const found = await source.search(q, controller.signal);\n settle({ query: q, status: \"done\", entries: found });\n } catch (error) {\n if (controller.signal.aborted) return;\n source.onError?.(error);\n settle({ query: q, status: \"error\", entries: [] });\n }\n }, source.debounceMs ?? 200),\n );\n }\n return () => {\n timers.forEach(clearTimeout);\n controllers.forEach((c) => c.abort());\n };\n }, [open, q, sourceKey]);\n\n /* ── rows ─────────────────────────────────────────────────────────────── */\n\n const toItem = useCallback(\n (prefix: string, entry: SearchEntry, group: string, redact = redactLabels): CommandItem => ({\n id: `${prefix}${entry.id}`,\n label: entry.title,\n hint: entry.hint,\n group,\n icon: entry.icon,\n redact: entry.redact ?? redact,\n href: entry.href !== undefined ? hrefFor(entry.href) : undefined,\n onSelect: () => {\n // The entry as the app has it NOW — its handler may close over newer state\n // than the one this row was built from.\n const current = live.current.entries.find((e) => e.id === entry.id) ?? entry;\n if (current.onSelect) current.onSelect();\n else if (current.href !== undefined) live.current.navigate(current.href);\n },\n }),\n [hrefFor, redactLabels],\n );\n\n // `toItem` only BUILDS `onSelect` closures here; `live` is read when a row is chosen,\n // which the compiler lint cannot tell from a read during render.\n /* eslint-disable react-hooks/refs */\n const items = useMemo((): CommandItem[] => {\n const ordered = (out: CommandItem[]) => {\n if (!order.length) return out;\n const rank = (group: string) => {\n const i = order.indexOf(group);\n return i === -1 ? order.length : i;\n };\n // Stable: within a rank, groups keep the order their best hit gave them.\n return out\n .map((item, i) => ({ item, i }))\n .sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i)\n .map(({ item }) => item);\n };\n\n if (!q) {\n const out: CommandItem[] = [];\n const groupOf = (entry: SearchEntry) =>\n suggestionsKeepGroups ? (entry.group ?? l.suggestions) : l.suggestions;\n suggestionList.forEach((s, i) => {\n if (typeof s === \"string\") {\n const entry = byId.get(s);\n if (entry) out.push(toItem(\"try:\", entry, groupOf(entry)));\n } else if (\"query\" in s) {\n out.push({\n id: `try:q${i}`,\n label: s.query,\n group: l.suggestions,\n icon: <Search className=\"size-4\" />,\n onSelect: () => {\n // The palette closes before it runs a row; a query suggestion keeps it\n // open and types itself in instead.\n setOpen(true);\n setQuery(s.query);\n refocusField();\n },\n });\n } else out.push(toItem(\"try:\", s, groupOf(s)));\n });\n // One heading has nothing to order; kept groups are ordered like results.\n return suggestionsKeepGroups ? ordered(out) : out;\n }\n\n const out = index.search(q).map((hit) => toItem(\"s:\", hit.entry, hit.entry.group ?? l.results));\n for (const source of sourceList) {\n if (q.length < (source.minChars ?? 2)) continue;\n const result = sourceResults[source.id];\n const status = (label: string, icon: ReactNode): CommandItem => ({\n id: `src:${source.id}:status`,\n label,\n group: source.group,\n icon,\n kind: \"status\",\n onSelect: noop,\n });\n // Anything but an answer to THIS query is still loading — including the previous\n // query's rows, which would otherwise sit under the new query as if they matched.\n if (!result || result.query !== q) {\n out.push(status(pl.loading, <Loader2 className=\"size-3.5 animate-spin\" />));\n } else if (result.status === \"error\") {\n out.push(status(pl.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error!, <AlertCircle className=\"size-3.5\" />));\n } else {\n for (const entry of result.entries.slice(0, source.limit ?? groupLimit)) {\n out.push(toItem(`src:${source.id}:`, entry, source.group, source.redact ?? redactLabels));\n }\n }\n }\n\n return ordered(out);\n }, [q, suggestionList, suggestionsKeepGroups, byId, index, sourceList, sourceResults, order, groupLimit, toItem, l.suggestions, l.results, pl.loading, pl.error, setOpen, setQuery, noop, redactLabels]);\n /* eslint-enable react-hooks/refs */\n\n // The palette re-runs its provider on a `revision` change, never on the provider's\n // identity — and `items` is everything the provider answers with.\n const provider = useCallback(() => items, [items]);\n\n const resolvedPaletteLabels = useMemo(\n () => ({ dialog: l.trigger, placeholder: l.placeholder, ...paletteLabels }),\n [l.trigger, l.placeholder, paletteLabels],\n );\n\n const apple = useMemo(() => isApplePlatform(), []);\n const keys = apple ? \"⌘K\" : \"Ctrl K\";\n const triggerLabel = triggerName === \"withShortcut\" ? l.shortcut(keys) : l.trigger;\n const triggerProps: GlobalSearchTriggerProps = {\n open: openSearch,\n label: triggerLabel,\n tooltip: l.shortcut(keys),\n keys,\n ariaKeyShortcuts: \"Meta+K Control+K\",\n };\n\n return (\n <>\n {typeof trigger === \"function\"\n ? trigger(triggerProps)\n : trigger === \"button\" && (\n <Tooltip\n label={triggerProps.tooltip}\n side=\"bottom\"\n portal\n className={hideTriggerOnPhone ? \"hidden sm:block\" : undefined}\n >\n <button\n type=\"button\"\n onClick={openSearch}\n aria-label={triggerLabel}\n aria-keyshortcuts={triggerProps.ariaKeyShortcuts}\n aria-haspopup=\"dialog\"\n className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}\n >\n <Search\n className={triggerIconSize === undefined ? \"size-4\" : undefined}\n style={triggerIconSize === undefined ? undefined : { width: triggerIconSize, height: triggerIconSize }}\n />\n </button>\n </Tooltip>\n )}\n <CommandPalette\n open={open}\n onClose={() => setOpen(false)}\n search={provider}\n revision={items}\n query={query}\n onQueryChange={setQuery}\n searchOn={searchOn}\n redactLabels={redactLabels}\n fullScreenOnPhone={fullScreenOnPhone}\n density={density}\n labels={resolvedPaletteLabels}\n />\n </>\n );\n}\n\nconst EMPTY: readonly SearchEntry[] = [];\n"],"mappings":"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","names":["out"]}
|
package/dist/search.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CommandItem, CommandPalette, CommandPaletteLabels, DEFAULT_COMMAND_PALETTE_LABELS, useCommandKey } from './search/command-palette.js';
|
|
1
|
+
export { CommandItem, CommandPalette, CommandPaletteDensity, CommandPaletteLabels, DEFAULT_COMMAND_PALETTE_LABELS, useCommandKey } from './search/command-palette.js';
|
|
2
2
|
export { SEARCH_TIER_POINTS, SearchEntry, SearchHit, SearchIndex, SearchIndexOptions, SearchMatchField, createSearchIndex, matchEntries, normalizeSearchText } from './search/search-index.js';
|
|
3
3
|
export { DEFAULT_GLOBAL_SEARCH_LABELS, GlobalSearch, GlobalSearchLabels, GlobalSearchProps, GlobalSearchSource, GlobalSearchSuggestion, GlobalSearchTriggerProps } from './search/global-search.js';
|
|
4
4
|
import 'react';
|
|
@@ -33,7 +33,7 @@ function StepperNav({
|
|
|
33
33
|
const canFinish = wizard.canFinish;
|
|
34
34
|
const pending = wizard.isValidating;
|
|
35
35
|
const stepNextLabel = wizard.currentStep.nextLabel;
|
|
36
|
-
let forwardButton
|
|
36
|
+
let forwardButton;
|
|
37
37
|
if (wizard.isCommitStep) {
|
|
38
38
|
const finishButton = /* @__PURE__ */ jsxs(
|
|
39
39
|
Button,
|