@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.
- package/README.md +13 -0
- package/dist/CommandPalette/CommandPalette.d.ts +43 -0
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPalette.js +53 -0
- package/dist/CommandPalette/CommandPalette.js.map +1 -0
- package/dist/CommandPalette/CommandPaletteFooter.d.ts +16 -0
- package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPaletteFooter.js +14 -0
- package/dist/CommandPalette/CommandPaletteFooter.js.map +1 -0
- package/dist/CommandPalette/CommandPaletteOption.d.ts +44 -0
- package/dist/CommandPalette/CommandPaletteOption.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPaletteOption.js +72 -0
- package/dist/CommandPalette/CommandPaletteOption.js.map +1 -0
- package/dist/CommandPalette/CommandPalettePanel.d.ts +52 -0
- package/dist/CommandPalette/CommandPalettePanel.d.ts.map +1 -0
- package/dist/CommandPalette/CommandPalettePanel.js +168 -0
- package/dist/CommandPalette/CommandPalettePanel.js.map +1 -0
- package/dist/CommandPalette/commandPaletteSections.d.ts +93 -0
- package/dist/CommandPalette/commandPaletteSections.d.ts.map +1 -0
- package/dist/CommandPalette/commandPaletteSections.js +94 -0
- package/dist/CommandPalette/commandPaletteSections.js.map +1 -0
- package/dist/CommandPalette/useCommandPalette.d.ts +32 -0
- package/dist/CommandPalette/useCommandPalette.d.ts.map +1 -0
- package/dist/CommandPalette/useCommandPalette.js +40 -0
- package/dist/CommandPalette/useCommandPalette.js.map +1 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.d.ts +35 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.d.ts.map +1 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.js +114 -0
- package/dist/CommandPalette/useCommandPaletteHotkeys.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/CommandPalette/CommandPalette.mdx +129 -0
- package/src/CommandPalette/CommandPalette.stories.tsx +413 -0
- package/src/CommandPalette/CommandPalette.test.tsx +553 -0
- package/src/CommandPalette/CommandPalette.tsx +91 -0
- package/src/CommandPalette/CommandPaletteFooter.tsx +45 -0
- package/src/CommandPalette/CommandPaletteOption.tsx +200 -0
- package/src/CommandPalette/CommandPalettePanel.tsx +411 -0
- package/src/CommandPalette/commandPalette.storyHelpers.tsx +248 -0
- package/src/CommandPalette/commandPaletteSections.test.ts +156 -0
- package/src/CommandPalette/commandPaletteSections.ts +219 -0
- package/src/CommandPalette/useCommandPalette.ts +75 -0
- package/src/CommandPalette/useCommandPaletteHotkeys.ts +158 -0
- package/src/index.ts +14 -0
- 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 {
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -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
|
-
|
|
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} }`)
|