@alikhalilll/a-skeleton 1.1.0 → 1.2.1

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 +458 -172
  4. package/dist/index.cjs +3685 -840
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +530 -43
  7. package/dist/index.d.ts +530 -43
  8. package/dist/index.js +3666 -842
  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 +9 -3
  20. package/src/components/ASkeleton.vue +212 -113
  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 +251 -22
  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 +118 -2
  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 +9 -3
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Comment, Fragment, Text, computed, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, h, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, renderList, renderSlot, shallowRef, useId, useSlots, watch } from "vue";
1
+ import { Comment, Fragment, Text, cloneVNode, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, h, normalizeClass, normalizeStyle, onBeforeUnmount, openBlock, ref, renderList, renderSlot, shallowRef, useId, useSlots, watch } from "vue";
2
2
  //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
3
3
  function r(e) {
4
4
  var t, f, n = "";
@@ -3398,704 +3398,1118 @@ function cn(...inputs) {
3398
3398
  return twMerge(clsx(inputs));
3399
3399
  }
3400
3400
  //#endregion
3401
- //#region src/utils/walkDom.ts
3402
- const DEFAULT_MAX_NODES$1 = 500;
3403
- const DEFAULT_MIN_SIZE = 4;
3404
- const LEAF_TAGS = new Set([
3405
- "IMG",
3406
- "SVG",
3407
- "CANVAS",
3408
- "VIDEO",
3409
- "INPUT",
3410
- "TEXTAREA",
3411
- "SELECT",
3412
- "BUTTON",
3413
- "PROGRESS",
3414
- "METER",
3415
- "HR"
3401
+ //#region src/utils/buildStructuralSkeleton.ts
3402
+ /**
3403
+ * Atomic tags — rendered as a single shimmer block. Their internal rendering
3404
+ * is opaque to vnode walking (no meaningful child structure for us to mirror).
3405
+ * Their own `class` + `style` are preserved so Tailwind utilities like
3406
+ * `size-16` and `rounded-full` still drive the dimensions.
3407
+ *
3408
+ * Note: `button`, `a`, `label` are deliberately NOT atomic — they're treated
3409
+ * as containers so their real `background-color` / `border` / shadow survive
3410
+ * (we add `.a-skel-block` to atomics, which paints a skeleton gradient that
3411
+ * would override Tailwind's `bg-emerald-600` and friends). Their text content
3412
+ * is recursed and replaced with a shimmer span inside the real button shape.
3413
+ */
3414
+ const ATOMIC_TAGS$2 = new Set([
3415
+ "img",
3416
+ "svg",
3417
+ "canvas",
3418
+ "video",
3419
+ "audio",
3420
+ "input",
3421
+ "textarea",
3422
+ "select",
3423
+ "progress",
3424
+ "meter",
3425
+ "hr",
3426
+ "iframe",
3427
+ "object",
3428
+ "embed",
3429
+ "picture"
3416
3430
  ]);
3417
3431
  /**
3418
- * Walk `root`'s descendants and produce a list of shimmer blocks that mirror its
3419
- * rendered layout. Coordinates are relative to `root`'s top-left so the result can
3420
- * be replayed in any container of the same size.
3432
+ * SVG child tags that should never be walked by the structural skeleton
3433
+ * SVG interior elements use a different coordinate space (`x`/`y` are
3434
+ * attributes, not CSS), so emitting `.a-skel-block` divs at their tag would
3435
+ * yield non-rendering elements. When we see one of these, the parent `<svg>`
3436
+ * has already been treated as an atomic block — we just return null/skip.
3437
+ */
3438
+ const SVG_INTERIOR_TAGS = new Set([
3439
+ "circle",
3440
+ "rect",
3441
+ "path",
3442
+ "line",
3443
+ "polyline",
3444
+ "polygon",
3445
+ "ellipse",
3446
+ "g",
3447
+ "defs",
3448
+ "clippath",
3449
+ "mask",
3450
+ "pattern",
3451
+ "lineargradient",
3452
+ "radialgradient",
3453
+ "stop",
3454
+ "use",
3455
+ "symbol",
3456
+ "foreignobject",
3457
+ "text",
3458
+ "tspan",
3459
+ "textpath",
3460
+ "marker",
3461
+ "filter",
3462
+ "feblend",
3463
+ "fecolormatrix",
3464
+ "fegaussianblur",
3465
+ "feoffset",
3466
+ "fedropshadow",
3467
+ "femerge",
3468
+ "femergenode"
3469
+ ]);
3470
+ /**
3471
+ * Table-structural tags — preserve them as-is. Their semantics (`display:
3472
+ * table-*`) are critical for layout, and replacing them with `<div>` would
3473
+ * break the grid. Children are recursed normally.
3474
+ */
3475
+ const TABLE_TAGS = new Set([
3476
+ "table",
3477
+ "thead",
3478
+ "tbody",
3479
+ "tfoot",
3480
+ "tr",
3481
+ "td",
3482
+ "th",
3483
+ "caption",
3484
+ "colgroup",
3485
+ "col"
3486
+ ]);
3487
+ /**
3488
+ * List-structural tags — `<ul>`, `<ol>`, `<li>`, `<dl>`, `<dt>`, `<dd>`.
3489
+ * Preserved as containers; `<li>` recurses into its text/children so each
3490
+ * list item gets the right shimmer treatment.
3491
+ */
3492
+ const LIST_TAGS = new Set([
3493
+ "ul",
3494
+ "ol",
3495
+ "li",
3496
+ "dl",
3497
+ "dt",
3498
+ "dd"
3499
+ ]);
3500
+ /**
3501
+ * Tags whose content is conventionally text. When the author writes
3502
+ * `<h3>{{ data?.name }}</h3>` and `data` is null during loading, the
3503
+ * interpolation produces no children — the walker would otherwise emit an
3504
+ * empty `<h3></h3>` and no skeleton bar shows. For these tags we emit a
3505
+ * synthetic placeholder text-content span so the bar renders at the tag's
3506
+ * natural rendered width (Tailwind sizing on the tag still drives height).
3507
+ */
3508
+ const TEXT_OWNER_TAGS = new Set([
3509
+ "h1",
3510
+ "h2",
3511
+ "h3",
3512
+ "h4",
3513
+ "h5",
3514
+ "h6",
3515
+ "p",
3516
+ "span",
3517
+ "a",
3518
+ "em",
3519
+ "strong",
3520
+ "small",
3521
+ "code",
3522
+ "b",
3523
+ "i",
3524
+ "mark",
3525
+ "label",
3526
+ "caption",
3527
+ "time",
3528
+ "dt",
3529
+ "dd",
3530
+ "li",
3531
+ "th",
3532
+ "td",
3533
+ "figcaption",
3534
+ "blockquote",
3535
+ "cite",
3536
+ "q"
3537
+ ]);
3538
+ /**
3539
+ * Non-breaking-space placeholder for empty text-owners. ~24 chars wide is
3540
+ * enough to read as "a line of text" in most fonts while still being short
3541
+ * enough that the rendered bar doesn't wrap in narrow containers.
3542
+ */
3543
+ const TEXT_PLACEHOLDER = "\xA0".repeat(24);
3544
+ const DEFAULT_MAX_DEPTH$3 = 16;
3545
+ const DEFAULT_MAX_NODES$3 = 600;
3546
+ /**
3547
+ * Walk a slot's vnode tree and produce a **DOM-mirror skeleton**: every element
3548
+ * is preserved (same tag, same `class`, same inline `style`), and only the
3549
+ * content is replaced. The output is structurally identical to what the real
3550
+ * component would render — Tailwind utilities for layout, spacing, sizing,
3551
+ * backgrounds and shadows all carry through. The CSS does the work of making
3552
+ * it *look* like a skeleton.
3421
3553
  *
3422
- * Performance:
3423
- * - `maxNodes` caps the walk so a 5000-row table doesn't lock up the main thread.
3424
- * - `minSize` filters out hairlines (1-2 px paddings, decorative dots) that
3425
- * inflate node count without adding visual signal.
3426
- * - All `getBoundingClientRect` / `getComputedStyle` reads happen in a single
3427
- * top-down pass with no intervening writes, so the browser does one layout
3428
- * up front and serves cached values from then on (no layout thrashing).
3429
- * - Each emitted `ShapeNode` has a frozen pre-computed `style` (and `lineStyles`
3430
- * for multi-line text) so the render path is allocation-free.
3554
+ * Replacement rules:
3555
+ * - **Raw text** (e.g. interpolations, static strings) is wrapped in
3556
+ * `<span class="a-skel-text-content">…the real text…</span>`. The text is
3557
+ * kept as the span's children so the inline layout reserves its real
3558
+ * rendered width but the glyphs are painted transparent and a skeleton
3559
+ * gradient is painted in their place. Multi-line text wraps naturally,
3560
+ * producing one shimmer rect per visual line at the exact rendered position.
3561
+ * - **Atomic / interactive tags** (`img`, `svg`, `button`, `input`, …) are
3562
+ * replaced by a `<div class="a-skel-block">` carrying the original element's
3563
+ * `class` and `style`, so dimensions and shapes (size, border-radius, etc.)
3564
+ * still drive the layout.
3565
+ * - **Container elements** (`div`, `section`, `h1`, `p`, `a`, `span`, …) are
3566
+ * preserved as the same tag with the same `class` and `style`; we recurse
3567
+ * into their children.
3568
+ * - **Component vnodes** can't be introspected at render time, so we emit a
3569
+ * single `<div class="a-skel-block">` carrying their outer `class` / `style`.
3431
3570
  */
3432
- function walkDom(root, options) {
3433
- const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES$1;
3434
- const minSize = options.minSize ?? DEFAULT_MIN_SIZE;
3435
- const nodes = [];
3436
- const rootRect = root.getBoundingClientRect();
3437
- let truncated = false;
3438
- function visit(el, depth) {
3439
- if (nodes.length >= maxNodes) {
3440
- truncated = true;
3571
+ function buildStructuralSkeleton(vnodes, opts) {
3572
+ const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH$3;
3573
+ const state = {
3574
+ emitted: 0,
3575
+ cap: opts.maxNodes ?? DEFAULT_MAX_NODES$3
3576
+ };
3577
+ const out = [];
3578
+ walk(vnodes, opts, 0, maxDepth, state, out);
3579
+ return out;
3580
+ }
3581
+ function walk(input, opts, depth, max, state, out) {
3582
+ if (state.emitted >= state.cap) return;
3583
+ if (input == null || typeof input === "boolean") return;
3584
+ if (Array.isArray(input)) {
3585
+ for (let i = 0; i < input.length; i++) {
3586
+ if (state.emitted >= state.cap) return;
3587
+ walk(input[i], opts, depth, max, state, out);
3588
+ }
3589
+ return;
3590
+ }
3591
+ if (typeof input === "string" || typeof input === "number") {
3592
+ const str = String(input);
3593
+ if (str.trim()) push(out, textContentSpan(str, opts.animationClass), state);
3594
+ return;
3595
+ }
3596
+ const v = input;
3597
+ const type = v.type;
3598
+ if (type === Comment) return;
3599
+ if (type === Text) {
3600
+ const text = typeof v.children === "string" ? v.children : "";
3601
+ if (text.trim()) push(out, textContentSpan(text, opts.animationClass), state);
3602
+ return;
3603
+ }
3604
+ if (type === Fragment) {
3605
+ walk(v.children, opts, depth, max, state, out);
3606
+ return;
3607
+ }
3608
+ if (typeof type === "string") {
3609
+ const tag = type.toLowerCase();
3610
+ if (SVG_INTERIOR_TAGS.has(tag)) return;
3611
+ const props = v.props ?? {};
3612
+ if (props["data-skeleton-ignore"] !== void 0) {
3613
+ push(out, cloneVNode(v), state);
3441
3614
  return;
3442
3615
  }
3443
- const html = el;
3444
- if (html.dataset?.skeletonIgnore !== void 0) return;
3445
- const cs = window.getComputedStyle(el);
3446
- if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0") return;
3447
- const rect = el.getBoundingClientRect();
3448
- if (rect.width < minSize || rect.height < minSize) return;
3449
- const tag = el.tagName.toUpperCase();
3450
- const isLeafTag = LEAF_TAGS.has(tag);
3451
- const hasStop = html.dataset?.skeletonStop !== void 0;
3452
- const childElements = [];
3453
- for (let i = 0; i < el.children.length; i++) {
3454
- const c = el.children[i];
3455
- if (c.dataset?.skeletonIgnore === void 0) childElements.push(c);
3616
+ if (props["data-skeleton-stop"] !== void 0) {
3617
+ push(out, h("div", {
3618
+ class: [
3619
+ "a-skel-block",
3620
+ v.props?.class,
3621
+ opts.animationClass
3622
+ ],
3623
+ style: v.props?.style,
3624
+ "aria-hidden": "true"
3625
+ }), state);
3626
+ return;
3456
3627
  }
3457
- const hasOwnText = hasDirectTextContent(el);
3458
- const reachedDepth = depth >= options.maxDepth;
3459
- if (isLeafTag || hasStop || reachedDepth || childElements.length === 0) {
3460
- const node = elementToShape(tag, cs, rect, rootRect, hasOwnText);
3461
- if (node) nodes.push(node);
3628
+ if (props["data-skeleton-text"] !== void 0) {
3629
+ push(out, textContentSpan(" ", opts.animationClass), state);
3462
3630
  return;
3463
3631
  }
3464
- for (let i = 0; i < childElements.length; i++) {
3465
- if (nodes.length >= maxNodes) {
3466
- truncated = true;
3467
- return;
3468
- }
3469
- visit(childElements[i], depth + 1);
3632
+ if (props["data-skeleton-block"] !== void 0) {
3633
+ push(out, h("div", {
3634
+ class: [
3635
+ "a-skel-block",
3636
+ v.props?.class,
3637
+ opts.animationClass
3638
+ ],
3639
+ style: v.props?.style,
3640
+ "aria-hidden": "true"
3641
+ }), state);
3642
+ return;
3470
3643
  }
3644
+ push(out, transformElement(v, tag, opts, depth, max, state), state);
3645
+ return;
3471
3646
  }
3472
- for (let i = 0; i < root.children.length; i++) {
3473
- if (nodes.length >= maxNodes) {
3474
- truncated = true;
3475
- break;
3476
- }
3477
- visit(root.children[i], 1);
3647
+ if (typeof type === "object" || typeof type === "function") push(out, h("div", {
3648
+ class: [
3649
+ "a-skel-block",
3650
+ v.props?.class,
3651
+ opts.animationClass
3652
+ ],
3653
+ style: v.props?.style,
3654
+ "aria-hidden": "true"
3655
+ }), state);
3656
+ }
3657
+ function push(out, vn, state) {
3658
+ if (state.emitted >= state.cap) return;
3659
+ out.push(vn);
3660
+ state.emitted++;
3661
+ }
3662
+ /**
3663
+ * Surface-bearing tags — when these are encountered without their own explicit
3664
+ * background, the skeleton paints the default fill so they still read as a
3665
+ * solid element (a button without `bg-*` shouldn't look invisible).
3666
+ *
3667
+ * Pure layout containers (`div`, `section`, `main`, `article`, `header`,
3668
+ * `footer`, `nav`, `aside`) deliberately don't get the fallback — they're
3669
+ * transparent in the real DOM and should stay that way in the skeleton.
3670
+ */
3671
+ const SURFACE_TAGS = new Set([
3672
+ "button",
3673
+ "a",
3674
+ "label",
3675
+ "summary",
3676
+ "fieldset",
3677
+ "legend"
3678
+ ]);
3679
+ function transformElement(v, tag, opts, depth, max, state) {
3680
+ const cls = v.props?.class;
3681
+ const styl = v.props?.style;
3682
+ if (ATOMIC_TAGS$2.has(tag)) {
3683
+ const dimStyle = atomicDimensionStyle(v.props);
3684
+ return h("div", {
3685
+ class: [
3686
+ "a-skel-block",
3687
+ cls,
3688
+ opts.animationClass
3689
+ ],
3690
+ style: dimStyle ? mergeStyle(dimStyle, styl) : styl,
3691
+ "aria-hidden": "true"
3692
+ });
3478
3693
  }
3479
- return Object.freeze({
3480
- nodes: Object.freeze(nodes),
3481
- width: Math.round(rootRect.width),
3482
- height: Math.round(rootRect.height),
3483
- truncated
3694
+ if (TABLE_TAGS.has(tag)) {
3695
+ if (depth >= max) return h(tag, {
3696
+ class: cls,
3697
+ style: styl
3698
+ });
3699
+ const recursed = [];
3700
+ walk(v.children, opts, depth + 1, max, state, recursed);
3701
+ if (recursed.length === 0 && TEXT_OWNER_TAGS.has(tag)) return h(tag, {
3702
+ class: cls,
3703
+ style: styl
3704
+ }, [textContentSpan(TEXT_PLACEHOLDER, opts.animationClass)]);
3705
+ return h(tag, {
3706
+ class: cls,
3707
+ style: styl
3708
+ }, recursed.length > 0 ? recursed : void 0);
3709
+ }
3710
+ if (LIST_TAGS.has(tag)) {
3711
+ if (depth >= max) return h(tag, {
3712
+ class: cls,
3713
+ style: styl
3714
+ });
3715
+ const recursed = [];
3716
+ walk(v.children, opts, depth + 1, max, state, recursed);
3717
+ if (recursed.length === 0 && TEXT_OWNER_TAGS.has(tag)) return h(tag, {
3718
+ class: cls,
3719
+ style: styl
3720
+ }, [textContentSpan(TEXT_PLACEHOLDER, opts.animationClass)]);
3721
+ return h(tag, {
3722
+ class: cls,
3723
+ style: styl
3724
+ }, recursed.length > 0 ? recursed : void 0);
3725
+ }
3726
+ if (depth >= max) return h("div", {
3727
+ class: [
3728
+ "a-skel-block",
3729
+ cls,
3730
+ opts.animationClass
3731
+ ],
3732
+ style: styl,
3733
+ "aria-hidden": "true"
3734
+ });
3735
+ const recursed = [];
3736
+ walk(v.children, opts, depth + 1, max, state, recursed);
3737
+ if (recursed.length === 0 && TEXT_OWNER_TAGS.has(tag)) return h(tag, {
3738
+ class: cls,
3739
+ style: styl
3740
+ }, [textContentSpan(TEXT_PLACEHOLDER, opts.animationClass)]);
3741
+ if (recursed.length === 0) return h(tag, {
3742
+ class: cls,
3743
+ style: styl
3484
3744
  });
3745
+ if (SURFACE_TAGS.has(tag) && !hasExplicitBackground(cls, styl)) return h(tag, {
3746
+ class: [
3747
+ "a-skel-block",
3748
+ cls,
3749
+ opts.animationClass
3750
+ ],
3751
+ style: styl
3752
+ }, recursed);
3753
+ return cloneTag(tag, cls, styl, recursed);
3485
3754
  }
3486
- function hasDirectTextContent(el) {
3487
- for (let i = 0; i < el.childNodes.length; i++) {
3488
- const node = el.childNodes[i];
3489
- if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").trim().length > 0) return true;
3755
+ /**
3756
+ * Extract width/height from HTML attributes that affect layout (`<svg
3757
+ * width="408">`, `<img width="…" height="…">`) and project them into inline
3758
+ * style. Without this, replacing an attribute-sized atomic element with a
3759
+ * `<div>` would drop the size — divs don't honour those attributes.
3760
+ */
3761
+ function atomicDimensionStyle(props) {
3762
+ if (!props) return null;
3763
+ const out = {};
3764
+ const w = props.width;
3765
+ const h = props.height;
3766
+ if (w !== void 0 && w !== null && w !== "") out.width = typeof w === "number" ? `${w}px` : /^\d+$/.test(String(w)) ? `${w}px` : String(w);
3767
+ if (h !== void 0 && h !== null && h !== "") out.height = typeof h === "number" ? `${h}px` : /^\d+$/.test(String(h)) ? `${h}px` : String(h);
3768
+ return Object.keys(out).length > 0 ? out : null;
3769
+ }
3770
+ /**
3771
+ * Detect whether the element has an explicit background — either via a
3772
+ * Tailwind / DaisyUI / CSS-modules `bg-*` class or via an inline
3773
+ * `background` / `background-color` / `background-image` style. When false,
3774
+ * surface-bearing elements (button, a, label, …) fall back to the default
3775
+ * skeleton fill so they don't render as a transparent gap during loading.
3776
+ */
3777
+ function hasExplicitBackground(cls, styl) {
3778
+ if (hasBackgroundInStyle(styl)) return true;
3779
+ if (hasBackgroundInClass(cls)) return true;
3780
+ return false;
3781
+ }
3782
+ const BG_CLASS_RE = /(?:^|\s|:)bg-(?!skel)(?:\[|[a-z])/i;
3783
+ function hasBackgroundInClass(cls) {
3784
+ if (cls == null || cls === false) return false;
3785
+ if (typeof cls === "string") return BG_CLASS_RE.test(cls);
3786
+ if (Array.isArray(cls)) {
3787
+ for (const item of cls) if (hasBackgroundInClass(item)) return true;
3788
+ return false;
3789
+ }
3790
+ if (typeof cls === "object") {
3791
+ for (const k of Object.keys(cls)) if (cls[k] && BG_CLASS_RE.test(k)) return true;
3490
3792
  }
3491
3793
  return false;
3492
3794
  }
3493
- function elementToShape(tag, cs, rect, origin, hasText) {
3494
- const x = Math.round(rect.left - origin.left);
3495
- const y = Math.round(rect.top - origin.top);
3496
- const w = Math.round(rect.width);
3497
- const h = Math.round(rect.height);
3498
- const radius = parseFloat(cs.borderRadius) || 0;
3499
- const minDim = Math.min(w, h);
3500
- const isCircle = radius >= minDim / 2 - 1 && Math.abs(w - h) <= 2 && minDim > 0;
3501
- let type;
3502
- let resolvedRadius = radius;
3503
- let lines;
3504
- let lineHeight;
3505
- if (tag === "IMG" || tag === "SVG" || tag === "VIDEO" || tag === "CANVAS") type = "image";
3506
- else if (isCircle) {
3507
- type = "circle";
3508
- resolvedRadius = Math.floor(minDim / 2);
3509
- } else if (hasText) {
3510
- type = "text";
3511
- lineHeight = Math.round(parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.4 || 16);
3512
- lines = Math.max(1, Math.round(h / lineHeight));
3513
- resolvedRadius = Math.min(radius, 4);
3514
- } else type = "block";
3515
- return freezeShape({
3516
- type,
3517
- x,
3518
- y,
3519
- w,
3520
- h,
3521
- radius: resolvedRadius,
3522
- lines,
3523
- lineHeight
3524
- });
3795
+ function hasBackgroundInStyle(styl) {
3796
+ if (!styl) return false;
3797
+ if (typeof styl === "string") return /background(?:-color|-image)?\s*:/i.test(styl);
3798
+ if (typeof styl === "object") {
3799
+ const s = styl;
3800
+ return "background" in s || "backgroundColor" in s || "backgroundImage" in s || "background-color" in s || "background-image" in s;
3801
+ }
3802
+ return false;
3525
3803
  }
3526
- /**
3527
- * Pre-compute (and freeze) the inline styles used at render time. Doing it once
3528
- * here means rendering 500 blocks doesn't allocate 500 style objects per frame.
3804
+ function mergeStyle(a, b) {
3805
+ if (!b) return a;
3806
+ if (typeof b === "string") return `${Object.entries(a).map(([k, v]) => `${k}: ${v}`).join("; ")}; ${b}`;
3807
+ return {
3808
+ ...a,
3809
+ ...b
3810
+ };
3811
+ }
3812
+ function cloneTag(tag, cls, styl, children) {
3813
+ return h(tag, {
3814
+ class: cls,
3815
+ style: styl
3816
+ }, children);
3817
+ }
3818
+ function textContentSpan(text, animationClass) {
3819
+ return h("span", {
3820
+ class: ["a-skel-text-content", animationClass],
3821
+ "aria-hidden": "true"
3822
+ }, text);
3823
+ }
3824
+ //#endregion
3825
+ //#region src/components/StructuralSkeleton.ts
3826
+ /**
3827
+ * Renders a structural skeleton derived from a slot's vnode tree. Pure render
3828
+ * function — no template, no scoped styles — so the parent's class strings
3829
+ * pass through unchanged and Tailwind utilities continue to drive layout.
3830
+ *
3831
+ * `maxNodes` is forwarded to the walker; cap defaults to 300 (see
3832
+ * `buildStructuralSkeleton`). Beyond that the walk stops emitting and the cap
3833
+ * propagates back as a clipped tree, keeping first-paint bounded.
3529
3834
  */
3530
- function freezeShape(node) {
3531
- const style = Object.freeze({
3532
- left: `${node.x}px`,
3533
- top: `${node.y}px`,
3534
- width: `${node.w}px`,
3535
- height: `${node.h}px`,
3536
- borderRadius: `${node.radius}px`
3537
- });
3538
- let lineStyles;
3539
- if (node.type === "text" && node.lines && node.lines > 1) {
3540
- const lh = node.lineHeight ?? Math.round(node.h / node.lines);
3541
- const barHeight = Math.max(8, Math.round(lh * .7));
3542
- const widthFull = `${node.w}px`;
3543
- const widthLast = `${Math.max(40, Math.round(node.w * .7))}px`;
3544
- const heightStr = `${barHeight}px`;
3545
- const radiusStr = `${node.radius}px`;
3546
- const arr = [];
3547
- for (let i = 1; i <= node.lines; i++) {
3548
- const isLast = i === node.lines;
3549
- arr.push(Object.freeze({
3550
- left: `${node.x}px`,
3551
- top: `${node.y + (i - 1) * lh}px`,
3552
- width: isLast ? widthLast : widthFull,
3553
- height: heightStr,
3554
- borderRadius: radiusStr
3555
- }));
3835
+ const StructuralSkeleton = defineComponent({
3836
+ name: "StructuralSkeleton",
3837
+ props: {
3838
+ vnodes: {
3839
+ type: Array,
3840
+ required: true
3841
+ },
3842
+ animation: {
3843
+ type: String,
3844
+ default: null
3845
+ },
3846
+ maxDepth: {
3847
+ type: Number,
3848
+ default: 8
3849
+ },
3850
+ maxNodes: {
3851
+ type: Number,
3852
+ default: 300
3556
3853
  }
3557
- lineStyles = Object.freeze(arr);
3854
+ },
3855
+ setup(props) {
3856
+ return () => buildStructuralSkeleton(props.vnodes, {
3857
+ animationClass: props.animation,
3858
+ maxDepth: props.maxDepth,
3859
+ maxNodes: props.maxNodes
3860
+ });
3558
3861
  }
3559
- return Object.freeze({
3560
- type: node.type,
3561
- x: node.x,
3562
- y: node.y,
3563
- w: node.w,
3564
- h: node.h,
3565
- radius: node.radius,
3566
- lines: node.lines,
3567
- lineHeight: node.lineHeight,
3568
- style,
3569
- lineStyles
3570
- });
3862
+ });
3863
+ //#endregion
3864
+ //#region src/components/icons.ts
3865
+ /**
3866
+ * Shared placeholder icon renderers for leaf nodes (image / video). Lives at
3867
+ * one site so both `CloneNode.ts` (clone mode) and `StructuralLayerNode.ts`
3868
+ * (Recipe 3 structural mode) render identical glyphs.
3869
+ */
3870
+ function renderImageIcon() {
3871
+ return h("svg", {
3872
+ class: "a-skel-clone-icon",
3873
+ viewBox: "0 0 24 24",
3874
+ "aria-hidden": "true"
3875
+ }, [h("path", {
3876
+ 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",
3877
+ fill: "currentColor"
3878
+ })]);
3879
+ }
3880
+ function renderPlayIcon() {
3881
+ return h("svg", {
3882
+ class: "a-skel-clone-icon",
3883
+ viewBox: "0 0 24 24",
3884
+ "aria-hidden": "true"
3885
+ }, [h("circle", {
3886
+ cx: 12,
3887
+ cy: 12,
3888
+ r: 10,
3889
+ stroke: "currentColor",
3890
+ "stroke-width": 1.5,
3891
+ fill: "none"
3892
+ }), h("path", {
3893
+ d: "M10 8l6 4-6 4V8Z",
3894
+ fill: "currentColor"
3895
+ })]);
3571
3896
  }
3572
3897
  //#endregion
3573
- //#region src/composables/useShapeProbe.ts
3574
- const DEFAULT_RESIZE_DEBOUNCE_MS = 150;
3898
+ //#region src/components/CloneNode.ts
3575
3899
  /**
3576
- * Observe `getTarget()` and capture its rendered shape whenever the element
3577
- * appears or resizes.
3900
+ * `CloneNode` recursive renderer for one node of a `CaptureSnapshot`.
3578
3901
  *
3579
- * Performance:
3580
- * - Initial capture runs via `requestAnimationFrame` so it sneaks into the
3581
- * first idle window after mount no synchronous layout from inside the
3582
- * render queue.
3583
- * - Subsequent `ResizeObserver` callbacks are debounced (default 150 ms) so a
3584
- * drag-resize doesn't trigger a fresh DOM walk per frame.
3585
- * - `walkDom` itself enforces `maxNodes` so even a worst-case capture (10k
3586
- * descendants) returns in bounded time.
3902
+ * Design:
3903
+ * - **Strategy table by `CapturedNode.kind`**. Each kind ('container' |
3904
+ * 'text' | 'image' | 'video' | 'media' | 'block') has its own renderer
3905
+ * function. Adding a new kind is one entry in `RENDERERS` — no edits
3906
+ * elsewhere (Open/Closed).
3907
+ * - **Pure render function**. No reactive state of its own; everything
3908
+ * flows from props.
3909
+ * - **Composition**: per-line text bars share the same shape as block
3910
+ * leaves so there's no behaviour duplication.
3587
3911
  */
3588
- function useShapeProbe(getTarget, options) {
3589
- let observer;
3590
- let frame;
3591
- let timer;
3592
- let hasCaptured = false;
3593
- const debounceMs = options.resizeDebounceMs ?? DEFAULT_RESIZE_DEBOUNCE_MS;
3594
- function cleanup() {
3595
- if (observer) {
3596
- observer.disconnect();
3597
- observer = void 0;
3912
+ const RENDERERS$1 = {
3913
+ container: renderContainer$1,
3914
+ text: renderText,
3915
+ image: (ctx) => renderLeafWithIcon$1(ctx, "image"),
3916
+ video: (ctx) => renderLeafWithIcon$1(ctx, "video"),
3917
+ media: (ctx) => renderLeaf(ctx),
3918
+ block: (ctx) => renderLeaf(ctx)
3919
+ };
3920
+ /**
3921
+ * Container positioned wrapper with captured background / border / shadow.
3922
+ * No `.a-skel` class so the surface shows through exactly as in the real DOM.
3923
+ * Recurse into children via the same `CloneNode`.
3924
+ *
3925
+ * `CapturedNode.{x,y}` is always root-relative, but children render inside
3926
+ * this container — itself `position: absolute`. If we hand children the
3927
+ * root-relative coords as-is, their `left` / `top` resolve against the
3928
+ * container (their new offset parent), doubling the offset. Pass a
3929
+ * `blockStyle` closure that subtracts this container's offset so each
3930
+ * descendant lands at its captured root-relative position regardless of
3931
+ * how deeply it's nested.
3932
+ */
3933
+ function renderContainer$1(ctx) {
3934
+ const { node } = ctx;
3935
+ const childBlockStyle = (n) => ({
3936
+ position: "absolute",
3937
+ left: `${n.x - node.x}px`,
3938
+ top: `${n.y - node.y}px`,
3939
+ width: `${n.w}px`,
3940
+ height: `${n.h}px`,
3941
+ ...n.style
3942
+ });
3943
+ return h("div", {
3944
+ class: "a-skel-clone-container",
3945
+ style: ctx.blockStyle(node),
3946
+ "aria-hidden": "true"
3947
+ }, (node.children ?? []).map((child) => h(CloneNode, {
3948
+ node: child,
3949
+ animClass: ctx.animClass,
3950
+ blockStyle: childBlockStyle,
3951
+ lineStyle: ctx.lineStyle
3952
+ })));
3953
+ }
3954
+ /**
3955
+ * Text — container carrying captured background/border, with per-line
3956
+ * shimmer bars positioned absolutely inside. Each text line becomes one bar
3957
+ * at the exact rendered text rect (multi-line / wrapped / RTL all replay 1:1).
3958
+ */
3959
+ function renderText(ctx) {
3960
+ const { node, animClass } = ctx;
3961
+ const lines = node.textLines ?? [{
3962
+ x: node.x,
3963
+ y: node.y,
3964
+ w: node.w,
3965
+ h: node.h
3966
+ }];
3967
+ return h("div", {
3968
+ class: "a-skel-clone-container a-skel-clone-text",
3969
+ style: ctx.blockStyle(node),
3970
+ "aria-hidden": "true"
3971
+ }, lines.map((line) => h("div", {
3972
+ class: ["a-skel-clone-textbar", animClass],
3973
+ style: ctx.lineStyle({
3974
+ x: line.x - node.x,
3975
+ y: line.y - node.y,
3976
+ w: line.w,
3977
+ h: line.h
3978
+ }, node.style)
3979
+ })));
3980
+ }
3981
+ /** Plain shimmer leaf (block / media). */
3982
+ function renderLeaf(ctx) {
3983
+ return h("div", {
3984
+ class: ["a-skel a-skel-clone-leaf", ctx.animClass],
3985
+ style: ctx.blockStyle(ctx.node),
3986
+ "aria-hidden": "true"
3987
+ });
3988
+ }
3989
+ /** Shimmer leaf with a centered placeholder icon (image / video). */
3990
+ function renderLeafWithIcon$1(ctx, kind) {
3991
+ return h("div", {
3992
+ class: ["a-skel a-skel-clone-leaf a-skel-clone-leaf--with-icon", ctx.animClass],
3993
+ style: ctx.blockStyle(ctx.node),
3994
+ "aria-hidden": "true"
3995
+ }, [kind === "image" ? renderImageIcon() : renderPlayIcon()]);
3996
+ }
3997
+ const CloneNode = defineComponent({
3998
+ name: "CloneNode",
3999
+ props: {
4000
+ node: {
4001
+ type: Object,
4002
+ required: true
4003
+ },
4004
+ animClass: {
4005
+ type: [String, null],
4006
+ default: null
4007
+ },
4008
+ blockStyle: {
4009
+ type: Function,
4010
+ required: true
4011
+ },
4012
+ lineStyle: {
4013
+ type: Function,
4014
+ required: true
3598
4015
  }
3599
- if (frame !== void 0) {
3600
- cancelAnimationFrame(frame);
3601
- frame = void 0;
4016
+ },
4017
+ setup(props) {
4018
+ return () => {
4019
+ const renderer = RENDERERS$1[props.node.kind];
4020
+ return renderer({
4021
+ node: props.node,
4022
+ animClass: props.animClass,
4023
+ blockStyle: props.blockStyle,
4024
+ lineStyle: props.lineStyle
4025
+ });
4026
+ };
4027
+ }
4028
+ });
4029
+ //#endregion
4030
+ //#region \0/plugin-vue/export-helper
4031
+ var export_helper_default = (sfc, props) => {
4032
+ const target = sfc.__vccOpts || sfc;
4033
+ for (const [key, val] of props) target[key] = val;
4034
+ return target;
4035
+ };
4036
+ //#endregion
4037
+ //#region src/components/ASkeletonClone.vue
4038
+ const _sfc_main$18 = /* @__PURE__ */ defineComponent({
4039
+ __name: "ASkeletonClone",
4040
+ props: {
4041
+ shape: {
4042
+ type: Object,
4043
+ required: true
4044
+ },
4045
+ animation: {
4046
+ type: String,
4047
+ required: false,
4048
+ default: "pulse"
4049
+ },
4050
+ class: {
4051
+ type: [
4052
+ String,
4053
+ Array,
4054
+ Object
4055
+ ],
4056
+ required: false
3602
4057
  }
3603
- if (timer !== void 0) {
3604
- clearTimeout(timer);
3605
- timer = void 0;
4058
+ },
4059
+ setup(__props, { expose: __expose }) {
4060
+ __expose();
4061
+ const props = __props;
4062
+ const animClass = computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`);
4063
+ const layerStyle = computed(() => ({
4064
+ position: "relative",
4065
+ width: `${props.shape.width}px`,
4066
+ height: `${props.shape.height}px`
4067
+ }));
4068
+ function blockStyle(n) {
4069
+ return {
4070
+ position: "absolute",
4071
+ left: `${n.x}px`,
4072
+ top: `${n.y}px`,
4073
+ width: `${n.w}px`,
4074
+ height: `${n.h}px`,
4075
+ ...n.style
4076
+ };
3606
4077
  }
3607
- }
3608
- function capture(el) {
3609
- const result = walkDom(el, {
3610
- maxDepth: options.maxDepth,
3611
- maxNodes: options.maxNodes,
3612
- minSize: options.minSize
3613
- });
3614
- if (result.width > 0 && result.height > 0 && result.nodes.length > 0) {
3615
- hasCaptured = true;
3616
- options.onCapture(result);
4078
+ function lineStyle(line, parentStyle) {
4079
+ const radius = parentStyle.borderTopLeftRadius ?? "var(--ak-skel-radius-sm)";
4080
+ return {
4081
+ position: "absolute",
4082
+ left: `${line.x}px`,
4083
+ top: `${line.y}px`,
4084
+ width: `${line.w}px`,
4085
+ height: `${line.h}px`,
4086
+ backgroundColor: "var(--ak-skel-base)",
4087
+ borderRadius: radius
4088
+ };
3617
4089
  }
3618
- }
3619
- function scheduleImmediate(el) {
3620
- if (frame !== void 0) cancelAnimationFrame(frame);
3621
- frame = requestAnimationFrame(() => {
3622
- frame = void 0;
3623
- capture(el);
4090
+ const __returned__ = {
4091
+ props,
4092
+ animClass,
4093
+ layerStyle,
4094
+ blockStyle,
4095
+ lineStyle,
4096
+ get cn() {
4097
+ return cn;
4098
+ },
4099
+ get CloneNode() {
4100
+ return CloneNode;
4101
+ }
4102
+ };
4103
+ Object.defineProperty(__returned__, "__isScriptSetup", {
4104
+ enumerable: false,
4105
+ value: true
3624
4106
  });
4107
+ return __returned__;
3625
4108
  }
3626
- function scheduleDebounced(el) {
3627
- if (timer !== void 0) clearTimeout(timer);
3628
- timer = setTimeout(() => {
3629
- timer = void 0;
3630
- capture(el);
3631
- }, debounceMs);
3632
- }
3633
- watch(getTarget, (el) => {
3634
- cleanup();
3635
- hasCaptured = false;
3636
- if (!el || typeof window === "undefined") return;
3637
- scheduleImmediate(el);
3638
- if (typeof ResizeObserver !== "undefined") {
3639
- observer = new ResizeObserver(() => {
3640
- if (hasCaptured) scheduleDebounced(el);
3641
- });
3642
- observer.observe(el);
3643
- }
3644
- }, {
3645
- immediate: true,
3646
- flush: "post"
3647
- });
3648
- onBeforeUnmount(cleanup);
4109
+ });
4110
+ function _sfc_render$18(_ctx, _cache, $props, $setup, $data, $options) {
4111
+ return openBlock(), createElementBlock("div", {
4112
+ class: normalizeClass($setup.cn("a-skeleton__clone", $setup.props.class)),
4113
+ style: normalizeStyle($setup.layerStyle),
4114
+ role: "status",
4115
+ "aria-busy": "true",
4116
+ "aria-live": "polite"
4117
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.shape.nodes, (node, idx) => {
4118
+ return openBlock(), createBlock($setup["CloneNode"], {
4119
+ key: idx,
4120
+ node,
4121
+ "anim-class": $setup.animClass,
4122
+ "block-style": $setup.blockStyle,
4123
+ "line-style": $setup.lineStyle
4124
+ }, null, 8, ["node", "anim-class"]);
4125
+ }), 128)), _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading…", -1))], 6);
3649
4126
  }
4127
+ var ASkeletonClone_default = /* @__PURE__ */ export_helper_default(_sfc_main$18, [
4128
+ ["render", _sfc_render$18],
4129
+ ["__scopeId", "data-v-1554ff36"],
4130
+ ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/ASkeletonClone.vue"]
4131
+ ]);
3650
4132
  //#endregion
3651
- //#region src/composables/useSkeletonCache.ts
3652
- const memory = /* @__PURE__ */ new Map();
3653
- const STORAGE_PREFIX = "a-skeleton:";
4133
+ //#region src/utils/domRead.ts
3654
4134
  /**
3655
- * Schema version for persisted entries. Bump whenever the `ShapeNode` /
3656
- * `CachedShape` field set changes so stale localStorage payloads from older
3657
- * releases get dropped on read instead of rehydrating into a wrong layout.
4135
+ * Computed values that count as "default" and shouldn't be persisted. Stripped
4136
+ * before a captured `style` object lands on a node so a plain unstyled element
4137
+ * produces an empty style keeps snapshots small and the rendered DOM clean.
3658
4138
  */
3659
- const SCHEMA_VERSION = 1;
4139
+ const SKIP_VALUES = new Set([
4140
+ "none",
4141
+ "normal",
4142
+ "auto",
4143
+ "0px",
4144
+ "0",
4145
+ "0 0",
4146
+ "0% 0%",
4147
+ "0px 0px",
4148
+ "0deg",
4149
+ "0s",
4150
+ "visible",
4151
+ "static",
4152
+ "transparent",
4153
+ "rgba(0, 0, 0, 0)",
4154
+ "rgb(0, 0, 0, 0)",
4155
+ "initial",
4156
+ "inherit",
4157
+ "unset",
4158
+ "currentcolor"
4159
+ ]);
3660
4160
  /**
3661
- * Lookup a captured shape by key. Reads in-memory first, then `localStorage` if
3662
- * `persist` is enabled. SSR-safe bypasses `window` access on the server.
3663
- * Rehydrates pre-computed styles for shapes deserialized from older sessions
3664
- * (Object.freeze + style/lineStyles don't survive `JSON.stringify` round-trip).
3665
- * Drops the entry if it was written by a different schema version.
4161
+ * Read the subset of `props` from `cs` and return a frozen camelCased style
4162
+ * map with the SKIP_VALUES entries omitted. `opacity: 1` is treated as default
4163
+ * (matches CSS' initial value).
3666
4164
  */
3667
- function getCached(key, persist) {
3668
- const hit = memory.get(key);
3669
- if (hit) return hit;
3670
- if (!persist || typeof window === "undefined") return void 0;
3671
- try {
3672
- const storageKey = STORAGE_PREFIX + key;
3673
- const raw = window.localStorage.getItem(storageKey);
3674
- if (!raw) return void 0;
3675
- const parsed = JSON.parse(raw);
3676
- if (parsed.v !== SCHEMA_VERSION) {
3677
- window.localStorage.removeItem(storageKey);
3678
- return;
3679
- }
3680
- const rehydrated = rehydrateShape(parsed);
3681
- memory.set(key, rehydrated);
3682
- return rehydrated;
3683
- } catch {
3684
- return;
4165
+ function readComputedStyles(cs, props) {
4166
+ const out = {};
4167
+ for (const prop of props) {
4168
+ const val = cs.getPropertyValue(prop).trim();
4169
+ if (!val) continue;
4170
+ if (SKIP_VALUES.has(val.toLowerCase())) continue;
4171
+ if (prop === "opacity" && (val === "1" || parseFloat(val) === 1)) continue;
4172
+ out[camelCase(prop)] = val;
3685
4173
  }
4174
+ return Object.freeze(out);
3686
4175
  }
3687
- /** Store a captured shape. `persist=true` mirrors to `localStorage`. */
3688
- function setCached(key, value, persist) {
3689
- memory.set(key, value);
3690
- if (!persist || typeof window === "undefined") return;
3691
- try {
3692
- const lean = {
3693
- v: SCHEMA_VERSION,
3694
- width: value.width,
3695
- height: value.height,
3696
- nodes: leanNodes(value.nodes),
3697
- truncated: value.truncated
3698
- };
3699
- window.localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(lean));
3700
- } catch {}
4176
+ function camelCase(prop) {
4177
+ return prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
3701
4178
  }
3702
- /** Drop a single entry (or all entries when no key). Exposed for tests + manual invalidation. */
3703
- function clearCached(key) {
3704
- if (!key) {
3705
- memory.clear();
3706
- if (typeof window === "undefined") return;
3707
- try {
3708
- for (const k of Object.keys(window.localStorage)) if (k.startsWith(STORAGE_PREFIX)) window.localStorage.removeItem(k);
3709
- } catch {}
3710
- return;
4179
+ /** True when the element has at least one non-whitespace direct text-node child. */
4180
+ function hasDirectText(el) {
4181
+ for (let i = 0; i < el.childNodes.length; i++) {
4182
+ const node = el.childNodes[i];
4183
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").trim().length > 0) return true;
3711
4184
  }
3712
- memory.delete(key);
3713
- if (typeof window === "undefined") return;
3714
- try {
3715
- window.localStorage.removeItem(STORAGE_PREFIX + key);
3716
- } catch {}
3717
- }
3718
- /**
3719
- * Rebuild `style` + `lineStyles` for nodes that lost them via serialization.
3720
- * Walks the array in-place where possible and freezes the result so the render
3721
- * path stays allocation-free.
3722
- */
3723
- function rehydrateShape(shape) {
3724
- const nodes = shape.nodes.map((n) => freezeNodeStyles(n));
3725
- return Object.freeze({
3726
- nodes: Object.freeze(nodes),
3727
- width: shape.width,
3728
- height: shape.height,
3729
- truncated: shape.truncated
3730
- });
3731
- }
3732
- function leanNodes(nodes) {
3733
- return nodes.map((n) => ({
3734
- type: n.type,
3735
- x: n.x,
3736
- y: n.y,
3737
- w: n.w,
3738
- h: n.h,
3739
- radius: n.radius,
3740
- lines: n.lines,
3741
- lineHeight: n.lineHeight
3742
- }));
4185
+ return false;
3743
4186
  }
3744
- function freezeNodeStyles(node) {
3745
- const style = Object.freeze({
3746
- left: `${node.x}px`,
3747
- top: `${node.y}px`,
3748
- width: `${node.w}px`,
3749
- height: `${node.h}px`,
3750
- borderRadius: `${node.radius}px`
3751
- });
3752
- let lineStyles;
3753
- if (node.type === "text" && node.lines && node.lines > 1) {
3754
- const lh = node.lineHeight ?? Math.round(node.h / node.lines);
3755
- const barHeight = Math.max(8, Math.round(lh * .7));
3756
- const widthFull = `${node.w}px`;
3757
- const widthLast = `${Math.max(40, Math.round(node.w * .7))}px`;
3758
- const heightStr = `${barHeight}px`;
3759
- const radiusStr = `${node.radius}px`;
3760
- const arr = [];
3761
- for (let i = 1; i <= node.lines; i++) {
3762
- const isLast = i === node.lines;
3763
- arr.push(Object.freeze({
3764
- left: `${node.x}px`,
3765
- top: `${node.y + (i - 1) * lh}px`,
3766
- width: isLast ? widthLast : widthFull,
3767
- height: heightStr,
3768
- borderRadius: radiusStr
3769
- }));
3770
- }
3771
- lineStyles = Object.freeze(arr);
4187
+ /** Element children, with `data-skeleton-ignore` descendants filtered out. */
4188
+ function collectVisibleChildren(el) {
4189
+ const out = [];
4190
+ for (let i = 0; i < el.children.length; i++) {
4191
+ const c = el.children[i];
4192
+ if (c.dataset?.skeletonIgnore !== void 0) continue;
4193
+ out.push(c);
3772
4194
  }
3773
- return Object.freeze({
3774
- type: node.type,
3775
- x: node.x,
3776
- y: node.y,
3777
- w: node.w,
3778
- h: node.h,
3779
- radius: node.radius,
3780
- lines: node.lines,
3781
- lineHeight: node.lineHeight,
3782
- style,
3783
- lineStyles
3784
- });
4195
+ return out;
3785
4196
  }
3786
- //#endregion
3787
- //#region src/utils/fingerprint.ts
3788
4197
  /**
3789
- * Derive a default cache key from a slot's vnode tree. Returns the first
3790
- * non-comment child's component name (or HTML tag). When the slot only contains
3791
- * text / comments / unknown content, returns `'anonymous'` so the caller can
3792
- * still cache, but with no useful identity encourage an explicit `cacheKey`.
4198
+ * Per-line text rects via `Range.getClientRects()`. Returns one rect per visual
4199
+ * line exact left/width for each line so wrapped paragraphs, RTL last-line
4200
+ * positions, centered headings replay 1:1 without heuristics. Returns
4201
+ * `undefined` if the element has no direct text or the Range API isn't usable.
4202
+ *
4203
+ * Two rects on the same baseline AND horizontally touching (gap ≤ 2 px) are
4204
+ * merged so inline spans on the same line collapse to one bar. Same-baseline
4205
+ * rects on different visual lines (rare; float-into-paragraph layouts) won't
4206
+ * touch horizontally and stay separate.
3793
4207
  */
3794
- function fingerprintSlot(vnodes) {
3795
- if (!vnodes) return "anonymous";
3796
- for (const vnode of vnodes) {
3797
- const tag = describeVNode(vnode);
3798
- if (tag) return tag;
3799
- }
3800
- return "anonymous";
3801
- }
3802
- function describeVNode(vnode) {
3803
- const t = vnode.type;
3804
- if (t === Comment || t === Text) return void 0;
3805
- if (t === Fragment) {
3806
- const children = vnode.children;
3807
- if (Array.isArray(children)) {
3808
- for (const child of children) if (child && typeof child === "object" && "type" in child) {
3809
- const found = describeVNode(child);
3810
- if (found) return found;
3811
- }
3812
- }
4208
+ function captureTextLines(el, origin) {
4209
+ if (typeof document === "undefined" || typeof document.createRange !== "function") return void 0;
4210
+ let range;
4211
+ try {
4212
+ range = document.createRange();
4213
+ range.selectNodeContents(el);
4214
+ } catch {
3813
4215
  return;
3814
4216
  }
3815
- if (typeof t === "string") return t;
3816
- if (typeof t === "object" && t !== null) {
3817
- const named = t.name ?? t.__name ?? t.displayName;
3818
- if (named) return named;
4217
+ const rects = range.getClientRects();
4218
+ if (!rects || rects.length === 0) return void 0;
4219
+ const merged = [];
4220
+ for (let i = 0; i < rects.length; i++) {
4221
+ const r = rects[i];
4222
+ if (r.width <= 0 || r.height <= 0) continue;
4223
+ const lr = {
4224
+ x: Math.round(r.left - origin.left),
4225
+ y: Math.round(r.top - origin.top),
4226
+ w: Math.round(r.width),
4227
+ h: Math.round(r.height)
4228
+ };
4229
+ const last = merged[merged.length - 1];
4230
+ if (last && Math.abs(last.y - lr.y) <= 1 && Math.abs(last.h - lr.h) <= 1 && Math.max(last.x, lr.x) - Math.min(last.x + last.w, lr.x + lr.w) <= 2) {
4231
+ const leftEdge = Math.min(last.x, lr.x);
4232
+ const rightEdge = Math.max(last.x + last.w, lr.x + lr.w);
4233
+ last.x = leftEdge;
4234
+ last.w = rightEdge - leftEdge;
4235
+ } else merged.push(lr);
3819
4236
  }
4237
+ return merged.length > 0 ? merged : void 0;
3820
4238
  }
3821
4239
  //#endregion
3822
- //#region src/utils/buildStructuralSkeleton.ts
4240
+ //#region src/utils/captureStyles.ts
3823
4241
  /**
3824
- * Atomic HTML tags — rendered as a single skeleton block. Their own class/style
3825
- * is preserved so Tailwind utilities (`size-16`, `rounded-full`, …) carry the
3826
- * dimensions across without us needing to measure.
4242
+ * Comprehensive style-capture engine.
4243
+ *
4244
+ * Walks the slot's rendered DOM after mount and captures **every visible CSS
4245
+ * property** of every element it encounters, plus geometry, into a frozen
4246
+ * snapshot tree. The snapshot is replayed by `<ASkeletonClone>` as a tree
4247
+ * of positioned divs each carrying its captured inline style.
4248
+ *
4249
+ * Why this exists (vs the DOM-mirror walker in `buildStructuralSkeleton.ts`):
4250
+ *
4251
+ * - DOM-mirror preserves the slot's vnode tree and inherits styling from the
4252
+ * consumer's `class` / inline `style` attributes. That works for *static*
4253
+ * styles, but it can't see styles applied via:
4254
+ * - JavaScript-set inline style (refs, watchers, animation libs)
4255
+ * - CSS-in-JS runtimes that hash class names per instance
4256
+ * - DaisyUI / shadcn / custom CSS where the computed result differs
4257
+ * from what the class string implies
4258
+ * - Scoped styles compiled with content-hash data attributes
4259
+ *
4260
+ * - `captureSnapshot` reads `getComputedStyle()` for each element — the
4261
+ * **final** computed style after the cascade has resolved. Replaying that
4262
+ * produces a pixel-identical surface no matter what styling system the
4263
+ * consumer uses.
4264
+ *
4265
+ * Cost is bounded by the same `maxNodes` / `maxDepth` / `minSize` filters
4266
+ * as the legacy `walkDom` capture. The whole pass is a single top-down read
4267
+ * with no DOM writes between, so the browser does one layout up front.
3827
4268
  */
3828
- const ATOMIC_TAGS = new Set([
4269
+ const DEFAULT_MAX_DEPTH$2 = 12;
4270
+ const DEFAULT_MAX_NODES$2 = 800;
4271
+ const DEFAULT_MIN_SIZE$2 = 4;
4272
+ /** Tags treated as atomic leaves — no recursion into their contents. */
4273
+ const ATOMIC_TAGS$1 = new Set([
3829
4274
  "img",
3830
4275
  "svg",
3831
4276
  "canvas",
3832
4277
  "video",
4278
+ "audio",
3833
4279
  "input",
3834
4280
  "textarea",
3835
4281
  "select",
3836
- "button",
3837
4282
  "progress",
3838
4283
  "meter",
3839
- "hr"
4284
+ "hr",
4285
+ "iframe",
4286
+ "object",
4287
+ "embed",
4288
+ "picture",
4289
+ "br"
3840
4290
  ]);
3841
- /** Single-line text containers produce one bar. */
3842
- const HEADING_TAGS = new Set([
4291
+ /** Tags whose content is text — captured as text bars (per-line via Range). */
4292
+ const TEXT_OWNERS$1 = new Set([
4293
+ "p",
3843
4294
  "h1",
3844
4295
  "h2",
3845
4296
  "h3",
3846
4297
  "h4",
3847
4298
  "h5",
3848
- "h6"
3849
- ]);
3850
- /** Multi-line text containers — produce N bars with a shortened last line. */
3851
- const PARAGRAPH_TAGS = new Set(["p", "blockquote"]);
3852
- /** Inline text — single bar, but inherits parent font sizing. */
3853
- const INLINE_TEXT_TAGS = new Set([
4299
+ "h6",
3854
4300
  "span",
3855
4301
  "a",
3856
- "small",
3857
- "strong",
3858
4302
  "em",
4303
+ "strong",
4304
+ "small",
3859
4305
  "code",
3860
- "time",
3861
- "label",
3862
4306
  "b",
3863
4307
  "i",
3864
- "mark"
4308
+ "mark",
4309
+ "label",
4310
+ "caption",
4311
+ "time",
4312
+ "dt",
4313
+ "dd",
4314
+ "li",
4315
+ "th",
4316
+ "td",
4317
+ "figcaption",
4318
+ "blockquote",
4319
+ "cite",
4320
+ "q"
3865
4321
  ]);
3866
- const DEFAULT_MAX_DEPTH = 8;
3867
- const DEFAULT_MAX_NODES = 300;
3868
4322
  /**
3869
- * Walk a slot's vnode tree and produce a skeleton that mirrors its rendered
3870
- * structure: same wrapping tags, same `class` strings (so flex/grid/spacing/
3871
- * sizing utilities still apply), but text/atomic leaves replaced with shimmer
3872
- * blocks. The result renders correctly on the FIRST paint without any DOM
3873
- * measurement, as long as the slot's template renders structure even when its
3874
- * data is empty (use `v-if`/`v-else` to swap content, not to gate the wrapper).
4323
+ * Visual CSS properties read from `getComputedStyle()`. Property listed here
4324
+ * is included in the captured snapshot **only when its value differs from the
4325
+ * skip set** so a plain unstyled `<div>` produces an empty style object.
3875
4326
  *
3876
- * Performance: `maxNodes` caps the work. When the cap is hit we stop emitting
3877
- * the caller still gets a valid skeleton, just clipped at the budget. A 1000-
3878
- * row list renders ~300 skeleton rows on first paint and then the measured cache
3879
- * takes over for subsequent loads.
4327
+ * Listed in the order they're written into the snapshot's `style` object so
4328
+ * the replay is deterministic across captures.
3880
4329
  */
3881
- function buildStructuralSkeleton(vnodes, opts) {
3882
- const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
4330
+ const VISUAL_PROPS$1 = [
4331
+ "padding-top",
4332
+ "padding-right",
4333
+ "padding-bottom",
4334
+ "padding-left",
4335
+ "border-top-width",
4336
+ "border-right-width",
4337
+ "border-bottom-width",
4338
+ "border-left-width",
4339
+ "border-top-style",
4340
+ "border-right-style",
4341
+ "border-bottom-style",
4342
+ "border-left-style",
4343
+ "border-top-color",
4344
+ "border-right-color",
4345
+ "border-bottom-color",
4346
+ "border-left-color",
4347
+ "border-top-left-radius",
4348
+ "border-top-right-radius",
4349
+ "border-bottom-right-radius",
4350
+ "border-bottom-left-radius",
4351
+ "background-color",
4352
+ "background-image",
4353
+ "background-position",
4354
+ "background-size",
4355
+ "background-repeat",
4356
+ "background-origin",
4357
+ "background-clip",
4358
+ "box-shadow",
4359
+ "opacity",
4360
+ "filter",
4361
+ "backdrop-filter",
4362
+ "transform",
4363
+ "transform-origin",
4364
+ "mix-blend-mode",
4365
+ "font-family",
4366
+ "font-size",
4367
+ "font-weight",
4368
+ "font-style",
4369
+ "line-height",
4370
+ "letter-spacing",
4371
+ "text-align",
4372
+ "text-transform",
4373
+ "text-decoration-line",
4374
+ "text-decoration-color"
4375
+ ];
4376
+ /**
4377
+ * Snapshot the rendered DOM under `root`, returning a frozen tree of every
4378
+ * visible element + its computed visual styles. Replaying the snapshot
4379
+ * produces a surface visually identical to `root`.
4380
+ */
4381
+ function captureSnapshot(root, options = {}) {
4382
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH$2;
4383
+ const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES$2;
4384
+ const minSize = options.minSize ?? DEFAULT_MIN_SIZE$2;
4385
+ const walkAtomic = options.walkAtomic ?? false;
4386
+ const rootRect = root.getBoundingClientRect();
3883
4387
  const state = {
3884
- emitted: 0,
3885
- cap: opts.maxNodes ?? DEFAULT_MAX_NODES
4388
+ count: 0,
4389
+ truncated: false
3886
4390
  };
3887
- const out = [];
3888
- walk(vnodes, opts, 0, maxDepth, state, out);
3889
- return out;
3890
- }
3891
- function walk(input, opts, depth, max, state, out) {
3892
- if (state.emitted >= state.cap) return;
3893
- if (input == null || typeof input === "boolean") return;
3894
- if (Array.isArray(input)) {
3895
- for (let i = 0; i < input.length; i++) {
3896
- if (state.emitted >= state.cap) return;
3897
- walk(input[i], opts, depth, max, state, out);
4391
+ const nodes = [];
4392
+ for (let i = 0; i < root.children.length; i++) {
4393
+ if (state.count >= maxNodes) {
4394
+ state.truncated = true;
4395
+ break;
3898
4396
  }
3899
- return;
3900
- }
3901
- if (typeof input === "string" || typeof input === "number") {
3902
- if (String(input).trim()) push(out, textBar(opts.animationClass), state);
3903
- return;
3904
- }
3905
- const v = input;
3906
- const type = v.type;
3907
- if (type === Comment) return;
3908
- if (type === Text) {
3909
- if (typeof v.children === "string" ? v.children.trim() : "") push(out, textBar(opts.animationClass), state);
3910
- return;
3911
- }
3912
- if (type === Fragment) {
3913
- walk(v.children, opts, depth, max, state, out);
3914
- return;
3915
- }
3916
- if (typeof type === "string") {
3917
- push(out, transformElement(v, type.toLowerCase(), opts, depth, max, state), state);
3918
- return;
4397
+ const node = capture$1(root.children[i], rootRect, 1, {
4398
+ maxDepth,
4399
+ maxNodes,
4400
+ minSize,
4401
+ walkAtomic,
4402
+ state
4403
+ });
4404
+ if (node) nodes.push(node);
3919
4405
  }
3920
- if (typeof type === "object" || typeof type === "function") push(out, h("div", {
3921
- class: [
3922
- "a-skel-block",
3923
- v.props?.class,
3924
- opts.animationClass
3925
- ],
3926
- style: v.props?.style
3927
- }), state);
3928
- }
3929
- function push(out, vn, state) {
3930
- if (state.emitted >= state.cap) return;
3931
- out.push(vn);
3932
- state.emitted++;
3933
- }
3934
- function transformElement(v, tag, opts, depth, max, state) {
3935
- const cls = v.props?.class;
3936
- const styl = v.props?.style;
3937
- if (ATOMIC_TAGS.has(tag)) return h("div", {
3938
- class: [
3939
- "a-skel-block",
3940
- cls,
3941
- opts.animationClass
3942
- ],
3943
- style: styl
4406
+ return Object.freeze({
4407
+ width: Math.round(rootRect.width),
4408
+ height: Math.round(rootRect.height),
4409
+ nodes: Object.freeze(nodes),
4410
+ truncated: state.truncated,
4411
+ capturedAt: Date.now()
3944
4412
  });
3945
- if (HEADING_TAGS.has(tag)) return h(tag, {
3946
- class: cls,
3947
- style: styl
3948
- }, [textBar(opts.animationClass)]);
3949
- if (PARAGRAPH_TAGS.has(tag)) {
3950
- const children = v.children;
3951
- const recursedChildren = [];
3952
- walk(children, opts, depth + 1, max, state, recursedChildren);
3953
- if (recursedChildren.length > 0) return h(tag, {
3954
- class: cls,
3955
- style: styl
3956
- }, recursedChildren);
3957
- const lines = estimateLines(children, 3);
3958
- return h(tag, {
3959
- class: cls,
3960
- style: styl
3961
- }, multiLineBars(lines, opts.animationClass));
4413
+ }
4414
+ function capture$1(el, origin, depth, ctx) {
4415
+ if (ctx.state.count >= ctx.maxNodes) {
4416
+ ctx.state.truncated = true;
4417
+ return null;
3962
4418
  }
3963
- if (INLINE_TEXT_TAGS.has(tag)) {
3964
- const children = v.children;
3965
- const recursedChildren = [];
3966
- walk(children, opts, depth + 1, max, state, recursedChildren);
3967
- if (recursedChildren.length > 0) return h(tag, {
3968
- class: cls,
3969
- style: styl
3970
- }, recursedChildren);
3971
- return h(tag, {
3972
- class: cls,
3973
- style: styl
3974
- }, [textBar(opts.animationClass)]);
4419
+ if (el.dataset?.skeletonIgnore !== void 0) return null;
4420
+ const cs = window.getComputedStyle(el);
4421
+ if (cs.display === "none" || cs.visibility === "hidden") return null;
4422
+ const o = parseFloat(cs.opacity);
4423
+ if (Number.isFinite(o) && o === 0) return null;
4424
+ const rect = el.getBoundingClientRect();
4425
+ const tag = el.tagName.toLowerCase();
4426
+ const isTextOwnerTag = TEXT_OWNERS$1.has(tag);
4427
+ let effectiveHeight = rect.height;
4428
+ if (isTextOwnerTag && effectiveHeight < ctx.minSize) {
4429
+ const lh = parseFloat(cs.lineHeight);
4430
+ const fs = parseFloat(cs.fontSize);
4431
+ if (Number.isFinite(lh) && lh > 0) effectiveHeight = lh;
4432
+ else if (Number.isFinite(fs) && fs > 0) effectiveHeight = fs * 1.4;
3975
4433
  }
3976
- if (depth >= max) return h("div", {
3977
- class: [
3978
- "a-skel-block",
3979
- cls,
3980
- opts.animationClass
3981
- ],
3982
- style: styl
3983
- });
3984
- const recursed = [];
3985
- walk(v.children, opts, depth + 1, max, state, recursed);
3986
- if (recursed.length === 0) return h("div", {
3987
- class: [
3988
- "a-skel-block",
3989
- cls,
3990
- opts.animationClass
3991
- ],
3992
- style: styl
3993
- });
3994
- return h(tag, {
3995
- class: cls,
3996
- style: styl
3997
- }, recursed);
3998
- }
3999
- function estimateLines(children, max) {
4000
- if (typeof children !== "string") return 1;
4001
- const len = children.trim().length;
4002
- if (len === 0) return 2;
4003
- if (len < 40) return 1;
4004
- if (len < 100) return 2;
4005
- return Math.min(max, 3);
4006
- }
4007
- function multiLineBars(lines, animationClass) {
4008
- const out = [];
4009
- for (let i = 0; i < lines; i++) out.push(textBar(animationClass, i === lines - 1 && lines > 1 ? .65 : 1));
4010
- return out;
4011
- }
4012
- const BAR_STYLE_FULL = Object.freeze({
4013
- display: "inline-block",
4014
- width: "100%",
4015
- height: "0.75em",
4016
- verticalAlign: "middle",
4017
- borderRadius: "4px"
4018
- });
4019
- const PARTIAL_BAR_CACHE = /* @__PURE__ */ new Map();
4020
- function partialBarStyle(widthFraction) {
4021
- const key = Math.round(widthFraction * 10) / 10;
4022
- const hit = PARTIAL_BAR_CACHE.get(key);
4023
- if (hit) return hit;
4024
- const made = Object.freeze({
4025
- display: "inline-block",
4026
- width: `${Math.round(key * 100)}%`,
4027
- height: "0.75em",
4028
- verticalAlign: "middle",
4029
- borderRadius: "4px"
4030
- });
4031
- PARTIAL_BAR_CACHE.set(key, made);
4032
- return made;
4033
- }
4034
- function textBar(animationClass, widthFraction = 1) {
4035
- return h("span", {
4036
- class: [
4037
- "a-skel-block",
4038
- "a-skel-block--text",
4039
- animationClass
4040
- ],
4041
- style: widthFraction === 1 ? BAR_STYLE_FULL : partialBarStyle(widthFraction)
4434
+ if (rect.width < ctx.minSize || effectiveHeight < ctx.minSize) return null;
4435
+ const x = Math.round(rect.left - origin.left);
4436
+ const y = Math.round(rect.top - origin.top);
4437
+ const w = Math.round(rect.width);
4438
+ const h = Math.round(effectiveHeight);
4439
+ const style = readComputedStyles(cs, VISUAL_PROPS$1);
4440
+ const isAtomic = ATOMIC_TAGS$1.has(tag);
4441
+ const hasOwnText = hasDirectText(el);
4442
+ const childrenEls = collectVisibleChildren(el);
4443
+ let kind;
4444
+ if (tag === "img" || tag === "picture") kind = "image";
4445
+ else if (tag === "video") kind = "video";
4446
+ else if (isAtomic) kind = "media";
4447
+ else if (childrenEls.length === 0 && (hasOwnText || isTextOwnerTag)) kind = "text";
4448
+ else if (childrenEls.length === 0) kind = "block";
4449
+ else kind = "container";
4450
+ let textLines;
4451
+ if (kind === "text") textLines = captureTextLines(el, origin);
4452
+ let children;
4453
+ if (kind === "container" && depth < ctx.maxDepth) {
4454
+ children = [];
4455
+ for (const c of childrenEls) {
4456
+ if (ctx.state.count >= ctx.maxNodes) {
4457
+ ctx.state.truncated = true;
4458
+ break;
4459
+ }
4460
+ const childNode = capture$1(c, origin, depth + 1, ctx);
4461
+ if (childNode) children.push(childNode);
4462
+ }
4463
+ if (children.length === 0) {
4464
+ children = void 0;
4465
+ kind = style && Object.keys(style).length > 0 ? "block" : "container";
4466
+ }
4467
+ } else if (kind === "container" && depth >= ctx.maxDepth || isAtomic && ctx.walkAtomic) kind = "block";
4468
+ ctx.state.count++;
4469
+ return Object.freeze({
4470
+ tag,
4471
+ x,
4472
+ y,
4473
+ w,
4474
+ h,
4475
+ style,
4476
+ kind,
4477
+ textLines: textLines ? Object.freeze(textLines) : void 0,
4478
+ children: children ? Object.freeze(children) : void 0
4042
4479
  });
4043
4480
  }
4044
4481
  //#endregion
4045
- //#region src/components/StructuralSkeleton.ts
4482
+ //#region src/components/ASkeleton.vue
4046
4483
  /**
4047
- * Renders a structural skeleton derived from a slot's vnode tree. Pure render
4048
- * function — no template, no scoped styles — so the parent's class strings
4049
- * pass through unchanged and Tailwind utilities continue to drive layout.
4484
+ * `<ASkeleton>` the package's headline wrapper.
4050
4485
  *
4051
- * `maxNodes` is forwarded to the walker; cap defaults to 300 (see
4052
- * `buildStructuralSkeleton`). Beyond that the walk stops emitting and the cap
4053
- * propagates back as a clipped tree, keeping first-paint bounded.
4486
+ * Two rendering strategies, selected via `mode`:
4487
+ * - `mirror` (default): walks the slot's vnode tree (`buildStructuralSkeleton`)
4488
+ * and preserves every element with its real `class` / inline `style`. Pure
4489
+ * Vue, SSR-safe, no DOM read.
4490
+ * - `clone`: mounts the slot off-screen once, snapshots every leaf's
4491
+ * `getComputedStyle()` (`captureSnapshot`), then replays the snapshot via
4492
+ * `<ASkeletonClone>` as positioned divs carrying every captured CSS
4493
+ * property. Pixel-identical regardless of styling system.
4494
+ *
4495
+ * The strategies are exclusive — picking one decides the entire loading-state
4496
+ * render path. The wrapper itself only orchestrates: it doesn't decide
4497
+ * per-element treatment (that's the strategies' job). Single Responsibility:
4498
+ * orchestration + a11y + containment.
4054
4499
  */
4055
- const StructuralSkeleton = defineComponent({
4056
- name: "StructuralSkeleton",
4057
- props: {
4058
- vnodes: {
4059
- type: Array,
4060
- required: true
4061
- },
4062
- animation: {
4063
- type: String,
4064
- default: null
4065
- },
4066
- maxDepth: {
4067
- type: Number,
4068
- default: 8
4069
- },
4070
- maxNodes: {
4071
- type: Number,
4072
- default: 300
4073
- }
4074
- },
4075
- setup(props) {
4076
- return () => buildStructuralSkeleton(props.vnodes, {
4077
- animationClass: props.animation,
4078
- maxDepth: props.maxDepth,
4079
- maxNodes: props.maxNodes
4080
- });
4081
- }
4082
- });
4083
- //#endregion
4084
- //#region \0/plugin-vue/export-helper
4085
- var export_helper_default = (sfc, props) => {
4086
- const target = sfc.__vccOpts || sfc;
4087
- for (const [key, val] of props) target[key] = val;
4088
- return target;
4089
- };
4090
- //#endregion
4091
- //#region src/components/ASkeleton.vue
4092
- const _sfc_main$2 = /* @__PURE__ */ defineComponent({
4500
+ const MAX_RETRY_ATTEMPTS = 5;
4501
+ const _sfc_main$17 = /* @__PURE__ */ defineComponent({
4093
4502
  __name: "ASkeleton",
4094
4503
  props: {
4095
4504
  loading: {
4096
4505
  type: Boolean,
4097
4506
  required: true
4098
4507
  },
4508
+ mode: {
4509
+ type: String,
4510
+ required: false,
4511
+ default: "clone"
4512
+ },
4099
4513
  cacheKey: {
4100
4514
  type: String,
4101
4515
  required: false
@@ -4103,12 +4517,12 @@ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
4103
4517
  maxDepth: {
4104
4518
  type: Number,
4105
4519
  required: false,
4106
- default: 6
4520
+ default: 16
4107
4521
  },
4108
4522
  maxNodes: {
4109
4523
  type: Number,
4110
4524
  required: false,
4111
- default: 500
4525
+ default: 600
4112
4526
  },
4113
4527
  minNodeSize: {
4114
4528
  type: Number,
@@ -4147,61 +4561,90 @@ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
4147
4561
  const props = __props;
4148
4562
  const slots = useSlots();
4149
4563
  const instanceId = useId();
4150
- const resolvedKey = computed(() => props.cacheKey ?? `${fingerprintSlot(slots.default?.())}:${instanceId}`);
4151
- const warnedKeys = /* @__PURE__ */ new Set();
4152
- const cached = shallowRef(getCached(resolvedKey.value, props.persist));
4153
- watch(resolvedKey, (key) => {
4154
- cached.value = getCached(key, props.persist);
4155
- });
4156
- const wrapperRef = ref(null);
4157
- useShapeProbe(() => props.loading ? null : wrapperRef.value, {
4158
- maxDepth: props.maxDepth,
4159
- maxNodes: props.maxNodes,
4160
- minSize: props.minNodeSize,
4161
- onCapture: (shape) => {
4162
- setCached(resolvedKey.value, shape, props.persist);
4163
- cached.value = shape;
4164
- if (shape.truncated && !warnedKeys.has(resolvedKey.value)) {
4165
- warnedKeys.add(resolvedKey.value);
4166
- 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.`);
4167
- }
4168
- }
4169
- });
4170
4564
  const animationClass = computed(() => props.animation === "none" ? null : `a-skel-block--anim-${props.animation}`);
4171
- const layerStyle = computed(() => cached.value ? {
4172
- width: `${cached.value.width}px`,
4173
- height: `${cached.value.height}px`
4174
- } : {});
4175
- const blockClassByType = computed(() => {
4176
- const anim = animationClass.value;
4177
- const suffix = anim ? ` ${anim}` : "";
4178
- return Object.freeze({
4179
- block: `a-skel-block a-skel-block--block${suffix}`,
4180
- text: `a-skel-block a-skel-block--text${suffix}`,
4181
- image: `a-skel-block a-skel-block--image${suffix}`,
4182
- circle: `a-skel-block a-skel-block--circle${suffix}`
4565
+ const cloneAnimation = computed(() => props.animation);
4566
+ const mirrorVNodes = computed(() => props.loading ? slots.default?.() ?? [] : []);
4567
+ const hasContent = computed(() => mirrorVNodes.value.length > 0);
4568
+ const captureRef = ref(null);
4569
+ const snapshot = shallowRef(void 0);
4570
+ const snapshotValid = computed(() => !!snapshot.value && snapshot.value.width > 0 && snapshot.value.height > 0);
4571
+ const snapshotRenderable = computed(() => snapshotValid.value && snapshot.value.nodes.length > 0);
4572
+ let captureFrame;
4573
+ let retryAttempts = 0;
4574
+ function takeSnapshot() {
4575
+ if (captureFrame !== void 0) cancelAnimationFrame(captureFrame);
4576
+ retryAttempts = 0;
4577
+ scheduleCapture();
4578
+ }
4579
+ function scheduleCapture() {
4580
+ captureFrame = requestAnimationFrame(() => {
4581
+ captureFrame = requestAnimationFrame(() => {
4582
+ captureFrame = void 0;
4583
+ if (!captureRef.value) return;
4584
+ const result = captureSnapshot(captureRef.value, {
4585
+ maxDepth: props.maxDepth,
4586
+ maxNodes: props.maxNodes,
4587
+ minSize: props.minNodeSize
4588
+ });
4589
+ if (result.width > 0 && result.height > 0) {
4590
+ snapshot.value = result;
4591
+ return;
4592
+ }
4593
+ if (retryAttempts < MAX_RETRY_ATTEMPTS) {
4594
+ retryAttempts++;
4595
+ scheduleCapture();
4596
+ }
4597
+ });
4183
4598
  });
4599
+ }
4600
+ watch(captureRef, (el) => {
4601
+ if (props.mode !== "clone") return;
4602
+ if (el) takeSnapshot();
4603
+ }, { flush: "post" });
4604
+ watch(() => props.mode, (mode) => {
4605
+ if (mode === "clone" && captureRef.value) takeSnapshot();
4606
+ });
4607
+ watch(() => props.loading, (next, prev) => {
4608
+ if (props.mode !== "clone") return;
4609
+ if (next && !prev && captureRef.value) takeSnapshot();
4610
+ });
4611
+ onBeforeUnmount(() => {
4612
+ if (captureFrame !== void 0) cancelAnimationFrame(captureFrame);
4184
4613
  });
4185
- const structuralVNodes = computed(() => props.loading ? slots.default?.() ?? [] : []);
4186
4614
  const __returned__ = {
4187
4615
  props,
4188
4616
  slots,
4189
4617
  instanceId,
4190
- resolvedKey,
4191
- warnedKeys,
4192
- cached,
4193
- wrapperRef,
4194
4618
  animationClass,
4195
- layerStyle,
4196
- blockClassByType,
4197
- structuralVNodes,
4198
- hasStructure: computed(() => structuralVNodes.value.length > 0),
4619
+ cloneAnimation,
4620
+ mirrorVNodes,
4621
+ hasContent,
4622
+ captureRef,
4623
+ snapshot,
4624
+ snapshotValid,
4625
+ snapshotRenderable,
4626
+ get captureFrame() {
4627
+ return captureFrame;
4628
+ },
4629
+ set captureFrame(v) {
4630
+ captureFrame = v;
4631
+ },
4632
+ get retryAttempts() {
4633
+ return retryAttempts;
4634
+ },
4635
+ set retryAttempts(v) {
4636
+ retryAttempts = v;
4637
+ },
4638
+ MAX_RETRY_ATTEMPTS,
4639
+ takeSnapshot,
4640
+ scheduleCapture,
4199
4641
  get cn() {
4200
4642
  return cn;
4201
4643
  },
4202
4644
  get StructuralSkeleton() {
4203
4645
  return StructuralSkeleton;
4204
- }
4646
+ },
4647
+ ASkeletonClone: ASkeletonClone_default
4205
4648
  };
4206
4649
  Object.defineProperty(__returned__, "__isScriptSetup", {
4207
4650
  enumerable: false,
@@ -4210,44 +4653,61 @@ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
4210
4653
  return __returned__;
4211
4654
  }
4212
4655
  });
4213
- const _hoisted_1 = ["data-loading"];
4214
- const _hoisted_2 = {
4215
- class: "a-skeleton__structural",
4216
- role: "status",
4217
- "aria-live": "polite",
4218
- "aria-busy": "true"
4656
+ const _hoisted_1$5 = [
4657
+ "data-loading",
4658
+ "aria-busy",
4659
+ "aria-live"
4660
+ ];
4661
+ const _hoisted_2$1 = ["aria-hidden"];
4662
+ const _hoisted_3$1 = {
4663
+ key: 0,
4664
+ class: "a-skeleton__mirror a-skeleton__mirror--fallback"
4219
4665
  };
4220
- const _hoisted_3 = {
4221
- class: "a-skeleton__fallback",
4222
- role: "status",
4223
- "aria-busy": "true"
4666
+ const _hoisted_4 = {
4667
+ key: 1,
4668
+ class: "a-skeleton__replay"
4224
4669
  };
4225
- function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
4670
+ const _hoisted_5 = {
4671
+ key: 0,
4672
+ class: "a-skeleton__mirror"
4673
+ };
4674
+ const _hoisted_6 = {
4675
+ key: 1,
4676
+ class: "a-skeleton__fallback"
4677
+ };
4678
+ function _sfc_render$17(_ctx, _cache, $props, $setup, $data, $options) {
4226
4679
  return openBlock(), createElementBlock("div", {
4227
- ref: "wrapperRef",
4228
- class: normalizeClass($setup.cn("a-skeleton", $setup.props.class)),
4229
- "data-loading": $setup.props.loading ? "" : void 0
4230
- }, [$setup.props.loading ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createCommentVNode(" Cache hit: pixel-aligned positioned blocks from a previous measurement.\n Styles are pre-computed during capture so the loop below never calls\n a function or allocates a style object per node. "), $setup.cached ? (openBlock(), createElementBlock("div", {
4231
- key: 0,
4232
- class: "a-skeleton__layer",
4233
- style: normalizeStyle($setup.layerStyle),
4680
+ class: normalizeClass($setup.cn("a-skeleton", `a-skeleton--mode-${$setup.props.mode}`, $setup.props.class)),
4681
+ "data-loading": $setup.props.loading ? "" : void 0,
4234
4682
  role: "status",
4235
- "aria-live": "polite",
4236
- "aria-busy": "true"
4237
- }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.cached.nodes, (node, idx) => {
4238
- return openBlock(), createElementBlock(Fragment, { key: idx }, [node.lineStyles ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(node.lineStyles, (lineStyle, i) => {
4239
- return openBlock(), createElementBlock("div", {
4240
- key: `${idx}-${i}`,
4241
- class: normalizeClass($setup.blockClassByType.text),
4242
- style: normalizeStyle(lineStyle)
4243
- }, null, 6);
4244
- }), 128)) : (openBlock(), createElementBlock("div", {
4245
- key: 1,
4246
- class: normalizeClass($setup.blockClassByType[node.type]),
4247
- style: normalizeStyle(node.style)
4248
- }, null, 6))], 64);
4249
- }), 128))], 4)) : $setup.hasStructure ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createCommentVNode(" Cache miss + slot has structure: render a structural skeleton derived\n from the slot's vnode tree. First paint already looks right. "), createElementVNode("div", _hoisted_2, [createVNode($setup["StructuralSkeleton"], {
4250
- vnodes: $setup.structuralVNodes,
4683
+ "aria-busy": $setup.props.loading ? "true" : void 0,
4684
+ "aria-live": $setup.props.loading ? "polite" : void 0
4685
+ }, [$setup.props.mode === "clone" ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
4686
+ createCommentVNode(" Off-screen capture mount: the slot is always rendered (so we have a\n live target to snapshot from) but visually suppressed while the\n skeleton is showing. `aria-hidden` keeps it out of AT trees too. "),
4687
+ createElementVNode("div", {
4688
+ ref: "captureRef",
4689
+ class: normalizeClass(["a-skeleton__capture", $setup.props.loading ? "a-skeleton__capture--hidden" : null]),
4690
+ "aria-hidden": $setup.props.loading ? "true" : void 0
4691
+ }, [renderSlot(_ctx.$slots, "default", {}, void 0, true)], 10, _hoisted_2$1),
4692
+ createCommentVNode(" Mirror fallback — **always rendered as a backdrop** while loading,\n regardless of snapshot state. The replay layer below sits on top of\n it (higher z-index). If the snapshot turns out to be empty / zero-\n sized / otherwise unrenderable, the mirror underneath still gives the\n user a structural skeleton instead of a blank wrapper. "),
4693
+ $setup.props.loading && $setup.hasContent ? (openBlock(), createElementBlock("div", _hoisted_3$1, [createVNode($setup["StructuralSkeleton"], {
4694
+ vnodes: $setup.mirrorVNodes,
4695
+ animation: $setup.animationClass,
4696
+ "max-depth": $props.maxDepth,
4697
+ "max-nodes": $props.maxNodes
4698
+ }, null, 8, [
4699
+ "vnodes",
4700
+ "animation",
4701
+ "max-depth",
4702
+ "max-nodes"
4703
+ ])])) : createCommentVNode("v-if", true),
4704
+ createCommentVNode(" Replay layer — wrapped in a positioning div whose scoped styles\n are GUARANTEED to apply (a child-component root only inherits the\n parent's scope when no inner div is present; placing the absolute-\n positioning on a regular div in the parent template removes that\n ambiguity). Gated on `snapshotValid` (width AND height > 0) AND\n snapshot.nodes.length > 0 so an empty capture doesn't render a\n transparent overlay that masks the mirror underneath. "),
4705
+ $setup.props.loading && $setup.snapshotRenderable ? (openBlock(), createElementBlock("div", _hoisted_4, [createVNode($setup["ASkeletonClone"], {
4706
+ shape: $setup.snapshot,
4707
+ animation: $setup.cloneAnimation
4708
+ }, null, 8, ["shape", "animation"])])) : createCommentVNode("v-if", true)
4709
+ ], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [$setup.props.loading ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [$setup.hasContent ? (openBlock(), createElementBlock("div", _hoisted_5, [createVNode($setup["StructuralSkeleton"], {
4710
+ vnodes: $setup.mirrorVNodes,
4251
4711
  animation: $setup.animationClass,
4252
4712
  "max-depth": $props.maxDepth,
4253
4713
  "max-nodes": $props.maxNodes
@@ -4256,16 +4716,153 @@ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
4256
4716
  "animation",
4257
4717
  "max-depth",
4258
4718
  "max-nodes"
4259
- ])])], 2112)) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [createCommentVNode(" Cache miss + nothing to walk: generic shimmer. "), createElementVNode("div", _hoisted_3, [renderSlot(_ctx.$slots, "fallback", {}, () => [createElementVNode("div", { class: normalizeClass(["a-skel-block a-skel-block--block a-skel-fallback-default", $setup.animationClass]) }, null, 2)], true)])], 2112))], 64)) : renderSlot(_ctx.$slots, "default", { key: 1 }, void 0, true)], 10, _hoisted_1);
4719
+ ])])) : (openBlock(), createElementBlock("div", _hoisted_6, [renderSlot(_ctx.$slots, "fallback", {}, () => [createElementVNode("div", { class: normalizeClass(["a-skel-block a-skel-block--block a-skel-fallback-default", $setup.animationClass]) }, null, 2)], true)]))], 64)) : renderSlot(_ctx.$slots, "default", { key: 1 }, void 0, true)], 64))], 10, _hoisted_1$5);
4260
4720
  }
4261
- var ASkeleton_default = /* @__PURE__ */ export_helper_default(_sfc_main$2, [
4262
- ["render", _sfc_render$2],
4721
+ var ASkeleton_default = /* @__PURE__ */ export_helper_default(_sfc_main$17, [
4722
+ ["render", _sfc_render$17],
4263
4723
  ["__scopeId", "data-v-16717541"],
4264
4724
  ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/ASkeleton.vue"]
4265
4725
  ]);
4266
4726
  //#endregion
4727
+ //#region src/components/StructuralLayerNode.ts
4728
+ /**
4729
+ * `StructuralLayerNode` — recursive renderer for a `StructuralNode` from a
4730
+ * `StructuralShape` captured by `walkStructural`. Used internally by
4731
+ * `<ASkeletonLayer>`.
4732
+ *
4733
+ * Design:
4734
+ * - **Strategy table** keyed by `node.kind` + `node.leafKind`. Each kind has
4735
+ * its own renderer function. Adding a new kind is one entry in `RENDERERS`
4736
+ * (Open/Closed) — no edits elsewhere.
4737
+ * - **Pure render function**: no reactive state of its own; everything flows
4738
+ * from props.
4739
+ * - **Container preservation**: container nodes emit `<{node.tag}
4740
+ * :class="node.className" :style="node.style">…children…</…>`. The
4741
+ * captured `style` carries comprehensive layout + visual CSS so the
4742
+ * skeleton reads correctly even when the consumer's stylesheet isn't
4743
+ * loaded at the mount point.
4744
+ * - **Leaves**: render `<div class="a-skel + originalClass" :style="style">`.
4745
+ * The original `class` is preserved so utility-first CSS (`mt-4`,
4746
+ * `flex-1`, `inline-block`, …) survives; the captured style carries the
4747
+ * same layout + visual properties used on containers plus `width` +
4748
+ * `height` so the placeholder claims the right space.
4749
+ */
4750
+ const LEAF_RENDERERS = {
4751
+ block: renderLeafBlock,
4752
+ media: renderLeafBlock,
4753
+ text: renderLeafText,
4754
+ image: (node, animClass) => renderLeafWithIcon(node, animClass, "image")
4755
+ };
4756
+ function renderContainer(node, animClass) {
4757
+ return h(node.tag, {
4758
+ class: node.className || void 0,
4759
+ style: node.style,
4760
+ "aria-hidden": "true"
4761
+ }, node.children.map((child) => h(StructuralLayerNode, {
4762
+ node: child,
4763
+ animClass
4764
+ })));
4765
+ }
4766
+ /**
4767
+ * Build the leaf's `class` payload. Always includes `a-skel` (skeleton
4768
+ * surface) + the animation class; the original consumer class trails so it
4769
+ * survives the cascade alongside utility-first frameworks.
4770
+ */
4771
+ function leafClass(node, animClass, extra) {
4772
+ const classes = ["a-skel"];
4773
+ if (extra) classes.push(extra);
4774
+ if (animClass) classes.push(animClass);
4775
+ if (node.className) classes.push(node.className);
4776
+ return classes;
4777
+ }
4778
+ function renderLeafBlock(node, animClass) {
4779
+ return h("div", {
4780
+ class: leafClass(node, animClass),
4781
+ style: node.style,
4782
+ "aria-hidden": "true"
4783
+ });
4784
+ }
4785
+ /**
4786
+ * Text leaf — host carries the captured layout + visual surface (including
4787
+ * the leaf's `width` / `height` so it claims the right space in its parent's
4788
+ * layout). Each captured per-line rect renders as one bar inside the host,
4789
+ * absolutely positioned at the captured rect (rects are leaf-relative; the
4790
+ * host's captured style includes `position: relative` via the `.a-skel-text-host`
4791
+ * default — see styles.src.css).
4792
+ *
4793
+ * When `textLines` is absent (Range API unusable), the leaf renders as a
4794
+ * single shimmer bar at the host's bounds — same as a leaf-block.
4795
+ */
4796
+ function renderLeafText(node, animClass) {
4797
+ const lines = node.textLines;
4798
+ if (!lines || lines.length === 0) return h("div", {
4799
+ class: leafClass(node, animClass, "a-skel--text"),
4800
+ style: node.style,
4801
+ "aria-hidden": "true"
4802
+ });
4803
+ return h("div", {
4804
+ class: ["a-skel-text-host", node.className || void 0],
4805
+ style: node.style,
4806
+ "aria-hidden": "true"
4807
+ }, lines.map((line) => h("div", {
4808
+ class: [
4809
+ "a-skel",
4810
+ "a-skel--text-line",
4811
+ animClass
4812
+ ],
4813
+ style: {
4814
+ position: "absolute",
4815
+ left: `${line.x}px`,
4816
+ top: `${line.y}px`,
4817
+ width: `${line.w}px`,
4818
+ height: `${line.h}px`
4819
+ }
4820
+ })));
4821
+ }
4822
+ function renderLeafWithIcon(node, animClass, kind) {
4823
+ return h("div", {
4824
+ class: leafClass(node, animClass, "a-skel--with-icon"),
4825
+ style: node.style,
4826
+ "aria-hidden": "true"
4827
+ }, [kind === "image" ? renderImageIcon() : renderPlayIcon()]);
4828
+ }
4829
+ const RENDERERS = {
4830
+ container: (ctx) => renderContainer(ctx.node, ctx.animClass),
4831
+ leaf: (ctx) => {
4832
+ const leaf = ctx.node;
4833
+ return LEAF_RENDERERS[leaf.leafKind](leaf, ctx.animClass);
4834
+ }
4835
+ };
4836
+ const StructuralLayerNode = defineComponent({
4837
+ name: "StructuralLayerNode",
4838
+ props: {
4839
+ node: {
4840
+ type: Object,
4841
+ required: true
4842
+ },
4843
+ animClass: {
4844
+ type: [String, null],
4845
+ default: null
4846
+ }
4847
+ },
4848
+ setup(props) {
4849
+ return () => RENDERERS[props.node.kind]({
4850
+ node: props.node,
4851
+ animClass: props.animClass
4852
+ });
4853
+ }
4854
+ });
4855
+ //#endregion
4267
4856
  //#region src/components/ASkeletonLayer.vue
4268
- const _sfc_main$1 = /* @__PURE__ */ defineComponent({
4857
+ /**
4858
+ * `<ASkeletonLayer>` — replays a `StructuralShape` produced by
4859
+ * `walkStructural` / `useSkeleton()` as a tree of preserved containers +
4860
+ * a-skel leaf placeholders. The layer is a transparent shell with no
4861
+ * width / height / position constraints, so it drops into the consumer's
4862
+ * own container and lets the captured tree's flex/grid/spacing dictate
4863
+ * how the skeleton lays itself out.
4864
+ */
4865
+ const _sfc_main$16 = /* @__PURE__ */ defineComponent({
4269
4866
  __name: "ASkeletonLayer",
4270
4867
  props: {
4271
4868
  shape: {
@@ -4289,176 +4886,2366 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
4289
4886
  skipCheck: true
4290
4887
  }
4291
4888
  },
4889
+ setup(__props, { expose: __expose }) {
4890
+ __expose();
4891
+ const props = __props;
4892
+ const __returned__ = {
4893
+ props,
4894
+ animationClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
4895
+ get cn() {
4896
+ return cn;
4897
+ },
4898
+ get StructuralLayerNode() {
4899
+ return StructuralLayerNode;
4900
+ }
4901
+ };
4902
+ Object.defineProperty(__returned__, "__isScriptSetup", {
4903
+ enumerable: false,
4904
+ value: true
4905
+ });
4906
+ return __returned__;
4907
+ }
4908
+ });
4909
+ function _sfc_render$16(_ctx, _cache, $props, $setup, $data, $options) {
4910
+ return $props.shape ? (openBlock(), createElementBlock("div", {
4911
+ key: 0,
4912
+ class: normalizeClass($setup.cn("a-skeleton__layer", $setup.props.class)),
4913
+ role: "status",
4914
+ "aria-live": "polite",
4915
+ "aria-busy": "true"
4916
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList($props.shape.nodes, (node, idx) => {
4917
+ return openBlock(), createBlock($setup["StructuralLayerNode"], {
4918
+ key: idx,
4919
+ node,
4920
+ "anim-class": $setup.animationClass
4921
+ }, null, 8, ["node", "anim-class"]);
4922
+ }), 128)), _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading…", -1))], 2)) : createCommentVNode("v-if", true);
4923
+ }
4924
+ var ASkeletonLayer_default = /* @__PURE__ */ export_helper_default(_sfc_main$16, [["render", _sfc_render$16], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/ASkeletonLayer.vue"]]);
4925
+ //#endregion
4926
+ //#region src/components/ASkeletonBlock.vue
4927
+ const _sfc_main$15 = /* @__PURE__ */ defineComponent({
4928
+ __name: "ASkeletonBlock",
4929
+ props: {
4930
+ type: {
4931
+ type: String,
4932
+ required: false,
4933
+ default: "block"
4934
+ },
4935
+ w: {
4936
+ type: [Number, String],
4937
+ required: false
4938
+ },
4939
+ h: {
4940
+ type: [Number, String],
4941
+ required: false
4942
+ },
4943
+ radius: {
4944
+ type: [Number, String],
4945
+ required: false
4946
+ },
4947
+ lines: {
4948
+ type: Number,
4949
+ required: false,
4950
+ default: 1
4951
+ },
4952
+ animation: {
4953
+ type: String,
4954
+ required: false,
4955
+ default: "shimmer"
4956
+ },
4957
+ class: {
4958
+ type: [
4959
+ Boolean,
4960
+ null,
4961
+ String,
4962
+ Object,
4963
+ Array
4964
+ ],
4965
+ required: false,
4966
+ skipCheck: true
4967
+ }
4968
+ },
4292
4969
  setup(__props, { expose: __expose }) {
4293
4970
  __expose();
4294
4971
  const props = __props;
4295
4972
  const animationClass = computed(() => props.animation === "none" ? null : `a-skel-block--anim-${props.animation}`);
4973
+ const blockClass = computed(() => [
4974
+ "a-skel-block",
4975
+ `a-skel-block--${props.type}`,
4976
+ animationClass.value
4977
+ ]);
4978
+ function toLength(v) {
4979
+ if (v === void 0) return void 0;
4980
+ return typeof v === "number" ? `${v}px` : v;
4981
+ }
4982
+ const radiusValue = computed(() => props.type === "circle" && props.radius === void 0 ? "50%" : toLength(props.radius));
4296
4983
  const __returned__ = {
4297
4984
  props,
4298
4985
  animationClass,
4299
- layerStyle: computed(() => props.shape ? {
4300
- width: `${props.shape.width}px`,
4301
- height: `${props.shape.height}px`
4302
- } : {}),
4303
- blockClassByType: computed(() => {
4304
- const anim = animationClass.value;
4305
- const suffix = anim ? ` ${anim}` : "";
4306
- return Object.freeze({
4307
- block: `a-skel-block a-skel-block--block${suffix}`,
4308
- text: `a-skel-block a-skel-block--text${suffix}`,
4309
- image: `a-skel-block a-skel-block--image${suffix}`,
4310
- circle: `a-skel-block a-skel-block--circle${suffix}`
4311
- });
4312
- }),
4986
+ blockClass,
4987
+ toLength,
4988
+ radiusValue,
4989
+ blockStyle: computed(() => ({
4990
+ width: toLength(props.w),
4991
+ height: toLength(props.h),
4992
+ borderRadius: radiusValue.value
4993
+ })),
4994
+ isMultiLineText: computed(() => props.type === "text" && props.lines > 1),
4313
4995
  get cn() {
4314
4996
  return cn;
4315
4997
  }
4316
- };
4317
- Object.defineProperty(__returned__, "__isScriptSetup", {
4318
- enumerable: false,
4319
- value: true
4320
- });
4321
- return __returned__;
4998
+ };
4999
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5000
+ enumerable: false,
5001
+ value: true
5002
+ });
5003
+ return __returned__;
5004
+ }
5005
+ });
5006
+ function _sfc_render$15(_ctx, _cache, $props, $setup, $data, $options) {
5007
+ return $setup.isMultiLineText ? (openBlock(), createElementBlock("div", {
5008
+ key: 0,
5009
+ class: normalizeClass($setup.cn("a-skel-block-stack", $setup.props.class)),
5010
+ role: "status",
5011
+ "aria-busy": "true"
5012
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.lines, (i) => {
5013
+ return openBlock(), createElementBlock("div", {
5014
+ key: i,
5015
+ class: normalizeClass($setup.blockClass),
5016
+ style: normalizeStyle({
5017
+ height: $setup.blockStyle.height ?? "0.75em",
5018
+ width: i === $setup.props.lines ? "70%" : "100%",
5019
+ borderRadius: $setup.blockStyle.borderRadius ?? "4px"
5020
+ })
5021
+ }, null, 6);
5022
+ }), 128))], 2)) : (openBlock(), createElementBlock("div", {
5023
+ key: 1,
5024
+ class: normalizeClass($setup.cn($setup.blockClass, $setup.props.class)),
5025
+ style: normalizeStyle($setup.blockStyle),
5026
+ role: "status",
5027
+ "aria-busy": "true"
5028
+ }, null, 6));
5029
+ }
5030
+ var ASkeletonBlock_default = /* @__PURE__ */ export_helper_default(_sfc_main$15, [
5031
+ ["render", _sfc_render$15],
5032
+ ["__scopeId", "data-v-bdfba69a"],
5033
+ ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/ASkeletonBlock.vue"]
5034
+ ]);
5035
+ //#endregion
5036
+ //#region src/components/variants/ASkeletonText.vue
5037
+ const _sfc_main$14 = /* @__PURE__ */ defineComponent({
5038
+ __name: "ASkeletonText",
5039
+ props: {
5040
+ lines: {
5041
+ type: Number,
5042
+ required: false,
5043
+ default: 1
5044
+ },
5045
+ width: {
5046
+ type: [Number, String],
5047
+ required: false
5048
+ },
5049
+ animation: {
5050
+ type: String,
5051
+ required: false,
5052
+ default: "pulse"
5053
+ },
5054
+ class: {
5055
+ type: [
5056
+ Boolean,
5057
+ null,
5058
+ String,
5059
+ Object,
5060
+ Array
5061
+ ],
5062
+ required: false,
5063
+ skipCheck: true
5064
+ }
5065
+ },
5066
+ setup(__props, { expose: __expose }) {
5067
+ __expose();
5068
+ const props = __props;
5069
+ const animClass = computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`);
5070
+ const rootStyle = computed(() => props.width !== void 0 ? { width: typeof props.width === "number" ? `${props.width}px` : String(props.width) } : void 0);
5071
+ function widthForLine(i) {
5072
+ if (i === props.lines - 1 && props.lines > 1) return "65%";
5073
+ const palette = [
5074
+ "100%",
5075
+ "92%",
5076
+ "88%",
5077
+ "95%"
5078
+ ];
5079
+ return palette[i % palette.length];
5080
+ }
5081
+ const __returned__ = {
5082
+ props,
5083
+ animClass,
5084
+ rootStyle,
5085
+ widthForLine,
5086
+ get cn() {
5087
+ return cn;
5088
+ }
5089
+ };
5090
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5091
+ enumerable: false,
5092
+ value: true
5093
+ });
5094
+ return __returned__;
5095
+ }
5096
+ });
5097
+ function _sfc_render$14(_ctx, _cache, $props, $setup, $data, $options) {
5098
+ return openBlock(), createElementBlock("div", {
5099
+ class: normalizeClass($setup.cn("a-skel-variants-text", $setup.props.class)),
5100
+ style: normalizeStyle($setup.rootStyle),
5101
+ role: "status",
5102
+ "aria-live": "polite",
5103
+ "aria-busy": "true"
5104
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.lines, (i) => {
5105
+ return openBlock(), createElementBlock("div", {
5106
+ key: i,
5107
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-text", $setup.animClass)),
5108
+ style: normalizeStyle({
5109
+ width: $setup.widthForLine(i - 1),
5110
+ marginTop: i > 1 ? "0.5em" : void 0
5111
+ })
5112
+ }, null, 6);
5113
+ }), 128)), _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading…", -1))], 6);
5114
+ }
5115
+ var ASkeletonText_default = /* @__PURE__ */ export_helper_default(_sfc_main$14, [["render", _sfc_render$14], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonText.vue"]]);
5116
+ //#endregion
5117
+ //#region src/components/variants/ASkeletonHeading.vue
5118
+ const _sfc_main$13 = /* @__PURE__ */ defineComponent({
5119
+ __name: "ASkeletonHeading",
5120
+ props: {
5121
+ level: {
5122
+ type: Number,
5123
+ required: false,
5124
+ default: 2
5125
+ },
5126
+ width: {
5127
+ type: [Number, String],
5128
+ required: false
5129
+ },
5130
+ animation: {
5131
+ type: String,
5132
+ required: false,
5133
+ default: "pulse"
5134
+ },
5135
+ class: {
5136
+ type: [
5137
+ Boolean,
5138
+ null,
5139
+ String,
5140
+ Object,
5141
+ Array
5142
+ ],
5143
+ required: false,
5144
+ skipCheck: true
5145
+ }
5146
+ },
5147
+ setup(__props, { expose: __expose }) {
5148
+ __expose();
5149
+ const props = __props;
5150
+ const animClass = computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`);
5151
+ const HEIGHT_BY_LEVEL = {
5152
+ 1: "2.25rem",
5153
+ 2: "1.75rem",
5154
+ 3: "1.5rem",
5155
+ 4: "1.25rem",
5156
+ 5: "1.125rem",
5157
+ 6: "1rem"
5158
+ };
5159
+ const __returned__ = {
5160
+ props,
5161
+ animClass,
5162
+ HEIGHT_BY_LEVEL,
5163
+ rootStyle: computed(() => ({
5164
+ width: props.width !== void 0 ? typeof props.width === "number" ? `${props.width}px` : String(props.width) : "60%",
5165
+ height: HEIGHT_BY_LEVEL[props.level]
5166
+ })),
5167
+ get cn() {
5168
+ return cn;
5169
+ }
5170
+ };
5171
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5172
+ enumerable: false,
5173
+ value: true
5174
+ });
5175
+ return __returned__;
5176
+ }
5177
+ });
5178
+ function _sfc_render$13(_ctx, _cache, $props, $setup, $data, $options) {
5179
+ return openBlock(), createElementBlock("div", {
5180
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-heading", $setup.animClass, $setup.props.class)),
5181
+ style: normalizeStyle($setup.rootStyle),
5182
+ role: "status",
5183
+ "aria-busy": "true"
5184
+ }, [..._cache[0] || (_cache[0] = [createElementVNode("span", { class: "a-skel-sr-only" }, "Loading heading…", -1)])], 6);
5185
+ }
5186
+ var ASkeletonHeading_default = /* @__PURE__ */ export_helper_default(_sfc_main$13, [["render", _sfc_render$13], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonHeading.vue"]]);
5187
+ //#endregion
5188
+ //#region src/components/variants/ASkeletonAvatar.vue
5189
+ const _sfc_main$12 = /* @__PURE__ */ defineComponent({
5190
+ __name: "ASkeletonAvatar",
5191
+ props: {
5192
+ size: {
5193
+ type: [Number, String],
5194
+ required: false,
5195
+ default: 48
5196
+ },
5197
+ shape: {
5198
+ type: String,
5199
+ required: false,
5200
+ default: "circle"
5201
+ },
5202
+ animation: {
5203
+ type: String,
5204
+ required: false,
5205
+ default: "pulse"
5206
+ },
5207
+ class: {
5208
+ type: [
5209
+ Boolean,
5210
+ null,
5211
+ String,
5212
+ Object,
5213
+ Array
5214
+ ],
5215
+ required: false,
5216
+ skipCheck: true
5217
+ }
5218
+ },
5219
+ setup(__props, { expose: __expose }) {
5220
+ __expose();
5221
+ const props = __props;
5222
+ const __returned__ = {
5223
+ props,
5224
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
5225
+ sizeStyle: computed(() => {
5226
+ const s = typeof props.size === "number" ? `${props.size}px` : String(props.size);
5227
+ return {
5228
+ width: s,
5229
+ height: s,
5230
+ borderRadius: props.shape === "circle" ? "9999px" : props.shape === "rounded" ? "var(--ak-skel-radius)" : "0"
5231
+ };
5232
+ }),
5233
+ get cn() {
5234
+ return cn;
5235
+ }
5236
+ };
5237
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5238
+ enumerable: false,
5239
+ value: true
5240
+ });
5241
+ return __returned__;
5242
+ }
5243
+ });
5244
+ function _sfc_render$12(_ctx, _cache, $props, $setup, $data, $options) {
5245
+ return openBlock(), createElementBlock("div", {
5246
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-avatar", $setup.animClass, $setup.props.class)),
5247
+ style: normalizeStyle($setup.sizeStyle),
5248
+ role: "status",
5249
+ "aria-busy": "true"
5250
+ }, [..._cache[0] || (_cache[0] = [createElementVNode("span", { class: "a-skel-sr-only" }, "Loading avatar…", -1)])], 6);
5251
+ }
5252
+ var ASkeletonAvatar_default = /* @__PURE__ */ export_helper_default(_sfc_main$12, [["render", _sfc_render$12], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonAvatar.vue"]]);
5253
+ //#endregion
5254
+ //#region src/components/variants/ASkeletonImage.vue
5255
+ const _sfc_main$11 = /* @__PURE__ */ defineComponent({
5256
+ __name: "ASkeletonImage",
5257
+ props: {
5258
+ ratio: {
5259
+ type: String,
5260
+ required: false,
5261
+ default: "16 / 9"
5262
+ },
5263
+ width: {
5264
+ type: [Number, String],
5265
+ required: false
5266
+ },
5267
+ height: {
5268
+ type: [Number, String],
5269
+ required: false
5270
+ },
5271
+ showIcon: {
5272
+ type: Boolean,
5273
+ required: false,
5274
+ default: true
5275
+ },
5276
+ animation: {
5277
+ type: String,
5278
+ required: false,
5279
+ default: "pulse"
5280
+ },
5281
+ class: {
5282
+ type: [
5283
+ Boolean,
5284
+ null,
5285
+ String,
5286
+ Object,
5287
+ Array
5288
+ ],
5289
+ required: false,
5290
+ skipCheck: true
5291
+ }
5292
+ },
5293
+ setup(__props, { expose: __expose }) {
5294
+ __expose();
5295
+ const props = __props;
5296
+ const __returned__ = {
5297
+ props,
5298
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
5299
+ rootStyle: computed(() => {
5300
+ const s = {};
5301
+ if (props.ratio) s.aspectRatio = props.ratio;
5302
+ if (props.width !== void 0) s.width = typeof props.width === "number" ? `${props.width}px` : String(props.width);
5303
+ if (props.height !== void 0) s.height = typeof props.height === "number" ? `${props.height}px` : String(props.height);
5304
+ return s;
5305
+ }),
5306
+ get cn() {
5307
+ return cn;
5308
+ }
5309
+ };
5310
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5311
+ enumerable: false,
5312
+ value: true
5313
+ });
5314
+ return __returned__;
5315
+ }
5316
+ });
5317
+ const _hoisted_1$4 = {
5318
+ key: 0,
5319
+ class: "size-10",
5320
+ viewBox: "0 0 24 24",
5321
+ fill: "none",
5322
+ xmlns: "http://www.w3.org/2000/svg",
5323
+ "aria-hidden": "true"
5324
+ };
5325
+ function _sfc_render$11(_ctx, _cache, $props, $setup, $data, $options) {
5326
+ return openBlock(), createElementBlock("div", {
5327
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-image", $setup.animClass, $setup.props.class)),
5328
+ style: normalizeStyle($setup.rootStyle),
5329
+ role: "status",
5330
+ "aria-busy": "true"
5331
+ }, [$setup.props.showIcon ? (openBlock(), createElementBlock("svg", _hoisted_1$4, [..._cache[0] || (_cache[0] = [createElementVNode("path", {
5332
+ 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",
5333
+ fill: "currentColor"
5334
+ }, null, -1)])])) : createCommentVNode("v-if", true), _cache[1] || (_cache[1] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading image…", -1))], 6);
5335
+ }
5336
+ var ASkeletonImage_default = /* @__PURE__ */ export_helper_default(_sfc_main$11, [["render", _sfc_render$11], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonImage.vue"]]);
5337
+ //#endregion
5338
+ //#region src/components/variants/ASkeletonVideo.vue
5339
+ const _sfc_main$10 = /* @__PURE__ */ defineComponent({
5340
+ __name: "ASkeletonVideo",
5341
+ props: {
5342
+ ratio: {
5343
+ type: String,
5344
+ required: false,
5345
+ default: "16 / 9"
5346
+ },
5347
+ width: {
5348
+ type: [Number, String],
5349
+ required: false
5350
+ },
5351
+ height: {
5352
+ type: [Number, String],
5353
+ required: false
5354
+ },
5355
+ showIcon: {
5356
+ type: Boolean,
5357
+ required: false,
5358
+ default: true
5359
+ },
5360
+ animation: {
5361
+ type: String,
5362
+ required: false,
5363
+ default: "pulse"
5364
+ },
5365
+ class: {
5366
+ type: [
5367
+ Boolean,
5368
+ null,
5369
+ String,
5370
+ Object,
5371
+ Array
5372
+ ],
5373
+ required: false,
5374
+ skipCheck: true
5375
+ }
5376
+ },
5377
+ setup(__props, { expose: __expose }) {
5378
+ __expose();
5379
+ const props = __props;
5380
+ const __returned__ = {
5381
+ props,
5382
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
5383
+ rootStyle: computed(() => {
5384
+ const s = {};
5385
+ if (props.ratio) s.aspectRatio = props.ratio;
5386
+ if (props.width !== void 0) s.width = typeof props.width === "number" ? `${props.width}px` : String(props.width);
5387
+ if (props.height !== void 0) s.height = typeof props.height === "number" ? `${props.height}px` : String(props.height);
5388
+ return s;
5389
+ }),
5390
+ get cn() {
5391
+ return cn;
5392
+ }
5393
+ };
5394
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5395
+ enumerable: false,
5396
+ value: true
5397
+ });
5398
+ return __returned__;
5399
+ }
5400
+ });
5401
+ const _hoisted_1$3 = {
5402
+ key: 0,
5403
+ class: "size-12",
5404
+ viewBox: "0 0 24 24",
5405
+ fill: "none",
5406
+ xmlns: "http://www.w3.org/2000/svg",
5407
+ "aria-hidden": "true"
5408
+ };
5409
+ function _sfc_render$10(_ctx, _cache, $props, $setup, $data, $options) {
5410
+ return openBlock(), createElementBlock("div", {
5411
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-video", $setup.animClass, $setup.props.class)),
5412
+ style: normalizeStyle($setup.rootStyle),
5413
+ role: "status",
5414
+ "aria-busy": "true"
5415
+ }, [$setup.props.showIcon ? (openBlock(), createElementBlock("svg", _hoisted_1$3, [..._cache[0] || (_cache[0] = [createElementVNode("circle", {
5416
+ cx: "12",
5417
+ cy: "12",
5418
+ r: "10",
5419
+ stroke: "currentColor",
5420
+ "stroke-width": "1.5"
5421
+ }, null, -1), createElementVNode("path", {
5422
+ d: "M10 8l6 4-6 4V8Z",
5423
+ fill: "currentColor"
5424
+ }, null, -1)])])) : createCommentVNode("v-if", true), _cache[1] || (_cache[1] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading video…", -1))], 6);
5425
+ }
5426
+ var ASkeletonVideo_default = /* @__PURE__ */ export_helper_default(_sfc_main$10, [["render", _sfc_render$10], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonVideo.vue"]]);
5427
+ //#endregion
5428
+ //#region src/components/variants/ASkeletonButton.vue
5429
+ const _sfc_main$9 = /* @__PURE__ */ defineComponent({
5430
+ __name: "ASkeletonButton",
5431
+ props: {
5432
+ width: {
5433
+ type: [Number, String],
5434
+ required: false,
5435
+ default: 120
5436
+ },
5437
+ height: {
5438
+ type: [Number, String],
5439
+ required: false,
5440
+ default: 40
5441
+ },
5442
+ outlined: {
5443
+ type: Boolean,
5444
+ required: false
5445
+ },
5446
+ animation: {
5447
+ type: String,
5448
+ required: false,
5449
+ default: "pulse"
5450
+ },
5451
+ class: {
5452
+ type: [
5453
+ Boolean,
5454
+ null,
5455
+ String,
5456
+ Object,
5457
+ Array
5458
+ ],
5459
+ required: false,
5460
+ skipCheck: true
5461
+ }
5462
+ },
5463
+ setup(__props, { expose: __expose }) {
5464
+ __expose();
5465
+ const props = __props;
5466
+ const __returned__ = {
5467
+ props,
5468
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
5469
+ rootStyle: computed(() => ({
5470
+ width: typeof props.width === "number" ? `${props.width}px` : String(props.width),
5471
+ height: typeof props.height === "number" ? `${props.height}px` : String(props.height),
5472
+ ...props.outlined ? {
5473
+ backgroundColor: "transparent",
5474
+ border: "1px solid var(--ak-skel-ring)"
5475
+ } : {}
5476
+ })),
5477
+ get cn() {
5478
+ return cn;
5479
+ }
5480
+ };
5481
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5482
+ enumerable: false,
5483
+ value: true
5484
+ });
5485
+ return __returned__;
5486
+ }
5487
+ });
5488
+ function _sfc_render$9(_ctx, _cache, $props, $setup, $data, $options) {
5489
+ return openBlock(), createElementBlock("div", {
5490
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-button", $setup.animClass, $setup.props.class)),
5491
+ style: normalizeStyle($setup.rootStyle),
5492
+ role: "status",
5493
+ "aria-busy": "true"
5494
+ }, [..._cache[0] || (_cache[0] = [createElementVNode("span", { class: "a-skel-sr-only" }, "Loading button…", -1)])], 6);
5495
+ }
5496
+ var ASkeletonButton_default = /* @__PURE__ */ export_helper_default(_sfc_main$9, [["render", _sfc_render$9], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonButton.vue"]]);
5497
+ //#endregion
5498
+ //#region src/components/variants/ASkeletonInput.vue
5499
+ const _sfc_main$8 = /* @__PURE__ */ defineComponent({
5500
+ __name: "ASkeletonInput",
5501
+ props: {
5502
+ width: {
5503
+ type: [Number, String],
5504
+ required: false,
5505
+ default: "100%"
5506
+ },
5507
+ height: {
5508
+ type: [Number, String],
5509
+ required: false,
5510
+ default: 40
5511
+ },
5512
+ animation: {
5513
+ type: String,
5514
+ required: false,
5515
+ default: "pulse"
5516
+ },
5517
+ class: {
5518
+ type: [
5519
+ Boolean,
5520
+ null,
5521
+ String,
5522
+ Object,
5523
+ Array
5524
+ ],
5525
+ required: false,
5526
+ skipCheck: true
5527
+ }
5528
+ },
5529
+ setup(__props, { expose: __expose }) {
5530
+ __expose();
5531
+ const props = __props;
5532
+ const __returned__ = {
5533
+ props,
5534
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
5535
+ rootStyle: computed(() => ({
5536
+ width: typeof props.width === "number" ? `${props.width}px` : String(props.width),
5537
+ height: typeof props.height === "number" ? `${props.height}px` : String(props.height)
5538
+ })),
5539
+ get cn() {
5540
+ return cn;
5541
+ }
5542
+ };
5543
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5544
+ enumerable: false,
5545
+ value: true
5546
+ });
5547
+ return __returned__;
5548
+ }
5549
+ });
5550
+ function _sfc_render$8(_ctx, _cache, $props, $setup, $data, $options) {
5551
+ return openBlock(), createElementBlock("div", {
5552
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-input", $setup.animClass, $setup.props.class)),
5553
+ style: normalizeStyle($setup.rootStyle),
5554
+ role: "status",
5555
+ "aria-busy": "true"
5556
+ }, [..._cache[0] || (_cache[0] = [createElementVNode("span", { class: "a-skel-sr-only" }, "Loading input…", -1)])], 6);
5557
+ }
5558
+ var ASkeletonInput_default = /* @__PURE__ */ export_helper_default(_sfc_main$8, [["render", _sfc_render$8], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonInput.vue"]]);
5559
+ //#endregion
5560
+ //#region src/components/variants/ASkeletonListItem.vue
5561
+ const _sfc_main$7 = /* @__PURE__ */ defineComponent({
5562
+ __name: "ASkeletonListItem",
5563
+ props: {
5564
+ avatar: {
5565
+ type: Boolean,
5566
+ required: false,
5567
+ default: true
5568
+ },
5569
+ lines: {
5570
+ type: Number,
5571
+ required: false,
5572
+ default: 2
5573
+ },
5574
+ trailing: {
5575
+ type: Boolean,
5576
+ required: false,
5577
+ default: false
5578
+ },
5579
+ animation: {
5580
+ type: String,
5581
+ required: false,
5582
+ default: "pulse"
5583
+ },
5584
+ class: {
5585
+ type: [
5586
+ Boolean,
5587
+ null,
5588
+ String,
5589
+ Object,
5590
+ Array
5591
+ ],
5592
+ required: false,
5593
+ skipCheck: true
5594
+ }
5595
+ },
5596
+ setup(__props, { expose: __expose }) {
5597
+ __expose();
5598
+ const props = __props;
5599
+ const __returned__ = {
5600
+ props,
5601
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
5602
+ get cn() {
5603
+ return cn;
5604
+ },
5605
+ ASkeletonAvatar: ASkeletonAvatar_default,
5606
+ ASkeletonText: ASkeletonText_default
5607
+ };
5608
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5609
+ enumerable: false,
5610
+ value: true
5611
+ });
5612
+ return __returned__;
5613
+ }
5614
+ });
5615
+ const _hoisted_1$2 = { class: "a-skel-variant-list-item__body" };
5616
+ function _sfc_render$7(_ctx, _cache, $props, $setup, $data, $options) {
5617
+ return openBlock(), createElementBlock("div", {
5618
+ class: normalizeClass($setup.cn("a-skel-variant-list-item", $setup.props.class)),
5619
+ role: "status",
5620
+ "aria-busy": "true"
5621
+ }, [
5622
+ $setup.props.avatar ? (openBlock(), createBlock($setup["ASkeletonAvatar"], {
5623
+ key: 0,
5624
+ size: 40,
5625
+ animation: $setup.props.animation
5626
+ }, null, 8, ["animation"])) : createCommentVNode("v-if", true),
5627
+ createElementVNode("div", _hoisted_1$2, [createVNode($setup["ASkeletonText"], {
5628
+ lines: $setup.props.lines,
5629
+ animation: $setup.props.animation
5630
+ }, null, 8, ["lines", "animation"])]),
5631
+ $setup.props.trailing ? (openBlock(), createElementBlock("div", {
5632
+ key: 1,
5633
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-button", $setup.animClass)),
5634
+ style: {
5635
+ width: "40px",
5636
+ height: "24px"
5637
+ }
5638
+ }, null, 2)) : createCommentVNode("v-if", true),
5639
+ _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading list item…", -1))
5640
+ ], 2);
5641
+ }
5642
+ var ASkeletonListItem_default = /* @__PURE__ */ export_helper_default(_sfc_main$7, [["render", _sfc_render$7], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonListItem.vue"]]);
5643
+ //#endregion
5644
+ //#region src/components/variants/ASkeletonCard.vue
5645
+ const _sfc_main$6 = /* @__PURE__ */ defineComponent({
5646
+ __name: "ASkeletonCard",
5647
+ props: {
5648
+ media: {
5649
+ type: Boolean,
5650
+ required: false,
5651
+ default: true
5652
+ },
5653
+ heading: {
5654
+ type: Boolean,
5655
+ required: false,
5656
+ default: true
5657
+ },
5658
+ lines: {
5659
+ type: Number,
5660
+ required: false,
5661
+ default: 3
5662
+ },
5663
+ actions: {
5664
+ type: Boolean,
5665
+ required: false,
5666
+ default: true
5667
+ },
5668
+ footerAvatar: {
5669
+ type: Boolean,
5670
+ required: false,
5671
+ default: false
5672
+ },
5673
+ animation: {
5674
+ type: String,
5675
+ required: false,
5676
+ default: "pulse"
5677
+ },
5678
+ class: {
5679
+ type: [
5680
+ Boolean,
5681
+ null,
5682
+ String,
5683
+ Object,
5684
+ Array
5685
+ ],
5686
+ required: false,
5687
+ skipCheck: true
5688
+ }
5689
+ },
5690
+ setup(__props, { expose: __expose }) {
5691
+ __expose();
5692
+ const __returned__ = {
5693
+ get cn() {
5694
+ return cn;
5695
+ },
5696
+ ASkeletonImage: ASkeletonImage_default,
5697
+ ASkeletonHeading: ASkeletonHeading_default,
5698
+ ASkeletonText: ASkeletonText_default,
5699
+ ASkeletonButton: ASkeletonButton_default,
5700
+ ASkeletonAvatar: ASkeletonAvatar_default
5701
+ };
5702
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5703
+ enumerable: false,
5704
+ value: true
5705
+ });
5706
+ return __returned__;
5707
+ }
5708
+ });
5709
+ const _hoisted_1$1 = {
5710
+ key: 2,
5711
+ class: "mt-2 flex gap-2"
5712
+ };
5713
+ const _hoisted_2 = {
5714
+ key: 3,
5715
+ class: "mt-3 flex items-center gap-3"
5716
+ };
5717
+ const _hoisted_3 = { style: { "flex": "1" } };
5718
+ function _sfc_render$6(_ctx, _cache, $props, $setup, $data, $options) {
5719
+ return openBlock(), createElementBlock("div", {
5720
+ class: normalizeClass($setup.cn("a-skel-variant-card", _ctx.$props.class)),
5721
+ role: "status",
5722
+ "aria-busy": "true"
5723
+ }, [
5724
+ _ctx.$props.media ? (openBlock(), createBlock($setup["ASkeletonImage"], {
5725
+ key: 0,
5726
+ animation: _ctx.$props.animation
5727
+ }, null, 8, ["animation"])) : createCommentVNode("v-if", true),
5728
+ _ctx.$props.heading ? (openBlock(), createBlock($setup["ASkeletonHeading"], {
5729
+ key: 1,
5730
+ level: 3,
5731
+ animation: _ctx.$props.animation
5732
+ }, null, 8, ["animation"])) : createCommentVNode("v-if", true),
5733
+ createVNode($setup["ASkeletonText"], {
5734
+ lines: _ctx.$props.lines,
5735
+ animation: _ctx.$props.animation
5736
+ }, null, 8, ["lines", "animation"]),
5737
+ _ctx.$props.actions ? (openBlock(), createElementBlock("div", _hoisted_1$1, [createVNode($setup["ASkeletonButton"], {
5738
+ width: 96,
5739
+ height: 36,
5740
+ animation: _ctx.$props.animation
5741
+ }, null, 8, ["animation"]), createVNode($setup["ASkeletonButton"], {
5742
+ width: 96,
5743
+ height: 36,
5744
+ outlined: "",
5745
+ animation: _ctx.$props.animation
5746
+ }, null, 8, ["animation"])])) : createCommentVNode("v-if", true),
5747
+ _ctx.$props.footerAvatar ? (openBlock(), createElementBlock("div", _hoisted_2, [createVNode($setup["ASkeletonAvatar"], {
5748
+ size: 36,
5749
+ animation: _ctx.$props.animation
5750
+ }, null, 8, ["animation"]), createElementVNode("div", _hoisted_3, [createVNode($setup["ASkeletonText"], {
5751
+ lines: 2,
5752
+ animation: _ctx.$props.animation
5753
+ }, null, 8, ["animation"])])])) : createCommentVNode("v-if", true),
5754
+ _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading card…", -1))
5755
+ ], 2);
5756
+ }
5757
+ var ASkeletonCard_default = /* @__PURE__ */ export_helper_default(_sfc_main$6, [["render", _sfc_render$6], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonCard.vue"]]);
5758
+ //#endregion
5759
+ //#region src/components/variants/ASkeletonTable.vue
5760
+ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
5761
+ __name: "ASkeletonTable",
5762
+ props: {
5763
+ rows: {
5764
+ type: Number,
5765
+ required: false,
5766
+ default: 5
5767
+ },
5768
+ columns: {
5769
+ type: Number,
5770
+ required: false,
5771
+ default: 4
5772
+ },
5773
+ showHeader: {
5774
+ type: Boolean,
5775
+ required: false,
5776
+ default: true
5777
+ },
5778
+ animation: {
5779
+ type: String,
5780
+ required: false,
5781
+ default: "pulse"
5782
+ },
5783
+ class: {
5784
+ type: [
5785
+ Boolean,
5786
+ null,
5787
+ String,
5788
+ Object,
5789
+ Array
5790
+ ],
5791
+ required: false,
5792
+ skipCheck: true
5793
+ }
5794
+ },
5795
+ setup(__props, { expose: __expose }) {
5796
+ __expose();
5797
+ const props = __props;
5798
+ const __returned__ = {
5799
+ props,
5800
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
5801
+ rowStyle: computed(() => ({ gridTemplateColumns: `repeat(${props.columns}, minmax(0, 1fr))` })),
5802
+ get cn() {
5803
+ return cn;
5804
+ }
5805
+ };
5806
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5807
+ enumerable: false,
5808
+ value: true
5809
+ });
5810
+ return __returned__;
5811
+ }
5812
+ });
5813
+ function _sfc_render$5(_ctx, _cache, $props, $setup, $data, $options) {
5814
+ return openBlock(), createElementBlock("div", {
5815
+ class: normalizeClass($setup.cn("a-skel-variant-table", $setup.props.class)),
5816
+ role: "status",
5817
+ "aria-busy": "true"
5818
+ }, [
5819
+ $setup.props.showHeader ? (openBlock(), createElementBlock("div", {
5820
+ key: 0,
5821
+ class: "a-skel-variant-table__row",
5822
+ style: normalizeStyle($setup.rowStyle)
5823
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.columns, (c) => {
5824
+ return openBlock(), createElementBlock("div", {
5825
+ key: `h-${c}`,
5826
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-heading", $setup.animClass)),
5827
+ style: {
5828
+ height: "1.5rem",
5829
+ width: "70%"
5830
+ }
5831
+ }, null, 2);
5832
+ }), 128))], 4)) : createCommentVNode("v-if", true),
5833
+ (openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.rows, (r) => {
5834
+ return openBlock(), createElementBlock("div", {
5835
+ key: r,
5836
+ class: "a-skel-variant-table__row",
5837
+ style: normalizeStyle($setup.rowStyle)
5838
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.columns, (c) => {
5839
+ return openBlock(), createElementBlock("div", {
5840
+ key: `${r}-${c}`,
5841
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-text", $setup.animClass)),
5842
+ style: normalizeStyle({ width: c === 1 ? "85%" : c === $setup.props.columns ? "50%" : "70%" })
5843
+ }, null, 6);
5844
+ }), 128))], 4);
5845
+ }), 128)),
5846
+ _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading table…", -1))
5847
+ ], 2);
5848
+ }
5849
+ var ASkeletonTable_default = /* @__PURE__ */ export_helper_default(_sfc_main$5, [["render", _sfc_render$5], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonTable.vue"]]);
5850
+ //#endregion
5851
+ //#region src/components/variants/ASkeletonChart.vue
5852
+ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
5853
+ __name: "ASkeletonChart",
5854
+ props: {
5855
+ bars: {
5856
+ type: Number,
5857
+ required: false,
5858
+ default: 7
5859
+ },
5860
+ height: {
5861
+ type: [Number, String],
5862
+ required: false,
5863
+ default: "8rem"
5864
+ },
5865
+ showHeader: {
5866
+ type: Boolean,
5867
+ required: false,
5868
+ default: true
5869
+ },
5870
+ animation: {
5871
+ type: String,
5872
+ required: false,
5873
+ default: "pulse"
5874
+ },
5875
+ class: {
5876
+ type: [
5877
+ Boolean,
5878
+ null,
5879
+ String,
5880
+ Object,
5881
+ Array
5882
+ ],
5883
+ required: false,
5884
+ skipCheck: true
5885
+ }
5886
+ },
5887
+ setup(__props, { expose: __expose }) {
5888
+ __expose();
5889
+ const props = __props;
5890
+ const animClass = computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`);
5891
+ const chartStyle = computed(() => ({ height: typeof props.height === "number" ? `${props.height}px` : String(props.height) }));
5892
+ const HEIGHTS = [
5893
+ 62,
5894
+ 78,
5895
+ 45,
5896
+ 91,
5897
+ 68,
5898
+ 82,
5899
+ 55,
5900
+ 73,
5901
+ 39,
5902
+ 88,
5903
+ 60,
5904
+ 74
5905
+ ];
5906
+ function barHeight(i) {
5907
+ return `${HEIGHTS[i % HEIGHTS.length]}%`;
5908
+ }
5909
+ const __returned__ = {
5910
+ props,
5911
+ animClass,
5912
+ chartStyle,
5913
+ HEIGHTS,
5914
+ barHeight,
5915
+ get cn() {
5916
+ return cn;
5917
+ },
5918
+ ASkeletonHeading: ASkeletonHeading_default,
5919
+ ASkeletonText: ASkeletonText_default
5920
+ };
5921
+ Object.defineProperty(__returned__, "__isScriptSetup", {
5922
+ enumerable: false,
5923
+ value: true
5924
+ });
5925
+ return __returned__;
5926
+ }
5927
+ });
5928
+ const _hoisted_1 = { style: { "margin-top": "0.4rem" } };
5929
+ function _sfc_render$4(_ctx, _cache, $props, $setup, $data, $options) {
5930
+ return openBlock(), createElementBlock("div", {
5931
+ class: normalizeClass($setup.cn($setup.props.class)),
5932
+ role: "status",
5933
+ "aria-busy": "true"
5934
+ }, [
5935
+ $setup.props.showHeader ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createVNode($setup["ASkeletonHeading"], {
5936
+ level: 4,
5937
+ animation: $setup.props.animation,
5938
+ width: "45%"
5939
+ }, null, 8, ["animation"]), createElementVNode("div", _hoisted_1, [createVNode($setup["ASkeletonText"], {
5940
+ lines: 1,
5941
+ animation: $setup.props.animation,
5942
+ width: "60%"
5943
+ }, null, 8, ["animation"])])], 64)) : createCommentVNode("v-if", true),
5944
+ createElementVNode("div", {
5945
+ class: "a-skel-variant-chart",
5946
+ style: normalizeStyle([$setup.chartStyle, { "margin-top": "1rem" }])
5947
+ }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.bars, (i) => {
5948
+ return openBlock(), createElementBlock("div", {
5949
+ key: i,
5950
+ class: normalizeClass($setup.cn("a-skel a-skel-variant-chart__bar", $setup.animClass)),
5951
+ style: normalizeStyle({ height: $setup.barHeight(i - 1) })
5952
+ }, null, 6);
5953
+ }), 128))], 4),
5954
+ _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading chart…", -1))
5955
+ ], 2);
5956
+ }
5957
+ var ASkeletonChart_default = /* @__PURE__ */ export_helper_default(_sfc_main$4, [["render", _sfc_render$4], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonChart.vue"]]);
5958
+ //#endregion
5959
+ //#region src/components/variants/ASkeletonForm.vue
5960
+ const _sfc_main$3 = /* @__PURE__ */ defineComponent({
5961
+ __name: "ASkeletonForm",
5962
+ props: {
5963
+ fields: {
5964
+ type: Number,
5965
+ required: false,
5966
+ default: 3
5967
+ },
5968
+ showSubmit: {
5969
+ type: Boolean,
5970
+ required: false,
5971
+ default: true
5972
+ },
5973
+ animation: {
5974
+ type: String,
5975
+ required: false,
5976
+ default: "pulse"
5977
+ },
5978
+ class: {
5979
+ type: [
5980
+ Boolean,
5981
+ null,
5982
+ String,
5983
+ Object,
5984
+ Array
5985
+ ],
5986
+ required: false,
5987
+ skipCheck: true
5988
+ }
5989
+ },
5990
+ setup(__props, { expose: __expose }) {
5991
+ __expose();
5992
+ const __returned__ = {
5993
+ get cn() {
5994
+ return cn;
5995
+ },
5996
+ ASkeletonText: ASkeletonText_default,
5997
+ ASkeletonInput: ASkeletonInput_default,
5998
+ ASkeletonButton: ASkeletonButton_default
5999
+ };
6000
+ Object.defineProperty(__returned__, "__isScriptSetup", {
6001
+ enumerable: false,
6002
+ value: true
6003
+ });
6004
+ return __returned__;
6005
+ }
6006
+ });
6007
+ function _sfc_render$3(_ctx, _cache, $props, $setup, $data, $options) {
6008
+ return openBlock(), createElementBlock("div", {
6009
+ class: normalizeClass($setup.cn("flex flex-col gap-4", _ctx.$props.class)),
6010
+ role: "status",
6011
+ "aria-busy": "true"
6012
+ }, [
6013
+ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.$props.fields, (i) => {
6014
+ return openBlock(), createElementBlock("div", {
6015
+ key: i,
6016
+ class: "flex flex-col gap-2"
6017
+ }, [createVNode($setup["ASkeletonText"], {
6018
+ lines: 1,
6019
+ width: "30%",
6020
+ animation: _ctx.$props.animation
6021
+ }, null, 8, ["animation"]), createVNode($setup["ASkeletonInput"], { animation: _ctx.$props.animation }, null, 8, ["animation"])]);
6022
+ }), 128)),
6023
+ _ctx.$props.showSubmit ? (openBlock(), createBlock($setup["ASkeletonButton"], {
6024
+ key: 0,
6025
+ width: 120,
6026
+ height: 40,
6027
+ animation: _ctx.$props.animation
6028
+ }, null, 8, ["animation"])) : createCommentVNode("v-if", true),
6029
+ _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading form…", -1))
6030
+ ], 2);
6031
+ }
6032
+ var ASkeletonForm_default = /* @__PURE__ */ export_helper_default(_sfc_main$3, [["render", _sfc_render$3], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonForm.vue"]]);
6033
+ //#endregion
6034
+ //#region src/components/variants/ASkeletonArticle.vue
6035
+ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
6036
+ __name: "ASkeletonArticle",
6037
+ props: {
6038
+ media: {
6039
+ type: Boolean,
6040
+ required: false,
6041
+ default: true
6042
+ },
6043
+ paragraphs: {
6044
+ type: Number,
6045
+ required: false,
6046
+ default: 3
6047
+ },
6048
+ linesPerParagraph: {
6049
+ type: Number,
6050
+ required: false,
6051
+ default: 4
6052
+ },
6053
+ animation: {
6054
+ type: String,
6055
+ required: false,
6056
+ default: "pulse"
6057
+ },
6058
+ class: {
6059
+ type: [
6060
+ Boolean,
6061
+ null,
6062
+ String,
6063
+ Object,
6064
+ Array
6065
+ ],
6066
+ required: false,
6067
+ skipCheck: true
6068
+ }
6069
+ },
6070
+ setup(__props, { expose: __expose }) {
6071
+ __expose();
6072
+ const __returned__ = {
6073
+ get cn() {
6074
+ return cn;
6075
+ },
6076
+ ASkeletonHeading: ASkeletonHeading_default,
6077
+ ASkeletonText: ASkeletonText_default,
6078
+ ASkeletonImage: ASkeletonImage_default
6079
+ };
6080
+ Object.defineProperty(__returned__, "__isScriptSetup", {
6081
+ enumerable: false,
6082
+ value: true
6083
+ });
6084
+ return __returned__;
6085
+ }
6086
+ });
6087
+ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
6088
+ return openBlock(), createElementBlock("article", {
6089
+ class: normalizeClass($setup.cn("flex flex-col gap-4", _ctx.$props.class)),
6090
+ role: "status",
6091
+ "aria-busy": "true"
6092
+ }, [
6093
+ createVNode($setup["ASkeletonHeading"], {
6094
+ level: 1,
6095
+ animation: _ctx.$props.animation
6096
+ }, null, 8, ["animation"]),
6097
+ _ctx.$props.media ? (openBlock(), createBlock($setup["ASkeletonImage"], {
6098
+ key: 0,
6099
+ animation: _ctx.$props.animation
6100
+ }, null, 8, ["animation"])) : createCommentVNode("v-if", true),
6101
+ (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.$props.paragraphs, (i) => {
6102
+ return openBlock(), createElementBlock("div", { key: i }, [createVNode($setup["ASkeletonText"], {
6103
+ lines: _ctx.$props.linesPerParagraph,
6104
+ animation: _ctx.$props.animation
6105
+ }, null, 8, ["lines", "animation"])]);
6106
+ }), 128)),
6107
+ _cache[0] || (_cache[0] = createElementVNode("span", { class: "a-skel-sr-only" }, "Loading article…", -1))
6108
+ ], 2);
6109
+ }
6110
+ var ASkeletonArticle_default = /* @__PURE__ */ export_helper_default(_sfc_main$2, [["render", _sfc_render$2], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonArticle.vue"]]);
6111
+ //#endregion
6112
+ //#region src/components/variants/ASkeletonDivider.vue
6113
+ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
6114
+ __name: "ASkeletonDivider",
6115
+ props: {
6116
+ thickness: {
6117
+ type: Number,
6118
+ required: false,
6119
+ default: 1
6120
+ },
6121
+ animation: {
6122
+ type: String,
6123
+ required: false,
6124
+ default: "pulse"
6125
+ },
6126
+ class: {
6127
+ type: [
6128
+ Boolean,
6129
+ null,
6130
+ String,
6131
+ Object,
6132
+ Array
6133
+ ],
6134
+ required: false,
6135
+ skipCheck: true
6136
+ }
6137
+ },
6138
+ setup(__props, { expose: __expose }) {
6139
+ __expose();
6140
+ const props = __props;
6141
+ const __returned__ = {
6142
+ props,
6143
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
6144
+ rootStyle: computed(() => ({
6145
+ height: `${props.thickness}px`,
6146
+ width: "100%"
6147
+ })),
6148
+ get cn() {
6149
+ return cn;
6150
+ }
6151
+ };
6152
+ Object.defineProperty(__returned__, "__isScriptSetup", {
6153
+ enumerable: false,
6154
+ value: true
6155
+ });
6156
+ return __returned__;
6157
+ }
6158
+ });
6159
+ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
6160
+ return openBlock(), createElementBlock("div", {
6161
+ class: normalizeClass($setup.cn("a-skel", $setup.animClass, $setup.props.class)),
6162
+ style: normalizeStyle($setup.rootStyle),
6163
+ role: "separator",
6164
+ "aria-hidden": "true"
6165
+ }, null, 6);
6166
+ }
6167
+ var ASkeletonDivider_default = /* @__PURE__ */ export_helper_default(_sfc_main$1, [["render", _sfc_render$1], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonDivider.vue"]]);
6168
+ //#endregion
6169
+ //#region src/components/variants/ASkeletonChip.vue
6170
+ const _sfc_main = /* @__PURE__ */ defineComponent({
6171
+ __name: "ASkeletonChip",
6172
+ props: {
6173
+ width: {
6174
+ type: [Number, String],
6175
+ required: false,
6176
+ default: 80
6177
+ },
6178
+ height: {
6179
+ type: [Number, String],
6180
+ required: false,
6181
+ default: 24
6182
+ },
6183
+ animation: {
6184
+ type: String,
6185
+ required: false,
6186
+ default: "pulse"
6187
+ },
6188
+ class: {
6189
+ type: [
6190
+ Boolean,
6191
+ null,
6192
+ String,
6193
+ Object,
6194
+ Array
6195
+ ],
6196
+ required: false,
6197
+ skipCheck: true
6198
+ }
6199
+ },
6200
+ setup(__props, { expose: __expose }) {
6201
+ __expose();
6202
+ const props = __props;
6203
+ const __returned__ = {
6204
+ props,
6205
+ animClass: computed(() => props.animation === "none" ? null : `a-skel-anim-${props.animation}`),
6206
+ rootStyle: computed(() => ({
6207
+ width: typeof props.width === "number" ? `${props.width}px` : String(props.width),
6208
+ height: typeof props.height === "number" ? `${props.height}px` : String(props.height),
6209
+ borderRadius: "9999px",
6210
+ display: "inline-block"
6211
+ })),
6212
+ get cn() {
6213
+ return cn;
6214
+ }
6215
+ };
6216
+ Object.defineProperty(__returned__, "__isScriptSetup", {
6217
+ enumerable: false,
6218
+ value: true
6219
+ });
6220
+ return __returned__;
6221
+ }
6222
+ });
6223
+ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
6224
+ return openBlock(), createElementBlock("span", {
6225
+ class: normalizeClass($setup.cn("a-skel", $setup.animClass, $setup.props.class)),
6226
+ style: normalizeStyle($setup.rootStyle),
6227
+ role: "status",
6228
+ "aria-busy": "true"
6229
+ }, null, 6);
6230
+ }
6231
+ var ASkeletonChip_default = /* @__PURE__ */ export_helper_default(_sfc_main, [["render", _sfc_render], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/variants/ASkeletonChip.vue"]]);
6232
+ //#endregion
6233
+ //#region src/utils/walkDom.ts
6234
+ const DEFAULT_MAX_NODES$1 = 500;
6235
+ const DEFAULT_MIN_SIZE$1 = 4;
6236
+ const LEAF_TAGS = new Set([
6237
+ "IMG",
6238
+ "SVG",
6239
+ "CANVAS",
6240
+ "VIDEO",
6241
+ "INPUT",
6242
+ "TEXTAREA",
6243
+ "SELECT",
6244
+ "BUTTON",
6245
+ "PROGRESS",
6246
+ "METER",
6247
+ "HR"
6248
+ ]);
6249
+ /**
6250
+ * Walk `root`'s descendants and produce a list of shimmer blocks that mirror its
6251
+ * rendered layout. Coordinates are relative to `root`'s top-left so the result can
6252
+ * be replayed in any container of the same size.
6253
+ *
6254
+ * Performance:
6255
+ * - `maxNodes` caps the walk so a 5000-row table doesn't lock up the main thread.
6256
+ * - `minSize` filters out hairlines (1-2 px paddings, decorative dots) that
6257
+ * inflate node count without adding visual signal.
6258
+ * - All `getBoundingClientRect` / `getComputedStyle` reads happen in a single
6259
+ * top-down pass with no intervening writes, so the browser does one layout
6260
+ * up front and serves cached values from then on (no layout thrashing).
6261
+ * - Each emitted `ShapeNode` has a frozen pre-computed `style` (and `lineStyles`
6262
+ * for multi-line text) so the render path is allocation-free.
6263
+ */
6264
+ function walkDom(root, options) {
6265
+ const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES$1;
6266
+ const minSize = options.minSize ?? DEFAULT_MIN_SIZE$1;
6267
+ const nodes = [];
6268
+ const rootRect = root.getBoundingClientRect();
6269
+ let truncated = false;
6270
+ function visit(el, depth) {
6271
+ if (nodes.length >= maxNodes) {
6272
+ truncated = true;
6273
+ return;
6274
+ }
6275
+ const html = el;
6276
+ if (html.dataset?.skeletonIgnore !== void 0) return;
6277
+ const cs = window.getComputedStyle(el);
6278
+ if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0") return;
6279
+ const rect = el.getBoundingClientRect();
6280
+ if (rect.width < minSize || rect.height < minSize) return;
6281
+ const tag = el.tagName.toUpperCase();
6282
+ const isLeafTag = LEAF_TAGS.has(tag);
6283
+ const hasStop = html.dataset?.skeletonStop !== void 0;
6284
+ const childElements = [];
6285
+ for (let i = 0; i < el.children.length; i++) {
6286
+ const c = el.children[i];
6287
+ if (c.dataset?.skeletonIgnore === void 0) childElements.push(c);
6288
+ }
6289
+ const hasOwnText = hasDirectTextContent(el);
6290
+ const reachedDepth = depth >= options.maxDepth;
6291
+ if (isLeafTag || hasStop || reachedDepth || childElements.length === 0) {
6292
+ const node = elementToShape(el, tag, cs, rect, rootRect, hasOwnText);
6293
+ if (node) nodes.push(node);
6294
+ return;
6295
+ }
6296
+ const containerNode = containerSurfaceToShape(cs, rect, rootRect);
6297
+ if (containerNode) nodes.push(containerNode);
6298
+ for (let i = 0; i < childElements.length; i++) {
6299
+ if (nodes.length >= maxNodes) {
6300
+ truncated = true;
6301
+ return;
6302
+ }
6303
+ visit(childElements[i], depth + 1);
6304
+ }
6305
+ }
6306
+ for (let i = 0; i < root.children.length; i++) {
6307
+ if (nodes.length >= maxNodes) {
6308
+ truncated = true;
6309
+ break;
6310
+ }
6311
+ visit(root.children[i], 1);
6312
+ }
6313
+ return Object.freeze({
6314
+ nodes: Object.freeze(nodes),
6315
+ width: Math.round(rootRect.width),
6316
+ height: Math.round(rootRect.height),
6317
+ truncated
6318
+ });
6319
+ }
6320
+ function hasDirectTextContent(el) {
6321
+ for (let i = 0; i < el.childNodes.length; i++) {
6322
+ const node = el.childNodes[i];
6323
+ if (node.nodeType === Node.TEXT_NODE && (node.textContent ?? "").trim().length > 0) return true;
6324
+ }
6325
+ return false;
6326
+ }
6327
+ /**
6328
+ * Emit a backing block for a container with its own visible surface — real
6329
+ * background, border, box-shadow, or non-full opacity. Geometry is the
6330
+ * container's exact bounding rect so the width / height / radius match the
6331
+ * real DOM 1:1. Returns `null` when the container has no visible surface
6332
+ * (a plain unstyled `<div>` is layout-only and shouldn't add a block).
6333
+ */
6334
+ function containerSurfaceToShape(cs, rect, origin) {
6335
+ const bg = readBackgroundColor(cs);
6336
+ const border = readBorder(cs);
6337
+ const boxShadow = readBoxShadow(cs);
6338
+ const opacity = readOpacity(cs);
6339
+ if (!bg && !border && !boxShadow && opacity === void 0) return null;
6340
+ return freezeShape({
6341
+ type: "block",
6342
+ x: Math.round(rect.left - origin.left),
6343
+ y: Math.round(rect.top - origin.top),
6344
+ w: Math.round(rect.width),
6345
+ h: Math.round(rect.height),
6346
+ radius: parseFloat(cs.borderRadius) || 0,
6347
+ bg,
6348
+ border,
6349
+ boxShadow,
6350
+ opacity
6351
+ });
6352
+ }
6353
+ function elementToShape(el, tag, cs, rect, origin, hasText) {
6354
+ const x = Math.round(rect.left - origin.left);
6355
+ const y = Math.round(rect.top - origin.top);
6356
+ const w = Math.round(rect.width);
6357
+ const h = Math.round(rect.height);
6358
+ const radius = parseFloat(cs.borderRadius) || 0;
6359
+ const minDim = Math.min(w, h);
6360
+ const isCircle = radius >= minDim / 2 - 1 && Math.abs(w - h) <= 2 && minDim > 0;
6361
+ let type;
6362
+ let resolvedRadius = radius;
6363
+ let lines;
6364
+ let lineHeight;
6365
+ let textRects;
6366
+ let textAlign;
6367
+ if (tag === "IMG" || tag === "SVG" || tag === "VIDEO" || tag === "CANVAS") type = "image";
6368
+ else if (isCircle) {
6369
+ type = "circle";
6370
+ resolvedRadius = Math.floor(minDim / 2);
6371
+ } else if (hasText) {
6372
+ type = "text";
6373
+ lineHeight = Math.round(parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.4 || 16);
6374
+ lines = Math.max(1, Math.round(h / lineHeight));
6375
+ resolvedRadius = Math.min(radius, 4);
6376
+ textAlign = readTextAlign(cs);
6377
+ textRects = measureTextRects(el, origin);
6378
+ } else type = "block";
6379
+ const bg = readBackgroundColor(cs);
6380
+ const border = readBorder(cs);
6381
+ const boxShadow = readBoxShadow(cs);
6382
+ const opacity = readOpacity(cs);
6383
+ return freezeShape({
6384
+ type,
6385
+ x,
6386
+ y,
6387
+ w,
6388
+ h,
6389
+ radius: resolvedRadius,
6390
+ lines,
6391
+ lineHeight,
6392
+ textRects,
6393
+ bg,
6394
+ border,
6395
+ boxShadow,
6396
+ opacity,
6397
+ textAlign
6398
+ });
6399
+ }
6400
+ /**
6401
+ * Per-line text rects via `Range.getClientRects()`. Returns one rect per visual
6402
+ * line of rendered text — exact left/width for each line so wrapped paragraphs,
6403
+ * RTL last-line position, centered headings all replay 1:1 without heuristics.
6404
+ * Returns `undefined` if the element has no direct text content or the Range
6405
+ * API isn't usable in this environment.
6406
+ */
6407
+ function measureTextRects(el, origin) {
6408
+ if (typeof document === "undefined" || typeof document.createRange !== "function") return void 0;
6409
+ let range;
6410
+ try {
6411
+ range = document.createRange();
6412
+ range.selectNodeContents(el);
6413
+ } catch {
6414
+ return;
6415
+ }
6416
+ const rects = range.getClientRects();
6417
+ if (!rects || rects.length === 0) return void 0;
6418
+ const merged = [];
6419
+ for (let i = 0; i < rects.length; i++) {
6420
+ const r = rects[i];
6421
+ if (r.width <= 0 || r.height <= 0) continue;
6422
+ const lr = {
6423
+ x: Math.round(r.left - origin.left),
6424
+ y: Math.round(r.top - origin.top),
6425
+ w: Math.round(r.width),
6426
+ h: Math.round(r.height)
6427
+ };
6428
+ const last = merged[merged.length - 1];
6429
+ if (last && Math.abs(last.y - lr.y) <= 1 && Math.abs(last.h - lr.h) <= 1 && Math.max(last.x, lr.x) - Math.min(last.x + last.w, lr.x + lr.w) <= 2) {
6430
+ const leftEdge = Math.min(last.x, lr.x);
6431
+ const rightEdge = Math.max(last.x + last.w, lr.x + lr.w);
6432
+ last.x = leftEdge;
6433
+ last.w = rightEdge - leftEdge;
6434
+ } else merged.push(lr);
6435
+ }
6436
+ return merged.length > 0 ? merged : void 0;
6437
+ }
6438
+ const TRANSPARENT_RE = /^(transparent|rgba?\([^)]*,\s*0(\.0+)?\s*\)|hsla?\([^)]*,\s*0%?\s*\))$/i;
6439
+ function readBackgroundColor(cs) {
6440
+ const bg = cs.backgroundColor;
6441
+ if (!bg || TRANSPARENT_RE.test(bg)) return void 0;
6442
+ return bg;
6443
+ }
6444
+ function readBorder(cs) {
6445
+ const width = parseFloat(cs.borderTopWidth) || 0;
6446
+ if (width < .5) return void 0;
6447
+ const style = cs.borderTopStyle;
6448
+ if (!style || style === "none" || style === "hidden") return void 0;
6449
+ const color = cs.borderTopColor;
6450
+ if (!color || TRANSPARENT_RE.test(color)) return void 0;
6451
+ return `${Math.round(width)}px ${style} ${color}`;
6452
+ }
6453
+ function readBoxShadow(cs) {
6454
+ const sh = cs.boxShadow;
6455
+ if (!sh || sh === "none") return void 0;
6456
+ return sh;
6457
+ }
6458
+ function readOpacity(cs) {
6459
+ const o = parseFloat(cs.opacity);
6460
+ if (!Number.isFinite(o) || o >= 1) return void 0;
6461
+ if (o <= 0) return void 0;
6462
+ return Math.round(o * 100) / 100;
6463
+ }
6464
+ function readTextAlign(cs) {
6465
+ const ta = cs.textAlign;
6466
+ if (!ta) return void 0;
6467
+ if (ta === "left" || ta === "right" || ta === "center" || ta === "justify" || ta === "start" || ta === "end") return ta;
6468
+ }
6469
+ /**
6470
+ * Pre-compute (and freeze) the inline styles used at render time. Doing it once
6471
+ * here means rendering 500 blocks doesn't allocate 500 style objects per frame.
6472
+ *
6473
+ * Captured visual signals (bg, border, shadow, opacity) are merged into the
6474
+ * frozen style so the replay carries the real DOM's surface — a white button
6475
+ * stays white, a ring-bordered button keeps its ring, a shadowed card keeps
6476
+ * its elevation.
6477
+ */
6478
+ function freezeShape(node) {
6479
+ const baseStyle = {
6480
+ left: `${node.x}px`,
6481
+ top: `${node.y}px`,
6482
+ width: `${node.w}px`,
6483
+ height: `${node.h}px`,
6484
+ borderRadius: `${node.radius}px`
6485
+ };
6486
+ applyVisualSignals$1(baseStyle, node);
6487
+ const style = Object.freeze(baseStyle);
6488
+ let lineStyles;
6489
+ if (node.type === "text" && node.textRects && node.textRects.length > 0) {
6490
+ const radiusStr = `${node.radius}px`;
6491
+ const arr = [];
6492
+ for (const r of node.textRects) {
6493
+ const lineStyle = {
6494
+ left: `${r.x}px`,
6495
+ top: `${r.y}px`,
6496
+ width: `${r.w}px`,
6497
+ height: `${r.h}px`,
6498
+ borderRadius: radiusStr
6499
+ };
6500
+ applyVisualSignals$1(lineStyle, node);
6501
+ arr.push(Object.freeze(lineStyle));
6502
+ }
6503
+ lineStyles = Object.freeze(arr);
6504
+ } else if (node.type === "text" && node.lines && node.lines > 1) {
6505
+ const lh = node.lineHeight ?? Math.round(node.h / node.lines);
6506
+ const barHeight = Math.max(8, Math.round(lh * .7));
6507
+ const widthFull = node.w;
6508
+ const widthLast = Math.max(40, Math.round(node.w * .7));
6509
+ const heightStr = `${barHeight}px`;
6510
+ const radiusStr = `${node.radius}px`;
6511
+ const arr = [];
6512
+ for (let i = 1; i <= node.lines; i++) {
6513
+ const isLast = i === node.lines;
6514
+ const lineWidth = isLast ? widthLast : widthFull;
6515
+ let leftX = node.x;
6516
+ if (isLast && node.textAlign) {
6517
+ const slack = widthFull - lineWidth;
6518
+ if (node.textAlign === "center") leftX = node.x + Math.round(slack / 2);
6519
+ else if (node.textAlign === "right" || node.textAlign === "end") leftX = node.x + slack;
6520
+ }
6521
+ const lineStyle = {
6522
+ left: `${leftX}px`,
6523
+ top: `${node.y + (i - 1) * lh}px`,
6524
+ width: `${lineWidth}px`,
6525
+ height: heightStr,
6526
+ borderRadius: radiusStr
6527
+ };
6528
+ applyVisualSignals$1(lineStyle, node);
6529
+ arr.push(Object.freeze(lineStyle));
6530
+ }
6531
+ lineStyles = Object.freeze(arr);
6532
+ }
6533
+ return Object.freeze({
6534
+ type: node.type,
6535
+ x: node.x,
6536
+ y: node.y,
6537
+ w: node.w,
6538
+ h: node.h,
6539
+ radius: node.radius,
6540
+ lines: node.lines,
6541
+ lineHeight: node.lineHeight,
6542
+ textRects: node.textRects ? Object.freeze(node.textRects) : void 0,
6543
+ bg: node.bg,
6544
+ border: node.border,
6545
+ boxShadow: node.boxShadow,
6546
+ opacity: node.opacity,
6547
+ textAlign: node.textAlign,
6548
+ style,
6549
+ lineStyles
6550
+ });
6551
+ }
6552
+ /**
6553
+ * Merge captured surface signals into a style object in place. Each signal is
6554
+ * additive; `bg` uses `background` shorthand so it wipes the default linear
6555
+ * gradient on `.a-skel-block` cleanly.
6556
+ */
6557
+ function applyVisualSignals$1(out, node) {
6558
+ if (node.bg) out.background = node.bg;
6559
+ if (node.border) out.border = node.border;
6560
+ if (node.boxShadow) out.boxShadow = node.boxShadow;
6561
+ if (node.opacity !== void 0) out.opacity = node.opacity;
6562
+ }
6563
+ //#endregion
6564
+ //#region src/composables/useShapeProbe.ts
6565
+ const DEFAULT_RESIZE_DEBOUNCE_MS = 150;
6566
+ /**
6567
+ * Observe `getTarget()` and capture its rendered shape whenever the element
6568
+ * appears or resizes.
6569
+ *
6570
+ * Performance:
6571
+ * - Initial capture runs via `requestAnimationFrame` so it sneaks into the
6572
+ * first idle window after mount — no synchronous layout from inside the
6573
+ * render queue.
6574
+ * - Subsequent `ResizeObserver` callbacks are debounced (default 150 ms) so a
6575
+ * drag-resize doesn't trigger a fresh DOM walk per frame.
6576
+ * - The capture strategy itself enforces `maxNodes` so even a worst-case
6577
+ * capture (10k descendants) returns in bounded time.
6578
+ */
6579
+ function useShapeProbe(getTarget, options) {
6580
+ let observer;
6581
+ let frame;
6582
+ let timer;
6583
+ let hasCaptured = false;
6584
+ const debounceMs = options.resizeDebounceMs ?? DEFAULT_RESIZE_DEBOUNCE_MS;
6585
+ const captureFn = options.capture ?? walkDom;
6586
+ function cleanup() {
6587
+ if (observer) {
6588
+ observer.disconnect();
6589
+ observer = void 0;
6590
+ }
6591
+ if (frame !== void 0) {
6592
+ cancelAnimationFrame(frame);
6593
+ frame = void 0;
6594
+ }
6595
+ if (timer !== void 0) {
6596
+ clearTimeout(timer);
6597
+ timer = void 0;
6598
+ }
6599
+ }
6600
+ function capture(el) {
6601
+ const result = captureFn(el, {
6602
+ maxDepth: options.maxDepth,
6603
+ maxNodes: options.maxNodes,
6604
+ minSize: options.minSize
6605
+ });
6606
+ if (result.width > 0 && result.height > 0 && result.nodes.length > 0) {
6607
+ hasCaptured = true;
6608
+ options.onCapture(result);
6609
+ }
6610
+ }
6611
+ function scheduleImmediate(el) {
6612
+ if (frame !== void 0) cancelAnimationFrame(frame);
6613
+ frame = requestAnimationFrame(() => {
6614
+ frame = void 0;
6615
+ capture(el);
6616
+ });
6617
+ }
6618
+ function scheduleDebounced(el) {
6619
+ if (timer !== void 0) clearTimeout(timer);
6620
+ timer = setTimeout(() => {
6621
+ timer = void 0;
6622
+ capture(el);
6623
+ }, debounceMs);
6624
+ }
6625
+ watch(getTarget, (el) => {
6626
+ cleanup();
6627
+ hasCaptured = false;
6628
+ if (!el || typeof window === "undefined") return;
6629
+ scheduleImmediate(el);
6630
+ if (typeof ResizeObserver !== "undefined") {
6631
+ observer = new ResizeObserver(() => {
6632
+ if (hasCaptured) scheduleDebounced(el);
6633
+ });
6634
+ observer.observe(el);
6635
+ }
6636
+ }, {
6637
+ immediate: true,
6638
+ flush: "post"
6639
+ });
6640
+ onBeforeUnmount(cleanup);
6641
+ }
6642
+ //#endregion
6643
+ //#region src/composables/useSkeletonCache.ts
6644
+ const memory = /* @__PURE__ */ new Map();
6645
+ const STORAGE_PREFIX = "a-skeleton:";
6646
+ /**
6647
+ * Schema version for persisted flat `CachedShape` entries. Bump whenever the
6648
+ * `ShapeNode` / `CachedShape` field set changes so stale localStorage payloads
6649
+ * from older releases get dropped on read instead of rehydrating into a wrong
6650
+ * layout.
6651
+ *
6652
+ * v2 — added advanced surface signals (textRects, bg, border, boxShadow,
6653
+ * opacity, textAlign).
6654
+ */
6655
+ const SCHEMA_VERSION = 2;
6656
+ /**
6657
+ * Separate in-memory + localStorage namespace for the Recipe 3 structural
6658
+ * shape (`StructuralShape`, `v: 3`). Kept apart from the flat-shape cache so
6659
+ * the two pipelines can't collide on the same `cacheKey` — `<ASkeleton>`'s
6660
+ * legacy cache-replay path and Recipe 3 may both run on the same page with
6661
+ * the same key.
6662
+ */
6663
+ const structuralMemory = /* @__PURE__ */ new Map();
6664
+ const STRUCTURAL_PREFIX = "a-skeleton:s:";
6665
+ const STRUCTURAL_SCHEMA_VERSION = 3;
6666
+ /**
6667
+ * Lookup a captured shape by key. Reads in-memory first, then `localStorage` if
6668
+ * `persist` is enabled. SSR-safe — bypasses `window` access on the server.
6669
+ * Rehydrates pre-computed styles for shapes deserialized from older sessions
6670
+ * (Object.freeze + style/lineStyles don't survive `JSON.stringify` round-trip).
6671
+ * Drops the entry if it was written by a different schema version.
6672
+ */
6673
+ function getCached(key, persist) {
6674
+ const hit = memory.get(key);
6675
+ if (hit) return hit;
6676
+ if (!persist || typeof window === "undefined") return void 0;
6677
+ try {
6678
+ const storageKey = STORAGE_PREFIX + key;
6679
+ const raw = window.localStorage.getItem(storageKey);
6680
+ if (!raw) return void 0;
6681
+ const parsed = JSON.parse(raw);
6682
+ if (parsed.v !== SCHEMA_VERSION) {
6683
+ window.localStorage.removeItem(storageKey);
6684
+ return;
6685
+ }
6686
+ const rehydrated = rehydrateShape(parsed);
6687
+ memory.set(key, rehydrated);
6688
+ return rehydrated;
6689
+ } catch {
6690
+ return;
6691
+ }
6692
+ }
6693
+ /** Store a captured shape. `persist=true` mirrors to `localStorage`. */
6694
+ function setCached(key, value, persist) {
6695
+ memory.set(key, value);
6696
+ if (!persist || typeof window === "undefined") return;
6697
+ try {
6698
+ const lean = {
6699
+ v: SCHEMA_VERSION,
6700
+ width: value.width,
6701
+ height: value.height,
6702
+ nodes: leanNodes(value.nodes),
6703
+ truncated: value.truncated
6704
+ };
6705
+ window.localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(lean));
6706
+ } catch {}
6707
+ }
6708
+ /**
6709
+ * Drop a single entry (or all entries when no key) — wipes both the flat-shape
6710
+ * and the structural-shape namespaces. Exposed for tests + manual invalidation.
6711
+ */
6712
+ function clearCached(key) {
6713
+ if (!key) {
6714
+ memory.clear();
6715
+ structuralMemory.clear();
6716
+ if (typeof window === "undefined") return;
6717
+ try {
6718
+ const ls = window.localStorage;
6719
+ const stale = [];
6720
+ for (let i = 0; i < ls.length; i++) {
6721
+ const k = ls.key(i);
6722
+ if (!k) continue;
6723
+ if (k.startsWith(STORAGE_PREFIX) || k.startsWith(STRUCTURAL_PREFIX)) stale.push(k);
6724
+ }
6725
+ for (const k of stale) ls.removeItem(k);
6726
+ } catch {}
6727
+ return;
6728
+ }
6729
+ memory.delete(key);
6730
+ structuralMemory.delete(key);
6731
+ if (typeof window === "undefined") return;
6732
+ try {
6733
+ window.localStorage.removeItem(STORAGE_PREFIX + key);
6734
+ window.localStorage.removeItem(STRUCTURAL_PREFIX + key);
6735
+ } catch {}
6736
+ }
6737
+ /**
6738
+ * Drop a single structural-shape entry (or all when no key). Kept separate
6739
+ * from `clearCached` so callers that only manage structural shapes don't have
6740
+ * to reach across namespaces.
6741
+ */
6742
+ function clearCachedStructural(key) {
6743
+ if (!key) {
6744
+ structuralMemory.clear();
6745
+ if (typeof window === "undefined") return;
6746
+ try {
6747
+ const ls = window.localStorage;
6748
+ const stale = [];
6749
+ for (let i = 0; i < ls.length; i++) {
6750
+ const k = ls.key(i);
6751
+ if (k && k.startsWith(STRUCTURAL_PREFIX)) stale.push(k);
6752
+ }
6753
+ for (const k of stale) ls.removeItem(k);
6754
+ } catch {}
6755
+ return;
6756
+ }
6757
+ structuralMemory.delete(key);
6758
+ if (typeof window === "undefined") return;
6759
+ try {
6760
+ window.localStorage.removeItem(STRUCTURAL_PREFIX + key);
6761
+ } catch {}
6762
+ }
6763
+ /**
6764
+ * Lookup a structural shape by key. Reads in-memory first, then `localStorage`
6765
+ * when `persist` is enabled. Stale schema versions get purged on read.
6766
+ */
6767
+ function getCachedStructural(key, persist) {
6768
+ const hit = structuralMemory.get(key);
6769
+ if (hit) return hit;
6770
+ if (!persist || typeof window === "undefined") return void 0;
6771
+ try {
6772
+ const storageKey = STRUCTURAL_PREFIX + key;
6773
+ const raw = window.localStorage.getItem(storageKey);
6774
+ if (!raw) return void 0;
6775
+ const parsed = JSON.parse(raw);
6776
+ if (parsed.v !== STRUCTURAL_SCHEMA_VERSION) {
6777
+ window.localStorage.removeItem(storageKey);
6778
+ return;
6779
+ }
6780
+ const rehydrated = rehydrateStructuralShape(parsed);
6781
+ structuralMemory.set(key, rehydrated);
6782
+ return rehydrated;
6783
+ } catch {
6784
+ return;
6785
+ }
6786
+ }
6787
+ /** Store a structural shape. `persist=true` mirrors to `localStorage`. */
6788
+ function setCachedStructural(key, value, persist) {
6789
+ structuralMemory.set(key, value);
6790
+ if (!persist || typeof window === "undefined") return;
6791
+ try {
6792
+ const payload = {
6793
+ v: STRUCTURAL_SCHEMA_VERSION,
6794
+ width: value.width,
6795
+ height: value.height,
6796
+ nodes: value.nodes.map(leanStructuralNode),
6797
+ truncated: value.truncated,
6798
+ capturedAt: value.capturedAt
6799
+ };
6800
+ window.localStorage.setItem(STRUCTURAL_PREFIX + key, JSON.stringify(payload));
6801
+ } catch {}
6802
+ }
6803
+ function leanStructuralNode(node) {
6804
+ if (node.kind === "container") return {
6805
+ kind: "container",
6806
+ tag: node.tag,
6807
+ className: node.className,
6808
+ style: node.style,
6809
+ children: node.children.map(leanStructuralNode)
6810
+ };
6811
+ return {
6812
+ kind: "leaf",
6813
+ leafKind: node.leafKind,
6814
+ className: node.className,
6815
+ style: node.style,
6816
+ textLines: node.textLines
6817
+ };
6818
+ }
6819
+ function rehydrateStructuralShape(persisted) {
6820
+ return Object.freeze({
6821
+ v: STRUCTURAL_SCHEMA_VERSION,
6822
+ width: persisted.width,
6823
+ height: persisted.height,
6824
+ nodes: Object.freeze(persisted.nodes.map(rehydrateStructuralNode)),
6825
+ truncated: persisted.truncated,
6826
+ capturedAt: persisted.capturedAt
6827
+ });
6828
+ }
6829
+ function rehydrateStructuralNode(node) {
6830
+ if (node.kind === "container") return Object.freeze({
6831
+ kind: "container",
6832
+ tag: node.tag,
6833
+ className: node.className,
6834
+ style: Object.freeze({ ...node.style }),
6835
+ children: Object.freeze(node.children.map(rehydrateStructuralNode))
6836
+ });
6837
+ return Object.freeze({
6838
+ kind: "leaf",
6839
+ leafKind: node.leafKind,
6840
+ className: node.className ?? "",
6841
+ style: Object.freeze({ ...node.style }),
6842
+ textLines: node.textLines ? Object.freeze([...node.textLines]) : void 0
6843
+ });
6844
+ }
6845
+ /**
6846
+ * Rebuild `style` + `lineStyles` for nodes that lost them via serialization.
6847
+ * Walks the array in-place where possible and freezes the result so the render
6848
+ * path stays allocation-free.
6849
+ */
6850
+ function rehydrateShape(shape) {
6851
+ const nodes = shape.nodes.map((n) => freezeNodeStyles(n));
6852
+ return Object.freeze({
6853
+ nodes: Object.freeze(nodes),
6854
+ width: shape.width,
6855
+ height: shape.height,
6856
+ truncated: shape.truncated
6857
+ });
6858
+ }
6859
+ function leanNodes(nodes) {
6860
+ return nodes.map((n) => ({
6861
+ type: n.type,
6862
+ x: n.x,
6863
+ y: n.y,
6864
+ w: n.w,
6865
+ h: n.h,
6866
+ radius: n.radius,
6867
+ lines: n.lines,
6868
+ lineHeight: n.lineHeight,
6869
+ textRects: n.textRects ? n.textRects.map((r) => ({
6870
+ x: r.x,
6871
+ y: r.y,
6872
+ w: r.w,
6873
+ h: r.h
6874
+ })) : void 0,
6875
+ bg: n.bg,
6876
+ border: n.border,
6877
+ boxShadow: n.boxShadow,
6878
+ opacity: n.opacity,
6879
+ textAlign: n.textAlign
6880
+ }));
6881
+ }
6882
+ function freezeNodeStyles(node) {
6883
+ const baseStyle = {
6884
+ left: `${node.x}px`,
6885
+ top: `${node.y}px`,
6886
+ width: `${node.w}px`,
6887
+ height: `${node.h}px`,
6888
+ borderRadius: `${node.radius}px`
6889
+ };
6890
+ applyVisualSignals(baseStyle, node);
6891
+ const style = Object.freeze(baseStyle);
6892
+ let lineStyles;
6893
+ if (node.type === "text" && node.textRects && node.textRects.length > 0) {
6894
+ const radiusStr = `${node.radius}px`;
6895
+ const arr = [];
6896
+ for (const r of node.textRects) {
6897
+ const lineStyle = {
6898
+ left: `${r.x}px`,
6899
+ top: `${r.y}px`,
6900
+ width: `${r.w}px`,
6901
+ height: `${r.h}px`,
6902
+ borderRadius: radiusStr
6903
+ };
6904
+ applyVisualSignals(lineStyle, node);
6905
+ arr.push(Object.freeze(lineStyle));
6906
+ }
6907
+ lineStyles = Object.freeze(arr);
6908
+ } else if (node.type === "text" && node.lines && node.lines > 1) {
6909
+ const lh = node.lineHeight ?? Math.round(node.h / node.lines);
6910
+ const barHeight = Math.max(8, Math.round(lh * .7));
6911
+ const widthFull = node.w;
6912
+ const widthLast = Math.max(40, Math.round(node.w * .7));
6913
+ const heightStr = `${barHeight}px`;
6914
+ const radiusStr = `${node.radius}px`;
6915
+ const arr = [];
6916
+ for (let i = 1; i <= node.lines; i++) {
6917
+ const isLast = i === node.lines;
6918
+ const lineWidth = isLast ? widthLast : widthFull;
6919
+ let leftX = node.x;
6920
+ if (isLast && node.textAlign) {
6921
+ const slack = widthFull - lineWidth;
6922
+ if (node.textAlign === "center") leftX = node.x + Math.round(slack / 2);
6923
+ else if (node.textAlign === "right" || node.textAlign === "end") leftX = node.x + slack;
6924
+ }
6925
+ const lineStyle = {
6926
+ left: `${leftX}px`,
6927
+ top: `${node.y + (i - 1) * lh}px`,
6928
+ width: `${lineWidth}px`,
6929
+ height: heightStr,
6930
+ borderRadius: radiusStr
6931
+ };
6932
+ applyVisualSignals(lineStyle, node);
6933
+ arr.push(Object.freeze(lineStyle));
6934
+ }
6935
+ lineStyles = Object.freeze(arr);
4322
6936
  }
4323
- });
4324
- function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
4325
- return $props.shape ? (openBlock(), createElementBlock("div", {
4326
- key: 0,
4327
- class: normalizeClass($setup.cn("a-skeleton__layer", $setup.props.class)),
4328
- style: normalizeStyle($setup.layerStyle),
4329
- role: "status",
4330
- "aria-live": "polite",
4331
- "aria-busy": "true"
4332
- }, [(openBlock(true), createElementBlock(Fragment, null, renderList($props.shape.nodes, (node, idx) => {
4333
- return openBlock(), createElementBlock(Fragment, { key: idx }, [node.lineStyles ? (openBlock(true), createElementBlock(Fragment, { key: 0 }, renderList(node.lineStyles, (lineStyle, i) => {
4334
- return openBlock(), createElementBlock("div", {
4335
- key: `${idx}-${i}`,
4336
- class: normalizeClass($setup.blockClassByType.text),
4337
- style: normalizeStyle(lineStyle)
4338
- }, null, 6);
4339
- }), 128)) : (openBlock(), createElementBlock("div", {
4340
- key: 1,
4341
- class: normalizeClass($setup.blockClassByType[node.type]),
4342
- style: normalizeStyle(node.style)
4343
- }, null, 6))], 64);
4344
- }), 128))], 6)) : createCommentVNode("v-if", true);
6937
+ return Object.freeze({
6938
+ type: node.type,
6939
+ x: node.x,
6940
+ y: node.y,
6941
+ w: node.w,
6942
+ h: node.h,
6943
+ radius: node.radius,
6944
+ lines: node.lines,
6945
+ lineHeight: node.lineHeight,
6946
+ textRects: node.textRects ? Object.freeze(node.textRects) : void 0,
6947
+ bg: node.bg,
6948
+ border: node.border,
6949
+ boxShadow: node.boxShadow,
6950
+ opacity: node.opacity,
6951
+ textAlign: node.textAlign,
6952
+ style,
6953
+ lineStyles
6954
+ });
6955
+ }
6956
+ function applyVisualSignals(out, node) {
6957
+ if (node.bg) out.background = node.bg;
6958
+ if (node.border) out.border = node.border;
6959
+ if (node.boxShadow) out.boxShadow = node.boxShadow;
6960
+ if (node.opacity !== void 0) out.opacity = node.opacity;
4345
6961
  }
4346
- var ASkeletonLayer_default = /* @__PURE__ */ export_helper_default(_sfc_main$1, [["render", _sfc_render$1], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/ASkeletonLayer.vue"]]);
4347
6962
  //#endregion
4348
- //#region src/components/ASkeletonBlock.vue
4349
- const _sfc_main = /* @__PURE__ */ defineComponent({
4350
- __name: "ASkeletonBlock",
4351
- props: {
4352
- type: {
4353
- type: String,
4354
- required: false,
4355
- default: "block"
4356
- },
4357
- w: {
4358
- type: [Number, String],
4359
- required: false
4360
- },
4361
- h: {
4362
- type: [Number, String],
4363
- required: false
4364
- },
4365
- radius: {
4366
- type: [Number, String],
4367
- required: false
4368
- },
4369
- lines: {
4370
- type: Number,
4371
- required: false,
4372
- default: 1
4373
- },
4374
- animation: {
4375
- type: String,
4376
- required: false,
4377
- default: "shimmer"
4378
- },
4379
- class: {
4380
- type: [
4381
- Boolean,
4382
- null,
4383
- String,
4384
- Object,
4385
- Array
4386
- ],
4387
- required: false,
4388
- skipCheck: true
6963
+ //#region src/utils/walkStructural.ts
6964
+ const DEFAULT_MAX_DEPTH$1 = 12;
6965
+ const DEFAULT_MAX_NODES = 500;
6966
+ const DEFAULT_MIN_SIZE = 4;
6967
+ /** Tags treated as atomic leaves — never recursed into, rendered as one block. */
6968
+ const ATOMIC_TAGS = new Set([
6969
+ "img",
6970
+ "picture",
6971
+ "svg",
6972
+ "canvas",
6973
+ "video",
6974
+ "audio",
6975
+ "input",
6976
+ "textarea",
6977
+ "select",
6978
+ "button",
6979
+ "progress",
6980
+ "meter",
6981
+ "hr",
6982
+ "iframe",
6983
+ "object",
6984
+ "embed",
6985
+ "br"
6986
+ ]);
6987
+ /**
6988
+ * Tags whose content is conventionally text. When the author writes
6989
+ * `<h3>{{ data?.name }}</h3>` and `data` is null during loading, the
6990
+ * interpolation is empty and `hasDirectText` returns false — without this
6991
+ * fallback the element classifies as `leaf-block` (a generic shimmer rect)
6992
+ * instead of `leaf-text` (a shimmer text bar at the element's natural
6993
+ * rendered dimensions).
6994
+ */
6995
+ const TEXT_OWNERS = new Set([
6996
+ "h1",
6997
+ "h2",
6998
+ "h3",
6999
+ "h4",
7000
+ "h5",
7001
+ "h6",
7002
+ "p",
7003
+ "span",
7004
+ "a",
7005
+ "em",
7006
+ "strong",
7007
+ "small",
7008
+ "code",
7009
+ "b",
7010
+ "i",
7011
+ "mark",
7012
+ "label",
7013
+ "caption",
7014
+ "time",
7015
+ "dt",
7016
+ "dd",
7017
+ "li",
7018
+ "th",
7019
+ "td",
7020
+ "figcaption",
7021
+ "blockquote",
7022
+ "cite",
7023
+ "q"
7024
+ ]);
7025
+ /**
7026
+ * Layout-affecting CSS captured on **every** node. Includes display + box
7027
+ * model + flex + grid + positioning so any element type renders correctly
7028
+ * regardless of its role in the parent's layout (flex item, inline-block,
7029
+ * absolutely-positioned overlay, sticky bar, grid cell).
7030
+ *
7031
+ * Width / height are intentionally NOT here — containers get sized by their
7032
+ * children; leaves get explicit width / height from their `getBoundingClientRect`
7033
+ * in `emitLeaf` below.
7034
+ */
7035
+ const LAYOUT_PROPS = [
7036
+ "display",
7037
+ "position",
7038
+ "top",
7039
+ "right",
7040
+ "bottom",
7041
+ "left",
7042
+ "z-index",
7043
+ "vertical-align",
7044
+ "float",
7045
+ "clear",
7046
+ "box-sizing",
7047
+ "margin-top",
7048
+ "margin-right",
7049
+ "margin-bottom",
7050
+ "margin-left",
7051
+ "padding-top",
7052
+ "padding-right",
7053
+ "padding-bottom",
7054
+ "padding-left",
7055
+ "flex",
7056
+ "flex-direction",
7057
+ "flex-wrap",
7058
+ "flex-grow",
7059
+ "flex-shrink",
7060
+ "flex-basis",
7061
+ "justify-content",
7062
+ "align-items",
7063
+ "align-content",
7064
+ "align-self",
7065
+ "justify-self",
7066
+ "gap",
7067
+ "row-gap",
7068
+ "column-gap",
7069
+ "order",
7070
+ "grid-template-columns",
7071
+ "grid-template-rows",
7072
+ "grid-template-areas",
7073
+ "grid-auto-flow",
7074
+ "grid-auto-columns",
7075
+ "grid-auto-rows",
7076
+ "grid-column",
7077
+ "grid-row",
7078
+ "grid-area"
7079
+ ];
7080
+ /**
7081
+ * Visual signals captured on **every** node. These give each rendered
7082
+ * placeholder the same surface identity the real element had — fill,
7083
+ * outline, elevation, transparency, transforms.
7084
+ */
7085
+ const VISUAL_PROPS = [
7086
+ "border-top-width",
7087
+ "border-right-width",
7088
+ "border-bottom-width",
7089
+ "border-left-width",
7090
+ "border-top-style",
7091
+ "border-right-style",
7092
+ "border-bottom-style",
7093
+ "border-left-style",
7094
+ "border-top-color",
7095
+ "border-right-color",
7096
+ "border-bottom-color",
7097
+ "border-left-color",
7098
+ "border-top-left-radius",
7099
+ "border-top-right-radius",
7100
+ "border-bottom-right-radius",
7101
+ "border-bottom-left-radius",
7102
+ "background-color",
7103
+ "background-image",
7104
+ "background-position",
7105
+ "background-size",
7106
+ "background-repeat",
7107
+ "background-origin",
7108
+ "background-clip",
7109
+ "box-shadow",
7110
+ "opacity",
7111
+ "filter",
7112
+ "backdrop-filter",
7113
+ "transform",
7114
+ "transform-origin",
7115
+ "mix-blend-mode"
7116
+ ];
7117
+ const ALL_PROPS = [...LAYOUT_PROPS, ...VISUAL_PROPS];
7118
+ /**
7119
+ * Walk `root`'s descendants and produce a frozen tree-shaped capture.
7120
+ * Direct children of `root` become top-level `nodes`; recursive children
7121
+ * live on their respective `ContainerNode`s.
7122
+ */
7123
+ function walkStructural(root, options = {}) {
7124
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH$1;
7125
+ const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
7126
+ const minSize = options.minSize ?? DEFAULT_MIN_SIZE;
7127
+ const rootRect = root.getBoundingClientRect();
7128
+ const state = {
7129
+ count: 0,
7130
+ truncated: false
7131
+ };
7132
+ const ctx = {
7133
+ maxDepth,
7134
+ maxNodes,
7135
+ minSize,
7136
+ state
7137
+ };
7138
+ const nodes = [];
7139
+ for (let i = 0; i < root.children.length; i++) {
7140
+ if (state.count >= maxNodes) {
7141
+ state.truncated = true;
7142
+ break;
4389
7143
  }
4390
- },
4391
- setup(__props, { expose: __expose }) {
4392
- __expose();
4393
- const props = __props;
4394
- const animationClass = computed(() => props.animation === "none" ? null : `a-skel-block--anim-${props.animation}`);
4395
- const blockClass = computed(() => [
4396
- "a-skel-block",
4397
- `a-skel-block--${props.type}`,
4398
- animationClass.value
4399
- ]);
4400
- function toLength(v) {
4401
- if (v === void 0) return void 0;
4402
- return typeof v === "number" ? `${v}px` : v;
7144
+ const node = capture(root.children[i], rootRect, 1, ctx);
7145
+ if (node) nodes.push(node);
7146
+ }
7147
+ return Object.freeze({
7148
+ width: Math.round(rootRect.width),
7149
+ height: Math.round(rootRect.height),
7150
+ nodes: Object.freeze(nodes),
7151
+ truncated: state.truncated,
7152
+ capturedAt: Date.now(),
7153
+ v: 3
7154
+ });
7155
+ }
7156
+ function capture(el, origin, depth, ctx) {
7157
+ if (ctx.state.count >= ctx.maxNodes) {
7158
+ ctx.state.truncated = true;
7159
+ return null;
7160
+ }
7161
+ if (el.dataset?.skeletonIgnore !== void 0) return null;
7162
+ const cs = window.getComputedStyle(el);
7163
+ if (cs.display === "none" || cs.visibility === "hidden") return null;
7164
+ const opacity = parseFloat(cs.opacity);
7165
+ if (Number.isFinite(opacity) && opacity === 0) return null;
7166
+ const rect = el.getBoundingClientRect();
7167
+ const tag = el.tagName.toLowerCase();
7168
+ const isTextOwner = TEXT_OWNERS.has(tag);
7169
+ let effectiveHeight = rect.height;
7170
+ if (isTextOwner && effectiveHeight < ctx.minSize) {
7171
+ const lh = parseFloat(cs.lineHeight);
7172
+ const fs = parseFloat(cs.fontSize);
7173
+ if (Number.isFinite(lh) && lh > 0) effectiveHeight = lh;
7174
+ else if (Number.isFinite(fs) && fs > 0) effectiveHeight = fs * 1.4;
7175
+ }
7176
+ if (rect.width < ctx.minSize || effectiveHeight < ctx.minSize) return null;
7177
+ const hasStop = el.dataset?.skeletonStop !== void 0;
7178
+ const isAtomic = ATOMIC_TAGS.has(tag);
7179
+ const reachedDepth = depth >= ctx.maxDepth;
7180
+ const childrenEls = collectVisibleChildren(el);
7181
+ if (hasStop || isAtomic || reachedDepth || childrenEls.length === 0) return emitLeaf(el, tag, cs, rect, effectiveHeight, isAtomic, ctx);
7182
+ const children = [];
7183
+ for (const child of childrenEls) {
7184
+ if (ctx.state.count >= ctx.maxNodes) {
7185
+ ctx.state.truncated = true;
7186
+ break;
4403
7187
  }
4404
- const radiusValue = computed(() => props.type === "circle" && props.radius === void 0 ? "50%" : toLength(props.radius));
4405
- const __returned__ = {
4406
- props,
4407
- animationClass,
4408
- blockClass,
4409
- toLength,
4410
- radiusValue,
4411
- blockStyle: computed(() => ({
4412
- width: toLength(props.w),
4413
- height: toLength(props.h),
4414
- borderRadius: radiusValue.value
4415
- })),
4416
- isMultiLineText: computed(() => props.type === "text" && props.lines > 1),
4417
- get cn() {
4418
- return cn;
4419
- }
4420
- };
4421
- Object.defineProperty(__returned__, "__isScriptSetup", {
4422
- enumerable: false,
4423
- value: true
4424
- });
4425
- return __returned__;
7188
+ const node = capture(child, origin, depth + 1, ctx);
7189
+ if (node) children.push(node);
4426
7190
  }
4427
- });
4428
- function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
4429
- return $setup.isMultiLineText ? (openBlock(), createElementBlock("div", {
4430
- key: 0,
4431
- class: normalizeClass($setup.cn("a-skel-block-stack", $setup.props.class)),
4432
- role: "status",
4433
- "aria-busy": "true"
4434
- }, [(openBlock(true), createElementBlock(Fragment, null, renderList($setup.props.lines, (i) => {
4435
- return openBlock(), createElementBlock("div", {
4436
- key: i,
4437
- class: normalizeClass($setup.blockClass),
4438
- style: normalizeStyle({
4439
- height: $setup.blockStyle.height ?? "0.75em",
4440
- width: i === $setup.props.lines ? "70%" : "100%",
4441
- borderRadius: $setup.blockStyle.borderRadius ?? "4px"
4442
- })
4443
- }, null, 6);
4444
- }), 128))], 2)) : (openBlock(), createElementBlock("div", {
4445
- key: 1,
4446
- class: normalizeClass($setup.cn($setup.blockClass, $setup.props.class)),
4447
- style: normalizeStyle($setup.blockStyle),
4448
- role: "status",
4449
- "aria-busy": "true"
4450
- }, null, 6));
7191
+ if (children.length === 0) return emitLeaf(el, tag, cs, rect, effectiveHeight, false, ctx);
7192
+ ctx.state.count++;
7193
+ const node = {
7194
+ kind: "container",
7195
+ tag,
7196
+ className: el.getAttribute("class") ?? "",
7197
+ style: readComputedStyles(cs, ALL_PROPS),
7198
+ children: Object.freeze(children)
7199
+ };
7200
+ return Object.freeze(node);
7201
+ }
7202
+ function emitLeaf(el, tag, cs, rect, effectiveHeight, isAtomic, ctx) {
7203
+ const w = Math.round(rect.width);
7204
+ const h = Math.round(effectiveHeight);
7205
+ const leafKind = classifyLeaf(tag, el, isAtomic);
7206
+ const captured = { ...readComputedStyles(cs, ALL_PROPS) };
7207
+ if (captured.display === "inline") captured.display = "inline-block";
7208
+ const style = {
7209
+ ...captured,
7210
+ width: `${w}px`,
7211
+ height: `${h}px`
7212
+ };
7213
+ let textLines;
7214
+ if (leafKind === "text") {
7215
+ const rawLines = captureTextLines(el, rect);
7216
+ if (rawLines && rawLines.length > 0) textLines = Object.freeze(rawLines.map((r) => ({
7217
+ x: Math.max(0, r.x),
7218
+ y: Math.max(0, r.y),
7219
+ w: r.w,
7220
+ h: r.h
7221
+ })));
7222
+ }
7223
+ ctx.state.count++;
7224
+ const node = {
7225
+ kind: "leaf",
7226
+ leafKind,
7227
+ className: el.getAttribute("class") ?? "",
7228
+ style: Object.freeze(style),
7229
+ textLines
7230
+ };
7231
+ return Object.freeze(node);
7232
+ }
7233
+ function classifyLeaf(tag, el, isAtomic) {
7234
+ if (tag === "img" || tag === "picture") return "image";
7235
+ if (tag === "video") return "image";
7236
+ if (isAtomic) return "media";
7237
+ if (hasDirectText(el)) return "text";
7238
+ if (TEXT_OWNERS.has(tag)) return "text";
7239
+ return "block";
4451
7240
  }
4452
- var ASkeletonBlock_default = /* @__PURE__ */ export_helper_default(_sfc_main, [
4453
- ["render", _sfc_render],
4454
- ["__scopeId", "data-v-bdfba69a"],
4455
- ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ASkeleton/src/components/ASkeletonBlock.vue"]
4456
- ]);
4457
7241
  //#endregion
4458
7242
  //#region src/composables/useSkeleton.ts
7243
+ const DEFAULT_MAX_DEPTH = 12;
4459
7244
  /**
4460
- * High-level building block for custom skeleton UIs. Wires up the probe + cache
4461
- * + reactivity so the consumer just renders something using the reactive shape:
7245
+ * High-level building block for Recipe 3 wires the structural capture +
7246
+ * cache + reactivity around a target element. The reactive `shape` is fed to
7247
+ * `<ASkeletonLayer>` which renders it in normal flow inside the consumer's
7248
+ * container.
4462
7249
  *
4463
7250
  * ```ts
4464
7251
  * const containerRef = ref<HTMLElement | null>(null);
@@ -4475,13 +7262,14 @@ var ASkeletonBlock_default = /* @__PURE__ */ export_helper_default(_sfc_main, [
4475
7262
  * </div>
4476
7263
  * ```
4477
7264
  *
4478
- * For more control, drop down to `useShapeProbe` + `getCached`/`setCached` and
4479
- * compose your own.
7265
+ * For more control, drop down to `useShapeProbe` (pass `walkStructural` as the
7266
+ * capture strategy) + `getCachedStructural` / `setCachedStructural` and compose
7267
+ * your own orchestration.
4480
7268
  */
4481
7269
  function useSkeleton(options) {
4482
7270
  const persist = options.persist ?? false;
4483
- const maxDepth = options.maxDepth ?? 6;
4484
- const shape = shallowRef(getCached(options.cacheKey, persist));
7271
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
7272
+ const shape = shallowRef(getCachedStructural(options.cacheKey, persist));
4485
7273
  if (options.target) {
4486
7274
  const getTarget = options.target;
4487
7275
  useShapeProbe(getTarget, {
@@ -4489,8 +7277,9 @@ function useSkeleton(options) {
4489
7277
  maxNodes: options.maxNodes,
4490
7278
  minSize: options.minSize,
4491
7279
  resizeDebounceMs: options.resizeDebounceMs,
7280
+ capture: walkStructural,
4492
7281
  onCapture: (captured) => {
4493
- setCached(options.cacheKey, captured, persist);
7282
+ setCachedStructural(options.cacheKey, captured, persist);
4494
7283
  shape.value = captured;
4495
7284
  }
4496
7285
  });
@@ -4498,18 +7287,18 @@ function useSkeleton(options) {
4498
7287
  function captureNow() {
4499
7288
  const el = options.target?.();
4500
7289
  if (!el || typeof window === "undefined") return void 0;
4501
- const captured = walkDom(el, {
7290
+ const captured = walkStructural(el, {
4502
7291
  maxDepth,
4503
7292
  maxNodes: options.maxNodes,
4504
7293
  minSize: options.minSize
4505
7294
  });
4506
7295
  if (captured.width <= 0 || captured.height <= 0 || captured.nodes.length === 0) return void 0;
4507
- setCached(options.cacheKey, captured, persist);
7296
+ setCachedStructural(options.cacheKey, captured, persist);
4508
7297
  shape.value = captured;
4509
7298
  return captured;
4510
7299
  }
4511
7300
  function clear() {
4512
- clearCached(options.cacheKey);
7301
+ clearCachedStructural(options.cacheKey);
4513
7302
  shape.value = void 0;
4514
7303
  }
4515
7304
  return {
@@ -4519,6 +7308,41 @@ function useSkeleton(options) {
4519
7308
  };
4520
7309
  }
4521
7310
  //#endregion
4522
- export { ASkeleton_default as ASkeleton, ASkeletonBlock_default as ASkeletonBlock, ASkeletonLayer_default as ASkeletonLayer, StructuralSkeleton, buildStructuralSkeleton, clearCached, fingerprintSlot, getCached, setCached, useShapeProbe, useSkeleton, walkDom };
7311
+ //#region src/utils/fingerprint.ts
7312
+ /**
7313
+ * Derive a default cache key from a slot's vnode tree. Returns the first
7314
+ * non-comment child's component name (or HTML tag). When the slot only contains
7315
+ * text / comments / unknown content, returns `'anonymous'` so the caller can
7316
+ * still cache, but with no useful identity — encourage an explicit `cacheKey`.
7317
+ */
7318
+ function fingerprintSlot(vnodes) {
7319
+ if (!vnodes) return "anonymous";
7320
+ for (const vnode of vnodes) {
7321
+ const tag = describeVNode(vnode);
7322
+ if (tag) return tag;
7323
+ }
7324
+ return "anonymous";
7325
+ }
7326
+ function describeVNode(vnode) {
7327
+ const t = vnode.type;
7328
+ if (t === Comment || t === Text) return void 0;
7329
+ if (t === Fragment) {
7330
+ const children = vnode.children;
7331
+ if (Array.isArray(children)) {
7332
+ for (const child of children) if (child && typeof child === "object" && "type" in child) {
7333
+ const found = describeVNode(child);
7334
+ if (found) return found;
7335
+ }
7336
+ }
7337
+ return;
7338
+ }
7339
+ if (typeof t === "string") return t;
7340
+ if (typeof t === "object" && t !== null) {
7341
+ const named = t.name ?? t.__name ?? t.displayName;
7342
+ if (named) return named;
7343
+ }
7344
+ }
7345
+ //#endregion
7346
+ export { ASkeleton_default as ASkeleton, ASkeletonArticle_default as ASkeletonArticle, ASkeletonAvatar_default as ASkeletonAvatar, ASkeletonBlock_default as ASkeletonBlock, ASkeletonButton_default as ASkeletonButton, ASkeletonCard_default as ASkeletonCard, ASkeletonChart_default as ASkeletonChart, ASkeletonChip_default as ASkeletonChip, ASkeletonClone_default as ASkeletonClone, ASkeletonDivider_default as ASkeletonDivider, ASkeletonForm_default as ASkeletonForm, ASkeletonHeading_default as ASkeletonHeading, ASkeletonImage_default as ASkeletonImage, ASkeletonInput_default as ASkeletonInput, ASkeletonLayer_default as ASkeletonLayer, ASkeletonListItem_default as ASkeletonListItem, ASkeletonTable_default as ASkeletonTable, ASkeletonText_default as ASkeletonText, ASkeletonVideo_default as ASkeletonVideo, StructuralSkeleton, buildStructuralSkeleton, captureSnapshot, clearCached, clearCachedStructural, fingerprintSlot, getCached, getCachedStructural, setCached, setCachedStructural, useShapeProbe, useSkeleton, walkDom, walkStructural };
4523
7347
 
4524
7348
  //# sourceMappingURL=index.js.map