@0xgf/boneyard 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/README.md +32 -0
- package/bin/boneyard.mjs +272 -0
- package/dist/extract.d.ts +15 -0
- package/dist/extract.js +380 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +57 -0
- package/dist/layout.d.ts +15 -0
- package/dist/layout.js +256 -0
- package/dist/react.d.ts +107 -0
- package/dist/react.js +146 -0
- package/dist/responsive.d.ts +34 -0
- package/dist/responsive.js +67 -0
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +38 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
package/dist/extract.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
const DEFAULT_LEAF_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'tr']);
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot the exact visual layout of a rendered DOM element as skeleton bones.
|
|
4
|
+
* Walks the DOM, finds every visible leaf element, and captures its exact
|
|
5
|
+
* bounding rect relative to the root. No layout engine, no heuristics —
|
|
6
|
+
* just pixel-perfect positions read directly from the browser.
|
|
7
|
+
*
|
|
8
|
+
* const { bones, width, height } = snapshotBones(el, 'my-component', config)
|
|
9
|
+
*/
|
|
10
|
+
export function snapshotBones(el, name = 'component', config) {
|
|
11
|
+
const rootRect = el.getBoundingClientRect();
|
|
12
|
+
const bones = [];
|
|
13
|
+
const leafTags = config?.leafTags ? new Set(config.leafTags) : DEFAULT_LEAF_TAGS;
|
|
14
|
+
const captureRoundedBorders = config?.captureRoundedBorders ?? true;
|
|
15
|
+
const excludeTags = config?.excludeTags ? new Set(config.excludeTags) : null;
|
|
16
|
+
const excludeSelectors = config?.excludeSelectors ?? null;
|
|
17
|
+
function walk(node) {
|
|
18
|
+
const style = getComputedStyle(node);
|
|
19
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')
|
|
20
|
+
return;
|
|
21
|
+
const tag = node.tagName.toLowerCase();
|
|
22
|
+
// Exclusions — skip element and all descendants
|
|
23
|
+
if (excludeTags?.has(tag))
|
|
24
|
+
return;
|
|
25
|
+
if (excludeSelectors?.some(sel => node.matches(sel)))
|
|
26
|
+
return;
|
|
27
|
+
const children = [...node.children].filter(child => {
|
|
28
|
+
const cs = getComputedStyle(child);
|
|
29
|
+
return cs.display !== 'none' && cs.visibility !== 'hidden' && cs.opacity !== '0';
|
|
30
|
+
});
|
|
31
|
+
const isMedia = tag === 'img' || tag === 'svg' || tag === 'video' || tag === 'canvas';
|
|
32
|
+
const isFormEl = tag === 'input' || tag === 'button' || tag === 'textarea' || tag === 'select';
|
|
33
|
+
const isLeaf = children.length === 0 || isMedia || isFormEl || leafTags.has(tag);
|
|
34
|
+
// Container emits a bone if it has any non-transparent background, a background image,
|
|
35
|
+
// or (when captureRoundedBorders is true) a visible border on a rounded element.
|
|
36
|
+
// Every background color counts — white cards are still cards.
|
|
37
|
+
const bg = style.backgroundColor;
|
|
38
|
+
const hasBg = bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent';
|
|
39
|
+
const hasBgImage = style.backgroundImage !== 'none';
|
|
40
|
+
const borderTopWidth = parseFloat(style.borderTopWidth) || 0;
|
|
41
|
+
const hasBorder = captureRoundedBorders && borderTopWidth > 0 && style.borderTopColor !== 'rgba(0, 0, 0, 0)' && style.borderTopColor !== 'transparent';
|
|
42
|
+
const hasBorderRadius = (parseFloat(style.borderTopLeftRadius) || 0) > 0;
|
|
43
|
+
const hasVisualSurface = hasBg || hasBgImage || (hasBorder && hasBorderRadius);
|
|
44
|
+
if (isLeaf) {
|
|
45
|
+
const rect = node.getBoundingClientRect();
|
|
46
|
+
if (rect.width < 1 || rect.height < 1)
|
|
47
|
+
return;
|
|
48
|
+
const br = parseBorderRadius(style, node);
|
|
49
|
+
bones.push({
|
|
50
|
+
x: Math.round(rect.left - rootRect.left),
|
|
51
|
+
y: Math.round(rect.top - rootRect.top),
|
|
52
|
+
w: Math.round(rect.width),
|
|
53
|
+
h: Math.round(rect.height),
|
|
54
|
+
r: br ?? 8,
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Container with visible background: emit a lighter bone (c: true) so that
|
|
59
|
+
// child bones rendered on top stand out against it, then recurse.
|
|
60
|
+
if (hasVisualSurface) {
|
|
61
|
+
const rect = node.getBoundingClientRect();
|
|
62
|
+
if (rect.width >= 1 && rect.height >= 1) {
|
|
63
|
+
const br = parseBorderRadius(style, node);
|
|
64
|
+
bones.push({
|
|
65
|
+
x: Math.round(rect.left - rootRect.left),
|
|
66
|
+
y: Math.round(rect.top - rootRect.top),
|
|
67
|
+
w: Math.round(rect.width),
|
|
68
|
+
h: Math.round(rect.height),
|
|
69
|
+
r: br ?? 8,
|
|
70
|
+
c: true, // container bone — rendered at reduced opacity
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Recurse into children regardless — their bones overlay the container bone
|
|
75
|
+
for (const child of children) {
|
|
76
|
+
walk(child);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Don't snapshot the root itself — walk its children
|
|
80
|
+
for (const child of el.children) {
|
|
81
|
+
walk(child);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
viewportWidth: Math.round(rootRect.width),
|
|
86
|
+
width: Math.round(rootRect.width),
|
|
87
|
+
height: Math.round(rootRect.height),
|
|
88
|
+
bones,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extract a SkeletonDescriptor from a rendered DOM element.
|
|
93
|
+
* Reads computed styles — no config needed. Just point it at your component.
|
|
94
|
+
*/
|
|
95
|
+
export function fromElement(el) {
|
|
96
|
+
return extractNode(el);
|
|
97
|
+
}
|
|
98
|
+
function extractNode(el) {
|
|
99
|
+
const style = getComputedStyle(el);
|
|
100
|
+
const desc = {};
|
|
101
|
+
// Bail on layouts we can't recompute — snapshot as fixed-size leaf
|
|
102
|
+
if (style.display === 'grid' || style.display === 'inline-grid') {
|
|
103
|
+
return snapshotAsLeaf(el, style, desc);
|
|
104
|
+
}
|
|
105
|
+
if (style.position === 'absolute' || style.position === 'fixed') {
|
|
106
|
+
return snapshotAsLeaf(el, style, desc);
|
|
107
|
+
}
|
|
108
|
+
// Layout
|
|
109
|
+
if (style.display === 'flex' || style.display === 'inline-flex') {
|
|
110
|
+
desc.display = 'flex';
|
|
111
|
+
desc.flexDirection = (style.flexDirection === 'column' || style.flexDirection === 'column-reverse') ? 'column' : 'row';
|
|
112
|
+
if (style.alignItems && style.alignItems !== 'normal' && style.alignItems !== 'stretch')
|
|
113
|
+
desc.alignItems = style.alignItems;
|
|
114
|
+
if (style.justifyContent && style.justifyContent !== 'normal' && style.justifyContent !== 'flex-start')
|
|
115
|
+
desc.justifyContent = style.justifyContent;
|
|
116
|
+
// Read row-gap and column-gap separately (Tailwind gap-x-*, gap-y-*)
|
|
117
|
+
const rowGap = parseFloat(style.rowGap);
|
|
118
|
+
const colGap = parseFloat(style.columnGap);
|
|
119
|
+
if (rowGap > 0 && colGap > 0 && rowGap === colGap) {
|
|
120
|
+
desc.gap = rowGap;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
if (rowGap > 0)
|
|
124
|
+
desc.rowGap = rowGap;
|
|
125
|
+
if (colGap > 0)
|
|
126
|
+
desc.columnGap = colGap;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Spacing
|
|
130
|
+
const pad = extractSides(style, 'padding');
|
|
131
|
+
if (pad)
|
|
132
|
+
desc.padding = pad;
|
|
133
|
+
const mar = extractSides(style, 'margin');
|
|
134
|
+
if (mar)
|
|
135
|
+
desc.margin = mar;
|
|
136
|
+
// Border radius
|
|
137
|
+
const br = parseBorderRadius(style, el);
|
|
138
|
+
if (br !== undefined)
|
|
139
|
+
desc.borderRadius = br;
|
|
140
|
+
// Max width
|
|
141
|
+
const maxW = parseFloat(style.maxWidth);
|
|
142
|
+
if (maxW > 0 && isFinite(maxW))
|
|
143
|
+
desc.maxWidth = maxW;
|
|
144
|
+
// Measure element dimensions
|
|
145
|
+
const rect = el.getBoundingClientRect();
|
|
146
|
+
const w = rect.width;
|
|
147
|
+
const h = rect.height;
|
|
148
|
+
const parentW = el.parentElement ? el.parentElement.getBoundingClientRect().width : w;
|
|
149
|
+
// Leaf detection — width is handled inside extractLeaf per element type
|
|
150
|
+
if (isLeafElement(el, style)) {
|
|
151
|
+
return extractLeaf(el, style, desc, w, h, parentW);
|
|
152
|
+
}
|
|
153
|
+
// Fixed-size containers need explicit width so the layout engine doesn't stretch them
|
|
154
|
+
if (isFixedSize(el, style, w, parentW) && w > 0) {
|
|
155
|
+
desc.width = Math.round(w);
|
|
156
|
+
}
|
|
157
|
+
// Container: recurse into children
|
|
158
|
+
const children = [];
|
|
159
|
+
for (const child of el.children) {
|
|
160
|
+
const childStyle = getComputedStyle(child);
|
|
161
|
+
if (childStyle.display === 'none' || childStyle.visibility === 'hidden' || childStyle.opacity === '0')
|
|
162
|
+
continue;
|
|
163
|
+
children.push(extractNode(child));
|
|
164
|
+
}
|
|
165
|
+
if (children.length > 0)
|
|
166
|
+
desc.children = children;
|
|
167
|
+
return desc;
|
|
168
|
+
}
|
|
169
|
+
/** Is this element explicitly sized (not stretching to fill its parent)? */
|
|
170
|
+
function isFixedSize(el, style, w, parentW) {
|
|
171
|
+
// flex-grow > 0 means the element is designed to stretch — never treat as fixed
|
|
172
|
+
if (parseFloat(style.flexGrow) > 0)
|
|
173
|
+
return false;
|
|
174
|
+
// If flex-shrink: 0, it's explicitly sized (e.g. shrink-0 avatars, icons)
|
|
175
|
+
if (style.flexShrink === '0')
|
|
176
|
+
return true;
|
|
177
|
+
// Check if the parent is a flex row — if so, only treat as fixed if the
|
|
178
|
+
// element doesn't participate in flex distribution
|
|
179
|
+
const parent = el.parentElement;
|
|
180
|
+
if (parent) {
|
|
181
|
+
const parentStyle = getComputedStyle(parent);
|
|
182
|
+
const parentIsFlex = parentStyle.display === 'flex' || parentStyle.display === 'inline-flex';
|
|
183
|
+
const parentIsRow = parentIsFlex && (parentStyle.flexDirection === 'row' || parentStyle.flexDirection === 'row-reverse');
|
|
184
|
+
if (parentIsRow) {
|
|
185
|
+
// In a flex row, only fixed if flex-basis is explicit or flex-shrink: 0
|
|
186
|
+
// Default flex items can grow/shrink, so don't assume fixed
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Block-level element that's narrower than parent — likely has explicit width
|
|
191
|
+
if (w > 0 && parentW > 0 && w < parentW * 0.8)
|
|
192
|
+
return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
function isLeafElement(el, style) {
|
|
196
|
+
const tag = el.tagName.toLowerCase();
|
|
197
|
+
if (tag === 'img' || tag === 'video' || tag === 'canvas' || tag === 'svg')
|
|
198
|
+
return true;
|
|
199
|
+
if (tag === 'input' || tag === 'button' || tag === 'textarea' || tag === 'select')
|
|
200
|
+
return true;
|
|
201
|
+
if (el.children.length === 0)
|
|
202
|
+
return true;
|
|
203
|
+
if (style.backgroundImage && style.backgroundImage !== 'none' && !el.querySelector('*:not(br)'))
|
|
204
|
+
return true;
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
function extractLeaf(el, style, desc, w, h, parentW) {
|
|
208
|
+
const tag = el.tagName.toLowerCase();
|
|
209
|
+
// Images and media
|
|
210
|
+
if (tag === 'img' || tag === 'video' || tag === 'canvas') {
|
|
211
|
+
return applyDimensions(el, style, desc, w, h, parentW);
|
|
212
|
+
}
|
|
213
|
+
// SVG icons
|
|
214
|
+
if (tag === 'svg') {
|
|
215
|
+
if (!desc.width && w > 0)
|
|
216
|
+
desc.width = Math.round(w);
|
|
217
|
+
desc.height = Math.round(h > 0 ? h : 24);
|
|
218
|
+
return desc;
|
|
219
|
+
}
|
|
220
|
+
// Background image/gradient containers (hero images, avatars)
|
|
221
|
+
if (style.backgroundImage && style.backgroundImage !== 'none' && el.children.length === 0) {
|
|
222
|
+
return applyDimensions(el, style, desc, w, h, parentW);
|
|
223
|
+
}
|
|
224
|
+
// Buttons and inputs
|
|
225
|
+
if (tag === 'button' || tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
226
|
+
desc.leaf = true;
|
|
227
|
+
desc.height = Math.round(h > 0 ? h : 40);
|
|
228
|
+
return desc;
|
|
229
|
+
}
|
|
230
|
+
// Text nodes
|
|
231
|
+
const text = el.textContent?.trim();
|
|
232
|
+
if (text) {
|
|
233
|
+
desc.text = text;
|
|
234
|
+
desc.font = buildFontString(style);
|
|
235
|
+
const lh = parseFloat(style.lineHeight);
|
|
236
|
+
if (lh > 0 && isFinite(lh))
|
|
237
|
+
desc.lineHeight = Math.round(lh * 100) / 100;
|
|
238
|
+
return desc;
|
|
239
|
+
}
|
|
240
|
+
// Fallback — capture both dimensions for fixed-size empty elements
|
|
241
|
+
if (isFixedSize(el, style, w, parentW) && w > 0) {
|
|
242
|
+
desc.width = Math.round(w);
|
|
243
|
+
}
|
|
244
|
+
desc.height = Math.round(h > 0 ? h : 20);
|
|
245
|
+
return desc;
|
|
246
|
+
}
|
|
247
|
+
/** Apply width/height or aspectRatio depending on whether element is full-width or fixed-size */
|
|
248
|
+
function applyDimensions(el, style, desc, w, h, parentW) {
|
|
249
|
+
// Check CSS aspect-ratio first
|
|
250
|
+
const ar = style.aspectRatio;
|
|
251
|
+
if (ar && ar !== 'auto') {
|
|
252
|
+
const parsed = parseAspectRatio(ar);
|
|
253
|
+
if (parsed) {
|
|
254
|
+
desc.aspectRatio = parsed;
|
|
255
|
+
return desc;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Fixed-size element (avatar, icon) — use explicit dimensions
|
|
259
|
+
if (desc.width || isFixedSize(el, style, w, parentW)) {
|
|
260
|
+
if (!desc.width && w > 0)
|
|
261
|
+
desc.width = Math.round(w);
|
|
262
|
+
desc.height = Math.round(h > 0 ? h : w);
|
|
263
|
+
return desc;
|
|
264
|
+
}
|
|
265
|
+
// Full-width responsive element — use aspect ratio
|
|
266
|
+
if (w > 0 && h > 0) {
|
|
267
|
+
desc.aspectRatio = Math.round((w / h) * 1000) / 1000;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
desc.height = Math.round(h > 0 ? h : 150);
|
|
271
|
+
}
|
|
272
|
+
return desc;
|
|
273
|
+
}
|
|
274
|
+
function buildFontString(style) {
|
|
275
|
+
const weight = style.fontWeight;
|
|
276
|
+
const size = style.fontSize;
|
|
277
|
+
const family = style.fontFamily.split(',')[0].trim().replace(/^["']|["']$/g, '');
|
|
278
|
+
return `${weight} ${size} ${family}`;
|
|
279
|
+
}
|
|
280
|
+
function extractSides(style, prop) {
|
|
281
|
+
const top = parseFloat(style.getPropertyValue(`${prop}-top`)) || 0;
|
|
282
|
+
const right = parseFloat(style.getPropertyValue(`${prop}-right`)) || 0;
|
|
283
|
+
const bottom = parseFloat(style.getPropertyValue(`${prop}-bottom`)) || 0;
|
|
284
|
+
const left = parseFloat(style.getPropertyValue(`${prop}-left`)) || 0;
|
|
285
|
+
if (top === 0 && right === 0 && bottom === 0 && left === 0)
|
|
286
|
+
return undefined;
|
|
287
|
+
if (top === right && right === bottom && bottom === left)
|
|
288
|
+
return top;
|
|
289
|
+
const sides = {};
|
|
290
|
+
if (top)
|
|
291
|
+
sides.top = top;
|
|
292
|
+
if (right)
|
|
293
|
+
sides.right = right;
|
|
294
|
+
if (bottom)
|
|
295
|
+
sides.bottom = bottom;
|
|
296
|
+
if (left)
|
|
297
|
+
sides.left = left;
|
|
298
|
+
return sides;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Parse border-radius from computed style, preserving per-corner values.
|
|
302
|
+
* Returns: '50%' for circles, a number for uniform radius,
|
|
303
|
+
* or a CSS string like '8px 8px 0px 8px' for asymmetric corners.
|
|
304
|
+
*
|
|
305
|
+
* Important distinction:
|
|
306
|
+
* - `border-radius: 50%` on a square → circle → returns '50%'
|
|
307
|
+
* - `border-radius: 9999px` on a rectangle → pill → returns 9999 (large number)
|
|
308
|
+
* - `border-radius: 50%` on a rectangle → oval → returns '50%'
|
|
309
|
+
*/
|
|
310
|
+
function parseBorderRadius(style, el) {
|
|
311
|
+
const tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
312
|
+
const tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
313
|
+
const br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
314
|
+
const bl = parseFloat(style.borderBottomLeftRadius) || 0;
|
|
315
|
+
// No radius
|
|
316
|
+
if (tl === 0 && tr === 0 && br === 0 && bl === 0)
|
|
317
|
+
return undefined;
|
|
318
|
+
// Detect if element is roughly square (circle candidate)
|
|
319
|
+
const isSquarish = el ? (() => {
|
|
320
|
+
const rect = el.getBoundingClientRect();
|
|
321
|
+
return rect.width > 0 && rect.height > 0 && Math.abs(rect.width - rect.height) < 4;
|
|
322
|
+
})() : false;
|
|
323
|
+
// Check for 50% (actual percentage-based)
|
|
324
|
+
const raw = style.borderRadius;
|
|
325
|
+
if (raw === '50%')
|
|
326
|
+
return '50%';
|
|
327
|
+
// Large pixel values (9999px / rounded-full): circle if square, pill if rectangle
|
|
328
|
+
const maxCorner = Math.max(tl, tr, br, bl);
|
|
329
|
+
if (maxCorner > 9998) {
|
|
330
|
+
return isSquarish ? '50%' : 9999;
|
|
331
|
+
}
|
|
332
|
+
// All corners equal — return single number (skip if default 8)
|
|
333
|
+
if (tl === tr && tr === br && br === bl) {
|
|
334
|
+
return tl !== 8 ? tl : undefined;
|
|
335
|
+
}
|
|
336
|
+
// Asymmetric corners — return full CSS string
|
|
337
|
+
return `${tl}px ${tr}px ${br}px ${bl}px`;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Snapshot an element we can't recompute layout for (grid, absolute, etc.)
|
|
341
|
+
* Captures bounding box and recurses into children that ARE supported,
|
|
342
|
+
* positioning them relative to this element's bounds.
|
|
343
|
+
*/
|
|
344
|
+
function snapshotAsLeaf(el, style, desc) {
|
|
345
|
+
const rect = el.getBoundingClientRect();
|
|
346
|
+
desc.width = Math.round(rect.width);
|
|
347
|
+
desc.height = Math.round(rect.height);
|
|
348
|
+
// Border radius
|
|
349
|
+
const br = parseBorderRadius(style, el);
|
|
350
|
+
if (br !== undefined)
|
|
351
|
+
desc.borderRadius = br;
|
|
352
|
+
// If it has children, try to extract them — we just fix this node's own dimensions
|
|
353
|
+
// so the layout engine treats it as a fixed-size container
|
|
354
|
+
const children = [];
|
|
355
|
+
for (const child of el.children) {
|
|
356
|
+
const childStyle = getComputedStyle(child);
|
|
357
|
+
if (childStyle.display === 'none' || childStyle.visibility === 'hidden' || childStyle.opacity === '0')
|
|
358
|
+
continue;
|
|
359
|
+
children.push(extractNode(child));
|
|
360
|
+
}
|
|
361
|
+
if (children.length > 0) {
|
|
362
|
+
desc.display = 'flex';
|
|
363
|
+
desc.flexDirection = 'column';
|
|
364
|
+
desc.children = children;
|
|
365
|
+
}
|
|
366
|
+
return desc;
|
|
367
|
+
}
|
|
368
|
+
function parseAspectRatio(ar) {
|
|
369
|
+
const parts = ar.split('/');
|
|
370
|
+
if (parts.length === 2) {
|
|
371
|
+
const num = parseFloat(parts[0]);
|
|
372
|
+
const den = parseFloat(parts[1]);
|
|
373
|
+
if (num > 0 && den > 0)
|
|
374
|
+
return Math.round((num / den) * 1000) / 1000;
|
|
375
|
+
}
|
|
376
|
+
const val = parseFloat(ar);
|
|
377
|
+
if (val > 0 && isFinite(val))
|
|
378
|
+
return val;
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type { Bone, SkeletonResult, ResponsiveBones, SkeletonDescriptor, ResponsiveDescriptor, SnapshotConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Snapshot exact pixel positions of a rendered element as skeleton bones.
|
|
4
|
+
* Reads `getBoundingClientRect()` on every visible element — no simulation,
|
|
5
|
+
* just what the browser already computed.
|
|
6
|
+
*
|
|
7
|
+
* Use this to pre-generate bones at dev time, save as JSON, and pass as
|
|
8
|
+
* `initialBones` to `<Skeleton>` for zero first-load flash.
|
|
9
|
+
*
|
|
10
|
+
* @example In a browser console or dev script:
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { snapshotBones } from '@0xgf/boneyard'
|
|
13
|
+
* const bones = snapshotBones(document.querySelector('.my-card'))
|
|
14
|
+
* console.log(JSON.stringify(bones, null, 2))
|
|
15
|
+
* // → paste into my-card.bones.json
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export { snapshotBones } from './extract.js';
|
|
19
|
+
/**
|
|
20
|
+
* Extract a skeleton descriptor from a rendered DOM element.
|
|
21
|
+
* Captures layout structure as plain JSON — use with `computeLayout` for
|
|
22
|
+
* SSR or build-time paths where a live browser DOM isn't available.
|
|
23
|
+
*/
|
|
24
|
+
export { fromElement } from './extract.js';
|
|
25
|
+
/**
|
|
26
|
+
* Extract responsive descriptors at multiple breakpoints.
|
|
27
|
+
* Resizes the container to each width, extracts a descriptor per breakpoint.
|
|
28
|
+
* Returns a `ResponsiveDescriptor` you can serialize and ship as JSON.
|
|
29
|
+
*/
|
|
30
|
+
export { extractResponsive } from './responsive.js';
|
|
31
|
+
/**
|
|
32
|
+
* Compute skeleton bone positions from a descriptor at a given width.
|
|
33
|
+
* Uses @chenglou/pretext for text measurement — no DOM needed.
|
|
34
|
+
* Ideal for SSR or workers where `snapshotBones` isn't available.
|
|
35
|
+
*/
|
|
36
|
+
export { computeLayout } from './layout.js';
|
|
37
|
+
/**
|
|
38
|
+
* Render a `SkeletonResult` to an HTML string.
|
|
39
|
+
* Use for `innerHTML`, SSR, edge functions, etc.
|
|
40
|
+
*/
|
|
41
|
+
export { renderBones } from './runtime.js';
|
|
42
|
+
/**
|
|
43
|
+
* All-in-one convenience: extract descriptor → compute layout → render HTML.
|
|
44
|
+
*
|
|
45
|
+
* container.innerHTML = skeleton(element)
|
|
46
|
+
*
|
|
47
|
+
* For React, prefer `<Skeleton>` from '@0xgf/boneyard/react' — it calls
|
|
48
|
+
* `snapshotBones()` directly and handles caching automatically.
|
|
49
|
+
*/
|
|
50
|
+
export declare function skeleton(el: Element, options?: {
|
|
51
|
+
width?: number;
|
|
52
|
+
color?: string;
|
|
53
|
+
animate?: boolean;
|
|
54
|
+
}): string;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fromElement } from './extract.js';
|
|
2
|
+
import { computeLayout } from './layout.js';
|
|
3
|
+
import { renderBones } from './runtime.js';
|
|
4
|
+
/**
|
|
5
|
+
* Snapshot exact pixel positions of a rendered element as skeleton bones.
|
|
6
|
+
* Reads `getBoundingClientRect()` on every visible element — no simulation,
|
|
7
|
+
* just what the browser already computed.
|
|
8
|
+
*
|
|
9
|
+
* Use this to pre-generate bones at dev time, save as JSON, and pass as
|
|
10
|
+
* `initialBones` to `<Skeleton>` for zero first-load flash.
|
|
11
|
+
*
|
|
12
|
+
* @example In a browser console or dev script:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { snapshotBones } from '@0xgf/boneyard'
|
|
15
|
+
* const bones = snapshotBones(document.querySelector('.my-card'))
|
|
16
|
+
* console.log(JSON.stringify(bones, null, 2))
|
|
17
|
+
* // → paste into my-card.bones.json
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export { snapshotBones } from './extract.js';
|
|
21
|
+
/**
|
|
22
|
+
* Extract a skeleton descriptor from a rendered DOM element.
|
|
23
|
+
* Captures layout structure as plain JSON — use with `computeLayout` for
|
|
24
|
+
* SSR or build-time paths where a live browser DOM isn't available.
|
|
25
|
+
*/
|
|
26
|
+
export { fromElement } from './extract.js';
|
|
27
|
+
/**
|
|
28
|
+
* Extract responsive descriptors at multiple breakpoints.
|
|
29
|
+
* Resizes the container to each width, extracts a descriptor per breakpoint.
|
|
30
|
+
* Returns a `ResponsiveDescriptor` you can serialize and ship as JSON.
|
|
31
|
+
*/
|
|
32
|
+
export { extractResponsive } from './responsive.js';
|
|
33
|
+
/**
|
|
34
|
+
* Compute skeleton bone positions from a descriptor at a given width.
|
|
35
|
+
* Uses @chenglou/pretext for text measurement — no DOM needed.
|
|
36
|
+
* Ideal for SSR or workers where `snapshotBones` isn't available.
|
|
37
|
+
*/
|
|
38
|
+
export { computeLayout } from './layout.js';
|
|
39
|
+
/**
|
|
40
|
+
* Render a `SkeletonResult` to an HTML string.
|
|
41
|
+
* Use for `innerHTML`, SSR, edge functions, etc.
|
|
42
|
+
*/
|
|
43
|
+
export { renderBones } from './runtime.js';
|
|
44
|
+
/**
|
|
45
|
+
* All-in-one convenience: extract descriptor → compute layout → render HTML.
|
|
46
|
+
*
|
|
47
|
+
* container.innerHTML = skeleton(element)
|
|
48
|
+
*
|
|
49
|
+
* For React, prefer `<Skeleton>` from '@0xgf/boneyard/react' — it calls
|
|
50
|
+
* `snapshotBones()` directly and handles caching automatically.
|
|
51
|
+
*/
|
|
52
|
+
export function skeleton(el, options) {
|
|
53
|
+
const structure = fromElement(el);
|
|
54
|
+
const w = options?.width ?? el.getBoundingClientRect().width;
|
|
55
|
+
const result = computeLayout(structure, w);
|
|
56
|
+
return renderBones(result, options?.color, options?.animate);
|
|
57
|
+
}
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout engine — uses @chenglou/pretext for exact text measurement.
|
|
3
|
+
*
|
|
4
|
+
* Takes a SkeletonDescriptor (developer-defined component structure) and a
|
|
5
|
+
* width, then computes pixel-perfect bone positions using pretext for text
|
|
6
|
+
* and box-model arithmetic for containers.
|
|
7
|
+
*
|
|
8
|
+
* No DOM, no puppeteer, no build step. Describe your component, get bones.
|
|
9
|
+
*/
|
|
10
|
+
import type { SkeletonDescriptor, SkeletonResult } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Compute skeleton bones from a descriptor at a given width.
|
|
13
|
+
* Uses pretext for all text measurement — no DOM needed.
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeLayout(desc: SkeletonDescriptor, width: number, name?: string): SkeletonResult;
|