@lukas_holdings/castdom 1.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.
- package/LICENSE +21 -0
- package/README.md +707 -0
- package/bin/castdom.js +2 -0
- package/dist/astro.cjs +86 -0
- package/dist/astro.cjs.map +1 -0
- package/dist/astro.d.cts +88 -0
- package/dist/astro.d.ts +88 -0
- package/dist/astro.js +80 -0
- package/dist/astro.js.map +1 -0
- package/dist/chunk-COLESJ66.js +57 -0
- package/dist/chunk-COLESJ66.js.map +1 -0
- package/dist/chunk-EJRNKHL5.js +31 -0
- package/dist/chunk-EJRNKHL5.js.map +1 -0
- package/dist/chunk-JRQ6EVQP.cjs +35 -0
- package/dist/chunk-JRQ6EVQP.cjs.map +1 -0
- package/dist/chunk-KGLTVTHU.js +73 -0
- package/dist/chunk-KGLTVTHU.js.map +1 -0
- package/dist/chunk-O4OOMGGM.cjs +198 -0
- package/dist/chunk-O4OOMGGM.cjs.map +1 -0
- package/dist/chunk-ONS533CQ.js +104 -0
- package/dist/chunk-ONS533CQ.js.map +1 -0
- package/dist/chunk-ORY4OMZ5.cjs +110 -0
- package/dist/chunk-ORY4OMZ5.cjs.map +1 -0
- package/dist/chunk-QLEBTZIB.cjs +64 -0
- package/dist/chunk-QLEBTZIB.cjs.map +1 -0
- package/dist/chunk-XS5HAU5E.cjs +109 -0
- package/dist/chunk-XS5HAU5E.cjs.map +1 -0
- package/dist/chunk-YDT4TPB7.cjs +84 -0
- package/dist/chunk-YDT4TPB7.cjs.map +1 -0
- package/dist/chunk-ZBJB7WVV.js +193 -0
- package/dist/chunk-ZBJB7WVV.js.map +1 -0
- package/dist/chunk-ZWZ5ZLJE.js +103 -0
- package/dist/chunk-ZWZ5ZLJE.js.map +1 -0
- package/dist/cli.js +135 -0
- package/dist/index.cjs +540 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +176 -0
- package/dist/index.d.ts +176 -0
- package/dist/index.js +440 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +65 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +72 -0
- package/dist/next.d.ts +72 -0
- package/dist/next.js +48 -0
- package/dist/next.js.map +1 -0
- package/dist/react.cjs +30 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +70 -0
- package/dist/react.d.ts +70 -0
- package/dist/react.js +7 -0
- package/dist/react.js.map +1 -0
- package/dist/renderer-B1R7u2wm.d.ts +30 -0
- package/dist/renderer-Bfzjr6l9.d.cts +30 -0
- package/dist/ssr.cjs +46 -0
- package/dist/ssr.cjs.map +1 -0
- package/dist/ssr.d.cts +83 -0
- package/dist/ssr.d.ts +83 -0
- package/dist/ssr.js +5 -0
- package/dist/ssr.js.map +1 -0
- package/dist/types-ChD5jENU.d.cts +105 -0
- package/dist/types-ChD5jENU.d.ts +105 -0
- package/dist/vite.cjs +83 -0
- package/dist/vite.cjs.map +1 -0
- package/dist/vite.d.cts +81 -0
- package/dist/vite.d.ts +81 -0
- package/dist/vite.js +77 -0
- package/dist/vite.js.map +1 -0
- package/package.json +130 -0
package/README.md
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/npm/v/castdom?style=flat-square&color=000" alt="npm version" />
|
|
3
|
+
<img src="https://img.shields.io/badge/license-MIT-000?style=flat-square" alt="license" />
|
|
4
|
+
<img src="https://img.shields.io/badge/runtime-CSS%20only-000?style=flat-square" alt="CSS only runtime" />
|
|
5
|
+
<img src="https://img.shields.io/badge/bundle-~14KB-000?style=flat-square" alt="bundle size" />
|
|
6
|
+
<img src="https://img.shields.io/badge/tests-71%20passed-000?style=flat-square" alt="tests" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<h1 align="center">CastDOM</h1>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<strong>Pixel-perfect skeleton loading screens, extracted from your real DOM.</strong><br/>
|
|
13
|
+
No manual measurement. No hand-tuned placeholders. Zero-config, SSR-first, CSS-only runtime.
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="#quick-start">Quick Start</a> •
|
|
18
|
+
<a href="#how-it-works">How It Works</a> •
|
|
19
|
+
<a href="#react">React</a> •
|
|
20
|
+
<a href="#nextjs">Next.js</a> •
|
|
21
|
+
<a href="#astro">Astro</a> •
|
|
22
|
+
<a href="#vite">Vite</a> •
|
|
23
|
+
<a href="#ssr--seo">SSR & SEO</a> •
|
|
24
|
+
<a href="#api-reference">API</a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Why CastDOM?
|
|
30
|
+
|
|
31
|
+
Every skeleton screen you've ever built was a lie. You eyeballed the widths, guessed the heights, and prayed the layout didn't shift. CastDOM fixes that.
|
|
32
|
+
|
|
33
|
+
It reads `getBoundingClientRect()` on every visible element in your component, stores the positions as a flat array of bones, and renders them as CSS-only rectangles that match your real layout **exactly** — at every breakpoint.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Your Component CastDOM Skeleton
|
|
37
|
+
+----------------------------------+ +----------------------------------+
|
|
38
|
+
| [====] [O] | | [####] [O] |
|
|
39
|
+
| | | |
|
|
40
|
+
| Some heading text | | [################] |
|
|
41
|
+
| A longer paragraph that wraps | | [##########################] |
|
|
42
|
+
| across multiple lines of text | | [########################] |
|
|
43
|
+
| | | |
|
|
44
|
+
| [ Button ] | | [##########] |
|
|
45
|
+
+----------------------------------+ +----------------------------------+
|
|
46
|
+
Real DOM Extracted Skeleton
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Key Advantages
|
|
50
|
+
|
|
51
|
+
- **Pixel-perfect** — bones are extracted from your actual rendered DOM, not approximated
|
|
52
|
+
- **SSR-first** — skeletons render server-side as real HTML. Crawlers see them. Zero CLS
|
|
53
|
+
- **CSS-only runtime** — no JavaScript needed to display skeletons. Just CSS animations
|
|
54
|
+
- **Content-aware** — detects text, headings, images, avatars, buttons, and inputs
|
|
55
|
+
- **Responsive** — captures at multiple breakpoints (375px, 768px, 1280px by default)
|
|
56
|
+
- **Compressed** — delta-encoded bone data is 60-70% smaller than raw JSON
|
|
57
|
+
- **Accessible** — `role="status"`, `aria-busy`, `aria-label`, `prefers-reduced-motion`
|
|
58
|
+
- **Framework-native** — adapters for Next.js, Astro, Vite, and vanilla JS
|
|
59
|
+
- **Zero runtime dependencies** — the client bundle is pure CSS + tiny registry
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### 1. Install
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm install castdom
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Mark your components
|
|
72
|
+
|
|
73
|
+
Add `data-castdom` attributes to the components you want skeletons for:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<div data-castdom="user-card">
|
|
77
|
+
<img class="avatar" src="..." />
|
|
78
|
+
<h2>Jane Doe</h2>
|
|
79
|
+
<p>Software Engineer at Acme Corp</p>
|
|
80
|
+
<button>Follow</button>
|
|
81
|
+
</div>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Configure
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx castdom init
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This creates `castdom.config.json`:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"devServer": "http://localhost:3000",
|
|
95
|
+
"outDir": ".castdom",
|
|
96
|
+
"breakpoints": [375, 768, 1280],
|
|
97
|
+
"color": "#e0e0e0",
|
|
98
|
+
"shimmerColor": "#f0f0f0",
|
|
99
|
+
"animationDuration": 1500,
|
|
100
|
+
"contentAware": true,
|
|
101
|
+
"targets": [
|
|
102
|
+
{
|
|
103
|
+
"name": "user-card",
|
|
104
|
+
"selector": "[data-castdom=\"user-card\"]",
|
|
105
|
+
"route": "/"
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 4. Extract
|
|
112
|
+
|
|
113
|
+
Start your dev server, then:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx castdom build
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
CastDOM Build
|
|
121
|
+
Server: http://localhost:3000
|
|
122
|
+
Breakpoints: 375, 768, 1280px
|
|
123
|
+
Targets: 1
|
|
124
|
+
Output: .castdom
|
|
125
|
+
|
|
126
|
+
Skeletons: 1
|
|
127
|
+
Bones: 12
|
|
128
|
+
Raw size: 2.1 KB
|
|
129
|
+
Compressed: 0.8 KB (62% smaller)
|
|
130
|
+
Files: 8
|
|
131
|
+
Total time: 1240ms
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 5. Use
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// Import the loader once at your app entry
|
|
138
|
+
import ".castdom/loader.js";
|
|
139
|
+
|
|
140
|
+
// Use anywhere
|
|
141
|
+
import { CastDOM } from "castdom/react";
|
|
142
|
+
|
|
143
|
+
function UserProfile({ userId }) {
|
|
144
|
+
const { data, isLoading } = useFetch(`/api/users/${userId}`);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<CastDOM name="user-card" loading={isLoading}>
|
|
148
|
+
<UserCard data={data} />
|
|
149
|
+
</CastDOM>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
That's it. The skeleton matches your real layout perfectly, at every screen size.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## How It Works
|
|
159
|
+
|
|
160
|
+
### Extraction Pipeline
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Dev Server Playwright Extractor Compiler
|
|
164
|
+
| | | |
|
|
165
|
+
| load page | | |
|
|
166
|
+
|<------------------| | |
|
|
167
|
+
| set viewport | | |
|
|
168
|
+
|<------------------| | |
|
|
169
|
+
| | inject script | |
|
|
170
|
+
| |------------------->| |
|
|
171
|
+
| | | walk DOM tree |
|
|
172
|
+
| | | getBoundingRect |
|
|
173
|
+
| | | detect kinds |
|
|
174
|
+
| | bone data | |
|
|
175
|
+
| |<-------------------| |
|
|
176
|
+
| | | |
|
|
177
|
+
| | compile, compress, codegen |
|
|
178
|
+
| |-------------------------------------->|
|
|
179
|
+
| | | |
|
|
180
|
+
| | | .castdom/
|
|
181
|
+
| | | manifest.json
|
|
182
|
+
| | | castdom.css
|
|
183
|
+
| | | loader.js
|
|
184
|
+
| | | index.d.ts
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Bone Data Model
|
|
188
|
+
|
|
189
|
+
Each element becomes a **bone** — a minimal rectangle descriptor:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface Bone {
|
|
193
|
+
x: number; // X position relative to container
|
|
194
|
+
y: number; // Y position relative to container
|
|
195
|
+
w: number; // Width
|
|
196
|
+
h: number; // Height
|
|
197
|
+
r: number; // Border radius (9999 = circle)
|
|
198
|
+
kind?: BoneKind; // "text" | "heading" | "image" | "avatar" | "button" | ...
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Content-Aware Detection
|
|
203
|
+
|
|
204
|
+
CastDOM doesn't just make rectangles. It detects what each element **is**:
|
|
205
|
+
|
|
206
|
+
| Element | Detection | Skeleton Shape |
|
|
207
|
+
|---------|-----------|----------------|
|
|
208
|
+
| `<h1>`-`<h6>` | Tag name | Rounded rectangle, heading height |
|
|
209
|
+
| `<p>`, text nodes | Text content check | Multiple line rectangles |
|
|
210
|
+
| `<img>` | Tag name + aspect ratio | Rectangle with image proportions |
|
|
211
|
+
| Avatar | Small + `border-radius: 50%` | Circle |
|
|
212
|
+
| `<button>` | Tag or `role="button"` | Pill shape |
|
|
213
|
+
| `<input>` | Tag name | Bordered rectangle |
|
|
214
|
+
| `<hr>` | Tag name | Thin divider line |
|
|
215
|
+
|
|
216
|
+
### Compression
|
|
217
|
+
|
|
218
|
+
Bone data is compressed using delta encoding and zigzag varint packing:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
Raw JSON: 2,100 bytes
|
|
222
|
+
Compressed: 800 bytes (62% smaller)
|
|
223
|
+
Base64: 540 bytes (74% smaller)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The compression pipeline:
|
|
227
|
+
1. Sort bones top-to-bottom, left-to-right
|
|
228
|
+
2. Delta-encode X/Y positions
|
|
229
|
+
3. Quantize to half-pixels
|
|
230
|
+
4. Zigzag encode signed integers
|
|
231
|
+
5. Variable-length integer packing
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## React
|
|
236
|
+
|
|
237
|
+
### `<CastDOM>` Component
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
import { CastDOM } from "castdom/react";
|
|
241
|
+
|
|
242
|
+
<CastDOM
|
|
243
|
+
name="user-card" // Registered skeleton name
|
|
244
|
+
loading={isLoading} // Show skeleton when true
|
|
245
|
+
animation="shimmer" // "shimmer" | "pulse" | "wave" | "none"
|
|
246
|
+
color="#e0e0e0" // Base bone color
|
|
247
|
+
shimmerColor="#f0f0f0" // Shimmer highlight color
|
|
248
|
+
duration={1500} // Animation duration (ms)
|
|
249
|
+
className="my-skeleton" // Additional CSS class
|
|
250
|
+
onSkeletonShow={() => console.log("Skeleton visible")}
|
|
251
|
+
onContentShow={() => console.log("Content rendered")}
|
|
252
|
+
ariaLabel="Loading user profile"
|
|
253
|
+
>
|
|
254
|
+
<UserCard data={data} />
|
|
255
|
+
</CastDOM>
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `<CastDOMStyle>` Component
|
|
259
|
+
|
|
260
|
+
Add to your `<head>` for critical CSS:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
import { CastDOMStyle } from "castdom/react";
|
|
264
|
+
|
|
265
|
+
function Layout({ children }) {
|
|
266
|
+
return (
|
|
267
|
+
<html>
|
|
268
|
+
<head>
|
|
269
|
+
<CastDOMStyle />
|
|
270
|
+
</head>
|
|
271
|
+
<body>{children}</body>
|
|
272
|
+
</html>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### `useCastDOM` Hook
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
import { useCastDOM } from "castdom/react";
|
|
281
|
+
|
|
282
|
+
function CustomSkeleton() {
|
|
283
|
+
const { exists, data, breakpoint, css, html } = useCastDOM("user-card");
|
|
284
|
+
|
|
285
|
+
if (!exists) return <FallbackSkeleton />;
|
|
286
|
+
|
|
287
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} />;
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Next.js
|
|
294
|
+
|
|
295
|
+
### App Router
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
// app/layout.tsx
|
|
299
|
+
import { initCastDOM } from "castdom/next";
|
|
300
|
+
import manifest from "../.castdom/manifest.json";
|
|
301
|
+
|
|
302
|
+
initCastDOM(manifest);
|
|
303
|
+
|
|
304
|
+
export default function RootLayout({ children }) {
|
|
305
|
+
return (
|
|
306
|
+
<html>
|
|
307
|
+
<body>{children}</body>
|
|
308
|
+
</html>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
// app/dashboard/page.tsx
|
|
315
|
+
import { CastDOM } from "castdom/next";
|
|
316
|
+
|
|
317
|
+
export default async function Dashboard() {
|
|
318
|
+
const data = await fetchDashboard();
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<CastDOM name="dashboard" loading={!data}>
|
|
322
|
+
<DashboardContent data={data} />
|
|
323
|
+
</CastDOM>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Pages Router
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
// pages/_app.tsx
|
|
332
|
+
import { initCastDOM } from "castdom/next";
|
|
333
|
+
import manifest from "../.castdom/manifest.json";
|
|
334
|
+
|
|
335
|
+
initCastDOM(manifest);
|
|
336
|
+
|
|
337
|
+
export default function App({ Component, pageProps }) {
|
|
338
|
+
return <Component {...pageProps} />;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Server-Side Props
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
import { getSkeletonProps } from "castdom/next";
|
|
346
|
+
|
|
347
|
+
export async function getServerSideProps() {
|
|
348
|
+
const { skeletonHTML, skeletonCSS } = getSkeletonProps(["user-card"]);
|
|
349
|
+
return { props: { skeletonHTML, skeletonCSS } };
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Auto-Generated `loading.tsx`
|
|
354
|
+
|
|
355
|
+
After `npx castdom build`, a ready-to-use `loading.tsx` is generated at `.castdom/nextjs-loading.tsx`. Copy it to your route:
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
cp .castdom/nextjs-loading.tsx app/dashboard/loading.tsx
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Astro
|
|
364
|
+
|
|
365
|
+
### Integration
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
// astro.config.mjs
|
|
369
|
+
import { defineConfig } from "astro/config";
|
|
370
|
+
import { castdomIntegration } from "castdom/astro";
|
|
371
|
+
|
|
372
|
+
export default defineConfig({
|
|
373
|
+
integrations: [castdomIntegration()],
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Component Usage
|
|
378
|
+
|
|
379
|
+
```astro
|
|
380
|
+
---
|
|
381
|
+
import { skeleton } from "castdom/astro";
|
|
382
|
+
const { html, css } = skeleton("user-card");
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
<style set:html={css}></style>
|
|
386
|
+
<div data-castdom="user-card">
|
|
387
|
+
<Fragment set:html={html} />
|
|
388
|
+
<slot />
|
|
389
|
+
</div>
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### View Transitions
|
|
393
|
+
|
|
394
|
+
```astro
|
|
395
|
+
---
|
|
396
|
+
import { skeleton, viewTransitionProps } from "castdom/astro";
|
|
397
|
+
const { html } = skeleton("sidebar");
|
|
398
|
+
const transitionProps = viewTransitionProps("sidebar");
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
<div {...transitionProps}>
|
|
402
|
+
<Fragment set:html={html} />
|
|
403
|
+
</div>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Vite
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
// vite.config.ts
|
|
412
|
+
import { defineConfig } from "vite";
|
|
413
|
+
import { castdom } from "castdom/vite";
|
|
414
|
+
|
|
415
|
+
export default defineConfig({
|
|
416
|
+
plugins: [castdom()],
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Then import the virtual module:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import "virtual:castdom";
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The plugin provides:
|
|
427
|
+
- **HMR** — hot-reload when skeleton data changes
|
|
428
|
+
- **Virtual module** — `virtual:castdom` auto-loads the manifest
|
|
429
|
+
- **Dev middleware** — `/__castdom/extract` endpoint
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Vanilla JS
|
|
434
|
+
|
|
435
|
+
For non-framework usage:
|
|
436
|
+
|
|
437
|
+
```js
|
|
438
|
+
import { createCastDOM } from "castdom";
|
|
439
|
+
import manifest from ".castdom/manifest.json";
|
|
440
|
+
|
|
441
|
+
const castdom = createCastDOM();
|
|
442
|
+
castdom.loadManifest(manifest);
|
|
443
|
+
|
|
444
|
+
// Show skeleton
|
|
445
|
+
const container = document.getElementById("user-card");
|
|
446
|
+
castdom.show("user-card", container);
|
|
447
|
+
|
|
448
|
+
// Later, hide and show real content
|
|
449
|
+
castdom.hide("user-card", container);
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## SSR & SEO
|
|
455
|
+
|
|
456
|
+
CastDOM is SSR-first. Skeletons render as server-side HTML with zero JavaScript.
|
|
457
|
+
|
|
458
|
+
### Why This Matters for SEO
|
|
459
|
+
|
|
460
|
+
| Metric | Without CastDOM | With CastDOM |
|
|
461
|
+
|--------|-----------------|--------------|
|
|
462
|
+
| **CLS** | Layout shifts when content loads | Zero — skeletons match exact dimensions |
|
|
463
|
+
| **FCP** | Blank until JS loads | Instant — CSS-only skeletons |
|
|
464
|
+
| **LCP** | Delayed by data fetching | Skeleton paints immediately |
|
|
465
|
+
| **Accessibility** | No loading indication | `role="status"`, `aria-busy`, screen reader labels |
|
|
466
|
+
| **Crawler visibility** | Empty containers | Structured placeholders with ARIA |
|
|
467
|
+
|
|
468
|
+
### Server-Side Rendering
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
import { renderSkeleton, renderSSRFragment } from "castdom/ssr";
|
|
472
|
+
|
|
473
|
+
// Single skeleton
|
|
474
|
+
const html = renderSkeleton(skeletonData);
|
|
475
|
+
// Includes: <style> + skeleton HTML + hydration data attributes
|
|
476
|
+
|
|
477
|
+
// Multiple skeletons (shared CSS)
|
|
478
|
+
const { head, body } = renderSSRFragment([skeleton1, skeleton2]);
|
|
479
|
+
// head: <style> + <script> hydration
|
|
480
|
+
// body: { "user-card": "...", "feed-item": "..." }
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Hydration
|
|
484
|
+
|
|
485
|
+
SSR skeletons include `data-castdom-ssr` attributes. A tiny (~200 byte) inline hydration script automatically removes them once real content renders:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { renderHydrationScript } from "castdom/ssr";
|
|
489
|
+
|
|
490
|
+
const script = renderHydrationScript();
|
|
491
|
+
// <script data-castdom="hydration">(...)</script>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Critical CSS
|
|
495
|
+
|
|
496
|
+
```ts
|
|
497
|
+
import { renderCriticalStyleTag } from "castdom/ssr";
|
|
498
|
+
|
|
499
|
+
const styleTag = renderCriticalStyleTag([skeleton1, skeleton2]);
|
|
500
|
+
// <style data-castdom="critical">@keyframes castdom-shimmer{...}...</style>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## CLI Reference
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
castdom build [options] # Extract skeletons and generate files
|
|
509
|
+
castdom init # Create castdom.config.json template
|
|
510
|
+
castdom list # List skeletons in the manifest
|
|
511
|
+
castdom clean # Remove generated output directory
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Build Options
|
|
515
|
+
|
|
516
|
+
| Flag | Default | Description |
|
|
517
|
+
|------|---------|-------------|
|
|
518
|
+
| `--url <url>` | `http://localhost:3000` | Dev server URL |
|
|
519
|
+
| `--config <path>` | `castdom.config.json` | Config file path |
|
|
520
|
+
| `--out <dir>` | `.castdom` | Output directory |
|
|
521
|
+
| `--breakpoints <list>` | `375,768,1280` | Comma-separated breakpoints |
|
|
522
|
+
| `--no-headless` | `false` | Show browser UI (for debugging) |
|
|
523
|
+
| `--verbose` | `false` | Detailed progress output |
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Configuration
|
|
528
|
+
|
|
529
|
+
### `castdom.config.json`
|
|
530
|
+
|
|
531
|
+
```json
|
|
532
|
+
{
|
|
533
|
+
"devServer": "http://localhost:3000",
|
|
534
|
+
"outDir": ".castdom",
|
|
535
|
+
"breakpoints": [375, 768, 1280],
|
|
536
|
+
"color": "#e0e0e0",
|
|
537
|
+
"shimmerColor": "#f0f0f0",
|
|
538
|
+
"animationDuration": 1500,
|
|
539
|
+
"contentAware": true,
|
|
540
|
+
"minBoneSize": 4,
|
|
541
|
+
"targets": [
|
|
542
|
+
{
|
|
543
|
+
"name": "user-card",
|
|
544
|
+
"selector": "[data-castdom=\"user-card\"]",
|
|
545
|
+
"route": "/"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"name": "feed-item",
|
|
549
|
+
"selector": "[data-castdom=\"feed-item\"]",
|
|
550
|
+
"route": "/feed"
|
|
551
|
+
}
|
|
552
|
+
]
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
| Option | Type | Default | Description |
|
|
557
|
+
|--------|------|---------|-------------|
|
|
558
|
+
| `devServer` | `string` | `http://localhost:3000` | URL of your dev server |
|
|
559
|
+
| `outDir` | `string` | `.castdom` | Where to write generated files |
|
|
560
|
+
| `breakpoints` | `number[]` | `[375, 768, 1280]` | Viewport widths to capture |
|
|
561
|
+
| `color` | `string` | `#e0e0e0` | Base bone color |
|
|
562
|
+
| `shimmerColor` | `string` | `#f0f0f0` | Shimmer highlight color |
|
|
563
|
+
| `animationDuration` | `number` | `1500` | Animation cycle duration (ms) |
|
|
564
|
+
| `contentAware` | `boolean` | `true` | Detect element types for shaped bones |
|
|
565
|
+
| `minBoneSize` | `number` | `4` | Skip elements smaller than this (px) |
|
|
566
|
+
|
|
567
|
+
### Targets
|
|
568
|
+
|
|
569
|
+
Each target defines a component to extract:
|
|
570
|
+
|
|
571
|
+
| Field | Required | Description |
|
|
572
|
+
|-------|----------|-------------|
|
|
573
|
+
| `name` | Yes | Unique skeleton identifier |
|
|
574
|
+
| `selector` | Yes | CSS selector for the container element |
|
|
575
|
+
| `route` | No | Page route to navigate to before extracting |
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Animation Types
|
|
580
|
+
|
|
581
|
+
CastDOM supports four animation types, all CSS-only:
|
|
582
|
+
|
|
583
|
+
### Shimmer (default)
|
|
584
|
+
|
|
585
|
+
A gradient sweep from left to right. Classic skeleton animation.
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
<CastDOM name="card" animation="shimmer" />
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Pulse
|
|
592
|
+
|
|
593
|
+
Opacity fade between 100% and 40%.
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
<CastDOM name="card" animation="pulse" />
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Wave
|
|
600
|
+
|
|
601
|
+
Staggered opacity animation — each bone animates with a 50ms delay.
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
<CastDOM name="card" animation="wave" />
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### None
|
|
608
|
+
|
|
609
|
+
Static gray rectangles. Also used automatically when `prefers-reduced-motion: reduce` is active.
|
|
610
|
+
|
|
611
|
+
```tsx
|
|
612
|
+
<CastDOM name="card" animation="none" />
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## API Reference
|
|
618
|
+
|
|
619
|
+
### Core
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
import {
|
|
623
|
+
loadManifest, // Register all skeletons from generated manifest
|
|
624
|
+
register, // Register a single skeleton
|
|
625
|
+
get, // Get skeleton entry by name
|
|
626
|
+
has, // Check if skeleton exists
|
|
627
|
+
names, // List all registered skeleton names
|
|
628
|
+
configure, // Set global config (colors, animation, etc.)
|
|
629
|
+
getAllCSS, // Get combined CSS for all registered skeletons
|
|
630
|
+
extractBones, // Extract bones from a DOM element (browser-side)
|
|
631
|
+
compressBones, // Compress bone data for storage
|
|
632
|
+
decompressBones, // Decompress bone data
|
|
633
|
+
} from "castdom";
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### React
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
import {
|
|
640
|
+
CastDOM, // Wrapper component
|
|
641
|
+
CastDOMStyle, // Critical CSS component for <head>
|
|
642
|
+
useCastDOM, // Hook for programmatic access
|
|
643
|
+
} from "castdom/react";
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### SSR
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
import {
|
|
650
|
+
renderSkeleton, // Render skeleton to HTML string
|
|
651
|
+
renderSkeletons, // Render multiple with shared CSS
|
|
652
|
+
renderCriticalStyleTag, // Generate <style> tag
|
|
653
|
+
renderHydrationScript, // Generate hydration <script>
|
|
654
|
+
renderSSRFragment, // Complete head + body fragments
|
|
655
|
+
} from "castdom/ssr";
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Generated Output
|
|
661
|
+
|
|
662
|
+
After running `npx castdom build`, the `.castdom/` directory contains:
|
|
663
|
+
|
|
664
|
+
```
|
|
665
|
+
.castdom/
|
|
666
|
+
manifest.json # All skeleton data (register with loadManifest)
|
|
667
|
+
castdom.css # Critical CSS for all skeletons
|
|
668
|
+
index.js # ESM module with tree-shakeable exports
|
|
669
|
+
index.d.ts # TypeScript declarations
|
|
670
|
+
loader.js # Auto-registers all skeletons on import
|
|
671
|
+
nextjs-loading.tsx # Ready-to-use Next.js loading component
|
|
672
|
+
skeletons/
|
|
673
|
+
user-card.json # Individual skeleton data
|
|
674
|
+
user-card.js # Individual ESM export
|
|
675
|
+
feed-item.json
|
|
676
|
+
feed-item.js
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## TypeScript
|
|
682
|
+
|
|
683
|
+
CastDOM is written in TypeScript and ships full type declarations. After extraction, skeleton names are typed:
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// .castdom/index.d.ts (auto-generated)
|
|
687
|
+
export type SkeletonName = "user-card" | "feed-item";
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Browser Support
|
|
693
|
+
|
|
694
|
+
CastDOM's runtime is pure CSS — it works everywhere CSS animations work:
|
|
695
|
+
|
|
696
|
+
- Chrome 60+
|
|
697
|
+
- Firefox 60+
|
|
698
|
+
- Safari 12+
|
|
699
|
+
- Edge 79+
|
|
700
|
+
|
|
701
|
+
The extraction tool (CLI) requires Node.js 18+ and Playwright.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## License
|
|
706
|
+
|
|
707
|
+
MIT
|
package/bin/castdom.js
ADDED