@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,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,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 };
|