@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/dist/styles.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}:root,.light{--ak-ui-background:0 0% 100%;--ak-ui-foreground:240 10% 3.9%;--ak-ui-card:0 0% 100%;--ak-ui-card-foreground:240 10% 3.9%;--ak-ui-popover:0 0% 100%;--ak-ui-popover-foreground:240 10% 3.9%;--ak-ui-primary:240 5.9% 10%;--ak-ui-primary-foreground:0 0% 98%;--ak-ui-secondary:240 4.8% 95.9%;--ak-ui-secondary-foreground:240 5.9% 10%;--ak-ui-muted:240 4.8% 95.9%;--ak-ui-muted-foreground:240 3.8% 46.1%;--ak-ui-accent:240 4.8% 95.9%;--ak-ui-accent-foreground:240 5.9% 10%;--ak-ui-destructive:0 84.2% 60.2%;--ak-ui-destructive-foreground:0 0% 98%;--ak-ui-border:240 5.9% 90%;--ak-ui-input:240 5.9% 90%;--ak-ui-ring:240 5.9% 10%;--ak-ui-radius:.5rem}.dark{--ak-ui-background:240 10% 3.9%;--ak-ui-foreground:0 0% 98%;--ak-ui-card:240 10% 3.9%;--ak-ui-card-foreground:0 0% 98%;--ak-ui-popover:240 10% 3.9%;--ak-ui-popover-foreground:0 0% 98%;--ak-ui-primary:0 0% 98%;--ak-ui-primary-foreground:240 5.9% 10%;--ak-ui-secondary:240 3.7% 15.9%;--ak-ui-secondary-foreground:0 0% 98%;--ak-ui-muted:240 3.7% 15.9%;--ak-ui-muted-foreground:240 5% 64.9%;--ak-ui-accent:240 3.7% 15.9%;--ak-ui-accent-foreground:0 0% 98%;--ak-ui-destructive:0 62.8% 30.6%;--ak-ui-destructive-foreground:0 0% 98%;--ak-ui-border:240 3.7% 15.9%;--ak-ui-input:240 3.7% 15.9%;--ak-ui-ring:240 4.9% 83.9%}:root,:host{--spacing:.25rem}.relative{position:relative}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.\!mt-3{margin-top:calc(var(--spacing) * 3)!important}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.items-start{align-items:flex-start}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.p-4{padding:calc(var(--spacing) * 4)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}:root{--ak-skeleton-block:hsl(var(--ak-ui-muted) / .55);--ak-skeleton-block-soft:hsl(var(--ak-ui-muted) / .32);--ak-skeleton-shimmer:hsl(var(--ak-ui-foreground) / .08);--ak-skeleton-radius:.375rem;--ak-skeleton-duration:1.6s;--ak-skeleton-pulse-opacity:.48;--ak-skeleton-shimmer-angle:110deg;--ak-skeleton-ring:hsl(var(--ak-ui-foreground) / .04)}.light{--ak-skeleton-shimmer:#ffffffd9;--ak-skeleton-ring:#0000000d}.a-skel-block{background-image:linear-gradient(180deg, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 100%);background-color:var(--ak-skeleton-block);border-radius:var(--ak-skeleton-radius);box-shadow:inset 0 0 0 1px var(--ak-skeleton-ring);isolation:isolate;position:relative;overflow:hidden}.a-skel-block--anim-shimmer:after{content:"";background-image:linear-gradient(var(--ak-skeleton-shimmer-angle), transparent 30%, var(--ak-skeleton-shimmer) 50%, transparent 70%);will-change:transform;animation:a-skel-shimmer var(--ak-skeleton-duration) cubic-bezier(.42, 0, .58, 1) infinite;z-index:1;position:absolute;inset:0 -25%;transform:translate(-110%)}.a-skel-block--anim-pulse{animation:a-skel-pulse var(--ak-skeleton-duration) cubic-bezier(.42, 0, .58, 1) infinite;will-change:opacity}@keyframes a-skel-shimmer{0%{transform:translate(-110%)}to{transform:translate(110%)}}@keyframes a-skel-pulse{0%,to{opacity:1}50%{opacity:var(--ak-skeleton-pulse-opacity)}}@media (prefers-reduced-motion:reduce){.a-skel-block--anim-shimmer:after,.a-skel-block--anim-pulse{will-change:auto;animation:none}.a-skel-block--anim-shimmer:after{opacity:.5;transform:translate(0)}}.a-skeleton__layer{contain:layout style paint;content-visibility:auto;position:relative}.a-skeleton__layer>.a-skel-block{contain:strict;position:absolute}.a-skel-block--text{border-radius:calc(var(--ak-skeleton-radius) * .6)}.a-skel-block--circle{background-image:radial-gradient(circle at 35% 30%, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 100%)}.a-skel-block--image{background-image:linear-gradient(160deg, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 60%, var(--ak-skeleton-block) 100%)}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}:root,.light{--ak-ui-background:0 0% 100%;--ak-ui-foreground:240 10% 3.9%;--ak-ui-card:0 0% 100%;--ak-ui-card-foreground:240 10% 3.9%;--ak-ui-popover:0 0% 100%;--ak-ui-popover-foreground:240 10% 3.9%;--ak-ui-primary:240 5.9% 10%;--ak-ui-primary-foreground:0 0% 98%;--ak-ui-secondary:240 4.8% 95.9%;--ak-ui-secondary-foreground:240 5.9% 10%;--ak-ui-muted:240 4.8% 95.9%;--ak-ui-muted-foreground:240 3.8% 46.1%;--ak-ui-accent:240 4.8% 95.9%;--ak-ui-accent-foreground:240 5.9% 10%;--ak-ui-destructive:0 84.2% 60.2%;--ak-ui-destructive-foreground:0 0% 98%;--ak-ui-border:240 5.9% 90%;--ak-ui-input:240 5.9% 90%;--ak-ui-ring:240 5.9% 10%;--ak-ui-radius:.5rem}.dark{--ak-ui-background:240 10% 3.9%;--ak-ui-foreground:0 0% 98%;--ak-ui-card:240 10% 3.9%;--ak-ui-card-foreground:0 0% 98%;--ak-ui-popover:240 10% 3.9%;--ak-ui-popover-foreground:0 0% 98%;--ak-ui-primary:0 0% 98%;--ak-ui-primary-foreground:240 5.9% 10%;--ak-ui-secondary:240 3.7% 15.9%;--ak-ui-secondary-foreground:0 0% 98%;--ak-ui-muted:240 3.7% 15.9%;--ak-ui-muted-foreground:240 5% 64.9%;--ak-ui-accent:240 3.7% 15.9%;--ak-ui-accent-foreground:0 0% 98%;--ak-ui-destructive:0 62.8% 30.6%;--ak-ui-destructive-foreground:0 0% 98%;--ak-ui-border:240 3.7% 15.9%;--ak-ui-input:240 3.7% 15.9%;--ak-ui-ring:240 4.9% 83.9%}:root,:host{--spacing:.25rem}.collapse{visibility:collapse}.relative{position:relative}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.\!mt-3{margin-top:calc(var(--spacing) * 3)!important}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.items-start{align-items:flex-start}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.p-4{padding:calc(var(--spacing) * 4)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}:root{--ak-skeleton-block:hsl(var(--ak-ui-muted) / .55);--ak-skeleton-block-soft:hsl(var(--ak-ui-muted) / .32);--ak-skeleton-shimmer:hsl(var(--ak-ui-foreground) / .08);--ak-skeleton-radius:.375rem;--ak-skeleton-duration:1.6s;--ak-skeleton-pulse-opacity:.48;--ak-skeleton-shimmer-angle:110deg;--ak-skeleton-ring:hsl(var(--ak-ui-foreground) / .04)}.light{--ak-skeleton-shimmer:#ffffffd9;--ak-skeleton-ring:#0000000d}.a-skel-block{background-image:linear-gradient(180deg, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 100%);background-color:var(--ak-skeleton-block);border-radius:var(--ak-skeleton-radius);box-shadow:inset 0 0 0 1px var(--ak-skeleton-ring);isolation:isolate;position:relative;overflow:hidden}.a-skel-block--anim-shimmer:after{content:"";background-image:linear-gradient(var(--ak-skeleton-shimmer-angle), transparent 30%, var(--ak-skeleton-shimmer) 50%, transparent 70%);will-change:transform;animation:a-skel-shimmer var(--ak-skeleton-duration) cubic-bezier(.42, 0, .58, 1) infinite;z-index:1;position:absolute;inset:0 -25%;transform:translate(-110%)}.a-skel-block--anim-pulse{animation:a-skel-pulse var(--ak-skeleton-duration) cubic-bezier(.42, 0, .58, 1) infinite;will-change:opacity}@keyframes a-skel-shimmer{0%{transform:translate(-110%)}to{transform:translate(110%)}}@keyframes a-skel-pulse{0%,to{opacity:1}50%{opacity:var(--ak-skeleton-pulse-opacity)}}@media (prefers-reduced-motion:reduce){.a-skel-block--anim-shimmer:after,.a-skel-block--anim-pulse{will-change:auto;animation:none}.a-skel-block--anim-shimmer:after{opacity:.5;transform:translate(0)}}.a-skeleton__layer{contain:layout style paint;content-visibility:auto;position:relative}.a-skeleton__layer>.a-skel-block{contain:strict;position:absolute}.a-skel-block--text{border-radius:calc(var(--ak-skeleton-radius) * .6)}.a-skel-block--circle{background-image:radial-gradient(circle at 35% 30%, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 100%)}.a-skel-block--image{background-image:linear-gradient(160deg, var(--ak-skeleton-block) 0%, var(--ak-skeleton-block-soft) 60%, var(--ak-skeleton-block) 100%)}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
3
3
|
/* --- bundled SFC styles --- */
|
|
4
4
|
|
|
5
5
|
.a-skeleton[data-v-16717541] {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alikhalilll/a-skeleton",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Self-generating Vue 3 / Nuxt 3+ skeleton loader. First paint mirrors the slot's HTML structure; second load replays a pixel-aligned shape captured from the real DOM. Themeable via CSS vars. Part of the @alikhalilll/a-* toolkit.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "alikhalilll",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, shallowRef, useSlots, watch, type CSSProperties } from 'vue';
|
|
2
|
+
import { computed, ref, shallowRef, useId, useSlots, watch, type CSSProperties } from 'vue';
|
|
3
3
|
import { cn } from '@alikhalilll/a-ui-base';
|
|
4
4
|
import type { ASkeletonProps, ASkeletonSlots, CachedShape, ShapeNodeType } from '../types';
|
|
5
5
|
import { useShapeProbe } from '../composables/useShapeProbe';
|
|
@@ -19,7 +19,18 @@ defineSlots<ASkeletonSlots>();
|
|
|
19
19
|
|
|
20
20
|
const slots = useSlots();
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/* Per-instance suffix from Vue's useId() — deterministic across SSR/hydration
|
|
23
|
+
* and stable across re-renders, but distinct per <ASkeleton> instance. Two
|
|
24
|
+
* <ASkeleton><UserCard/></ASkeleton> on the same page therefore never collide
|
|
25
|
+
* on the auto-generated key. Pass an explicit `cacheKey` to share a shape
|
|
26
|
+
* across instances on purpose. */
|
|
27
|
+
const instanceId = useId();
|
|
28
|
+
|
|
29
|
+
const resolvedKey = computed(
|
|
30
|
+
() => props.cacheKey ?? `${fingerprintSlot(slots.default?.())}:${instanceId}`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const warnedKeys = new Set<string>();
|
|
23
34
|
|
|
24
35
|
const cached = shallowRef<CachedShape | undefined>(getCached(resolvedKey.value, props.persist));
|
|
25
36
|
|
|
@@ -38,6 +49,14 @@ useShapeProbe(() => (props.loading ? null : wrapperRef.value), {
|
|
|
38
49
|
onCapture: (shape) => {
|
|
39
50
|
setCached(resolvedKey.value, shape, props.persist);
|
|
40
51
|
cached.value = shape;
|
|
52
|
+
if (shape.truncated && !warnedKeys.has(resolvedKey.value)) {
|
|
53
|
+
warnedKeys.add(resolvedKey.value);
|
|
54
|
+
console.warn(
|
|
55
|
+
`[ASkeleton] Capture truncated at maxNodes=${props.maxNodes} for cacheKey="${resolvedKey.value}". ` +
|
|
56
|
+
`The replayed skeleton will be missing nodes. Raise \`max-nodes\` or mark dense subtrees with ` +
|
|
57
|
+
`\`data-skeleton-stop\` to collapse them into a single block.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
41
60
|
},
|
|
42
61
|
});
|
|
43
62
|
|
|
@@ -4,21 +4,44 @@ import type { CachedShape, ShapeNode } from '../types';
|
|
|
4
4
|
const memory = new Map<string, CachedShape>();
|
|
5
5
|
const STORAGE_PREFIX = 'a-skeleton:';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Schema version for persisted entries. Bump whenever the `ShapeNode` /
|
|
9
|
+
* `CachedShape` field set changes so stale localStorage payloads from older
|
|
10
|
+
* releases get dropped on read instead of rehydrating into a wrong layout.
|
|
11
|
+
*/
|
|
12
|
+
const SCHEMA_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
interface PersistedShape {
|
|
15
|
+
v: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
nodes: Partial<ShapeNode>[];
|
|
19
|
+
truncated?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
/**
|
|
8
23
|
* Lookup a captured shape by key. Reads in-memory first, then `localStorage` if
|
|
9
24
|
* `persist` is enabled. SSR-safe — bypasses `window` access on the server.
|
|
10
25
|
* Rehydrates pre-computed styles for shapes deserialized from older sessions
|
|
11
26
|
* (Object.freeze + style/lineStyles don't survive `JSON.stringify` round-trip).
|
|
27
|
+
* Drops the entry if it was written by a different schema version.
|
|
12
28
|
*/
|
|
13
29
|
export function getCached(key: string, persist: boolean): CachedShape | undefined {
|
|
14
30
|
const hit = memory.get(key);
|
|
15
31
|
if (hit) return hit;
|
|
16
32
|
if (!persist || typeof window === 'undefined') return undefined;
|
|
17
33
|
try {
|
|
18
|
-
const
|
|
34
|
+
const storageKey = STORAGE_PREFIX + key;
|
|
35
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
19
36
|
if (!raw) return undefined;
|
|
20
|
-
const parsed = JSON.parse(raw) as
|
|
21
|
-
|
|
37
|
+
const parsed = JSON.parse(raw) as Partial<PersistedShape>;
|
|
38
|
+
if (parsed.v !== SCHEMA_VERSION) {
|
|
39
|
+
/* Stale payload from a previous release — purge so the next capture
|
|
40
|
+
* writes a fresh entry instead of rehydrating into a wrong layout. */
|
|
41
|
+
window.localStorage.removeItem(storageKey);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const rehydrated = rehydrateShape(parsed as PersistedShape);
|
|
22
45
|
memory.set(key, rehydrated);
|
|
23
46
|
return rehydrated;
|
|
24
47
|
} catch {
|
|
@@ -32,7 +55,13 @@ export function setCached(key: string, value: CachedShape, persist: boolean): vo
|
|
|
32
55
|
if (!persist || typeof window === 'undefined') return;
|
|
33
56
|
try {
|
|
34
57
|
/* Only the geometry survives the round-trip; styles get rebuilt on read. */
|
|
35
|
-
const lean = {
|
|
58
|
+
const lean: PersistedShape = {
|
|
59
|
+
v: SCHEMA_VERSION,
|
|
60
|
+
width: value.width,
|
|
61
|
+
height: value.height,
|
|
62
|
+
nodes: leanNodes(value.nodes),
|
|
63
|
+
truncated: value.truncated,
|
|
64
|
+
};
|
|
36
65
|
window.localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(lean));
|
|
37
66
|
} catch {
|
|
38
67
|
/* quota exceeded / disabled storage — silently degrade to in-memory only. */
|
|
@@ -67,8 +96,8 @@ export function clearCached(key?: string): void {
|
|
|
67
96
|
* Walks the array in-place where possible and freezes the result so the render
|
|
68
97
|
* path stays allocation-free.
|
|
69
98
|
*/
|
|
70
|
-
function rehydrateShape(shape:
|
|
71
|
-
const nodes = shape.nodes.map((n) => (n
|
|
99
|
+
function rehydrateShape(shape: PersistedShape): CachedShape {
|
|
100
|
+
const nodes = shape.nodes.map((n) => freezeNodeStyles(n as ShapeNode));
|
|
72
101
|
return Object.freeze({
|
|
73
102
|
nodes: Object.freeze(nodes),
|
|
74
103
|
width: shape.width,
|
package/src/types.ts
CHANGED
|
@@ -42,9 +42,12 @@ export interface ASkeletonProps {
|
|
|
42
42
|
/** When true, render the captured skeleton (or `fallback` slot) instead of the default slot. */
|
|
43
43
|
loading: boolean;
|
|
44
44
|
/**
|
|
45
|
-
* Identifier used to look up + persist the captured shape.
|
|
46
|
-
* slot
|
|
47
|
-
*
|
|
45
|
+
* Identifier used to look up + persist the captured shape. Defaults to
|
|
46
|
+
* `"<slot-fingerprint>:<useId()>"` so every `<ASkeleton>` instance gets its
|
|
47
|
+
* own cache slot automatically — two `<ASkeleton><UserCard/></ASkeleton>` on
|
|
48
|
+
* the same page never collide. Pass explicitly when you *want* multiple
|
|
49
|
+
* instances to share a captured shape (e.g. a list of identical cards), or
|
|
50
|
+
* when one instance renders different shapes depending on a prop.
|
|
48
51
|
*/
|
|
49
52
|
cacheKey?: string;
|
|
50
53
|
/** Max recursion depth during shape capture. Default 6. */
|
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.1.0",
|
|
5
5
|
"js-types-syntax": "typescript",
|
|
6
6
|
"description-markup": "markdown",
|
|
7
7
|
"framework": "vue",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"name": "cacheKey",
|
|
33
33
|
"type": "string",
|
|
34
34
|
"required": false,
|
|
35
|
-
"description": "Identifier used to look up + persist the captured shape.
|
|
35
|
+
"description": "Identifier used to look up + persist the captured shape. Defaults to\n`\"<slot-fingerprint>:<useId()>\"` so every `<ASkeleton>` instance gets its\nown cache slot automatically — two `<ASkeleton><UserCard/></ASkeleton>` on\nthe same page never collide. Pass explicitly when you *want* multiple\ninstances to share a captured shape (e.g. a list of identical cards), or\nwhen one instance renders different shapes depending on a prop."
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
"name": "class",
|