@alikhalilll/a-skeleton 1.1.0 → 1.2.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.
- package/.media/hero.png +0 -0
- package/.media/hero.svg +232 -0
- package/README.md +458 -172
- package/dist/index.cjs +3685 -840
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +527 -40
- package/dist/index.d.ts +527 -40
- package/dist/index.js +3666 -842
- package/dist/index.js.map +1 -1
- package/dist/nuxt/index.cjs +16 -1
- package/dist/nuxt/index.cjs.map +1 -1
- package/dist/nuxt/index.js +16 -1
- package/dist/nuxt/index.js.map +1 -1
- package/dist/resolver/index.cjs +16 -1
- package/dist/resolver/index.cjs.map +1 -1
- package/dist/resolver/index.js +16 -1
- package/dist/resolver/index.js.map +1 -1
- package/dist/styles.css +56 -11
- package/package.json +8 -2
- package/src/components/ASkeleton.vue +212 -113
- package/src/components/ASkeletonClone.vue +106 -0
- package/src/components/ASkeletonLayer.vue +20 -32
- package/src/components/CloneNode.ts +161 -0
- package/src/components/StructuralLayerNode.ts +157 -0
- package/src/components/icons.ts +45 -0
- package/src/components/variants/ASkeletonArticle.vue +33 -0
- package/src/components/variants/ASkeletonAvatar.vue +42 -0
- package/src/components/variants/ASkeletonButton.vue +37 -0
- package/src/components/variants/ASkeletonCard.vue +47 -0
- package/src/components/variants/ASkeletonChart.vue +56 -0
- package/src/components/variants/ASkeletonChip.vue +32 -0
- package/src/components/variants/ASkeletonDivider.vue +26 -0
- package/src/components/variants/ASkeletonForm.vue +32 -0
- package/src/components/variants/ASkeletonHeading.vue +47 -0
- package/src/components/variants/ASkeletonImage.vue +57 -0
- package/src/components/variants/ASkeletonInput.vue +33 -0
- package/src/components/variants/ASkeletonListItem.vue +40 -0
- package/src/components/variants/ASkeletonTable.vue +49 -0
- package/src/components/variants/ASkeletonText.vue +49 -0
- package/src/components/variants/ASkeletonVideo.vue +55 -0
- package/src/composables/useShapeProbe.ts +33 -9
- package/src/composables/useSkeleton.ts +33 -21
- package/src/composables/useSkeletonCache.ts +251 -22
- package/src/index.ts +48 -2
- package/src/nuxt/index.ts +16 -0
- package/src/resolver/index.ts +16 -0
- package/src/types.ts +118 -2
- package/src/utils/buildStructuralSkeleton.ts +400 -103
- package/src/utils/captureStyles.ts +378 -0
- package/src/utils/domRead.ts +143 -0
- package/src/utils/walkDom.ts +261 -16
- package/src/utils/walkStructural.ts +418 -0
- package/web-types.json +9 -3
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `walkStructural` — tree-shaped DOM capture for Recipe 3
|
|
3
|
+
* (`useSkeleton()` + `<ASkeletonLayer>`).
|
|
4
|
+
*
|
|
5
|
+
* Walks the rendered DOM top-down once. Every element is classified as either:
|
|
6
|
+
*
|
|
7
|
+
* - **container** — has element children. Output preserves its tag + original
|
|
8
|
+
* `class` + a comprehensive capture of layout + visual CSS. Children
|
|
9
|
+
* recurse via the same walker. The result *flows* through the consumer's
|
|
10
|
+
* own layout rules instead of being absolutely positioned to fixed
|
|
11
|
+
* coordinates, so the skeleton stays correct when the viewport / parent
|
|
12
|
+
* reflows.
|
|
13
|
+
* - **leaf-{block, text, image, media}** — atomic or childless element.
|
|
14
|
+
* Output is a `<div class="a-skel">` with captured `width`, `height`,
|
|
15
|
+
* plus the **same** layout + visual capture used for containers. No
|
|
16
|
+
* `position: absolute` — the leaf sits inside whatever space its parent
|
|
17
|
+
* reserves for it. Original class is preserved so utility-first CSS
|
|
18
|
+
* (`mt-4`, `flex-1`, `inline-block`, …) survives.
|
|
19
|
+
*
|
|
20
|
+
* Generic property set — containers and leaves use one shared list
|
|
21
|
+
* (`LAYOUT_PROPS` + `VISUAL_PROPS`). This mirrors clone-mode's design: every
|
|
22
|
+
* captured node carries the same layout context, so any element type (flex
|
|
23
|
+
* item, inline-block, positioned overlay, sticky bar) renders correctly
|
|
24
|
+
* without bespoke per-kind logic. The only leaf-specific transform is
|
|
25
|
+
* normalising `display: inline` → `inline-block`, since width / height don't
|
|
26
|
+
* apply to inline boxes.
|
|
27
|
+
*
|
|
28
|
+
* Performance: all `getBoundingClientRect()` + `getComputedStyle()` reads
|
|
29
|
+
* happen in a single top-down pass with no intervening writes, so the browser
|
|
30
|
+
* does one layout up front and serves cached values from then on (no layout
|
|
31
|
+
* thrashing). Same `maxDepth` / `maxNodes` / `minSize` budget as `walkDom` and
|
|
32
|
+
* `captureSnapshot`.
|
|
33
|
+
*/
|
|
34
|
+
import type { CSSProperties } from 'vue';
|
|
35
|
+
import type {
|
|
36
|
+
ContainerNode,
|
|
37
|
+
LeafKind,
|
|
38
|
+
LeafNode,
|
|
39
|
+
StructuralNode,
|
|
40
|
+
StructuralShape,
|
|
41
|
+
StructuralTextLineRect,
|
|
42
|
+
} from '../types';
|
|
43
|
+
import {
|
|
44
|
+
captureTextLines,
|
|
45
|
+
collectVisibleChildren,
|
|
46
|
+
hasDirectText,
|
|
47
|
+
readComputedStyles,
|
|
48
|
+
} from './domRead';
|
|
49
|
+
|
|
50
|
+
export interface WalkStructuralOptions {
|
|
51
|
+
/** Max recursion depth. Default 12. */
|
|
52
|
+
maxDepth?: number;
|
|
53
|
+
/** Hard cap on captured nodes. Default 500. */
|
|
54
|
+
maxNodes?: number;
|
|
55
|
+
/** Skip elements smaller than this many CSS pixels (either axis). Default 4. */
|
|
56
|
+
minSize?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const DEFAULT_MAX_DEPTH = 12;
|
|
60
|
+
const DEFAULT_MAX_NODES = 500;
|
|
61
|
+
const DEFAULT_MIN_SIZE = 4;
|
|
62
|
+
|
|
63
|
+
/** Tags treated as atomic leaves — never recursed into, rendered as one block. */
|
|
64
|
+
const ATOMIC_TAGS: ReadonlySet<string> = new Set([
|
|
65
|
+
'img',
|
|
66
|
+
'picture',
|
|
67
|
+
'svg',
|
|
68
|
+
'canvas',
|
|
69
|
+
'video',
|
|
70
|
+
'audio',
|
|
71
|
+
'input',
|
|
72
|
+
'textarea',
|
|
73
|
+
'select',
|
|
74
|
+
'button',
|
|
75
|
+
'progress',
|
|
76
|
+
'meter',
|
|
77
|
+
'hr',
|
|
78
|
+
'iframe',
|
|
79
|
+
'object',
|
|
80
|
+
'embed',
|
|
81
|
+
'br',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Tags whose content is conventionally text. When the author writes
|
|
86
|
+
* `<h3>{{ data?.name }}</h3>` and `data` is null during loading, the
|
|
87
|
+
* interpolation is empty and `hasDirectText` returns false — without this
|
|
88
|
+
* fallback the element classifies as `leaf-block` (a generic shimmer rect)
|
|
89
|
+
* instead of `leaf-text` (a shimmer text bar at the element's natural
|
|
90
|
+
* rendered dimensions).
|
|
91
|
+
*/
|
|
92
|
+
const TEXT_OWNERS: ReadonlySet<string> = new Set([
|
|
93
|
+
'h1',
|
|
94
|
+
'h2',
|
|
95
|
+
'h3',
|
|
96
|
+
'h4',
|
|
97
|
+
'h5',
|
|
98
|
+
'h6',
|
|
99
|
+
'p',
|
|
100
|
+
'span',
|
|
101
|
+
'a',
|
|
102
|
+
'em',
|
|
103
|
+
'strong',
|
|
104
|
+
'small',
|
|
105
|
+
'code',
|
|
106
|
+
'b',
|
|
107
|
+
'i',
|
|
108
|
+
'mark',
|
|
109
|
+
'label',
|
|
110
|
+
'caption',
|
|
111
|
+
'time',
|
|
112
|
+
'dt',
|
|
113
|
+
'dd',
|
|
114
|
+
'li',
|
|
115
|
+
'th',
|
|
116
|
+
'td',
|
|
117
|
+
'figcaption',
|
|
118
|
+
'blockquote',
|
|
119
|
+
'cite',
|
|
120
|
+
'q',
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Layout-affecting CSS captured on **every** node. Includes display + box
|
|
125
|
+
* model + flex + grid + positioning so any element type renders correctly
|
|
126
|
+
* regardless of its role in the parent's layout (flex item, inline-block,
|
|
127
|
+
* absolutely-positioned overlay, sticky bar, grid cell).
|
|
128
|
+
*
|
|
129
|
+
* Width / height are intentionally NOT here — containers get sized by their
|
|
130
|
+
* children; leaves get explicit width / height from their `getBoundingClientRect`
|
|
131
|
+
* in `emitLeaf` below.
|
|
132
|
+
*/
|
|
133
|
+
const LAYOUT_PROPS: ReadonlyArray<string> = [
|
|
134
|
+
/* Box model & flow. */
|
|
135
|
+
'display',
|
|
136
|
+
'position',
|
|
137
|
+
'top',
|
|
138
|
+
'right',
|
|
139
|
+
'bottom',
|
|
140
|
+
'left',
|
|
141
|
+
'z-index',
|
|
142
|
+
'vertical-align',
|
|
143
|
+
'float',
|
|
144
|
+
'clear',
|
|
145
|
+
'box-sizing',
|
|
146
|
+
|
|
147
|
+
/* Margins (outer-box spacing — survives the structural layout). */
|
|
148
|
+
'margin-top',
|
|
149
|
+
'margin-right',
|
|
150
|
+
'margin-bottom',
|
|
151
|
+
'margin-left',
|
|
152
|
+
|
|
153
|
+
/* Padding (inner inset — matters for containers carrying their own surface). */
|
|
154
|
+
'padding-top',
|
|
155
|
+
'padding-right',
|
|
156
|
+
'padding-bottom',
|
|
157
|
+
'padding-left',
|
|
158
|
+
|
|
159
|
+
/* Flex — both as a container AND as an item (align-self, justify-self, flex). */
|
|
160
|
+
'flex',
|
|
161
|
+
'flex-direction',
|
|
162
|
+
'flex-wrap',
|
|
163
|
+
'flex-grow',
|
|
164
|
+
'flex-shrink',
|
|
165
|
+
'flex-basis',
|
|
166
|
+
'justify-content',
|
|
167
|
+
'align-items',
|
|
168
|
+
'align-content',
|
|
169
|
+
'align-self',
|
|
170
|
+
'justify-self',
|
|
171
|
+
'gap',
|
|
172
|
+
'row-gap',
|
|
173
|
+
'column-gap',
|
|
174
|
+
'order',
|
|
175
|
+
|
|
176
|
+
/* Grid — both as a container AND as a cell. */
|
|
177
|
+
'grid-template-columns',
|
|
178
|
+
'grid-template-rows',
|
|
179
|
+
'grid-template-areas',
|
|
180
|
+
'grid-auto-flow',
|
|
181
|
+
'grid-auto-columns',
|
|
182
|
+
'grid-auto-rows',
|
|
183
|
+
'grid-column',
|
|
184
|
+
'grid-row',
|
|
185
|
+
'grid-area',
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Visual signals captured on **every** node. These give each rendered
|
|
190
|
+
* placeholder the same surface identity the real element had — fill,
|
|
191
|
+
* outline, elevation, transparency, transforms.
|
|
192
|
+
*/
|
|
193
|
+
const VISUAL_PROPS: ReadonlyArray<string> = [
|
|
194
|
+
/* Borders per edge — non-uniform borders (e.g. `border-b`) survive. */
|
|
195
|
+
'border-top-width',
|
|
196
|
+
'border-right-width',
|
|
197
|
+
'border-bottom-width',
|
|
198
|
+
'border-left-width',
|
|
199
|
+
'border-top-style',
|
|
200
|
+
'border-right-style',
|
|
201
|
+
'border-bottom-style',
|
|
202
|
+
'border-left-style',
|
|
203
|
+
'border-top-color',
|
|
204
|
+
'border-right-color',
|
|
205
|
+
'border-bottom-color',
|
|
206
|
+
'border-left-color',
|
|
207
|
+
'border-top-left-radius',
|
|
208
|
+
'border-top-right-radius',
|
|
209
|
+
'border-bottom-right-radius',
|
|
210
|
+
'border-bottom-left-radius',
|
|
211
|
+
|
|
212
|
+
/* Background. */
|
|
213
|
+
'background-color',
|
|
214
|
+
'background-image',
|
|
215
|
+
'background-position',
|
|
216
|
+
'background-size',
|
|
217
|
+
'background-repeat',
|
|
218
|
+
'background-origin',
|
|
219
|
+
'background-clip',
|
|
220
|
+
|
|
221
|
+
/* Effects. */
|
|
222
|
+
'box-shadow',
|
|
223
|
+
'opacity',
|
|
224
|
+
'filter',
|
|
225
|
+
'backdrop-filter',
|
|
226
|
+
'transform',
|
|
227
|
+
'transform-origin',
|
|
228
|
+
'mix-blend-mode',
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
const ALL_PROPS: ReadonlyArray<string> = [...LAYOUT_PROPS, ...VISUAL_PROPS];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Walk `root`'s descendants and produce a frozen tree-shaped capture.
|
|
235
|
+
* Direct children of `root` become top-level `nodes`; recursive children
|
|
236
|
+
* live on their respective `ContainerNode`s.
|
|
237
|
+
*/
|
|
238
|
+
export function walkStructural(
|
|
239
|
+
root: HTMLElement,
|
|
240
|
+
options: WalkStructuralOptions = {}
|
|
241
|
+
): StructuralShape {
|
|
242
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
243
|
+
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
244
|
+
const minSize = options.minSize ?? DEFAULT_MIN_SIZE;
|
|
245
|
+
|
|
246
|
+
const rootRect = root.getBoundingClientRect();
|
|
247
|
+
const state = { count: 0, truncated: false };
|
|
248
|
+
const ctx: InternalCtx = { maxDepth, maxNodes, minSize, state };
|
|
249
|
+
|
|
250
|
+
const nodes: StructuralNode[] = [];
|
|
251
|
+
for (let i = 0; i < root.children.length; i++) {
|
|
252
|
+
if (state.count >= maxNodes) {
|
|
253
|
+
state.truncated = true;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
const node = capture(root.children[i] as HTMLElement, rootRect, 1, ctx);
|
|
257
|
+
if (node) nodes.push(node);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
width: Math.round(rootRect.width),
|
|
262
|
+
height: Math.round(rootRect.height),
|
|
263
|
+
nodes: Object.freeze(nodes),
|
|
264
|
+
truncated: state.truncated,
|
|
265
|
+
capturedAt: Date.now(),
|
|
266
|
+
v: 3,
|
|
267
|
+
}) as StructuralShape;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
interface InternalCtx {
|
|
271
|
+
maxDepth: number;
|
|
272
|
+
maxNodes: number;
|
|
273
|
+
minSize: number;
|
|
274
|
+
state: { count: number; truncated: boolean };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function capture(
|
|
278
|
+
el: HTMLElement,
|
|
279
|
+
origin: DOMRect,
|
|
280
|
+
depth: number,
|
|
281
|
+
ctx: InternalCtx
|
|
282
|
+
): StructuralNode | null {
|
|
283
|
+
if (ctx.state.count >= ctx.maxNodes) {
|
|
284
|
+
ctx.state.truncated = true;
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
if (el.dataset?.skeletonIgnore !== undefined) return null;
|
|
288
|
+
|
|
289
|
+
const cs = window.getComputedStyle(el);
|
|
290
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') return null;
|
|
291
|
+
const opacity = parseFloat(cs.opacity);
|
|
292
|
+
if (Number.isFinite(opacity) && opacity === 0) return null;
|
|
293
|
+
|
|
294
|
+
const rect = el.getBoundingClientRect();
|
|
295
|
+
const tag = el.tagName.toLowerCase();
|
|
296
|
+
const isTextOwner = TEXT_OWNERS.has(tag);
|
|
297
|
+
|
|
298
|
+
/* Text-owner tags can have zero height when their interpolation is empty
|
|
299
|
+
* (`<h3>{{ data?.name }}</h3>` with null data). For those, synthesize a
|
|
300
|
+
* height from line-height / font-size so the heading still shimmers at its
|
|
301
|
+
* natural rendered height instead of being filtered as sub-minSize. */
|
|
302
|
+
let effectiveHeight = rect.height;
|
|
303
|
+
if (isTextOwner && effectiveHeight < ctx.minSize) {
|
|
304
|
+
const lh = parseFloat(cs.lineHeight);
|
|
305
|
+
const fs = parseFloat(cs.fontSize);
|
|
306
|
+
if (Number.isFinite(lh) && lh > 0) effectiveHeight = lh;
|
|
307
|
+
else if (Number.isFinite(fs) && fs > 0) effectiveHeight = fs * 1.4;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (rect.width < ctx.minSize || effectiveHeight < ctx.minSize) return null;
|
|
311
|
+
|
|
312
|
+
const hasStop = el.dataset?.skeletonStop !== undefined;
|
|
313
|
+
const isAtomic = ATOMIC_TAGS.has(tag);
|
|
314
|
+
const reachedDepth = depth >= ctx.maxDepth;
|
|
315
|
+
const childrenEls = collectVisibleChildren(el);
|
|
316
|
+
|
|
317
|
+
/* Classification — order matters: stop > atomic > childless > container. */
|
|
318
|
+
if (hasStop || isAtomic || reachedDepth || childrenEls.length === 0) {
|
|
319
|
+
return emitLeaf(el, tag, cs, rect, effectiveHeight, isAtomic, ctx);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Container path — recurse into children first; if every child was
|
|
323
|
+
* filtered (sub-minSize, hidden, …) we demote this node to a leaf so the
|
|
324
|
+
* container's own visual surface still renders. */
|
|
325
|
+
const children: StructuralNode[] = [];
|
|
326
|
+
for (const child of childrenEls) {
|
|
327
|
+
if (ctx.state.count >= ctx.maxNodes) {
|
|
328
|
+
ctx.state.truncated = true;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
const node = capture(child, origin, depth + 1, ctx);
|
|
332
|
+
if (node) children.push(node);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (children.length === 0) {
|
|
336
|
+
return emitLeaf(el, tag, cs, rect, effectiveHeight, /* isAtomic */ false, ctx);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
ctx.state.count++;
|
|
340
|
+
const node: ContainerNode = {
|
|
341
|
+
kind: 'container',
|
|
342
|
+
tag,
|
|
343
|
+
className: el.getAttribute('class') ?? '',
|
|
344
|
+
style: readComputedStyles(cs, ALL_PROPS),
|
|
345
|
+
children: Object.freeze(children),
|
|
346
|
+
};
|
|
347
|
+
return Object.freeze(node);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function emitLeaf(
|
|
351
|
+
el: HTMLElement,
|
|
352
|
+
tag: string,
|
|
353
|
+
cs: CSSStyleDeclaration,
|
|
354
|
+
rect: DOMRect,
|
|
355
|
+
effectiveHeight: number,
|
|
356
|
+
isAtomic: boolean,
|
|
357
|
+
ctx: InternalCtx
|
|
358
|
+
): LeafNode | null {
|
|
359
|
+
const w = Math.round(rect.width);
|
|
360
|
+
const h = Math.round(effectiveHeight);
|
|
361
|
+
|
|
362
|
+
const leafKind = classifyLeaf(tag, el, isAtomic);
|
|
363
|
+
|
|
364
|
+
/* Same comprehensive capture used for containers, plus width/height inlined
|
|
365
|
+
* from the rect so the placeholder claims the right space in its parent's
|
|
366
|
+
* layout. Captured `display: inline` is upgraded to `inline-block` — inline
|
|
367
|
+
* boxes ignore width/height, and we need both to take effect on the div
|
|
368
|
+
* placeholder. */
|
|
369
|
+
const captured: Record<string, string> = { ...readComputedStyles(cs, ALL_PROPS) };
|
|
370
|
+
if (captured.display === 'inline') captured.display = 'inline-block';
|
|
371
|
+
|
|
372
|
+
const style: CSSProperties = {
|
|
373
|
+
...captured,
|
|
374
|
+
width: `${w}px`,
|
|
375
|
+
height: `${h}px`,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
let textLines: ReadonlyArray<StructuralTextLineRect> | undefined;
|
|
379
|
+
if (leafKind === 'text') {
|
|
380
|
+
const rawLines = captureTextLines(el, rect);
|
|
381
|
+
if (rawLines && rawLines.length > 0) {
|
|
382
|
+
/* `captureTextLines` returns origin-relative rects — here the origin
|
|
383
|
+
* *is* the leaf box, so each rect's coordinates are already box-local.
|
|
384
|
+
* Clamp to non-negative so sub-pixel rounding can't sneak a negative
|
|
385
|
+
* `left` into the rendered style. */
|
|
386
|
+
textLines = Object.freeze(
|
|
387
|
+
rawLines.map((r) => ({
|
|
388
|
+
x: Math.max(0, r.x),
|
|
389
|
+
y: Math.max(0, r.y),
|
|
390
|
+
w: r.w,
|
|
391
|
+
h: r.h,
|
|
392
|
+
}))
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
ctx.state.count++;
|
|
398
|
+
const node: LeafNode = {
|
|
399
|
+
kind: 'leaf',
|
|
400
|
+
leafKind,
|
|
401
|
+
className: el.getAttribute('class') ?? '',
|
|
402
|
+
style: Object.freeze(style),
|
|
403
|
+
textLines,
|
|
404
|
+
};
|
|
405
|
+
return Object.freeze(node);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function classifyLeaf(tag: string, el: HTMLElement, isAtomic: boolean): LeafKind {
|
|
409
|
+
if (tag === 'img' || tag === 'picture') return 'image';
|
|
410
|
+
if (tag === 'video') return 'image'; /* play-icon decorated via the renderer */
|
|
411
|
+
if (isAtomic) return 'media';
|
|
412
|
+
if (hasDirectText(el)) return 'text';
|
|
413
|
+
/* Empty text-owner (`<p>{{ data?.price }}</p>` with null data) — classify
|
|
414
|
+
* as text so the renderer falls back to a full-box bar instead of a
|
|
415
|
+
* generic shimmer block. */
|
|
416
|
+
if (TEXT_OWNERS.has(tag)) return 'text';
|
|
417
|
+
return 'block';
|
|
418
|
+
}
|
package/web-types.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/web-types",
|
|
3
3
|
"name": "@alikhalilll/a-skeleton",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"js-types-syntax": "typescript",
|
|
6
6
|
"description-markup": "markdown",
|
|
7
7
|
"framework": "vue",
|
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
"required": false,
|
|
71
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
72
|
},
|
|
73
|
+
{
|
|
74
|
+
"name": "mode",
|
|
75
|
+
"type": "ASkeletonMode",
|
|
76
|
+
"required": false,
|
|
77
|
+
"description": "Rendering strategy. Default `'mirror'`.\n - `'mirror'`: walks the slot's vnode tree and preserves every element\n with its real class / inline style. Pure-Vue, SSR-safe, no DOM read.\n - `'clone'`: mounts the slot off-screen once, takes a comprehensive\n `getComputedStyle()` snapshot of every leaf, then replays the\n snapshot as positioned divs carrying every captured CSS property.\n Pixel-identical to the real render, regardless of styling system\n (Tailwind, CSS-in-JS, DaisyUI, computed inline styles). Requires\n a DOM (client-side only)."
|
|
78
|
+
},
|
|
73
79
|
{
|
|
74
80
|
"name": "persist",
|
|
75
81
|
"type": "boolean",
|
|
@@ -109,9 +115,9 @@
|
|
|
109
115
|
},
|
|
110
116
|
{
|
|
111
117
|
"name": "shape",
|
|
112
|
-
"type": "
|
|
118
|
+
"type": "StructuralShape",
|
|
113
119
|
"required": false,
|
|
114
|
-
"description": "
|
|
120
|
+
"description": "Structural shape captured by `useSkeleton()` (Recipe 3) or `walkStructural()`\ndirectly. Renders nothing when `undefined`. The layer drops into the\nconsumer's container as a transparent shell — captured containers preserve\ntheir tag/class/layout so the skeleton flows naturally inside the\nsurrounding layout rather than being absolutely positioned."
|
|
115
121
|
}
|
|
116
122
|
]
|
|
117
123
|
},
|