@charcuterie/ui 4.25.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 (48) 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/index.d.ts +7 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -0
  33. package/dist/index.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/CommandPalette/CommandPalette.mdx +129 -0
  36. package/src/CommandPalette/CommandPalette.stories.tsx +413 -0
  37. package/src/CommandPalette/CommandPalette.test.tsx +553 -0
  38. package/src/CommandPalette/CommandPalette.tsx +91 -0
  39. package/src/CommandPalette/CommandPaletteFooter.tsx +45 -0
  40. package/src/CommandPalette/CommandPaletteOption.tsx +200 -0
  41. package/src/CommandPalette/CommandPalettePanel.tsx +411 -0
  42. package/src/CommandPalette/commandPalette.storyHelpers.tsx +248 -0
  43. package/src/CommandPalette/commandPaletteSections.test.ts +156 -0
  44. package/src/CommandPalette/commandPaletteSections.ts +219 -0
  45. package/src/CommandPalette/useCommandPalette.ts +75 -0
  46. package/src/CommandPalette/useCommandPaletteHotkeys.ts +158 -0
  47. package/src/index.ts +14 -0
  48. package/src/sourceRules.test.ts +2 -1
@@ -0,0 +1,158 @@
1
+ import { useLatestRef } from "@charcuterie/logic"
2
+ import { useEffect } from "react"
3
+
4
+ export type CommandPaletteHotkeysOptions = {
5
+ /** Off while the palette is open, or on a page that should not take the keys. Default `true`. */
6
+ isEnabled?: boolean
7
+ /**
8
+ * A printable key typed outside an editable field opens the palette
9
+ * with that character as the query. Default `true`.
10
+ */
11
+ isOpenedByTyping?: boolean
12
+ /** Receives the text the palette should open with: `""`, or the character typed. */
13
+ onOpen: (initialQuery: string) => void
14
+ }
15
+
16
+ /**
17
+ * Elements that keep their keys: anything you type into, and the
18
+ * widgets with their own type-to-find (a focused `Listbox` or `Menu`
19
+ * jumps to the option you type, and stealing that key breaks it).
20
+ */
21
+ const TYPEAHEAD_OWNER_SELECTOR = [
22
+ "input",
23
+ "textarea",
24
+ "select",
25
+ "[contenteditable]:not([contenteditable='false'])",
26
+ "[role='combobox']",
27
+ "[role='grid']",
28
+ "[role='listbox']",
29
+ "[role='menu']",
30
+ "[role='menubar']",
31
+ "[role='searchbox']",
32
+ "[role='textbox']",
33
+ "[role='tree']",
34
+ "[role='treegrid']",
35
+ ].join(",")
36
+
37
+ /** Input types that take no text, so a letter typed on one is free to open the palette. */
38
+ const NON_TEXT_INPUT_TYPES = new Set([
39
+ "button",
40
+ "checkbox",
41
+ "color",
42
+ "file",
43
+ "image",
44
+ "radio",
45
+ "range",
46
+ "reset",
47
+ "submit",
48
+ ])
49
+
50
+ export const getIsTypingTarget = (
51
+ target: EventTarget | null,
52
+ ): boolean => {
53
+ if (!(target instanceof Element)) {
54
+ return false
55
+ }
56
+
57
+ if (
58
+ target instanceof HTMLInputElement &&
59
+ NON_TEXT_INPUT_TYPES.has(target.type)
60
+ ) {
61
+ return false
62
+ }
63
+
64
+ return (
65
+ (target instanceof HTMLElement &&
66
+ target.isContentEditable) ||
67
+ target.closest(TYPEAHEAD_OWNER_SELECTOR) !== null
68
+ )
69
+ }
70
+
71
+ /**
72
+ * The global keys that open a command palette, as an opt-in hook.
73
+ *
74
+ * - **`Ctrl K`, `Ctrl Space`** (or `⌘` on a Mac) open it from
75
+ * anywhere, a text field included — the chord types nothing.
76
+ * - **`/`** opens it when focus is not somewhere that types, and
77
+ * the slash itself is not typed.
78
+ * - **Any printable key** does the same and becomes the query, so
79
+ * the page is a search box without a click (`isOpenedByTyping`).
80
+ *
81
+ * It is a hook, not something `CommandPalette` does on mount,
82
+ * because a document-wide key listener is a page-level decision: an
83
+ * app with two palettes, or a page where letters already mean
84
+ * something (a board with its own shortcuts), must be able to say
85
+ * no. See
86
+ * `docs/decisions/2026-10-16-command-palette-hotkeys-are-an-opt-in-hook.md`.
87
+ *
88
+ * A key another handler already claimed (`defaultPrevented`), a key
89
+ * pressed during IME composition, and a key with `Alt` held are all
90
+ * left alone.
91
+ */
92
+ export const useCommandPaletteHotkeys = ({
93
+ isEnabled = true,
94
+ isOpenedByTyping = true,
95
+ onOpen,
96
+ }: CommandPaletteHotkeysOptions): void => {
97
+ const onOpenRef = useLatestRef(onOpen)
98
+
99
+ useEffect(() => {
100
+ if (!isEnabled) {
101
+ return
102
+ }
103
+
104
+ const handleKeyDown = (event: KeyboardEvent) => {
105
+ if (
106
+ event.defaultPrevented ||
107
+ event.isComposing ||
108
+ event.altKey
109
+ ) {
110
+ return
111
+ }
112
+
113
+ const isChord = event.ctrlKey || event.metaKey
114
+
115
+ if (isChord) {
116
+ if (
117
+ !event.shiftKey &&
118
+ (event.key.toLowerCase() === "k" ||
119
+ event.key === " ")
120
+ ) {
121
+ event.preventDefault()
122
+ onOpenRef.current("")
123
+ }
124
+
125
+ return
126
+ }
127
+
128
+ if (getIsTypingTarget(event.target)) {
129
+ return
130
+ }
131
+
132
+ if (event.key === "/") {
133
+ event.preventDefault()
134
+ onOpenRef.current("")
135
+
136
+ return
137
+ }
138
+
139
+ // `key` is the character for a printable key and a name
140
+ // ("Enter", "ArrowDown") for everything else; a space is
141
+ // printable but presses the focused button.
142
+ if (
143
+ isOpenedByTyping &&
144
+ [...event.key].length === 1 &&
145
+ /\S/u.test(event.key)
146
+ ) {
147
+ event.preventDefault()
148
+ onOpenRef.current(event.key)
149
+ }
150
+ }
151
+
152
+ document.addEventListener("keydown", handleKeyDown)
153
+
154
+ return () => {
155
+ document.removeEventListener("keydown", handleKeyDown)
156
+ }
157
+ }, [isEnabled, isOpenedByTyping, onOpenRef])
158
+ }
package/src/index.ts CHANGED
@@ -130,6 +130,20 @@ export type {
130
130
  export { ColorSchemeToggle } from "./ColorSchemeToggle/ColorSchemeToggle.tsx"
131
131
  export type { ComboboxProps } from "./Combobox/Combobox.tsx"
132
132
  export { Combobox } from "./Combobox/Combobox.tsx"
133
+ export type { CommandPaletteProps } from "./CommandPalette/CommandPalette.tsx"
134
+ export { CommandPalette } from "./CommandPalette/CommandPalette.tsx"
135
+ export type {
136
+ CommandPaletteGroup,
137
+ CommandPaletteItem,
138
+ CommandPalettePrefix,
139
+ } from "./CommandPalette/commandPaletteSections.ts"
140
+ export type {
141
+ UseCommandPaletteOptions,
142
+ UseCommandPaletteResult,
143
+ } from "./CommandPalette/useCommandPalette.ts"
144
+ export { useCommandPalette } from "./CommandPalette/useCommandPalette.ts"
145
+ export type { CommandPaletteHotkeysOptions } from "./CommandPalette/useCommandPaletteHotkeys.ts"
146
+ export { useCommandPaletteHotkeys } from "./CommandPalette/useCommandPaletteHotkeys.ts"
133
147
  export type { ComparisonChartProps } from "./ComparisonChart/ComparisonChart.tsx"
134
148
  export { ComparisonChart } from "./ComparisonChart/ComparisonChart.tsx"
135
149
  export type {
@@ -626,7 +626,8 @@ test("the barrel is the only place components are re-exported", async () => {
626
626
  // GroupedList owns rows under group headings; castkit, tally-marks
627
627
  // and whereabouts each drew it by hand. `GroupedListRow.tsx` is its
628
628
  // member and stays out of this count by the `<Name>/<Name>.tsx` rule.
629
- expect(componentNames.length).toBe(78)
629
+ // CommandPalette composes Modal and SearchInput into the launcher.
630
+ expect(componentNames.length).toBe(79)
630
631
 
631
632
  for (const name of componentNames) {
632
633
  expect(barrel).toContain(`export { ${name} }`)