@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.
- package/LICENSE +21 -0
- package/README.md +324 -0
- package/dist/index.cjs +4513 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +351 -0
- package/dist/index.d.ts +351 -0
- package/dist/index.js +4501 -0
- package/dist/index.js.map +1 -0
- package/dist/nuxt/index.cjs +30 -0
- package/dist/nuxt/index.cjs.map +1 -0
- package/dist/nuxt/index.d.cts +25 -0
- package/dist/nuxt/index.d.ts +25 -0
- package/dist/nuxt/index.js +30 -0
- package/dist/nuxt/index.js.map +1 -0
- package/dist/resolver/index.cjs +25 -0
- package/dist/resolver/index.cjs.map +1 -0
- package/dist/resolver/index.d.cts +21 -0
- package/dist/resolver/index.d.ts +21 -0
- package/dist/resolver/index.js +25 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/styles.css +35 -0
- package/package.json +120 -0
- package/src/components/ASkeleton.vue +167 -0
- package/src/components/ASkeletonBlock.vue +74 -0
- package/src/components/ASkeletonLayer.vue +52 -0
- package/src/components/StructuralSkeleton.ts +41 -0
- package/src/composables/useShapeProbe.ts +108 -0
- package/src/composables/useSkeleton.ts +112 -0
- package/src/composables/useSkeletonCache.ts +141 -0
- package/src/index.ts +32 -0
- package/src/nuxt/index.ts +47 -0
- package/src/resolver/index.ts +36 -0
- package/src/types.ts +108 -0
- package/src/utils/buildStructuralSkeleton.ts +261 -0
- package/src/utils/fingerprint.ts +42 -0
- package/src/utils/walkDom.ts +226 -0
- package/web-types.json +172 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 alikhalilll
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# @alikhalilll/a-skeleton
|
|
2
|
+
|
|
3
|
+
> Self-generating skeleton loaders for Vue 3 / Nuxt 3+. First paint mirrors the slot's HTML structure; second load replays a pixel-aligned shape captured from the real DOM. Themeable via CSS variables.
|
|
4
|
+
|
|
5
|
+
```vue
|
|
6
|
+
<ASkeleton :loading="isFetching">
|
|
7
|
+
<UserProfileCard :data="user" />
|
|
8
|
+
</ASkeleton>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's the whole API for 90% of cases. For the other 10%, every layer the wrapper uses is a public export — composables, primitive blocks, pure utilities.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @alikhalilll/a-skeleton
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import '@alikhalilll/a-skeleton/styles.css';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Nuxt 3 / 4
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// nuxt.config.ts
|
|
29
|
+
export default defineNuxtConfig({
|
|
30
|
+
modules: ['@alikhalilll/a-skeleton/nuxt'],
|
|
31
|
+
css: ['@alikhalilll/a-skeleton/styles.css'],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Auto-imports `<ASkeleton>`, `<ASkeletonLayer>`, `<ASkeletonBlock>`.
|
|
36
|
+
|
|
37
|
+
### Vue + Vite (unplugin-vue-components)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// vite.config.ts
|
|
41
|
+
import Components from 'unplugin-vue-components/vite';
|
|
42
|
+
import ASkeletonResolver from '@alikhalilll/a-skeleton/resolver';
|
|
43
|
+
|
|
44
|
+
export default { plugins: [Components({ resolvers: [ASkeletonResolver()] })] };
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Public API at a glance
|
|
50
|
+
|
|
51
|
+
| Export | Kind | When to reach for it |
|
|
52
|
+
| ----------------------------------------- | ---------- | -------------------------------------------------------------------------------- |
|
|
53
|
+
| `<ASkeleton>` | Component | The default wrapper. Two-layer flow (structural pass → measured cache). |
|
|
54
|
+
| `<ASkeletonLayer>` | Component | Render a `CachedShape` directly. Use when you don't want the wrapper. |
|
|
55
|
+
| `<ASkeletonBlock>` | Component | Hand-crafted skeleton from primitive blocks. Flow-layout friendly. |
|
|
56
|
+
| `<StructuralSkeleton>` | Component | Render a structural skeleton from any vnode tree (the cache-miss render path). |
|
|
57
|
+
| `useSkeleton()` | Composable | Wire probe + cache + reactivity around your own DOM. Returns a reactive `shape`. |
|
|
58
|
+
| `useShapeProbe()` | Composable | Lower-level. `ResizeObserver` + debounced capture; you manage the cache. |
|
|
59
|
+
| `getCached` / `setCached` / `clearCached` | Function | Imperative cache primitives. Read / write / wipe entries by key. |
|
|
60
|
+
| `walkDom()` | Function | Synchronous one-shot measurement. Returns a `CachedShape`. |
|
|
61
|
+
| `buildStructuralSkeleton()` | Function | Pure: walk a vnode tree, return skeleton vnodes mirroring its layout. |
|
|
62
|
+
| `fingerprintSlot()` | Function | Default `cacheKey` derivation — first non-comment vnode's component name. |
|
|
63
|
+
|
|
64
|
+
Types: `ASkeletonProps`, `ASkeletonLayerProps`, `ASkeletonBlockProps`, `ASkeletonSlots`, `CachedShape`, `ShapeNode`, `ShapeNodeType`, `SkeletonAnimation`, `SkeletonFallback`, `UseSkeletonOptions`, `UseSkeletonReturn`, `ShapeProbeOptions`, `WalkOptions`, `BuildOptions`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Recipes
|
|
69
|
+
|
|
70
|
+
### Recipe 1 — `<ASkeleton>` · the default wrapper
|
|
71
|
+
|
|
72
|
+
Drop it in, pass `loading`, you're done.
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<script setup lang="ts">
|
|
76
|
+
import { ref, computed } from 'vue';
|
|
77
|
+
|
|
78
|
+
const user = ref(null);
|
|
79
|
+
const loading = computed(() => user.value === null);
|
|
80
|
+
|
|
81
|
+
async function load() {
|
|
82
|
+
user.value = await fetch('/api/me').then((r) => r.json());
|
|
83
|
+
}
|
|
84
|
+
load();
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<ASkeleton :loading="loading" cache-key="user-card">
|
|
89
|
+
<!-- Keep structure unconditional; gate per leaf so the structural pass has
|
|
90
|
+
something to mirror during loading. -->
|
|
91
|
+
<div class="flex items-start gap-4 p-4">
|
|
92
|
+
<img v-if="user?.avatar" :src="user.avatar" class="size-16 rounded-full" />
|
|
93
|
+
<div v-else class="size-16 rounded-full" />
|
|
94
|
+
|
|
95
|
+
<div class="flex-1">
|
|
96
|
+
<h3>{{ user?.name }}</h3>
|
|
97
|
+
<p>{{ user?.bio }}</p>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</ASkeleton>
|
|
101
|
+
</template>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Authoring rule** — gate **content**, not **structure**. The structural pass walks whatever the slot actually renders during loading. If you gate the whole template on `v-if="data"`, the walker sees a comment and falls back to a generic shimmer.
|
|
105
|
+
|
|
106
|
+
### Recipe 2 — `<ASkeletonBlock>` · hand-crafted from primitives
|
|
107
|
+
|
|
108
|
+
For custom designs where you'd rather author the skeleton yourself than rely on auto-capture. Flow-layout friendly — composes with flex, grid, stacks.
|
|
109
|
+
|
|
110
|
+
```vue
|
|
111
|
+
<template>
|
|
112
|
+
<div v-if="loading" class="flex items-start gap-4 p-4">
|
|
113
|
+
<ASkeletonBlock type="circle" :w="64" :h="64" />
|
|
114
|
+
<div class="flex-1 space-y-2">
|
|
115
|
+
<ASkeletonBlock type="text" :w="160" :h="18" />
|
|
116
|
+
<ASkeletonBlock type="text" :w="100" :h="12" />
|
|
117
|
+
<ASkeletonBlock type="text" :lines="3" :h="14" class="!mt-3" />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
<UserCard v-else :data="user" />
|
|
121
|
+
</template>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Prop | Type | Default | Notes |
|
|
125
|
+
| ----------- | ------------------------------------------ | ----------- | ------------------------------------------------------------------- |
|
|
126
|
+
| `type` | `'block' \| 'text' \| 'image' \| 'circle'` | `'block'` | `circle` defaults `border-radius: 50%`. |
|
|
127
|
+
| `w` | `number \| string` | — | Width (number = px). |
|
|
128
|
+
| `h` | `number \| string` | — | Height (number = px). |
|
|
129
|
+
| `radius` | `number \| string` | — | Border radius (number = px). |
|
|
130
|
+
| `lines` | `number` | `1` | For `type='text'`, render N stacked bars; last is 70% width. |
|
|
131
|
+
| `animation` | `'shimmer' \| 'pulse' \| 'none'` | `'shimmer'` | Animation variant. |
|
|
132
|
+
| `class` | `HTMLAttributes['class']` | — | Class on the root (single block, or the stack for multi-line text). |
|
|
133
|
+
|
|
134
|
+
### Recipe 3 — `useSkeleton()` + `<ASkeletonLayer>` · DIY orchestration
|
|
135
|
+
|
|
136
|
+
When you want the probe + cache + reactivity but don't want the wrapper. The composable returns a reactive `shape`; `<ASkeletonLayer>` replays it.
|
|
137
|
+
|
|
138
|
+
```vue
|
|
139
|
+
<script setup lang="ts">
|
|
140
|
+
import { computed, ref } from 'vue';
|
|
141
|
+
import { useSkeleton } from '@alikhalilll/a-skeleton';
|
|
142
|
+
|
|
143
|
+
const props = defineProps<{ userId: string }>();
|
|
144
|
+
const user = ref(null);
|
|
145
|
+
const loading = computed(() => user.value === null);
|
|
146
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
147
|
+
|
|
148
|
+
const { shape, captureNow, clear } = useSkeleton({
|
|
149
|
+
cacheKey: `user-card:${props.userId}`,
|
|
150
|
+
// While loading, target is null → no capture. When real content mounts,
|
|
151
|
+
// target returns the wrapper → ResizeObserver + capture.
|
|
152
|
+
target: () => (loading.value ? null : containerRef.value),
|
|
153
|
+
persist: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
fetchUser(props.userId).then((u) => (user.value = u));
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<template>
|
|
160
|
+
<div ref="containerRef">
|
|
161
|
+
<ASkeletonLayer v-if="loading" :shape="shape" />
|
|
162
|
+
<UserCard v-else :data="user" />
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**`useSkeleton` options** — `cacheKey` (required), `target?`, `persist?`, `maxDepth?`, `maxNodes?`, `minSize?`, `resizeDebounceMs?`.
|
|
168
|
+
|
|
169
|
+
**`useSkeleton` returns** — `{ shape: Readonly<Ref<CachedShape | undefined>>, captureNow: () => CachedShape | undefined, clear: () => void }`.
|
|
170
|
+
|
|
171
|
+
**`<ASkeletonLayer>` props** — `shape?: CachedShape`, `animation?: 'shimmer' \| 'pulse' \| 'none'`, `class?`.
|
|
172
|
+
|
|
173
|
+
### Recipe 4 — Pure utilities · build your own flow
|
|
174
|
+
|
|
175
|
+
`walkDom`, `buildStructuralSkeleton`, `fingerprintSlot` are pure functions. Use them when none of the components fit.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import {
|
|
179
|
+
walkDom,
|
|
180
|
+
buildStructuralSkeleton,
|
|
181
|
+
fingerprintSlot,
|
|
182
|
+
setCached,
|
|
183
|
+
getCached,
|
|
184
|
+
} from '@alikhalilll/a-skeleton';
|
|
185
|
+
|
|
186
|
+
// Take a measurement yourself and stash it
|
|
187
|
+
const shape = walkDom(containerEl, { maxDepth: 6, maxNodes: 500, minSize: 4 });
|
|
188
|
+
setCached('user-card', shape, /* persist */ true);
|
|
189
|
+
|
|
190
|
+
// Derive a default cacheKey from a slot
|
|
191
|
+
const key = fingerprintSlot(slots.default?.()); // 'UserCard' or 'div' or 'anonymous'
|
|
192
|
+
|
|
193
|
+
// Render skeleton vnodes for an arbitrary tree (e.g. inside a render-function component)
|
|
194
|
+
const skeletonVNodes = buildStructuralSkeleton(slots.default?.() ?? [], {
|
|
195
|
+
animationClass: 'a-skel-block--anim-shimmer',
|
|
196
|
+
maxNodes: 300,
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Recipe 5 — `useShapeProbe()` · probe without the cache
|
|
201
|
+
|
|
202
|
+
Use when you maintain the cache externally (e.g. Pinia store, server-rendered geometry).
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { useShapeProbe } from '@alikhalilll/a-skeleton';
|
|
206
|
+
|
|
207
|
+
useShapeProbe(() => containerRef.value, {
|
|
208
|
+
maxDepth: 6,
|
|
209
|
+
maxNodes: 500,
|
|
210
|
+
resizeDebounceMs: 200,
|
|
211
|
+
onCapture: (shape) => myStore.saveShape('user-card', shape),
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## `<ASkeleton>` props
|
|
218
|
+
|
|
219
|
+
| Prop | Type | Default | Description |
|
|
220
|
+
| ------------- | -------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
|
|
221
|
+
| `loading` | `boolean` | — | When `true`, show the skeleton. |
|
|
222
|
+
| `cacheKey` | `string` | slot's name | Identifier for the shape cache. Pass explicitly when one component renders different shapes by prop. |
|
|
223
|
+
| `maxDepth` | `number` | `6` | Max recursion depth when capturing shape. |
|
|
224
|
+
| `maxNodes` | `number` | `500` | Hard cap on captured / structural nodes. Walk bails out beyond this with `truncated: true`. |
|
|
225
|
+
| `minNodeSize` | `number` | `4` | Skip elements smaller than this many CSS pixels (either axis) during capture. |
|
|
226
|
+
| `persist` | `boolean` | `false` | Mirror captured shape to `localStorage` so first-visit-after-reload skips the cold-start fallback. |
|
|
227
|
+
| `animation` | `'shimmer' \| 'pulse' \| 'none'` | `'shimmer'` | Animation variant. `prefers-reduced-motion` disables animation automatically. |
|
|
228
|
+
| `fallback` | `'shimmer' \| 'block'` | `'shimmer'` | Default cache-miss UI when no `#fallback` slot is provided. |
|
|
229
|
+
| `class` | `HTMLAttributes['class']` | — | Class on the outer wrapper. |
|
|
230
|
+
|
|
231
|
+
### Slots
|
|
232
|
+
|
|
233
|
+
| Slot | Description |
|
|
234
|
+
| ---------- | ----------------------------------------------------------------------------------- |
|
|
235
|
+
| `default` | The real content. Rendered when `loading` is false; measured to build the skeleton. |
|
|
236
|
+
| `fallback` | Custom UI for cache misses. Defaults to a single full-width shimmer block. |
|
|
237
|
+
|
|
238
|
+
### DOM escape hatches
|
|
239
|
+
|
|
240
|
+
Mark elements during the walk:
|
|
241
|
+
|
|
242
|
+
| Attribute | Effect |
|
|
243
|
+
| ---------------------- | --------------------------------------------------------------- |
|
|
244
|
+
| `data-skeleton-stop` | Stop recursing into this element — render it as a single block. |
|
|
245
|
+
| `data-skeleton-ignore` | Skip this element entirely (no block emitted). |
|
|
246
|
+
|
|
247
|
+
### Cache helpers
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
import { getCached, setCached, clearCached } from '@alikhalilll/a-skeleton';
|
|
251
|
+
|
|
252
|
+
clearCached(); // wipe every shape
|
|
253
|
+
clearCached('user-card'); // wipe one key
|
|
254
|
+
setCached('user-card', shape, true); // write + persist
|
|
255
|
+
const shape = getCached('user-card', true);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Theming
|
|
261
|
+
|
|
262
|
+
Every visual primitive is a CSS variable prefixed `--ak-skeleton-`. Override on `:root`, on a wrapper class (multi-tenant), or inline. Defaults inherit from the shared `@alikhalilll/a-ui-base` token set so the skeleton matches whatever theme the rest of your app uses.
|
|
263
|
+
|
|
264
|
+
```css
|
|
265
|
+
/* Per-tenant override — applies to anything inside .tenant-acme */
|
|
266
|
+
.tenant-acme {
|
|
267
|
+
--ak-skeleton-block: hsl(220 30% 18%);
|
|
268
|
+
--ak-skeleton-shimmer: hsl(220 60% 60% / 0.35);
|
|
269
|
+
--ak-skeleton-radius: 0.5rem;
|
|
270
|
+
--ak-skeleton-duration: 2s;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```vue
|
|
275
|
+
<!-- Or inline, scoped to one tree -->
|
|
276
|
+
<ASkeleton :loading style="--ak-skeleton-block: hotpink; --ak-skeleton-radius: 9999px;">
|
|
277
|
+
<UserCard />
|
|
278
|
+
</ASkeleton>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Tokens
|
|
282
|
+
|
|
283
|
+
| Variable | Default (dark) | Used for |
|
|
284
|
+
| ----------------------------- | ------------------------------------- | --------------------------------------------------------------- |
|
|
285
|
+
| `--ak-skeleton-block` | `hsl(var(--ak-ui-muted) / 0.55)` | Base block colour (top of the vertical gradient). |
|
|
286
|
+
| `--ak-skeleton-block-soft` | `hsl(var(--ak-ui-muted) / 0.32)` | Fade colour (bottom of the vertical gradient). |
|
|
287
|
+
| `--ak-skeleton-shimmer` | `hsl(var(--ak-ui-foreground) / 0.08)` | Moving-highlight colour of the shimmer sweep. |
|
|
288
|
+
| `--ak-skeleton-radius` | `0.375rem` | Default border radius for blocks (text bars use 60% of this). |
|
|
289
|
+
| `--ak-skeleton-duration` | `1.6s` | Animation duration (shimmer + pulse). |
|
|
290
|
+
| `--ak-skeleton-pulse-opacity` | `0.48` | Min opacity at the trough of the pulse cycle. |
|
|
291
|
+
| `--ak-skeleton-shimmer-angle` | `110deg` | Sweep angle of the shimmer pseudo-element (90deg = horizontal). |
|
|
292
|
+
| `--ak-skeleton-ring` | `hsl(var(--ak-ui-foreground) / 0.04)` | 1px inset ring that gives blocks definition without a border. |
|
|
293
|
+
|
|
294
|
+
The `.light` scope overrides `--ak-skeleton-shimmer` to a brighter value so polarity stays correct (lighter sweep over the muted base in both light and dark mode).
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## How the two-layer flow works
|
|
299
|
+
|
|
300
|
+
1. **Cache miss + loading** — `ASkeleton` walks the slot's vnode tree and renders a structural skeleton. Same tags, same `class` strings, atomic leaves (`<img>`, `<button>`) as shimmer blocks, text tags (`<h3>`, `<p>`) as bars. Tailwind utilities like `size-16` and `rounded-full` still apply, so the avatar circle and headline widths look right on the very first paint — without DOM measurement.
|
|
301
|
+
2. **Data arrives** — the real slot renders. A `ResizeObserver` + `requestAnimationFrame` capture pass walks the DOM, freezing per-block `x/y/w/h`, border-radius, and multi-line counts into a `CachedShape` keyed by `cacheKey`.
|
|
302
|
+
3. **Next loading flip** — cache hit. Positioned blocks render absolutely inside a layer sized to the previously measured bounding box. Pixel-aligned to ±1px.
|
|
303
|
+
|
|
304
|
+
## Performance
|
|
305
|
+
|
|
306
|
+
Designed for components with hundreds of leaf elements (busy dashboards, long lists, dense forms). Cost is bounded at every layer:
|
|
307
|
+
|
|
308
|
+
- **Walk budget** — `walkDom` stops emitting after `maxNodes` (default 500) and returns `CachedShape.truncated: true`. A 5000-row table will not lock up the main thread. The structural pass enforces a separate cap (default 300).
|
|
309
|
+
- **Min-size filter** — `minNodeSize` (default 4 px) drops hairlines / spacer dots.
|
|
310
|
+
- **Allocation-free render** — captured `ShapeNode`s carry frozen, pre-computed `style` and `lineStyles` objects. Per-type block class strings are pre-joined once per animation value. The cache-hit render loop reads them directly with no per-node function calls.
|
|
311
|
+
- **Batched DOM reads** — `getBoundingClientRect` + `getComputedStyle` happen in one top-down pass with no intervening writes. One layout up front instead of one per element.
|
|
312
|
+
- **Debounced re-capture** — initial measurement via `requestAnimationFrame`. Subsequent `ResizeObserver` callbacks debounced 150 ms so a drag-resize doesn't trigger a re-walk per frame.
|
|
313
|
+
- **CSS containment** — `.a-skeleton__layer` uses `contain: layout style paint` + `content-visibility: auto` so off-screen blocks skip rendering; `.a-skel-block` inside uses `contain: strict` so per-block shimmer animations never trigger sibling layout/paint.
|
|
314
|
+
- **Composited shimmer** — only `transform: translateX(...)` changes each frame on the shimmer pseudo-element, with `will-change: transform` to lift each block to its own compositor layer up front.
|
|
315
|
+
|
|
316
|
+
## Limitations
|
|
317
|
+
|
|
318
|
+
- The structural pass mirrors what the slot's template actually renders during loading. Gate everything on `v-if="data"` and the walker sees only a comment — fall back to the generic shimmer.
|
|
319
|
+
- Captured shapes are snapshots. `ResizeObserver` re-measures when the wrapper resizes (debounced 150 ms). If you resize _during_ a skeleton render, the cached shape replays unchanged.
|
|
320
|
+
- SSR: the structural skeleton works during SSR (no `window` access needed). Pixel-aligned positioned blocks require a captured shape, which only happens client-side after mount.
|
|
321
|
+
|
|
322
|
+
## License
|
|
323
|
+
|
|
324
|
+
MIT © alikhalilll
|