@eifi1/ui-kit 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +4 -0
  2. package/dist/components/amount-input.d.ts +2 -0
  3. package/dist/components/calculator.d.ts +2 -0
  4. package/dist/components/chip.d.ts +27 -5
  5. package/dist/components/chip.js +6 -2
  6. package/dist/components/chip.js.map +1 -1
  7. package/dist/components/data-table-filter-popover.d.ts +1 -1
  8. package/dist/components/data-table-filters.d.ts +1 -1
  9. package/dist/components/data-table.d.ts +1 -1
  10. package/dist/components/data-table.js +122 -91
  11. package/dist/components/data-table.js.map +1 -1
  12. package/dist/components/date-picker.d.ts +69 -3
  13. package/dist/components/date-picker.js +152 -65
  14. package/dist/components/date-picker.js.map +1 -1
  15. package/dist/components/disclosure.d.ts +11 -2
  16. package/dist/components/disclosure.js +10 -4
  17. package/dist/components/disclosure.js.map +1 -1
  18. package/dist/components/floating-panel.d.ts +29 -2
  19. package/dist/components/floating-panel.js +16 -2
  20. package/dist/components/floating-panel.js.map +1 -1
  21. package/dist/components/number-field.d.ts +2 -0
  22. package/dist/components/number-input.d.ts +2 -0
  23. package/dist/components/numpad-sheet.d.ts +2 -0
  24. package/dist/components/progress-bar.d.ts +16 -2
  25. package/dist/components/progress-bar.js +4 -2
  26. package/dist/components/progress-bar.js.map +1 -1
  27. package/dist/components/ui.d.ts +37 -14
  28. package/dist/components/ui.js +28 -22
  29. package/dist/components/ui.js.map +1 -1
  30. package/dist/components/use-table-state.d.ts +1 -1
  31. package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
  32. package/dist/data-table.d.ts +1 -1
  33. package/dist/hooks/use-overlay-history.js +84 -4
  34. package/dist/hooks/use-overlay-history.js.map +1 -1
  35. package/dist/i18n/defaults.d.ts +2 -0
  36. package/dist/i18n/defaults.js +2 -0
  37. package/dist/i18n/defaults.js.map +1 -1
  38. package/dist/i18n/kit-labels.d.ts +3 -0
  39. package/dist/i18n/kit-labels.js.map +1 -1
  40. package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
  41. package/dist/i18n/locales/de-CH.d.ts +2 -0
  42. package/dist/i18n/locales/de-informal.d.ts +2 -0
  43. package/dist/i18n/locales/de.d.ts +2 -0
  44. package/dist/i18n/locales/de.js +10 -0
  45. package/dist/i18n/locales/de.js.map +1 -1
  46. package/dist/i18n/locales/es.d.ts +2 -0
  47. package/dist/i18n/locales/es.js +10 -0
  48. package/dist/i18n/locales/es.js.map +1 -1
  49. package/dist/i18n/locales/fr.d.ts +2 -0
  50. package/dist/i18n/locales/fr.js +10 -0
  51. package/dist/i18n/locales/fr.js.map +1 -1
  52. package/dist/i18n/locales/hu.d.ts +2 -0
  53. package/dist/i18n/locales/hu.js +10 -0
  54. package/dist/i18n/locales/hu.js.map +1 -1
  55. package/dist/i18n/locales/it.d.ts +2 -0
  56. package/dist/i18n/locales/it.js +10 -0
  57. package/dist/i18n/locales/it.js.map +1 -1
  58. package/dist/i18n/locales/zh.d.ts +2 -0
  59. package/dist/i18n/locales/zh.js +10 -0
  60. package/dist/i18n/locales/zh.js.map +1 -1
  61. package/dist/index.d.ts +5 -3
  62. package/dist/index.js +2 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/search/command-palette.d.ts +49 -1
  65. package/dist/search/command-palette.js +132 -29
  66. package/dist/search/command-palette.js.map +1 -1
  67. package/dist/search/global-search.d.ts +141 -0
  68. package/dist/search/global-search.js +277 -0
  69. package/dist/search/global-search.js.map +1 -0
  70. package/dist/search/search-index.d.ts +105 -0
  71. package/dist/search/search-index.js +142 -0
  72. package/dist/search/search-index.js.map +1 -0
  73. package/dist/search.d.ts +2 -0
  74. package/dist/search.js +2 -0
  75. package/dist/search.js.map +1 -1
  76. package/dist/wizard/stepper-nav.d.ts +10 -1
  77. package/dist/wizard/stepper-nav.js +2 -1
  78. package/dist/wizard/stepper-nav.js.map +1 -1
  79. package/package.json +12 -3
  80. package/src/components/chip.tsx +36 -6
  81. package/src/components/data-table.tsx +81 -5
  82. package/src/components/date-picker.tsx +328 -117
  83. package/src/components/disclosure.tsx +24 -8
  84. package/src/components/floating-panel.tsx +46 -2
  85. package/src/components/progress-bar.tsx +27 -3
  86. package/src/components/ui.tsx +88 -37
  87. package/src/hooks/use-overlay-history.ts +181 -4
  88. package/src/i18n/defaults.ts +2 -0
  89. package/src/i18n/kit-labels.tsx +2 -0
  90. package/src/i18n/locales/de.ts +10 -0
  91. package/src/i18n/locales/es.ts +10 -0
  92. package/src/i18n/locales/fr.ts +10 -0
  93. package/src/i18n/locales/hu.ts +10 -0
  94. package/src/i18n/locales/it.ts +10 -0
  95. package/src/i18n/locales/zh.ts +10 -0
  96. package/src/index.ts +4 -0
  97. package/src/search/command-palette.tsx +200 -27
  98. package/src/search/global-search.tsx +484 -0
  99. package/src/search/search-index.ts +295 -0
  100. package/src/search.ts +2 -0
  101. package/src/wizard/stepper-nav.tsx +11 -1
@@ -2,18 +2,38 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useId, useMemo, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { Search } from "lucide-react";
5
+ import { CornerDownLeft, Search, X } from "lucide-react";
6
6
  import { cn } from "../lib/cn.js";
7
+ import { PHONE_QUERY } from "../components/ui.js";
8
+ import { useMediaQuery } from "../hooks/use-media-query.js";
7
9
  import { useOverlayHistory } from "../hooks/use-overlay-history.js";
8
10
  import { useFocusTrap } from "../hooks/use-focus-trap.js";
9
11
  import { useBodyScrollLock } from "../hooks/use-body-scroll-lock.js";
10
12
  import { useKitLabels } from "../i18n/kit-labels.js";
13
+ function groupItems(items) {
14
+ const order = [];
15
+ const byGroup = /* @__PURE__ */ new Map();
16
+ for (const item of items) {
17
+ if (!byGroup.has(item.group)) {
18
+ byGroup.set(item.group, []);
19
+ order.push(item.group);
20
+ }
21
+ byGroup.get(item.group).push(item);
22
+ }
23
+ return {
24
+ groups: order.map((g) => ({ group: g, items: byGroup.get(g) })),
25
+ flat: order.flatMap((g) => byGroup.get(g)).filter((item) => item.kind !== "status")
26
+ };
27
+ }
11
28
  const DEFAULT_COMMAND_PALETTE_LABELS = {
12
29
  placeholder: "Search\u2026",
13
30
  empty: "No results",
14
31
  loading: "Searching\u2026",
15
32
  dialog: "Search",
16
- error: "Search failed. Try again."
33
+ error: "Search failed. Try again.",
34
+ clear: "Clear search",
35
+ submit: "Search",
36
+ close: "Close"
17
37
  };
18
38
  const { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;
19
39
  function useCommandKey(onOpen) {
@@ -35,6 +55,9 @@ function CommandPalette({
35
55
  revision,
36
56
  query: queryProp,
37
57
  onQueryChange,
58
+ searchOn = "input",
59
+ redactLabels = false,
60
+ fullScreenOnPhone = true,
38
61
  labels
39
62
  }) {
40
63
  const l = useKitLabels("commandPalette", DEFAULTS_WITHOUT_DIALOG, labels);
@@ -46,6 +69,20 @@ function CommandPalette({
46
69
  if (!controlled) setOwnQuery(next);
47
70
  onQueryChange?.(next);
48
71
  };
72
+ const submitMode = searchOn === "submit";
73
+ const [draft, setDraft] = useState(query);
74
+ const [draftBase, setDraftBase] = useState(query);
75
+ if (draftBase !== query) {
76
+ setDraftBase(query);
77
+ setDraft(query);
78
+ }
79
+ const fieldText = submitMode ? draft : query;
80
+ const commit = (next) => {
81
+ setDraft(next);
82
+ setQuery(next);
83
+ };
84
+ const dirty = submitMode && draft !== query;
85
+ const isPhone = useMediaQuery(PHONE_QUERY, false) && fullScreenOnPhone;
49
86
  const [results, setResults] = useState([]);
50
87
  const [loading, setLoading] = useState(false);
51
88
  const [failed, setFailed] = useState(false);
@@ -60,13 +97,17 @@ function CommandPalette({
60
97
  const searchRef = useRef(search);
61
98
  searchRef.current = search;
62
99
  const reqId = useRef(0);
100
+ const shownQuery = useRef(null);
101
+ const activeIdRef = useRef(void 0);
63
102
  useOverlayHistory(open, onClose);
64
103
  useFocusTrap(dialogRef, { active: open, initialFocus: () => inputRef.current });
65
104
  useBodyScrollLock(open);
66
105
  useEffect(() => {
67
106
  if (!open) return;
68
107
  if (!controlled) setOwnQuery("");
108
+ setDraft(controlled ? query : "");
69
109
  setActive(0);
110
+ shownQuery.current = null;
70
111
  }, [open]);
71
112
  useEffect(() => {
72
113
  if (!open) return;
@@ -76,12 +117,16 @@ function CommandPalette({
76
117
  try {
77
118
  const r = await Promise.resolve(searchRef.current(query));
78
119
  if (reqId.current === id) {
120
+ const keep = shownQuery.current === query ? activeIdRef.current : void 0;
121
+ const at = keep === void 0 ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);
122
+ shownQuery.current = query;
79
123
  setResults(r);
80
124
  setFailed(false);
81
- setActive(0);
125
+ setActive(Math.max(at, 0));
82
126
  }
83
127
  } catch (err) {
84
128
  if (reqId.current === id) {
129
+ shownQuery.current = null;
85
130
  setResults([]);
86
131
  setFailed(true);
87
132
  setActive(0);
@@ -94,21 +139,10 @@ function CommandPalette({
94
139
  const t = setTimeout(run, query ? 150 : 0);
95
140
  return () => clearTimeout(t);
96
141
  }, [query, open, revision]);
97
- const { groups, flat } = useMemo(() => {
98
- const order = [];
99
- const byGroup = /* @__PURE__ */ new Map();
100
- for (const item of results) {
101
- if (!byGroup.has(item.group)) {
102
- byGroup.set(item.group, []);
103
- order.push(item.group);
104
- }
105
- byGroup.get(item.group).push(item);
106
- }
107
- return {
108
- groups: order.map((g) => ({ group: g, items: byGroup.get(g) })),
109
- flat: order.flatMap((g) => byGroup.get(g))
110
- };
111
- }, [results]);
142
+ const { groups, flat } = useMemo(() => groupItems(results), [results]);
143
+ useEffect(() => {
144
+ activeIdRef.current = flat[active]?.id;
145
+ });
112
146
  const choose = (item) => {
113
147
  if (!item) return;
114
148
  onClose();
@@ -123,7 +157,8 @@ function CommandPalette({
123
157
  setActive((i) => Math.max(i - 1, 0));
124
158
  } else if (e.key === "Enter") {
125
159
  e.preventDefault();
126
- choose(flat[active]);
160
+ if (dirty) commit(draft);
161
+ else choose(flat[active]);
127
162
  }
128
163
  };
129
164
  const onDialogKeyDown = (e) => {
@@ -142,7 +177,10 @@ function CommandPalette({
142
177
  /* @__PURE__ */ jsx(
143
178
  "div",
144
179
  {
145
- className: "fixed inset-0 z-[70] flex items-start justify-center bg-black/40 p-4 pt-[10vh]",
180
+ className: cn(
181
+ "fixed inset-0 z-[70] flex items-start justify-center bg-black/40",
182
+ isPhone ? "p-0" : "p-4 pt-[10vh]"
183
+ ),
146
184
  onMouseDown: (e) => {
147
185
  if (e.target === e.currentTarget) onClose();
148
186
  },
@@ -155,9 +193,19 @@ function CommandPalette({
155
193
  "aria-label": dialogName,
156
194
  tabIndex: -1,
157
195
  onKeyDown: onDialogKeyDown,
158
- className: "flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl outline-none",
196
+ "data-fullscreen": isPhone || void 0,
197
+ className: cn(
198
+ "flex w-full flex-col overflow-hidden bg-[var(--bg-surface)] outline-none",
199
+ isPhone ? "h-full" : "max-h-[70vh] max-w-xl rounded-xl border border-[var(--border)] shadow-2xl"
200
+ ),
201
+ style: isPhone ? {
202
+ paddingTop: "max(0px, env(safe-area-inset-top))",
203
+ paddingBottom: "max(0px, env(safe-area-inset-bottom))",
204
+ paddingLeft: "max(0px, env(safe-area-inset-left))",
205
+ paddingRight: "max(0px, env(safe-area-inset-right))"
206
+ } : void 0,
159
207
  children: [
160
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-[var(--border)] px-3", children: [
208
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-[var(--border)] px-3", children: [
161
209
  /* @__PURE__ */ jsx(Search, { className: "size-4 shrink-0 text-[var(--text-placeholder)]" }),
162
210
  /* @__PURE__ */ jsx(
163
211
  "input",
@@ -167,17 +215,57 @@ function CommandPalette({
167
215
  "aria-expanded": true,
168
216
  "aria-controls": listId,
169
217
  "aria-activedescendant": flat[active] ? itemId(flat[active].id) : void 0,
170
- value: query,
171
- onChange: (e) => setQuery(e.target.value),
218
+ value: fieldText,
219
+ onChange: (e) => submitMode ? setDraft(e.target.value) : setQuery(e.target.value),
172
220
  onKeyDown,
173
221
  placeholder: l.placeholder,
174
- className: "w-full bg-transparent py-3 text-sm outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]"
222
+ enterKeyHint: submitMode ? "search" : void 0,
223
+ className: cn(
224
+ "w-full bg-transparent py-3 outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]",
225
+ // 16px on a phone, or iOS zooms the page in on focus.
226
+ isPhone ? "text-base" : "text-sm"
227
+ )
175
228
  }
176
229
  ),
177
- loading && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-[var(--text-placeholder)]", children: l.loading })
230
+ loading && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[11px] text-[var(--text-placeholder)]", children: l.loading }),
231
+ fieldText !== "" && /* @__PURE__ */ jsx(
232
+ "button",
233
+ {
234
+ type: "button",
235
+ "aria-label": l.clear ?? DEFAULT_COMMAND_PALETTE_LABELS.clear,
236
+ onClick: () => {
237
+ commit("");
238
+ inputRef.current?.focus();
239
+ },
240
+ className: "shrink-0 rounded p-1 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]",
241
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
242
+ }
243
+ ),
244
+ dirty && /* @__PURE__ */ jsx(
245
+ "button",
246
+ {
247
+ type: "button",
248
+ "aria-label": l.submit ?? DEFAULT_COMMAND_PALETTE_LABELS.submit,
249
+ onClick: () => {
250
+ commit(draft);
251
+ inputRef.current?.focus();
252
+ },
253
+ className: "shrink-0 rounded p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
254
+ children: /* @__PURE__ */ jsx(CornerDownLeft, { className: "size-4" })
255
+ }
256
+ ),
257
+ isPhone && /* @__PURE__ */ jsx(
258
+ "button",
259
+ {
260
+ type: "button",
261
+ onClick: onClose,
262
+ className: "-me-1 shrink-0 rounded px-1 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
263
+ children: l.close ?? DEFAULT_COMMAND_PALETTE_LABELS.close
264
+ }
265
+ )
178
266
  ] }),
179
267
  failed && !loading && /* @__PURE__ */ jsx("div", { role: "alert", className: "px-3 py-6 text-center text-sm text-[var(--text-muted)]", children: l.error ?? DEFAULT_COMMAND_PALETTE_LABELS.error }),
180
- !failed && flat.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "px-3 py-6 text-center text-sm text-[var(--text-muted)]", children: l.empty }),
268
+ !failed && results.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "px-3 py-6 text-center text-sm text-[var(--text-muted)]", children: l.empty }),
181
269
  /* @__PURE__ */ jsx("ul", { id: listId, ref: listRef, role: "listbox", "aria-label": dialogName, className: "min-h-0 flex-1 overflow-y-auto py-1", children: groups.map(({ group, items }, groupIndex) => /* @__PURE__ */ jsxs("li", { role: "presentation", children: [
182
270
  /* @__PURE__ */ jsx(
183
271
  "div",
@@ -189,9 +277,24 @@ function CommandPalette({
189
277
  }
190
278
  ),
191
279
  /* @__PURE__ */ jsx("ul", { role: "group", "aria-labelledby": groupId(groupIndex), children: items.map((item) => {
280
+ if (item.kind === "status") {
281
+ return /* @__PURE__ */ jsxs(
282
+ "li",
283
+ {
284
+ role: "presentation",
285
+ className: "flex items-center gap-2.5 px-3 py-2 text-xs text-[var(--text-placeholder)]",
286
+ children: [
287
+ item.icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center", children: item.icon }),
288
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label })
289
+ ]
290
+ },
291
+ item.id
292
+ );
293
+ }
192
294
  flatIndex += 1;
193
295
  const idx = flatIndex;
194
296
  const isActive = idx === active;
297
+ const redact = item.redact ?? redactLabels;
195
298
  const shared = {
196
299
  id: itemId(item.id),
197
300
  role: "option",
@@ -205,8 +308,8 @@ function CommandPalette({
205
308
  };
206
309
  const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
207
310
  item.icon && /* @__PURE__ */ jsx("span", { className: "flex size-4 shrink-0 items-center justify-center text-[var(--text-placeholder)]", children: item.icon }),
208
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: item.label }),
209
- item.hint && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-[var(--text-placeholder)]", children: item.hint })
311
+ /* @__PURE__ */ jsx("span", { "data-private": redact || void 0, className: "min-w-0 flex-1 truncate", children: item.label }),
312
+ item.hint && /* @__PURE__ */ jsx("span", { "data-private": redact || void 0, className: "shrink-0 text-xs text-[var(--text-placeholder)]", children: item.hint })
210
313
  ] });
211
314
  return /* @__PURE__ */ jsx("li", { role: "presentation", children: item.href ? /* @__PURE__ */ jsx(
212
315
  "a",
@@ -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 { Search } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\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}\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}\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};\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 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 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 [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\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 setActive(0);\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 setResults(r);\n setFailed(false);\n setActive(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 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(() => {\n const order: string[] = [];\n const byGroup = new Map<string, CommandItem[]>();\n for (const item of results) {\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)!),\n };\n }, [results]);\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 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=\"fixed inset-0 z-[70] flex items-start justify-center bg-black/40 p-4 pt-[10vh]\"\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 className=\"flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--bg-surface)] shadow-2xl outline-none\"\n >\n <div className=\"flex 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={query}\n onChange={(e) => setQuery(e.target.value)}\n onKeyDown={onKeyDown}\n placeholder={l.placeholder}\n className=\"w-full bg-transparent py-3 text-sm outline-none placeholder:text-[var(--text-placeholder)] text-[var(--text-primary)]\"\n />\n {loading && <span className=\"shrink-0 text-[11px] text-[var(--text-placeholder)]\">{l.loading}</span>}\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 {!failed && flat.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 flatIndex += 1;\n const idx = flatIndex;\n const isActive = idx === active;\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 className=\"min-w-0 flex-1 truncate\">{item.label}</span>\n {item.hint && (\n <span className=\"shrink-0 text-xs text-[var(--text-placeholder)]\">{item.hint}</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/** 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":[]}
@@ -0,0 +1,141 @@
1
+ import { CommandPaletteLabels } from './command-palette.js';
2
+ import { SearchEntry, SearchIndexOptions } from './search-index.js';
3
+ import * as react from 'react';
4
+ import { ReactNode } from 'react';
5
+
6
+ interface GlobalSearchLabels {
7
+ /** The trigger's accessible name, and the dialog's. */
8
+ trigger: string;
9
+ /** The field's placeholder — says WHAT can be found, which "Search…" does not. */
10
+ placeholder: string;
11
+ /** The trigger's tooltip, given the shortcut as the platform writes it ("⌘K",
12
+ * "Ctrl K"). A function so a language can put the keys where its grammar wants. */
13
+ shortcut: (keys: string) => string;
14
+ /** Heading of the suggestions shown for an empty query. */
15
+ suggestions: string;
16
+ /** Heading for entries that name no `group`. */
17
+ results: string;
18
+ }
19
+ declare const DEFAULT_GLOBAL_SEARCH_LABELS: GlobalSearchLabels;
20
+ /**
21
+ * A place results come from that is not the static index — a server search, a
22
+ * local-first store too large to hand over whole. Each source is its own group, with its
23
+ * own debounce, its own "Searching…" line and its own failure: the static groups never
24
+ * wait for it, and one source failing never takes another's results with it.
25
+ */
26
+ interface GlobalSearchSource {
27
+ /** Stable key — the effect that runs the source is keyed by it, not by identity. */
28
+ id: string;
29
+ /** The group its results are shown under (an entry's own `group` is ignored). */
30
+ group: string;
31
+ /** Called with the trimmed query. Its results are shown AS GIVEN — the source ranked
32
+ * them — and never re-filtered by the index's matcher. `signal` aborts when the query
33
+ * moves on or the palette closes; a stale answer is dropped either way. */
34
+ search: (query: string, signal: AbortSignal) => Promise<readonly SearchEntry[]> | readonly SearchEntry[];
35
+ /** Shortest query it is asked for. Default 2 — one letter matches half a database. */
36
+ minChars?: number;
37
+ /** Quiet period after the last keystroke before it is asked. Default 200ms. */
38
+ debounceMs?: number;
39
+ /** Rows kept. Default: `indexOptions.groupLimit` (8). */
40
+ limit?: number;
41
+ /** Mask every row of this source for session replay, hints (amounts) included. An
42
+ * entry's own `redact` still wins. */
43
+ redact?: boolean;
44
+ /** Told when `search` throws or rejects (not when it is aborted). The group then
45
+ * shows the palette's `error` line instead of rows. */
46
+ onError?: (error: unknown) => void;
47
+ }
48
+ /** What a custom trigger gets: everything the default button uses. */
49
+ interface GlobalSearchTriggerProps {
50
+ open: () => void;
51
+ /** Resolved `trigger` label — the accessible name. */
52
+ label: string;
53
+ /** Resolved `shortcut(keys)` — the tooltip. */
54
+ tooltip: string;
55
+ /** "⌘K" on Apple platforms, "Ctrl K" elsewhere. */
56
+ keys: string;
57
+ /** For the trigger's `aria-keyshortcuts`. */
58
+ ariaKeyShortcuts: string;
59
+ }
60
+ /** A suggestion for the empty query: an entry, the `id` of one in `entries`, or a query
61
+ * to type into the field. */
62
+ type GlobalSearchSuggestion = SearchEntry | string | {
63
+ query: string;
64
+ };
65
+ interface GlobalSearchProps {
66
+ /**
67
+ * The static index: pages, actions, settings rows — already filtered by the app (by
68
+ * role, by privacy mode). Matched and ranked by {@link createSearchIndex}.
69
+ *
70
+ * Changing it re-runs the open search by itself. Nothing has to be stable: the index is
71
+ * rebuilt only when the entries' TEXT changes (ids, titles, keywords, …), so a list
72
+ * rebuilt on every render — `data = []` while a query is pending, icons as fresh JSX —
73
+ * neither re-indexes nor loops, and the icons and handlers used are always the latest.
74
+ */
75
+ entries?: readonly SearchEntry[];
76
+ /** The async sources, each streamed into its own group. */
77
+ sources?: readonly GlobalSearchSource[];
78
+ /** What the empty query shows, under the `suggestions` heading. */
79
+ suggestions?: readonly GlobalSearchSuggestion[];
80
+ /** Groups listed here come first, in this order; the rest follow in ranking order. */
81
+ groupOrder?: readonly string[];
82
+ /** Ranking and capping — `groupLimit` (default 8) applies to every group. */
83
+ indexOptions?: SearchIndexOptions;
84
+ /**
85
+ * How an entry's `href` is followed on a plain click or ↵. Default: react-router's
86
+ * `navigate` when the search is rendered inside a router, else a full page load.
87
+ */
88
+ navigate?: (href: string) => void;
89
+ /**
90
+ * The browser URL of an `href`, for the row's real link (middle-/⌘-click opens a tab).
91
+ * Default inside a router: the router's own (`#/page` under a hash router, the
92
+ * basename prefixed under a browser router); outside one, the href itself.
93
+ */
94
+ hrefFor?: (href: string) => string;
95
+ /** Register ⌘K / Ctrl K. Default true; turn it off for a second, scoped search on a
96
+ * page that already has the app-wide one. */
97
+ shortcut?: boolean;
98
+ /** Controlled open state — so a button anywhere can open it. */
99
+ open?: boolean;
100
+ onOpenChange?: (open: boolean) => void;
101
+ /** Controlled query, as on `CommandPalette` (keksdose binds it to the URL's `q`). */
102
+ query?: string;
103
+ onQueryChange?: (query: string) => void;
104
+ searchOn?: "input" | "submit";
105
+ /** Mask every row for session replay (see `CommandPalette`). A source's or an entry's
106
+ * own `redact` wins. */
107
+ redactLabels?: boolean;
108
+ fullScreenOnPhone?: boolean;
109
+ /** `"button"` (default): the top-bar icon button. `"none"`: no trigger — the shortcut
110
+ * or a controlled `open` opens it. A function renders a trigger of your own. */
111
+ trigger?: "button" | "none" | ((props: GlobalSearchTriggerProps) => ReactNode);
112
+ /** Hide the default trigger below `sm` (kastlan's phone top bar has no room). */
113
+ hideTriggerOnPhone?: boolean;
114
+ /** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */
115
+ triggerClassName?: string;
116
+ labels?: Partial<GlobalSearchLabels>;
117
+ /** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */
118
+ paletteLabels?: Partial<CommandPaletteLabels>;
119
+ }
120
+ /**
121
+ * The app-wide ⌘K search, whole: the top-bar trigger, the shortcut, and a
122
+ * {@link CommandPalette} fed from a ranked index ({@link createSearchIndex}) and any
123
+ * number of async sources.
124
+ *
125
+ * Every consumer had built this out of `CommandPalette` by hand, and each copy had the
126
+ * same holes — a substring match, no ranking, no typo tolerance, results in declaration
127
+ * order, one monolithic provider in which the static groups waited on the slowest
128
+ * request. This is that wrapper once:
129
+ *
130
+ * <GlobalSearch
131
+ * entries={pages} // { id, title, keywords, group, href, icon }
132
+ * sources={[{ id: "tx", group: "Transactions", search: (q, signal) => api.search(q, { signal }) }]}
133
+ * suggestions={["page-budget", "action-new-account"]}
134
+ * />
135
+ *
136
+ * Inside a react-router tree it navigates with the router and builds its links with it;
137
+ * outside one, pass `navigate` (or accept full page loads).
138
+ */
139
+ declare function GlobalSearch(props: GlobalSearchProps): react.JSX.Element;
140
+
141
+ export { DEFAULT_GLOBAL_SEARCH_LABELS, GlobalSearch, type GlobalSearchLabels, type GlobalSearchProps, type GlobalSearchSource, type GlobalSearchSuggestion, type GlobalSearchTriggerProps };