@alikhalilll/a-skeleton 1.0.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.
Files changed (53) hide show
  1. package/.media/hero.png +0 -0
  2. package/.media/hero.svg +232 -0
  3. package/README.md +459 -170
  4. package/dist/index.cjs +3696 -828
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +534 -43
  7. package/dist/index.d.ts +534 -43
  8. package/dist/index.js +3677 -830
  9. package/dist/index.js.map +1 -1
  10. package/dist/nuxt/index.cjs +16 -1
  11. package/dist/nuxt/index.cjs.map +1 -1
  12. package/dist/nuxt/index.js +16 -1
  13. package/dist/nuxt/index.js.map +1 -1
  14. package/dist/resolver/index.cjs +16 -1
  15. package/dist/resolver/index.cjs.map +1 -1
  16. package/dist/resolver/index.js +16 -1
  17. package/dist/resolver/index.js.map +1 -1
  18. package/dist/styles.css +56 -11
  19. package/package.json +8 -2
  20. package/src/components/ASkeleton.vue +216 -98
  21. package/src/components/ASkeletonClone.vue +106 -0
  22. package/src/components/ASkeletonLayer.vue +20 -32
  23. package/src/components/CloneNode.ts +161 -0
  24. package/src/components/StructuralLayerNode.ts +157 -0
  25. package/src/components/icons.ts +45 -0
  26. package/src/components/variants/ASkeletonArticle.vue +33 -0
  27. package/src/components/variants/ASkeletonAvatar.vue +42 -0
  28. package/src/components/variants/ASkeletonButton.vue +37 -0
  29. package/src/components/variants/ASkeletonCard.vue +47 -0
  30. package/src/components/variants/ASkeletonChart.vue +56 -0
  31. package/src/components/variants/ASkeletonChip.vue +32 -0
  32. package/src/components/variants/ASkeletonDivider.vue +26 -0
  33. package/src/components/variants/ASkeletonForm.vue +32 -0
  34. package/src/components/variants/ASkeletonHeading.vue +47 -0
  35. package/src/components/variants/ASkeletonImage.vue +57 -0
  36. package/src/components/variants/ASkeletonInput.vue +33 -0
  37. package/src/components/variants/ASkeletonListItem.vue +40 -0
  38. package/src/components/variants/ASkeletonTable.vue +49 -0
  39. package/src/components/variants/ASkeletonText.vue +49 -0
  40. package/src/components/variants/ASkeletonVideo.vue +55 -0
  41. package/src/composables/useShapeProbe.ts +33 -9
  42. package/src/composables/useSkeleton.ts +33 -21
  43. package/src/composables/useSkeletonCache.ts +282 -24
  44. package/src/index.ts +48 -2
  45. package/src/nuxt/index.ts +16 -0
  46. package/src/resolver/index.ts +16 -0
  47. package/src/types.ts +124 -5
  48. package/src/utils/buildStructuralSkeleton.ts +400 -103
  49. package/src/utils/captureStyles.ts +378 -0
  50. package/src/utils/domRead.ts +143 -0
  51. package/src/utils/walkDom.ts +261 -16
  52. package/src/utils/walkStructural.ts +418 -0
  53. package/web-types.json +10 -4
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+
5
+ interface Props {
6
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
7
+ width?: number | string;
8
+ animation?: 'pulse' | 'shimmer' | 'wave' | 'none';
9
+ class?: HTMLAttributes['class'];
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), { level: 2, animation: 'pulse' });
13
+
14
+ const animClass = computed(() =>
15
+ props.animation === 'none' ? null : `a-skel-anim-${props.animation}`
16
+ );
17
+
18
+ const HEIGHT_BY_LEVEL: Record<number, string> = {
19
+ 1: '2.25rem',
20
+ 2: '1.75rem',
21
+ 3: '1.5rem',
22
+ 4: '1.25rem',
23
+ 5: '1.125rem',
24
+ 6: '1rem',
25
+ };
26
+
27
+ const rootStyle = computed(() => ({
28
+ width:
29
+ props.width !== undefined
30
+ ? typeof props.width === 'number'
31
+ ? `${props.width}px`
32
+ : String(props.width)
33
+ : '60%',
34
+ height: HEIGHT_BY_LEVEL[props.level],
35
+ }));
36
+ </script>
37
+
38
+ <template>
39
+ <div
40
+ :class="cn('a-skel a-skel-variant-heading', animClass, props.class)"
41
+ :style="rootStyle"
42
+ role="status"
43
+ aria-busy="true"
44
+ >
45
+ <span class="a-skel-sr-only">Loading heading…</span>
46
+ </div>
47
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+
5
+ interface Props {
6
+ ratio?: string;
7
+ width?: number | string;
8
+ height?: number | string;
9
+ showIcon?: boolean;
10
+ animation?: 'pulse' | 'shimmer' | 'wave' | 'none';
11
+ class?: HTMLAttributes['class'];
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ ratio: '16 / 9',
16
+ showIcon: true,
17
+ animation: 'pulse',
18
+ });
19
+
20
+ const animClass = computed(() =>
21
+ props.animation === 'none' ? null : `a-skel-anim-${props.animation}`
22
+ );
23
+
24
+ const rootStyle = computed(() => {
25
+ const s: Record<string, string> = {};
26
+ if (props.ratio) s.aspectRatio = props.ratio;
27
+ if (props.width !== undefined)
28
+ s.width = typeof props.width === 'number' ? `${props.width}px` : String(props.width);
29
+ if (props.height !== undefined)
30
+ s.height = typeof props.height === 'number' ? `${props.height}px` : String(props.height);
31
+ return s;
32
+ });
33
+ </script>
34
+
35
+ <template>
36
+ <div
37
+ :class="cn('a-skel a-skel-variant-image', animClass, props.class)"
38
+ :style="rootStyle"
39
+ role="status"
40
+ aria-busy="true"
41
+ >
42
+ <svg
43
+ v-if="props.showIcon"
44
+ class="size-10"
45
+ viewBox="0 0 24 24"
46
+ fill="none"
47
+ xmlns="http://www.w3.org/2000/svg"
48
+ aria-hidden="true"
49
+ >
50
+ <path
51
+ d="M19 5H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2Zm-3.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM19 17H5l3.5-4.5 2.5 3 3.5-4.5L19 17Z"
52
+ fill="currentColor"
53
+ />
54
+ </svg>
55
+ <span class="a-skel-sr-only">Loading image…</span>
56
+ </div>
57
+ </template>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+
5
+ interface Props {
6
+ width?: number | string;
7
+ height?: number | string;
8
+ animation?: 'pulse' | 'shimmer' | 'wave' | 'none';
9
+ class?: HTMLAttributes['class'];
10
+ }
11
+
12
+ const props = withDefaults(defineProps<Props>(), { width: '100%', height: 40, animation: 'pulse' });
13
+
14
+ const animClass = computed(() =>
15
+ props.animation === 'none' ? null : `a-skel-anim-${props.animation}`
16
+ );
17
+
18
+ const rootStyle = computed(() => ({
19
+ width: typeof props.width === 'number' ? `${props.width}px` : String(props.width),
20
+ height: typeof props.height === 'number' ? `${props.height}px` : String(props.height),
21
+ }));
22
+ </script>
23
+
24
+ <template>
25
+ <div
26
+ :class="cn('a-skel a-skel-variant-input', animClass, props.class)"
27
+ :style="rootStyle"
28
+ role="status"
29
+ aria-busy="true"
30
+ >
31
+ <span class="a-skel-sr-only">Loading input…</span>
32
+ </div>
33
+ </template>
@@ -0,0 +1,40 @@
1
+ <script setup lang="ts">
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+ import ASkeletonAvatar from './ASkeletonAvatar.vue';
5
+ import ASkeletonText from './ASkeletonText.vue';
6
+
7
+ interface Props {
8
+ avatar?: boolean;
9
+ lines?: 1 | 2 | 3;
10
+ trailing?: boolean;
11
+ animation?: 'pulse' | 'shimmer' | 'wave' | 'none';
12
+ class?: HTMLAttributes['class'];
13
+ }
14
+
15
+ const props = withDefaults(defineProps<Props>(), {
16
+ avatar: true,
17
+ lines: 2,
18
+ trailing: false,
19
+ animation: 'pulse',
20
+ });
21
+
22
+ const animClass = computed(() =>
23
+ props.animation === 'none' ? null : `a-skel-anim-${props.animation}`
24
+ );
25
+ </script>
26
+
27
+ <template>
28
+ <div :class="cn('a-skel-variant-list-item', props.class)" role="status" aria-busy="true">
29
+ <ASkeletonAvatar v-if="props.avatar" :size="40" :animation="props.animation" />
30
+ <div class="a-skel-variant-list-item__body">
31
+ <ASkeletonText :lines="props.lines" :animation="props.animation" />
32
+ </div>
33
+ <div
34
+ v-if="props.trailing"
35
+ :class="cn('a-skel a-skel-variant-button', animClass)"
36
+ :style="{ width: '40px', height: '24px' }"
37
+ />
38
+ <span class="a-skel-sr-only">Loading list item…</span>
39
+ </div>
40
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+
5
+ interface Props {
6
+ rows?: number;
7
+ columns?: number;
8
+ showHeader?: boolean;
9
+ animation?: 'pulse' | 'shimmer' | 'wave' | 'none';
10
+ class?: HTMLAttributes['class'];
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ rows: 5,
15
+ columns: 4,
16
+ showHeader: true,
17
+ animation: 'pulse',
18
+ });
19
+
20
+ const animClass = computed(() =>
21
+ props.animation === 'none' ? null : `a-skel-anim-${props.animation}`
22
+ );
23
+
24
+ const rowStyle = computed(() => ({
25
+ gridTemplateColumns: `repeat(${props.columns}, minmax(0, 1fr))`,
26
+ }));
27
+ </script>
28
+
29
+ <template>
30
+ <div :class="cn('a-skel-variant-table', props.class)" role="status" aria-busy="true">
31
+ <div v-if="props.showHeader" class="a-skel-variant-table__row" :style="rowStyle">
32
+ <div
33
+ v-for="c in props.columns"
34
+ :key="`h-${c}`"
35
+ :class="cn('a-skel a-skel-variant-heading', animClass)"
36
+ :style="{ height: '1.5rem', width: '70%' }"
37
+ />
38
+ </div>
39
+ <div v-for="r in props.rows" :key="r" class="a-skel-variant-table__row" :style="rowStyle">
40
+ <div
41
+ v-for="c in props.columns"
42
+ :key="`${r}-${c}`"
43
+ :class="cn('a-skel a-skel-variant-text', animClass)"
44
+ :style="{ width: c === 1 ? '85%' : c === props.columns ? '50%' : '70%' }"
45
+ />
46
+ </div>
47
+ <span class="a-skel-sr-only">Loading table…</span>
48
+ </div>
49
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+
5
+ interface Props {
6
+ lines?: number;
7
+ width?: number | string;
8
+ /** Animation variant. Default `'pulse'`. */
9
+ animation?: 'pulse' | 'shimmer' | 'wave' | 'none';
10
+ class?: HTMLAttributes['class'];
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), { lines: 1, animation: 'pulse' });
14
+
15
+ const animClass = computed(() =>
16
+ props.animation === 'none' ? null : `a-skel-anim-${props.animation}`
17
+ );
18
+ const rootStyle = computed(() =>
19
+ props.width !== undefined
20
+ ? { width: typeof props.width === 'number' ? `${props.width}px` : String(props.width) }
21
+ : undefined
22
+ );
23
+
24
+ function widthForLine(i: number): string {
25
+ /* Vuetify-style: last line ≈ 65 %, intermediate lines ≈ 85–100 %.
26
+ * This adds rhythm so a 3-line paragraph doesn't look like a brick. */
27
+ if (i === props.lines - 1 && props.lines > 1) return '65%';
28
+ const palette = ['100%', '92%', '88%', '95%'];
29
+ return palette[i % palette.length]!;
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div
35
+ :class="cn('a-skel-variants-text', props.class)"
36
+ :style="rootStyle"
37
+ role="status"
38
+ aria-live="polite"
39
+ aria-busy="true"
40
+ >
41
+ <div
42
+ v-for="i in props.lines"
43
+ :key="i"
44
+ :class="cn('a-skel a-skel-variant-text', animClass)"
45
+ :style="{ width: widthForLine(i - 1), marginTop: i > 1 ? '0.5em' : undefined }"
46
+ />
47
+ <span class="a-skel-sr-only">Loading…</span>
48
+ </div>
49
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts">
2
+ import { computed, type HTMLAttributes } from 'vue';
3
+ import { cn } from '@alikhalilll/a-ui-base';
4
+
5
+ interface Props {
6
+ ratio?: string;
7
+ width?: number | string;
8
+ height?: number | string;
9
+ showIcon?: boolean;
10
+ animation?: 'pulse' | 'shimmer' | 'wave' | 'none';
11
+ class?: HTMLAttributes['class'];
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ ratio: '16 / 9',
16
+ showIcon: true,
17
+ animation: 'pulse',
18
+ });
19
+
20
+ const animClass = computed(() =>
21
+ props.animation === 'none' ? null : `a-skel-anim-${props.animation}`
22
+ );
23
+
24
+ const rootStyle = computed(() => {
25
+ const s: Record<string, string> = {};
26
+ if (props.ratio) s.aspectRatio = props.ratio;
27
+ if (props.width !== undefined)
28
+ s.width = typeof props.width === 'number' ? `${props.width}px` : String(props.width);
29
+ if (props.height !== undefined)
30
+ s.height = typeof props.height === 'number' ? `${props.height}px` : String(props.height);
31
+ return s;
32
+ });
33
+ </script>
34
+
35
+ <template>
36
+ <div
37
+ :class="cn('a-skel a-skel-variant-video', animClass, props.class)"
38
+ :style="rootStyle"
39
+ role="status"
40
+ aria-busy="true"
41
+ >
42
+ <svg
43
+ v-if="props.showIcon"
44
+ class="size-12"
45
+ viewBox="0 0 24 24"
46
+ fill="none"
47
+ xmlns="http://www.w3.org/2000/svg"
48
+ aria-hidden="true"
49
+ >
50
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" />
51
+ <path d="M10 8l6 4-6 4V8Z" fill="currentColor" />
52
+ </svg>
53
+ <span class="a-skel-sr-only">Loading video…</span>
54
+ </div>
55
+ </template>
@@ -1,12 +1,24 @@
1
1
  import { onBeforeUnmount, watch } from 'vue';
2
- import type { CachedShape } from '../types';
2
+ import type { CachedShape, StructuralShape } from '../types';
3
3
  import { walkDom } from '../utils/walkDom';
4
4
 
5
- export interface ShapeProbeOptions {
5
+ export type ProbeShape = CachedShape | StructuralShape;
6
+
7
+ /**
8
+ * Pluggable capture strategy. `walkDom` is the default (flat, absolute-
9
+ * positioned `CachedShape`). Recipe 3 passes `walkStructural` to produce a
10
+ * tree-shaped `StructuralShape` instead.
11
+ */
12
+ export type CaptureStrategy<S extends ProbeShape> = (
13
+ el: HTMLElement,
14
+ options: { maxDepth: number; maxNodes?: number; minSize?: number }
15
+ ) => S;
16
+
17
+ export interface ShapeProbeOptions<S extends ProbeShape = CachedShape> {
6
18
  maxDepth: number;
7
- /** Forwarded to `walkDom`. Default 500. */
19
+ /** Forwarded to the capture strategy. Default 500. */
8
20
  maxNodes?: number;
9
- /** Forwarded to `walkDom`. Default 4. */
21
+ /** Forwarded to the capture strategy. Default 4. */
10
22
  minSize?: number;
11
23
  /**
12
24
  * Debounce window for `ResizeObserver`-triggered re-captures, in ms.
@@ -15,7 +27,12 @@ export interface ShapeProbeOptions {
15
27
  * always immediate via `requestAnimationFrame`.
16
28
  */
17
29
  resizeDebounceMs?: number;
18
- onCapture: (shape: CachedShape) => void;
30
+ /**
31
+ * Capture strategy. Default: `walkDom` (flat positioned-block model).
32
+ * Pass `walkStructural` for the tree-shaped Recipe 3 model.
33
+ */
34
+ capture?: CaptureStrategy<S>;
35
+ onCapture: (shape: S) => void;
19
36
  }
20
37
 
21
38
  const DEFAULT_RESIZE_DEBOUNCE_MS = 150;
@@ -30,16 +47,20 @@ const DEFAULT_RESIZE_DEBOUNCE_MS = 150;
30
47
  * render queue.
31
48
  * - Subsequent `ResizeObserver` callbacks are debounced (default 150 ms) so a
32
49
  * drag-resize doesn't trigger a fresh DOM walk per frame.
33
- * - `walkDom` itself enforces `maxNodes` so even a worst-case capture (10k
34
- * descendants) returns in bounded time.
50
+ * - The capture strategy itself enforces `maxNodes` so even a worst-case
51
+ * capture (10k descendants) returns in bounded time.
35
52
  */
36
- export function useShapeProbe(getTarget: () => HTMLElement | null, options: ShapeProbeOptions) {
53
+ export function useShapeProbe<S extends ProbeShape = CachedShape>(
54
+ getTarget: () => HTMLElement | null,
55
+ options: ShapeProbeOptions<S>
56
+ ): void {
37
57
  let observer: ResizeObserver | undefined;
38
58
  let frame: number | undefined;
39
59
  let timer: ReturnType<typeof setTimeout> | undefined;
40
60
  let hasCaptured = false;
41
61
 
42
62
  const debounceMs = options.resizeDebounceMs ?? DEFAULT_RESIZE_DEBOUNCE_MS;
63
+ const captureFn = options.capture ?? (walkDom as unknown as CaptureStrategy<S>);
43
64
 
44
65
  function cleanup() {
45
66
  if (observer) {
@@ -57,11 +78,14 @@ export function useShapeProbe(getTarget: () => HTMLElement | null, options: Shap
57
78
  }
58
79
 
59
80
  function capture(el: HTMLElement) {
60
- const result = walkDom(el, {
81
+ const result = captureFn(el, {
61
82
  maxDepth: options.maxDepth,
62
83
  maxNodes: options.maxNodes,
63
84
  minSize: options.minSize,
64
85
  });
86
+ /* Both shape variants expose `width`, `height`, `nodes` — guard against an
87
+ * empty capture (target rendered with zero size or an unreachable subtree)
88
+ * so the cache never receives a meaningless entry. */
65
89
  if (result.width > 0 && result.height > 0 && result.nodes.length > 0) {
66
90
  hasCaptured = true;
67
91
  options.onCapture(result);
@@ -1,8 +1,12 @@
1
1
  import { shallowRef, type Ref } from 'vue';
2
- import type { CachedShape } from '../types';
2
+ import type { StructuralShape } from '../types';
3
3
  import { useShapeProbe } from './useShapeProbe';
4
- import { clearCached, getCached, setCached } from './useSkeletonCache';
5
- import { walkDom } from '../utils/walkDom';
4
+ import {
5
+ clearCachedStructural,
6
+ getCachedStructural,
7
+ setCachedStructural,
8
+ } from './useSkeletonCache';
9
+ import { walkStructural } from '../utils/walkStructural';
6
10
 
7
11
  export interface UseSkeletonOptions {
8
12
  /**
@@ -22,33 +26,37 @@ export interface UseSkeletonOptions {
22
26
  target?: () => HTMLElement | null;
23
27
  /** Persist to `localStorage` so first-paint after reload skips the cold start. Default false. */
24
28
  persist?: boolean;
25
- /** Forwarded to `walkDom`. Default 6. */
29
+ /** Forwarded to `walkStructural`. Default 12. */
26
30
  maxDepth?: number;
27
- /** Forwarded to `walkDom`. Default 500. */
31
+ /** Forwarded to `walkStructural`. Default 500. */
28
32
  maxNodes?: number;
29
- /** Forwarded to `walkDom`. Default 4. */
33
+ /** Forwarded to `walkStructural`. Default 4. */
30
34
  minSize?: number;
31
35
  /** Forwarded to `useShapeProbe`. Default 150 ms. */
32
36
  resizeDebounceMs?: number;
33
37
  }
34
38
 
35
39
  export interface UseSkeletonReturn {
36
- /** Reactive captured shape — `undefined` on cache miss. Replace your skeleton render path. */
37
- shape: Readonly<Ref<CachedShape | undefined>>;
40
+ /** Reactive captured shape — `undefined` on cache miss. Feed it to `<ASkeletonLayer>`. */
41
+ shape: Readonly<Ref<StructuralShape | undefined>>;
38
42
  /**
39
43
  * Synchronously measure the current target and write to cache. Returns the
40
44
  * captured shape, or `undefined` if the target wasn't available / nothing
41
45
  * worth measuring was rendered. Use when you want to force a capture outside
42
46
  * the automatic `ResizeObserver` flow (e.g. after an animation settles).
43
47
  */
44
- captureNow: () => CachedShape | undefined;
48
+ captureNow: () => StructuralShape | undefined;
45
49
  /** Drop the cache entry for this `cacheKey`. The reactive `shape` flips to `undefined`. */
46
50
  clear: () => void;
47
51
  }
48
52
 
53
+ const DEFAULT_MAX_DEPTH = 12;
54
+
49
55
  /**
50
- * High-level building block for custom skeleton UIs. Wires up the probe + cache
51
- * + reactivity so the consumer just renders something using the reactive shape:
56
+ * High-level building block for Recipe 3 wires the structural capture +
57
+ * cache + reactivity around a target element. The reactive `shape` is fed to
58
+ * `<ASkeletonLayer>` which renders it in normal flow inside the consumer's
59
+ * container.
52
60
  *
53
61
  * ```ts
54
62
  * const containerRef = ref<HTMLElement | null>(null);
@@ -65,46 +73,50 @@ export interface UseSkeletonReturn {
65
73
  * </div>
66
74
  * ```
67
75
  *
68
- * For more control, drop down to `useShapeProbe` + `getCached`/`setCached` and
69
- * compose your own.
76
+ * For more control, drop down to `useShapeProbe` (pass `walkStructural` as the
77
+ * capture strategy) + `getCachedStructural` / `setCachedStructural` and compose
78
+ * your own orchestration.
70
79
  */
71
80
  export function useSkeleton(options: UseSkeletonOptions): UseSkeletonReturn {
72
81
  const persist = options.persist ?? false;
73
- const maxDepth = options.maxDepth ?? 6;
82
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
74
83
 
75
- const shape = shallowRef<CachedShape | undefined>(getCached(options.cacheKey, persist));
84
+ const shape = shallowRef<StructuralShape | undefined>(
85
+ getCachedStructural(options.cacheKey, persist)
86
+ );
76
87
 
77
88
  if (options.target) {
78
89
  const getTarget = options.target;
79
- useShapeProbe(getTarget, {
90
+ useShapeProbe<StructuralShape>(getTarget, {
80
91
  maxDepth,
81
92
  maxNodes: options.maxNodes,
82
93
  minSize: options.minSize,
83
94
  resizeDebounceMs: options.resizeDebounceMs,
95
+ capture: walkStructural,
84
96
  onCapture: (captured) => {
85
- setCached(options.cacheKey, captured, persist);
97
+ setCachedStructural(options.cacheKey, captured, persist);
86
98
  shape.value = captured;
87
99
  },
88
100
  });
89
101
  }
90
102
 
91
- function captureNow(): CachedShape | undefined {
103
+ function captureNow(): StructuralShape | undefined {
92
104
  const el = options.target?.();
93
105
  if (!el || typeof window === 'undefined') return undefined;
94
- const captured = walkDom(el, {
106
+ const captured = walkStructural(el, {
95
107
  maxDepth,
96
108
  maxNodes: options.maxNodes,
97
109
  minSize: options.minSize,
98
110
  });
99
111
  if (captured.width <= 0 || captured.height <= 0 || captured.nodes.length === 0)
100
112
  return undefined;
101
- setCached(options.cacheKey, captured, persist);
113
+ setCachedStructural(options.cacheKey, captured, persist);
102
114
  shape.value = captured;
103
115
  return captured;
104
116
  }
105
117
 
106
118
  function clear(): void {
107
- clearCached(options.cacheKey);
119
+ clearCachedStructural(options.cacheKey);
108
120
  shape.value = undefined;
109
121
  }
110
122