@lukas_holdings/castdom 1.0.2 → 1.0.4

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 (3) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +811 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  <img src="https://img.shields.io/badge/license-MIT-000?style=flat-square" alt="license" />
5
5
  <img src="https://img.shields.io/badge/runtime-CSS%20only-000?style=flat-square" alt="CSS only runtime" />
6
6
  <img src="https://img.shields.io/badge/bundle-~14KB-000?style=flat-square" alt="bundle size" />
7
+ <img src="https://img.shields.io/badge/AI--friendly-CASTDOM.md-blue?style=flat-square" alt="AI friendly" />
7
8
  </p>
8
9
 
9
10
  <h1 align="center">CastDOM</h1>
@@ -18,6 +19,7 @@
18
19
  </p>
19
20
 
20
21
  <p align="center">
22
+ <a href="https://kameeleonn.github.io/CastDOM/"><strong>Live Demo</strong></a> &bull;
21
23
  <a href="#quick-start">Quick Start</a> &bull;
22
24
  <a href="#how-it-works">How It Works</a> &bull;
23
25
  <a href="#react">React</a> &bull;
@@ -28,6 +30,10 @@
28
30
  <a href="#api-reference">API</a>
29
31
  </p>
30
32
 
33
+ <p align="center">
34
+ <sub>Ships with <a href="CASTDOM.md"><code>CASTDOM.md</code></a> — an AI context file for Claude, Gemini, GPT, Cursor, Copilot, and Windsurf.<br/>Ask any AI assistant to integrate CastDOM and it will know every API, type, and pattern.</sub>
35
+ </p>
36
+
31
37
  ---
32
38
 
33
39
  ## Why CastDOM?
package/dist/cli.js CHANGED
@@ -1,46 +1,577 @@
1
1
  #!/usr/bin/env node
2
- var V=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{resolve as S,join as ae}from"path";import{readFileSync as _,writeFileSync as H,mkdirSync as ce,existsSync as C}from"fs";var p={outDir:".castdom",breakpoints:[375,768,1280],color:"#e0e0e0",shimmerColor:"#f0f0f0",animationDuration:1500,contentAware:!0,minBoneSize:4,classPrefix:"castdom",inlineStyles:!1,ssr:!0},v={text:1,heading:2,image:3,avatar:4,button:5,input:6,icon:7,divider:8,block:0},W=Object.fromEntries(Object.entries(v).map(([e,t])=>[t,e]));var B=new Set(["A","ABBR","B","BDO","BR","CITE","CODE","DFN","EM","I","KBD","MARK","Q","S","SAMP","SMALL","SPAN","STRONG","SUB","SUP","TIME","U","VAR","WBR"]),E=new Set(["SCRIPT","STYLE","NOSCRIPT","TEMPLATE","SVG","CANVAS","VIDEO","AUDIO","IFRAME","OBJECT","EMBED"]);function T(e){let t=e.tagName;if(t==="IMG"||t==="PICTURE"){let c=e.getBoundingClientRect().width,r=e.getBoundingClientRect().height;return Math.min(c,r)/Math.max(c,r)>.85&&c<120?"avatar":"image"}if(t==="INPUT"||t==="TEXTAREA"||t==="SELECT")return"input";if(t==="BUTTON"||e.getAttribute("role")==="button")return"button";if(t==="HR")return"divider";if(/^H[1-6]$/.test(t))return"heading";if(t==="SVG"||t==="svg")return"icon";let n=getComputedStyle(e),o=parseInt(n.borderRadius,10),a=e.getBoundingClientRect();if(o>=a.width/2&&a.width<120&&a.width>16)return"avatar";if(e.childNodes.length>0){let c=Array.from(e.childNodes).some(r=>r.nodeType===Node.TEXT_NODE&&r.textContent?.trim());if(c&&B.has(t)||c&&t==="P")return"text"}return"block"}function A(e,t){if(t==="avatar")return 9999;if(t==="button"){let a=getComputedStyle(e);return Math.min(parseInt(a.borderRadius,10)||6,9999)}let n=getComputedStyle(e),o=parseInt(n.borderRadius,10);return isNaN(o)?0:Math.min(o,9999)}function N(e){let t=getComputedStyle(e);if(t.display==="none"||t.visibility==="hidden"||parseFloat(t.opacity)===0)return!1;let n=e.getBoundingClientRect();return n.width>0&&n.height>0}function R(e,t){let n=e.tagName;return!!(n==="IMG"||n==="PICTURE"||n==="INPUT"||n==="TEXTAREA"||n==="SELECT"||n==="BUTTON"||n==="HR"||n==="SVG"||n==="svg"||t&&Array.from(e.childNodes).every(a=>a.nodeType===Node.TEXT_NODE||B.has(a.tagName))&&e.textContent?.trim()||e.children.length===0)}function X(e,t={}){let n=t.contentAware??p.contentAware,o=t.minBoneSize??p.minBoneSize,a=e.getBoundingClientRect(),c=[];function r(i){if(!E.has(i.tagName)&&N(i)){if(R(i,n)){let l=i.getBoundingClientRect();if(l.width<o||l.height<o)return;let s=n?T(i):"block",u=A(i,s);c.push({x:Math.round((l.left-a.left)*2)/2,y:Math.round((l.top-a.top)*2)/2,w:Math.round(l.width*2)/2,h:Math.round(l.height*2)/2,r:u,kind:s});return}for(let l of i.children)r(l)}}return r(e),{viewport:window.innerWidth,containerWidth:Math.round(a.width*2)/2,containerHeight:Math.round(a.height*2)/2,bones:c}}function I(e,t={}){return`
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ // src/cli.ts
10
+ import { resolve, join } from "path";
11
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
12
+
13
+ // src/core/types.ts
14
+ var DEFAULTS = {
15
+ outDir: ".castdom",
16
+ breakpoints: [375, 768, 1280],
17
+ color: "#e0e0e0",
18
+ shimmerColor: "#f0f0f0",
19
+ animationDuration: 1500,
20
+ contentAware: true,
21
+ minBoneSize: 4,
22
+ classPrefix: "castdom",
23
+ inlineStyles: false,
24
+ ssr: true
25
+ };
26
+ var BONE_KIND_INDEX = {
27
+ text: 1,
28
+ heading: 2,
29
+ image: 3,
30
+ avatar: 4,
31
+ button: 5,
32
+ input: 6,
33
+ icon: 7,
34
+ divider: 8,
35
+ block: 0
36
+ };
37
+ var BONE_KIND_FROM_INDEX = Object.fromEntries(
38
+ Object.entries(BONE_KIND_INDEX).map(([k, v]) => [v, k])
39
+ );
40
+
41
+ // src/core/extractor.ts
42
+ var INLINE_TAGS = /* @__PURE__ */ new Set([
43
+ "A",
44
+ "ABBR",
45
+ "B",
46
+ "BDO",
47
+ "BR",
48
+ "CITE",
49
+ "CODE",
50
+ "DFN",
51
+ "EM",
52
+ "I",
53
+ "KBD",
54
+ "MARK",
55
+ "Q",
56
+ "S",
57
+ "SAMP",
58
+ "SMALL",
59
+ "SPAN",
60
+ "STRONG",
61
+ "SUB",
62
+ "SUP",
63
+ "TIME",
64
+ "U",
65
+ "VAR",
66
+ "WBR"
67
+ ]);
68
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
69
+ "SCRIPT",
70
+ "STYLE",
71
+ "NOSCRIPT",
72
+ "TEMPLATE",
73
+ "SVG",
74
+ "CANVAS",
75
+ "VIDEO",
76
+ "AUDIO",
77
+ "IFRAME",
78
+ "OBJECT",
79
+ "EMBED"
80
+ ]);
81
+ function detectKind(el) {
82
+ const tag = el.tagName;
83
+ if (tag === "IMG" || tag === "PICTURE") {
84
+ const w = el.getBoundingClientRect().width;
85
+ const h = el.getBoundingClientRect().height;
86
+ const ratio = Math.min(w, h) / Math.max(w, h);
87
+ if (ratio > 0.85 && w < 120) return "avatar";
88
+ return "image";
89
+ }
90
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return "input";
91
+ if (tag === "BUTTON" || el.getAttribute("role") === "button") return "button";
92
+ if (tag === "HR") return "divider";
93
+ if (/^H[1-6]$/.test(tag)) return "heading";
94
+ if (tag === "SVG" || tag === "svg") return "icon";
95
+ const style = getComputedStyle(el);
96
+ const borderRadius = parseInt(style.borderRadius, 10);
97
+ const rect = el.getBoundingClientRect();
98
+ if (borderRadius >= rect.width / 2 && rect.width < 120 && rect.width > 16) {
99
+ return "avatar";
100
+ }
101
+ if (el.childNodes.length > 0) {
102
+ const hasDirectText = Array.from(el.childNodes).some(
103
+ (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
104
+ );
105
+ if (hasDirectText && INLINE_TAGS.has(tag)) return "text";
106
+ if (hasDirectText && tag === "P") return "text";
107
+ }
108
+ return "block";
109
+ }
110
+ function computeRadius(el, kind) {
111
+ if (kind === "avatar") return 9999;
112
+ if (kind === "button") {
113
+ const style2 = getComputedStyle(el);
114
+ return Math.min(parseInt(style2.borderRadius, 10) || 6, 9999);
115
+ }
116
+ const style = getComputedStyle(el);
117
+ const r = parseInt(style.borderRadius, 10);
118
+ return isNaN(r) ? 0 : Math.min(r, 9999);
119
+ }
120
+ function isVisible(el) {
121
+ const style = getComputedStyle(el);
122
+ if (style.display === "none") return false;
123
+ if (style.visibility === "hidden") return false;
124
+ if (parseFloat(style.opacity) === 0) return false;
125
+ const rect = el.getBoundingClientRect();
126
+ return rect.width > 0 && rect.height > 0;
127
+ }
128
+ function isLeaf(el, contentAware) {
129
+ const tag = el.tagName;
130
+ if (tag === "IMG" || tag === "PICTURE" || tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || tag === "BUTTON" || tag === "HR" || tag === "SVG" || tag === "svg") {
131
+ return true;
132
+ }
133
+ if (contentAware) {
134
+ const hasOnlyText = Array.from(el.childNodes).every(
135
+ (n) => n.nodeType === Node.TEXT_NODE || INLINE_TAGS.has(n.tagName)
136
+ );
137
+ if (hasOnlyText && el.textContent?.trim()) return true;
138
+ }
139
+ if (el.children.length === 0) return true;
140
+ return false;
141
+ }
142
+ function extractBones(container, options = {}) {
143
+ const contentAware = options.contentAware ?? DEFAULTS.contentAware;
144
+ const minSize = options.minBoneSize ?? DEFAULTS.minBoneSize;
145
+ const containerRect = container.getBoundingClientRect();
146
+ const bones = [];
147
+ function walk(el) {
148
+ if (SKIP_TAGS.has(el.tagName)) return;
149
+ if (!isVisible(el)) return;
150
+ if (isLeaf(el, contentAware)) {
151
+ const rect = el.getBoundingClientRect();
152
+ if (rect.width < minSize || rect.height < minSize) return;
153
+ const kind = contentAware ? detectKind(el) : "block";
154
+ const r = computeRadius(el, kind);
155
+ bones.push({
156
+ x: Math.round((rect.left - containerRect.left) * 2) / 2,
157
+ y: Math.round((rect.top - containerRect.top) * 2) / 2,
158
+ w: Math.round(rect.width * 2) / 2,
159
+ h: Math.round(rect.height * 2) / 2,
160
+ r,
161
+ kind
162
+ });
163
+ return;
164
+ }
165
+ for (const child of el.children) {
166
+ walk(child);
167
+ }
168
+ }
169
+ walk(container);
170
+ return {
171
+ viewport: window.innerWidth,
172
+ containerWidth: Math.round(containerRect.width * 2) / 2,
173
+ containerHeight: Math.round(containerRect.height * 2) / 2,
174
+ bones
175
+ };
176
+ }
177
+ function getExtractorScript(selector, options = {}) {
178
+ return `
3
179
  (function() {
4
- ${N.toString()}
5
- ${R.toString()}
6
- ${T.toString()}
7
- ${A.toString()}
180
+ ${isVisible.toString()}
181
+ ${isLeaf.toString()}
182
+ ${detectKind.toString()}
183
+ ${computeRadius.toString()}
8
184
 
9
- var INLINE_TAGS = new Set(${JSON.stringify([...B])});
10
- var SKIP_TAGS = new Set(${JSON.stringify([...E])});
185
+ var INLINE_TAGS = new Set(${JSON.stringify([...INLINE_TAGS])});
186
+ var SKIP_TAGS = new Set(${JSON.stringify([...SKIP_TAGS])});
11
187
  var DEFAULTS = { contentAware: true, minBoneSize: 4 };
12
188
 
13
- ${X.toString()}
189
+ ${extractBones.toString()}
14
190
 
15
- var container = document.querySelector(${JSON.stringify(e)});
16
- if (!container) throw new Error('CastDOM: Container not found: ' + ${JSON.stringify(e)});
17
- return extractBones(container, ${JSON.stringify(t)});
191
+ var container = document.querySelector(${JSON.stringify(selector)});
192
+ if (!container) throw new Error('CastDOM: Container not found: ' + ${JSON.stringify(selector)});
193
+ return extractBones(container, ${JSON.stringify(options)});
18
194
  })()
19
- `.trim()}async function q(){try{return await import("playwright")}catch{throw new Error(`CastDOM: Playwright is required for extraction. Install it with:
20
- npm install -D playwright
21
- npx playwright install chromium`)}}function Y(e){let t=JSON.stringify(e),n=0;for(let o=0;o<t.length;o++){let a=t.charCodeAt(o);n=(n<<5)-n+a|0}return Math.abs(n).toString(36).padStart(8,"0")}async function j(e){let t=await q(),n=e.breakpoints??p.breakpoints,o=e.headless??!0,a=e.timeout??3e4,c=e.targets.length*n.length,r=0,i=await t.chromium.launch({headless:o}),l=[];try{for(let s of e.targets){let u=s.route?`${e.baseURL}${s.route}`:e.baseURL,d=[];for(let h of n){r++,e.onProgress?.(`Extracting "${s.name}" at ${h}px`,r,c);let m=await i.newPage();try{await m.setViewportSize({width:h,height:900}),await m.goto(u,{waitUntil:"networkidle",timeout:a}),e.waitFor&&await m.waitForSelector(e.waitFor,{timeout:a}),await m.waitForSelector(s.selector,{timeout:a});let g=I(s.selector,{contentAware:e.contentAware??p.contentAware,minBoneSize:e.minBoneSize??p.minBoneSize}),x=await m.evaluate(g);d.push(x)}finally{await m.close()}}let b={name:s.name,hash:Y(d),breakpoints:d,extractedAt:Date.now()};l.push({target:s,skeleton:b})}}finally{await i.close()}return l}function F(e){let t=[...e.bones].sort((c,r)=>c.y-r.y||c.x-r.x),n=[],o=0,a=0;for(let c of t){let r=Math.round(c.x*2),i=Math.round(c.y*2),l=Math.round(c.w*2),s=Math.round(c.h*2),u=Math.round(c.r*2),d=v[c.kind??"block"];n.push(r-o,i-a,l,s,u,d),o=r,a=i}return{v:1,vw:e.viewport,c:[Math.round(e.containerWidth*2),Math.round(e.containerHeight*2)],d:n}}function L(e){let n=[...[e.v,e.vw,e.c[0],e.c[1]],e.d.length,...e.d],o=[];for(let a of n){let r=a>=0?a*2:-a*2-1;for(;r>=128;)o.push(r&127|128),r>>>=7;o.push(r&127)}return typeof Buffer<"u"?Buffer.from(new Uint8Array(o)).toString("base64"):btoa(String.fromCharCode(...o))}function z(e){return{color:e?.color??p.color,shimmerColor:e?.shimmerColor??p.shimmerColor,animationDuration:e?.animationDuration??p.animationDuration,classPrefix:e?.classPrefix??p.classPrefix,inlineStyles:e?.inlineStyles??p.inlineStyles}}function Q(e,t){let n=z(t),{classPrefix:o,color:a,shimmerColor:c,animationDuration:r}=n,i=[...e.breakpoints].sort((u,d)=>u.viewport-d.viewport),l=e.name,s=[];if(s.push(`.${o}-${l} .${o}-bone{position:absolute;background:linear-gradient(90deg,${a} 25%,${c} 50%,${a} 75%);background-size:200% 100%;animation:${o}-shimmer ${r}ms ease-in-out infinite}`),i.length>1)for(let u=0;u<i.length;u++){let d=i[u],b=i[u+1],h=i[u-1],m;if(u===0)m=`@media(max-width:${Math.floor((d.viewport+b.viewport)/2)-1}px)`;else if(u===i.length-1)m=`@media(min-width:${Math.floor((h.viewport+d.viewport)/2)}px)`;else{let g=Math.floor((h.viewport+d.viewport)/2),x=Math.floor((d.viewport+b.viewport)/2)-1;m=`@media(min-width:${g}px) and (max-width:${x}px)`}s.push(`${m}{.${o}-${l} .${o}-bp-${d.viewport}{display:block}}`)}else i.length===1&&s.push(`.${o}-${l} .${o}-bp-${i[0].viewport}{display:block}`);return s.join(`
22
- `)}function k(e,t){let n=z(t),o=[];o.push(`@keyframes ${n.classPrefix}-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}`),o.push(`@media(prefers-reduced-motion:reduce){.${n.classPrefix}-bone{animation:none!important}}`);for(let a of e)o.push(Q(a,t));return o.join(`
23
- `)}function Z(e,t){return{...e,breakpoints:e.breakpoints.map(o=>{let a=[...o.bones],c=new Array(a.length).fill(!0);for(let r=0;r<a.length;r++)if(c[r])for(let i=r+1;i<a.length;i++){if(!c[i])continue;let l=a[r],s=a[i];s.x>=l.x-t&&s.y>=l.y-t&&s.x+s.w<=l.x+l.w+t&&s.y+s.h<=l.y+l.h+t&&(l.w*l.h>s.w*s.h*4||(c[i]=!1)),l.x>=s.x-t&&l.y>=s.y-t&&l.x+l.w<=s.x+s.w+t&&l.y+l.h<=s.y+s.h+t&&(s.w*s.h>l.w*l.h*4||(c[r]=!1))}return{...o,bones:a.filter((r,i)=>c[i])}})}}function ee(e){return{...e,breakpoints:e.breakpoints.map(t=>({...t,bones:t.bones.filter(n=>isFinite(n.x)&&isFinite(n.y)&&n.w>0&&n.h>0&&isFinite(n.r))}))}}function U(e,t={}){let n=t.dedupe??!0,o=t.overlapThreshold??2,a=t.compress??!0,c=t.generateTypes??!0,r=t.base64??!1,i=e.map(f=>f.skeleton);i=i.map(ee),n&&(i=i.map(f=>Z(f,o)));let s=JSON.stringify(i).length,u=s,d={};if(a||r)for(let f of i){let w=[],y=0;for(let $ of f.breakpoints){let M=F($);y+=JSON.stringify(M).length,r&&w.push(L(M))}u=y,r&&(d[f.name]=w)}let b=k(i),h;c&&(h=te(i));let m=i.reduce((f,w)=>f+w.breakpoints.reduce((y,$)=>y+$.bones.length,0),0),g=[...new Set(i.flatMap(f=>f.breakpoints.map(w=>w.viewport)))].sort((f,w)=>f-w),x={skeletonCount:i.length,totalBones:m,rawSize:s,compressedSize:u,compressionRatio:s>0?1-u/s:0,breakpoints:g};return{manifest:{version:1,generatedAt:Date.now(),skeletons:i},css:b,types:h,encoded:r?d:void 0,stats:x}}function te(e){return`// Auto-generated by CastDOM \u2014 do not edit
195
+ `.trim();
196
+ }
197
+
198
+ // src/build/snapshot.ts
199
+ async function getPlaywright() {
200
+ try {
201
+ return await import("playwright");
202
+ } catch {
203
+ throw new Error(
204
+ "CastDOM: Playwright is required for extraction. Install it with:\n npm install -D playwright\n npx playwright install chromium"
205
+ );
206
+ }
207
+ }
208
+ function hashSkeleton(breakpoints) {
209
+ const str = JSON.stringify(breakpoints);
210
+ let hash = 0;
211
+ for (let i = 0; i < str.length; i++) {
212
+ const chr = str.charCodeAt(i);
213
+ hash = (hash << 5) - hash + chr | 0;
214
+ }
215
+ return Math.abs(hash).toString(36).padStart(8, "0");
216
+ }
217
+ async function snapshot(options) {
218
+ const pw = await getPlaywright();
219
+ const breakpoints = options.breakpoints ?? DEFAULTS.breakpoints;
220
+ const headless = options.headless ?? true;
221
+ const timeout = options.timeout ?? 3e4;
222
+ const total = options.targets.length * breakpoints.length;
223
+ let current = 0;
224
+ const browser = await pw.chromium.launch({ headless });
225
+ const results = [];
226
+ try {
227
+ for (const target of options.targets) {
228
+ const url = target.route ? `${options.baseURL}${target.route}` : options.baseURL;
229
+ const breakpointData = [];
230
+ for (const vpWidth of breakpoints) {
231
+ current++;
232
+ options.onProgress?.(
233
+ `Extracting "${target.name}" at ${vpWidth}px`,
234
+ current,
235
+ total
236
+ );
237
+ const page = await browser.newPage();
238
+ try {
239
+ await page.setViewportSize({ width: vpWidth, height: 900 });
240
+ await page.goto(url, { waitUntil: "networkidle", timeout });
241
+ if (options.waitFor) {
242
+ await page.waitForSelector(options.waitFor, { timeout });
243
+ }
244
+ await page.waitForSelector(target.selector, { timeout });
245
+ const script = getExtractorScript(target.selector, {
246
+ contentAware: options.contentAware ?? DEFAULTS.contentAware,
247
+ minBoneSize: options.minBoneSize ?? DEFAULTS.minBoneSize
248
+ });
249
+ const data = await page.evaluate(script);
250
+ breakpointData.push(data);
251
+ } finally {
252
+ await page.close();
253
+ }
254
+ }
255
+ const skeleton = {
256
+ name: target.name,
257
+ hash: hashSkeleton(breakpointData),
258
+ breakpoints: breakpointData,
259
+ extractedAt: Date.now()
260
+ };
261
+ results.push({ target, skeleton });
262
+ }
263
+ } finally {
264
+ await browser.close();
265
+ }
266
+ return results;
267
+ }
268
+
269
+ // src/core/compress.ts
270
+ function compressBones(bp) {
271
+ const sorted = [...bp.bones].sort((a, b) => a.y - b.y || a.x - b.x);
272
+ const data = [];
273
+ let prevX = 0;
274
+ let prevY = 0;
275
+ for (const bone of sorted) {
276
+ const x = Math.round(bone.x * 2);
277
+ const y = Math.round(bone.y * 2);
278
+ const w = Math.round(bone.w * 2);
279
+ const h = Math.round(bone.h * 2);
280
+ const r = Math.round(bone.r * 2);
281
+ const kind = BONE_KIND_INDEX[bone.kind ?? "block"];
282
+ data.push(x - prevX, y - prevY, w, h, r, kind);
283
+ prevX = x;
284
+ prevY = y;
285
+ }
286
+ return {
287
+ v: 1,
288
+ vw: bp.viewport,
289
+ c: [
290
+ Math.round(bp.containerWidth * 2),
291
+ Math.round(bp.containerHeight * 2)
292
+ ],
293
+ d: data
294
+ };
295
+ }
296
+ function encodeBonesToBase64(compressed) {
297
+ const header = [compressed.v, compressed.vw, compressed.c[0], compressed.c[1]];
298
+ const allNums = [...header, compressed.d.length, ...compressed.d];
299
+ const bytes = [];
300
+ for (const n of allNums) {
301
+ const z = n >= 0 ? n * 2 : -n * 2 - 1;
302
+ let val = z;
303
+ while (val >= 128) {
304
+ bytes.push(val & 127 | 128);
305
+ val >>>= 7;
306
+ }
307
+ bytes.push(val & 127);
308
+ }
309
+ if (typeof Buffer !== "undefined") {
310
+ return Buffer.from(new Uint8Array(bytes)).toString("base64");
311
+ }
312
+ return btoa(String.fromCharCode(...bytes));
313
+ }
314
+
315
+ // src/core/renderer.ts
316
+ function resolveConfig(config) {
317
+ return {
318
+ color: config?.color ?? DEFAULTS.color,
319
+ shimmerColor: config?.shimmerColor ?? DEFAULTS.shimmerColor,
320
+ animationDuration: config?.animationDuration ?? DEFAULTS.animationDuration,
321
+ classPrefix: config?.classPrefix ?? DEFAULTS.classPrefix,
322
+ inlineStyles: config?.inlineStyles ?? DEFAULTS.inlineStyles
323
+ };
324
+ }
325
+ function generateCSS(skeleton, config) {
326
+ const cfg = resolveConfig(config);
327
+ const { classPrefix: p, color, shimmerColor, animationDuration } = cfg;
328
+ const breakpoints = [...skeleton.breakpoints].sort(
329
+ (a, b) => a.viewport - b.viewport
330
+ );
331
+ const name = skeleton.name;
332
+ const parts = [];
333
+ parts.push(
334
+ `.${p}-${name} .${p}-bone{position:absolute;background:linear-gradient(90deg,${color} 25%,${shimmerColor} 50%,${color} 75%);background-size:200% 100%;animation:${p}-shimmer ${animationDuration}ms ease-in-out infinite}`
335
+ );
336
+ if (breakpoints.length > 1) {
337
+ for (let i = 0; i < breakpoints.length; i++) {
338
+ const bp = breakpoints[i];
339
+ const next = breakpoints[i + 1];
340
+ const prev = breakpoints[i - 1];
341
+ let query;
342
+ if (i === 0) {
343
+ const max = Math.floor((bp.viewport + next.viewport) / 2) - 1;
344
+ query = `@media(max-width:${max}px)`;
345
+ } else if (i === breakpoints.length - 1) {
346
+ const min = Math.floor((prev.viewport + bp.viewport) / 2);
347
+ query = `@media(min-width:${min}px)`;
348
+ } else {
349
+ const min = Math.floor((prev.viewport + bp.viewport) / 2);
350
+ const max = Math.floor((bp.viewport + next.viewport) / 2) - 1;
351
+ query = `@media(min-width:${min}px) and (max-width:${max}px)`;
352
+ }
353
+ parts.push(
354
+ `${query}{.${p}-${name} .${p}-bp-${bp.viewport}{display:block}}`
355
+ );
356
+ }
357
+ } else if (breakpoints.length === 1) {
358
+ parts.push(
359
+ `.${p}-${name} .${p}-bp-${breakpoints[0].viewport}{display:block}`
360
+ );
361
+ }
362
+ return parts.join("\n");
363
+ }
364
+ function generateCriticalCSS(skeletons, config) {
365
+ const cfg = resolveConfig(config);
366
+ const parts = [];
367
+ parts.push(
368
+ `@keyframes ${cfg.classPrefix}-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}`
369
+ );
370
+ parts.push(
371
+ `@media(prefers-reduced-motion:reduce){.${cfg.classPrefix}-bone{animation:none!important}}`
372
+ );
373
+ for (const skeleton of skeletons) {
374
+ parts.push(generateCSS(skeleton, config));
375
+ }
376
+ return parts.join("\n");
377
+ }
378
+
379
+ // src/build/compiler.ts
380
+ function deduplicateBones(skeleton, threshold) {
381
+ const result = {
382
+ ...skeleton,
383
+ breakpoints: skeleton.breakpoints.map((bp) => {
384
+ const bones = [...bp.bones];
385
+ const keep = new Array(bones.length).fill(true);
386
+ for (let i = 0; i < bones.length; i++) {
387
+ if (!keep[i]) continue;
388
+ for (let j = i + 1; j < bones.length; j++) {
389
+ if (!keep[j]) continue;
390
+ const a = bones[i];
391
+ const b = bones[j];
392
+ if (b.x >= a.x - threshold && b.y >= a.y - threshold && b.x + b.w <= a.x + a.w + threshold && b.y + b.h <= a.y + a.h + threshold) {
393
+ if (a.w * a.h > b.w * b.h * 4) {
394
+ } else {
395
+ keep[j] = false;
396
+ }
397
+ }
398
+ if (a.x >= b.x - threshold && a.y >= b.y - threshold && a.x + a.w <= b.x + b.w + threshold && a.y + a.h <= b.y + b.h + threshold) {
399
+ if (b.w * b.h > a.w * a.h * 4) {
400
+ } else {
401
+ keep[i] = false;
402
+ }
403
+ }
404
+ }
405
+ }
406
+ return {
407
+ ...bp,
408
+ bones: bones.filter((_, i) => keep[i])
409
+ };
410
+ })
411
+ };
412
+ return result;
413
+ }
414
+ function validateSkeleton(skeleton) {
415
+ return {
416
+ ...skeleton,
417
+ breakpoints: skeleton.breakpoints.map((bp) => ({
418
+ ...bp,
419
+ bones: bp.bones.filter(
420
+ (bone) => isFinite(bone.x) && isFinite(bone.y) && bone.w > 0 && bone.h > 0 && isFinite(bone.r)
421
+ )
422
+ }))
423
+ };
424
+ }
425
+ function compile(results, options = {}) {
426
+ const dedupe = options.dedupe ?? true;
427
+ const overlapThreshold = options.overlapThreshold ?? 2;
428
+ const shouldCompress = options.compress ?? true;
429
+ const generateTypes = options.generateTypes ?? true;
430
+ const base64 = options.base64 ?? false;
431
+ let skeletons = results.map((r) => r.skeleton);
432
+ skeletons = skeletons.map(validateSkeleton);
433
+ if (dedupe) {
434
+ skeletons = skeletons.map((s) => deduplicateBones(s, overlapThreshold));
435
+ }
436
+ const rawJSON = JSON.stringify(skeletons);
437
+ const rawSize = rawJSON.length;
438
+ let compressedSize = rawSize;
439
+ const encoded = {};
440
+ if (shouldCompress || base64) {
441
+ for (const skeleton of skeletons) {
442
+ const encodedBPs = [];
443
+ let totalCompressed = 0;
444
+ for (const bp of skeleton.breakpoints) {
445
+ const compressed = compressBones(bp);
446
+ totalCompressed += JSON.stringify(compressed).length;
447
+ if (base64) {
448
+ encodedBPs.push(encodeBonesToBase64(compressed));
449
+ }
450
+ }
451
+ compressedSize = totalCompressed;
452
+ if (base64) {
453
+ encoded[skeleton.name] = encodedBPs;
454
+ }
455
+ }
456
+ }
457
+ const css = generateCriticalCSS(skeletons);
458
+ let types;
459
+ if (generateTypes) {
460
+ types = generateTypeDeclarations(skeletons);
461
+ }
462
+ const totalBones = skeletons.reduce(
463
+ (sum, s) => sum + s.breakpoints.reduce((bpSum, bp) => bpSum + bp.bones.length, 0),
464
+ 0
465
+ );
466
+ const allBreakpoints = [
467
+ ...new Set(skeletons.flatMap((s) => s.breakpoints.map((bp) => bp.viewport)))
468
+ ].sort((a, b) => a - b);
469
+ const stats = {
470
+ skeletonCount: skeletons.length,
471
+ totalBones,
472
+ rawSize,
473
+ compressedSize,
474
+ compressionRatio: rawSize > 0 ? 1 - compressedSize / rawSize : 0,
475
+ breakpoints: allBreakpoints
476
+ };
477
+ return {
478
+ manifest: {
479
+ version: 1,
480
+ generatedAt: Date.now(),
481
+ skeletons
482
+ },
483
+ css,
484
+ types,
485
+ encoded: base64 ? encoded : void 0,
486
+ stats
487
+ };
488
+ }
489
+ function generateTypeDeclarations(skeletons) {
490
+ const names = skeletons.map((s) => ` | "${s.name}"`).join("\n");
491
+ return `// Auto-generated by CastDOM \u2014 do not edit
24
492
  // Regenerate with: npx castdom build
25
493
 
26
494
  export type CastDOMSkeletonName =
27
- ${e.map(n=>` | "${n.name}"`).join(`
28
- `)};
495
+ ${names};
29
496
 
30
497
  declare module "castdom" {
31
498
  interface CastDOMRegistry {
32
- ${e.map(n=>` "${n.name}": true;`).join(`
33
- `)}
499
+ ${skeletons.map((s) => ` "${s.name}": true;`).join("\n")}
34
500
  }
35
501
  }
36
- `}function J(e,t={}){let n=t.outDir??".castdom",o=t.splitFiles??!0,a=t.esm??!0,c=t.cjs??!1,r=[],i={version:1,generatedAt:Date.now(),skeletons:e};r.push({path:`${n}/manifest.json`,content:JSON.stringify(i,null,2)});let l=k(e,t.config);if(r.push({path:`${n}/castdom.css`,content:l}),a&&r.push({path:`${n}/index.js`,content:ne(e)}),c&&r.push({path:`${n}/index.cjs`,content:oe(e)}),r.push({path:`${n}/index.d.ts`,content:re(e)}),o)for(let s of e)r.push({path:`${n}/skeletons/${s.name}.json`,content:JSON.stringify(s,null,2)}),a&&r.push({path:`${n}/skeletons/${s.name}.js`,content:`export default ${JSON.stringify(s)};
37
- `});return r.push({path:`${n}/loader.js`,content:ie(e)}),r.push({path:`${n}/nextjs-loading.tsx`,content:se(e)}),r}function ne(e){let t=e.map(o=>`import ${K(o.name)} from "./skeletons/${o.name}.json" with { type: "json" };`).join(`
38
- `),n=e.map(o=>` "${o.name}": ${K(o.name)},`).join(`
39
- `);return`// Auto-generated by CastDOM \u2014 do not edit
40
- ${t}
502
+ `;
503
+ }
504
+
505
+ // src/build/codegen.ts
506
+ function generateFiles(skeletons, options = {}) {
507
+ const outDir = options.outDir ?? ".castdom";
508
+ const splitFiles = options.splitFiles ?? true;
509
+ const esm = options.esm ?? true;
510
+ const cjs = options.cjs ?? false;
511
+ const files = [];
512
+ const manifest = {
513
+ version: 1,
514
+ generatedAt: Date.now(),
515
+ skeletons
516
+ };
517
+ files.push({
518
+ path: `${outDir}/manifest.json`,
519
+ content: JSON.stringify(manifest, null, 2)
520
+ });
521
+ const css = generateCriticalCSS(skeletons, options.config);
522
+ files.push({
523
+ path: `${outDir}/castdom.css`,
524
+ content: css
525
+ });
526
+ if (esm) {
527
+ files.push({
528
+ path: `${outDir}/index.js`,
529
+ content: generateESMModule(skeletons)
530
+ });
531
+ }
532
+ if (cjs) {
533
+ files.push({
534
+ path: `${outDir}/index.cjs`,
535
+ content: generateCJSModule(skeletons)
536
+ });
537
+ }
538
+ files.push({
539
+ path: `${outDir}/index.d.ts`,
540
+ content: generateTypeDefs(skeletons)
541
+ });
542
+ if (splitFiles) {
543
+ for (const skeleton of skeletons) {
544
+ files.push({
545
+ path: `${outDir}/skeletons/${skeleton.name}.json`,
546
+ content: JSON.stringify(skeleton, null, 2)
547
+ });
548
+ if (esm) {
549
+ files.push({
550
+ path: `${outDir}/skeletons/${skeleton.name}.js`,
551
+ content: `export default ${JSON.stringify(skeleton)};
552
+ `
553
+ });
554
+ }
555
+ }
556
+ }
557
+ files.push({
558
+ path: `${outDir}/loader.js`,
559
+ content: generateLoader(skeletons)
560
+ });
561
+ files.push({
562
+ path: `${outDir}/nextjs-loading.tsx`,
563
+ content: generateNextJSLoading(skeletons)
564
+ });
565
+ return files;
566
+ }
567
+ function generateESMModule(skeletons) {
568
+ const imports = skeletons.map((s) => `import ${safeName(s.name)} from "./skeletons/${s.name}.json" with { type: "json" };`).join("\n");
569
+ const exports = skeletons.map((s) => ` "${s.name}": ${safeName(s.name)},`).join("\n");
570
+ return `// Auto-generated by CastDOM \u2014 do not edit
571
+ ${imports}
41
572
 
42
573
  export const skeletons = {
43
- ${n}
574
+ ${exports}
44
575
  };
45
576
 
46
577
  export const manifest = {
@@ -49,10 +580,13 @@ export const manifest = {
49
580
  };
50
581
 
51
582
  export default manifest;
52
- `}function oe(e){return`// Auto-generated by CastDOM \u2014 do not edit
583
+ `;
584
+ }
585
+ function generateCJSModule(skeletons) {
586
+ return `// Auto-generated by CastDOM \u2014 do not edit
53
587
  "use strict";
54
588
 
55
- const manifest = ${JSON.stringify({version:1,skeletons:e},null,2)};
589
+ const manifest = ${JSON.stringify({ version: 1, skeletons }, null, 2)};
56
590
 
57
591
  module.exports = manifest;
58
592
  module.exports.default = manifest;
@@ -60,12 +594,15 @@ module.exports.skeletons = manifest.skeletons.reduce((acc, s) => {
60
594
  acc[s.name] = s;
61
595
  return acc;
62
596
  }, {});
63
- `}function re(e){return`// Auto-generated by CastDOM \u2014 do not edit
597
+ `;
598
+ }
599
+ function generateTypeDefs(skeletons) {
600
+ const names = skeletons.map((s) => ` | "${s.name}"`).join("\n");
601
+ return `// Auto-generated by CastDOM \u2014 do not edit
64
602
  import type { SkeletonData } from "castdom";
65
603
 
66
604
  export type SkeletonName =
67
- ${e.map(n=>` | "${n.name}"`).join(`
68
- `)};
605
+ ${names};
69
606
 
70
607
  export declare const skeletons: Record<SkeletonName, SkeletonData>;
71
608
 
@@ -75,7 +612,10 @@ export declare const manifest: {
75
612
  };
76
613
 
77
614
  export default manifest;
78
- `}function ie(e){return`// Auto-generated by CastDOM \u2014 do not edit
615
+ `;
616
+ }
617
+ function generateLoader(skeletons) {
618
+ return `// Auto-generated by CastDOM \u2014 do not edit
79
619
  // Import this file once at your app's entry point to register all skeletons.
80
620
  //
81
621
  // import ".castdom/loader.js";
@@ -84,8 +624,12 @@ import { loadManifest } from "castdom";
84
624
  import manifest from "./manifest.json" with { type: "json" };
85
625
 
86
626
  loadManifest(manifest);
87
- `}function se(e){return e.length===0?`export default function Loading() { return null; }
88
- `:`// Auto-generated by CastDOM \u2014 do not edit
627
+ `;
628
+ }
629
+ function generateNextJSLoading(skeletons) {
630
+ if (skeletons.length === 0) return "export default function Loading() { return null; }\n";
631
+ const first = skeletons[0];
632
+ return `// Auto-generated by CastDOM \u2014 do not edit
89
633
  // Copy this to your app/loading.tsx or page-specific loading.tsx
90
634
  //
91
635
  // For multiple skeletons, use <CastDOM name="..."> directly.
@@ -96,13 +640,20 @@ export default function Loading() {
96
640
  return (
97
641
  <>
98
642
  <CastDOMStyle />
99
- <CastDOM name="${e[0].name}" loading={true}>
643
+ <CastDOM name="${first.name}" loading={true}>
100
644
  {null}
101
645
  </CastDOM>
102
646
  </>
103
647
  );
104
648
  }
105
- `}function K(e){return e.replace(/[^a-zA-Z0-9_$]/g,"_")}var O=`
649
+ `;
650
+ }
651
+ function safeName(name) {
652
+ return name.replace(/[^a-zA-Z0-9_$]/g, "_");
653
+ }
654
+
655
+ // src/cli.ts
656
+ var HELP = `
106
657
  CastDOM \u2014 Pixel-perfect skeleton screens from your real DOM
107
658
 
108
659
  Usage:
@@ -121,15 +672,228 @@ Build options:
121
672
  --headless Run browser in headless mode (default: true)
122
673
  --no-headless Run browser with visible UI (for debugging)
123
674
  --verbose Show detailed progress output
124
- `.trim(),G="1.0.0";function le(e){let t={command:e[0]??"build",url:"http://localhost:3000",configPath:"castdom.config.json",outDir:p.outDir,breakpoints:[...p.breakpoints],headless:!0,verbose:!1};for(let n=1;n<e.length;n++)switch(e[n]){case"--url":t.url=e[++n]??t.url;break;case"--config":t.configPath=e[++n]??t.configPath;break;case"--out":t.outDir=e[++n]??t.outDir;break;case"--breakpoints":t.breakpoints=(e[++n]??"").split(",").map(Number).filter(a=>a>0);break;case"--headless":t.headless=!0;break;case"--no-headless":t.headless=!1;break;case"--verbose":t.verbose=!0;break;case"--help":case"-h":console.log(O),process.exit(0);case"--version":case"-v":console.log(G),process.exit(0)}return t}function P(e){let t=S(e);if(!C(t))return{};try{let n=_(t,"utf-8");return JSON.parse(n)}catch{return console.error(`Failed to parse config: ${t}`),{}}}function D(e,...t){e&&console.log(...t)}async function ue(e){let t=P(e.configPath),n=S(t.outDir??e.outDir),o=t.breakpoints??e.breakpoints,a=t.devServer??e.url,c=t.targets??[];c.length===0&&(console.log("No targets configured. Scanning for [data-castdom] elements..."),c.push({name:"auto",selector:"[data-castdom]"})),console.log(`
125
- CastDOM Build`),console.log(` Server: ${a}`),console.log(` Breakpoints: ${o.join(", ")}px`),console.log(` Targets: ${c.length}`),console.log(` Output: ${n}
126
- `);let r=Date.now(),i=await j({baseURL:a,targets:c,breakpoints:o,headless:e.headless,contentAware:t.contentAware??!0,minBoneSize:t.minBoneSize??p.minBoneSize,onProgress(m,g,x){D(e.verbose,` [${g}/${x}] ${m}`)}}),l=Date.now()-r;D(e.verbose,`
127
- Extraction: ${l}ms`);let s=Date.now(),u=U(i),d=Date.now()-s;D(e.verbose,` Compilation: ${d}ms`);let b=J(u.manifest.skeletons,{outDir:n,config:t});for(let m of b){let g=m.path.substring(0,m.path.lastIndexOf("/"));ce(g,{recursive:!0}),H(m.path,m.content,"utf-8"),D(e.verbose,` Written: ${m.path}`)}let h=Date.now()-r;console.log(` Skeletons: ${u.stats.skeletonCount}`),console.log(` Bones: ${u.stats.totalBones}`),console.log(` Raw size: ${(u.stats.rawSize/1024).toFixed(1)} KB`),console.log(` Compressed: ${(u.stats.compressedSize/1024).toFixed(1)} KB (${(u.stats.compressionRatio*100).toFixed(0)}% smaller)`),console.log(` Files: ${b.length}`),console.log(` Total time: ${h}ms`),console.log(`
675
+ `.trim();
676
+ var VERSION = "1.0.0";
677
+ function parseArgs(args) {
678
+ const opts = {
679
+ command: args[0] ?? "build",
680
+ url: "http://localhost:3000",
681
+ configPath: "castdom.config.json",
682
+ outDir: DEFAULTS.outDir,
683
+ breakpoints: [...DEFAULTS.breakpoints],
684
+ headless: true,
685
+ verbose: false
686
+ };
687
+ for (let i = 1; i < args.length; i++) {
688
+ const arg = args[i];
689
+ switch (arg) {
690
+ case "--url":
691
+ opts.url = args[++i] ?? opts.url;
692
+ break;
693
+ case "--config":
694
+ opts.configPath = args[++i] ?? opts.configPath;
695
+ break;
696
+ case "--out":
697
+ opts.outDir = args[++i] ?? opts.outDir;
698
+ break;
699
+ case "--breakpoints":
700
+ opts.breakpoints = (args[++i] ?? "").split(",").map(Number).filter((n) => n > 0);
701
+ break;
702
+ case "--headless":
703
+ opts.headless = true;
704
+ break;
705
+ case "--no-headless":
706
+ opts.headless = false;
707
+ break;
708
+ case "--verbose":
709
+ opts.verbose = true;
710
+ break;
711
+ case "--help":
712
+ case "-h":
713
+ console.log(HELP);
714
+ process.exit(0);
715
+ case "--version":
716
+ case "-v":
717
+ console.log(VERSION);
718
+ process.exit(0);
719
+ }
720
+ }
721
+ return opts;
722
+ }
723
+ function loadConfig(configPath) {
724
+ const absPath = resolve(configPath);
725
+ if (!existsSync(absPath)) return {};
726
+ try {
727
+ const raw = readFileSync(absPath, "utf-8");
728
+ return JSON.parse(raw);
729
+ } catch (err) {
730
+ console.error(`Failed to parse config: ${absPath}`);
731
+ return {};
732
+ }
733
+ }
734
+ function log(verbose, ...args) {
735
+ if (verbose) console.log(...args);
736
+ }
737
+ async function cmdBuild(opts) {
738
+ const config = loadConfig(opts.configPath);
739
+ const outDir = resolve(config.outDir ?? opts.outDir);
740
+ const breakpoints = config.breakpoints ?? opts.breakpoints;
741
+ const url = config.devServer ?? opts.url;
742
+ const targets = config.targets ?? [];
743
+ if (targets.length === 0) {
744
+ console.log("No targets configured. Scanning for [data-castdom] elements...");
745
+ targets.push({
746
+ name: "auto",
747
+ selector: "[data-castdom]"
748
+ });
749
+ }
750
+ console.log(`
751
+ CastDOM Build`);
752
+ console.log(` Server: ${url}`);
753
+ console.log(` Breakpoints: ${breakpoints.join(", ")}px`);
754
+ console.log(` Targets: ${targets.length}`);
755
+ console.log(` Output: ${outDir}
756
+ `);
757
+ const startTime = Date.now();
758
+ const results = await snapshot({
759
+ baseURL: url,
760
+ targets,
761
+ breakpoints,
762
+ headless: opts.headless,
763
+ contentAware: config.contentAware ?? true,
764
+ minBoneSize: config.minBoneSize ?? DEFAULTS.minBoneSize,
765
+ onProgress(message, current, total) {
766
+ log(opts.verbose, ` [${current}/${total}] ${message}`);
767
+ }
768
+ });
769
+ const extractTime = Date.now() - startTime;
770
+ log(opts.verbose, `
771
+ Extraction: ${extractTime}ms`);
772
+ const compileStart = Date.now();
773
+ const compiled = compile(results);
774
+ const compileTime = Date.now() - compileStart;
775
+ log(opts.verbose, ` Compilation: ${compileTime}ms`);
776
+ const files = generateFiles(compiled.manifest.skeletons, {
777
+ outDir,
778
+ config
779
+ });
780
+ for (const file of files) {
781
+ const dir = file.path.substring(0, file.path.lastIndexOf("/"));
782
+ mkdirSync(dir, { recursive: true });
783
+ writeFileSync(file.path, file.content, "utf-8");
784
+ log(opts.verbose, ` Written: ${file.path}`);
785
+ }
786
+ const totalTime = Date.now() - startTime;
787
+ console.log(` Skeletons: ${compiled.stats.skeletonCount}`);
788
+ console.log(` Bones: ${compiled.stats.totalBones}`);
789
+ console.log(` Raw size: ${(compiled.stats.rawSize / 1024).toFixed(1)} KB`);
790
+ console.log(
791
+ ` Compressed: ${(compiled.stats.compressedSize / 1024).toFixed(1)} KB (${(compiled.stats.compressionRatio * 100).toFixed(0)}% smaller)`
792
+ );
793
+ console.log(` Files: ${files.length}`);
794
+ console.log(` Total time: ${totalTime}ms`);
795
+ console.log(`
128
796
  Done. Import the loader to register:
129
- `),console.log(` import "${n}/loader.js";
130
- `)}function me(e){let t=S(e.configPath);if(C(t)){console.log(`Config already exists: ${t}`);return}H(t,JSON.stringify({devServer:"http://localhost:3000",outDir:".castdom",breakpoints:[375,768,1280],color:"#e0e0e0",shimmerColor:"#f0f0f0",animationDuration:1500,contentAware:!0,minBoneSize:4,targets:[{name:"example-card",selector:'[data-castdom="example-card"]',route:"/"}]},null,2)+`
131
- `,"utf-8"),console.log(`Created: ${t}`),console.log(`
132
- Edit the "targets" array to define your skeleton targets.`),console.log(`Then run: npx castdom build
133
- `)}function de(e){let t=P(e.configPath),n=S(t.outDir??e.outDir),o=ae(n,"manifest.json");if(!C(o)){console.log('No manifest found. Run "npx castdom build" first.');return}let c=JSON.parse(_(o,"utf-8")).skeletons??[];console.log(`
134
- CastDOM Skeletons (${c.length}):
135
- `);for(let r of c){let i=r.breakpoints.reduce((s,u)=>s+u.bones.length,0),l=r.breakpoints.map(s=>`${s.viewport}px`).join(", ");console.log(` ${r.name}`),console.log(` Bones: ${i} | Breakpoints: ${l} | Hash: ${r.hash}`)}console.log()}function pe(e){let t=P(e.configPath),n=S(t.outDir??e.outDir);if(!C(n)){console.log("Nothing to clean.");return}let{rmSync:o}=V("fs");o(n,{recursive:!0,force:!0}),console.log(`Removed: ${n}`)}async function fe(){let e=process.argv.slice(2);if(e.length===0||e[0]==="--help"||e[0]==="-h"){console.log(O);return}if(e[0]==="--version"||e[0]==="-v"){console.log(G);return}let t=le(e);switch(t.command){case"build":await ue(t);break;case"init":me(t);break;case"list":de(t);break;case"clean":pe(t);break;default:console.error(`Unknown command: ${t.command}`),console.log(O),process.exit(1)}}fe().catch(e=>{console.error("CastDOM error:",e.message??e),process.exit(1)});
797
+ `);
798
+ console.log(` import "${outDir}/loader.js";
799
+ `);
800
+ }
801
+ function cmdInit(opts) {
802
+ const configPath = resolve(opts.configPath);
803
+ if (existsSync(configPath)) {
804
+ console.log(`Config already exists: ${configPath}`);
805
+ return;
806
+ }
807
+ const template = {
808
+ devServer: "http://localhost:3000",
809
+ outDir: ".castdom",
810
+ breakpoints: [375, 768, 1280],
811
+ color: "#e0e0e0",
812
+ shimmerColor: "#f0f0f0",
813
+ animationDuration: 1500,
814
+ contentAware: true,
815
+ minBoneSize: 4,
816
+ targets: [
817
+ {
818
+ name: "example-card",
819
+ selector: '[data-castdom="example-card"]',
820
+ route: "/"
821
+ }
822
+ ]
823
+ };
824
+ writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n", "utf-8");
825
+ console.log(`Created: ${configPath}`);
826
+ console.log(`
827
+ Edit the "targets" array to define your skeleton targets.`);
828
+ console.log(`Then run: npx castdom build
829
+ `);
830
+ }
831
+ function cmdList(opts) {
832
+ const config = loadConfig(opts.configPath);
833
+ const outDir = resolve(config.outDir ?? opts.outDir);
834
+ const manifestPath = join(outDir, "manifest.json");
835
+ if (!existsSync(manifestPath)) {
836
+ console.log(`No manifest found. Run "npx castdom build" first.`);
837
+ return;
838
+ }
839
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
840
+ const skeletons = manifest.skeletons ?? [];
841
+ console.log(`
842
+ CastDOM Skeletons (${skeletons.length}):
843
+ `);
844
+ for (const s of skeletons) {
845
+ const boneCount = s.breakpoints.reduce(
846
+ (sum, bp) => sum + bp.bones.length,
847
+ 0
848
+ );
849
+ const bps = s.breakpoints.map((bp) => `${bp.viewport}px`).join(", ");
850
+ console.log(` ${s.name}`);
851
+ console.log(` Bones: ${boneCount} | Breakpoints: ${bps} | Hash: ${s.hash}`);
852
+ }
853
+ console.log();
854
+ }
855
+ function cmdClean(opts) {
856
+ const config = loadConfig(opts.configPath);
857
+ const outDir = resolve(config.outDir ?? opts.outDir);
858
+ if (!existsSync(outDir)) {
859
+ console.log("Nothing to clean.");
860
+ return;
861
+ }
862
+ const { rmSync } = __require("fs");
863
+ rmSync(outDir, { recursive: true, force: true });
864
+ console.log(`Removed: ${outDir}`);
865
+ }
866
+ async function main() {
867
+ const args = process.argv.slice(2);
868
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
869
+ console.log(HELP);
870
+ return;
871
+ }
872
+ if (args[0] === "--version" || args[0] === "-v") {
873
+ console.log(VERSION);
874
+ return;
875
+ }
876
+ const opts = parseArgs(args);
877
+ switch (opts.command) {
878
+ case "build":
879
+ await cmdBuild(opts);
880
+ break;
881
+ case "init":
882
+ cmdInit(opts);
883
+ break;
884
+ case "list":
885
+ cmdList(opts);
886
+ break;
887
+ case "clean":
888
+ cmdClean(opts);
889
+ break;
890
+ default:
891
+ console.error(`Unknown command: ${opts.command}`);
892
+ console.log(HELP);
893
+ process.exit(1);
894
+ }
895
+ }
896
+ main().catch((err) => {
897
+ console.error("CastDOM error:", err.message ?? err);
898
+ process.exit(1);
899
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukas_holdings/castdom",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Pixel-perfect skeleton loading screens, extracted from your real DOM. Zero-config, SSR-first, CSS-only runtime.",
5
5
  "homepage": "https://kameeleonn.github.io/CastDOM/",
6
6
  "repository": {