@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.
Files changed (43) hide show
  1. package/dist/components/breadcrumb/breadcrumb.d.ts +163 -0
  2. package/dist/components/breadcrumb/breadcrumb.js +152 -0
  3. package/dist/components/breadcrumb/breadcrumb.props.d.ts +59 -0
  4. package/dist/components/breadcrumb/breadcrumb.props.js +68 -0
  5. package/dist/components/breadcrumb/breadcrumb.slots.d.ts +16 -0
  6. package/dist/components/breadcrumb/breadcrumb.slots.js +32 -0
  7. package/dist/components/breadcrumb/breadcrumb.types.d.ts +9 -0
  8. package/dist/components/breadcrumb/index.d.ts +3 -0
  9. package/dist/components/command/command-listbox.js +174 -0
  10. package/dist/components/command/command-rank.d.ts +40 -0
  11. package/dist/components/command/command-rank.js +61 -0
  12. package/dist/components/command/command-score.d.ts +25 -0
  13. package/dist/components/command/command-score.js +85 -0
  14. package/dist/components/command/command.context.d.ts +17 -0
  15. package/dist/components/command/command.context.js +13 -0
  16. package/dist/components/command/command.d.ts +396 -0
  17. package/dist/components/command/command.js +471 -0
  18. package/dist/components/command/command.props.d.ts +91 -0
  19. package/dist/components/command/command.props.js +94 -0
  20. package/dist/components/command/command.slots.d.ts +23 -0
  21. package/dist/components/command/command.slots.js +60 -0
  22. package/dist/components/command/index.d.ts +6 -0
  23. package/dist/components/command/use-command-ranking.d.ts +37 -0
  24. package/dist/components/command/use-command-ranking.js +127 -0
  25. package/dist/components/search-dialog/parts/root.js +1 -1
  26. package/dist/components/skeleton/index.d.ts +3 -0
  27. package/dist/components/skeleton/skeleton.context.js +12 -0
  28. package/dist/components/skeleton/skeleton.d.ts +157 -0
  29. package/dist/components/skeleton/skeleton.js +130 -0
  30. package/dist/components/skeleton/skeleton.props.d.ts +47 -0
  31. package/dist/components/skeleton/skeleton.props.js +57 -0
  32. package/dist/components/skeleton/skeleton.slots.d.ts +15 -0
  33. package/dist/components/skeleton/skeleton.slots.js +28 -0
  34. package/dist/components/skeleton/skeleton.types.d.ts +13 -0
  35. package/dist/components/surface/surface.d.ts +1 -1
  36. package/dist/index.d.ts +15 -3
  37. package/dist/index.js +13 -4
  38. package/dist/primitives/index.d.ts +1 -1
  39. package/dist/primitives/index.js +2 -2
  40. package/dist/props.d.ts +37 -34
  41. package/dist/props.js +37 -34
  42. package/dist/styles.css +715 -0
  43. 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 };