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