@alikhalilll/a-smart-skeleton 0.0.0-stage → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +356 -2
  3. package/dist/_chunks/core.js +2314 -0
  4. package/dist/_chunks/core.js.map +1 -0
  5. package/dist/_chunks/index.d.ts +431 -0
  6. package/dist/core/index.cjs +67 -0
  7. package/dist/core/index.d.cts +2 -0
  8. package/dist/core/index.d.ts +2 -0
  9. package/dist/core/index.js +2 -0
  10. package/dist/element/index.cjs +47 -0
  11. package/dist/element/index.cjs.map +1 -0
  12. package/dist/element/index.d.cts +24 -0
  13. package/dist/element/index.d.ts +24 -0
  14. package/dist/element/index.js +46 -0
  15. package/dist/element/index.js.map +1 -0
  16. package/dist/index.cjs +512 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +204 -0
  19. package/dist/index.d.ts +204 -0
  20. package/dist/index.js +441 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/nuxt/index.cjs +36 -0
  23. package/dist/nuxt/index.cjs.map +1 -0
  24. package/dist/nuxt/index.d.cts +30 -0
  25. package/dist/nuxt/index.d.ts +30 -0
  26. package/dist/nuxt/index.js +36 -0
  27. package/dist/nuxt/index.js.map +1 -0
  28. package/dist/resolver/index.cjs +26 -0
  29. package/dist/resolver/index.cjs.map +1 -0
  30. package/dist/resolver/index.d.cts +19 -0
  31. package/dist/resolver/index.d.ts +19 -0
  32. package/dist/resolver/index.js +26 -0
  33. package/dist/resolver/index.js.map +1 -0
  34. package/dist/styles.css +136 -0
  35. package/package.json +144 -3
  36. package/src/components/ASmartSkeleton.vue +228 -0
  37. package/src/components/ASmartSkeletonBox.vue +23 -0
  38. package/src/components/ASmartSkeletonProvider.vue +34 -0
  39. package/src/components/ASmartSkeletonSuspense.vue +142 -0
  40. package/src/composables/useSmartSkeleton.ts +20 -0
  41. package/src/composables/useStyles.ts +8 -0
  42. package/src/core/boot.ts +63 -0
  43. package/src/core/canvas.ts +7 -0
  44. package/src/core/constants.ts +76 -0
  45. package/src/core/debug.ts +154 -0
  46. package/src/core/dev.ts +53 -0
  47. package/src/core/fade.ts +490 -0
  48. package/src/core/ghost.ts +236 -0
  49. package/src/core/images.ts +28 -0
  50. package/src/core/index.ts +71 -0
  51. package/src/core/inline.generated.ts +9 -0
  52. package/src/core/measure.ts +571 -0
  53. package/src/core/memory.ts +146 -0
  54. package/src/core/motion.ts +12 -0
  55. package/src/core/plan.ts +368 -0
  56. package/src/core/reveal.ts +89 -0
  57. package/src/core/reveals.ts +39 -0
  58. package/src/core/shapes.ts +84 -0
  59. package/src/core/shimmer.ts +229 -0
  60. package/src/core/skeleton.ts +43 -0
  61. package/src/core/snapshot.ts +159 -0
  62. package/src/core/styles.ts +187 -0
  63. package/src/core/sweep.ts +203 -0
  64. package/src/core/theme.ts +162 -0
  65. package/src/element/index.ts +59 -0
  66. package/src/index.ts +27 -0
  67. package/src/nuxt/index.ts +59 -0
  68. package/src/resolver/index.ts +35 -0
  69. package/src/types.ts +100 -0
  70. package/src/vue/Skeleton.ts +275 -0
  71. package/src/vue/Suspense.ts +157 -0
  72. package/src/vue/config.ts +78 -0
  73. package/src/vue/index.ts +7 -0
  74. package/web-types.json +256 -0
@@ -0,0 +1,2314 @@
1
+ //#region src/core/constants.ts
2
+ const ATTR_REVEAL = "ask-reveal";
3
+ const ATTR_PANE = "ask-pane";
4
+ const ATTR_LOADING = "ask-loading";
5
+ const ATTR_GHOST = "ask-ghost";
6
+ const ATTR_SNAPSHOT = "ask-snapshot";
7
+ const ATTR_LAYER = "ask-layer";
8
+ const ATTR_RECT = "ask-rect";
9
+ const ATTR_BAR = "ask-bar";
10
+ const ATTR_INACTIVE = "ask-inactive";
11
+ const ATTR_EXITING = "ask-exiting";
12
+ const ATTR_COVERED = "ask-covered";
13
+ const ATTR_OWN = "ask-own";
14
+ const ATTR_IGNORE = "ask-ignore";
15
+ const ATTR_BLOCK = "ask-block";
16
+ const GHOSTED = Symbol.for("a-smart-skeleton.ghost");
17
+ const UNCOVER = Symbol.for("a-smart-skeleton.uncover");
18
+ const ATTR_STATUS = "ask-status";
19
+ const ATTR_XRAY = "ask-xray";
20
+ const ATTR_DEBUG = "ask-debug";
21
+ const BASE = "var(--ask-base, rgb(128 128 128 / 0.2))";
22
+ const HIGHLIGHT = "var(--ask-highlight, rgb(255 255 255 / 0.12))";
23
+ const FADE_DURATION = 200;
24
+ const CASCADE_SPREAD = 320;
25
+ const RESIZE_DURATION = 280;
26
+ const IMAGE_TIMEOUT = 1e4;
27
+ const DURATIONS = {
28
+ shimmer: {
29
+ slow: 4e3,
30
+ fast: 2e3
31
+ },
32
+ pulse: {
33
+ slow: 2400,
34
+ fast: 1200
35
+ }
36
+ };
37
+ const TEXT_TAGS = [
38
+ "a",
39
+ "b",
40
+ "code",
41
+ "dd",
42
+ "dt",
43
+ "em",
44
+ "h1",
45
+ "h2",
46
+ "h3",
47
+ "h4",
48
+ "h5",
49
+ "h6",
50
+ "i",
51
+ "label",
52
+ "li",
53
+ "p",
54
+ "small",
55
+ "span",
56
+ "strong",
57
+ "td",
58
+ "th",
59
+ "time"
60
+ ];
61
+ const MEDIA_TAGS = [
62
+ "audio",
63
+ "canvas",
64
+ "embed",
65
+ "iframe",
66
+ "img",
67
+ "input",
68
+ "object",
69
+ "picture",
70
+ "select",
71
+ "svg",
72
+ "textarea",
73
+ "video"
74
+ ];
75
+ //#endregion
76
+ //#region src/core/styles.ts
77
+ const media = MEDIA_TAGS.join(", ");
78
+ /** The whole stylesheet, for rendering into a page yourself. */
79
+ const SMART_SKELETON_CSS = `
80
+ [${ATTR_REVEAL}] {
81
+ position: relative;
82
+ }
83
+
84
+ a-smart-skeleton-reveal,
85
+ ask-reveal {
86
+ display: block;
87
+ }
88
+
89
+ a-smart-skeleton-reveal[loading]:not(:defined),
90
+ ask-reveal[loading]:not(:defined) {
91
+ visibility: hidden;
92
+ }
93
+
94
+ [${ATTR_PANE}] {
95
+ display: block;
96
+ }
97
+
98
+ [${ATTR_LOADING}] {
99
+ position: relative;
100
+ -webkit-user-select: none;
101
+ user-select: none;
102
+ pointer-events: none;
103
+ }
104
+
105
+ [${ATTR_INACTIVE}] {
106
+ display: none !important;
107
+ }
108
+
109
+ [${ATTR_LAYER}] {
110
+ position: absolute;
111
+ inset: 0;
112
+ overflow: visible;
113
+ }
114
+
115
+ [${ATTR_BAR}] {
116
+ display: block;
117
+ background-color: ${BASE};
118
+ background-repeat: no-repeat;
119
+ }
120
+
121
+ /*
122
+ * Bones drawn by \`drawSkeleton\` live in the light DOM (no shadow root), so a
123
+ * consumer can target them with their own stylesheet. Positioning stays inline
124
+ * (per-bone), but the fill colour reads from a CSS custom property so the
125
+ * consumer's theme flows through — set \`--ask-base\` on any ancestor to
126
+ * recolour every bone inside it, or use the type modifier to style text / media
127
+ * / box bones separately.
128
+ *
129
+ * Chrome bones are excluded: they reproduce the consumer's own container paint
130
+ * (background, border, shadow) that \`captureChrome\` snapshotted, so they must
131
+ * NOT get a generic fill underneath. Without the \`:not()\`, a bordered-only
132
+ * container (e.g. a footer with \`border-t\`) would show as a solid filled
133
+ * rectangle instead of just its captured divider.
134
+ */
135
+ [${ATTR_RECT}]:not([${ATTR_RECT}="chrome"]) {
136
+ background-color: var(--ask-base, var(--ask-block, var(--ak-skeleton-block, rgb(128 128 128 / 0.2))));
137
+ background-repeat: no-repeat;
138
+ }
139
+
140
+ /* Containers keep their surface; what they hold is drawn by bones, measured before the first paint */
141
+ @scope ([${ATTR_LOADING}]) to ([${ATTR_OWN}], [${ATTR_LAYER}]) {
142
+ :scope,
143
+ :scope * {
144
+ color: transparent !important;
145
+ -webkit-text-fill-color: transparent !important;
146
+ text-shadow: none !important;
147
+ text-decoration-color: transparent !important;
148
+ }
149
+
150
+ :scope ::placeholder {
151
+ color: transparent !important;
152
+ }
153
+
154
+ /* A line to measure in empty text; content only, never a size, or a dot-sized span would grow round it */
155
+ :scope :is(${TEXT_TAGS.join(", ")}):empty:not(svg *)::before {
156
+ content: "000000";
157
+ }
158
+
159
+ /* Leaves are drawn by bones, so their own paint never shows; visibility, as measuring reads it */
160
+ :scope :is(:not(:has(*)), ${media}, [${ATTR_COVERED}], [${ATTR_BLOCK}]):not(svg *) {
161
+ visibility: hidden !important;
162
+ }
163
+
164
+ /* No script to measure: a bar struck through each line of text instead */
165
+ @media (scripting: none) {
166
+ :scope :not(:has(*)):not(svg *) {
167
+ visibility: visible !important;
168
+ }
169
+
170
+ :scope,
171
+ :scope * {
172
+ text-decoration: line-through ${BASE} !important;
173
+ text-decoration-thickness: 0.72em !important;
174
+ text-decoration-skip-ink: none !important;
175
+ }
176
+
177
+ :scope :is(${media}, [${ATTR_BLOCK}]):not(svg *) {
178
+ visibility: visible !important;
179
+ background: ${BASE} !important;
180
+ }
181
+
182
+ :scope svg * {
183
+ fill: transparent !important;
184
+ stroke: transparent !important;
185
+ }
186
+
187
+ :scope :is(img, video) {
188
+ object-position: -100vw 0;
189
+ }
190
+ }
191
+ }
192
+
193
+ /* Read by screen readers in the skeleton's place, never seen */
194
+ [${ATTR_STATUS}] {
195
+ position: absolute;
196
+ width: 1px;
197
+ height: 1px;
198
+ margin: -1px;
199
+ overflow: hidden;
200
+ clip-path: inset(50%);
201
+ white-space: nowrap;
202
+ }
203
+
204
+ /* Debug x-ray: the ghost shown through its bones, to see what was measured */
205
+ [${ATTR_XRAY}] [${ATTR_LOADING}] [${ATTR_GHOST}],
206
+ [${ATTR_XRAY}] [${ATTR_LOADING}] [${ATTR_GHOST}] *:not(svg *) {
207
+ visibility: visible !important;
208
+ color: rgb(255 0 80 / 0.7) !important;
209
+ -webkit-text-fill-color: rgb(255 0 80 / 0.7) !important;
210
+ }
211
+
212
+ /* Lets addStyles see a stylesheet already on the page, from styles.css or a server render */
213
+ :root {
214
+ --ask: 1;
215
+ }`;
216
+ let styleEl = null;
217
+ let refCount = 0;
218
+ let present;
219
+ function addStyles() {
220
+ refCount++;
221
+ if (styleEl) return;
222
+ present ??= !!getComputedStyle(document.documentElement).getPropertyValue("--ask");
223
+ if (present) return;
224
+ styleEl = document.createElement("style");
225
+ styleEl.dataset.aSmartSkeleton = "true";
226
+ styleEl.textContent = SMART_SKELETON_CSS;
227
+ document.head.appendChild(styleEl);
228
+ }
229
+ function removeStyles() {
230
+ refCount--;
231
+ if (refCount > 0 || !styleEl) return;
232
+ styleEl.remove();
233
+ styleEl = null;
234
+ }
235
+ //#endregion
236
+ //#region src/core/inline.generated.ts
237
+ const INLINE_SCRIPT = "\"use strict\";(()=>{var H=\"ask-ghost\";var G=\"ask-layer\",M=\"ask-rect\";var A=\"ask-covered\",P=\"ask-own\",W=\"ask-ignore\",F=\"ask-block\",z=Symbol.for(\"a-smart-skeleton.ghost\"),U=Symbol.for(\"a-smart-skeleton.uncover\"),X=\"a-smart-skeleton.boot\";var j=\"var(--ask-highlight, rgb(255 255 255 / 0.12))\";var V={shimmer:{slow:4e3,fast:2e3},pulse:{slow:2400,fast:1200}},q=[\"a\",\"b\",\"code\",\"dd\",\"dt\",\"em\",\"h1\",\"h2\",\"h3\",\"h4\",\"h5\",\"h6\",\"i\",\"label\",\"li\",\"p\",\"small\",\"span\",\"strong\",\"td\",\"th\",\"time\"],Y=[\"audio\",\"canvas\",\"embed\",\"iframe\",\"img\",\"input\",\"object\",\"picture\",\"select\",\"svg\",\"textarea\",\"video\"];var Et,K=()=>Et??(Et=/jsdom/.test(navigator.userAgent)?null:document.createElement(\"canvas\").getContext(\"2d\"));var nt=(t,e)=>t?{left:Math.max(t.left,e.left),right:Math.min(t.right,e.right),top:Math.max(t.top,e.top),bottom:Math.min(t.bottom,e.bottom)}:e,k=t=>[[t.left,t.top],[t.right,t.top],[t.right,t.bottom],[t.left,t.bottom]],$=(t,e)=>Math.abs(t[0]-e[0])<1&&Math.abs(t[1]-e[1])<1;function rt(t,e){let r=Math.min(e.right-e.left,e.bottom-e.top),o=n=>n.endsWith(\"%\")?parseFloat(n)/100*r:parseFloat(n)||0;return[o(t.borderTopLeftRadius),o(t.borderTopRightRadius),o(t.borderBottomRightRadius),o(t.borderBottomLeftRadius)]}function At(t,e,r){let o=nt(t,e),n=rt(r,e),c=t?k(t):[],i=k(e),l=k(o).map((b,R)=>$(b,i[R])?n[R]:t&&$(b,c[R])?t.corners[R]:0),{left:s,right:a,top:d,bottom:h}=o;return{left:s,right:a,top:d,bottom:h,corners:l}}function Z(t,e,r,o){if(!t)return r;let n=k(t),c=k(e).map((l,s)=>t.corners[s]>0&&$(l,n[s]));if(!c.some(Boolean))return r;let i=rt(o,e);return c.map((l,s)=>l?Math.max(i[s],t.corners[s]):i[s]).map(l=>`${l}px`).join(\" \")}var O=(t,e)=>{let r=nt(e,t);return r.right-r.left>=2&&r.bottom-r.top>=2?r:null},J={top:\"ascender\",bottom:\"descender\"},w=new Map,st=t=>[...t.getClientRects?.()??[]];function kt(t){let e=`${t.fontStyle} ${t.fontWeight} ${t.fontSize} ${t.fontFamily}`;if(w.has(e))return w.get(e);let r=K(),o=null;if(r){r.font=e;let n=r.measureText(\"H\"),c=i=>r.measureText(i).actualBoundingBoxAscent;o={ascent:n.fontBoundingBoxAscent,descent:n.fontBoundingBoxDescent,ascender:c(\"bdfhkl\"),capHeight:n.actualBoundingBoxAscent,xHeight:c(\"xz\"),descender:r.measureText(\"gjpqy\").actualBoundingBoxDescent}}return w.set(e,o),o}var Q={ascender:\"ascender\",\"cap-height\":\"capHeight\",\"x-height\":\"xHeight\"},tt=(t,e,r,o)=>{let n=t.getPropertyValue(e).trim();return r.includes(n)?n:o};function Ct(t,e,r){let o=kt(e);if(!o){let s=Math.min(r*.72,t.bottom-t.top),a=(t.top+t.bottom)/2;return{barTop:a-s/2,barBottom:a+s/2}}let n=tt(e,\"--ask-text-top\",Object.keys(Q),J.top),c=tt(e,\"--ask-text-bottom\",[\"baseline\",\"descender\"],J.bottom),i=o.ascent+o.descent,l=t.top+(t.bottom-t.top-i)/2+o.ascent;return{barTop:l-o[Q[n]],barBottom:l+(c===\"descender\"?o.descender:0)}}var Mt=new Set(Y),Ot=new Set(q),it=.4,at=/[,/]\\s*0\\)$/,g=(t,e)=>t!==\"\"&&t!==e,N=[\"Top\",\"Right\",\"Bottom\",\"Left\"],wt=t=>N.every(e=>g(t[`border${e}Style`],\"none\")&&g(t[`border${e}Width`],\"0px\")),$t=t=>N.some(e=>g(t[`border${e}Style`],\"none\")&&g(t[`border${e}Width`],\"0px\")),Bt=t=>{let[e=0,r=0,o=0,n=0]=(t.replace(/rgba?\\([^)]*\\)|#\\w+/g,\"\").match(/-?[\\d.]+px/g)??[]).map(parseFloat);return t.includes(\"inset\")&&!o&&!n&&e===0!=(r===0)&&Math.abs(e+r)<=2},ct=t=>g(t,\"none\")&&!t.split(/,(?![^(]*\\))/).every(Bt);function Nt(t){let e=[],r=0,o=0;for(let n=0;n<t.length;n++){let c=t[n];c===\"(\"?r++:c===\")\"?r--:c===\",\"&&r===0&&(e.push(t.slice(o,n)),o=n+1)}return e.push(t.slice(o)),e.map(n=>n.trim()).filter(n=>n&&!n.startsWith(\"url(\")).join(\", \")}var dt=t=>g(t.backgroundColor,\"transparent\")&&!at.test(t.backgroundColor)||g(t.backgroundImage,\"none\")||ct(t.boxShadow)||wt(t);function _t(t){let e={},r=t.backgroundColor;g(r,\"transparent\")&&!at.test(r)&&(e.background=r);let o=t.backgroundImage;if(g(o,\"none\")){let n=Nt(o);n&&(e.background=e.background?`${n}, ${e.background}`:n)}for(let n of N){let c=t[`border${n}Width`],i=t[`border${n}Style`],l=t[`border${n}Color`];if(g(i,\"none\")&&g(c,\"0px\")&&parseFloat(c)>0){let s=`border${n}`;e[s]=`${c} ${i} ${l}`}}return ct(t.boxShadow)&&(e.boxShadow=t.boxShadow),Object.keys(e).length?e:void 0}var B=t=>Mt.has(t.localName),lt=t=>Ot.has(t.localName)&&!t.firstElementChild&&!t.textContent?.trim(),Dt=t=>{let e=document.createTreeWalker(t,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_ELEMENT),r=document.createRange(),o=[],n=0;for(;e.nextNode();){let a=e.currentNode;if(a.nodeType===Node.TEXT_NODE){let d=a.parentElement;if(!a.textContent?.trim()||!d)continue;n++;let h=d.getBoundingClientRect();if(h.width<=1||h.height<=1)continue;r.selectNodeContents(a),o.push(st(r).filter(b=>b.width||b.height))}else lt(a)&&(n++,o.push([...a.getClientRects()]))}if(!o.some(a=>a.length))return n;let c=(parseFloat(getComputedStyle(t).fontSize)||16)*it,i=(a,d)=>Math.min(a.bottom,d.bottom)-Math.max(a.top,d.top)>=Math.min(a.height,d.height)/2&&Math.max(a.left,d.left)-Math.min(a.right,d.right)<=c,l=o.map((a,d)=>d),s=a=>l[a]===a?a:l[a]=s(l[a]);for(let a=0;a<o.length;a++)for(let d=a+1;d<o.length;d++)o[a].some(h=>o[d].some(b=>i(h,b)))&&(l[s(d)]=s(a));return new Set(o.map((a,d)=>s(d))).size},Lt=(t,e)=>t.hasAttribute(F)||B(t)||dt(e)&&Dt(t)<=1;function et(t,e){if(parseFloat(e.borderRadius)||!t.parentElement)return e.borderRadius||\"0px\";let r=t.parentElement,o=getComputedStyle(r),n=t.getBoundingClientRect(),c=r.getBoundingClientRect();return Math.abs(n.width-c.width)<1&&Math.abs(n.height-c.height)<1&&o.overflow!==\"visible\"?o.borderRadius:e.borderRadius||\"0px\"}function ot(t){t.sort((r,o)=>r.left-o.left);let e=[];for(let r of t){let o=e.find(n=>{let c=Math.min(n.bottom,r.bottom)-Math.max(n.top,r.top),i=Math.min(n.bottom-n.top,r.bottom-r.top),l=r.left-n.right;return c>=i/2&&l<=Math.max(n.fontSize,r.fontSize)*it});o?(o.left=Math.min(o.left,r.left),o.right=Math.max(o.right,r.right),o.top=Math.min(o.top,r.top),o.bottom=Math.max(o.bottom,r.bottom),o.fontSize=Math.max(o.fontSize,r.fontSize),o.barTop=Math.min(o.barTop,r.barTop),o.barBottom=Math.max(o.barBottom,r.barBottom)):e.push({...r})}return e.map(r=>{let o=r.barBottom-r.barTop;return{type:\"text\",x:r.left,y:r.barTop,width:r.right-r.left,height:o,radius:r.radius||`${Math.round(o/3)}px`}})}function mt(t,e,{mark:r=!0,onCovered:o}={}){let n=e.getBoundingClientRect(),c=e.offsetWidth?n.width/e.offsetWidth:1,i=e.offsetHeight?n.height/e.offsetHeight:1,l=m=>({left:(m.left-n.left)/c,right:(m.right-n.left)/c,top:(m.top-n.top)/i,bottom:(m.bottom-n.top)/i}),s=[],a=[],d=new Set,h=document.createRange(),b=(m,f,T)=>{let u=O(m,T);if(!u)return;let p=parseFloat(f.fontSize)||16,C=l(u);a.push({...C,fontSize:p,...Ct(C,f,p),radius:f.getPropertyValue(\"--ask-radius\").trim()})},R=(m,f)=>{let T=m.data.search(/\\S/);if(T<0||!m.parentElement)return;h.setStart(m,T),h.setEnd(m,m.data.trimEnd().length);let u=getComputedStyle(m.parentElement);for(let p of st(h))b(p,u,f)},I=(m,f)=>{for(let T of m.childNodes){if(T.nodeType===Node.TEXT_NODE){R(T,f);continue}if(T.nodeType!==Node.ELEMENT_NODE)continue;let u=T;if(u.hasAttribute(W)||u.hasAttribute(P))continue;let p=getComputedStyle(u);if(p.display===\"none\")continue;if(Lt(u,p)){let x=O(u.getBoundingClientRect(),f);if(x){let S=l(x);s.push({type:B(u)?\"media\":\"box\",x:S.left,y:S.top,width:S.right-S.left,height:S.bottom-S.top,radius:Z(f,x,et(u,p),p)})}B(u)||d.add(u);continue}if(lt(u)){for(let x of u.getClientRects())b(x,p,f);continue}if(dt(p)||$t(p)){let x=_t(p);if(x){let S=O(u.getBoundingClientRect(),f);if(S){let y=l(S);s.push({type:\"chrome\",x:y.left,y:y.top,width:y.right-y.left,height:y.bottom-y.top,radius:Z(f,S,et(u,p),p),chrome:x})}}}let C=p.overflowX!==\"visible\"||p.overflowY!==\"visible\";I(u,C?At(f,u.getBoundingClientRect(),p):f)}};if(I(t,null),o?.(d),!r)return[...s,...ot(a)];for(let m of t.querySelectorAll(`[${A}]`))d.has(m)||m.removeAttribute(A);for(let m of d)m.hasAttribute(A)||m.setAttribute(A,\"\");return[...s,...ot(a)]}var vt=t=>typeof matchMedia<\"u\"&&matchMedia(t).matches,It=()=>typeof Element<\"u\"&&typeof Element.prototype.animate==\"function\",ut=()=>!It()||vt(\"(prefers-reduced-motion: reduce)\");var _=t=>t.getAnimations?.()??[];var Ht=.7,Gt=1280,pt=320,Pt=2.5,Wt=.45,Ft=[\"shimmer\",\"group-shimmer\",\"pulse\",\"none\"];function bt(t){let e=getComputedStyle(t),r=(c,i,l)=>{let s=e.getPropertyValue(c).trim();return i.includes(s)?s:l},o=r(\"--ask-animation\",Ft,\"shimmer\"),n=r(\"--ask-speed\",[\"slow\",\"fast\"],\"slow\");return{animation:o,duration:V[o===\"pulse\"?\"pulse\":\"shimmer\"][n]}}var L=20,zt=Math.sin(L*Math.PI/180),gt=Math.cos(L*Math.PI/180),Ut=zt/gt;var Xt=`${90+L}deg`,D=.85,E=12,ht=t=>(1-Math.cos(Math.PI*t))/2;function jt(t){let e=t*Pt,r=(n,c)=>`color-mix(in srgb, ${j} ${(c*100).toFixed(1)}%, transparent) ${(n*t).toFixed(2)}px`,o=[];for(let n=0;n<=E;n++)o.push(r(n/E*D,ht(n/E)));for(let n=1;n<=E;n++)o.push(r(D+n/E*(1-D),ht(1-n/E)));return{width:e/gt,image:`repeating-linear-gradient(${Xt}, ${o.join(\", \")}, transparent ${e.toFixed(2)}px)`}}function Vt(t,e,r=bt(t)){for(let b of _(t))b.cancel();if(t.style.backgroundImage=\"\",t.style.backgroundRepeat=\"\",ut()||typeof t.animate!=\"function\"||r.animation===\"none\")return;let o={duration:r.duration,iterations:1/0};if(r.animation===\"pulse\"){t.animate([{opacity:1,easing:\"ease-in-out\"},{opacity:Wt,easing:\"ease-in-out\"},{opacity:1}],o).startTime=0;return}let n=t.getBoundingClientRect(),c=n.left+scrollX,i=r.animation===\"group-shimmer\"?e??{left:c,top:n.top,width:n.width,height:n.height}:{left:0,top:0,width:document.documentElement.clientWidth||innerWidth,height:innerHeight},l=Math.min(Math.max(i.width*.4,80),pt),{width:s,image:a}=jt(l),d=c-i.left+(n.top-i.top)*Ut,h=r.duration*Ht/(Gt+pt);t.style.backgroundImage=a,t.style.backgroundSize=`${s}px 100%`,t.style.backgroundRepeat=\"repeat-x\",t.animate([{backgroundPosition:`${-d}px 0`},{backgroundPosition:`${s-d}px 0`}],{duration:s*h,iterations:1/0}).startTime=0}var qt=t=>[...t.querySelectorAll(`:scope > [${M}]`)],ft=4,Yt=t=>t.replace(/(\\d+(?:\\.\\d+)?)px/g,(e,r)=>{let o=parseFloat(r);return Number.isFinite(o)&&o<ft?`${ft}px`:`${o}px`});function St(t,e){let r=qt(t),o=t.getBoundingClientRect(),n={left:o.left+scrollX,top:o.top,width:o.width,height:o.height},c=bt(t);e.forEach((i,l)=>{let s=r[l];if(s||(s=document.createElement(\"div\"),s.style.cssText=\"position:absolute;display:block;pointer-events:none;background-repeat:no-repeat\",t.append(s)),s.setAttribute(M,i.type),Object.assign(s.style,{left:`${i.x}px`,top:`${i.y}px`,width:`${i.width}px`,height:`${i.height}px`,borderRadius:i.type===\"chrome\"?i.radius:Yt(i.radius)}),i.type===\"chrome\"){for(let h of _(s))h.cancel();s.style.backgroundImage=\"\",s.style.backgroundSize=\"\",s.style.backgroundRepeat=\"\";let d=i.chrome;s.style.background=d?.background??\"\",s.style.borderTop=d?.borderTop??\"\",s.style.borderRight=d?.borderRight??\"\",s.style.borderBottom=d?.borderBottom??\"\",s.style.borderLeft=d?.borderLeft??\"\",s.style.boxShadow=d?.boxShadow??\"\",s.dataset.motion=\"chrome\";return}s.dataset.motion===\"chrome\"&&(s.style.background=\"\",s.style.borderTop=\"\",s.style.borderRight=\"\",s.style.borderBottom=\"\",s.style.borderLeft=\"\",s.style.boxShadow=\"\");let a=`${Math.round(n.left+i.x)}|${Math.round(i.y)}|${Math.round(i.height)}|${Math.round(n.left)}|${Math.round(n.width)}|${Math.round(n.height)}|${document.documentElement.clientWidth}|${c.animation}|${c.duration}`;s.dataset.motion!==a&&(s.dataset.motion=a,Vt(s,n,c))});for(let i=r.length-1;i>=e.length;i--)r[i].remove()}var xt=t=>t.parentElement?`${xt(t.parentElement)} > :nth-child(${[...t.parentElement.children].indexOf(t)+1})`:\":root\";function v(t){let e=t?.previousElementSibling,r=e?.querySelector(`:scope > [${H}]`),o=e?.querySelector(`:scope > [${G}]`);if(!e||!r||!o)return;let n=typeof CSSStyleSheet==\"function\"&&\"replaceSync\"in CSSStyleSheet.prototype?new CSSStyleSheet:null;n&&(document.adoptedStyleSheets=[...document.adoptedStyleSheets,n],e[U]=()=>{document.adoptedStyleSheets=document.adoptedStyleSheets.filter(s=>s!==n)});let c=s=>n?.replaceSync(s.size?`${[...s].map(xt).join(\", \")} { visibility: hidden !important; }`:\"\"),i=()=>{if(!o.isConnected||e[z])return l.disconnect();r.getClientRects().length&&St(o,mt(r,o,{mark:!1,onCovered:c}))},l=new ResizeObserver(i);l.observe(e),document.fonts?.ready.then(i)}var Rt=self,yt=Symbol.for(X),Tt=Rt[yt];Rt[yt]=v;Array.isArray(Tt)&&Tt.forEach(v);v(document.currentScript);})();";
238
+ /** For every skeleton after a page's first: calls the boot INLINE_SCRIPT left on window. */
239
+ const INLINE_STUB = "(s=>{let k=Symbol.for(\"a-smart-skeleton.boot\"),b=self[k];typeof b==\"function\"?b(s):(self[k]=b||[]).push(s)})(document.currentScript)";
240
+ /** Either, as a whole element — for templates where a literal script tag would be read as the component's own. */
241
+ const inlineScriptTag = (nonce, stub = false) => `<script${nonce ? ` nonce="${nonce.replace(/"/g, "&quot;")}"` : ""}>${stub ? INLINE_STUB : INLINE_SCRIPT}<\/script>`;
242
+ //#endregion
243
+ //#region src/core/theme.ts
244
+ const DEFAULT_COLORS = {
245
+ light: {
246
+ base: "rgb(0 0 0 / 0.07)",
247
+ highlight: "rgb(255 255 255 / 0.6)"
248
+ },
249
+ dark: {
250
+ base: "rgb(255 255 255 / 0.08)",
251
+ highlight: "rgb(255 255 255 / 0.1)"
252
+ }
253
+ };
254
+ const DEFAULT_THEME = {
255
+ ...DEFAULT_COLORS,
256
+ animation: "shimmer",
257
+ speed: "slow",
258
+ text: {
259
+ top: "ascender",
260
+ bottom: "descender"
261
+ }
262
+ };
263
+ const ANIMATION = new Set([
264
+ "shimmer",
265
+ "group-shimmer",
266
+ "pulse",
267
+ "none"
268
+ ]);
269
+ const SPEED = new Set(["slow", "fast"]);
270
+ const TEXT_TOP = new Set([
271
+ "ascender",
272
+ "cap-height",
273
+ "x-height"
274
+ ]);
275
+ const TEXT_BOTTOM = new Set(["baseline", "descender"]);
276
+ const LENGTH = /^[\d.\s,%()+\-*/a-z]+$/i;
277
+ const safeLength = (value) => {
278
+ const trimmed = value.trim();
279
+ if (!trimmed || trimmed.length > 100) return null;
280
+ if (/[<>;}{"'`]/.test(trimmed)) return null;
281
+ return LENGTH.test(trimmed) ? trimmed : null;
282
+ };
283
+ const safeColor = (value) => {
284
+ const trimmed = value.trim();
285
+ if (!trimmed || trimmed.length > 200) return null;
286
+ if (/[;}<>]/.test(trimmed)) return null;
287
+ return trimmed;
288
+ };
289
+ /** A theme's custom properties for one set of colours, for a style attribute. */
290
+ function themeVariables(theme = {}, mode = null) {
291
+ const variables = {};
292
+ const colors = mode && (theme[mode] ?? DEFAULT_COLORS[mode]);
293
+ if (colors) {
294
+ const base = safeColor(colors.base);
295
+ const highlight = safeColor(colors.highlight);
296
+ if (base) variables["--ask-base"] = base;
297
+ if (highlight) variables["--ask-highlight"] = highlight;
298
+ }
299
+ if (theme.radius) {
300
+ const radius = safeLength(theme.radius);
301
+ if (radius) variables["--ask-radius"] = radius;
302
+ }
303
+ if (theme.animation && ANIMATION.has(theme.animation)) variables["--ask-animation"] = theme.animation;
304
+ if (theme.speed && SPEED.has(theme.speed)) variables["--ask-speed"] = theme.speed;
305
+ if (theme.text?.top && TEXT_TOP.has(theme.text.top)) variables["--ask-text-top"] = theme.text.top;
306
+ if (theme.text?.bottom && TEXT_BOTTOM.has(theme.text.bottom)) variables["--ask-text-bottom"] = theme.text.bottom;
307
+ return variables;
308
+ }
309
+ const declarations = (variables) => Object.entries(variables).map(([name, value]) => `${name}: ${value};`).join(" ");
310
+ /** CSS for a theme on `selector`; colours follow `mode`, and are left out when neither is given. */
311
+ function skeletonCSS({ theme = {}, mode, text, selector = ":root" }) {
312
+ const resolved = {
313
+ ...theme,
314
+ text: {
315
+ ...theme.text,
316
+ ...text
317
+ }
318
+ };
319
+ const colored = mode ?? (theme.light || theme.dark ? "system" : null);
320
+ const base = themeVariables(resolved, colored === "dark" ? "dark" : colored ? "light" : null);
321
+ const rules = [];
322
+ if (Object.keys(base).length) rules.push(`${selector} { ${declarations(base)} }`);
323
+ if (colored === "system") {
324
+ const dark = themeVariables({ dark: theme.dark }, "dark");
325
+ rules.push(`@media (prefers-color-scheme: dark) { ${selector} { ${declarations(dark)} } }`);
326
+ }
327
+ return rules.join("\n");
328
+ }
329
+ let themeEl = null;
330
+ /** Sets a theme for the whole page, for setups without a provider. */
331
+ function applyTheme(theme, mode, { nonce } = {}) {
332
+ themeEl ??= document.head.appendChild(document.createElement("style"));
333
+ if (nonce) themeEl.nonce = nonce;
334
+ themeEl.textContent = skeletonCSS({
335
+ theme,
336
+ mode
337
+ });
338
+ }
339
+ /** The same CSS as a whole element, for templates that can only render HTML strings. */
340
+ const themeStyleTag = (config, nonce) => {
341
+ const css = skeletonCSS(config);
342
+ return css ? `<style${nonce ? ` nonce="${nonce.replace(/"/g, "&quot;")}"` : ""}>${css}</style>` : "";
343
+ };
344
+ //#endregion
345
+ //#region src/core/debug.ts
346
+ const entries = /* @__PURE__ */ new Map();
347
+ let enabled;
348
+ let host$1 = null;
349
+ let frame = 0;
350
+ function fromPage() {
351
+ try {
352
+ return new URLSearchParams(location.search).has("ask-debug") || localStorage.getItem("a-smart-skeleton:debug") === "1";
353
+ } catch {
354
+ return false;
355
+ }
356
+ }
357
+ const everywhere = () => enabled ??= typeof window !== "undefined" && fromPage();
358
+ /** On with `?ask-debug`, `a-smart-skeleton:debug` = "1" in localStorage or `setDebug(true)`; for `element`, also inside `[ask-debug]`. */
359
+ const debugging = (element) => everywhere() || !!element?.closest(`[ask-debug]`);
360
+ function setDebug(on) {
361
+ enabled = on;
362
+ if (on) start();
363
+ else stop();
364
+ }
365
+ function reportLoading(reveal, source) {
366
+ if (!reveal || !debugging(reveal)) return;
367
+ entries.set(reveal, {
368
+ state: "loading",
369
+ source
370
+ });
371
+ show();
372
+ }
373
+ function reportReveal(reveal, moved) {
374
+ if (!reveal || !debugging(reveal)) return;
375
+ entries.set(reveal, {
376
+ ...entries.get(reveal),
377
+ state: "shown",
378
+ moved
379
+ });
380
+ show();
381
+ }
382
+ function show() {
383
+ start();
384
+ cancelAnimationFrame(frame);
385
+ draw();
386
+ }
387
+ const PANEL_CSS = `
388
+ :host { all: initial; }
389
+ .box { position: fixed; box-sizing: border-box; border: 1px dashed; border-radius: 4px; }
390
+ .label {
391
+ position: absolute; left: -1px; bottom: 100%; margin-bottom: 2px;
392
+ padding: 1px 6px; border-radius: 4px; white-space: nowrap;
393
+ font: 500 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; color: #fff;
394
+ }
395
+ .loading { border-color: #3b82f6; } .loading .label { background: #3b82f6; }
396
+ .still { border-color: #22c55e; } .still .label { background: #16a34a; }
397
+ .moved { border-color: #ef4444; border-style: solid; } .moved .label { background: #dc2626; }
398
+ .panel {
399
+ position: fixed; right: 12px; bottom: 12px; display: flex; gap: 8px; align-items: center;
400
+ padding: 6px 10px; border-radius: 8px; background: #111; color: #fff; pointer-events: auto;
401
+ font: 500 12px/16px ui-sans-serif, system-ui, sans-serif; box-shadow: 0 4px 16px rgb(0 0 0 / 0.3);
402
+ }
403
+ .panel label { display: flex; gap: 4px; align-items: center; cursor: pointer; }
404
+ `;
405
+ function start() {
406
+ if (typeof document === "undefined") return;
407
+ if (!host$1?.isConnected) {
408
+ host$1 = document.createElement("div");
409
+ host$1.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:2147483647";
410
+ const shadow = host$1.attachShadow({ mode: "open" });
411
+ shadow.innerHTML = `<style>${PANEL_CSS}</style><div class="boxes"></div>`;
412
+ document.body.append(host$1);
413
+ }
414
+ const shadow = host$1.shadowRoot;
415
+ if (enabled && !shadow.querySelector(".panel")) {
416
+ const panel = document.createElement("div");
417
+ panel.className = "panel";
418
+ panel.innerHTML = `a-smart-skeleton debug <label><input type="checkbox" class="xray"> x-ray</label>`;
419
+ panel.querySelector(".xray").addEventListener("change", (event) => {
420
+ document.documentElement.toggleAttribute(ATTR_XRAY, event.target.checked);
421
+ });
422
+ shadow.append(panel);
423
+ }
424
+ if (!frame) frame = requestAnimationFrame(draw);
425
+ }
426
+ function stop() {
427
+ host$1?.shadowRoot?.querySelector(".panel")?.remove();
428
+ document.documentElement.removeAttribute(ATTR_XRAY);
429
+ if ([...entries.keys()].some((reveal) => debugging(reveal))) return;
430
+ cancelAnimationFrame(frame);
431
+ frame = 0;
432
+ host$1?.remove();
433
+ host$1 = null;
434
+ }
435
+ const describe = ({ state, source, moved }) => {
436
+ const from = source ? ` · ${source}` : "";
437
+ if (state === "loading") return {
438
+ kind: "loading",
439
+ text: `skeleton${from}`
440
+ };
441
+ if (!moved || moved.width === 0 && moved.height === 0) return {
442
+ kind: "still",
443
+ text: `✓ no movement${from}`
444
+ };
445
+ return {
446
+ kind: "moved",
447
+ text: `✗ moved ${moved.width}×${moved.height}px${from}`
448
+ };
449
+ };
450
+ function draw() {
451
+ frame = 0;
452
+ if (!host$1?.shadowRoot) return;
453
+ const boxes = host$1.shadowRoot.querySelector(".boxes");
454
+ for (const reveal of entries.keys()) if (!reveal.isConnected || !debugging(reveal)) entries.delete(reveal);
455
+ const items = [...entries];
456
+ while (boxes.children.length > items.length) boxes.lastElementChild.remove();
457
+ items.forEach(([reveal, entry], index) => {
458
+ let box = boxes.children[index];
459
+ if (!box) {
460
+ box = boxes.appendChild(document.createElement("div"));
461
+ box.innerHTML = `<span class="label"></span>`;
462
+ }
463
+ const rect = reveal.getBoundingClientRect();
464
+ const { kind, text } = describe(entry);
465
+ box.className = `box ${kind}`;
466
+ Object.assign(box.style, {
467
+ left: `${rect.left - 2}px`,
468
+ top: `${rect.top - 2}px`,
469
+ width: `${rect.width + 4}px`,
470
+ height: `${rect.height + 4}px`
471
+ });
472
+ box.firstElementChild.textContent = text;
473
+ });
474
+ if (enabled || entries.size) frame = requestAnimationFrame(draw);
475
+ }
476
+ //#endregion
477
+ //#region src/core/canvas.ts
478
+ let context;
479
+ const textContext = () => context ??= /jsdom/.test(navigator.userAgent) ? null : document.createElement("canvas").getContext("2d");
480
+ //#endregion
481
+ //#region src/core/snapshot.ts
482
+ const MEDIA$1 = new Set(MEDIA_TAGS);
483
+ const REMOVED = "script, noscript, template, base, meta, link";
484
+ const REPLACED = new Set([
485
+ "iframe",
486
+ "object",
487
+ "embed"
488
+ ]);
489
+ const STRIPPED = new Set([
490
+ "src",
491
+ "srcset",
492
+ "sizes",
493
+ "poster",
494
+ "data",
495
+ "srcdoc",
496
+ "action",
497
+ "formaction",
498
+ "formtarget",
499
+ "background",
500
+ "ping",
501
+ "dynsrc",
502
+ "lowsrc",
503
+ "value",
504
+ "alt",
505
+ ATTR_COVERED
506
+ ]);
507
+ const STYLE_UNSAFE = /url\s*\(\s*['"]?\s*(javascript|vbscript):|expression\s*\(|<|>/i;
508
+ const fill = (value) => value.replace(/[A-Z]/g, "X").replace(/[a-z]/g, "x").replace(/\d/g, "0");
509
+ function fillText(original, copy) {
510
+ const filled = fill(copy.data);
511
+ copy.data = filled;
512
+ const parent = original.parentElement;
513
+ const context = textContext();
514
+ if (!parent || !context || !filled.trim()) return;
515
+ const style = getComputedStyle(parent);
516
+ context.font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
517
+ const shape = (value) => {
518
+ const collapsed = value.replace(/\s+/g, " ");
519
+ if (style.textTransform === "uppercase") return collapsed.toUpperCase();
520
+ if (style.textTransform === "lowercase") return collapsed.toLowerCase();
521
+ return collapsed;
522
+ };
523
+ const delta = (context.measureText(shape(original.data)).width - context.measureText(shape(filled)).width) / shape(filled).length;
524
+ if (Math.abs(delta) < .01) return;
525
+ const span = document.createElement("span");
526
+ span.style.letterSpacing = `${(parseFloat(style.letterSpacing) || 0) + delta}px`;
527
+ copy.replaceWith(span);
528
+ span.append(copy);
529
+ }
530
+ function sanitize(root) {
531
+ for (const element of root.querySelectorAll(REMOVED)) element.remove();
532
+ for (const element of root.querySelectorAll("*")) {
533
+ if (REPLACED.has(element.localName)) {
534
+ const stand = document.createElement("span");
535
+ stand.style.cssText = element.getAttribute("style") ?? "";
536
+ stand.style.display ||= "inline-block";
537
+ element.replaceWith(stand);
538
+ continue;
539
+ }
540
+ for (const { name, value } of [...element.attributes]) {
541
+ const lower = name.toLowerCase();
542
+ if (lower.startsWith("on") || STRIPPED.has(lower) || lower.endsWith("href") && /^\s*(javascript|vbscript|data):/i.test(value) || lower === "style" && STYLE_UNSAFE.test(value)) element.removeAttribute(name);
543
+ }
544
+ if (element.localName === "img") element.setAttribute("alt", "");
545
+ }
546
+ for (const source of root.querySelectorAll("video source, picture source")) source.remove();
547
+ }
548
+ /** The markup in `root`, safe to keep: no scripts, handlers or media sources; `filler` replaces its text too. */
549
+ function serialize(root, filler) {
550
+ const clone = root.cloneNode(true);
551
+ const show = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT;
552
+ const original = document.createTreeWalker(root, show);
553
+ const copy = document.createTreeWalker(clone, show);
554
+ const texts = [];
555
+ const media = [];
556
+ while (original.nextNode() && copy.nextNode()) {
557
+ const a = original.currentNode;
558
+ const b = copy.currentNode;
559
+ if (a.nodeType === Node.TEXT_NODE) {
560
+ if (filler) texts.push([a, b]);
561
+ } else if (MEDIA$1.has(a.localName) || REPLACED.has(a.localName)) media.push([a, b]);
562
+ }
563
+ for (const [a, b] of media) {
564
+ const rect = a.getBoundingClientRect();
565
+ b.style.boxSizing = "border-box";
566
+ b.style.width = `${rect.width}px`;
567
+ if (rect.width && rect.height) {
568
+ b.style.maxWidth = "100%";
569
+ b.style.height = "auto";
570
+ b.style.aspectRatio = `${rect.width} / ${rect.height}`;
571
+ } else b.style.height = `${rect.height}px`;
572
+ }
573
+ for (const [a, b] of texts) fillText(a, b);
574
+ sanitize(clone);
575
+ return clone.innerHTML;
576
+ }
577
+ function restore$1(html) {
578
+ const template = document.createElement("template");
579
+ template.innerHTML = html;
580
+ sanitize(template.content);
581
+ return template.content;
582
+ }
583
+ const signature = (element, depth) => [...element.children].map((child) => {
584
+ const classes = [...child.classList].sort().join(".");
585
+ const inner = depth > 1 && child.firstElementChild ? `(${signature(child, depth - 1)})` : "";
586
+ return `${child.localName}${classes ? `.${classes}` : ""}${inner}`;
587
+ }).join(",");
588
+ const hash = (value) => {
589
+ let h = 5381;
590
+ for (let i = 0; i < value.length; i++) h = h * 33 ^ value.charCodeAt(i);
591
+ return (h >>> 0).toString(36);
592
+ };
593
+ /** Identifies content by its page and the shape of its top two levels — the parts that rarely hang on data. */
594
+ function autoKey(element) {
595
+ const shape = signature(element, 2);
596
+ return shape ? `${location.pathname}|${hash(shape)}` : null;
597
+ }
598
+ //#endregion
599
+ //#region src/core/dev.ts
600
+ let dev = false;
601
+ try {
602
+ dev = process.env.NODE_ENV !== "production";
603
+ } catch {
604
+ dev = false;
605
+ }
606
+ const isDev = dev;
607
+ const warned = /* @__PURE__ */ new Set();
608
+ function warnOnce(id, message) {
609
+ if (!isDev || warned.has(id)) return;
610
+ warned.add(id);
611
+ console.warn(`[a-smart-skeleton] ${message}`);
612
+ }
613
+ const URL_LIKE = /^(https?:|\/|data:|blob:)/;
614
+ /**
615
+ * Warns once per app if the page has a meta CSP that restricts inline scripts and
616
+ * the consumer did not pass a nonce through the provider. Silent when CSP is absent
617
+ * or when the loading pane isn't on the client yet — we can't read CSP headers from
618
+ * script, so the meta-tag check is a best-effort signal to developers.
619
+ */
620
+ function checkNonce(nonce) {
621
+ if (!isDev || typeof document === "undefined" || nonce) return;
622
+ const content = document.querySelector("meta[http-equiv=\"Content-Security-Policy\" i]")?.getAttribute("content") ?? "";
623
+ if (!/script-src/i.test(content)) return;
624
+ if (/'unsafe-inline'/.test(content) || /\*/.test(content)) return;
625
+ warnOnce("csp:missing-nonce", "ASmartSkeleton ships an inline boot script that measures the server-rendered skeleton before first paint. Your page has a Content-Security-Policy with script-src but no nonce was passed to <ASmartSkeletonProvider :nonce=\"…\">, so the browser will block it and first paint will be blank. Pass the same nonce you used for the response header.");
626
+ }
627
+ /** Same keys and lengths as `data`, with every string replaced by filler. */
628
+ function sampleFrom(data) {
629
+ if (typeof data === "string") return URL_LIKE.test(data) ? "" : fill(data);
630
+ if (Array.isArray(data)) return data.map(sampleFrom);
631
+ if (data instanceof Date) return data;
632
+ if (data && typeof data === "object") return Object.fromEntries(Object.entries(data).map(([key, value]) => [key, sampleFrom(value)]));
633
+ return data;
634
+ }
635
+ //#endregion
636
+ //#region src/core/measure.ts
637
+ const intersect = (clip, rect) => clip ? {
638
+ left: Math.max(clip.left, rect.left),
639
+ right: Math.min(clip.right, rect.right),
640
+ top: Math.max(clip.top, rect.top),
641
+ bottom: Math.min(clip.bottom, rect.bottom)
642
+ } : rect;
643
+ const cornersOf = (rect) => [
644
+ [rect.left, rect.top],
645
+ [rect.right, rect.top],
646
+ [rect.right, rect.bottom],
647
+ [rect.left, rect.bottom]
648
+ ];
649
+ const near = (a, b) => Math.abs(a[0] - b[0]) < 1 && Math.abs(a[1] - b[1]) < 1;
650
+ function radiiOf(style, rect) {
651
+ const size = Math.min(rect.right - rect.left, rect.bottom - rect.top);
652
+ const px = (value) => value.endsWith("%") ? parseFloat(value) / 100 * size : parseFloat(value) || 0;
653
+ return [
654
+ px(style.borderTopLeftRadius),
655
+ px(style.borderTopRightRadius),
656
+ px(style.borderBottomRightRadius),
657
+ px(style.borderBottomLeftRadius)
658
+ ];
659
+ }
660
+ function clipWithin(clip, rect, style) {
661
+ const box = intersect(clip, rect);
662
+ const own = radiiOf(style, rect);
663
+ const outer = clip ? cornersOf(clip) : [];
664
+ const ownCorners = cornersOf(rect);
665
+ const corners = cornersOf(box).map((corner, i) => near(corner, ownCorners[i]) ? own[i] : clip && near(corner, outer[i]) ? clip.corners[i] : 0);
666
+ const { left, right, top, bottom } = box;
667
+ return {
668
+ left,
669
+ right,
670
+ top,
671
+ bottom,
672
+ corners
673
+ };
674
+ }
675
+ function roundedBy(clip, box, own, style) {
676
+ if (!clip) return own;
677
+ const clipCorners = cornersOf(clip);
678
+ const touching = cornersOf(box).map((corner, i) => clip.corners[i] > 0 && near(corner, clipCorners[i]));
679
+ if (!touching.some(Boolean)) return own;
680
+ const mine = radiiOf(style, box);
681
+ return touching.map((touches, i) => touches ? Math.max(mine[i], clip.corners[i]) : mine[i]).map((r) => `${r}px`).join(" ");
682
+ }
683
+ const clipped = (rect, clip) => {
684
+ const box = intersect(clip, rect);
685
+ return box.right - box.left >= 2 && box.bottom - box.top >= 2 ? box : null;
686
+ };
687
+ const DEFAULT_TEXT = {
688
+ top: "ascender",
689
+ bottom: "descender"
690
+ };
691
+ const metricsByFont = /* @__PURE__ */ new Map();
692
+ const lineRects = (range) => [...range.getClientRects?.() ?? []];
693
+ function metricsOf(style) {
694
+ const font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
695
+ if (metricsByFont.has(font)) return metricsByFont.get(font);
696
+ const context = textContext();
697
+ let metrics = null;
698
+ if (context) {
699
+ context.font = font;
700
+ const box = context.measureText("H");
701
+ const up = (text) => context.measureText(text).actualBoundingBoxAscent;
702
+ metrics = {
703
+ ascent: box.fontBoundingBoxAscent,
704
+ descent: box.fontBoundingBoxDescent,
705
+ ascender: up("bdfhkl"),
706
+ capHeight: box.actualBoundingBoxAscent,
707
+ xHeight: up("xz"),
708
+ descender: context.measureText("gjpqy").actualBoundingBoxDescent
709
+ };
710
+ }
711
+ metricsByFont.set(font, metrics);
712
+ return metrics;
713
+ }
714
+ const TOPS = {
715
+ ascender: "ascender",
716
+ "cap-height": "capHeight",
717
+ "x-height": "xHeight"
718
+ };
719
+ const rule = (style, name, allowed, fallback) => {
720
+ const value = style.getPropertyValue(name).trim();
721
+ return allowed.includes(value) ? value : fallback;
722
+ };
723
+ function barOf(rect, style, fontSize) {
724
+ const metrics = metricsOf(style);
725
+ if (!metrics) {
726
+ const height = Math.min(fontSize * .72, rect.bottom - rect.top);
727
+ const middle = (rect.top + rect.bottom) / 2;
728
+ return {
729
+ barTop: middle - height / 2,
730
+ barBottom: middle + height / 2
731
+ };
732
+ }
733
+ const top = rule(style, "--ask-text-top", Object.keys(TOPS), DEFAULT_TEXT.top);
734
+ const bottom = rule(style, "--ask-text-bottom", ["baseline", "descender"], DEFAULT_TEXT.bottom);
735
+ const box = metrics.ascent + metrics.descent;
736
+ const baseline = rect.top + (rect.bottom - rect.top - box) / 2 + metrics.ascent;
737
+ return {
738
+ barTop: baseline - metrics[TOPS[top]],
739
+ barBottom: baseline + (bottom === "descender" ? metrics.descender : 0)
740
+ };
741
+ }
742
+ const MEDIA = new Set(MEDIA_TAGS);
743
+ const TEXT = new Set(TEXT_TAGS);
744
+ const LINE_GAP = .4;
745
+ const TRANSPARENT = /[,/]\s*0\)$/;
746
+ const isSet = (value, none) => value !== "" && value !== none;
747
+ const SIDES = [
748
+ "Top",
749
+ "Right",
750
+ "Bottom",
751
+ "Left"
752
+ ];
753
+ const bordered = (style) => SIDES.every((side) => isSet(style[`border${side}Style`], "none") && isSet(style[`border${side}Width`], "0px"));
754
+ const anyBordered = (style) => SIDES.some((side) => isSet(style[`border${side}Style`], "none") && isSet(style[`border${side}Width`], "0px"));
755
+ const isRule = (shadow) => {
756
+ const [x = 0, y = 0, blur = 0, spread = 0] = (shadow.replace(/rgba?\([^)]*\)|#\w+/g, "").match(/-?[\d.]+px/g) ?? []).map(parseFloat);
757
+ return shadow.includes("inset") && !blur && !spread && x === 0 !== (y === 0) && Math.abs(x + y) <= 2;
758
+ };
759
+ const shadowPaints = (boxShadow) => isSet(boxShadow, "none") && !boxShadow.split(/,(?![^(]*\))/).every(isRule);
760
+ /** Splits `background-image` on its top-level commas (respecting nested parens, which
761
+ * gradient stops abuse heavily) and drops any layer that starts with `url(`. Returns
762
+ * an empty string when nothing survives — the caller treats that as "no background". */
763
+ function stripUrlLayers(bgImage) {
764
+ const layers = [];
765
+ let depth = 0;
766
+ let start = 0;
767
+ for (let i = 0; i < bgImage.length; i++) {
768
+ const ch = bgImage[i];
769
+ if (ch === "(") depth++;
770
+ else if (ch === ")") depth--;
771
+ else if (ch === "," && depth === 0) {
772
+ layers.push(bgImage.slice(start, i));
773
+ start = i + 1;
774
+ }
775
+ }
776
+ layers.push(bgImage.slice(start));
777
+ return layers.map((l) => l.trim()).filter((l) => l && !l.startsWith("url(")).join(", ");
778
+ }
779
+ const isPainted = (style) => isSet(style.backgroundColor, "transparent") && !TRANSPARENT.test(style.backgroundColor) || isSet(style.backgroundImage, "none") || shadowPaints(style.boxShadow) || bordered(style);
780
+ /** Reads the painted chrome off a container so the skeleton can reproduce it exactly —
781
+ * background, four border sides, and box-shadow, but only the properties that are set.
782
+ * Returns `undefined` when nothing paints, so callers can skip emitting a chrome bone. */
783
+ function captureChrome(style) {
784
+ const chrome = {};
785
+ const bg = style.backgroundColor;
786
+ if (isSet(bg, "transparent") && !TRANSPARENT.test(bg)) chrome.background = bg;
787
+ const bgImage = style.backgroundImage;
788
+ if (isSet(bgImage, "none")) {
789
+ const gradientsOnly = stripUrlLayers(bgImage);
790
+ if (gradientsOnly) chrome.background = chrome.background ? `${gradientsOnly}, ${chrome.background}` : gradientsOnly;
791
+ }
792
+ for (const side of SIDES) {
793
+ const width = style[`border${side}Width`];
794
+ const styleProp = style[`border${side}Style`];
795
+ const color = style[`border${side}Color`];
796
+ if (isSet(styleProp, "none") && isSet(width, "0px") && parseFloat(width) > 0) {
797
+ const key = `border${side}`;
798
+ chrome[key] = `${width} ${styleProp} ${color}`;
799
+ }
800
+ }
801
+ if (shadowPaints(style.boxShadow)) chrome.boxShadow = style.boxShadow;
802
+ return Object.keys(chrome).length ? chrome : void 0;
803
+ }
804
+ const isMedia = (element) => MEDIA.has(element.localName);
805
+ const isEmptyText = (element) => TEXT.has(element.localName) && !element.firstElementChild && !element.textContent?.trim();
806
+ /** Labels inside: one per piece of text, however many lines it wraps to or nodes it's split into. */
807
+ const countLabels = (element) => {
808
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
809
+ const range = document.createRange();
810
+ const pieces = [];
811
+ let nodes = 0;
812
+ while (walker.nextNode()) {
813
+ const node = walker.currentNode;
814
+ if (node.nodeType === Node.TEXT_NODE) {
815
+ const parent = node.parentElement;
816
+ if (!node.textContent?.trim() || !parent) continue;
817
+ nodes++;
818
+ const box = parent.getBoundingClientRect();
819
+ if (box.width <= 1 || box.height <= 1) continue;
820
+ range.selectNodeContents(node);
821
+ pieces.push(lineRects(range).filter((r) => r.width || r.height));
822
+ } else if (isEmptyText(node)) {
823
+ nodes++;
824
+ pieces.push([...node.getClientRects()]);
825
+ }
826
+ }
827
+ if (!pieces.some((rects) => rects.length)) return nodes;
828
+ const reach = (parseFloat(getComputedStyle(element).fontSize) || 16) * LINE_GAP;
829
+ const beside = (a, b) => Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) >= Math.min(a.height, b.height) / 2 && Math.max(a.left, b.left) - Math.min(a.right, b.right) <= reach;
830
+ const label = pieces.map((_, i) => i);
831
+ const find = (i) => label[i] === i ? i : label[i] = find(label[i]);
832
+ for (let i = 0; i < pieces.length; i++) for (let j = i + 1; j < pieces.length; j++) if (pieces[i].some((a) => pieces[j].some((b) => beside(a, b)))) label[find(j)] = find(i);
833
+ return new Set(pieces.map((_, i) => find(i))).size;
834
+ };
835
+ const isBlock = (element, style) => element.hasAttribute("ask-block") || isMedia(element) || isPainted(style) && countLabels(element) <= 1;
836
+ function radiusOf(element, style) {
837
+ if (parseFloat(style.borderRadius) || !element.parentElement) return style.borderRadius || "0px";
838
+ const parent = element.parentElement;
839
+ const parentStyle = getComputedStyle(parent);
840
+ const a = element.getBoundingClientRect();
841
+ const b = parent.getBoundingClientRect();
842
+ return Math.abs(a.width - b.width) < 1 && Math.abs(a.height - b.height) < 1 && parentStyle.overflow !== "visible" ? parentStyle.borderRadius : style.borderRadius || "0px";
843
+ }
844
+ function toBars(lines) {
845
+ lines.sort((a, b) => a.left - b.left);
846
+ const merged = [];
847
+ for (const line of lines) {
848
+ const last = merged.find((m) => {
849
+ const overlap = Math.min(m.bottom, line.bottom) - Math.max(m.top, line.top);
850
+ const shorter = Math.min(m.bottom - m.top, line.bottom - line.top);
851
+ const gap = line.left - m.right;
852
+ return overlap >= shorter / 2 && gap <= Math.max(m.fontSize, line.fontSize) * LINE_GAP;
853
+ });
854
+ if (last) {
855
+ last.left = Math.min(last.left, line.left);
856
+ last.right = Math.max(last.right, line.right);
857
+ last.top = Math.min(last.top, line.top);
858
+ last.bottom = Math.max(last.bottom, line.bottom);
859
+ last.fontSize = Math.max(last.fontSize, line.fontSize);
860
+ last.barTop = Math.min(last.barTop, line.barTop);
861
+ last.barBottom = Math.max(last.barBottom, line.barBottom);
862
+ } else merged.push({ ...line });
863
+ }
864
+ return merged.map((line) => {
865
+ const height = line.barBottom - line.barTop;
866
+ return {
867
+ type: "text",
868
+ x: line.left,
869
+ y: line.barTop,
870
+ width: line.right - line.left,
871
+ height,
872
+ radius: line.radius || `${Math.round(height / 3)}px`
873
+ };
874
+ });
875
+ }
876
+ /** Measures what `root` holds into rects relative to `layer`; without `mark`, `root` is left untouched. */
877
+ function measureSkeleton(root, layer, { mark = true, onCovered } = {}) {
878
+ const origin = layer.getBoundingClientRect();
879
+ const scaleX = layer.offsetWidth ? origin.width / layer.offsetWidth : 1;
880
+ const scaleY = layer.offsetHeight ? origin.height / layer.offsetHeight : 1;
881
+ const local = (rect) => ({
882
+ left: (rect.left - origin.left) / scaleX,
883
+ right: (rect.right - origin.left) / scaleX,
884
+ top: (rect.top - origin.top) / scaleY,
885
+ bottom: (rect.bottom - origin.top) / scaleY
886
+ });
887
+ const blocks = [];
888
+ const lines = [];
889
+ const covered = /* @__PURE__ */ new Set();
890
+ const range = document.createRange();
891
+ const addLine = (rect, style, clip) => {
892
+ const box = clipped(rect, clip);
893
+ if (!box) return;
894
+ const fontSize = parseFloat(style.fontSize) || 16;
895
+ const line = local(box);
896
+ lines.push({
897
+ ...line,
898
+ fontSize,
899
+ ...barOf(line, style, fontSize),
900
+ radius: style.getPropertyValue("--ask-radius").trim()
901
+ });
902
+ };
903
+ const addText = (node, clip) => {
904
+ const start = node.data.search(/\S/);
905
+ if (start < 0 || !node.parentElement) return;
906
+ range.setStart(node, start);
907
+ range.setEnd(node, node.data.trimEnd().length);
908
+ const style = getComputedStyle(node.parentElement);
909
+ for (const rect of lineRects(range)) addLine(rect, style, clip);
910
+ };
911
+ const visit = (parent, clip) => {
912
+ for (const node of parent.childNodes) {
913
+ if (node.nodeType === Node.TEXT_NODE) {
914
+ addText(node, clip);
915
+ continue;
916
+ }
917
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
918
+ const element = node;
919
+ if (element.hasAttribute("ask-ignore") || element.hasAttribute("ask-own")) continue;
920
+ const style = getComputedStyle(element);
921
+ if (style.display === "none") continue;
922
+ if (isBlock(element, style)) {
923
+ const visible = clipped(element.getBoundingClientRect(), clip);
924
+ if (visible) {
925
+ const box = local(visible);
926
+ blocks.push({
927
+ type: isMedia(element) ? "media" : "box",
928
+ x: box.left,
929
+ y: box.top,
930
+ width: box.right - box.left,
931
+ height: box.bottom - box.top,
932
+ radius: roundedBy(clip, visible, radiusOf(element, style), style)
933
+ });
934
+ }
935
+ if (!isMedia(element)) covered.add(element);
936
+ continue;
937
+ }
938
+ if (isEmptyText(element)) {
939
+ for (const rect of element.getClientRects()) addLine(rect, style, clip);
940
+ continue;
941
+ }
942
+ if (isPainted(style) || anyBordered(style)) {
943
+ const chrome = captureChrome(style);
944
+ if (chrome) {
945
+ const visible = clipped(element.getBoundingClientRect(), clip);
946
+ if (visible) {
947
+ const box = local(visible);
948
+ blocks.push({
949
+ type: "chrome",
950
+ x: box.left,
951
+ y: box.top,
952
+ width: box.right - box.left,
953
+ height: box.bottom - box.top,
954
+ radius: roundedBy(clip, visible, radiusOf(element, style), style),
955
+ chrome
956
+ });
957
+ }
958
+ }
959
+ }
960
+ visit(element, style.overflowX !== "visible" || style.overflowY !== "visible" ? clipWithin(clip, element.getBoundingClientRect(), style) : clip);
961
+ }
962
+ };
963
+ visit(root, null);
964
+ onCovered?.(covered);
965
+ if (!mark) return [...blocks, ...toBars(lines)];
966
+ for (const element of root.querySelectorAll(`[${ATTR_COVERED}]`)) if (!covered.has(element)) element.removeAttribute(ATTR_COVERED);
967
+ for (const element of covered) if (!element.hasAttribute("ask-covered")) element.setAttribute(ATTR_COVERED, "");
968
+ return [...blocks, ...toBars(lines)];
969
+ }
970
+ //#endregion
971
+ //#region src/core/motion.ts
972
+ const prefers = (query) => typeof matchMedia !== "undefined" && matchMedia(query).matches;
973
+ const canAnimate = () => typeof Element !== "undefined" && typeof Element.prototype.animate === "function";
974
+ const prefersReducedMotion = () => !canAnimate() || prefers("(prefers-reduced-motion: reduce)");
975
+ const prefersReducedTransparency = () => prefers("(prefers-reduced-transparency: reduce)");
976
+ const animationsOf = (element) => element.getAnimations?.() ?? [];
977
+ //#endregion
978
+ //#region src/core/shimmer.ts
979
+ const SWEEP = .7;
980
+ const MAX_BAND = 320;
981
+ const SPACING = 2.5;
982
+ const PULSE_LOW = .45;
983
+ const ANIMATIONS = [
984
+ "shimmer",
985
+ "group-shimmer",
986
+ "pulse",
987
+ "none"
988
+ ];
989
+ /** The animation an element inherits from the theme: `--ask-animation` and `--ask-speed`. */
990
+ function motionOf(element) {
991
+ const style = getComputedStyle(element);
992
+ const read = (name, allowed, fallback) => {
993
+ const value = style.getPropertyValue(name).trim();
994
+ return allowed.includes(value) ? value : fallback;
995
+ };
996
+ const animation = read("--ask-animation", ANIMATIONS, "shimmer");
997
+ const speed = read("--ask-speed", ["slow", "fast"], "slow");
998
+ return {
999
+ animation,
1000
+ duration: DURATIONS[animation === "pulse" ? "pulse" : "shimmer"][speed]
1001
+ };
1002
+ }
1003
+ const SLANT = 20;
1004
+ const SIN = Math.sin(SLANT * Math.PI / 180);
1005
+ const COS = Math.cos(SLANT * Math.PI / 180);
1006
+ const SLANT_TAN = SIN / COS;
1007
+ const SLANT_COS = COS;
1008
+ const SLANT_ANGLE = `110deg`;
1009
+ const PEAK = .85;
1010
+ const ease = (t) => (1 - Math.cos(Math.PI * t)) / 2;
1011
+ /** Highlights `SPACING` band-widths apart, in px from the box's corner, so every rect draws the same stream. */
1012
+ function highlights(band) {
1013
+ const period = band * SPACING;
1014
+ const stop = (at, alpha) => `color-mix(in srgb, ${HIGHLIGHT} ${(alpha * 100).toFixed(1)}%, transparent) ${(at * band).toFixed(2)}px`;
1015
+ const stops = [];
1016
+ for (let i = 0; i <= 12; i++) stops.push(stop(i / 12 * PEAK, ease(i / 12)));
1017
+ for (let i = 1; i <= 12; i++) stops.push(stop(PEAK + i / 12 * (1 - PEAK), ease(1 - i / 12)));
1018
+ return {
1019
+ width: period / COS,
1020
+ image: `repeating-linear-gradient(${SLANT_ANGLE}, ${stops.join(", ")}, transparent ${period.toFixed(2)}px)`
1021
+ };
1022
+ }
1023
+ /** Animates `element` as its slice of one sweep, on the document clock, so however late it mounts it joins the sweep under way. */
1024
+ function shimmer(element, group, motion = motionOf(element)) {
1025
+ for (const animation of animationsOf(element)) animation.cancel();
1026
+ element.style.backgroundImage = "";
1027
+ element.style.backgroundRepeat = "";
1028
+ if (prefersReducedMotion() || typeof element.animate !== "function") return;
1029
+ if (motion.animation === "none") return;
1030
+ const timing = {
1031
+ duration: motion.duration,
1032
+ iterations: Infinity
1033
+ };
1034
+ if (motion.animation === "pulse") {
1035
+ element.animate([
1036
+ {
1037
+ opacity: 1,
1038
+ easing: "ease-in-out"
1039
+ },
1040
+ {
1041
+ opacity: PULSE_LOW,
1042
+ easing: "ease-in-out"
1043
+ },
1044
+ { opacity: 1 }
1045
+ ], timing).startTime = 0;
1046
+ return;
1047
+ }
1048
+ const rect = element.getBoundingClientRect();
1049
+ const x = rect.left + scrollX;
1050
+ const area = motion.animation === "group-shimmer" ? group ?? {
1051
+ left: x,
1052
+ top: rect.top,
1053
+ width: rect.width,
1054
+ height: rect.height
1055
+ } : {
1056
+ left: 0,
1057
+ top: 0,
1058
+ width: document.documentElement.clientWidth || innerWidth,
1059
+ height: innerHeight
1060
+ };
1061
+ const { width: tile, image } = highlights(Math.min(Math.max(area.width * .4, 80), MAX_BAND));
1062
+ const along = x - area.left + (rect.top - area.top) * SLANT_TAN;
1063
+ const pace = motion.duration * SWEEP / 1600;
1064
+ element.style.backgroundImage = image;
1065
+ element.style.backgroundSize = `${tile}px 100%`;
1066
+ element.style.backgroundRepeat = "repeat-x";
1067
+ element.animate([{ backgroundPosition: `${-along}px 0` }, { backgroundPosition: `${tile - along}px 0` }], {
1068
+ duration: tile * pace,
1069
+ iterations: Infinity
1070
+ }).startTime = 0;
1071
+ }
1072
+ /** ms per px of the band in `element`'s shimmer; null when it has no band. */
1073
+ function paceOf(element) {
1074
+ if (prefersReducedMotion()) return null;
1075
+ const motion = motionOf(element);
1076
+ if (motion.animation !== "shimmer" && motion.animation !== "group-shimmer") return null;
1077
+ return motion.duration * SWEEP / 1600;
1078
+ }
1079
+ /** The rect elements drawn into `layer`. */
1080
+ const rectsOf = (layer) => [...layer.querySelectorAll(`:scope > [${ATTR_RECT}]`)];
1081
+ /** Soft floor on skeleton bone corners — anything sharper than this reads as a glyph,
1082
+ * not a placeholder, which fights the whole point of a skeleton. 4 px is the smallest
1083
+ * radius that still reads as "rounded" at typical display DPI. Applied only to
1084
+ * text / media / box bones; chrome bones carry the consumer's own radius verbatim. */
1085
+ const MIN_BONE_RADIUS = 4;
1086
+ const clampRadius = (radius) => radius.replace(/(\d+(?:\.\d+)?)px/g, (_, raw) => {
1087
+ const n = parseFloat(raw);
1088
+ return Number.isFinite(n) && n < MIN_BONE_RADIUS ? `${MIN_BONE_RADIUS}px` : `${n}px`;
1089
+ });
1090
+ /** Draws `rects` into `layer`, reusing the elements already there.
1091
+ *
1092
+ * Bones live in the light DOM (not a shadow root) so a consumer's stylesheet
1093
+ * can reach them — selectors like `[ask-rect="text"]`, `[ask-rect="media"]`,
1094
+ * `[ask-rect="box"]`, or the `--ask-base` / `--ask-highlight` custom
1095
+ * properties — and theme the skeleton with the consumer's own tokens rather
1096
+ * than plugin defaults. Positioning (x/y/width/height/radius) stays inline
1097
+ * because it is per-bone; fill colour comes from the stylesheet.
1098
+ *
1099
+ * Added after mount, so Vue/React hydration never sees them.
1100
+ */
1101
+ function drawSkeleton(layer, rects) {
1102
+ const existing = rectsOf(layer);
1103
+ const rect = layer.getBoundingClientRect();
1104
+ const group = {
1105
+ left: rect.left + scrollX,
1106
+ top: rect.top,
1107
+ width: rect.width,
1108
+ height: rect.height
1109
+ };
1110
+ const motion = motionOf(layer);
1111
+ rects.forEach((part, index) => {
1112
+ let element = existing[index];
1113
+ if (!element) {
1114
+ element = document.createElement("div");
1115
+ element.style.cssText = `position:absolute;display:block;pointer-events:none;background-repeat:no-repeat`;
1116
+ layer.append(element);
1117
+ }
1118
+ element.setAttribute(ATTR_RECT, part.type);
1119
+ Object.assign(element.style, {
1120
+ left: `${part.x}px`,
1121
+ top: `${part.y}px`,
1122
+ width: `${part.width}px`,
1123
+ height: `${part.height}px`,
1124
+ borderRadius: part.type === "chrome" ? part.radius : clampRadius(part.radius)
1125
+ });
1126
+ if (part.type === "chrome") {
1127
+ for (const anim of animationsOf(element)) anim.cancel();
1128
+ element.style.backgroundImage = "";
1129
+ element.style.backgroundSize = "";
1130
+ element.style.backgroundRepeat = "";
1131
+ const chrome = part.chrome;
1132
+ element.style.background = chrome?.background ?? "";
1133
+ element.style.borderTop = chrome?.borderTop ?? "";
1134
+ element.style.borderRight = chrome?.borderRight ?? "";
1135
+ element.style.borderBottom = chrome?.borderBottom ?? "";
1136
+ element.style.borderLeft = chrome?.borderLeft ?? "";
1137
+ element.style.boxShadow = chrome?.boxShadow ?? "";
1138
+ element.dataset.motion = "chrome";
1139
+ return;
1140
+ }
1141
+ if (element.dataset.motion === "chrome") {
1142
+ element.style.background = "";
1143
+ element.style.borderTop = "";
1144
+ element.style.borderRight = "";
1145
+ element.style.borderBottom = "";
1146
+ element.style.borderLeft = "";
1147
+ element.style.boxShadow = "";
1148
+ }
1149
+ const key = `${Math.round(group.left + part.x)}|${Math.round(part.y)}|${Math.round(part.height)}|${Math.round(group.left)}|${Math.round(group.width)}|${Math.round(group.height)}|${document.documentElement.clientWidth}|${motion.animation}|${motion.duration}`;
1150
+ if (element.dataset.motion !== key) {
1151
+ element.dataset.motion = key;
1152
+ shimmer(element, group, motion);
1153
+ }
1154
+ });
1155
+ for (let i = existing.length - 1; i >= rects.length; i--) existing[i].remove();
1156
+ }
1157
+ //#endregion
1158
+ //#region src/core/plan.ts
1159
+ const SKELETON_DELAY = 150;
1160
+ const QUICK_FADE = 120;
1161
+ const SMALL_HEIGHT = 72;
1162
+ const SMALL_PIECES = 3;
1163
+ const SECONDARY_DELAY = 60;
1164
+ const ACTIVE_WINDOW = 400;
1165
+ const MAX_DELAY = 1e3;
1166
+ const MAX_DURATION = 1e3;
1167
+ const ENTER = new Set([
1168
+ "filter",
1169
+ "transform",
1170
+ "translate",
1171
+ "scale",
1172
+ "rotate",
1173
+ "clipPath",
1174
+ "easing",
1175
+ "composite"
1176
+ ]);
1177
+ let lastInput = -Infinity;
1178
+ let listening = false;
1179
+ function listen() {
1180
+ if (listening || typeof addEventListener === "undefined") return;
1181
+ listening = true;
1182
+ for (const type of [
1183
+ "wheel",
1184
+ "touchmove",
1185
+ "keydown"
1186
+ ]) addEventListener(type, () => lastInput = performance.now(), {
1187
+ passive: true,
1188
+ capture: true
1189
+ });
1190
+ }
1191
+ const loading = /* @__PURE__ */ new WeakMap();
1192
+ /** Notes a skeleton going up in `reveal` — when, and whether it's a refetch over the data on screen. */
1193
+ function noteSkeleton(reveal, { served, refetch }) {
1194
+ listen();
1195
+ const since = loading.get(reveal)?.since ?? performance.now();
1196
+ loading.set(reveal, {
1197
+ since: served ? -Infinity : since,
1198
+ refetch
1199
+ });
1200
+ }
1201
+ function pieces(root) {
1202
+ const found = [];
1203
+ const visit = (element) => {
1204
+ for (const child of element.children) {
1205
+ const ownText = [...child.childNodes].some((node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim());
1206
+ if (!child.firstElementChild || ownText || child.localName === "svg") found.push(child);
1207
+ else visit(child);
1208
+ }
1209
+ };
1210
+ visit(root);
1211
+ return found;
1212
+ }
1213
+ function unitOf(piece, root) {
1214
+ for (let element = piece; element && element !== root; element = element.parentElement) {
1215
+ const parent = element.parentElement;
1216
+ if (!parent) break;
1217
+ if ([...parent.children].filter((sibling) => sibling.localName === element.localName && sibling.className === element.className).length >= 3) return element;
1218
+ }
1219
+ return piece;
1220
+ }
1221
+ const contains = (outer, inner) => {
1222
+ const x = inner.left + inner.width / 2;
1223
+ const y = inner.top + inner.height / 2;
1224
+ return x >= outer.left && x <= outer.right && y >= outer.top && y <= outer.bottom;
1225
+ };
1226
+ /** The box a wipe crosses: the reveal, and any pane leaving it that stands taller or wider. */
1227
+ function revealBox(root) {
1228
+ let { left, top, right, bottom } = root.getBoundingClientRect();
1229
+ for (const pane of root.querySelectorAll(`:scope > [${ATTR_EXITING}]`)) {
1230
+ const rect = pane.getBoundingClientRect();
1231
+ left = Math.min(left, rect.left);
1232
+ top = Math.min(top, rect.top);
1233
+ right = Math.max(right, rect.right);
1234
+ bottom = Math.max(bottom, rect.bottom);
1235
+ }
1236
+ return new DOMRect(left, top, right - left, bottom - top);
1237
+ }
1238
+ const clamp = (value, max) => Math.min(Math.max(value, 0), max);
1239
+ let batch = -Infinity;
1240
+ /** Times each piece of `content`, and each skeleton rect it replaces, by `order`. */
1241
+ function sequencePlan(content, root, order, spec, duration) {
1242
+ const now = performance.now();
1243
+ if (now - batch > 150) batch = now;
1244
+ const late = now - batch;
1245
+ const reveal = root.getBoundingClientRect();
1246
+ const units = /* @__PURE__ */ new Map();
1247
+ const found = /* @__PURE__ */ new Map();
1248
+ const sizeOf = (element) => parseFloat(getComputedStyle(element).fontSize) || 16;
1249
+ for (const piece of pieces(content)) {
1250
+ const element = unitOf(piece, content);
1251
+ let unit = units.get(element);
1252
+ if (!unit) {
1253
+ unit = {
1254
+ rect: element.getBoundingClientRect(),
1255
+ size: 0
1256
+ };
1257
+ units.set(element, unit);
1258
+ }
1259
+ const size = sizeOf(piece);
1260
+ unit.size = Math.max(unit.size, size);
1261
+ found.set(piece, {
1262
+ unit,
1263
+ size
1264
+ });
1265
+ }
1266
+ const at = (piece) => {
1267
+ const delay = order(piece);
1268
+ return delay === null ? null : clamp(delay, MAX_DELAY) + Math.max(spec.delay ?? 0, 0);
1269
+ };
1270
+ return {
1271
+ kind: "sequence",
1272
+ duration,
1273
+ enter: spec.enter,
1274
+ pieceDelay: (element) => {
1275
+ const piece = found.get(element);
1276
+ if (!piece) return null;
1277
+ return at({
1278
+ rect: element.getBoundingClientRect(),
1279
+ unit: piece.unit.rect,
1280
+ secondary: piece.size < piece.unit.size,
1281
+ reveal,
1282
+ late
1283
+ });
1284
+ },
1285
+ boneDelay: (rect) => {
1286
+ const unit = [...units.values()].find((unit) => contains(unit.rect, rect));
1287
+ return at({
1288
+ rect,
1289
+ unit: unit?.rect ?? null,
1290
+ secondary: !!unit && rect.height < unit.size * .8,
1291
+ reveal,
1292
+ late
1293
+ });
1294
+ }
1295
+ };
1296
+ }
1297
+ /** Keeps `enter` to what paints, warning in development about anything that would move the page. */
1298
+ function enterOf(enter, warn) {
1299
+ if (!enter) return void 0;
1300
+ const kept = {};
1301
+ for (const [key, value] of Object.entries(enter)) if (ENTER.has(key)) kept[key] = value;
1302
+ else if (key !== "opacity") warn(key);
1303
+ return kept;
1304
+ }
1305
+ /** Decides how `content` replaces the skeleton in `root`, now that it has arrived. */
1306
+ function planReveal(content, root, reveal, warn = () => {}) {
1307
+ const info = loading.get(root);
1308
+ loading.delete(root);
1309
+ let spec = reveal;
1310
+ while (spec.choose) {
1311
+ const { choose, ...rest } = spec;
1312
+ spec = {
1313
+ ...rest,
1314
+ ...choose({
1315
+ content,
1316
+ reveal: root,
1317
+ waited: info ? performance.now() - info.since : Infinity,
1318
+ input: performance.now() - lastInput < ACTIVE_WINDOW,
1319
+ refetch: !!info?.refetch,
1320
+ height: content.getBoundingClientRect().height,
1321
+ pieces: pieces(content).length
1322
+ })
1323
+ };
1324
+ }
1325
+ const duration = clamp(spec.duration ?? 200, MAX_DURATION);
1326
+ spec = {
1327
+ ...spec,
1328
+ delay: Math.min(spec.delay ?? 0, MAX_DELAY)
1329
+ };
1330
+ if (!duration) return { kind: "instant" };
1331
+ const enter = enterOf(spec.enter, warn);
1332
+ if (spec.wipe) {
1333
+ const rect = revealBox(root);
1334
+ if (rect.width * rect.height <= innerWidth * innerHeight * 1.5) return {
1335
+ kind: "wipe",
1336
+ wipe: spec.wipe,
1337
+ start: performance.now() + (spec.delay ?? 0),
1338
+ duration,
1339
+ reveal: rect,
1340
+ enter
1341
+ };
1342
+ } else if (typeof spec.order === "function") return sequencePlan(content, root, spec.order, {
1343
+ ...spec,
1344
+ enter
1345
+ }, duration);
1346
+ return {
1347
+ kind: "together",
1348
+ delay: Math.max(spec.delay ?? 0, 0),
1349
+ duration,
1350
+ enter
1351
+ };
1352
+ }
1353
+ /** `sequence`: each piece timed by its place in the reveal, top to bottom, then a little left to right. */
1354
+ const byPosition = ({ rect, reveal }) => {
1355
+ const down = (rect.top - reveal.top) / Math.max(reveal.height, 1);
1356
+ const across = (rect.left - reveal.left) / Math.max(reveal.width, 1);
1357
+ return clamp(down * .8 + across * .2, 1) * 320;
1358
+ };
1359
+ const onScreen = (rect) => rect.bottom > 0 && rect.top < innerHeight;
1360
+ /** `smart`'s order: rows arrive whole, in page order, on screen only, main text first. */
1361
+ const byRow = ({ rect, unit, secondary, late }) => {
1362
+ if (!unit) return onScreen(rect) ? 0 : null;
1363
+ if (!onScreen(unit)) return null;
1364
+ const at = clamp(unit.top / innerHeight, 1) * 320;
1365
+ return Math.max(at - late, 0) + (secondary ? SECONDARY_DELAY : 0);
1366
+ };
1367
+ const instant = { duration: 0 };
1368
+ const together = { order: "together" };
1369
+ const sequence = { order: byPosition };
1370
+ /** The cases `smart` settles before looking at the content: instant, or a quick crossfade. */
1371
+ const quickly = (context) => context.input || context.waited < 150 ? instant : context.refetch ? {
1372
+ order: "together",
1373
+ duration: QUICK_FADE
1374
+ } : null;
1375
+ /** `together` for a small reveal, else `byRow`. */
1376
+ const byContent = (context) => context.height < SMALL_HEIGHT || context.pieces <= SMALL_PIECES ? together : { order: byRow };
1377
+ /** Picks for each reveal from how it loaded and what it holds, holding its skeleton back a moment. */
1378
+ const smart = {
1379
+ hold: 150,
1380
+ choose: (context) => quickly(context) ?? byContent(context)
1381
+ };
1382
+ //#endregion
1383
+ //#region src/core/shapes.ts
1384
+ const STORAGE_KEY = "a-smart-skeleton:shapes";
1385
+ const VERSION = 2;
1386
+ const MAX_SHAPES = 50;
1387
+ const MAX_LENGTH = 2e4;
1388
+ const session = /* @__PURE__ */ new Map();
1389
+ const shown = /* @__PURE__ */ new Map();
1390
+ /** Registers a save for content on screen under `key`; returns its unregister. */
1391
+ function trackShape(key, save) {
1392
+ if (!shown.has(key)) shown.set(key, /* @__PURE__ */ new Set());
1393
+ shown.get(key).add(save);
1394
+ return () => shown.get(key)?.delete(save);
1395
+ }
1396
+ /** Saves every shown copy of `key` now, so a skeleton about to recall it gets the latest. */
1397
+ function flushShapes(key) {
1398
+ for (const save of shown.get(key) ?? []) save();
1399
+ }
1400
+ function read() {
1401
+ try {
1402
+ const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "null");
1403
+ if (!raw || typeof raw !== "object" || raw.v !== VERSION) return Object.create(null);
1404
+ const incoming = raw.shapes;
1405
+ if (!incoming || typeof incoming !== "object") return Object.create(null);
1406
+ const out = Object.create(null);
1407
+ for (const key of Object.keys(incoming)) {
1408
+ const value = incoming[key];
1409
+ if (typeof value === "string") out[key] = value;
1410
+ }
1411
+ return out;
1412
+ } catch {
1413
+ return Object.create(null);
1414
+ }
1415
+ }
1416
+ function write(shapes) {
1417
+ try {
1418
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
1419
+ v: VERSION,
1420
+ shapes
1421
+ }));
1422
+ } catch {}
1423
+ }
1424
+ function rememberShape(key, element) {
1425
+ session.set(key, serialize(element, false));
1426
+ const html = serialize(element, true);
1427
+ if (html.length > MAX_LENGTH) return;
1428
+ const shapes = read();
1429
+ delete shapes[key];
1430
+ shapes[key] = html;
1431
+ const keys = Object.keys(shapes);
1432
+ for (const stale of keys.slice(0, keys.length - MAX_SHAPES)) delete shapes[stale];
1433
+ write(shapes);
1434
+ }
1435
+ function recallShape(key) {
1436
+ return session.get(key) ?? read()[key] ?? null;
1437
+ }
1438
+ /** Forgets remembered shapes — call it on sign-out. */
1439
+ function forgetShapes({ persisted = true } = {}) {
1440
+ session.clear();
1441
+ if (!persisted) return;
1442
+ try {
1443
+ localStorage.removeItem(STORAGE_KEY);
1444
+ } catch {}
1445
+ }
1446
+ //#endregion
1447
+ //#region src/core/ghost.ts
1448
+ const FALLBACK = [
1449
+ 100,
1450
+ 90,
1451
+ 60
1452
+ ].map((width) => `<div ${ATTR_BLOCK} style="width:${width}%;height:0.75em;margin:0.5em 0;border-radius:0.25em"></div>`).join("");
1453
+ const served = new WeakSet(typeof document === "undefined" ? [] : document.querySelectorAll(`[${ATTR_LOADING}]`));
1454
+ const loadingSizes = /* @__PURE__ */ new WeakMap();
1455
+ /** The size a reveal's skeleton last measured at, taken once by whatever replaces it. */
1456
+ function takeLoadingSize(reveal) {
1457
+ if (!reveal) return void 0;
1458
+ const size = loadingSizes.get(reveal);
1459
+ loadingSizes.delete(reveal);
1460
+ return size;
1461
+ }
1462
+ function child(root, attribute) {
1463
+ for (const element of root.children) if (element.hasAttribute(attribute)) return element;
1464
+ const element = document.createElement("div");
1465
+ element.setAttribute(attribute, "");
1466
+ root.append(element);
1467
+ return element;
1468
+ }
1469
+ const hasContent = (element) => !!element.firstElementChild || !!element.textContent?.trim();
1470
+ /** Draws a skeleton over content laid out unseen in `root`, so it takes exactly the content's space. */
1471
+ var Ghost = class {
1472
+ root;
1473
+ options;
1474
+ ghost;
1475
+ snapshot;
1476
+ layer;
1477
+ source = null;
1478
+ hadContent = false;
1479
+ frame = 0;
1480
+ mutations;
1481
+ resize;
1482
+ constructor(root, options = {}) {
1483
+ this.root = root;
1484
+ this.options = options;
1485
+ addStyles();
1486
+ root[GHOSTED] = true;
1487
+ root.setAttribute(ATTR_LOADING, "");
1488
+ root.setAttribute("aria-hidden", "true");
1489
+ root.inert = true;
1490
+ this.ghost = child(root, ATTR_GHOST);
1491
+ this.snapshot = child(root, ATTR_SNAPSHOT);
1492
+ this.layer = child(root, ATTR_LAYER);
1493
+ this.hadContent = hasContent(this.ghost);
1494
+ this.note();
1495
+ this.choose();
1496
+ this.measure();
1497
+ root[UNCOVER]?.();
1498
+ this.mutations = new MutationObserver(this.schedule);
1499
+ this.mutations.observe(this.ghost, {
1500
+ childList: true,
1501
+ subtree: true,
1502
+ characterData: true,
1503
+ attributes: true
1504
+ });
1505
+ this.resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(this.schedule);
1506
+ this.resize?.observe(root);
1507
+ root.addEventListener("load", this.schedule, true);
1508
+ addEventListener("resize", this.schedule);
1509
+ document.fonts?.ready.then(this.schedule);
1510
+ }
1511
+ /** Swaps in fresh options without tearing down the observers or stylesheet refcount. */
1512
+ update(options = {}) {
1513
+ this.options = options;
1514
+ this.note();
1515
+ this.choose();
1516
+ this.measure();
1517
+ }
1518
+ note() {
1519
+ const reveal = this.root.closest(`[${ATTR_REVEAL}]`);
1520
+ if (!reveal) return;
1521
+ noteSkeleton(reveal, {
1522
+ served: served.has(this.root),
1523
+ refetch: !!this.options.refetch && this.hadContent
1524
+ });
1525
+ }
1526
+ schedule = () => {
1527
+ if (this.root.closest(`[ask-exiting]`)) return;
1528
+ cancelAnimationFrame(this.frame);
1529
+ this.frame = requestAnimationFrame(() => {
1530
+ const has = hasContent(this.ghost);
1531
+ if (has !== this.hadContent) {
1532
+ this.hadContent = has;
1533
+ this.choose();
1534
+ }
1535
+ this.measure();
1536
+ });
1537
+ };
1538
+ choose() {
1539
+ let next = "fallback";
1540
+ let html = null;
1541
+ const settled = this.options.exact || served.has(this.root);
1542
+ const own = !!this.ghost.querySelector(`[${ATTR_OWN}]`);
1543
+ if ((settled || own) && this.hadContent) next = "ghost";
1544
+ else {
1545
+ const key = this.options.id ?? autoKey(this.ghost);
1546
+ if (key) {
1547
+ flushShapes(key);
1548
+ html = recallShape(key);
1549
+ }
1550
+ if (html) next = "recall";
1551
+ else if (this.hadContent) next = "ghost";
1552
+ }
1553
+ if (next !== this.source) if (next === "recall" && html) this.snapshot.replaceChildren(restore$1(html));
1554
+ else if (next === "fallback") {
1555
+ this.snapshot.innerHTML = FALLBACK;
1556
+ warnOnce(`fallback:${this.options.id}`, `Nothing to draw a skeleton from${this.options.id ? ` for "${this.options.id}"` : ""}. Pass \`sample\` data, or an \`id\` so its shape is remembered after the first load. For a lone placeholder box, use \`SkeletonBox\`.`);
1557
+ } else this.snapshot.replaceChildren();
1558
+ this.ghost.toggleAttribute(ATTR_INACTIVE, next !== "ghost");
1559
+ this.snapshot.toggleAttribute(ATTR_INACTIVE, next === "ghost");
1560
+ this.source = next;
1561
+ reportLoading(this.root.closest(`[${ATTR_REVEAL}]`), next === "recall" ? "remembered shape" : next === "fallback" ? "fallback" : this.options.exact ? "data" : served.has(this.root) ? "server render" : "sample");
1562
+ }
1563
+ measure() {
1564
+ const active = this.source === "ghost" ? this.ghost : this.snapshot;
1565
+ drawSkeleton(this.layer, measureSkeleton(active, this.layer));
1566
+ const reveal = this.root.closest(`[${ATTR_REVEAL}]`);
1567
+ if (reveal) loadingSizes.set(reveal, {
1568
+ width: this.root.offsetWidth,
1569
+ height: this.root.offsetHeight,
1570
+ guessed: !(this.options.exact && this.source === "ghost"),
1571
+ images: [...active.querySelectorAll("img")].map((image) => image.getBoundingClientRect().height)
1572
+ });
1573
+ }
1574
+ destroy() {
1575
+ cancelAnimationFrame(this.frame);
1576
+ this.mutations.disconnect();
1577
+ this.resize?.disconnect();
1578
+ this.root.removeEventListener("load", this.schedule, true);
1579
+ removeEventListener("resize", this.schedule);
1580
+ removeStyles();
1581
+ }
1582
+ };
1583
+ //#endregion
1584
+ //#region src/core/memory.ts
1585
+ const SAVE_DELAY = 250;
1586
+ const anonIds = /* @__PURE__ */ new WeakMap();
1587
+ let anonSeq = 0;
1588
+ const anonId = (element) => {
1589
+ let id = anonIds.get(element);
1590
+ if (!id) {
1591
+ id = `anon:${++anonSeq}`;
1592
+ anonIds.set(element, id);
1593
+ }
1594
+ return id;
1595
+ };
1596
+ /** Remembers the shape of shown content, so its next skeleton can be drawn from it. */
1597
+ var Memory = class {
1598
+ element;
1599
+ options;
1600
+ key;
1601
+ warnKey;
1602
+ timer;
1603
+ mutations;
1604
+ resize;
1605
+ untrack;
1606
+ constructor(element, options = {}) {
1607
+ this.element = element;
1608
+ this.options = options;
1609
+ this.key = options.id ?? autoKey(element);
1610
+ this.warnKey = this.key ?? anonId(element);
1611
+ this.untrack = this.key ? trackShape(this.key, () => this.save()) : () => {};
1612
+ if (isDev || debugging(element)) this.check();
1613
+ this.save();
1614
+ this.mutations = new MutationObserver(this.schedule);
1615
+ this.mutations.observe(element, {
1616
+ childList: true,
1617
+ subtree: true,
1618
+ characterData: true
1619
+ });
1620
+ this.resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(this.schedule);
1621
+ this.resize?.observe(element);
1622
+ element.addEventListener("load", this.schedule, true);
1623
+ }
1624
+ schedule = () => {
1625
+ clearTimeout(this.timer);
1626
+ this.timer = setTimeout(() => this.save(), SAVE_DELAY);
1627
+ };
1628
+ save() {
1629
+ clearTimeout(this.timer);
1630
+ if (this.key && this.element.isConnected && this.element.offsetHeight) rememberShape(this.key, this.element);
1631
+ }
1632
+ check() {
1633
+ const { data, id, rendered, sample } = this.options;
1634
+ const name = id ? `<Skeleton id="${id}">` : "<Skeleton>";
1635
+ const reveal = this.element.closest(`[${ATTR_REVEAL}]`);
1636
+ const before = takeLoadingSize(reveal);
1637
+ if (!before) return;
1638
+ if (before.guessed && rendered && !sample && data != null) warnOnce(`sample:${this.warnKey}`, `${name} has no \`sample\`, so a first load guesses its shape${id ? "" : " and it can't be remembered"}. Pass this to make it exact:\n sample={${JSON.stringify(sampleFrom(data))}}`);
1639
+ const images = [...this.element.querySelectorAll("img")].filter((image) => !image.complete);
1640
+ Promise.all(images.map((image) => new Promise((resolve) => {
1641
+ image.addEventListener("load", resolve, { once: true });
1642
+ image.addEventListener("error", resolve, { once: true });
1643
+ }))).then(() => {
1644
+ if (!this.element.isConnected) return;
1645
+ const width = this.element.offsetWidth;
1646
+ const height = this.element.offsetHeight;
1647
+ reportReveal(reveal, {
1648
+ width: width - before.width,
1649
+ height: height - before.height
1650
+ });
1651
+ if (Math.abs(width - before.width) <= 1 && Math.abs(height - before.height) <= 1) return;
1652
+ const unsized = [...this.element.querySelectorAll("img")].filter((image, i) => (before.images[i] ?? 0) < 1 && image.getBoundingClientRect().height >= 1).map((image) => image.currentSrc.split("/").pop() || "an image");
1653
+ if (unsized.length) {
1654
+ warnOnce(`unsized:${this.warnKey}`, `${name}'s skeleton had no room for ${unsized.length === 1 ? "an image" : `${unsized.length} images`} (${unsized.slice(0, 3).join(", ")}${unsized.length > 3 ? ", …" : ""}), so the layout jumped when they loaded. Give each \`width\` and \`height\` attributes, or an \`aspect-ratio\`, so its space is known before it loads.`);
1655
+ return;
1656
+ }
1657
+ warnOnce(`jump:${this.warnKey}`, before.guessed ? `${name}'s skeleton was ${before.width}×${before.height}px but its content is ${width}×${height}px, so the layout jumped on reveal. ${rendered ? "Pass `sample` data shaped like the real thing" : "Pass an `id` so its shape is remembered"}, and give images a size.` : `${name}'s skeleton was drawn from the data on screen (${before.width}×${before.height}px), but the new data is ${width}×${height}px, so the layout jumped on reveal. When new data adds to what's shown — another page of a list — give the addition a reveal of its own.`);
1658
+ });
1659
+ }
1660
+ destroy() {
1661
+ this.save();
1662
+ clearTimeout(this.timer);
1663
+ this.mutations.disconnect();
1664
+ this.resize?.disconnect();
1665
+ this.element.removeEventListener("load", this.schedule, true);
1666
+ this.untrack();
1667
+ }
1668
+ };
1669
+ //#endregion
1670
+ //#region src/core/skeleton.ts
1671
+ const DEFAULT_SKELETON_BOX_OPTIONS = {
1672
+ height: "1em",
1673
+ width: "60%",
1674
+ radius: "var(--ask-radius, 0.25em)"
1675
+ };
1676
+ const toCSS = (value) => typeof value === "number" ? `${value}px` : value;
1677
+ function skeletonBoxStyle({ height = DEFAULT_SKELETON_BOX_OPTIONS.height, width = DEFAULT_SKELETON_BOX_OPTIONS.width, radius = DEFAULT_SKELETON_BOX_OPTIONS.radius } = {}) {
1678
+ return {
1679
+ height: toCSS(height),
1680
+ width: toCSS(width),
1681
+ borderRadius: toCSS(radius)
1682
+ };
1683
+ }
1684
+ /** A single bar, shimmering in step with every skeleton on the page once it is in the document. */
1685
+ function createSkeletonBox(options) {
1686
+ addStyles();
1687
+ const element = document.createElement("span");
1688
+ element.setAttribute(ATTR_BAR, "");
1689
+ Object.assign(element.style, skeletonBoxStyle(options));
1690
+ requestAnimationFrame(() => {
1691
+ if (element.isConnected) shimmer(element);
1692
+ });
1693
+ return element;
1694
+ }
1695
+ //#endregion
1696
+ //#region src/core/images.ts
1697
+ const noop$1 = () => {};
1698
+ const deferred = (image) => {
1699
+ if (image.loading !== "lazy") return false;
1700
+ const rect = image.getBoundingClientRect();
1701
+ return rect.bottom < -innerHeight || rect.top > innerHeight * 2;
1702
+ };
1703
+ /** Images inside still on their way. */
1704
+ const pendingImages = (element) => [...element.querySelectorAll("img")].filter((image) => !image.complete && !deferred(image));
1705
+ /** Loaded and decoded, so its first frame on screen is the picture; failing counts, as nothing more is coming. */
1706
+ function imageReady(image) {
1707
+ const loaded = image.complete ? Promise.resolve() : new Promise((resolve) => {
1708
+ image.addEventListener("load", () => resolve(), { once: true });
1709
+ image.addEventListener("error", () => resolve(), { once: true });
1710
+ });
1711
+ return Promise.race([loaded.then(() => image.decode?.()).then(noop$1, noop$1), new Promise((resolve) => setTimeout(resolve, IMAGE_TIMEOUT))]);
1712
+ }
1713
+ //#endregion
1714
+ //#region src/core/sweep.ts
1715
+ const WAVE_SPREAD = 360;
1716
+ const WAVE_PACE = 2;
1717
+ const FEATHER = 40;
1718
+ const PAD = FEATHER / SLANT_COS;
1719
+ const GLOSS = 320;
1720
+ const GLOSS_LAG = 120;
1721
+ const GLOSS_LIFT = .45;
1722
+ const uOf = (rect) => rect.left + rect.top * SLANT_TAN;
1723
+ const spanOf = (rect) => rect.width + rect.height * SLANT_TAN;
1724
+ const frontAt = (progress, reveal) => uOf(reveal) - PAD + (spanOf(reveal) + 2 * PAD) * progress;
1725
+ let wave = {
1726
+ start: -Infinity,
1727
+ origin: 0,
1728
+ speed: 1
1729
+ };
1730
+ /** When the shared front reaches `reveal`, and how long it takes across — null without a shimmer to match. */
1731
+ function timing(reveal) {
1732
+ const layer = reveal.querySelector(`:scope > [${ATTR_EXITING}] [${ATTR_LAYER}]`);
1733
+ const pace = layer && paceOf(layer);
1734
+ if (!pace) return null;
1735
+ const rect = revealBox(reveal);
1736
+ if (rect.width * rect.height > innerWidth * innerHeight * 1.5) return null;
1737
+ const from = uOf(rect);
1738
+ const to = from + spanOf(rect);
1739
+ const now = performance.now();
1740
+ const elapsed = now - wave.start;
1741
+ if (!(elapsed <= 150 && (from - wave.origin) / wave.speed >= elapsed && (to - wave.origin) / wave.speed - elapsed <= 600)) wave = {
1742
+ start: now,
1743
+ origin: from,
1744
+ speed: Math.max(WAVE_PACE / pace, (to - from) / WAVE_SPREAD)
1745
+ };
1746
+ const { start, origin, speed } = wave;
1747
+ return {
1748
+ delay: (from - PAD - origin) / speed - (now - start),
1749
+ duration: (to - from + 2 * PAD) / speed
1750
+ };
1751
+ }
1752
+ const stops = (incoming) => (progress, { rect, reveal }) => {
1753
+ const at = (frontAt(progress, reveal) - uOf(rect)) * SLANT_COS;
1754
+ return `linear-gradient(${SLANT_ANGLE}, ${Array.from({ length: 13 }, (_, i) => {
1755
+ const shown = incoming ? 1 - ease(i / 12) : ease(i / 12);
1756
+ const offset = -40 + 2 * FEATHER * i / 12;
1757
+ return `rgb(0 0 0 / ${shown.toFixed(3)}) ${(at + offset).toFixed(2)}px`;
1758
+ }).join(", ")})`;
1759
+ };
1760
+ const SVG = "http://www.w3.org/2000/svg";
1761
+ let host = null;
1762
+ let glosses = 0;
1763
+ function filterHost() {
1764
+ if (!host?.isConnected) {
1765
+ host = document.createElementNS(SVG, "svg");
1766
+ host.setAttribute("aria-hidden", "true");
1767
+ host.style.cssText = "position:absolute;width:0;height:0;overflow:hidden";
1768
+ document.body.append(host);
1769
+ }
1770
+ return host;
1771
+ }
1772
+ const svg = (tag, attributes) => {
1773
+ const node = document.createElementNS(SVG, tag);
1774
+ for (const [name, value] of Object.entries(attributes)) node.setAttribute(name, String(value));
1775
+ return node;
1776
+ };
1777
+ const gloss = (element, { rect, reveal, progress }) => {
1778
+ if (prefersReducedTransparency()) return;
1779
+ const from = uOf(rect);
1780
+ const span = spanOf(rect);
1781
+ const half = GLOSS / 2 / SLANT_COS;
1782
+ const centre = () => frontAt(progress(), reveal) - from - GLOSS_LAG / SLANT_COS;
1783
+ if (centre() - half > span) return;
1784
+ const width = 2 * half + rect.height * SLANT_TAN;
1785
+ const cx = width / 2;
1786
+ const cy = rect.height / 2;
1787
+ const dx = GLOSS / 2 * SLANT_COS;
1788
+ const dy = GLOSS / 2 * SLANT_TAN * SLANT_COS;
1789
+ const canvas = document.createElement("canvas");
1790
+ canvas.width = Math.ceil(width);
1791
+ canvas.height = Math.ceil(rect.height);
1792
+ const context = canvas.getContext("2d");
1793
+ if (!context) return;
1794
+ const gradient = context.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy);
1795
+ for (let i = 0; i <= 12; i++) {
1796
+ const t = i / 12;
1797
+ gradient.addColorStop(t, `rgb(255 255 255 / ${ease(1 - Math.abs(t * 2 - 1)).toFixed(3)})`);
1798
+ }
1799
+ context.fillStyle = gradient;
1800
+ context.fillRect(0, 0, canvas.width, canvas.height);
1801
+ const id = `ask-gloss-${++glosses}`;
1802
+ const filter = svg("filter", {
1803
+ id,
1804
+ "color-interpolation-filters": "sRGB",
1805
+ primitiveUnits: "objectBoundingBox"
1806
+ });
1807
+ const lit = svg("feComponentTransfer", {
1808
+ in: "SourceGraphic",
1809
+ result: "lit"
1810
+ });
1811
+ for (const channel of [
1812
+ "R",
1813
+ "G",
1814
+ "B"
1815
+ ]) lit.append(svg(`feFunc${channel}`, {
1816
+ type: "linear",
1817
+ slope: 1 - GLOSS_LIFT,
1818
+ intercept: GLOSS_LIFT
1819
+ }));
1820
+ const image = svg("feImage", {
1821
+ href: canvas.toDataURL(),
1822
+ y: 0,
1823
+ width: width / rect.width,
1824
+ height: 1,
1825
+ preserveAspectRatio: "none",
1826
+ result: "band"
1827
+ });
1828
+ filter.append(lit, image, svg("feComposite", {
1829
+ in: "lit",
1830
+ in2: "band",
1831
+ operator: "in",
1832
+ result: "shine"
1833
+ }), svg("feComposite", {
1834
+ in: "shine",
1835
+ in2: "SourceGraphic",
1836
+ operator: "over"
1837
+ }));
1838
+ filterHost().append(filter);
1839
+ element.style.filter = `url(#${id})`;
1840
+ const tick = () => {
1841
+ const at = centre();
1842
+ if (at - half > span || !element.isConnected) {
1843
+ if (element.style.filter.includes(id)) element.style.filter = "";
1844
+ filter.remove();
1845
+ return;
1846
+ }
1847
+ image.setAttribute("x", ((at - cx - cy * SLANT_TAN) / rect.width).toFixed(4));
1848
+ requestAnimationFrame(tick);
1849
+ };
1850
+ tick();
1851
+ };
1852
+ const wipe$1 = {
1853
+ mask: stops(true),
1854
+ out: stops(false),
1855
+ effect: gloss
1856
+ };
1857
+ /** `smart`, wiping content in behind one band of the shimmer. */
1858
+ const sweep = {
1859
+ hold: 150,
1860
+ choose: (context) => {
1861
+ const quick = quickly(context);
1862
+ if (quick) return quick;
1863
+ const swept = timing(context.reveal);
1864
+ return swept ? {
1865
+ wipe: wipe$1,
1866
+ ...swept
1867
+ } : byContent(context);
1868
+ }
1869
+ };
1870
+ //#endregion
1871
+ //#region src/core/reveals.ts
1872
+ const NAMED = {
1873
+ together,
1874
+ sequence,
1875
+ smart,
1876
+ sweep,
1877
+ instant
1878
+ };
1879
+ const revealSpec = (mode = "smart") => {
1880
+ if (typeof mode !== "string") return mode;
1881
+ return NAMED[mode] ?? smart;
1882
+ };
1883
+ /** Longest a pane can take to leave under `mode`, for a framework that has to know up front. */
1884
+ function exitDuration(mode) {
1885
+ const spec = revealSpec(mode);
1886
+ const hold = spec.hold ?? 0;
1887
+ if (spec === together) return 200;
1888
+ if (spec === sequence) return 520;
1889
+ if (spec === smart) return hold + 320 + 200;
1890
+ if (spec === sweep) return hold + 600 + 200;
1891
+ if (spec.duration === 0) return 0;
1892
+ return hold + (spec.choose ? MAX_DELAY : Math.min(Math.max(spec.delay ?? 0, 0), MAX_DELAY)) + MAX_DELAY + MAX_DURATION;
1893
+ }
1894
+ //#endregion
1895
+ //#region src/core/fade.ts
1896
+ const noop = () => {};
1897
+ const settle = (animation) => animation.finished.then(noop, noop);
1898
+ const plans = /* @__PURE__ */ new WeakMap();
1899
+ function appear(element, delay = 0, duration = 200, from = 0, enter) {
1900
+ if (prefersReducedMotion()) return Promise.resolve();
1901
+ return settle(element.animate([{
1902
+ ...enter,
1903
+ opacity: from
1904
+ }, { opacity: 1 }], {
1905
+ duration,
1906
+ delay,
1907
+ easing: "ease",
1908
+ fill: "backwards"
1909
+ }));
1910
+ }
1911
+ const isSkeleton = (element) => !!element.querySelector(`[${ATTR_LAYER}], [${ATTR_GHOST}], :scope > [${ATTR_OWN}]`);
1912
+ const sameBox = (a, b) => Math.abs(a.left - b.left) < 2 && Math.abs(a.top - b.top) < 2 && Math.abs(a.width - b.width) < 2 && Math.abs(a.height - b.height) < 2;
1913
+ const standIns = /* @__PURE__ */ new WeakMap();
1914
+ function clearStandIns(root) {
1915
+ for (const stand of standIns.get(root) ?? []) stand.remove();
1916
+ standIns.delete(root);
1917
+ }
1918
+ function holdImages(element, root) {
1919
+ const images = pendingImages(element);
1920
+ const overSkeleton = [...root.children].some((pane) => pane !== element && pane.hasAttribute("ask-pane") && isSkeleton(pane));
1921
+ if (!images.length || !overSkeleton) return;
1922
+ for (const image of images) image.style.opacity = "0";
1923
+ queueMicrotask(() => {
1924
+ if (!element.isConnected) return;
1925
+ const origin = root.getBoundingClientRect();
1926
+ const bones = [...root.querySelectorAll(`:scope > [${ATTR_EXITING}] [${ATTR_LAYER}]`)].flatMap(rectsOf);
1927
+ const stands = standIns.get(root) ?? /* @__PURE__ */ new Set();
1928
+ standIns.set(root, stands);
1929
+ for (const image of images) {
1930
+ const rect = image.getBoundingClientRect();
1931
+ const bone = bones.find((b) => sameBox(b.getBoundingClientRect(), rect));
1932
+ const box = bone?.getBoundingClientRect() ?? rect;
1933
+ const stand = document.createElement("div");
1934
+ stand.setAttribute(ATTR_RECT, "media");
1935
+ Object.assign(stand.style, {
1936
+ position: "absolute",
1937
+ left: `${box.left - origin.left - root.clientLeft}px`,
1938
+ top: `${box.top - origin.top - root.clientTop}px`,
1939
+ width: `${box.width}px`,
1940
+ height: `${box.height}px`,
1941
+ borderRadius: bone?.style.borderRadius || getComputedStyle(image).borderRadius,
1942
+ backgroundRepeat: "no-repeat",
1943
+ pointerEvents: "none"
1944
+ });
1945
+ root.append(stand);
1946
+ stands.add(stand);
1947
+ if (bone) bone.style.visibility = "hidden";
1948
+ shimmer(stand);
1949
+ imageReady(image).then(() => {
1950
+ if (!stands.has(stand)) return;
1951
+ stands.delete(stand);
1952
+ image.style.opacity = "";
1953
+ appear(image);
1954
+ if (prefersReducedMotion()) return stand.remove();
1955
+ settle(stand.animate([{ opacity: 1 }, { opacity: 0 }], {
1956
+ duration: 200,
1957
+ easing: "ease",
1958
+ fill: "forwards"
1959
+ })).then(() => stand.remove());
1960
+ });
1961
+ }
1962
+ });
1963
+ }
1964
+ function sequenceIn(element, plan) {
1965
+ if (prefersReducedMotion()) return Promise.resolve();
1966
+ return Promise.all(pieces(element).map((piece) => {
1967
+ const delay = plan.pieceDelay(piece);
1968
+ if (delay === null) return;
1969
+ return appear(piece, delay, plan.duration, 0, plan.enter);
1970
+ })).then(noop);
1971
+ }
1972
+ function clearExiting(root) {
1973
+ for (const pane of root.querySelectorAll(`:scope > [${ATTR_EXITING}]`)) {
1974
+ for (const animation of animationsOf(pane)) animation.finish();
1975
+ pane.style.opacity = "0";
1976
+ }
1977
+ }
1978
+ function takeOver(root) {
1979
+ let from = 0;
1980
+ for (const pane of root.querySelectorAll(`:scope > [${ATTR_EXITING}]`)) {
1981
+ if (isSkeleton(pane)) continue;
1982
+ from = Math.max(from, parseFloat(getComputedStyle(pane).opacity) || 0);
1983
+ for (const animation of animationsOf(pane)) animation.finish();
1984
+ pane.style.opacity = "0";
1985
+ }
1986
+ return from;
1987
+ }
1988
+ /** Brings a pane in: a skeleton after the reveal's `hold`, content as `reveal` plans; `initial` has nothing to replace. */
1989
+ function fadeIn(element, reveal = "smart", { initial = false } = {}) {
1990
+ let arriving = arrivals.get(element);
1991
+ if (!arriving) {
1992
+ arriving = arrive(element, reveal, initial);
1993
+ arrivals.set(element, arriving);
1994
+ }
1995
+ return arriving;
1996
+ }
1997
+ const arrivals = /* @__PURE__ */ new WeakMap();
1998
+ function arrive(element, reveal, initial) {
1999
+ const root = element.parentElement;
2000
+ const spec = revealSpec(reveal);
2001
+ if (isSkeleton(element)) {
2002
+ if (root) clearStandIns(root);
2003
+ const hold = Math.max(spec.hold ?? 0, 0);
2004
+ if (!hold) {
2005
+ if (root) plans.delete(root);
2006
+ return initial ? Promise.resolve() : appear(element);
2007
+ }
2008
+ if (root) plans.set(root, {
2009
+ kind: "skeleton",
2010
+ hold
2011
+ });
2012
+ if (initial) return Promise.resolve();
2013
+ return appear(element, hold);
2014
+ }
2015
+ if (initial || !root) return Promise.resolve();
2016
+ const from = takeOver(root);
2017
+ if (from > .01) {
2018
+ const duration = 200 * (1 - from);
2019
+ plans.set(root, {
2020
+ kind: "together",
2021
+ delay: 0,
2022
+ duration
2023
+ });
2024
+ return appear(element, 0, duration, from);
2025
+ }
2026
+ holdImages(element, root);
2027
+ const plan = planReveal(element, root, spec, (key) => warnOnce(`enter:${key}`, `\`enter\` can't set \`${key}\`: only properties that change nothing's layout animate, so the reveal never moves the page.`));
2028
+ plans.set(root, plan);
2029
+ if (plan.kind === "instant") {
2030
+ clearExiting(root);
2031
+ return Promise.resolve();
2032
+ }
2033
+ if (plan.kind === "sequence") return sequenceIn(element, plan);
2034
+ if (plan.kind === "wipe") return wipe(element, plan, true);
2035
+ return appear(element, plan.delay, plan.duration, 0, plan.enter);
2036
+ }
2037
+ /** Measures `element` for `fadeOut`: before its replacement is inserted, or scroll anchoring moves the page. */
2038
+ function boxOf(element) {
2039
+ const rect = element.getBoundingClientRect();
2040
+ const parent = element.offsetParent ?? document.documentElement;
2041
+ const origin = parent.getBoundingClientRect();
2042
+ return {
2043
+ top: rect.top - origin.top - parent.clientTop,
2044
+ left: rect.left - origin.left - parent.clientLeft,
2045
+ width: rect.width,
2046
+ height: rect.height
2047
+ };
2048
+ }
2049
+ function popOut(element, box = boxOf(element)) {
2050
+ Object.assign(element.style, {
2051
+ position: "absolute",
2052
+ top: `${box.top}px`,
2053
+ left: `${box.left}px`,
2054
+ width: `${box.width}px`,
2055
+ height: `${box.height}px`,
2056
+ margin: "0",
2057
+ boxSizing: "border-box",
2058
+ pointerEvents: "none"
2059
+ });
2060
+ element.inert = true;
2061
+ element.setAttribute(ATTR_EXITING, "");
2062
+ return box;
2063
+ }
2064
+ const resizes = /* @__PURE__ */ new WeakMap();
2065
+ function resize(root, box) {
2066
+ const incoming = root.querySelector(`:scope > [${ATTR_PANE}]:not([${ATTR_EXITING}])`);
2067
+ if (!incoming) return;
2068
+ const running = resizes.get(root);
2069
+ const shown = root.getBoundingClientRect().height;
2070
+ running?.animation.cancel();
2071
+ const to = root.getBoundingClientRect().height;
2072
+ const from = running ? shown : to - incoming.getBoundingClientRect().height + box.height;
2073
+ if (Math.abs(from - to) < .5) {
2074
+ if (running) restore(root, running);
2075
+ return;
2076
+ }
2077
+ const { overflow, boxSizing } = running ?? root.style;
2078
+ Object.assign(root.style, {
2079
+ overflow: "clip",
2080
+ boxSizing: "border-box"
2081
+ });
2082
+ const animation = root.animate([{ height: `${from}px` }, { height: `${to}px` }], {
2083
+ duration: 280,
2084
+ easing: "cubic-bezier(0.2, 0, 0, 1)"
2085
+ });
2086
+ const current = {
2087
+ animation,
2088
+ overflow,
2089
+ boxSizing
2090
+ };
2091
+ resizes.set(root, current);
2092
+ settle(animation).then(() => {
2093
+ if (resizes.get(root) === current) restore(root, current);
2094
+ });
2095
+ }
2096
+ function restore(root, { overflow, boxSizing }) {
2097
+ resizes.delete(root);
2098
+ Object.assign(root.style, {
2099
+ overflow,
2100
+ boxSizing
2101
+ });
2102
+ }
2103
+ /** Resolves once `element` can be removed, leaving as the pane replacing it decides; its parent must be positioned. */
2104
+ function fadeOut(element, box) {
2105
+ const opacity = parseFloat(getComputedStyle(element).opacity);
2106
+ const from = Number.isNaN(opacity) ? 1 : opacity;
2107
+ for (const animation of animationsOf(element)) animation.cancel();
2108
+ clearMask(element);
2109
+ element.style.filter = "";
2110
+ const parent = element.parentElement;
2111
+ if (parent && !isSkeleton(element)) clearStandIns(parent);
2112
+ const stood = popOut(element, box);
2113
+ const fade = (delay, duration) => {
2114
+ if (!canAnimate()) {
2115
+ element.style.opacity = "0";
2116
+ return Promise.resolve();
2117
+ }
2118
+ return settle(element.animate([{ opacity: from }, { opacity: 0 }], {
2119
+ duration: duration * from,
2120
+ delay,
2121
+ easing: "ease",
2122
+ fill: "forwards"
2123
+ }));
2124
+ };
2125
+ return Promise.resolve().then(() => {
2126
+ const plan = parent && plans.get(parent);
2127
+ const instant = prefersReducedMotion() || plan?.kind === "instant";
2128
+ if (parent && !instant) resize(parent, stood);
2129
+ if (plan?.kind === "skeleton") return fade(plan.hold, 200);
2130
+ if (instant) {
2131
+ element.style.opacity = "0";
2132
+ return;
2133
+ }
2134
+ if (plan?.kind === "sequence" && isSkeleton(element)) return sequenceOut(element, plan);
2135
+ if (plan?.kind === "wipe" && isSkeleton(element)) return wipe(element, plan, false);
2136
+ if (plan?.kind === "together") return fade(plan.delay, plan.duration);
2137
+ return fade(0, 200);
2138
+ });
2139
+ }
2140
+ function sequenceOut(element, plan) {
2141
+ for (const ghost of element.querySelectorAll(`[${ATTR_GHOST}], [${ATTR_SNAPSHOT}]`)) ghost.style.visibility = "hidden";
2142
+ const own = [...element.querySelectorAll(`[${ATTR_GHOST}] [${ATTR_OWN}]`)].filter((part) => !part.parentElement?.closest(`[${ATTR_OWN}]`));
2143
+ for (const part of own) part.style.visibility = "visible";
2144
+ const bones = [...[...element.querySelectorAll(`[${ATTR_LAYER}]`)].flatMap(rectsOf), ...own];
2145
+ return Promise.all(bones.map((bone) => {
2146
+ const rect = bone.getBoundingClientRect();
2147
+ const go = (delay) => settle(bone.animate([{ opacity: 1 }, { opacity: 0 }], {
2148
+ duration: plan.duration,
2149
+ delay,
2150
+ easing: "ease",
2151
+ fill: "forwards"
2152
+ }));
2153
+ const delay = plan.boneDelay(rect);
2154
+ if (delay === null) {
2155
+ bone.style.opacity = "0";
2156
+ return;
2157
+ }
2158
+ return go(delay);
2159
+ })).then(noop);
2160
+ }
2161
+ const STALL = 100;
2162
+ const fronts = /* @__PURE__ */ new WeakMap();
2163
+ function setMask(element, image) {
2164
+ element.style.setProperty("mask-image", image);
2165
+ element.style.setProperty("-webkit-mask-image", image);
2166
+ }
2167
+ function setInverseMask(element, image) {
2168
+ setMask(element, `linear-gradient(#000 0 0), ${image}`);
2169
+ element.style.setProperty("-webkit-mask-composite", "xor");
2170
+ element.style.setProperty("mask-composite", "exclude");
2171
+ }
2172
+ function clearMask(element) {
2173
+ fronts.get(element)?.();
2174
+ for (const property of [
2175
+ "mask-image",
2176
+ "-webkit-mask-image",
2177
+ "mask-composite",
2178
+ "-webkit-mask-composite"
2179
+ ]) element.style.removeProperty(property);
2180
+ }
2181
+ function wipe(element, plan, incoming) {
2182
+ const pane = {
2183
+ rect: element.getBoundingClientRect(),
2184
+ reveal: plan.reveal
2185
+ };
2186
+ const { mask, out } = plan.wipe;
2187
+ const draw = (at) => {
2188
+ if (incoming) setMask(element, mask(at, pane));
2189
+ else if (out) setMask(element, out(at, pane));
2190
+ else setInverseMask(element, mask(at, pane));
2191
+ };
2192
+ const progress = () => (performance.now() - plan.start) / plan.duration;
2193
+ if (progress() >= 1) {
2194
+ if (!incoming) element.style.opacity = "0";
2195
+ return Promise.resolve();
2196
+ }
2197
+ if (incoming) {
2198
+ plan.wipe.effect?.(element, {
2199
+ ...pane,
2200
+ progress
2201
+ });
2202
+ if (plan.enter) element.animate([plan.enter, {}], {
2203
+ duration: plan.duration,
2204
+ delay: Math.max(plan.start - performance.now(), 0),
2205
+ easing: "ease",
2206
+ fill: "backwards"
2207
+ });
2208
+ }
2209
+ return new Promise((resolve) => {
2210
+ let frame = 0;
2211
+ let last = performance.now();
2212
+ let at = 0;
2213
+ const tick = () => {
2214
+ const now = performance.now();
2215
+ if (now - last > STALL) plan.stalled = true;
2216
+ last = now;
2217
+ if (plan.stalled) return done(at);
2218
+ at = Math.min(Math.max(progress(), 0), 1);
2219
+ draw(at);
2220
+ if (at < 1) frame = requestAnimationFrame(tick);
2221
+ else done(null);
2222
+ };
2223
+ const done = (stalledAt = null) => {
2224
+ cancelAnimationFrame(frame);
2225
+ clearTimeout(deadline);
2226
+ fronts.delete(element);
2227
+ resolve(stalledAt);
2228
+ };
2229
+ const deadline = setTimeout(() => done(null), Math.max(plan.start + plan.duration - performance.now(), 0) + STALL);
2230
+ fronts.set(element, done);
2231
+ tick();
2232
+ }).then((stalledAt) => {
2233
+ clearMask(element);
2234
+ if (stalledAt === null) {
2235
+ if (!incoming) element.style.opacity = "0";
2236
+ return;
2237
+ }
2238
+ const rest = 200 * (1 - stalledAt);
2239
+ if (incoming) return appear(element, 0, rest, stalledAt);
2240
+ return settle(element.animate([{ opacity: 1 - stalledAt }, { opacity: 0 }], {
2241
+ duration: rest,
2242
+ easing: "ease",
2243
+ fill: "forwards"
2244
+ })).then(() => {
2245
+ element.style.opacity = "0";
2246
+ });
2247
+ });
2248
+ }
2249
+ //#endregion
2250
+ //#region src/core/reveal.ts
2251
+ /** Shows a skeleton of content while it loads, then crossfades to the content itself. */
2252
+ var Reveal = class {
2253
+ element;
2254
+ options;
2255
+ current = null;
2256
+ ghost = null;
2257
+ memory = null;
2258
+ arrival = Promise.resolve();
2259
+ constructor(element, options = {}) {
2260
+ this.element = element;
2261
+ this.options = options;
2262
+ addStyles();
2263
+ element.setAttribute(ATTR_REVEAL, "");
2264
+ }
2265
+ /** Shows a skeleton measured from `content`, rendered from sample data, or else the shape last shown. */
2266
+ load(content) {
2267
+ const loading = document.createElement("div");
2268
+ const ghost = document.createElement("div");
2269
+ ghost.setAttribute(ATTR_GHOST, "");
2270
+ if (content) ghost.append(content);
2271
+ loading.append(ghost);
2272
+ const pane = this.swap(loading);
2273
+ if (this.options.label) {
2274
+ const status = document.createElement("span");
2275
+ status.setAttribute(ATTR_STATUS, "");
2276
+ status.textContent = this.options.label;
2277
+ pane.prepend(status);
2278
+ }
2279
+ this.element.setAttribute("aria-busy", "true");
2280
+ this.ghost = new Ghost(loading, { id: this.options.id });
2281
+ return pane;
2282
+ }
2283
+ show(content) {
2284
+ const loading = this.element.getAttribute("aria-busy") === "true";
2285
+ const pane = this.swap(content);
2286
+ if (loading && this.options.onReveal) this.arrival.then(this.options.onReveal);
2287
+ this.element.removeAttribute("aria-busy");
2288
+ this.memory = new Memory(pane, { id: this.options.id });
2289
+ return pane;
2290
+ }
2291
+ swap(content) {
2292
+ this.memory?.destroy();
2293
+ this.ghost?.destroy();
2294
+ this.memory = this.ghost = null;
2295
+ const pane = document.createElement("div");
2296
+ pane.setAttribute(ATTR_PANE, "");
2297
+ pane.append(content);
2298
+ const previous = this.current;
2299
+ this.current = pane;
2300
+ if (previous) fadeOut(previous).then(() => previous.remove());
2301
+ this.element.append(pane);
2302
+ this.arrival = fadeIn(pane, this.options.reveal, { initial: !previous });
2303
+ return pane;
2304
+ }
2305
+ destroy() {
2306
+ this.memory?.destroy();
2307
+ this.ghost?.destroy();
2308
+ removeStyles();
2309
+ }
2310
+ };
2311
+ //#endregion
2312
+ export { ATTR_LOADING as $, DEFAULT_TEXT as A, themeVariables as B, smart as C, rectsOf as D, motionOf as E, DEFAULT_COLORS as F, addStyles as G, INLINE_STUB as H, DEFAULT_THEME as I, ATTR_BLOCK as J, removeStyles as K, applyTheme as L, checkNonce as M, debugging as N, shimmer as O, setDebug as P, ATTR_LAYER as Q, skeletonCSS as R, sequence as S, drawSkeleton as T, inlineScriptTag as U, INLINE_SCRIPT as V, SMART_SKELETON_CSS as W, ATTR_GHOST as X, ATTR_DEBUG as Y, ATTR_IGNORE as Z, byPosition as _, exitDuration as a, ATTR_STATUS as at, planReveal as b, pendingImages as c, DURATIONS as ct, skeletonBoxStyle as d, RESIZE_DURATION as dt, ATTR_OWN as et, Memory as f, byContent as g, SKELETON_DELAY as h, fadeOut as i, ATTR_SNAPSHOT as it, measureSkeleton as j, prefersReducedMotion as k, DEFAULT_SKELETON_BOX_OPTIONS as l, FADE_DURATION as lt, forgetShapes as m, boxOf as n, ATTR_RECT as nt, sweep as o, ATTR_XRAY as ot, Ghost as p, ATTR_BAR as q, fadeIn as r, ATTR_REVEAL as rt, imageReady as s, CASCADE_SPREAD as st, Reveal as t, ATTR_PANE as tt, createSkeletonBox as u, IMAGE_TIMEOUT as ut, byRow as v, together as w, quickly as x, instant as y, themeStyleTag as z };
2313
+
2314
+ //# sourceMappingURL=core.js.map