@stridge/noctis 1.0.0-beta.5 → 1.0.0-beta.6
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/dist/components/breadcrumb/breadcrumb.d.ts +163 -0
- package/dist/components/breadcrumb/breadcrumb.js +152 -0
- package/dist/components/breadcrumb/breadcrumb.props.d.ts +59 -0
- package/dist/components/breadcrumb/breadcrumb.props.js +68 -0
- package/dist/components/breadcrumb/breadcrumb.slots.d.ts +16 -0
- package/dist/components/breadcrumb/breadcrumb.slots.js +32 -0
- package/dist/components/breadcrumb/breadcrumb.types.d.ts +9 -0
- package/dist/components/breadcrumb/index.d.ts +3 -0
- package/dist/components/command/command-listbox.js +174 -0
- package/dist/components/command/command-rank.d.ts +40 -0
- package/dist/components/command/command-rank.js +61 -0
- package/dist/components/command/command-score.d.ts +25 -0
- package/dist/components/command/command-score.js +85 -0
- package/dist/components/command/command.context.d.ts +17 -0
- package/dist/components/command/command.context.js +13 -0
- package/dist/components/command/command.d.ts +396 -0
- package/dist/components/command/command.js +471 -0
- package/dist/components/command/command.props.d.ts +91 -0
- package/dist/components/command/command.props.js +94 -0
- package/dist/components/command/command.slots.d.ts +23 -0
- package/dist/components/command/command.slots.js +60 -0
- package/dist/components/command/index.d.ts +6 -0
- package/dist/components/command/use-command-ranking.d.ts +37 -0
- package/dist/components/command/use-command-ranking.js +127 -0
- package/dist/components/search-dialog/parts/root.js +1 -1
- package/dist/components/skeleton/index.d.ts +3 -0
- package/dist/components/skeleton/skeleton.context.js +12 -0
- package/dist/components/skeleton/skeleton.d.ts +157 -0
- package/dist/components/skeleton/skeleton.js +130 -0
- package/dist/components/skeleton/skeleton.props.d.ts +47 -0
- package/dist/components/skeleton/skeleton.props.js +57 -0
- package/dist/components/skeleton/skeleton.slots.d.ts +15 -0
- package/dist/components/skeleton/skeleton.slots.js +28 -0
- package/dist/components/skeleton/skeleton.types.d.ts +13 -0
- package/dist/components/surface/surface.d.ts +1 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.js +13 -4
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.js +2 -2
- package/dist/props.d.ts +37 -34
- package/dist/props.js +37 -34
- package/dist/styles.css +715 -0
- package/package.json +4 -4
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useNoctisStringFormatter } from "../../core/use-injected-labels.js";
|
|
3
|
+
import { VisuallyHidden } from "../../core/visually-hidden/visually-hidden.js";
|
|
4
|
+
import { useRender } from "../../core/render.js";
|
|
5
|
+
import { Surface } from "../surface/surface.js";
|
|
6
|
+
import { Dialog } from "../dialog/dialog.js";
|
|
7
|
+
import { CommandGroupProvider, CommandListboxProvider, useCommandGroup, useCommandListbox, useCommandListboxState, useCommandOption, useRegisterGroupLabel } from "./command-listbox.js";
|
|
8
|
+
import { CommandProvider, useCommandContext } from "./command.context.js";
|
|
9
|
+
import { COMMAND_SLOTS } from "./command.slots.js";
|
|
10
|
+
import { breadcrumbProps, emptyProps, footerProps, groupLabelProps, groupProps, headerProps, inputActionProps, inputProps, itemIconProps, itemLabelProps, itemProps, listProps, loadingProps, panelProps, separatorProps } from "./command.props.js";
|
|
11
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
//#region src/components/command/command.tsx
|
|
14
|
+
/** Derive the controlled-or-uncontrolled query and the drill-in handlers shared by Root and Dialog. */
|
|
15
|
+
function usePaletteState({ value, defaultValue, onValueChange, pages = [], onPagesChange, loading }) {
|
|
16
|
+
const [uncontrolled, setUncontrolled] = useState(defaultValue ?? "");
|
|
17
|
+
const query = value ?? uncontrolled;
|
|
18
|
+
const setQuery = useCallback((next) => {
|
|
19
|
+
if (value === void 0) setUncontrolled(next);
|
|
20
|
+
onValueChange?.(next);
|
|
21
|
+
}, [value, onValueChange]);
|
|
22
|
+
const depth = pages.length;
|
|
23
|
+
const queryStack = useRef([]);
|
|
24
|
+
const prevDepth = useRef(depth);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (depth > prevDepth.current) setQuery("");
|
|
27
|
+
else if (depth < prevDepth.current) setQuery(queryStack.current[depth] ?? "");
|
|
28
|
+
prevDepth.current = depth;
|
|
29
|
+
}, [depth, setQuery]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
queryStack.current[depth] = query;
|
|
32
|
+
}, [depth, query]);
|
|
33
|
+
const navigateTo = useCallback((index) => {
|
|
34
|
+
onPagesChange?.(index < 0 ? [] : pages.slice(0, index + 1));
|
|
35
|
+
}, [pages, onPagesChange]);
|
|
36
|
+
return {
|
|
37
|
+
query,
|
|
38
|
+
setQuery,
|
|
39
|
+
context: useMemo(() => ({
|
|
40
|
+
pages,
|
|
41
|
+
loading,
|
|
42
|
+
navigateTo
|
|
43
|
+
}), [
|
|
44
|
+
pages,
|
|
45
|
+
loading,
|
|
46
|
+
navigateTo
|
|
47
|
+
])
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The drill-in view key — changes on push/pop (not on typing), so the highlight resets to the top of a
|
|
51
|
+
* new view but persists across keystrokes within one. */
|
|
52
|
+
function viewKeyFor(pages) {
|
|
53
|
+
return pages.length === 0 ? "root" : `${pages.length}:${pages[pages.length - 1].id}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The interaction chassis both modes wrap. Noctis owns the combobox/listbox keyboard model directly (see
|
|
57
|
+
* `command-listbox`) — auto-highlighting the top result, the smart key-repeat loop, Enter activation —
|
|
58
|
+
* so the cmdk details behave exactly as a command bar should, with the input keeping focus and
|
|
59
|
+
* `aria-activedescendant` tracking the highlighted row.
|
|
60
|
+
*/
|
|
61
|
+
function PaletteBody({ query, setQuery, context, loop = false, children }) {
|
|
62
|
+
return /* @__PURE__ */ jsx(CommandProvider, {
|
|
63
|
+
value: context,
|
|
64
|
+
children: /* @__PURE__ */ jsx(CommandListboxProvider, {
|
|
65
|
+
value: useCommandListboxState({
|
|
66
|
+
query,
|
|
67
|
+
setQuery,
|
|
68
|
+
loop,
|
|
69
|
+
viewKey: viewKeyFor(context.pages)
|
|
70
|
+
}),
|
|
71
|
+
children
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The inline palette: the command interface embedded in normal flow (a page, a sidebar, a settings
|
|
77
|
+
* panel) rather than a modal. Renders the panel through `Surface` (the `menu` elevation scope, border,
|
|
78
|
+
* and card shadow) and shares the query + drill-in state with every part. For the modal launcher use
|
|
79
|
+
* `Command.Dialog`; both share identical inner parts.
|
|
80
|
+
*
|
|
81
|
+
* @see {@link CommandRootProps}
|
|
82
|
+
*/
|
|
83
|
+
function CommandRoot({ value, defaultValue, onValueChange, pages, onPagesChange, loading, loop, children, ...props }) {
|
|
84
|
+
return /* @__PURE__ */ jsx(PaletteBody, {
|
|
85
|
+
...usePaletteState({
|
|
86
|
+
value,
|
|
87
|
+
defaultValue,
|
|
88
|
+
onValueChange,
|
|
89
|
+
pages,
|
|
90
|
+
onPagesChange,
|
|
91
|
+
loading
|
|
92
|
+
}),
|
|
93
|
+
loop,
|
|
94
|
+
children: /* @__PURE__ */ jsx(Surface, {
|
|
95
|
+
elevation: "menu",
|
|
96
|
+
bordered: true,
|
|
97
|
+
shadow: "card",
|
|
98
|
+
"data-slot": COMMAND_SLOTS.panel,
|
|
99
|
+
...props,
|
|
100
|
+
children
|
|
101
|
+
})
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The modal palette: the centred, focus-trapped command launcher built on the Noctis `Dialog` chassis
|
|
106
|
+
* (portal, blurred backdrop, scroll-lock, Esc/outside-click dismissal) with a top-anchored, fixed-height
|
|
107
|
+
* panel that scale-fades in. Wire `open`/`onOpenChange` to a `Command.Trigger` (or any control). Shares
|
|
108
|
+
* identical inner parts with the inline `Command.Root`.
|
|
109
|
+
*
|
|
110
|
+
* @see {@link CommandDialogProps}
|
|
111
|
+
*/
|
|
112
|
+
function CommandDialog({ open, onOpenChange, label, value, defaultValue, onValueChange, pages, onPagesChange, loading, loop, children }) {
|
|
113
|
+
const state = usePaletteState({
|
|
114
|
+
value,
|
|
115
|
+
defaultValue,
|
|
116
|
+
onValueChange,
|
|
117
|
+
pages,
|
|
118
|
+
onPagesChange,
|
|
119
|
+
loading
|
|
120
|
+
});
|
|
121
|
+
const formatter = useNoctisStringFormatter();
|
|
122
|
+
const dialogLabel = label ?? formatter.format("command.label");
|
|
123
|
+
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
124
|
+
open,
|
|
125
|
+
onOpenChange,
|
|
126
|
+
children: /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { "data-slot": COMMAND_SLOTS.backdrop }), /* @__PURE__ */ jsxs(Dialog.Popup, {
|
|
127
|
+
elevation: "menu",
|
|
128
|
+
"data-slot": COMMAND_SLOTS.panel,
|
|
129
|
+
"data-modal": "",
|
|
130
|
+
children: [/* @__PURE__ */ jsx(Dialog.Title, {
|
|
131
|
+
"data-slot": "noctis-visually-hidden",
|
|
132
|
+
render: /* @__PURE__ */ jsx(VisuallyHidden, {}),
|
|
133
|
+
children: dialogLabel
|
|
134
|
+
}), /* @__PURE__ */ jsx(PaletteBody, {
|
|
135
|
+
...state,
|
|
136
|
+
loop,
|
|
137
|
+
children
|
|
138
|
+
})]
|
|
139
|
+
})] })
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* A headless launcher for the modal palette. It wires only behaviour and accessibility — `onClick` to
|
|
144
|
+
* open, `aria-haspopup="dialog"`, and `aria-expanded` — onto whatever element you render, so you bring
|
|
145
|
+
* your own `Button`/`Kbd` visuals. Wire the same `open`/`onOpenChange` state you pass to `Command.Dialog`.
|
|
146
|
+
*
|
|
147
|
+
* @see {@link CommandTriggerProps}
|
|
148
|
+
*/
|
|
149
|
+
function CommandTrigger({ open, onOpenChange, render, ref, onClick, ...props }) {
|
|
150
|
+
return useRender({
|
|
151
|
+
defaultTagName: "button",
|
|
152
|
+
render,
|
|
153
|
+
ref,
|
|
154
|
+
props: {
|
|
155
|
+
type: "button",
|
|
156
|
+
"aria-haspopup": "dialog",
|
|
157
|
+
"aria-expanded": open ?? void 0,
|
|
158
|
+
onClick: (event) => {
|
|
159
|
+
onClick?.(event);
|
|
160
|
+
if (!event.defaultPrevented) onOpenChange?.(true);
|
|
161
|
+
},
|
|
162
|
+
...props
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** The input row: holds an optional `Command.Breadcrumb`, a leading glyph, the `Command.Input`, and an
|
|
167
|
+
* optional `Command.InputAction`, over a hairline divider. */
|
|
168
|
+
function CommandHeader({ children, ...props }) {
|
|
169
|
+
return /* @__PURE__ */ jsx("div", {
|
|
170
|
+
...headerProps(),
|
|
171
|
+
...props,
|
|
172
|
+
children
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The drill-in trail. Renders one button per page in the stack and nothing at the top level; clicking a
|
|
177
|
+
* segment truncates the stack back to its depth. Reads the stack from context — just drop it in the
|
|
178
|
+
* header.
|
|
179
|
+
*/
|
|
180
|
+
function CommandBreadcrumb(props) {
|
|
181
|
+
const { pages, navigateTo } = useCommandContext("Breadcrumb");
|
|
182
|
+
if (pages.length === 0) return null;
|
|
183
|
+
return /* @__PURE__ */ jsx("div", {
|
|
184
|
+
...breadcrumbProps(),
|
|
185
|
+
...props,
|
|
186
|
+
children: pages.map((page, index) => /* @__PURE__ */ jsx("button", {
|
|
187
|
+
type: "button",
|
|
188
|
+
onClick: () => navigateTo(index),
|
|
189
|
+
children: page.label
|
|
190
|
+
}, page.id))
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The query field — a `role="combobox"` that keeps focus while a virtual cursor (`aria-activedescendant`)
|
|
195
|
+
* moves the active row. Typing filters (you rank externally); arrows / Home / End / PageUp-Down move the
|
|
196
|
+
* highlight, Enter activates it, and Backspace on an empty query pops the drill-in stack. The keyboard
|
|
197
|
+
* model is the palette's own (see `command-listbox`), not a delegated primitive's.
|
|
198
|
+
*
|
|
199
|
+
* @see {@link CommandInput.Props}
|
|
200
|
+
*/
|
|
201
|
+
function CommandInput({ onKeyDown, ...props }) {
|
|
202
|
+
const { pages, navigateTo } = useCommandContext("Input");
|
|
203
|
+
const { query, setQuery, listId, activeId, onInputKeyDown } = useCommandListbox("Input");
|
|
204
|
+
return /* @__PURE__ */ jsx("input", {
|
|
205
|
+
"data-slot": COMMAND_SLOTS.input,
|
|
206
|
+
type: "text",
|
|
207
|
+
role: "combobox",
|
|
208
|
+
value: query,
|
|
209
|
+
"aria-expanded": true,
|
|
210
|
+
"aria-controls": listId,
|
|
211
|
+
"aria-activedescendant": activeId,
|
|
212
|
+
"aria-autocomplete": "list",
|
|
213
|
+
autoComplete: "off",
|
|
214
|
+
autoCorrect: "off",
|
|
215
|
+
spellCheck: false,
|
|
216
|
+
...props,
|
|
217
|
+
onChange: (event) => setQuery(event.target.value),
|
|
218
|
+
onKeyDown: (event) => {
|
|
219
|
+
onKeyDown?.(event);
|
|
220
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
221
|
+
if (event.key === "Backspace" && event.currentTarget.value === "" && pages.length > 0) {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
navigateTo(pages.length - 2);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
onInputKeyDown(event);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** A trailing affordance slot in the input row (a mode pill, an "ask" hand-off, a hint). */
|
|
231
|
+
function CommandInputAction({ children, ...props }) {
|
|
232
|
+
return /* @__PURE__ */ jsx("div", {
|
|
233
|
+
...inputActionProps(),
|
|
234
|
+
...props,
|
|
235
|
+
children
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** The CSS var the list animates its `block-size` off; the hook writes the measured content height here. */
|
|
239
|
+
const LIST_HEIGHT_VAR = "--_command-list-height";
|
|
240
|
+
/**
|
|
241
|
+
* Drive the palette's height animation. The rows live in a measured inner sizer; a `ResizeObserver`
|
|
242
|
+
* watches it and writes its current height to the list's `--_command-list-height` var, so the list (and
|
|
243
|
+
* the auto-height panel riding on it) animates smoothly between sizes as the rows change instead of
|
|
244
|
+
* snapping. The list's own `transition` does the tweening; this only feeds it concrete heights.
|
|
245
|
+
*/
|
|
246
|
+
function useListAutoHeight(sizerRef) {
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
const sizer = sizerRef.current;
|
|
249
|
+
/* v8 ignore next -- the sizer ref is always attached by the time the effect runs */
|
|
250
|
+
if (!sizer) return void 0;
|
|
251
|
+
const list = sizer.parentElement;
|
|
252
|
+
/* v8 ignore next -- the sizer always renders inside the list element */
|
|
253
|
+
if (!list) return void 0;
|
|
254
|
+
const observer = new ResizeObserver(() => {
|
|
255
|
+
list.style.setProperty(LIST_HEIGHT_VAR, `${sizer.offsetHeight}px`);
|
|
256
|
+
});
|
|
257
|
+
observer.observe(sizer);
|
|
258
|
+
return () => observer.disconnect();
|
|
259
|
+
}, [sizerRef]);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The scrolling `role="listbox"` of command rows and sections. The rows render inside a measured sizer so
|
|
263
|
+
* the palette can animate its height as the result set changes (see {@link useListAutoHeight}). Within it,
|
|
264
|
+
* a per-page view wrapper is keyed by the drill-in depth: pushing or popping a page remounts it, so its
|
|
265
|
+
* CSS enter animation replays — the old view gives way and the new one opens — while a query change (same
|
|
266
|
+
* page) leaves it in place so typing doesn't re-animate.
|
|
267
|
+
*/
|
|
268
|
+
function CommandList({ children, ...props }) {
|
|
269
|
+
const sizerRef = useRef(null);
|
|
270
|
+
const { pages } = useCommandContext("List");
|
|
271
|
+
const { listId, listRef } = useCommandListbox("List");
|
|
272
|
+
useListAutoHeight(sizerRef);
|
|
273
|
+
return /* @__PURE__ */ jsx("div", {
|
|
274
|
+
"data-slot": COMMAND_SLOTS.list,
|
|
275
|
+
role: "listbox",
|
|
276
|
+
id: listId,
|
|
277
|
+
ref: listRef,
|
|
278
|
+
...props,
|
|
279
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
280
|
+
ref: sizerRef,
|
|
281
|
+
"data-slot": COMMAND_SLOTS.listSizer,
|
|
282
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
283
|
+
"data-slot": COMMAND_SLOTS.listView,
|
|
284
|
+
children
|
|
285
|
+
}, viewKeyFor(pages))
|
|
286
|
+
})
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** A labelled section: a `role="group"` wrapping a `Command.GroupLabel` and its rows, `aria-labelledby`
|
|
290
|
+
* the label while one is rendered. */
|
|
291
|
+
function CommandGroup({ children, ...props }) {
|
|
292
|
+
const { labelId, setLabelId } = useCommandGroup();
|
|
293
|
+
return /* @__PURE__ */ jsx("div", {
|
|
294
|
+
"data-slot": COMMAND_SLOTS.group,
|
|
295
|
+
role: "group",
|
|
296
|
+
"aria-labelledby": labelId,
|
|
297
|
+
...props,
|
|
298
|
+
children: /* @__PURE__ */ jsx(CommandGroupProvider, {
|
|
299
|
+
value: setLabelId,
|
|
300
|
+
children
|
|
301
|
+
})
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/** A section heading (not an option). Lends its id to the enclosing `Command.Group`'s `aria-labelledby`. */
|
|
305
|
+
function CommandGroupLabel({ children, ...props }) {
|
|
306
|
+
const id = useId();
|
|
307
|
+
useRegisterGroupLabel(id);
|
|
308
|
+
return /* @__PURE__ */ jsx("div", {
|
|
309
|
+
"data-slot": COMMAND_SLOTS.groupLabel,
|
|
310
|
+
id,
|
|
311
|
+
...props,
|
|
312
|
+
children
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* One command row (`role="option"`). Activated by click or by pressing Enter while highlighted
|
|
317
|
+
* (`onSelect`). Compose a `Command.ItemIcon` and a `Command.ItemLabel` inside; any further content (a
|
|
318
|
+
* `Kbd` shortcut, a badge, a count) is pushed to the row's end by the label's `flex` — the row stays
|
|
319
|
+
* yours to fill past the icon. Hovering highlights it; the keyboard highlight (`data-highlighted`, the
|
|
320
|
+
* input's `aria-activedescendant`) is the palette's own; activation runs the action, never fills the query.
|
|
321
|
+
*
|
|
322
|
+
* @see {@link CommandItem.Props}
|
|
323
|
+
*/
|
|
324
|
+
function CommandItem({ value, disabled, onSelect, onClick, onPointerMove, children, ...props }) {
|
|
325
|
+
const { id, active, setActive } = useCommandOption();
|
|
326
|
+
return /* @__PURE__ */ jsx("div", {
|
|
327
|
+
...itemProps({
|
|
328
|
+
highlighted: active,
|
|
329
|
+
disabled
|
|
330
|
+
}),
|
|
331
|
+
id,
|
|
332
|
+
role: "option",
|
|
333
|
+
tabIndex: -1,
|
|
334
|
+
"aria-selected": active,
|
|
335
|
+
"aria-disabled": disabled || void 0,
|
|
336
|
+
"data-value": value,
|
|
337
|
+
...props,
|
|
338
|
+
onPointerMove: (event) => {
|
|
339
|
+
onPointerMove?.(event);
|
|
340
|
+
if (!disabled) setActive();
|
|
341
|
+
},
|
|
342
|
+
onClick: (event) => {
|
|
343
|
+
if (disabled) return;
|
|
344
|
+
onClick?.(event);
|
|
345
|
+
if (!event.defaultPrevented) onSelect?.(value);
|
|
346
|
+
},
|
|
347
|
+
children
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/** The leading glyph column of a row (wrap an `Icon` so it keeps its token sizing). */
|
|
351
|
+
function CommandItemIcon({ children, ...props }) {
|
|
352
|
+
return /* @__PURE__ */ jsx("span", {
|
|
353
|
+
...itemIconProps(),
|
|
354
|
+
...props,
|
|
355
|
+
children
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/** The row's text column (truncates rather than wrapping). */
|
|
359
|
+
function CommandItemLabel({ children, ...props }) {
|
|
360
|
+
return /* @__PURE__ */ jsx("span", {
|
|
361
|
+
...itemLabelProps(),
|
|
362
|
+
...props,
|
|
363
|
+
children
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** A thin divider between sections. */
|
|
367
|
+
function CommandSeparator(props) {
|
|
368
|
+
return /* @__PURE__ */ jsx("div", {
|
|
369
|
+
role: "separator",
|
|
370
|
+
...separatorProps(),
|
|
371
|
+
...props
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The centred no-results message. A polite live region so screen readers announce it; render it
|
|
376
|
+
* conditionally when your ranked list is empty (you own the data, so you know when there's nothing to
|
|
377
|
+
* show — Noctis doesn't filter internally).
|
|
378
|
+
*/
|
|
379
|
+
function CommandEmpty({ children, ...props }) {
|
|
380
|
+
return /* @__PURE__ */ jsx("div", {
|
|
381
|
+
role: "status",
|
|
382
|
+
"aria-live": "polite",
|
|
383
|
+
...emptyProps(),
|
|
384
|
+
...props,
|
|
385
|
+
children
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/** The centred async progress shell, shown before results settle (compose a spinner glyph + copy). */
|
|
389
|
+
function CommandLoading({ children, ...props }) {
|
|
390
|
+
return /* @__PURE__ */ jsx("div", {
|
|
391
|
+
...loadingProps(),
|
|
392
|
+
...props,
|
|
393
|
+
children
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* The footer region — a bare, end-aligned strip below the list. Unopinionated about its contents: drop
|
|
398
|
+
* in whatever status or hints you want (e.g. `Kbd` caps beside a label). The component ships no
|
|
399
|
+
* "hint" part, so you own the markup.
|
|
400
|
+
*/
|
|
401
|
+
function CommandFooter({ children, ...props }) {
|
|
402
|
+
return /* @__PURE__ */ jsx("div", {
|
|
403
|
+
...footerProps(),
|
|
404
|
+
...props,
|
|
405
|
+
children
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* An accessible command palette — a query field over a ranked, sectioned list of commands with full
|
|
410
|
+
* combobox/`aria-activedescendant` keyboard navigation and breadcrumb drill-in. Ships as a modal
|
|
411
|
+
* (`Command.Dialog` + a headless `Command.Trigger`) or inline (`Command.Root`); both share identical
|
|
412
|
+
* inner parts. Ranking is owned by Noctis — rank your commands with the exported `rankItems` /
|
|
413
|
+
* `useCommandRanking` and render the survivors as `Command.Item`s.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```tsx
|
|
417
|
+
* const ranked = rankItems(commands, query);
|
|
418
|
+
* <Command.Dialog open={open} onOpenChange={setOpen} value={query} onValueChange={setQuery}>
|
|
419
|
+
* <Command.Header>
|
|
420
|
+
* <SearchIcon />
|
|
421
|
+
* <Command.Input placeholder="Type a command…" />
|
|
422
|
+
* </Command.Header>
|
|
423
|
+
* <Command.List>
|
|
424
|
+
* <Command.Empty>No commands found.</Command.Empty>
|
|
425
|
+
* {ranked.map((c) => (
|
|
426
|
+
* <Command.Item key={c.value} value={c.value} onSelect={c.run}>
|
|
427
|
+
* <Command.ItemLabel>{c.label}</Command.ItemLabel>
|
|
428
|
+
* </Command.Item>
|
|
429
|
+
* ))}
|
|
430
|
+
* </Command.List>
|
|
431
|
+
* </Command.Dialog>
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
const Command = {
|
|
435
|
+
/** The inline palette (embedded in normal flow). `Command.Root.props()` → its spreadable panel prop bag. */
|
|
436
|
+
Root: Object.assign(CommandRoot, { props: panelProps }),
|
|
437
|
+
/** The modal palette (centred, focus-trapped). */
|
|
438
|
+
Dialog: CommandDialog,
|
|
439
|
+
/** A headless launcher that wires open + ARIA onto your own button. */
|
|
440
|
+
Trigger: CommandTrigger,
|
|
441
|
+
/** The input row. `Command.Header.props()` → its spreadable prop bag. */
|
|
442
|
+
Header: Object.assign(CommandHeader, { props: headerProps }),
|
|
443
|
+
/** The drill-in trail. `Command.Breadcrumb.props()` → its spreadable prop bag. */
|
|
444
|
+
Breadcrumb: Object.assign(CommandBreadcrumb, { props: breadcrumbProps }),
|
|
445
|
+
/** The query field. `Command.Input.props()` → its spreadable prop bag. */
|
|
446
|
+
Input: Object.assign(CommandInput, { props: inputProps }),
|
|
447
|
+
/** The trailing input affordance. `Command.InputAction.props()` → its spreadable prop bag. */
|
|
448
|
+
InputAction: Object.assign(CommandInputAction, { props: inputActionProps }),
|
|
449
|
+
/** The scrolling listbox. `Command.List.props()` → its spreadable prop bag. */
|
|
450
|
+
List: Object.assign(CommandList, { props: listProps }),
|
|
451
|
+
/** A labelled section. `Command.Group.props()` → its spreadable prop bag. */
|
|
452
|
+
Group: Object.assign(CommandGroup, { props: groupProps }),
|
|
453
|
+
/** A section heading. `Command.GroupLabel.props()` → its spreadable prop bag. */
|
|
454
|
+
GroupLabel: Object.assign(CommandGroupLabel, { props: groupLabelProps }),
|
|
455
|
+
/** One command row. `Command.Item.props({ highlighted, disabled })` → its spreadable prop bag for a foreign element. */
|
|
456
|
+
Item: Object.assign(CommandItem, { props: itemProps }),
|
|
457
|
+
/** The leading row glyph. `Command.ItemIcon.props()` → its spreadable prop bag. */
|
|
458
|
+
ItemIcon: Object.assign(CommandItemIcon, { props: itemIconProps }),
|
|
459
|
+
/** The row's text column. `Command.ItemLabel.props()` → its spreadable prop bag. */
|
|
460
|
+
ItemLabel: Object.assign(CommandItemLabel, { props: itemLabelProps }),
|
|
461
|
+
/** A section divider. `Command.Separator.props()` → its spreadable prop bag. */
|
|
462
|
+
Separator: Object.assign(CommandSeparator, { props: separatorProps }),
|
|
463
|
+
/** The no-results message. `Command.Empty.props()` → its spreadable prop bag. */
|
|
464
|
+
Empty: Object.assign(CommandEmpty, { props: emptyProps }),
|
|
465
|
+
/** The async progress shell. `Command.Loading.props()` → its spreadable prop bag. */
|
|
466
|
+
Loading: Object.assign(CommandLoading, { props: loadingProps }),
|
|
467
|
+
/** The footer region (compose your own status/hints). `Command.Footer.props()` → its spreadable prop bag. */
|
|
468
|
+
Footer: Object.assign(CommandFooter, { props: footerProps })
|
|
469
|
+
};
|
|
470
|
+
//#endregion
|
|
471
|
+
export { Command };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//#region src/components/command/command.props.d.ts
|
|
2
|
+
/** A spreadable data-attribute prop bag — the shape every `Command.*.props()` returns. */
|
|
3
|
+
type CommandPartProps = {
|
|
4
|
+
/** The slot value the matching `command.css` rules anchor on. */"data-slot": string; /** Forwarded verbatim — styling is attribute-driven, so this is an optional consumer passthrough. */
|
|
5
|
+
className?: string; /** A data-attribute present (empty string) or absent (`undefined`); never `false`. */
|
|
6
|
+
[attr: `data-${string}`]: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
/** Common shape: every part's `.props()` accepts an optional `className` passthrough. */
|
|
9
|
+
interface BasePropsArgs {
|
|
10
|
+
/** Forwarded verbatim onto the returned prop bag. */
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Argument to `Command.Panel.props(...)` — whether the panel is in its modal (centred popup) form. */
|
|
14
|
+
interface CommandPanelPropsArgs extends BasePropsArgs {
|
|
15
|
+
/** Whether the panel is the modal popup (drives the fixed-position scale-fade via `data-modal`). */
|
|
16
|
+
modal?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Argument to `Command.Item.props(...)` — the per-row state the CSS keys its highlight off. */
|
|
19
|
+
interface CommandItemPropsArgs extends BasePropsArgs {
|
|
20
|
+
/** Whether the pointer/keyboard is over this row (drives the highlight via `data-highlighted`). */
|
|
21
|
+
highlighted?: boolean;
|
|
22
|
+
/** Whether this row is disabled (drives the not-allowed affordance via `data-disabled`). */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Argument to a stateless part's `.props(...)` — no variants/state of its own. */
|
|
26
|
+
type CommandStatelessPropsArgs = BasePropsArgs;
|
|
27
|
+
/** Panel prop bag: the slot anchor plus the modal flag (the surface paint is owned by the composed Surface). */
|
|
28
|
+
declare function panelProps({
|
|
29
|
+
modal,
|
|
30
|
+
className
|
|
31
|
+
}?: CommandPanelPropsArgs): CommandPartProps;
|
|
32
|
+
/** Header prop bag: just the slot anchor (the input row). */
|
|
33
|
+
declare function headerProps({
|
|
34
|
+
className
|
|
35
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
36
|
+
/** Breadcrumb prop bag: just the slot anchor (the drill-in trail). */
|
|
37
|
+
declare function breadcrumbProps({
|
|
38
|
+
className
|
|
39
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
40
|
+
/** Input prop bag: just the slot anchor (the query field). */
|
|
41
|
+
declare function inputProps({
|
|
42
|
+
className
|
|
43
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
44
|
+
/** Input-action prop bag: just the slot anchor (the trailing affordance). */
|
|
45
|
+
declare function inputActionProps({
|
|
46
|
+
className
|
|
47
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
48
|
+
/** List prop bag: just the slot anchor (the scrolling listbox). */
|
|
49
|
+
declare function listProps({
|
|
50
|
+
className
|
|
51
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
52
|
+
/** Group prop bag: just the slot anchor (a labelled section). */
|
|
53
|
+
declare function groupProps({
|
|
54
|
+
className
|
|
55
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
56
|
+
/** Group-label prop bag: just the slot anchor (a section heading). */
|
|
57
|
+
declare function groupLabelProps({
|
|
58
|
+
className
|
|
59
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
60
|
+
/** Item prop bag: slot anchor plus the `data-highlighted`/`data-disabled` state. */
|
|
61
|
+
declare function itemProps({
|
|
62
|
+
highlighted,
|
|
63
|
+
disabled,
|
|
64
|
+
className
|
|
65
|
+
}?: CommandItemPropsArgs): CommandPartProps;
|
|
66
|
+
/** Item-icon prop bag: just the slot anchor (the leading row glyph). */
|
|
67
|
+
declare function itemIconProps({
|
|
68
|
+
className
|
|
69
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
70
|
+
/** Item-label prop bag: just the slot anchor (the row's text column). */
|
|
71
|
+
declare function itemLabelProps({
|
|
72
|
+
className
|
|
73
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
74
|
+
/** Separator prop bag: just the slot anchor (a section divider). */
|
|
75
|
+
declare function separatorProps({
|
|
76
|
+
className
|
|
77
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
78
|
+
/** Empty prop bag: just the slot anchor (the no-results message). */
|
|
79
|
+
declare function emptyProps({
|
|
80
|
+
className
|
|
81
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
82
|
+
/** Loading prop bag: just the slot anchor (the async progress shell). */
|
|
83
|
+
declare function loadingProps({
|
|
84
|
+
className
|
|
85
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
86
|
+
/** Footer prop bag: just the slot anchor (the footer region). */
|
|
87
|
+
declare function footerProps({
|
|
88
|
+
className
|
|
89
|
+
}?: CommandStatelessPropsArgs): CommandPartProps;
|
|
90
|
+
//#endregion
|
|
91
|
+
export { CommandPartProps, breadcrumbProps, emptyProps, footerProps, groupLabelProps, groupProps, headerProps, inputActionProps, inputProps, itemIconProps, itemLabelProps, itemProps, listProps, loadingProps, panelProps, separatorProps };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { COMMAND_SLOTS } from "./command.slots.js";
|
|
2
|
+
//#region src/components/command/command.props.ts
|
|
3
|
+
/**
|
|
4
|
+
* The D12 unified variant contract for Command — a per-part set of `props(...)` builders that each
|
|
5
|
+
* return a **spreadable props object** of the form `{ "data-slot": "noctis-command-<part>",
|
|
6
|
+
* ...dataAttrs }`, derived from the part's variant/state inputs.
|
|
7
|
+
*
|
|
8
|
+
* Under the single-`data-slot` anchor model the `data-slot` is the only styling hook needed —
|
|
9
|
+
* `command.css` keys every rule off it — so spreading a part's `props()` onto a *foreign* element
|
|
10
|
+
* styles it as that part:
|
|
11
|
+
*
|
|
12
|
+
* <li {...Command.Item.props({ highlighted: true })}>Transfer assets…</li>
|
|
13
|
+
* // → <li data-slot="noctis-command-item" data-highlighted="">
|
|
14
|
+
*
|
|
15
|
+
* The escape hatch carries no className (styling is attribute-driven); an optional `className`
|
|
16
|
+
* passthrough is accepted and forwarded verbatim. The same variant→data-attribute→values mapping is
|
|
17
|
+
* emitted as data from the token graph (`generated/declarations.json` → `variantSchema`) so non-React /
|
|
18
|
+
* agent consumers can hand-write the markup from the docs.
|
|
19
|
+
*/
|
|
20
|
+
/** Stamp a boolean state as a bare data-attribute: present (`""`) when on, absent (`undefined`) when off. */
|
|
21
|
+
const flag = (on) => on ? "" : void 0;
|
|
22
|
+
const withClassName = (bag, className) => className === void 0 ? bag : {
|
|
23
|
+
...bag,
|
|
24
|
+
className
|
|
25
|
+
};
|
|
26
|
+
/** Panel prop bag: the slot anchor plus the modal flag (the surface paint is owned by the composed Surface). */
|
|
27
|
+
function panelProps({ modal, className } = {}) {
|
|
28
|
+
return withClassName({
|
|
29
|
+
"data-slot": COMMAND_SLOTS.panel,
|
|
30
|
+
"data-modal": flag(modal)
|
|
31
|
+
}, className);
|
|
32
|
+
}
|
|
33
|
+
/** Header prop bag: just the slot anchor (the input row). */
|
|
34
|
+
function headerProps({ className } = {}) {
|
|
35
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.header }, className);
|
|
36
|
+
}
|
|
37
|
+
/** Breadcrumb prop bag: just the slot anchor (the drill-in trail). */
|
|
38
|
+
function breadcrumbProps({ className } = {}) {
|
|
39
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.breadcrumb }, className);
|
|
40
|
+
}
|
|
41
|
+
/** Input prop bag: just the slot anchor (the query field). */
|
|
42
|
+
function inputProps({ className } = {}) {
|
|
43
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.input }, className);
|
|
44
|
+
}
|
|
45
|
+
/** Input-action prop bag: just the slot anchor (the trailing affordance). */
|
|
46
|
+
function inputActionProps({ className } = {}) {
|
|
47
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.inputAction }, className);
|
|
48
|
+
}
|
|
49
|
+
/** List prop bag: just the slot anchor (the scrolling listbox). */
|
|
50
|
+
function listProps({ className } = {}) {
|
|
51
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.list }, className);
|
|
52
|
+
}
|
|
53
|
+
/** Group prop bag: just the slot anchor (a labelled section). */
|
|
54
|
+
function groupProps({ className } = {}) {
|
|
55
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.group }, className);
|
|
56
|
+
}
|
|
57
|
+
/** Group-label prop bag: just the slot anchor (a section heading). */
|
|
58
|
+
function groupLabelProps({ className } = {}) {
|
|
59
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.groupLabel }, className);
|
|
60
|
+
}
|
|
61
|
+
/** Item prop bag: slot anchor plus the `data-highlighted`/`data-disabled` state. */
|
|
62
|
+
function itemProps({ highlighted, disabled, className } = {}) {
|
|
63
|
+
return withClassName({
|
|
64
|
+
"data-slot": COMMAND_SLOTS.item,
|
|
65
|
+
"data-highlighted": flag(highlighted),
|
|
66
|
+
"data-disabled": flag(disabled)
|
|
67
|
+
}, className);
|
|
68
|
+
}
|
|
69
|
+
/** Item-icon prop bag: just the slot anchor (the leading row glyph). */
|
|
70
|
+
function itemIconProps({ className } = {}) {
|
|
71
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.itemIcon }, className);
|
|
72
|
+
}
|
|
73
|
+
/** Item-label prop bag: just the slot anchor (the row's text column). */
|
|
74
|
+
function itemLabelProps({ className } = {}) {
|
|
75
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.itemLabel }, className);
|
|
76
|
+
}
|
|
77
|
+
/** Separator prop bag: just the slot anchor (a section divider). */
|
|
78
|
+
function separatorProps({ className } = {}) {
|
|
79
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.separator }, className);
|
|
80
|
+
}
|
|
81
|
+
/** Empty prop bag: just the slot anchor (the no-results message). */
|
|
82
|
+
function emptyProps({ className } = {}) {
|
|
83
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.empty }, className);
|
|
84
|
+
}
|
|
85
|
+
/** Loading prop bag: just the slot anchor (the async progress shell). */
|
|
86
|
+
function loadingProps({ className } = {}) {
|
|
87
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.loading }, className);
|
|
88
|
+
}
|
|
89
|
+
/** Footer prop bag: just the slot anchor (the footer region). */
|
|
90
|
+
function footerProps({ className } = {}) {
|
|
91
|
+
return withClassName({ "data-slot": COMMAND_SLOTS.footer }, className);
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { breadcrumbProps, emptyProps, footerProps, groupLabelProps, groupProps, headerProps, inputActionProps, inputProps, itemIconProps, itemLabelProps, itemProps, listProps, loadingProps, panelProps, separatorProps };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/components/command/command.slots.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `data-*` hooks `Command` stamps on its parts, for host-side styling and tests. Slot values mark
|
|
4
|
+
* each rendered element; the state attributes are emitted by the palette's own combobox/listbox engine
|
|
5
|
+
* (which drives the keyboard model — see `command-listbox`) and by the composed Dialog — pair a slot with
|
|
6
|
+
* a state to target, say, the highlighted row or the modal panel while it transitions in.
|
|
7
|
+
*/
|
|
8
|
+
declare enum CommandDataAttributes {
|
|
9
|
+
/** Marks each rendered part. */
|
|
10
|
+
slot = "data-slot",
|
|
11
|
+
/** Present on the panel in its modal form (the centred, fixed-height, scale-fading popup). */
|
|
12
|
+
modal = "data-modal",
|
|
13
|
+
/** Present on the command row the pointer or keyboard is currently over (the active descendant). */
|
|
14
|
+
highlighted = "data-highlighted",
|
|
15
|
+
/** Present on a disabled command row. */
|
|
16
|
+
disabled = "data-disabled",
|
|
17
|
+
/** Present on the modal panel/scrim for the first frame after mount — the transition's start state. */
|
|
18
|
+
startingStyle = "data-starting-style",
|
|
19
|
+
/** Present on the modal panel/scrim while it transitions out before unmounting. */
|
|
20
|
+
endingStyle = "data-ending-style"
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { CommandDataAttributes };
|