@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 CHANGED
@@ -216,17 +216,17 @@ useShapeProbe(() => containerRef.value, {
216
216
 
217
217
  ## `<ASkeleton>` props
218
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. |
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 raw = window.localStorage.getItem(STORAGE_PREFIX + key);
3673
+ const storageKey = STORAGE_PREFIX + key;
3674
+ const raw = window.localStorage.getItem(storageKey);
3667
3675
  if (!raw) return void 0;
3668
- const rehydrated = rehydrateShape(JSON.parse(raw));
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) => n.style ? n : freezeNodeStyles(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 resolvedKey = (0, vue.computed)(() => props.cacheKey ?? fingerprintSlot(slots.default?.()));
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,