hyperframes 0.4.23 → 0.5.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1538 -696
- package/dist/commands/layout-audit.browser.js +423 -0
- package/dist/skills/hyperframes/SKILL.md +17 -0
- package/dist/skills/hyperframes-cli/SKILL.md +26 -4
- package/dist/studio/assets/index-Bi30tos-.js +105 -0
- package/dist/studio/assets/index-Dm9VsShj.css +1 -0
- package/dist/studio/index.html +2 -2
- package/package.json +1 -1
- package/dist/studio/assets/index-D0VntLIQ.js +0 -115
- package/dist/studio/assets/index-kT65pCwW.css +0 -1
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const IGNORE_TAGS = new Set(["SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "META", "LINK"]);
|
|
3
|
+
|
|
4
|
+
function toRect(rect) {
|
|
5
|
+
return {
|
|
6
|
+
left: round(rect.left),
|
|
7
|
+
top: round(rect.top),
|
|
8
|
+
right: round(rect.right),
|
|
9
|
+
bottom: round(rect.bottom),
|
|
10
|
+
width: round(rect.width),
|
|
11
|
+
height: round(rect.height),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function rectFromOrigin(left, top, width, height) {
|
|
16
|
+
return {
|
|
17
|
+
left: round(left),
|
|
18
|
+
top: round(top),
|
|
19
|
+
right: round(left + width),
|
|
20
|
+
bottom: round(top + height),
|
|
21
|
+
width: round(width),
|
|
22
|
+
height: round(height),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function round(value) {
|
|
27
|
+
return Math.round(value * 100) / 100;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function overflowFor(subject, container, tolerance) {
|
|
31
|
+
const overflow = {};
|
|
32
|
+
if (subject.left < container.left - tolerance)
|
|
33
|
+
overflow.left = round(container.left - subject.left);
|
|
34
|
+
if (subject.right > container.right + tolerance)
|
|
35
|
+
overflow.right = round(subject.right - container.right);
|
|
36
|
+
if (subject.top < container.top - tolerance) overflow.top = round(container.top - subject.top);
|
|
37
|
+
if (subject.bottom > container.bottom + tolerance)
|
|
38
|
+
overflow.bottom = round(subject.bottom - container.bottom);
|
|
39
|
+
return Object.keys(overflow).length > 0 ? overflow : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeCss(value) {
|
|
43
|
+
if (window.CSS && typeof window.CSS.escape === "function") return window.CSS.escape(value);
|
|
44
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function escapeAttr(value) {
|
|
48
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function selectorFor(element) {
|
|
52
|
+
if (element.id) return `#${escapeCss(element.id)}`;
|
|
53
|
+
const dataName =
|
|
54
|
+
element.getAttribute("data-layout-name") ||
|
|
55
|
+
element.getAttribute("data-composition-id") ||
|
|
56
|
+
element.getAttribute("data-start");
|
|
57
|
+
if (dataName) {
|
|
58
|
+
const attr = element.hasAttribute("data-layout-name")
|
|
59
|
+
? "data-layout-name"
|
|
60
|
+
: element.hasAttribute("data-composition-id")
|
|
61
|
+
? "data-composition-id"
|
|
62
|
+
: "data-start";
|
|
63
|
+
const attrSelector = `[${attr}="${escapeAttr(dataName)}"]`;
|
|
64
|
+
if (document.querySelectorAll(attrSelector).length === 1) return attrSelector;
|
|
65
|
+
return `${element.tagName.toLowerCase()}${attrSelector}`;
|
|
66
|
+
}
|
|
67
|
+
const classes = Array.from(element.classList).slice(0, 2);
|
|
68
|
+
if (classes.length > 0) {
|
|
69
|
+
return `${element.tagName.toLowerCase()}.${classes.map(escapeCss).join(".")}`;
|
|
70
|
+
}
|
|
71
|
+
const parent = element.parentElement;
|
|
72
|
+
if (!parent) return element.tagName.toLowerCase();
|
|
73
|
+
const siblings = Array.from(parent.children).filter(
|
|
74
|
+
(child) => child.tagName === element.tagName,
|
|
75
|
+
);
|
|
76
|
+
const index = siblings.indexOf(element) + 1;
|
|
77
|
+
return `${selectorFor(parent)} > ${element.tagName.toLowerCase()}:nth-of-type(${index})`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasIgnoreFlag(element) {
|
|
81
|
+
return !!element.closest("[data-layout-ignore], [data-layout-check='ignore']");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasAllowOverflowFlag(element) {
|
|
85
|
+
return !!element.closest("[data-layout-allow-overflow]");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function opacityChain(element) {
|
|
89
|
+
let opacity = 1;
|
|
90
|
+
for (let current = element; current; current = current.parentElement) {
|
|
91
|
+
const parsed = Number.parseFloat(getComputedStyle(current).opacity || "1");
|
|
92
|
+
if (Number.isFinite(parsed)) opacity *= parsed;
|
|
93
|
+
}
|
|
94
|
+
return opacity;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isVisibleElement(element) {
|
|
98
|
+
if (IGNORE_TAGS.has(element.tagName)) return false;
|
|
99
|
+
if (hasIgnoreFlag(element)) return false;
|
|
100
|
+
const style = getComputedStyle(element);
|
|
101
|
+
if (
|
|
102
|
+
style.display === "none" ||
|
|
103
|
+
style.visibility === "hidden" ||
|
|
104
|
+
style.visibility === "collapse"
|
|
105
|
+
) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (opacityChain(element) < 0.2) return false;
|
|
109
|
+
const rect = element.getBoundingClientRect();
|
|
110
|
+
return rect.width > 0.5 && rect.height > 0.5;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function textContentFor(element) {
|
|
114
|
+
return (element.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasOwnTextCandidate(element) {
|
|
118
|
+
const text = textContentFor(element);
|
|
119
|
+
if (!text) return false;
|
|
120
|
+
for (const child of Array.from(element.children)) {
|
|
121
|
+
if (isVisibleElement(child) && textContentFor(child)) return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function textRectFor(element) {
|
|
127
|
+
const range = document.createRange();
|
|
128
|
+
range.selectNodeContents(element);
|
|
129
|
+
const rects = Array.from(range.getClientRects()).filter(
|
|
130
|
+
(rect) => rect.width > 0.5 && rect.height > 0.5,
|
|
131
|
+
);
|
|
132
|
+
range.detach();
|
|
133
|
+
if (rects.length === 0) return null;
|
|
134
|
+
|
|
135
|
+
const union = rects.reduce(
|
|
136
|
+
(acc, rect) => ({
|
|
137
|
+
left: Math.min(acc.left, rect.left),
|
|
138
|
+
top: Math.min(acc.top, rect.top),
|
|
139
|
+
right: Math.max(acc.right, rect.right),
|
|
140
|
+
bottom: Math.max(acc.bottom, rect.bottom),
|
|
141
|
+
}),
|
|
142
|
+
{
|
|
143
|
+
left: Number.POSITIVE_INFINITY,
|
|
144
|
+
top: Number.POSITIVE_INFINITY,
|
|
145
|
+
right: Number.NEGATIVE_INFINITY,
|
|
146
|
+
bottom: Number.NEGATIVE_INFINITY,
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return toRect({
|
|
151
|
+
...union,
|
|
152
|
+
width: union.right - union.left,
|
|
153
|
+
height: union.bottom - union.top,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parsePx(value) {
|
|
158
|
+
const parsed = Number.parseFloat(value);
|
|
159
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function hasMeaningfulBoxStyle(style) {
|
|
163
|
+
return (
|
|
164
|
+
parsePx(style.paddingTop) +
|
|
165
|
+
parsePx(style.paddingRight) +
|
|
166
|
+
parsePx(style.paddingBottom) +
|
|
167
|
+
parsePx(style.paddingLeft) +
|
|
168
|
+
parsePx(style.borderTopWidth) +
|
|
169
|
+
parsePx(style.borderRightWidth) +
|
|
170
|
+
parsePx(style.borderBottomWidth) +
|
|
171
|
+
parsePx(style.borderLeftWidth) +
|
|
172
|
+
parsePx(style.borderTopLeftRadius) +
|
|
173
|
+
parsePx(style.borderTopRightRadius) +
|
|
174
|
+
parsePx(style.borderBottomRightRadius) +
|
|
175
|
+
parsePx(style.borderBottomLeftRadius) >
|
|
176
|
+
0
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasPaint(style) {
|
|
181
|
+
const backgroundColor = style.backgroundColor || "";
|
|
182
|
+
const hasBackground =
|
|
183
|
+
backgroundColor !== "" &&
|
|
184
|
+
backgroundColor !== "transparent" &&
|
|
185
|
+
!backgroundColor.endsWith(", 0)") &&
|
|
186
|
+
backgroundColor !== "rgba(0, 0, 0, 0)";
|
|
187
|
+
const hasImage = style.backgroundImage && style.backgroundImage !== "none";
|
|
188
|
+
const hasBorder =
|
|
189
|
+
parsePx(style.borderTopWidth) +
|
|
190
|
+
parsePx(style.borderRightWidth) +
|
|
191
|
+
parsePx(style.borderBottomWidth) +
|
|
192
|
+
parsePx(style.borderLeftWidth) >
|
|
193
|
+
0;
|
|
194
|
+
const hasRadius =
|
|
195
|
+
parsePx(style.borderTopLeftRadius) +
|
|
196
|
+
parsePx(style.borderTopRightRadius) +
|
|
197
|
+
parsePx(style.borderBottomRightRadius) +
|
|
198
|
+
parsePx(style.borderBottomLeftRadius) >
|
|
199
|
+
0;
|
|
200
|
+
return hasBackground || hasImage || hasBorder || hasRadius;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function clipsOverflow(style) {
|
|
204
|
+
return [style.overflowX, style.overflowY, style.overflow].some(
|
|
205
|
+
(value) => value && value !== "visible" && value !== "clip visible",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function rootRectFor(root) {
|
|
210
|
+
const measured = toRect(root.getBoundingClientRect());
|
|
211
|
+
const authoredWidth = Number.parseFloat(root.getAttribute("data-width") || "");
|
|
212
|
+
const authoredHeight = Number.parseFloat(root.getAttribute("data-height") || "");
|
|
213
|
+
const hasAuthoredSize =
|
|
214
|
+
Number.isFinite(authoredWidth) &&
|
|
215
|
+
authoredWidth > 0 &&
|
|
216
|
+
Number.isFinite(authoredHeight) &&
|
|
217
|
+
authoredHeight > 0;
|
|
218
|
+
|
|
219
|
+
if (!hasAuthoredSize) return measured;
|
|
220
|
+
if (measured.width > 0.5 && measured.height > 0.5) return measured;
|
|
221
|
+
return rectFromOrigin(measured.left, measured.top, authoredWidth, authoredHeight);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isConstraintCandidate(element, root, rootRect) {
|
|
225
|
+
if (element === root) return true;
|
|
226
|
+
const style = getComputedStyle(element);
|
|
227
|
+
if (clipsOverflow(style)) return true;
|
|
228
|
+
if (element.hasAttribute("data-layout-boundary")) return true;
|
|
229
|
+
if (!hasPaint(style)) return false;
|
|
230
|
+
if (!hasMeaningfulBoxStyle(style)) return false;
|
|
231
|
+
const rect = element.getBoundingClientRect();
|
|
232
|
+
const rootArea = rootRect.width * rootRect.height;
|
|
233
|
+
const area = rect.width * rect.height;
|
|
234
|
+
return area > 0 && area < rootArea * 0.95;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function nearestConstraint(element, root, rootRect) {
|
|
238
|
+
for (
|
|
239
|
+
let current = element;
|
|
240
|
+
current && current !== document.body;
|
|
241
|
+
current = current.parentElement
|
|
242
|
+
) {
|
|
243
|
+
if (!isVisibleElement(current)) continue;
|
|
244
|
+
if (isConstraintCandidate(current, root, rootRect)) return current;
|
|
245
|
+
if (current === root) return current;
|
|
246
|
+
}
|
|
247
|
+
return root;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatPx(value) {
|
|
251
|
+
return `${Math.round(value)}px`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function maxOverflow(overflow) {
|
|
255
|
+
return Math.max(...Object.values(overflow).filter((value) => typeof value === "number"));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function textOverflowFixHint(textRect, containerRect, overflow, fontSize, targetName) {
|
|
259
|
+
const horizontalOverflow = (overflow.left || 0) + (overflow.right || 0);
|
|
260
|
+
const verticalOverflow = (overflow.top || 0) + (overflow.bottom || 0);
|
|
261
|
+
const neededWidth = containerRect.width + horizontalOverflow;
|
|
262
|
+
const neededHeight = containerRect.height + verticalOverflow;
|
|
263
|
+
const widthRatio = containerRect.width > 0 ? containerRect.width / textRect.width : 0;
|
|
264
|
+
const heightRatio = containerRect.height > 0 ? containerRect.height / textRect.height : 0;
|
|
265
|
+
const limitingRatio = Math.min(
|
|
266
|
+
widthRatio > 0 ? widthRatio : Number.POSITIVE_INFINITY,
|
|
267
|
+
heightRatio > 0 ? heightRatio : Number.POSITIVE_INFINITY,
|
|
268
|
+
);
|
|
269
|
+
const shrinkPercent =
|
|
270
|
+
Number.isFinite(limitingRatio) && limitingRatio < 1
|
|
271
|
+
? Math.ceil((1 - limitingRatio) * 100)
|
|
272
|
+
: 0;
|
|
273
|
+
const targetFont =
|
|
274
|
+
shrinkPercent > 0 && Number.isFinite(fontSize) && fontSize > 0
|
|
275
|
+
? ` or shrink font-size from ${formatPx(fontSize)} to ~${formatPx(fontSize * limitingRatio)}`
|
|
276
|
+
: "";
|
|
277
|
+
const sizeTarget =
|
|
278
|
+
horizontalOverflow > 0 && verticalOverflow > 0
|
|
279
|
+
? `resize ${targetName} to at least ~${formatPx(neededWidth)} x ${formatPx(neededHeight)}`
|
|
280
|
+
: horizontalOverflow > 0
|
|
281
|
+
? `widen ${targetName} to at least ~${formatPx(neededWidth)}`
|
|
282
|
+
: `increase ${targetName} height to at least ~${formatPx(neededHeight)}`;
|
|
283
|
+
|
|
284
|
+
return `Text is ${formatPx(textRect.width)} x ${formatPx(textRect.height)} inside ${formatPx(containerRect.width)} x ${formatPx(containerRect.height)} and overflows by up to ${formatPx(maxOverflow(overflow))}; ${sizeTarget}${targetFont}, or allow wrapping with max-width/fitTextFontSize.`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function clippedTextIssue(element, time, tolerance) {
|
|
288
|
+
const style = getComputedStyle(element);
|
|
289
|
+
if (!clipsOverflow(style)) return null;
|
|
290
|
+
const overflowX = element.scrollWidth - element.clientWidth;
|
|
291
|
+
const overflowY = element.scrollHeight - element.clientHeight;
|
|
292
|
+
if (overflowX <= tolerance && overflowY <= tolerance) return null;
|
|
293
|
+
const overflow = {};
|
|
294
|
+
if (overflowX > tolerance) overflow.right = round(overflowX);
|
|
295
|
+
if (overflowY > tolerance) overflow.bottom = round(overflowY);
|
|
296
|
+
const selector = selectorFor(element);
|
|
297
|
+
const text = textContentFor(element);
|
|
298
|
+
const rect = toRect(element.getBoundingClientRect());
|
|
299
|
+
const fontSize = parsePx(style.fontSize);
|
|
300
|
+
return {
|
|
301
|
+
code: "clipped_text",
|
|
302
|
+
severity: "error",
|
|
303
|
+
time,
|
|
304
|
+
selector,
|
|
305
|
+
text,
|
|
306
|
+
message: "Text content is clipped by its own box.",
|
|
307
|
+
rect,
|
|
308
|
+
overflow,
|
|
309
|
+
fixHint: textOverflowFixHint(rect, rect, overflow, fontSize, "the text box"),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function textOverflowIssues(element, root, rootRect, time, tolerance) {
|
|
314
|
+
const textRect = textRectFor(element);
|
|
315
|
+
if (!textRect) return [];
|
|
316
|
+
const text = textContentFor(element);
|
|
317
|
+
const selector = selectorFor(element);
|
|
318
|
+
const issues = [];
|
|
319
|
+
|
|
320
|
+
const container = nearestConstraint(element, root, rootRect);
|
|
321
|
+
const containerRect = container === root ? rootRect : toRect(container.getBoundingClientRect());
|
|
322
|
+
const containerOverflow = overflowFor(textRect, containerRect, tolerance);
|
|
323
|
+
if (containerOverflow && !hasAllowOverflowFlag(element)) {
|
|
324
|
+
const style = getComputedStyle(element);
|
|
325
|
+
issues.push({
|
|
326
|
+
code: "text_box_overflow",
|
|
327
|
+
severity: "error",
|
|
328
|
+
time,
|
|
329
|
+
selector,
|
|
330
|
+
containerSelector: selectorFor(container),
|
|
331
|
+
text,
|
|
332
|
+
message: "Text extends outside its nearest visual/container box.",
|
|
333
|
+
rect: textRect,
|
|
334
|
+
containerRect,
|
|
335
|
+
overflow: containerOverflow,
|
|
336
|
+
fixHint: textOverflowFixHint(
|
|
337
|
+
textRect,
|
|
338
|
+
containerRect,
|
|
339
|
+
containerOverflow,
|
|
340
|
+
parsePx(style.fontSize),
|
|
341
|
+
"the container",
|
|
342
|
+
),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const canvasOverflow = overflowFor(textRect, rootRect, tolerance);
|
|
347
|
+
if (canvasOverflow && !hasAllowOverflowFlag(element)) {
|
|
348
|
+
issues.push({
|
|
349
|
+
code: "canvas_overflow",
|
|
350
|
+
severity: "info",
|
|
351
|
+
time,
|
|
352
|
+
selector,
|
|
353
|
+
containerSelector: selectorFor(root),
|
|
354
|
+
text,
|
|
355
|
+
message: "Text extends outside the composition canvas.",
|
|
356
|
+
rect: textRect,
|
|
357
|
+
containerRect: rootRect,
|
|
358
|
+
overflow: canvasOverflow,
|
|
359
|
+
fixHint:
|
|
360
|
+
"Move the text inward, reduce its size, or mark intentional off-canvas animation with data-layout-allow-overflow.",
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return issues;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function containerOverflowIssues(root, time, tolerance) {
|
|
368
|
+
const issues = [];
|
|
369
|
+
const containers = Array.from(root.querySelectorAll("*")).filter((element) => {
|
|
370
|
+
if (!isVisibleElement(element) || hasAllowOverflowFlag(element)) return false;
|
|
371
|
+
const style = getComputedStyle(element);
|
|
372
|
+
return clipsOverflow(style) || element.hasAttribute("data-layout-boundary");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
for (const container of containers) {
|
|
376
|
+
const containerRect = toRect(container.getBoundingClientRect());
|
|
377
|
+
for (const child of Array.from(container.children)) {
|
|
378
|
+
if (!isVisibleElement(child) || hasAllowOverflowFlag(child)) continue;
|
|
379
|
+
const childRect = toRect(child.getBoundingClientRect());
|
|
380
|
+
const overflow = overflowFor(childRect, containerRect, tolerance);
|
|
381
|
+
if (!overflow) continue;
|
|
382
|
+
issues.push({
|
|
383
|
+
code: "container_overflow",
|
|
384
|
+
severity: "warning",
|
|
385
|
+
time,
|
|
386
|
+
selector: selectorFor(child),
|
|
387
|
+
containerSelector: selectorFor(container),
|
|
388
|
+
message: "Element extends outside a clipping layout container.",
|
|
389
|
+
rect: childRect,
|
|
390
|
+
containerRect,
|
|
391
|
+
overflow,
|
|
392
|
+
fixHint:
|
|
393
|
+
"Resize/reposition the child or container, or mark intentional overflow with data-layout-allow-overflow.",
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return issues;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
window.__hyperframesLayoutAudit = function auditLayout(options) {
|
|
402
|
+
const time = options && typeof options.time === "number" ? options.time : 0;
|
|
403
|
+
const tolerance =
|
|
404
|
+
options && typeof options.tolerance === "number" ? Math.max(0, options.tolerance) : 2;
|
|
405
|
+
const root =
|
|
406
|
+
document.querySelector("[data-composition-id][data-width][data-height]") ||
|
|
407
|
+
document.querySelector("[data-composition-id]") ||
|
|
408
|
+
document.body;
|
|
409
|
+
const rootRect = rootRectFor(root);
|
|
410
|
+
const elements = Array.from(root.querySelectorAll("*")).filter(isVisibleElement);
|
|
411
|
+
const issues = [];
|
|
412
|
+
|
|
413
|
+
for (const element of elements) {
|
|
414
|
+
if (!hasOwnTextCandidate(element)) continue;
|
|
415
|
+
const clipped = clippedTextIssue(element, time, tolerance);
|
|
416
|
+
if (clipped) issues.push(clipped);
|
|
417
|
+
issues.push(...textOverflowIssues(element, root, rootRect, time, tolerance));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
issues.push(...containerOverflowIssues(root, time, tolerance));
|
|
421
|
+
return issues;
|
|
422
|
+
};
|
|
423
|
+
})();
|
|
@@ -277,11 +277,28 @@ When no `visual-style.md` or animation direction is provided, follow [house-styl
|
|
|
277
277
|
## Output Checklist
|
|
278
278
|
|
|
279
279
|
- [ ] `npx hyperframes lint` and `npx hyperframes validate` both pass
|
|
280
|
+
- [ ] `npx hyperframes inspect` passes, or every reported overflow is intentionally marked
|
|
280
281
|
- [ ] Contrast warnings addressed (see Quality Checks below)
|
|
282
|
+
- [ ] Layout issues addressed (see Quality Checks below)
|
|
281
283
|
- [ ] Animation choreography verified (see Quality Checks below)
|
|
282
284
|
|
|
283
285
|
## Quality Checks
|
|
284
286
|
|
|
287
|
+
### Visual Inspect
|
|
288
|
+
|
|
289
|
+
`hyperframes inspect` runs the composition in headless Chrome, seeks through the timeline, and maps visual layout issues with timestamps, selectors, bounding boxes, and fix hints. Run it after `lint` and `validate`:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
npx hyperframes inspect
|
|
293
|
+
npx hyperframes inspect --json
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Failures usually mean text is spilling out of a bubble/card, a fixed-size label is clipping dynamic copy, or text has moved off the canvas. Fix by increasing container size or padding, reducing font size or letter spacing, adding a real `max-width` so text wraps inside the container, or using `window.__hyperframes.fitTextFontSize(...)` for dynamic copy.
|
|
297
|
+
|
|
298
|
+
Use `--samples 15` for dense videos and `--at 1.5,4,7.25` for specific hero frames. Repeated static issues are collapsed by default to avoid flooding agent context. If overflow is intentional for an entrance/exit animation, mark the element or ancestor with `data-layout-allow-overflow`. If a decorative element should never be audited, mark it with `data-layout-ignore`.
|
|
299
|
+
|
|
300
|
+
`hyperframes layout` is the compatibility alias for the same check.
|
|
301
|
+
|
|
285
302
|
### Contrast
|
|
286
303
|
|
|
287
304
|
`hyperframes validate` runs a WCAG contrast audit by default. It seeks to 5 timestamps, screenshots the page, samples background pixels behind every text element, and computes contrast ratios. Failures appear as warnings:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hyperframes-cli
|
|
3
|
-
description: HyperFrames CLI tool — hyperframes init, lint, preview, render, transcribe, tts, doctor, browser, info, upgrade, compositions, docs, benchmark. Use when scaffolding a project, linting
|
|
3
|
+
description: HyperFrames CLI tool — hyperframes init, lint, inspect, preview, render, transcribe, tts, doctor, browser, info, upgrade, compositions, docs, benchmark. Use when scaffolding a project, linting, validating, inspecting visual layout in compositions, previewing in the studio, rendering to video, transcribing audio, generating TTS, or troubleshooting the HyperFrames environment.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# HyperFrames CLI
|
|
@@ -12,10 +12,11 @@ Everything runs through `npx hyperframes`. Requires Node.js >= 22 and FFmpeg.
|
|
|
12
12
|
1. **Scaffold** — `npx hyperframes init my-video`
|
|
13
13
|
2. **Write** — author HTML composition (see the `hyperframes` skill)
|
|
14
14
|
3. **Lint** — `npx hyperframes lint`
|
|
15
|
-
4. **
|
|
16
|
-
5. **
|
|
15
|
+
4. **Visual inspect** — `npx hyperframes inspect`
|
|
16
|
+
5. **Preview** — `npx hyperframes preview`
|
|
17
|
+
6. **Render** — `npx hyperframes render`
|
|
17
18
|
|
|
18
|
-
Lint before preview
|
|
19
|
+
Lint and inspect before preview. `lint` catches missing `data-composition-id`, overlapping tracks, and unregistered timelines. `inspect` opens the rendered composition in headless Chrome, seeks through the timeline, and reports text spilling out of bubbles/containers or off the canvas.
|
|
19
20
|
|
|
20
21
|
## Scaffolding
|
|
21
22
|
|
|
@@ -42,6 +43,27 @@ npx hyperframes lint --json # machine-readable
|
|
|
42
43
|
|
|
43
44
|
Lints `index.html` and all files in `compositions/`. Reports errors (must fix), warnings (should fix), and info (with `--verbose`).
|
|
44
45
|
|
|
46
|
+
## Visual Inspect
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx hyperframes inspect # inspect rendered layout over the timeline
|
|
50
|
+
npx hyperframes inspect ./my-project # specific project
|
|
51
|
+
npx hyperframes inspect --json # agent-readable findings
|
|
52
|
+
npx hyperframes inspect --samples 15 # denser timeline sweep
|
|
53
|
+
npx hyperframes inspect --at 1.5,4,7.25 # explicit hero-frame timestamps
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use this after `lint` and `validate`, especially for compositions with speech bubbles, cards, captions, or tight typography. It reports:
|
|
57
|
+
|
|
58
|
+
- Text extending outside the nearest visual container or bubble
|
|
59
|
+
- Text clipped by its own fixed-width/fixed-height box
|
|
60
|
+
- Text extending outside the composition canvas
|
|
61
|
+
- Children escaping clipping containers
|
|
62
|
+
|
|
63
|
+
Errors should be fixed before rendering. Warnings are surfaced for agent review; add `--strict` to fail on warnings too. Repeated static issues are collapsed by default so JSON output stays compact for LLM context windows. If overflow is intentional for an entrance/exit animation, mark the element or ancestor with `data-layout-allow-overflow`. If a decorative element should never be audited, mark it with `data-layout-ignore`.
|
|
64
|
+
|
|
65
|
+
`npx hyperframes layout` remains available as a compatibility alias for the same visual inspection pass.
|
|
66
|
+
|
|
45
67
|
## Previewing
|
|
46
68
|
|
|
47
69
|
```bash
|