@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,60 @@
1
+ //#region src/components/command/command.slots.ts
2
+ /**
3
+ * The slot vocabulary every `Command` part stamps as its `data-slot`. The authored source the
4
+ * orchestration file reads from, prefixed `noctis-command-{part}` (the precompiled `command.css` keys
5
+ * every rule off these anchors); SLOTS.md generates from the token-graph declaration.
6
+ *
7
+ * `backdrop`, `header`, `breadcrumb`, `inputAction`, `listSizer`, `listView`, `group`, `itemIcon`,
8
+ * `itemLabel`, `separator`, `loading`, and `footer` are styling-only anchors (the modal scrim, the input
9
+ * row, the drill-in trail, the trailing input affordance, the list's measured content wrapper, the
10
+ * per-page view that re-animates on drill-in, the section wrapper, the row's glyph and label columns,
11
+ * the divider, and the footer region) — they carry no token mints, so they live here but not in the
12
+ * token-graph anatomy.
13
+ *
14
+ * The component is deliberately unopinionated past the leading icon: a row pushes any trailing content
15
+ * (a `Kbd` shortcut, a badge, a count) to its end via the label's `flex`, and the footer is a bare
16
+ * region — neither bakes in a "shortcut"/"hint" concept, so the consumer composes those freely.
17
+ */
18
+ const COMMAND_SLOTS = {
19
+ panel: "noctis-command",
20
+ backdrop: "noctis-command-backdrop",
21
+ header: "noctis-command-header",
22
+ breadcrumb: "noctis-command-breadcrumb",
23
+ input: "noctis-command-input",
24
+ inputAction: "noctis-command-input-action",
25
+ list: "noctis-command-list",
26
+ listSizer: "noctis-command-list-sizer",
27
+ listView: "noctis-command-list-view",
28
+ group: "noctis-command-group",
29
+ groupLabel: "noctis-command-group-label",
30
+ item: "noctis-command-item",
31
+ itemIcon: "noctis-command-item-icon",
32
+ itemLabel: "noctis-command-item-label",
33
+ separator: "noctis-command-separator",
34
+ empty: "noctis-command-empty",
35
+ loading: "noctis-command-loading",
36
+ footer: "noctis-command-footer"
37
+ };
38
+ /**
39
+ * The `data-*` hooks `Command` stamps on its parts, for host-side styling and tests. Slot values mark
40
+ * each rendered element; the state attributes are emitted by the palette's own combobox/listbox engine
41
+ * (which drives the keyboard model — see `command-listbox`) and by the composed Dialog — pair a slot with
42
+ * a state to target, say, the highlighted row or the modal panel while it transitions in.
43
+ */
44
+ let CommandDataAttributes = /* @__PURE__ */ function(CommandDataAttributes) {
45
+ /** Marks each rendered part. */
46
+ CommandDataAttributes["slot"] = "data-slot";
47
+ /** Present on the panel in its modal form (the centred, fixed-height, scale-fading popup). */
48
+ CommandDataAttributes["modal"] = "data-modal";
49
+ /** Present on the command row the pointer or keyboard is currently over (the active descendant). */
50
+ CommandDataAttributes["highlighted"] = "data-highlighted";
51
+ /** Present on a disabled command row. */
52
+ CommandDataAttributes["disabled"] = "data-disabled";
53
+ /** Present on the modal panel/scrim for the first frame after mount — the transition's start state. */
54
+ CommandDataAttributes["startingStyle"] = "data-starting-style";
55
+ /** Present on the modal panel/scrim while it transitions out before unmounting. */
56
+ CommandDataAttributes["endingStyle"] = "data-ending-style";
57
+ return CommandDataAttributes;
58
+ }({});
59
+ //#endregion
60
+ export { COMMAND_SLOTS, CommandDataAttributes };
@@ -0,0 +1,6 @@
1
+ import { CommandPage } from "./command.context.js";
2
+ import { Command } from "./command.js";
3
+ import { CommandDataAttributes } from "./command.slots.js";
4
+ import { commandScore } from "./command-score.js";
5
+ import { RankOptions, RankableItem, rankItems } from "./command-rank.js";
6
+ import { UseCommandRankingOptions, createRankingWorker, useCommandRanking } from "./use-command-ranking.js";
@@ -0,0 +1,37 @@
1
+ import { RankOptions, RankableItem } from "./command-rank.js";
2
+
3
+ //#region src/components/command/use-command-ranking.d.ts
4
+ /**
5
+ * Options for {@link useCommandRanking} — the {@link RankOptions} plus the Worker-offload controls.
6
+ * A custom `score` function forces the synchronous path (functions can't cross the Worker boundary).
7
+ */
8
+ interface UseCommandRankingOptions<T extends RankableItem> extends RankOptions<T> {
9
+ /**
10
+ * Offload ranking to a Web Worker once the candidate count reaches `workerThreshold`, keeping a
11
+ * large palette's keystrokes off the main thread. Falls back to synchronous ranking when a Worker
12
+ * can't be constructed (SSR, older bundlers, a custom `score`).
13
+ * @default true
14
+ */
15
+ worker?: boolean;
16
+ /**
17
+ * Candidate count at or above which the Worker is used (below it the synchronous path is faster
18
+ * than the round-trip).
19
+ * @default 250
20
+ */
21
+ workerThreshold?: number;
22
+ }
23
+ /**
24
+ * Build an inline-Blob ranking Worker, or `null` when the environment can't host one. Browser-only:
25
+ * jsdom has no `Worker`, so this is excluded from coverage (the hook's null-fallback path is tested).
26
+ */
27
+ declare function createRankingWorker(): Worker | null;
28
+ /**
29
+ * Rank `items` against `query`, offloading to a Web Worker for large candidate sets while staying
30
+ * synchronous (and SSR-safe) otherwise. The synchronous result is always returned immediately; a
31
+ * Worker result, when it arrives, supersedes it — so the palette never blocks on the round-trip.
32
+ *
33
+ * Pass `worker: false` (or a custom `score`) to force the synchronous path.
34
+ */
35
+ declare function useCommandRanking<T extends RankableItem>(items: readonly T[], query: string, options?: UseCommandRankingOptions<T>): T[];
36
+ //#endregion
37
+ export { UseCommandRankingOptions, createRankingWorker, useCommandRanking };
@@ -0,0 +1,127 @@
1
+ "use client";
2
+ import { commandScore } from "./command-score.js";
3
+ import { rankItems, rankValues } from "./command-rank.js";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ //#region src/components/command/use-command-ranking.ts
6
+ /**
7
+ * The Worker's message glue: take a ranking request, run the shared `rankValues`, post the ordered
8
+ * values back. This is the *only* worker-specific code — the scoring and ranking it relies on are the
9
+ * real, tested `commandScore` / `rankValues`, serialised verbatim alongside it (see `WORKER_SOURCE`),
10
+ * so the Worker reuses the exact same implementation rather than a copy. Runs only inside a Worker
11
+ * runtime, so it is excluded from coverage.
12
+ */
13
+ /* v8 ignore start -- executes only inside a Worker (absent in jsdom); its `rankValues`/`commandScore` are covered */
14
+ function workerGlue() {
15
+ const scope = self;
16
+ scope.onmessage = (event) => {
17
+ const { id, items, query, threshold } = event.data;
18
+ scope.postMessage({
19
+ id,
20
+ values: rankValues(items, query, threshold)
21
+ });
22
+ };
23
+ }
24
+ /* v8 ignore stop */
25
+ const WORKER_SOURCE = `${commandScore};\n${rankValues};\n(${workerGlue})();`;
26
+ /**
27
+ * Build an inline-Blob ranking Worker, or `null` when the environment can't host one. Browser-only:
28
+ * jsdom has no `Worker`, so this is excluded from coverage (the hook's null-fallback path is tested).
29
+ */
30
+ /* v8 ignore start -- browser-only Worker construction (no `Worker` in jsdom) */
31
+ function createRankingWorker() {
32
+ try {
33
+ if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL?.createObjectURL !== "function") return null;
34
+ const url = URL.createObjectURL(new Blob([WORKER_SOURCE], { type: "application/javascript" }));
35
+ const worker = new Worker(url);
36
+ URL.revokeObjectURL(url);
37
+ return worker;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+ /* v8 ignore stop */
43
+ /**
44
+ * Rank `items` against `query`, offloading to a Web Worker for large candidate sets while staying
45
+ * synchronous (and SSR-safe) otherwise. The synchronous result is always returned immediately; a
46
+ * Worker result, when it arrives, supersedes it — so the palette never blocks on the round-trip.
47
+ *
48
+ * Pass `worker: false` (or a custom `score`) to force the synchronous path.
49
+ */
50
+ function useCommandRanking(items, query, options = {}) {
51
+ const { worker = true, workerThreshold = 250, threshold = 0, score } = options;
52
+ const useWorker = worker && score === void 0 && items.length >= workerThreshold;
53
+ const sync = useMemo(() => rankItems(items, query, {
54
+ threshold,
55
+ score
56
+ }), [
57
+ items,
58
+ query,
59
+ threshold,
60
+ score
61
+ ]);
62
+ const [workerOrder, setWorkerOrder] = useState(null);
63
+ const workerRef = useRef(null);
64
+ const jobRef = useRef(0);
65
+ useEffect(() => {
66
+ if (!useWorker) {
67
+ setWorkerOrder(null);
68
+ return;
69
+ }
70
+ /* v8 ignore start -- the Worker path: jsdom has no Worker, so createRankingWorker is null and
71
+ the message wiring below never runs (the hook's sync fallback is what gets tested) */
72
+ const w = workerRef.current ??= createRankingWorker();
73
+ if (!w) {
74
+ setWorkerOrder(null);
75
+ return;
76
+ }
77
+ const id = ++jobRef.current;
78
+ setWorkerOrder(null);
79
+ const onMessage = (event) => {
80
+ const data = event.data;
81
+ if (data.id === id) setWorkerOrder(data.values);
82
+ };
83
+ w.addEventListener("message", onMessage);
84
+ w.postMessage({
85
+ id,
86
+ query,
87
+ threshold,
88
+ items: items.map((item) => ({
89
+ value: item.value,
90
+ label: item.label,
91
+ keywords: item.keywords,
92
+ boost: item.boost
93
+ }))
94
+ });
95
+ return () => w.removeEventListener("message", onMessage);
96
+ /* v8 ignore stop */
97
+ }, [
98
+ useWorker,
99
+ items,
100
+ query,
101
+ threshold
102
+ ]);
103
+ useEffect(() => () => {
104
+ /* v8 ignore next -- workerRef is only ever set on the live-Worker path */
105
+ workerRef.current?.terminate();
106
+ }, []);
107
+ return useMemo(() => {
108
+ if (!useWorker) return sync;
109
+ /* v8 ignore start -- reached only when a Worker has replied (never in jsdom) */
110
+ if (workerOrder === null) return sync;
111
+ const byValue = new Map(items.map((item) => [item.value, item]));
112
+ const ordered = [];
113
+ for (const value of workerOrder) {
114
+ const item = byValue.get(value);
115
+ if (item) ordered.push(item);
116
+ }
117
+ return ordered;
118
+ /* v8 ignore stop */
119
+ }, [
120
+ useWorker,
121
+ workerOrder,
122
+ sync,
123
+ items
124
+ ]);
125
+ }
126
+ //#endregion
127
+ export { createRankingWorker, useCommandRanking };
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { VisuallyHidden } from "../../../core/visually-hidden/visually-hidden.js";
3
+ import { Dialog } from "../../dialog/dialog.js";
3
4
  import { SearchDialogProvider } from "../search-dialog.context.js";
4
5
  import { SEARCH_DIALOG_SLOTS } from "../search-dialog.slots.js";
5
- import { Dialog } from "../../dialog/dialog.js";
6
6
  import { useEffect, useMemo, useRef, useState } from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/components/search-dialog/parts/root.tsx
@@ -0,0 +1,3 @@
1
+ import { SkeletonVariant } from "./skeleton.types.js";
2
+ import { Skeleton } from "./skeleton.js";
3
+ import { SkeletonDataAttributes } from "./skeleton.slots.js";
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { createContext, use } from "react";
3
+ //#region src/components/skeleton/skeleton.context.ts
4
+ const SkeletonContext = createContext({ variant: "shimmer" });
5
+ /** Provider used by `Skeleton.Root` to share its `variant` with descendant shapes. */
6
+ const SkeletonProvider = SkeletonContext.Provider;
7
+ /** Reads the active `Skeleton` context, falling back to the shimmer default for a standalone shape. */
8
+ function useSkeletonContext() {
9
+ return use(SkeletonContext);
10
+ }
11
+ //#endregion
12
+ export { SkeletonProvider, useSkeletonContext };
@@ -0,0 +1,157 @@
1
+ import { SkeletonVariant } from "./skeleton.types.js";
2
+ import { SkeletonLayoutPropsArgs, SkeletonPartProps, SkeletonShapePropsArgs, boxProps, circleProps, lineProps, rootProps, textProps } from "./skeleton.props.js";
3
+ import { ComponentPropsWithRef, ReactElement } from "react";
4
+
5
+ //#region src/components/skeleton/skeleton.d.ts
6
+ /**
7
+ * The accessible loading group — a `role="status"` live region that announces "Loading" to assistive
8
+ * tech while its placeholder shapes show. Lays its children out as a vertical stack by default (restyle
9
+ * via `className`/`style` for any other arrangement) and shares its `variant` down to every child shape,
10
+ * so you set the animation once on the group. Swap the announced text with `label`, or unmount the whole
11
+ * group once the real content is ready.
12
+ *
13
+ * The placeholder shapes (`Skeleton.Box`/`Circle`/`Text`) are decorative (`aria-hidden`) — this region
14
+ * is the single thing screen readers hear, so the loading state is conveyed once, not once per shape.
15
+ *
16
+ * @see {@link Skeleton.Root.Props}
17
+ */
18
+ declare function SkeletonRoot({
19
+ variant,
20
+ label,
21
+ className,
22
+ children,
23
+ ...props
24
+ }: Skeleton.Root.Props): ReactElement;
25
+ /**
26
+ * A rectangular placeholder — the general-purpose block for an image, a card, a thumbnail, or any
27
+ * fixed-shape region. Fills its inline space and defaults to a short height; set `style`/`className`
28
+ * dimensions to match the content it stands in for. Decorative (`aria-hidden`); the animation comes from
29
+ * its own `variant` or the surrounding `Skeleton.Root`.
30
+ */
31
+ declare function SkeletonBox({
32
+ variant,
33
+ className,
34
+ ...props
35
+ }: Skeleton.Box.Props): ReactElement;
36
+ /**
37
+ * A circular placeholder — for an avatar, a status dot, or an icon. Defaults to a `2.5rem` disc; size it
38
+ * with `style`/`className` to match the element it stands in for. Decorative (`aria-hidden`).
39
+ */
40
+ declare function SkeletonCircle({
41
+ variant,
42
+ className,
43
+ ...props
44
+ }: Skeleton.Circle.Props): ReactElement;
45
+ /**
46
+ * A multi-line text placeholder — a stack of line bars standing in for a paragraph. Renders `lines` bars
47
+ * (default 3); the last bar is drawn shorter so the block reads as ragged prose, not a solid box. The
48
+ * line height tracks the surrounding font size. Decorative (`aria-hidden`); compose `Skeleton.Line`
49
+ * directly for full control over individual line widths.
50
+ */
51
+ declare function SkeletonText({
52
+ lines,
53
+ variant,
54
+ className,
55
+ ...props
56
+ }: Skeleton.Text.Props): ReactElement;
57
+ /**
58
+ * A single text-line placeholder bar. Use it directly inside a `Skeleton.Text` (or any layout) when you
59
+ * want to set per-line widths via `style`/`className` instead of the uniform `Skeleton.Text` block.
60
+ * Decorative (`aria-hidden`).
61
+ */
62
+ declare function SkeletonLine({
63
+ variant,
64
+ className,
65
+ ...props
66
+ }: Skeleton.Line.Props): ReactElement;
67
+ /**
68
+ * A loading placeholder that mirrors the shape of the content it stands in for. `Skeleton.Root` is the
69
+ * accessible group (a `role="status"` region) that announces the load and shares the animation `variant`
70
+ * to its shapes; compose `Skeleton.Box` (a rectangle), `Skeleton.Circle` (an avatar/icon disc), and
71
+ * `Skeleton.Text` (a paragraph of `Skeleton.Line`s) inside it. Each shape also works standalone.
72
+ *
73
+ * The default `shimmer` variant sweeps a light band across each placeholder; `pulse` fades it and `none`
74
+ * holds it static. Every variant respects `prefers-reduced-motion`, and the placeholder fill is the
75
+ * surface-adaptive `well` overlay so it stays visible on any surface. Styling is precompiled CSS keyed
76
+ * off the `data-slot` anchor (`skeleton.css`); the shapes are accent-independent and purely presentational.
77
+ *
78
+ * The runtime compound is a plain object (kept tree-shakeable); per-part prop types are exposed through
79
+ * the matching `Skeleton` namespace — e.g. `Skeleton.Root.Props`.
80
+ */
81
+ declare const Skeleton: {
82
+ /** The accessible loading group. `Skeleton.Root.props({ variant })` → its prop bag. */Root: typeof SkeletonRoot & {
83
+ props: typeof rootProps;
84
+ }; /** A rectangular placeholder. `Skeleton.Box.props({ variant })` → its spreadable prop bag. */
85
+ Box: typeof SkeletonBox & {
86
+ props: typeof boxProps;
87
+ }; /** A circular placeholder. `Skeleton.Circle.props({ variant })` → its spreadable prop bag. */
88
+ Circle: typeof SkeletonCircle & {
89
+ props: typeof circleProps;
90
+ }; /** A multi-line text placeholder. `Skeleton.Text.props()` → its spreadable prop bag. */
91
+ Text: typeof SkeletonText & {
92
+ props: typeof textProps;
93
+ }; /** A single text-line placeholder. `Skeleton.Line.props({ variant })` → its spreadable prop bag. */
94
+ Line: typeof SkeletonLine & {
95
+ props: typeof lineProps;
96
+ };
97
+ };
98
+ /**
99
+ * Per-part prop types. Types-only — it emits no runtime code and merges with the `Skeleton` object
100
+ * above, so `Skeleton.Root` is the component value while `Skeleton.Root.Props` is its prop type.
101
+ */
102
+ declare namespace Skeleton {
103
+ /** Animation style — `shimmer` | `pulse` | `none`. */
104
+ type Variant = SkeletonVariant;
105
+ /** The spreadable data-attribute prop bag every `Skeleton.*.props()` returns (D12). */
106
+ type PartProps = SkeletonPartProps;
107
+ namespace Root {
108
+ type Props = ComponentPropsWithRef<"div"> & {
109
+ /**
110
+ * Animation style shared down to every child shape.
111
+ * @default "shimmer"
112
+ */
113
+ variant?: SkeletonVariant;
114
+ /**
115
+ * The accessible name announced for the loading region; overrides the localized "Loading".
116
+ */
117
+ label?: string;
118
+ };
119
+ /** Argument to the `Skeleton.Root.props(...)` escape-hatch helper. */
120
+ type PropsArgs = SkeletonShapePropsArgs;
121
+ }
122
+ namespace Box {
123
+ type Props = ComponentPropsWithRef<"div"> & {
124
+ /** Animation style; inherits from the surrounding `Skeleton.Root` when unset. @default "shimmer" */variant?: SkeletonVariant;
125
+ };
126
+ /** Argument to the `Skeleton.Box.props(...)` escape-hatch helper. */
127
+ type PropsArgs = SkeletonShapePropsArgs;
128
+ }
129
+ namespace Circle {
130
+ type Props = ComponentPropsWithRef<"div"> & {
131
+ /** Animation style; inherits from the surrounding `Skeleton.Root` when unset. @default "shimmer" */variant?: SkeletonVariant;
132
+ };
133
+ /** Argument to the `Skeleton.Circle.props(...)` escape-hatch helper. */
134
+ type PropsArgs = SkeletonShapePropsArgs;
135
+ }
136
+ namespace Text {
137
+ type Props = ComponentPropsWithRef<"div"> & {
138
+ /**
139
+ * How many line bars to render; the last bar is drawn shorter.
140
+ * @default 3
141
+ */
142
+ lines?: number; /** Animation style; inherits from the surrounding `Skeleton.Root` when unset. @default "shimmer" */
143
+ variant?: SkeletonVariant;
144
+ };
145
+ /** Argument to the `Skeleton.Text.props(...)` escape-hatch helper. */
146
+ type PropsArgs = SkeletonLayoutPropsArgs;
147
+ }
148
+ namespace Line {
149
+ type Props = ComponentPropsWithRef<"div"> & {
150
+ /** Animation style; inherits from the surrounding `Skeleton.Root` when unset. @default "shimmer" */variant?: SkeletonVariant;
151
+ };
152
+ /** Argument to the `Skeleton.Line.props(...)` escape-hatch helper. */
153
+ type PropsArgs = SkeletonShapePropsArgs;
154
+ }
155
+ }
156
+ //#endregion
157
+ export { Skeleton };
@@ -0,0 +1,130 @@
1
+ "use client";
2
+ import { useInjectedLabels } from "../../core/use-injected-labels.js";
3
+ import { VisuallyHidden } from "../../core/visually-hidden/visually-hidden.js";
4
+ import { SkeletonProvider, useSkeletonContext } from "./skeleton.context.js";
5
+ import { boxProps, circleProps, lineProps, rootProps, textProps } from "./skeleton.props.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/components/skeleton/skeleton.tsx
8
+ /** English fallbacks and catalog keys for the loading region's accessible name (resolved per locale). */
9
+ const DEFAULT_LABELS = { loading: "Loading" };
10
+ const LABEL_KEYS = { loading: "skeleton.loading" };
11
+ /**
12
+ * The accessible loading group — a `role="status"` live region that announces "Loading" to assistive
13
+ * tech while its placeholder shapes show. Lays its children out as a vertical stack by default (restyle
14
+ * via `className`/`style` for any other arrangement) and shares its `variant` down to every child shape,
15
+ * so you set the animation once on the group. Swap the announced text with `label`, or unmount the whole
16
+ * group once the real content is ready.
17
+ *
18
+ * The placeholder shapes (`Skeleton.Box`/`Circle`/`Text`) are decorative (`aria-hidden`) — this region
19
+ * is the single thing screen readers hear, so the loading state is conveyed once, not once per shape.
20
+ *
21
+ * @see {@link Skeleton.Root.Props}
22
+ */
23
+ function SkeletonRoot({ variant = "shimmer", label, className, children, ...props }) {
24
+ const labels = useInjectedLabels(DEFAULT_LABELS, LABEL_KEYS, label === void 0 ? void 0 : { loading: label });
25
+ return /* @__PURE__ */ jsx("div", {
26
+ ...rootProps({
27
+ variant,
28
+ className
29
+ }),
30
+ role: "status",
31
+ "aria-busy": "true",
32
+ ...props,
33
+ children: /* @__PURE__ */ jsxs(SkeletonProvider, {
34
+ value: { variant },
35
+ children: [/* @__PURE__ */ jsx(VisuallyHidden, { children: labels.loading }), children]
36
+ })
37
+ });
38
+ }
39
+ /**
40
+ * A rectangular placeholder — the general-purpose block for an image, a card, a thumbnail, or any
41
+ * fixed-shape region. Fills its inline space and defaults to a short height; set `style`/`className`
42
+ * dimensions to match the content it stands in for. Decorative (`aria-hidden`); the animation comes from
43
+ * its own `variant` or the surrounding `Skeleton.Root`.
44
+ */
45
+ function SkeletonBox({ variant, className, ...props }) {
46
+ const { variant: inherited } = useSkeletonContext();
47
+ return /* @__PURE__ */ jsx("div", {
48
+ ...boxProps({
49
+ variant: variant ?? inherited,
50
+ className
51
+ }),
52
+ "aria-hidden": "true",
53
+ ...props
54
+ });
55
+ }
56
+ /**
57
+ * A circular placeholder — for an avatar, a status dot, or an icon. Defaults to a `2.5rem` disc; size it
58
+ * with `style`/`className` to match the element it stands in for. Decorative (`aria-hidden`).
59
+ */
60
+ function SkeletonCircle({ variant, className, ...props }) {
61
+ const { variant: inherited } = useSkeletonContext();
62
+ return /* @__PURE__ */ jsx("div", {
63
+ ...circleProps({
64
+ variant: variant ?? inherited,
65
+ className
66
+ }),
67
+ "aria-hidden": "true",
68
+ ...props
69
+ });
70
+ }
71
+ /**
72
+ * A multi-line text placeholder — a stack of line bars standing in for a paragraph. Renders `lines` bars
73
+ * (default 3); the last bar is drawn shorter so the block reads as ragged prose, not a solid box. The
74
+ * line height tracks the surrounding font size. Decorative (`aria-hidden`); compose `Skeleton.Line`
75
+ * directly for full control over individual line widths.
76
+ */
77
+ function SkeletonText({ lines = 3, variant, className, ...props }) {
78
+ const { variant: inherited } = useSkeletonContext();
79
+ const resolved = variant ?? inherited;
80
+ return /* @__PURE__ */ jsx("div", {
81
+ ...textProps({ className }),
82
+ "aria-hidden": "true",
83
+ ...props,
84
+ children: Array.from({ length: Math.max(0, lines) }, (_, i) => /* @__PURE__ */ jsx(SkeletonLine, { variant: resolved }, i))
85
+ });
86
+ }
87
+ /**
88
+ * A single text-line placeholder bar. Use it directly inside a `Skeleton.Text` (or any layout) when you
89
+ * want to set per-line widths via `style`/`className` instead of the uniform `Skeleton.Text` block.
90
+ * Decorative (`aria-hidden`).
91
+ */
92
+ function SkeletonLine({ variant, className, ...props }) {
93
+ const { variant: inherited } = useSkeletonContext();
94
+ return /* @__PURE__ */ jsx("div", {
95
+ ...lineProps({
96
+ variant: variant ?? inherited,
97
+ className
98
+ }),
99
+ "aria-hidden": "true",
100
+ ...props
101
+ });
102
+ }
103
+ /**
104
+ * A loading placeholder that mirrors the shape of the content it stands in for. `Skeleton.Root` is the
105
+ * accessible group (a `role="status"` region) that announces the load and shares the animation `variant`
106
+ * to its shapes; compose `Skeleton.Box` (a rectangle), `Skeleton.Circle` (an avatar/icon disc), and
107
+ * `Skeleton.Text` (a paragraph of `Skeleton.Line`s) inside it. Each shape also works standalone.
108
+ *
109
+ * The default `shimmer` variant sweeps a light band across each placeholder; `pulse` fades it and `none`
110
+ * holds it static. Every variant respects `prefers-reduced-motion`, and the placeholder fill is the
111
+ * surface-adaptive `well` overlay so it stays visible on any surface. Styling is precompiled CSS keyed
112
+ * off the `data-slot` anchor (`skeleton.css`); the shapes are accent-independent and purely presentational.
113
+ *
114
+ * The runtime compound is a plain object (kept tree-shakeable); per-part prop types are exposed through
115
+ * the matching `Skeleton` namespace — e.g. `Skeleton.Root.Props`.
116
+ */
117
+ const Skeleton = {
118
+ /** The accessible loading group. `Skeleton.Root.props({ variant })` → its prop bag. */
119
+ Root: Object.assign(SkeletonRoot, { props: rootProps }),
120
+ /** A rectangular placeholder. `Skeleton.Box.props({ variant })` → its spreadable prop bag. */
121
+ Box: Object.assign(SkeletonBox, { props: boxProps }),
122
+ /** A circular placeholder. `Skeleton.Circle.props({ variant })` → its spreadable prop bag. */
123
+ Circle: Object.assign(SkeletonCircle, { props: circleProps }),
124
+ /** A multi-line text placeholder. `Skeleton.Text.props()` → its spreadable prop bag. */
125
+ Text: Object.assign(SkeletonText, { props: textProps }),
126
+ /** A single text-line placeholder. `Skeleton.Line.props({ variant })` → its spreadable prop bag. */
127
+ Line: Object.assign(SkeletonLine, { props: lineProps })
128
+ };
129
+ //#endregion
130
+ export { Skeleton };
@@ -0,0 +1,47 @@
1
+ import { SkeletonVariant } from "./skeleton.types.js";
2
+
3
+ //#region src/components/skeleton/skeleton.props.d.ts
4
+ /** A spreadable data-attribute prop bag — the shape every `Skeleton.*.props()` returns. */
5
+ type SkeletonPartProps = {
6
+ /** The slot value the matching `skeleton.css` rules anchor on. */"data-slot": string; /** Forwarded verbatim — styling is attribute-driven, so this is an optional consumer passthrough. */
7
+ className?: string; /** A data-attribute present (string) or absent (`undefined`); never `false`. */
8
+ [attr: `data-${string}`]: string | undefined;
9
+ };
10
+ /** Common shape: every part's `.props()` accepts an optional `className` passthrough. */
11
+ interface BasePropsArgs {
12
+ /** Forwarded verbatim onto the returned prop bag. */
13
+ className?: string;
14
+ }
15
+ /** Argument to a shape's `.props(...)` — its animation variant plus the optional `className`. */
16
+ interface SkeletonShapePropsArgs extends BasePropsArgs {
17
+ /** Animation style — `shimmer` (default) | `pulse` | `none`. @default "shimmer" */
18
+ variant?: SkeletonVariant;
19
+ }
20
+ /** Argument to a layout part's `.props(...)` — no animation of its own; the lines inside animate. */
21
+ type SkeletonLayoutPropsArgs = BasePropsArgs;
22
+ /** Root prop bag: the status-region slot plus the `data-variant` it shares down to its shapes. */
23
+ declare function rootProps({
24
+ variant,
25
+ className
26
+ }?: SkeletonShapePropsArgs): SkeletonPartProps;
27
+ /** Box prop bag: the rectangle slot plus its `data-variant`. */
28
+ declare function boxProps({
29
+ variant,
30
+ className
31
+ }?: SkeletonShapePropsArgs): SkeletonPartProps;
32
+ /** Circle prop bag: the disc slot plus its `data-variant`. */
33
+ declare function circleProps({
34
+ variant,
35
+ className
36
+ }?: SkeletonShapePropsArgs): SkeletonPartProps;
37
+ /** Text prop bag: the lines wrapper slot (a layout stack — its `Skeleton.Line`s carry the animation). */
38
+ declare function textProps({
39
+ className
40
+ }?: SkeletonLayoutPropsArgs): SkeletonPartProps;
41
+ /** Line prop bag: a single text-line slot plus its `data-variant`. */
42
+ declare function lineProps({
43
+ variant,
44
+ className
45
+ }?: SkeletonShapePropsArgs): SkeletonPartProps;
46
+ //#endregion
47
+ export { SkeletonLayoutPropsArgs, SkeletonPartProps, SkeletonShapePropsArgs, boxProps, circleProps, lineProps, rootProps, textProps };
@@ -0,0 +1,57 @@
1
+ import { SKELETON_SLOTS } from "./skeleton.slots.js";
2
+ //#region src/components/skeleton/skeleton.props.ts
3
+ /**
4
+ * The D12 unified variant contract for Skeleton — the data-attribute-native styling helpers.
5
+ *
6
+ * Each part exposes a `props(...)` builder returning a **spreadable props object** of the form
7
+ * `{ "data-slot": "noctis-skeleton-<part>", ...dataAttrs }`. Under the single-`data-slot` anchor model
8
+ * the `data-slot` is the only styling hook needed — `skeleton.css` keys every rule off it plus the
9
+ * `data-variant` animation recipe — so spreading a part's `props()` onto a *foreign* element styles it
10
+ * as that placeholder:
11
+ *
12
+ * <div {...Skeleton.Box.props({ variant: "pulse" })} style={{ height: 48 }} />
13
+ * // → <div data-slot="noctis-skeleton-box" data-variant="pulse" aria-hidden="true">
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
+ const withClassName = (bag, className) => className === void 0 ? bag : {
21
+ ...bag,
22
+ className
23
+ };
24
+ /** Root prop bag: the status-region slot plus the `data-variant` it shares down to its shapes. */
25
+ function rootProps({ variant = "shimmer", className } = {}) {
26
+ return withClassName({
27
+ "data-slot": SKELETON_SLOTS.root,
28
+ "data-variant": variant
29
+ }, className);
30
+ }
31
+ /** Box prop bag: the rectangle slot plus its `data-variant`. */
32
+ function boxProps({ variant = "shimmer", className } = {}) {
33
+ return withClassName({
34
+ "data-slot": SKELETON_SLOTS.box,
35
+ "data-variant": variant
36
+ }, className);
37
+ }
38
+ /** Circle prop bag: the disc slot plus its `data-variant`. */
39
+ function circleProps({ variant = "shimmer", className } = {}) {
40
+ return withClassName({
41
+ "data-slot": SKELETON_SLOTS.circle,
42
+ "data-variant": variant
43
+ }, className);
44
+ }
45
+ /** Text prop bag: the lines wrapper slot (a layout stack — its `Skeleton.Line`s carry the animation). */
46
+ function textProps({ className } = {}) {
47
+ return withClassName({ "data-slot": SKELETON_SLOTS.text }, className);
48
+ }
49
+ /** Line prop bag: a single text-line slot plus its `data-variant`. */
50
+ function lineProps({ variant = "shimmer", className } = {}) {
51
+ return withClassName({
52
+ "data-slot": SKELETON_SLOTS.line,
53
+ "data-variant": variant
54
+ }, className);
55
+ }
56
+ //#endregion
57
+ export { boxProps, circleProps, lineProps, rootProps, textProps };