@alikhalilll/a-skeleton 1.0.0 → 1.1.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/README.md +15 -12
- package/dist/index.cjs +28 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/ASkeleton.vue +21 -2
- package/src/composables/useSkeletonCache.ts +35 -6
- package/src/types.ts +6 -3
- package/web-types.json +2 -2
package/README.md
CHANGED
|
@@ -216,17 +216,17 @@ useShapeProbe(() => containerRef.value, {
|
|
|
216
216
|
|
|
217
217
|
## `<ASkeleton>` props
|
|
218
218
|
|
|
219
|
-
| Prop | Type | Default
|
|
220
|
-
| ------------- | -------------------------------- |
|
|
221
|
-
| `loading` | `boolean` | —
|
|
222
|
-
| `cacheKey` | `string` |
|
|
223
|
-
| `maxDepth` | `number` | `6`
|
|
224
|
-
| `maxNodes` | `number` | `500`
|
|
225
|
-
| `minNodeSize` | `number` | `4`
|
|
226
|
-
| `persist` | `boolean` | `false`
|
|
227
|
-
| `animation` | `'shimmer' \| 'pulse' \| 'none'` | `'shimmer'`
|
|
228
|
-
| `fallback` | `'shimmer' \| 'block'` | `'shimmer'`
|
|
229
|
-
| `class` | `HTMLAttributes['class']` | —
|
|
219
|
+
| Prop | Type | Default | Description |
|
|
220
|
+
| ------------- | -------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
221
|
+
| `loading` | `boolean` | — | When `true`, show the skeleton. |
|
|
222
|
+
| `cacheKey` | `string` | auto, per-instance | Identifier for the shape cache. Auto-generated as `"<slot-name>:<useId()>"` so each instance has its own slot. Pass explicitly to share a shape across instances, or when one instance renders different shapes per 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
230
|
|
|
231
231
|
### Slots
|
|
232
232
|
|
|
@@ -255,6 +255,8 @@ setCached('user-card', shape, true); // write + persist
|
|
|
255
255
|
const shape = getCached('user-card', true);
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
+
Persisted entries carry a schema version. Whenever the `ShapeNode` / `CachedShape` shape changes between releases the version is bumped, and `getCached` purges stale payloads on read — so an upgrade can't replay wrong geometry from a previous version's cache.
|
|
259
|
+
|
|
258
260
|
---
|
|
259
261
|
|
|
260
262
|
## Theming
|
|
@@ -305,7 +307,7 @@ The `.light` scope overrides `--ak-skeleton-shimmer` to a brighter value so pola
|
|
|
305
307
|
|
|
306
308
|
Designed for components with hundreds of leaf elements (busy dashboards, long lists, dense forms). Cost is bounded at every layer:
|
|
307
309
|
|
|
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).
|
|
310
|
+
- **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). `<ASkeleton>` logs a one-time `console.warn` per `cacheKey` whenever a capture truncates, so missing nodes surface during development instead of silently replaying a clipped shape.
|
|
309
311
|
- **Min-size filter** — `minNodeSize` (default 4 px) drops hairlines / spacer dots.
|
|
310
312
|
- **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
313
|
- **Batched DOM reads** — `getBoundingClientRect` + `getComputedStyle` happen in one top-down pass with no intervening writes. One layout up front instead of one per element.
|
|
@@ -318,6 +320,7 @@ Designed for components with hundreds of leaf elements (busy dashboards, long li
|
|
|
318
320
|
- 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
321
|
- 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
322
|
- 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.
|
|
323
|
+
- Two `<ASkeleton>` instances rendering the same component get separate caches by default — the auto-generated key includes `useId()`, so the slot is per-instance. Pass an explicit `cacheKey` to share one captured shape across, e.g., a list of identical cards.
|
|
321
324
|
|
|
322
325
|
## License
|
|
323
326
|
|
package/dist/index.cjs
CHANGED
|
@@ -3653,19 +3653,32 @@ function useShapeProbe(getTarget, options) {
|
|
|
3653
3653
|
const memory = /* @__PURE__ */ new Map();
|
|
3654
3654
|
const STORAGE_PREFIX = "a-skeleton:";
|
|
3655
3655
|
/**
|
|
3656
|
+
* Schema version for persisted entries. Bump whenever the `ShapeNode` /
|
|
3657
|
+
* `CachedShape` field set changes so stale localStorage payloads from older
|
|
3658
|
+
* releases get dropped on read instead of rehydrating into a wrong layout.
|
|
3659
|
+
*/
|
|
3660
|
+
const SCHEMA_VERSION = 1;
|
|
3661
|
+
/**
|
|
3656
3662
|
* Lookup a captured shape by key. Reads in-memory first, then `localStorage` if
|
|
3657
3663
|
* `persist` is enabled. SSR-safe — bypasses `window` access on the server.
|
|
3658
3664
|
* Rehydrates pre-computed styles for shapes deserialized from older sessions
|
|
3659
3665
|
* (Object.freeze + style/lineStyles don't survive `JSON.stringify` round-trip).
|
|
3666
|
+
* Drops the entry if it was written by a different schema version.
|
|
3660
3667
|
*/
|
|
3661
3668
|
function getCached(key, persist) {
|
|
3662
3669
|
const hit = memory.get(key);
|
|
3663
3670
|
if (hit) return hit;
|
|
3664
3671
|
if (!persist || typeof window === "undefined") return void 0;
|
|
3665
3672
|
try {
|
|
3666
|
-
const
|
|
3673
|
+
const storageKey = STORAGE_PREFIX + key;
|
|
3674
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
3667
3675
|
if (!raw) return void 0;
|
|
3668
|
-
const
|
|
3676
|
+
const parsed = JSON.parse(raw);
|
|
3677
|
+
if (parsed.v !== SCHEMA_VERSION) {
|
|
3678
|
+
window.localStorage.removeItem(storageKey);
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
const rehydrated = rehydrateShape(parsed);
|
|
3669
3682
|
memory.set(key, rehydrated);
|
|
3670
3683
|
return rehydrated;
|
|
3671
3684
|
} catch {
|
|
@@ -3678,9 +3691,11 @@ function setCached(key, value, persist) {
|
|
|
3678
3691
|
if (!persist || typeof window === "undefined") return;
|
|
3679
3692
|
try {
|
|
3680
3693
|
const lean = {
|
|
3694
|
+
v: SCHEMA_VERSION,
|
|
3681
3695
|
width: value.width,
|
|
3682
3696
|
height: value.height,
|
|
3683
|
-
nodes: leanNodes(value.nodes)
|
|
3697
|
+
nodes: leanNodes(value.nodes),
|
|
3698
|
+
truncated: value.truncated
|
|
3684
3699
|
};
|
|
3685
3700
|
window.localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(lean));
|
|
3686
3701
|
} catch {}
|
|
@@ -3707,7 +3722,7 @@ function clearCached(key) {
|
|
|
3707
3722
|
* path stays allocation-free.
|
|
3708
3723
|
*/
|
|
3709
3724
|
function rehydrateShape(shape) {
|
|
3710
|
-
const nodes = shape.nodes.map((n) =>
|
|
3725
|
+
const nodes = shape.nodes.map((n) => freezeNodeStyles(n));
|
|
3711
3726
|
return Object.freeze({
|
|
3712
3727
|
nodes: Object.freeze(nodes),
|
|
3713
3728
|
width: shape.width,
|
|
@@ -4132,7 +4147,9 @@ const _sfc_main$2 = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4132
4147
|
__expose();
|
|
4133
4148
|
const props = __props;
|
|
4134
4149
|
const slots = (0, vue.useSlots)();
|
|
4135
|
-
const
|
|
4150
|
+
const instanceId = (0, vue.useId)();
|
|
4151
|
+
const resolvedKey = (0, vue.computed)(() => props.cacheKey ?? `${fingerprintSlot(slots.default?.())}:${instanceId}`);
|
|
4152
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
4136
4153
|
const cached = (0, vue.shallowRef)(getCached(resolvedKey.value, props.persist));
|
|
4137
4154
|
(0, vue.watch)(resolvedKey, (key) => {
|
|
4138
4155
|
cached.value = getCached(key, props.persist);
|
|
@@ -4145,6 +4162,10 @@ const _sfc_main$2 = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4145
4162
|
onCapture: (shape) => {
|
|
4146
4163
|
setCached(resolvedKey.value, shape, props.persist);
|
|
4147
4164
|
cached.value = shape;
|
|
4165
|
+
if (shape.truncated && !warnedKeys.has(resolvedKey.value)) {
|
|
4166
|
+
warnedKeys.add(resolvedKey.value);
|
|
4167
|
+
console.warn(`[ASkeleton] Capture truncated at maxNodes=${props.maxNodes} for cacheKey="${resolvedKey.value}". The replayed skeleton will be missing nodes. Raise \`max-nodes\` or mark dense subtrees with \`data-skeleton-stop\` to collapse them into a single block.`);
|
|
4168
|
+
}
|
|
4148
4169
|
}
|
|
4149
4170
|
});
|
|
4150
4171
|
const animationClass = (0, vue.computed)(() => props.animation === "none" ? null : `a-skel-block--anim-${props.animation}`);
|
|
@@ -4166,7 +4187,9 @@ const _sfc_main$2 = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
4166
4187
|
const __returned__ = {
|
|
4167
4188
|
props,
|
|
4168
4189
|
slots,
|
|
4190
|
+
instanceId,
|
|
4169
4191
|
resolvedKey,
|
|
4192
|
+
warnedKeys,
|
|
4170
4193
|
cached,
|
|
4171
4194
|
wrapperRef,
|
|
4172
4195
|
animationClass,
|