@charcuterie/ui 4.24.0 → 4.26.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 (63) hide show
  1. package/README.md +13 -0
  2. package/dist/CommandPalette/CommandPalette.d.ts +43 -0
  3. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -0
  4. package/dist/CommandPalette/CommandPalette.js +53 -0
  5. package/dist/CommandPalette/CommandPalette.js.map +1 -0
  6. package/dist/CommandPalette/CommandPaletteFooter.d.ts +16 -0
  7. package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -0
  8. package/dist/CommandPalette/CommandPaletteFooter.js +14 -0
  9. package/dist/CommandPalette/CommandPaletteFooter.js.map +1 -0
  10. package/dist/CommandPalette/CommandPaletteOption.d.ts +44 -0
  11. package/dist/CommandPalette/CommandPaletteOption.d.ts.map +1 -0
  12. package/dist/CommandPalette/CommandPaletteOption.js +72 -0
  13. package/dist/CommandPalette/CommandPaletteOption.js.map +1 -0
  14. package/dist/CommandPalette/CommandPalettePanel.d.ts +52 -0
  15. package/dist/CommandPalette/CommandPalettePanel.d.ts.map +1 -0
  16. package/dist/CommandPalette/CommandPalettePanel.js +168 -0
  17. package/dist/CommandPalette/CommandPalettePanel.js.map +1 -0
  18. package/dist/CommandPalette/commandPaletteSections.d.ts +93 -0
  19. package/dist/CommandPalette/commandPaletteSections.d.ts.map +1 -0
  20. package/dist/CommandPalette/commandPaletteSections.js +94 -0
  21. package/dist/CommandPalette/commandPaletteSections.js.map +1 -0
  22. package/dist/CommandPalette/useCommandPalette.d.ts +32 -0
  23. package/dist/CommandPalette/useCommandPalette.d.ts.map +1 -0
  24. package/dist/CommandPalette/useCommandPalette.js +40 -0
  25. package/dist/CommandPalette/useCommandPalette.js.map +1 -0
  26. package/dist/CommandPalette/useCommandPaletteHotkeys.d.ts +35 -0
  27. package/dist/CommandPalette/useCommandPaletteHotkeys.d.ts.map +1 -0
  28. package/dist/CommandPalette/useCommandPaletteHotkeys.js +114 -0
  29. package/dist/CommandPalette/useCommandPaletteHotkeys.js.map +1 -0
  30. package/dist/GroupedList/GroupedList.d.ts +78 -1
  31. package/dist/GroupedList/GroupedList.d.ts.map +1 -1
  32. package/dist/GroupedList/GroupedList.js +14 -0
  33. package/dist/GroupedList/GroupedList.js.map +1 -1
  34. package/dist/GroupedList/GroupedListRow.d.ts +17 -6
  35. package/dist/GroupedList/GroupedListRow.d.ts.map +1 -1
  36. package/dist/GroupedList/GroupedListRow.js +75 -8
  37. package/dist/GroupedList/GroupedListRow.js.map +1 -1
  38. package/dist/index.d.ts +7 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/styles.css +36 -4
  43. package/package.json +2 -2
  44. package/src/CommandPalette/CommandPalette.mdx +129 -0
  45. package/src/CommandPalette/CommandPalette.stories.tsx +413 -0
  46. package/src/CommandPalette/CommandPalette.test.tsx +553 -0
  47. package/src/CommandPalette/CommandPalette.tsx +91 -0
  48. package/src/CommandPalette/CommandPaletteFooter.tsx +45 -0
  49. package/src/CommandPalette/CommandPaletteOption.tsx +200 -0
  50. package/src/CommandPalette/CommandPalettePanel.tsx +411 -0
  51. package/src/CommandPalette/commandPalette.storyHelpers.tsx +248 -0
  52. package/src/CommandPalette/commandPaletteSections.test.ts +156 -0
  53. package/src/CommandPalette/commandPaletteSections.ts +219 -0
  54. package/src/CommandPalette/useCommandPalette.ts +75 -0
  55. package/src/CommandPalette/useCommandPaletteHotkeys.ts +158 -0
  56. package/src/GroupedList/GroupedList.mdx +42 -1
  57. package/src/GroupedList/GroupedList.stories.tsx +252 -2
  58. package/src/GroupedList/GroupedList.test.tsx +391 -0
  59. package/src/GroupedList/GroupedList.tsx +78 -1
  60. package/src/GroupedList/GroupedListRow.tsx +135 -10
  61. package/src/index.ts +14 -0
  62. package/src/sourceRules.test.ts +2 -1
  63. package/src/styles.css +36 -4
package/README.md CHANGED
@@ -147,6 +147,19 @@ own `aria-valuemin`/`aria-valuemax`. What is genuinely shared is the arithmetic
147
147
  same extraction `tileStyles.ts` is), compared as **computed** styles in the test
148
148
  ([decision](../../docs/decisions/2026-09-01-a-two-thumb-range-is-its-own-component-and-shares-the-bar.md)).
149
149
 
150
+ **`CommandPalette` is the launcher** — a search field over a grouped list in a `Modal`, built
151
+ here first because a start page, Docket and Folio all want one and nothing in the fleet had
152
+ one. It lists the caller's `suggestions` before anything is typed, which is the defect it
153
+ exists to fix: the third-party launcher it replaces showed nothing until you typed. Typing
154
+ matches **word prefixes and initials, never subsequences** (`ha` finds `Home Automation`,
155
+ not `Chat`), through `rankWordPrefixMatches` in `@charcuterie/logic`. A short prefix and a
156
+ space (`au lego`) routes the rest of the query to a caller's target, and the caller's
157
+ fallback row ends every other list. The library owns the shape and no site names. The global
158
+ keys are an opt-in hook, `useCommandPaletteHotkeys`, and `useCommandPalette()` assembles them
159
+ with the open state
160
+ ([matching](../../docs/decisions/2026-10-16-a-command-palette-matches-word-prefixes.md),
161
+ [keys](../../docs/decisions/2026-10-16-command-palette-hotkeys-are-an-opt-in-hook.md)).
162
+
150
163
  **The unified app shell adds four**: `Shell`, `Header`, `Rail`, `Main` — the fleet's largest
151
164
  single duplication, where ten of twelve UI repos hand-roll the page chrome and three of them
152
165
  have a file named `AppShell.tsx` (two of those headers are a *byte-identical* class string,
@@ -0,0 +1,43 @@
1
+ import type { ReactNode } from "react";
2
+ import { type CommandPalettePanelProps } from "./CommandPalettePanel.tsx";
3
+ export type CommandPaletteProps = Omit<CommandPalettePanelProps, "inputRef" | "onChoose"> & {
4
+ className?: string;
5
+ isVisible: boolean;
6
+ /** Every route out: Escape, a press outside, and a chosen row. */
7
+ onClose: () => void;
8
+ };
9
+ /**
10
+ * The launcher: a search field over a grouped list, opened over the
11
+ * page.
12
+ *
13
+ * - **It shows something before anything is typed** — the caller's
14
+ * `suggestions` ("Pinned", "Recently opened").
15
+ * - **Typing filters `items` by word prefix**: every typed word must
16
+ * start a word of the title, the description or a keyword, and the
17
+ * initials count (`ha` → `Home Automation`). Matches are grouped by
18
+ * each item's `group`.
19
+ * - **A prefix command** — `au lego` — routes the rest of the query
20
+ * to one of the caller's `prefixes`.
21
+ * - **The last row is the caller's fallback** (`getFallbackItem`), so
22
+ * a query nothing matches still goes somewhere.
23
+ *
24
+ * The library owns the shape. Every name, link and target is data the
25
+ * app passes in.
26
+ *
27
+ * ### Open state
28
+ *
29
+ * `isVisible` and `onClose`, as on `Modal`: the app holds the state,
30
+ * usually in `useCommandPalette()`, which also wires the global keys.
31
+ * The query is controlled (`query` + `onQueryChange`) or uncontrolled
32
+ * (`defaultQuery`, read each time it opens), as on `Combobox`.
33
+ *
34
+ * ### Keys
35
+ *
36
+ * Arrow keys, Home and End move the active row; Enter opens it;
37
+ * `Ctrl Enter` or `⌘ Enter` opens an `href` row in a new tab; Escape
38
+ * closes. Focus stays in the field the whole time and the active row
39
+ * is its `aria-activedescendant`. Focus is trapped while it is open and
40
+ * goes back to where it was when it closes.
41
+ */
42
+ export declare const CommandPalette: ({ className, isVisible, label, onClose, ...panelProps }: CommandPaletteProps) => ReactNode;
43
+ //# sourceMappingURL=CommandPalette.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPalette.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPalette.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAKtC,OAAO,EAEL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAA;AAElC,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,wBAAwB,EACxB,UAAU,GAAG,UAAU,CACxB,GAAG;IACF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,cAAc,GAAI,yDAM5B,mBAAmB,KAAG,SAmBxB,CAAA"}
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { Modal } from "../Modal/Modal.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ import { CommandPalettePanel, } from "./CommandPalettePanel.js";
6
+ /**
7
+ * Top-anchored on a wide window, so the list grows downward from a
8
+ * fixed field instead of re-centering as it fills; nearly the whole
9
+ * screen in the Narrow View, where a phone keyboard takes the bottom.
10
+ *
11
+ * `self-start` because `OverlayPanel` centers its child in a flex
12
+ * box. `dvh` for the reason `Dialog` gives: on a phone `vh` includes
13
+ * the address bar.
14
+ */
15
+ const PALETTE_BOX_CLASS = "mt-[12dvh] max-h-[76dvh] w-[calc(100vw-2rem)] max-w-[45rem] self-start max-sm:mt-2 max-sm:h-[calc(100dvh-1rem)] max-sm:max-h-[calc(100dvh-1rem)] max-sm:w-[calc(100vw-1rem)]";
16
+ /**
17
+ * The launcher: a search field over a grouped list, opened over the
18
+ * page.
19
+ *
20
+ * - **It shows something before anything is typed** — the caller's
21
+ * `suggestions` ("Pinned", "Recently opened").
22
+ * - **Typing filters `items` by word prefix**: every typed word must
23
+ * start a word of the title, the description or a keyword, and the
24
+ * initials count (`ha` → `Home Automation`). Matches are grouped by
25
+ * each item's `group`.
26
+ * - **A prefix command** — `au lego` — routes the rest of the query
27
+ * to one of the caller's `prefixes`.
28
+ * - **The last row is the caller's fallback** (`getFallbackItem`), so
29
+ * a query nothing matches still goes somewhere.
30
+ *
31
+ * The library owns the shape. Every name, link and target is data the
32
+ * app passes in.
33
+ *
34
+ * ### Open state
35
+ *
36
+ * `isVisible` and `onClose`, as on `Modal`: the app holds the state,
37
+ * usually in `useCommandPalette()`, which also wires the global keys.
38
+ * The query is controlled (`query` + `onQueryChange`) or uncontrolled
39
+ * (`defaultQuery`, read each time it opens), as on `Combobox`.
40
+ *
41
+ * ### Keys
42
+ *
43
+ * Arrow keys, Home and End move the active row; Enter opens it;
44
+ * `Ctrl Enter` or `⌘ Enter` opens an `href` row in a new tab; Escape
45
+ * closes. Focus stays in the field the whole time and the active row
46
+ * is its `aria-activedescendant`. Focus is trapped while it is open and
47
+ * goes back to where it was when it closes.
48
+ */
49
+ export const CommandPalette = ({ className, isVisible, label = "Search", onClose, ...panelProps }) => {
50
+ const inputRef = useRef(null);
51
+ return (_jsx(Modal, { "aria-label": label, className: toClassName(PALETTE_BOX_CLASS, className), initialFocus: inputRef, isVisible: isVisible, onClose: onClose, children: _jsx(CommandPalettePanel, { ...panelProps, inputRef: inputRef, label: label, onChoose: onClose }) }));
52
+ };
53
+ //# sourceMappingURL=CommandPalette.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPalette.js","sourceRoot":"","sources":["../../src/CommandPalette/CommandPalette.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAoB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EACL,mBAAmB,GAEpB,MAAM,0BAA2B,CAAA;AAYlC;;;;;;;;GAQG;AACH,MAAM,iBAAiB,GACrB,8KAA8K,CAAA;AAEhL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAC7B,SAAS,EACT,SAAS,EACT,KAAK,GAAG,QAAQ,EAChB,OAAO,EACP,GAAG,UAAU,EACO,EAAa,EAAE;IACnC,MAAM,QAAQ,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAA;IAE/C,OAAO,CACL,KAAC,KAAK,kBACQ,KAAK,EACjB,SAAS,EAAE,WAAW,CAAC,iBAAiB,EAAE,SAAS,CAAC,EACpD,YAAY,EAAE,QAAQ,EACtB,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,OAAO,YAEhB,KAAC,mBAAmB,OACd,UAAU,EACd,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,OAAO,GACjB,GACI,CACT,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ import type { CommandPalettePrefix } from "./commandPaletteSections.ts";
3
+ /**
4
+ * The default strip under the list: the keys, and each prefix with
5
+ * its target.
6
+ *
7
+ * The keys are words — `Up`, `Down` — not arrow glyphs, because a font
8
+ * without the glyph paints nothing
9
+ * (`2026-07-29-ship-no-icons-and-no-symbol-glyphs.md`). In the Narrow
10
+ * View the key hints give way, since a phone has no arrow keys, and
11
+ * the prefixes stay: they work from a phone keyboard too.
12
+ */
13
+ export declare const CommandPaletteFooter: ({ prefixes, }: {
14
+ prefixes: readonly CommandPalettePrefix[];
15
+ }) => ReactNode;
16
+ //# sourceMappingURL=CommandPaletteFooter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPaletteFooter.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteFooter.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAQvE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,GAAI,eAElC;IACD,QAAQ,EAAE,SAAS,oBAAoB,EAAE,CAAA;CAC1C,KAAG,SAoBH,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ const renderKey = (name) => (_jsx("kbd", { className: "rounded-sm border border-border-default bg-surface-raised px-1 font-mono text-content-secondary text-xs", children: name }));
3
+ /**
4
+ * The default strip under the list: the keys, and each prefix with
5
+ * its target.
6
+ *
7
+ * The keys are words — `Up`, `Down` — not arrow glyphs, because a font
8
+ * without the glyph paints nothing
9
+ * (`2026-07-29-ship-no-icons-and-no-symbol-glyphs.md`). In the Narrow
10
+ * View the key hints give way, since a phone has no arrow keys, and
11
+ * the prefixes stay: they work from a phone keyboard too.
12
+ */
13
+ export const CommandPaletteFooter = ({ prefixes, }) => (_jsxs(_Fragment, { children: [_jsxs("span", { className: "max-sm:hidden", children: [renderKey("Up"), " ", renderKey("Down"), " move"] }), _jsxs("span", { className: "max-sm:hidden", children: [renderKey("Enter"), " open"] }), _jsxs("span", { className: "max-sm:hidden", children: [renderKey("Ctrl"), " ", renderKey("Enter"), " new tab"] }), prefixes.map(({ label, prefix }) => (_jsxs("span", { children: [renderKey(prefix), " ", label] }, prefix))), _jsxs("span", { className: "max-sm:hidden", children: [renderKey("Esc"), " close"] })] }));
14
+ //# sourceMappingURL=CommandPaletteFooter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPaletteFooter.js","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteFooter.tsx"],"names":[],"mappings":";AAIA,MAAM,SAAS,GAAG,CAAC,IAAY,EAAa,EAAE,CAAC,CAC7C,cAAK,SAAS,EAAC,yGAAyG,YACrH,IAAI,GACD,CACP,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,EACnC,QAAQ,GAGT,EAAa,EAAE,CAAC,CACf,8BACE,gBAAM,SAAS,EAAC,eAAe,aAC5B,SAAS,CAAC,IAAI,CAAC,OAAG,SAAS,CAAC,MAAM,CAAC,aAC/B,EACP,gBAAM,SAAS,EAAC,eAAe,aAC5B,SAAS,CAAC,OAAO,CAAC,aACd,EACP,gBAAM,SAAS,EAAC,eAAe,aAC5B,SAAS,CAAC,MAAM,CAAC,OAAG,SAAS,CAAC,OAAO,CAAC,gBAClC,EACN,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CACnC,2BACG,SAAS,CAAC,MAAM,CAAC,OAAG,KAAK,KADjB,MAAM,CAEV,CACR,CAAC,EACF,gBAAM,SAAS,EAAC,eAAe,aAC5B,SAAS,CAAC,KAAK,CAAC,cACZ,IACN,CACJ,CAAA"}
@@ -0,0 +1,44 @@
1
+ import type { TextMatchRange } from "@charcuterie/logic";
2
+ import type { ControlSize } from "@charcuterie/tokens";
3
+ import type { ReactNode } from "react";
4
+ import type { CommandPaletteItem } from "./commandPaletteSections.ts";
5
+ export type CommandPaletteOptionProps = {
6
+ /** The DOM id `aria-activedescendant` points at. */
7
+ id: string;
8
+ isActive: boolean;
9
+ item: CommandPaletteItem;
10
+ itemSize: ControlSize;
11
+ /** A click on the row: the link has navigated (or will), so close and run `onSelect`. */
12
+ onChoose: () => void;
13
+ /** The pointer moved over the row. */
14
+ onPoint: () => void;
15
+ titleRanges: readonly TextMatchRange[];
16
+ };
17
+ /**
18
+ * One row of the palette's listbox.
19
+ *
20
+ * **Focus never comes here.** The input keeps it, and the row the
21
+ * arrows have reached is announced through the input's
22
+ * `aria-activedescendant` — the APG combobox pattern `Combobox` uses
23
+ * too. So the row is `tabIndex={-1}`, and a mouse press is prevented
24
+ * from moving focus (`onMouseDown`), which keeps the caret in the
25
+ * input while the click still lands.
26
+ *
27
+ * **An `href` item is a real `<a href>`.** It goes through
28
+ * `UnstyledLink`, so an app's injected router link handles a plain
29
+ * click, and middle-click, ctrl-click and "copy link address" all
30
+ * work, because it is a link. Enter in the input clicks this same
31
+ * element, so the keyboard and the mouse take one path. An item
32
+ * without one is a `<button>`, as in `Combobox`.
33
+ *
34
+ * **The pointer moves the active row on `pointermove`, not
35
+ * `pointerenter`.** Arrowing scrolls the list under a mouse that has
36
+ * not moved, and an enter event would then hand the active row to
37
+ * whatever slid under the pointer.
38
+ *
39
+ * **One highlight.** The active row is tinted and nothing else is —
40
+ * no separate `:hover` tint — because the pointer makes a row active,
41
+ * and two tinted rows would leave Enter's target ambiguous.
42
+ */
43
+ export declare const CommandPaletteOption: ({ id, isActive, item, itemSize, onChoose, onPoint, titleRanges, }: CommandPaletteOptionProps) => ReactNode;
44
+ //# sourceMappingURL=CommandPaletteOption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPaletteOption.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteOption.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAEV,SAAS,EACV,MAAM,OAAO,CAAA;AAKd,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AAErE,MAAM,MAAM,yBAAyB,GAAG;IACtC,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,OAAO,CAAA;IACjB,IAAI,EAAE,kBAAkB,CAAA;IACxB,QAAQ,EAAE,WAAW,CAAA;IACrB,yFAAyF;IACzF,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,sCAAsC;IACtC,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,WAAW,EAAE,SAAS,cAAc,EAAE,CAAA;CACvC,CAAA;AAkDD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,oBAAoB,GAAI,mEAQlC,yBAAyB,KAAG,SA4F9B,CAAA"}
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { PANEL_ITEM_SIZE_CLASS } from "../controlStyles.js";
3
+ import { UnstyledLink } from "../RouterLink/UnstyledLink.js";
4
+ import { toClassName } from "../toClassName.js";
5
+ const ICON_SIZE_CLASS = {
6
+ lg: "size-7 [&>img]:size-7 [&>svg]:size-5",
7
+ md: "size-6 [&>img]:size-6 [&>svg]:size-4",
8
+ sm: "size-5 [&>img]:size-5 [&>svg]:size-4",
9
+ };
10
+ /**
11
+ * The title with each matched range wrapped in a `<mark>`. Plain
12
+ * text in, so nothing the caller typed is ever read as markup.
13
+ */
14
+ const HighlightedTitle = ({ ranges, title, }) => {
15
+ if (ranges.length === 0) {
16
+ return title;
17
+ }
18
+ const pieces = [];
19
+ let cursor = 0;
20
+ for (const { end, start } of ranges) {
21
+ if (start > cursor) {
22
+ pieces.push(title.slice(cursor, start));
23
+ }
24
+ pieces.push(_jsx("mark", { className: "bg-transparent font-semibold text-intent-accent-content", children: title.slice(start, end) }, start));
25
+ cursor = end;
26
+ }
27
+ if (cursor < title.length) {
28
+ pieces.push(title.slice(cursor));
29
+ }
30
+ return pieces;
31
+ };
32
+ /**
33
+ * One row of the palette's listbox.
34
+ *
35
+ * **Focus never comes here.** The input keeps it, and the row the
36
+ * arrows have reached is announced through the input's
37
+ * `aria-activedescendant` — the APG combobox pattern `Combobox` uses
38
+ * too. So the row is `tabIndex={-1}`, and a mouse press is prevented
39
+ * from moving focus (`onMouseDown`), which keeps the caret in the
40
+ * input while the click still lands.
41
+ *
42
+ * **An `href` item is a real `<a href>`.** It goes through
43
+ * `UnstyledLink`, so an app's injected router link handles a plain
44
+ * click, and middle-click, ctrl-click and "copy link address" all
45
+ * work, because it is a link. Enter in the input clicks this same
46
+ * element, so the keyboard and the mouse take one path. An item
47
+ * without one is a `<button>`, as in `Combobox`.
48
+ *
49
+ * **The pointer moves the active row on `pointermove`, not
50
+ * `pointerenter`.** Arrowing scrolls the list under a mouse that has
51
+ * not moved, and an enter event would then hand the active row to
52
+ * whatever slid under the pointer.
53
+ *
54
+ * **One highlight.** The active row is tinted and nothing else is —
55
+ * no separate `:hover` tint — because the pointer makes a row active,
56
+ * and two tinted rows would leave Enter's target ambiguous.
57
+ */
58
+ export const CommandPaletteOption = ({ id, isActive, item, itemSize, onChoose, onPoint, titleRanges, }) => {
59
+ const { description, hint, href, icon, target, title } = item;
60
+ const className = toClassName("flex w-full cursor-pointer items-center rounded-md text-start text-content-primary no-underline", PANEL_ITEM_SIZE_CLASS[itemSize], isActive && "bg-intent-neutral-surface-hover");
61
+ const content = (_jsxs(_Fragment, { children: [icon === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: toClassName("flex shrink-0 items-center justify-center overflow-hidden rounded-sm text-content-secondary [&>img]:object-contain", ICON_SIZE_CLASS[itemSize]), children: icon })), _jsxs("span", { className: "flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2", children: [_jsx("span", { className: "min-w-0 wrap-anywhere", children: _jsx(HighlightedTitle, { ranges: titleRanges, title: title }) }), description === undefined ? null : (_jsx("span", { className: "min-w-0 text-content-muted text-sm wrap-anywhere", children: description }))] }), hint === undefined ? null : (_jsx("span", { className: "shrink-0 text-content-muted text-sm", children: hint }))] }));
62
+ const keepFocusInInput = (mouseEvent) => {
63
+ mouseEvent.preventDefault();
64
+ };
65
+ if (href !== undefined) {
66
+ return (_jsx(UnstyledLink, { "aria-selected": isActive, className: className, href: href, id: id, onClick: onChoose, onMouseDown: keepFocusInInput, onPointerMove: onPoint, rel: target === "_blank"
67
+ ? "noopener noreferrer"
68
+ : undefined, role: "option", tabIndex: -1, target: target, children: content }));
69
+ }
70
+ return (_jsx("button", { "aria-selected": isActive, className: className, id: id, onClick: onChoose, onMouseDown: keepFocusInInput, onPointerMove: onPoint, role: "option", tabIndex: -1, type: "button", children: content }));
71
+ };
72
+ //# sourceMappingURL=CommandPaletteOption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPaletteOption.js","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteOption.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { ControlSize } from "@charcuterie/tokens";
2
+ import type { ReactNode, RefObject } from "react";
3
+ import { type CommandPaletteGroup, type CommandPaletteItem, type CommandPalettePrefix } from "./commandPaletteSections.ts";
4
+ export type CommandPalettePanelProps = {
5
+ /** The glyph inside the clear button. Without one, there is no clear button — Escape closes. */
6
+ clearIcon?: ReactNode;
7
+ /** The query the palette opens with, when the caller does not own `query`. Read once per open. */
8
+ defaultQuery?: string;
9
+ /** Shown when a query matches nothing and there is no fallback row. Default "No matches". */
10
+ emptyLabel?: string;
11
+ /**
12
+ * The strip under the list. Default: the keys, and each prefix
13
+ * with its target. `null` draws none.
14
+ */
15
+ footer?: ReactNode;
16
+ /** The title of a prefix command's row. Default `Search <label> for "<query>"`. */
17
+ formatPrefixTitle?: (label: string, query: string) => string;
18
+ /** The last row for any query that is not a prefix command — the "search the web" row. */
19
+ getFallbackItem?: (query: string) => CommandPaletteItem | null | undefined;
20
+ /** Ref to the search field, for the dialog's initial focus. */
21
+ inputRef?: RefObject<HTMLInputElement | null>;
22
+ /** How tall each row is — and the search field. Default `lg`, stepped down on a short window. */
23
+ itemSize?: ControlSize;
24
+ /** Everything a query can match. Folder members included, flattened. */
25
+ items: readonly CommandPaletteItem[];
26
+ /** The accessible name of the search field and the list. Default "Search". */
27
+ label?: string;
28
+ /** At most this many matches under one heading. Default: no cap. */
29
+ maxResultsPerGroup?: number;
30
+ /** After a row is chosen. `CommandPalette` closes here. */
31
+ onChoose?: () => void;
32
+ /** Every change to the query. With `query`, the caller owns it. */
33
+ onQueryChange?: (query: string) => void;
34
+ placeholder?: string;
35
+ /** The heading over a prefix command's row. Default "Search a site". */
36
+ prefixGroupLabel?: string;
37
+ prefixes?: readonly CommandPalettePrefix[];
38
+ /** Controlled query. Uncontrolled (from `defaultQuery`) when absent. */
39
+ query?: string;
40
+ /** What the palette lists before anything is typed — "Pinned", "Recently opened". */
41
+ suggestions?: readonly CommandPaletteGroup[];
42
+ /** The heading over matches whose item has no `group`. Default "Results". */
43
+ ungroupedLabel?: string;
44
+ };
45
+ /**
46
+ * The palette's contents — the search field, the grouped listbox and
47
+ * the footer — without the dialog around them. `CommandPalette` is
48
+ * this inside a `Modal`; the stories also lay it out inline, so every
49
+ * state can sit on one board.
50
+ */
51
+ export declare const CommandPalettePanel: ({ clearIcon, defaultQuery, emptyLabel, footer, formatPrefixTitle, getFallbackItem, inputRef: externalInputRef, itemSize: requestedItemSize, items, label, maxResultsPerGroup, onChoose, onQueryChange, placeholder, prefixGroupLabel, prefixes, query, suggestions, ungroupedLabel, }: CommandPalettePanelProps) => ReactNode;
52
+ //# sourceMappingURL=CommandPalettePanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPalettePanel.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPalettePanel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAEV,SAAS,EACT,SAAS,EACV,MAAM,OAAO,CAAA;AAQd,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EAE1B,MAAM,6BAA6B,CAAA;AAEpC,MAAM,MAAM,wBAAwB,GAAG;IACrC,gGAAgG;IAChG,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,kGAAkG;IAClG,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6FAA6F;IAC7F,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,CAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,KACV,MAAM,CAAA;IACX,0FAA0F;IAC1F,eAAe,CAAC,EAAE,CAChB,KAAK,EAAE,MAAM,KACV,kBAAkB,GAAG,IAAI,GAAG,SAAS,CAAA;IAC1C,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAC7C,iGAAiG;IACjG,QAAQ,CAAC,EAAE,WAAW,CAAA;IACtB,wEAAwE;IACxE,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACpC,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,mEAAmE;IACnE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAA;IAC1C,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qFAAqF;IACrF,WAAW,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAA;IAC5C,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AA2BD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,uRAoBjC,wBAAwB,KAAG,SAkS7B,CAAA"}
@@ -0,0 +1,168 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useUniqueId } from "@charcuterie/logic";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { usePanelItemSize } from "../Overlay/usePanelItemSize.js";
5
+ import { SearchInput } from "../SearchInput/SearchInput.js";
6
+ import { toClassName } from "../toClassName.js";
7
+ import { CommandPaletteFooter } from "./CommandPaletteFooter.js";
8
+ import { CommandPaletteOption } from "./CommandPaletteOption.js";
9
+ import { buildCommandPaletteSections, } from "./commandPaletteSections.js";
10
+ const defaultFormatPrefixTitle = (label, query) => `Search ${label} for “${query}”`;
11
+ const GROUP_HEADING_INSET_CLASS = {
12
+ lg: "px-(--control-padding-inline-lg)",
13
+ md: "px-(--control-padding-inline-md)",
14
+ sm: "px-(--control-padding-inline-sm)",
15
+ };
16
+ const SEARCH_SIZE = {
17
+ lg: "lg",
18
+ md: "md",
19
+ sm: "md",
20
+ };
21
+ /**
22
+ * The palette's contents — the search field, the grouped listbox and
23
+ * the footer — without the dialog around them. `CommandPalette` is
24
+ * this inside a `Modal`; the stories also lay it out inline, so every
25
+ * state can sit on one board.
26
+ */
27
+ export const CommandPalettePanel = ({ clearIcon, defaultQuery = "", emptyLabel = "No matches", footer, formatPrefixTitle = defaultFormatPrefixTitle, getFallbackItem, inputRef: externalInputRef, itemSize: requestedItemSize = "lg", items, label = "Search", maxResultsPerGroup, onChoose, onQueryChange, placeholder = "Search…", prefixGroupLabel = "Search a site", prefixes = [], query, suggestions = [], ungroupedLabel = "Results", }) => {
28
+ const itemSize = usePanelItemSize(requestedItemSize);
29
+ const listId = useUniqueId();
30
+ const ownInputRef = useRef(null);
31
+ const inputRef = externalInputRef ?? ownInputRef;
32
+ const [internalQuery, setInternalQuery] = useState(defaultQuery);
33
+ const currentQuery = query ?? internalQuery;
34
+ const [activeIndex, setActiveIndex] = useState(0);
35
+ const sections = useMemo(() => buildCommandPaletteSections({
36
+ formatPrefixTitle,
37
+ getFallbackItem,
38
+ items,
39
+ maxResultsPerGroup,
40
+ prefixGroupLabel,
41
+ prefixes,
42
+ query: currentQuery,
43
+ suggestions,
44
+ ungroupedLabel,
45
+ }), [
46
+ currentQuery,
47
+ formatPrefixTitle,
48
+ getFallbackItem,
49
+ items,
50
+ maxResultsPerGroup,
51
+ prefixGroupLabel,
52
+ prefixes,
53
+ suggestions,
54
+ ungroupedLabel,
55
+ ]);
56
+ const indexedSections = useMemo(() => {
57
+ let index = 0;
58
+ return sections.map((section) => ({
59
+ ...section,
60
+ headingId: `${listId}-${section.key}`,
61
+ rows: section.rows.map((row) => ({
62
+ ...row,
63
+ id: `${listId}-${section.key}-${row.item.key}`,
64
+ index: index++,
65
+ })),
66
+ }));
67
+ }, [listId, sections]);
68
+ const rows = indexedSections.flatMap((section) => section.rows);
69
+ const resolvedActiveIndex = rows.length === 0
70
+ ? -1
71
+ : Math.min(activeIndex, rows.length - 1);
72
+ const activeRow = rows[resolvedActiveIndex];
73
+ const setQuery = (nextQuery) => {
74
+ if (query === undefined) {
75
+ setInternalQuery(nextQuery);
76
+ }
77
+ setActiveIndex(0);
78
+ onQueryChange?.(nextQuery);
79
+ };
80
+ // The arrows can walk the active row out of the scrolled list; bring
81
+ // it back. `nearest` is a no-op for a row the pointer is already on.
82
+ useEffect(() => {
83
+ if (activeRow === undefined) {
84
+ return;
85
+ }
86
+ document
87
+ .getElementById(activeRow.id)
88
+ ?.scrollIntoView?.({ block: "nearest" });
89
+ }, [activeRow]);
90
+ /**
91
+ * Opening with a typed character puts the caret AFTER it, so the
92
+ * next key extends the query instead of landing in front of it.
93
+ * Once per mount: a later focus (back from the clear button) keeps
94
+ * wherever the person left the caret.
95
+ */
96
+ const hasPlacedCaret = useRef(false);
97
+ const choose = (row) => {
98
+ onChoose?.();
99
+ row.item.onSelect?.();
100
+ };
101
+ const activate = (row, isNewTab) => {
102
+ const { href } = row.item;
103
+ if (href === undefined) {
104
+ choose(row);
105
+ return;
106
+ }
107
+ if (isNewTab) {
108
+ window.open(href, "_blank", "noopener,noreferrer");
109
+ choose(row);
110
+ return;
111
+ }
112
+ // The row is the `<a href>` itself, so clicking it takes the path
113
+ // a mouse would — the injected router link, `target`, all of it —
114
+ // and its `onClick` runs `choose`.
115
+ document.getElementById(row.id)?.click();
116
+ };
117
+ const handleKeyDown = (keyboardEvent) => {
118
+ const lastIndex = rows.length - 1;
119
+ switch (keyboardEvent.key) {
120
+ case "ArrowDown": {
121
+ keyboardEvent.preventDefault();
122
+ setActiveIndex(Math.min(resolvedActiveIndex + 1, lastIndex));
123
+ return;
124
+ }
125
+ case "ArrowUp": {
126
+ keyboardEvent.preventDefault();
127
+ setActiveIndex(Math.max(resolvedActiveIndex - 1, 0));
128
+ return;
129
+ }
130
+ case "Home": {
131
+ keyboardEvent.preventDefault();
132
+ setActiveIndex(0);
133
+ return;
134
+ }
135
+ case "End": {
136
+ keyboardEvent.preventDefault();
137
+ setActiveIndex(Math.max(lastIndex, 0));
138
+ return;
139
+ }
140
+ case "Enter": {
141
+ if (activeRow === undefined) {
142
+ return;
143
+ }
144
+ keyboardEvent.preventDefault();
145
+ activate(activeRow, keyboardEvent.ctrlKey || keyboardEvent.metaKey);
146
+ return;
147
+ }
148
+ default:
149
+ }
150
+ };
151
+ const hasRows = rows.length > 0;
152
+ const isQueryEmpty = currentQuery.trim().length === 0;
153
+ const footerContent = footer === undefined ? (_jsx(CommandPaletteFooter, { prefixes: prefixes })) : (footer);
154
+ return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsx("div", { className: "shrink-0 border-border-subtle border-b p-2", children: _jsx(SearchInput, { "aria-activedescendant": activeRow?.id, "aria-autocomplete": "list", "aria-controls": hasRows ? listId : undefined, "aria-expanded": hasRows, "aria-label": label, autoCapitalize: "off", autoComplete: "off", autoCorrect: "off", clearIcon: clearIcon ?? null, enterKeyHint: "go", isClearVisible: clearIcon !== undefined &&
155
+ currentQuery.length > 0, onChange: (changeEvent) => setQuery(changeEvent.currentTarget.value), onClear: () => setQuery(""), onFocus: (focusEvent) => {
156
+ if (hasPlacedCaret.current) {
157
+ return;
158
+ }
159
+ hasPlacedCaret.current = true;
160
+ const { length } = focusEvent.currentTarget.value;
161
+ focusEvent.currentTarget.setSelectionRange(length, length);
162
+ }, onKeyDown: handleKeyDown, placeholder: placeholder, ref: inputRef, role: "combobox", size: SEARCH_SIZE[itemSize], spellCheck: false, value: currentQuery }) }), hasRows ? (_jsx("div", { "aria-label": label, className: "charcuterie-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain p-1.5", id: listId, role: "listbox", children: indexedSections.map((section) => (_jsxs("div", { "aria-labelledby": section.headingId, className: "not-first:mt-1", role: "group", children: [_jsx("div", { className: toClassName(GROUP_HEADING_INSET_CLASS[itemSize], "pt-1.5 pb-0.5 font-medium text-content-secondary text-xs"), id: section.headingId, children: section.label }), section.rows.map((row) => (_jsx(CommandPaletteOption, { id: row.id, isActive: row.index === resolvedActiveIndex, item: row.item, itemSize: itemSize, onChoose: () => choose(row), onPoint: () => {
163
+ if (row.index !== resolvedActiveIndex) {
164
+ setActiveIndex(row.index);
165
+ }
166
+ }, titleRanges: row.titleRanges }, row.id)))] }, section.key))) })) : isQueryEmpty ? null : (_jsx("p", { className: "m-0 px-4 py-6 text-center text-content-muted text-sm", children: emptyLabel })), footerContent === null ? null : (_jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-x-4 gap-y-1 border-border-subtle border-t px-4 py-2 text-content-muted text-xs", children: footerContent }))] }));
167
+ };
168
+ //# sourceMappingURL=CommandPalettePanel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPalettePanel.js","sourceRoot":"","sources":["../../src/CommandPalette/CommandPalettePanel.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import { type TextMatchRange } from "@charcuterie/logic";
2
+ import type { ReactNode } from "react";
3
+ /**
4
+ * One thing the palette can open or run.
5
+ *
6
+ * A folder's members are items like any other: the caller flattens
7
+ * them into `items`, and puts the folder's name in `description` so a
8
+ * hit inside a folder still says where it lives.
9
+ */
10
+ export type CommandPaletteItem = {
11
+ /** Secondary text after the title — a folder, a section, a host. Matched, never highlighted. */
12
+ description?: string;
13
+ /** Shown under this heading when the palette lists matches. Items without one share `ungroupedLabel`. */
14
+ group?: string;
15
+ /** Opens as a link: a real `<a href>`, routed through `RouterLinkProvider`. */
16
+ href?: string;
17
+ /** The app's own glyph or image. Decorative; the title is the name. */
18
+ icon?: ReactNode;
19
+ /** Matched as extra words and never shown: an old name, an abbreviation, a product the app hosts. */
20
+ keywords?: readonly string[];
21
+ /** Unique within the list it is in. */
22
+ key: string;
23
+ /** Trailing text at the end of the row: a kind ("Bookmark") or a status. */
24
+ hint?: ReactNode;
25
+ /** Runs on choice. With an `href` too, it runs as well as the navigation — a place to record a visit. */
26
+ onSelect?: () => void;
27
+ /** Passed to the link. `_blank` opens a new tab on a plain Enter or click. */
28
+ target?: string;
29
+ /** What is typed for and highlighted. */
30
+ title: string;
31
+ /** Breaks a tie inside a match tier, higher first: an open count, say. */
32
+ weight?: number;
33
+ };
34
+ /** A heading and its items, shown before anything is typed. */
35
+ export type CommandPaletteGroup = {
36
+ items: readonly CommandPaletteItem[];
37
+ key: string;
38
+ label: string;
39
+ };
40
+ /**
41
+ * A short prefix that sends the rest of the query to one target:
42
+ * `{ prefix: "au", label: "Auction Site", getHref: (query) => … }`
43
+ * turns `au lego` into one row that searches that site.
44
+ */
45
+ export type CommandPalettePrefix = {
46
+ /** The link the row opens, given what follows the prefix. */
47
+ getHref?: (query: string) => string;
48
+ icon?: ReactNode;
49
+ /** The target's name, in the row's title and in the footer. */
50
+ label: string;
51
+ /** Runs instead of, or as well as, the link. */
52
+ onSelect?: (query: string) => void;
53
+ /** What is typed before the space: `au`. Matched without case. */
54
+ prefix: string;
55
+ };
56
+ export type CommandPaletteRow = {
57
+ item: CommandPaletteItem;
58
+ /** Where the query matched the title; empty for a suggestion. */
59
+ titleRanges: readonly TextMatchRange[];
60
+ };
61
+ export type CommandPaletteSection = {
62
+ key: string;
63
+ label: string;
64
+ rows: readonly CommandPaletteRow[];
65
+ };
66
+ export type CommandPaletteSectionsOptions = {
67
+ formatPrefixTitle: (label: string, query: string) => string;
68
+ getFallbackItem?: (query: string) => CommandPaletteItem | null | undefined;
69
+ items: readonly CommandPaletteItem[];
70
+ maxResultsPerGroup?: number;
71
+ prefixGroupLabel: string;
72
+ prefixes: readonly CommandPalettePrefix[];
73
+ query: string;
74
+ suggestions: readonly CommandPaletteGroup[];
75
+ ungroupedLabel: string;
76
+ };
77
+ /**
78
+ * What the palette lists for a query, as headed sections in order.
79
+ *
80
+ * Three cases, decided in this order:
81
+ *
82
+ * 1. **A prefix command** (`au lego`) lists one row, for that target.
83
+ * 2. **Nothing typed** lists the caller's `suggestions` — the shape
84
+ * the palette exists for: something to choose the moment it opens.
85
+ * 3. **Anything else** lists the word-prefix matches from `items`,
86
+ * under each item's `group`, and then the caller's fallback row.
87
+ *
88
+ * Matched groups are ordered by their best member, so the first row
89
+ * — the one Enter opens — is always the best match, whichever group
90
+ * it belongs to.
91
+ */
92
+ export declare const buildCommandPaletteSections: ({ formatPrefixTitle, getFallbackItem, items, maxResultsPerGroup, prefixGroupLabel, prefixes, query, suggestions, ungroupedLabel, }: CommandPaletteSectionsOptions) => CommandPaletteSection[];
93
+ //# sourceMappingURL=commandPaletteSections.d.ts.map