@alikhalilll/a-skeleton 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,261 @@
1
+ import {
2
+ Comment,
3
+ Fragment,
4
+ Text,
5
+ h,
6
+ type VNode,
7
+ type VNodeArrayChildren,
8
+ type VNodeChild,
9
+ } from 'vue';
10
+
11
+ /**
12
+ * Atomic HTML tags — rendered as a single skeleton block. Their own class/style
13
+ * is preserved so Tailwind utilities (`size-16`, `rounded-full`, …) carry the
14
+ * dimensions across without us needing to measure.
15
+ */
16
+ const ATOMIC_TAGS = new Set([
17
+ 'img',
18
+ 'svg',
19
+ 'canvas',
20
+ 'video',
21
+ 'input',
22
+ 'textarea',
23
+ 'select',
24
+ 'button',
25
+ 'progress',
26
+ 'meter',
27
+ 'hr',
28
+ ]);
29
+
30
+ /** Single-line text containers — produce one bar. */
31
+ const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']);
32
+
33
+ /** Multi-line text containers — produce N bars with a shortened last line. */
34
+ const PARAGRAPH_TAGS = new Set(['p', 'blockquote']);
35
+
36
+ /** Inline text — single bar, but inherits parent font sizing. */
37
+ const INLINE_TEXT_TAGS = new Set([
38
+ 'span',
39
+ 'a',
40
+ 'small',
41
+ 'strong',
42
+ 'em',
43
+ 'code',
44
+ 'time',
45
+ 'label',
46
+ 'b',
47
+ 'i',
48
+ 'mark',
49
+ ]);
50
+
51
+ export interface BuildOptions {
52
+ animationClass: string | null;
53
+ /** Max recursion depth — guards runaway templates. Default 8. */
54
+ maxDepth?: number;
55
+ /**
56
+ * Hard cap on emitted skeleton nodes. Default 300. A 200-row table doesn't
57
+ * need 200 distinct skeleton rows on first paint; cap and stop early.
58
+ */
59
+ maxNodes?: number;
60
+ }
61
+
62
+ const DEFAULT_MAX_DEPTH = 8;
63
+ const DEFAULT_MAX_NODES = 300;
64
+
65
+ interface WalkState {
66
+ emitted: number;
67
+ cap: number;
68
+ }
69
+
70
+ /**
71
+ * Walk a slot's vnode tree and produce a skeleton that mirrors its rendered
72
+ * structure: same wrapping tags, same `class` strings (so flex/grid/spacing/
73
+ * sizing utilities still apply), but text/atomic leaves replaced with shimmer
74
+ * blocks. The result renders correctly on the FIRST paint without any DOM
75
+ * measurement, as long as the slot's template renders structure even when its
76
+ * data is empty (use `v-if`/`v-else` to swap content, not to gate the wrapper).
77
+ *
78
+ * Performance: `maxNodes` caps the work. When the cap is hit we stop emitting
79
+ * — the caller still gets a valid skeleton, just clipped at the budget. A 1000-
80
+ * row list renders ~300 skeleton rows on first paint and then the measured cache
81
+ * takes over for subsequent loads.
82
+ */
83
+ export function buildStructuralSkeleton(
84
+ vnodes: VNodeChild | VNodeArrayChildren | undefined | null,
85
+ opts: BuildOptions
86
+ ): VNode[] {
87
+ const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
88
+ const state: WalkState = { emitted: 0, cap: opts.maxNodes ?? DEFAULT_MAX_NODES };
89
+ const out: VNode[] = [];
90
+ walk(vnodes, opts, 0, maxDepth, state, out);
91
+ return out;
92
+ }
93
+
94
+ function walk(
95
+ input: VNodeChild | VNodeArrayChildren | undefined | null,
96
+ opts: BuildOptions,
97
+ depth: number,
98
+ max: number,
99
+ state: WalkState,
100
+ out: VNode[]
101
+ ): void {
102
+ if (state.emitted >= state.cap) return;
103
+ if (input == null || typeof input === 'boolean') return;
104
+
105
+ if (Array.isArray(input)) {
106
+ for (let i = 0; i < input.length; i++) {
107
+ if (state.emitted >= state.cap) return;
108
+ walk(input[i] as VNodeChild, opts, depth, max, state, out);
109
+ }
110
+ return;
111
+ }
112
+
113
+ if (typeof input === 'string' || typeof input === 'number') {
114
+ const str = String(input).trim();
115
+ if (str) push(out, textBar(opts.animationClass), state);
116
+ return;
117
+ }
118
+
119
+ const v = input as VNode;
120
+ const type = v.type;
121
+
122
+ if (type === Comment) return;
123
+
124
+ if (type === Text) {
125
+ const t = typeof v.children === 'string' ? v.children.trim() : '';
126
+ if (t) push(out, textBar(opts.animationClass), state);
127
+ return;
128
+ }
129
+
130
+ if (type === Fragment) {
131
+ walk(v.children as VNodeArrayChildren, opts, depth, max, state, out);
132
+ return;
133
+ }
134
+
135
+ if (typeof type === 'string') {
136
+ push(out, transformElement(v, type.toLowerCase(), opts, depth, max, state), state);
137
+ return;
138
+ }
139
+
140
+ /* Component vnode — we can't introspect its template, so render an opaque
141
+ * skeleton block carrying any utility classes the user attached to it. */
142
+ if (typeof type === 'object' || typeof type === 'function') {
143
+ push(
144
+ out,
145
+ h('div', {
146
+ class: ['a-skel-block', v.props?.class, opts.animationClass],
147
+ style: v.props?.style as Record<string, string>,
148
+ }),
149
+ state
150
+ );
151
+ }
152
+ }
153
+
154
+ function push(out: VNode[], vn: VNode, state: WalkState): void {
155
+ if (state.emitted >= state.cap) return;
156
+ out.push(vn);
157
+ state.emitted++;
158
+ }
159
+
160
+ function transformElement(
161
+ v: VNode,
162
+ tag: string,
163
+ opts: BuildOptions,
164
+ depth: number,
165
+ max: number,
166
+ state: WalkState
167
+ ): VNode {
168
+ const cls = v.props?.class;
169
+ const styl = v.props?.style as Record<string, string> | string | undefined;
170
+
171
+ if (ATOMIC_TAGS.has(tag)) {
172
+ return h('div', { class: ['a-skel-block', cls, opts.animationClass], style: styl });
173
+ }
174
+
175
+ if (HEADING_TAGS.has(tag)) {
176
+ return h(tag, { class: cls, style: styl }, [textBar(opts.animationClass)]);
177
+ }
178
+
179
+ if (PARAGRAPH_TAGS.has(tag)) {
180
+ const children = v.children;
181
+ const recursedChildren: VNode[] = [];
182
+ walk(children as VNodeArrayChildren, opts, depth + 1, max, state, recursedChildren);
183
+ if (recursedChildren.length > 0) return h(tag, { class: cls, style: styl }, recursedChildren);
184
+ const lines = estimateLines(children, 3);
185
+ return h(tag, { class: cls, style: styl }, multiLineBars(lines, opts.animationClass));
186
+ }
187
+
188
+ if (INLINE_TEXT_TAGS.has(tag)) {
189
+ const children = v.children;
190
+ const recursedChildren: VNode[] = [];
191
+ walk(children as VNodeArrayChildren, opts, depth + 1, max, state, recursedChildren);
192
+ if (recursedChildren.length > 0) return h(tag, { class: cls, style: styl }, recursedChildren);
193
+ return h(tag, { class: cls, style: styl }, [textBar(opts.animationClass)]);
194
+ }
195
+
196
+ /* Generic container — keep its classes (flex/grid/padding/etc.) and recurse. */
197
+ if (depth >= max) {
198
+ return h('div', { class: ['a-skel-block', cls, opts.animationClass], style: styl });
199
+ }
200
+ const recursed: VNode[] = [];
201
+ walk(v.children as VNodeArrayChildren, opts, depth + 1, max, state, recursed);
202
+ if (recursed.length === 0) {
203
+ /* Empty container in the source — render as a single block so the layout
204
+ * still reserves space rather than collapsing to zero height. */
205
+ return h('div', { class: ['a-skel-block', cls, opts.animationClass], style: styl });
206
+ }
207
+ return h(tag, { class: cls, style: styl }, recursed);
208
+ }
209
+
210
+ function estimateLines(children: unknown, max: number): number {
211
+ if (typeof children !== 'string') return 1;
212
+ const len = children.trim().length;
213
+ if (len === 0)
214
+ return 2; /* empty interpolation — assume 2 lines so the bar looks paragraph-shaped */
215
+ if (len < 40) return 1;
216
+ if (len < 100) return 2;
217
+ return Math.min(max, 3);
218
+ }
219
+
220
+ function multiLineBars(lines: number, animationClass: string | null): VNode[] {
221
+ const out: VNode[] = [];
222
+ for (let i = 0; i < lines; i++) {
223
+ out.push(textBar(animationClass, i === lines - 1 && lines > 1 ? 0.65 : 1));
224
+ }
225
+ return out;
226
+ }
227
+
228
+ /* Style objects for the two common bar shapes are reused across calls so a
229
+ * structural skeleton with 200 text bars doesn't allocate 200 style objects. */
230
+ const BAR_STYLE_FULL = Object.freeze({
231
+ display: 'inline-block',
232
+ width: '100%',
233
+ height: '0.75em',
234
+ verticalAlign: 'middle',
235
+ borderRadius: '4px',
236
+ });
237
+
238
+ const PARTIAL_BAR_CACHE = new Map<number, Readonly<Record<string, string>>>();
239
+
240
+ function partialBarStyle(widthFraction: number): Readonly<Record<string, string>> {
241
+ /* Round to one decimal so 0.65, 0.7, 0.85 each get a single cached style. */
242
+ const key = Math.round(widthFraction * 10) / 10;
243
+ const hit = PARTIAL_BAR_CACHE.get(key);
244
+ if (hit) return hit;
245
+ const made = Object.freeze({
246
+ display: 'inline-block',
247
+ width: `${Math.round(key * 100)}%`,
248
+ height: '0.75em',
249
+ verticalAlign: 'middle',
250
+ borderRadius: '4px',
251
+ });
252
+ PARTIAL_BAR_CACHE.set(key, made);
253
+ return made;
254
+ }
255
+
256
+ function textBar(animationClass: string | null, widthFraction = 1): VNode {
257
+ return h('span', {
258
+ class: ['a-skel-block', 'a-skel-block--text', animationClass],
259
+ style: widthFraction === 1 ? BAR_STYLE_FULL : partialBarStyle(widthFraction),
260
+ });
261
+ }
@@ -0,0 +1,42 @@
1
+ import { Comment, Fragment, Text, type VNode } from 'vue';
2
+
3
+ /**
4
+ * Derive a default cache key from a slot's vnode tree. Returns the first
5
+ * non-comment child's component name (or HTML tag). When the slot only contains
6
+ * text / comments / unknown content, returns `'anonymous'` so the caller can
7
+ * still cache, but with no useful identity — encourage an explicit `cacheKey`.
8
+ */
9
+ export function fingerprintSlot(vnodes: VNode[] | undefined): string {
10
+ if (!vnodes) return 'anonymous';
11
+ for (const vnode of vnodes) {
12
+ const tag = describeVNode(vnode);
13
+ if (tag) return tag;
14
+ }
15
+ return 'anonymous';
16
+ }
17
+
18
+ function describeVNode(vnode: VNode): string | undefined {
19
+ const t = vnode.type;
20
+ if (t === Comment || t === Text) return undefined;
21
+ if (t === Fragment) {
22
+ const children = vnode.children;
23
+ if (Array.isArray(children)) {
24
+ for (const child of children) {
25
+ if (child && typeof child === 'object' && 'type' in (child as object)) {
26
+ const found = describeVNode(child as VNode);
27
+ if (found) return found;
28
+ }
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ if (typeof t === 'string') return t;
34
+ if (typeof t === 'object' && t !== null) {
35
+ const named =
36
+ (t as { name?: string }).name ??
37
+ (t as { __name?: string }).__name ??
38
+ (t as { displayName?: string }).displayName;
39
+ if (named) return named;
40
+ }
41
+ return undefined;
42
+ }
@@ -0,0 +1,226 @@
1
+ import type { CSSProperties } from 'vue';
2
+ import type { CachedShape, ShapeNode, ShapeNodeType } from '../types';
3
+
4
+ export interface WalkOptions {
5
+ maxDepth: number;
6
+ /** Hard cap on captured nodes. Default 500. */
7
+ maxNodes?: number;
8
+ /** Min CSS-pixel size (either axis) for an element to be emitted. Default 4. */
9
+ minSize?: number;
10
+ }
11
+
12
+ const DEFAULT_MAX_NODES = 500;
13
+ const DEFAULT_MIN_SIZE = 4;
14
+
15
+ /* Atomic elements — never recursed into; rendered as a single block. */
16
+ const LEAF_TAGS = new Set([
17
+ 'IMG',
18
+ 'SVG',
19
+ 'CANVAS',
20
+ 'VIDEO',
21
+ 'INPUT',
22
+ 'TEXTAREA',
23
+ 'SELECT',
24
+ 'BUTTON',
25
+ 'PROGRESS',
26
+ 'METER',
27
+ 'HR',
28
+ ]);
29
+
30
+ /**
31
+ * Walk `root`'s descendants and produce a list of shimmer blocks that mirror its
32
+ * rendered layout. Coordinates are relative to `root`'s top-left so the result can
33
+ * be replayed in any container of the same size.
34
+ *
35
+ * Performance:
36
+ * - `maxNodes` caps the walk so a 5000-row table doesn't lock up the main thread.
37
+ * - `minSize` filters out hairlines (1-2 px paddings, decorative dots) that
38
+ * inflate node count without adding visual signal.
39
+ * - All `getBoundingClientRect` / `getComputedStyle` reads happen in a single
40
+ * top-down pass with no intervening writes, so the browser does one layout
41
+ * up front and serves cached values from then on (no layout thrashing).
42
+ * - Each emitted `ShapeNode` has a frozen pre-computed `style` (and `lineStyles`
43
+ * for multi-line text) so the render path is allocation-free.
44
+ */
45
+ export function walkDom(root: HTMLElement, options: WalkOptions): CachedShape {
46
+ const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
47
+ const minSize = options.minSize ?? DEFAULT_MIN_SIZE;
48
+
49
+ const nodes: ShapeNode[] = [];
50
+ const rootRect = root.getBoundingClientRect();
51
+ let truncated = false;
52
+
53
+ function visit(el: Element, depth: number): void {
54
+ if (nodes.length >= maxNodes) {
55
+ truncated = true;
56
+ return;
57
+ }
58
+
59
+ const html = el as HTMLElement;
60
+ if (html.dataset?.skeletonIgnore !== undefined) return;
61
+
62
+ const cs = window.getComputedStyle(el);
63
+ if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return;
64
+
65
+ const rect = el.getBoundingClientRect();
66
+ if (rect.width < minSize || rect.height < minSize) return;
67
+
68
+ const tag = el.tagName.toUpperCase();
69
+ const isLeafTag = LEAF_TAGS.has(tag);
70
+ const hasStop = html.dataset?.skeletonStop !== undefined;
71
+ const childElements: Element[] = [];
72
+ for (let i = 0; i < el.children.length; i++) {
73
+ const c = el.children[i];
74
+ if ((c as HTMLElement).dataset?.skeletonIgnore === undefined) childElements.push(c);
75
+ }
76
+ const hasOwnText = hasDirectTextContent(el);
77
+ const reachedDepth = depth >= options.maxDepth;
78
+ const isLeaf = isLeafTag || hasStop || reachedDepth || childElements.length === 0;
79
+
80
+ if (isLeaf) {
81
+ const node = elementToShape(tag, cs, rect, rootRect, hasOwnText);
82
+ if (node) nodes.push(node);
83
+ return;
84
+ }
85
+
86
+ for (let i = 0; i < childElements.length; i++) {
87
+ if (nodes.length >= maxNodes) {
88
+ truncated = true;
89
+ return;
90
+ }
91
+ visit(childElements[i], depth + 1);
92
+ }
93
+ }
94
+
95
+ for (let i = 0; i < root.children.length; i++) {
96
+ if (nodes.length >= maxNodes) {
97
+ truncated = true;
98
+ break;
99
+ }
100
+ visit(root.children[i], 1);
101
+ }
102
+
103
+ return Object.freeze({
104
+ nodes: Object.freeze(nodes),
105
+ width: Math.round(rootRect.width),
106
+ height: Math.round(rootRect.height),
107
+ truncated,
108
+ }) as CachedShape;
109
+ }
110
+
111
+ function hasDirectTextContent(el: Element): boolean {
112
+ for (let i = 0; i < el.childNodes.length; i++) {
113
+ const node = el.childNodes[i];
114
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').trim().length > 0) {
115
+ return true;
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+
121
+ function elementToShape(
122
+ tag: string,
123
+ cs: CSSStyleDeclaration,
124
+ rect: DOMRect,
125
+ origin: DOMRect,
126
+ hasText: boolean
127
+ ): ShapeNode | null {
128
+ const x = Math.round(rect.left - origin.left);
129
+ const y = Math.round(rect.top - origin.top);
130
+ const w = Math.round(rect.width);
131
+ const h = Math.round(rect.height);
132
+
133
+ const radius = parseFloat(cs.borderRadius) || 0;
134
+ const minDim = Math.min(w, h);
135
+ const isCircle = radius >= minDim / 2 - 1 && Math.abs(w - h) <= 2 && minDim > 0;
136
+
137
+ let type: ShapeNodeType;
138
+ let resolvedRadius = radius;
139
+ let lines: number | undefined;
140
+ let lineHeight: number | undefined;
141
+
142
+ if (tag === 'IMG' || tag === 'SVG' || tag === 'VIDEO' || tag === 'CANVAS') {
143
+ type = 'image';
144
+ } else if (isCircle) {
145
+ type = 'circle';
146
+ resolvedRadius = Math.floor(minDim / 2);
147
+ } else if (hasText) {
148
+ type = 'text';
149
+ lineHeight = Math.round(parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.4 || 16);
150
+ lines = Math.max(1, Math.round(h / lineHeight));
151
+ resolvedRadius = Math.min(radius, 4);
152
+ } else {
153
+ type = 'block';
154
+ }
155
+
156
+ return freezeShape({
157
+ type,
158
+ x,
159
+ y,
160
+ w,
161
+ h,
162
+ radius: resolvedRadius,
163
+ lines,
164
+ lineHeight,
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Pre-compute (and freeze) the inline styles used at render time. Doing it once
170
+ * here means rendering 500 blocks doesn't allocate 500 style objects per frame.
171
+ */
172
+ function freezeShape(node: {
173
+ type: ShapeNodeType;
174
+ x: number;
175
+ y: number;
176
+ w: number;
177
+ h: number;
178
+ radius: number;
179
+ lines?: number;
180
+ lineHeight?: number;
181
+ }): ShapeNode {
182
+ const style: CSSProperties = Object.freeze({
183
+ left: `${node.x}px`,
184
+ top: `${node.y}px`,
185
+ width: `${node.w}px`,
186
+ height: `${node.h}px`,
187
+ borderRadius: `${node.radius}px`,
188
+ });
189
+
190
+ let lineStyles: ReadonlyArray<Readonly<CSSProperties>> | undefined;
191
+ if (node.type === 'text' && node.lines && node.lines > 1) {
192
+ const lh = node.lineHeight ?? Math.round(node.h / node.lines);
193
+ const barHeight = Math.max(8, Math.round(lh * 0.7));
194
+ const widthFull = `${node.w}px`;
195
+ const widthLast = `${Math.max(40, Math.round(node.w * 0.7))}px`;
196
+ const heightStr = `${barHeight}px`;
197
+ const radiusStr = `${node.radius}px`;
198
+ const arr: Readonly<CSSProperties>[] = [];
199
+ for (let i = 1; i <= node.lines; i++) {
200
+ const isLast = i === node.lines;
201
+ arr.push(
202
+ Object.freeze<CSSProperties>({
203
+ left: `${node.x}px`,
204
+ top: `${node.y + (i - 1) * lh}px`,
205
+ width: isLast ? widthLast : widthFull,
206
+ height: heightStr,
207
+ borderRadius: radiusStr,
208
+ })
209
+ );
210
+ }
211
+ lineStyles = Object.freeze(arr);
212
+ }
213
+
214
+ return Object.freeze({
215
+ type: node.type,
216
+ x: node.x,
217
+ y: node.y,
218
+ w: node.w,
219
+ h: node.h,
220
+ radius: node.radius,
221
+ lines: node.lines,
222
+ lineHeight: node.lineHeight,
223
+ style,
224
+ lineStyles,
225
+ });
226
+ }
package/web-types.json ADDED
@@ -0,0 +1,172 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/web-types",
3
+ "name": "@alikhalilll/a-skeleton",
4
+ "version": "1.0.0",
5
+ "js-types-syntax": "typescript",
6
+ "description-markup": "markdown",
7
+ "framework": "vue",
8
+ "framework-config": {
9
+ "enable-when": {
10
+ "node-packages": [
11
+ "vue"
12
+ ]
13
+ }
14
+ },
15
+ "contributions": {
16
+ "html": {
17
+ "vue-components": [
18
+ {
19
+ "name": "ASkeleton",
20
+ "source": {
21
+ "module": "@alikhalilll/a-skeleton",
22
+ "symbol": "ASkeleton"
23
+ },
24
+ "props": [
25
+ {
26
+ "name": "animation",
27
+ "type": "SkeletonAnimation",
28
+ "required": false,
29
+ "description": "Animation variant applied to skeleton blocks. Default `'shimmer'`."
30
+ },
31
+ {
32
+ "name": "cacheKey",
33
+ "type": "string",
34
+ "required": false,
35
+ "description": "Identifier used to look up + persist the captured shape. Falls back to the\nslot's first child component name, then `'anonymous'`. Pass explicitly when\nthe same component renders different shapes depending on props."
36
+ },
37
+ {
38
+ "name": "class",
39
+ "type": "ClassValue",
40
+ "required": false,
41
+ "description": "Class on the outer wrapper."
42
+ },
43
+ {
44
+ "name": "fallback",
45
+ "type": "SkeletonFallback",
46
+ "required": false,
47
+ "description": "What to render when no cached shape is available yet. Default `'shimmer'`."
48
+ },
49
+ {
50
+ "name": "loading",
51
+ "type": "boolean",
52
+ "required": true,
53
+ "description": "When true, render the captured skeleton (or `fallback` slot) instead of the default slot."
54
+ },
55
+ {
56
+ "name": "maxDepth",
57
+ "type": "number",
58
+ "required": false,
59
+ "description": "Max recursion depth during shape capture. Default 6."
60
+ },
61
+ {
62
+ "name": "maxNodes",
63
+ "type": "number",
64
+ "required": false,
65
+ "description": "Hard cap on captured / structurally rendered nodes. Hit this and the walk\nbails out with `truncated: true`. Default 500 — enough for cards, lists,\nfull forms; cut deliberately for screens with hundreds of repeated rows."
66
+ },
67
+ {
68
+ "name": "minNodeSize",
69
+ "type": "number",
70
+ "required": false,
71
+ "description": "Skip elements smaller than this many CSS pixels on either axis during\ncapture. Default 4. Filters out hairlines / inline padding shims that\ninflate the node count without adding visual signal."
72
+ },
73
+ {
74
+ "name": "persist",
75
+ "type": "boolean",
76
+ "required": false,
77
+ "description": "Persist captured shapes to `localStorage` so first-visit skeletons survive reloads. Default false."
78
+ }
79
+ ],
80
+ "slots": [
81
+ {
82
+ "name": "default",
83
+ "description": "The real content. Rendered when `loading` is false; measured to build the skeleton."
84
+ },
85
+ {
86
+ "name": "fallback",
87
+ "description": "Custom UI to render on a cache miss. Defaults to a single shimmer block."
88
+ }
89
+ ]
90
+ },
91
+ {
92
+ "name": "ASkeletonLayer",
93
+ "source": {
94
+ "module": "@alikhalilll/a-skeleton",
95
+ "symbol": "ASkeletonLayer"
96
+ },
97
+ "props": [
98
+ {
99
+ "name": "animation",
100
+ "type": "SkeletonAnimation",
101
+ "required": false,
102
+ "description": "Animation variant. Default `'shimmer'`."
103
+ },
104
+ {
105
+ "name": "class",
106
+ "type": "ClassValue",
107
+ "required": false,
108
+ "description": "Class on the layer wrapper."
109
+ },
110
+ {
111
+ "name": "shape",
112
+ "type": "CachedShape",
113
+ "required": false,
114
+ "description": "Shape captured by `walkDom` / `useSkeleton`. Renders nothing when undefined."
115
+ }
116
+ ]
117
+ },
118
+ {
119
+ "name": "ASkeletonBlock",
120
+ "source": {
121
+ "module": "@alikhalilll/a-skeleton",
122
+ "symbol": "ASkeletonBlock"
123
+ },
124
+ "props": [
125
+ {
126
+ "name": "animation",
127
+ "type": "SkeletonAnimation",
128
+ "required": false,
129
+ "description": "Animation variant. Default `'shimmer'`."
130
+ },
131
+ {
132
+ "name": "class",
133
+ "type": "ClassValue",
134
+ "required": false,
135
+ "description": "Class on the root (the single block, or the stack wrapper for multi-line text)."
136
+ },
137
+ {
138
+ "name": "h",
139
+ "type": "string | number",
140
+ "required": false,
141
+ "description": "Height as CSS length. Number = pixels."
142
+ },
143
+ {
144
+ "name": "lines",
145
+ "type": "number",
146
+ "required": false,
147
+ "description": "For `type='text'`, render N stacked bars. Last bar is 70% width. Default 1."
148
+ },
149
+ {
150
+ "name": "radius",
151
+ "type": "string | number",
152
+ "required": false,
153
+ "description": "Border radius as CSS length. Number = pixels. For `type='circle'`, this\ndefaults to `'50%'` if not provided."
154
+ },
155
+ {
156
+ "name": "type",
157
+ "type": "ShapeNodeType",
158
+ "required": false,
159
+ "description": "Visual variant. Default `'block'`."
160
+ },
161
+ {
162
+ "name": "w",
163
+ "type": "string | number",
164
+ "required": false,
165
+ "description": "Width as CSS length. Number = pixels. Falls back to whatever the surrounding layout gives."
166
+ }
167
+ ]
168
+ }
169
+ ]
170
+ }
171
+ }
172
+ }