hyperframes 0.8.30 → 0.8.32
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 +3280 -1959
- package/dist/commands/layout-audit.browser.js +174 -18
- package/dist/fontLocalizeCli.js +2944 -2013
- package/dist/hyperframe-runtime.js +27 -27
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +27 -27
- package/dist/hyperframes-player.global.js +5 -1
- package/dist/hyperframes-slideshow.global.js +16 -16
- package/dist/studio/assets/{hyperframes-player-jDyYPwpC.js → hyperframes-player-CroojEG1.js} +5 -1
- package/dist/studio/assets/{index-DRWtJgb8.js → index-CSyaU1m2.js} +1 -1
- package/dist/studio/assets/{index-BLvqFdnB.js → index-D0AjUHNT.js} +1 -1
- package/dist/studio/assets/{index-hQAWl6re.js → index-PVOehm7h.js} +113 -113
- package/dist/studio/index.html +1 -1
- package/dist/studio/index.js +26 -11
- package/dist/studio/index.js.map +1 -1
- package/dist/templates/_shared/AGENTS.md +4 -1
- package/dist/templates/_shared/CLAUDE.md +4 -1
- package/package.json +1 -1
|
@@ -245,10 +245,8 @@
|
|
|
245
245
|
return rects;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function
|
|
249
|
-
const rects = textClientRects(element, directOnly);
|
|
248
|
+
function unionRects(rects) {
|
|
250
249
|
if (rects.length === 0) return null;
|
|
251
|
-
|
|
252
250
|
const union = rects.reduce(
|
|
253
251
|
(acc, rect) => ({
|
|
254
252
|
left: Math.min(acc.left, rect.left),
|
|
@@ -263,7 +261,6 @@
|
|
|
263
261
|
bottom: Number.NEGATIVE_INFINITY,
|
|
264
262
|
},
|
|
265
263
|
);
|
|
266
|
-
|
|
267
264
|
return toRect({
|
|
268
265
|
...union,
|
|
269
266
|
width: union.right - union.left,
|
|
@@ -271,6 +268,38 @@
|
|
|
271
268
|
});
|
|
272
269
|
}
|
|
273
270
|
|
|
271
|
+
function visibleTextClientRects(element, directOnly) {
|
|
272
|
+
// Range rects stay geometrically present outside an overflow clip. Reduce
|
|
273
|
+
// them in viewport coordinates so overlap measures only paintable text.
|
|
274
|
+
let rects = textClientRects(element, directOnly).map(toRect);
|
|
275
|
+
for (
|
|
276
|
+
let ancestor = element.parentElement;
|
|
277
|
+
ancestor && rects.length > 0;
|
|
278
|
+
ancestor = ancestor.parentElement
|
|
279
|
+
) {
|
|
280
|
+
const style = getComputedStyle(ancestor);
|
|
281
|
+
const clipX = clipsOverflowValue(style.overflowX || style.overflow);
|
|
282
|
+
const clipY = clipsOverflowValue(style.overflowY || style.overflow);
|
|
283
|
+
if (!clipX && !clipY) continue;
|
|
284
|
+
const clip = toRect(ancestor.getBoundingClientRect());
|
|
285
|
+
rects = rects
|
|
286
|
+
.map((rect) => {
|
|
287
|
+
const left = clipX ? Math.max(rect.left, clip.left) : rect.left;
|
|
288
|
+
const right = clipX ? Math.min(rect.right, clip.right) : rect.right;
|
|
289
|
+
const top = clipY ? Math.max(rect.top, clip.top) : rect.top;
|
|
290
|
+
const bottom = clipY ? Math.min(rect.bottom, clip.bottom) : rect.bottom;
|
|
291
|
+
if (right - left <= 0.5 || bottom - top <= 0.5) return null;
|
|
292
|
+
return toRect({ left, right, top, bottom, width: right - left, height: bottom - top });
|
|
293
|
+
})
|
|
294
|
+
.filter(Boolean);
|
|
295
|
+
}
|
|
296
|
+
return rects;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function textRectFor(element, directOnly) {
|
|
300
|
+
return unionRects(textClientRects(element, directOnly));
|
|
301
|
+
}
|
|
302
|
+
|
|
274
303
|
function parsePx(value) {
|
|
275
304
|
const parsed = Number.parseFloat(value);
|
|
276
305
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
@@ -317,9 +346,13 @@
|
|
|
317
346
|
return hasBackground || hasImage || hasBorder || hasRadius;
|
|
318
347
|
}
|
|
319
348
|
|
|
349
|
+
function clipsOverflowValue(value) {
|
|
350
|
+
return value && value !== "visible" && value !== "clip visible";
|
|
351
|
+
}
|
|
352
|
+
|
|
320
353
|
function clipsOverflow(style) {
|
|
321
|
-
return [style.overflowX, style.overflowY, style.overflow].some(
|
|
322
|
-
(value)
|
|
354
|
+
return [style.overflowX, style.overflowY, style.overflow].some((value) =>
|
|
355
|
+
clipsOverflowValue(value),
|
|
323
356
|
);
|
|
324
357
|
}
|
|
325
358
|
|
|
@@ -439,7 +472,7 @@
|
|
|
439
472
|
return false;
|
|
440
473
|
}
|
|
441
474
|
|
|
442
|
-
function textOverflowIssues(element, root, rootRect, time, tolerance) {
|
|
475
|
+
function textOverflowIssues(element, root, rootRect, time, tolerance, clippedIssue) {
|
|
443
476
|
const textRect = textRectFor(element, true);
|
|
444
477
|
if (!textRect) return [];
|
|
445
478
|
const text = textContentFor(element, true);
|
|
@@ -462,8 +495,15 @@
|
|
|
462
495
|
? tolerance
|
|
463
496
|
: Math.max(tolerance, parsePx(elementStyle.fontSize) * 0.2);
|
|
464
497
|
const containerOverflow = overflowFor(textRect, containerRect, tolerance, verticalTolerance);
|
|
498
|
+
const billedAsClippedText =
|
|
499
|
+
container === element &&
|
|
500
|
+
clippedIssue != null &&
|
|
501
|
+
containerOverflow != null &&
|
|
502
|
+
containerOverflow.left == null &&
|
|
503
|
+
containerOverflow.top == null;
|
|
465
504
|
if (
|
|
466
505
|
containerOverflow &&
|
|
506
|
+
!billedAsClippedText &&
|
|
467
507
|
!hasTextClipOptOut(element) &&
|
|
468
508
|
!clippedByAncestor(element, container)
|
|
469
509
|
) {
|
|
@@ -587,8 +627,8 @@
|
|
|
587
627
|
const blocks = [];
|
|
588
628
|
for (const element of Array.from(root.querySelectorAll("*"))) {
|
|
589
629
|
if (!isSolidTextBlock(element)) continue;
|
|
590
|
-
const rects =
|
|
591
|
-
const rect =
|
|
630
|
+
const rects = visibleTextClientRects(element, true);
|
|
631
|
+
const rect = unionRects(rects);
|
|
592
632
|
if (rect) blocks.push({ element, rect, rects });
|
|
593
633
|
}
|
|
594
634
|
return blocks;
|
|
@@ -663,8 +703,8 @@
|
|
|
663
703
|
code: "content_overlap",
|
|
664
704
|
severity: "warning",
|
|
665
705
|
time,
|
|
666
|
-
selector:
|
|
667
|
-
containerSelector:
|
|
706
|
+
selector: uniqueSelectorFor(a.element),
|
|
707
|
+
containerSelector: uniqueSelectorFor(b.element),
|
|
668
708
|
text: textContentFor(a.element),
|
|
669
709
|
message: "Two text blocks overlap and may render unreadable.",
|
|
670
710
|
rect: a.rect,
|
|
@@ -1021,8 +1061,8 @@
|
|
|
1021
1061
|
code: "text_occluded",
|
|
1022
1062
|
severity: "error",
|
|
1023
1063
|
time,
|
|
1024
|
-
selector:
|
|
1025
|
-
containerSelector:
|
|
1064
|
+
selector: uniqueSelectorFor(element),
|
|
1065
|
+
containerSelector: uniqueSelectorFor(occluder),
|
|
1026
1066
|
text,
|
|
1027
1067
|
message: "Text is hidden beneath an opaque element.",
|
|
1028
1068
|
rect: textRect,
|
|
@@ -1293,7 +1333,7 @@
|
|
|
1293
1333
|
const threshold = Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
|
|
1294
1334
|
const MIN_CONNECTOR_CHORD_PX = 8;
|
|
1295
1335
|
for (const svg of Array.from(root.querySelectorAll("svg"))) {
|
|
1296
|
-
if (!isVisibleElement(svg)
|
|
1336
|
+
if (!isVisibleElement(svg)) continue;
|
|
1297
1337
|
for (const path of Array.from(svg.querySelectorAll("path"))) {
|
|
1298
1338
|
if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
|
|
1299
1339
|
if (!isConnectorPath(svg, path)) continue;
|
|
@@ -1326,7 +1366,16 @@
|
|
|
1326
1366
|
// Paste-into-`d` bug: both raw endpoints land on distinct anchors as screen pixels.
|
|
1327
1367
|
const userStartKey = attachmentKey(user.start);
|
|
1328
1368
|
const userEndKey = attachmentKey(user.end);
|
|
1329
|
-
|
|
1369
|
+
const pasteBug = Boolean(userStartKey && userEndKey && userStartKey !== userEndKey);
|
|
1370
|
+
// Guessed marked shaft: both frames miss. Same-anchor grazes attach in user-space
|
|
1371
|
+
// and must stay skipped. Name-only decorative flow/arrow paths stay skipped.
|
|
1372
|
+
// 80px keeps short marker glyphs (chevrons, tips) out.
|
|
1373
|
+
const markedMiss =
|
|
1374
|
+
renderedChord >= 80 &&
|
|
1375
|
+
!userStartKey &&
|
|
1376
|
+
!userEndKey &&
|
|
1377
|
+
(path.hasAttribute("marker-start") || path.hasAttribute("marker-end"));
|
|
1378
|
+
if (!pasteBug && !markedMiss) continue;
|
|
1330
1379
|
const gap = Math.round(
|
|
1331
1380
|
Math.min(
|
|
1332
1381
|
Math.min(...anchors.compact.map((a) => distanceToRect(rendered.start, a.rect))),
|
|
@@ -1339,7 +1388,113 @@
|
|
|
1339
1388
|
time,
|
|
1340
1389
|
selector: selectorFor(path),
|
|
1341
1390
|
containerSelector: selectorFor(svg),
|
|
1342
|
-
message:
|
|
1391
|
+
message: pasteBug
|
|
1392
|
+
? `Connector path endpoints render ${gap}px from the nearest anchorable element, but the path's user-space coordinates would attach if read as screen pixels — screen/viewport numbers were likely written into SVG \`d\` without inverting the CTM.`
|
|
1393
|
+
: `Connector path endpoints render ${gap}px from the nearest anchorable element — a marked shaft that meets no node.`,
|
|
1394
|
+
rect: toRect({
|
|
1395
|
+
left: Math.min(rendered.start.x, rendered.end.x),
|
|
1396
|
+
top: Math.min(rendered.start.y, rendered.end.y),
|
|
1397
|
+
right: Math.max(rendered.start.x, rendered.end.x),
|
|
1398
|
+
bottom: Math.max(rendered.start.y, rendered.end.y),
|
|
1399
|
+
width: Math.abs(rendered.end.x - rendered.start.x),
|
|
1400
|
+
height: Math.abs(rendered.end.y - rendered.start.y),
|
|
1401
|
+
}),
|
|
1402
|
+
fixHint: pasteBug
|
|
1403
|
+
? "Convert measured screen coordinates into the SVG's user space (subtract the SVG rect / invert getScreenCTM) before writing path `d`, and keep the SVG a direct child of the stage."
|
|
1404
|
+
: "Measure the settled node boxes and write `d` in the SVG's user space (invert getScreenCTM), or grow a layout-owned shaft from the source node.",
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return issues;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function shaftIsPainted(path) {
|
|
1412
|
+
if (IGNORE_TAGS.has(path.tagName) || hasIgnoreFlag(path)) return false;
|
|
1413
|
+
const style = getComputedStyle(path);
|
|
1414
|
+
if (
|
|
1415
|
+
style.display === "none" ||
|
|
1416
|
+
style.visibility === "hidden" ||
|
|
1417
|
+
style.visibility === "collapse"
|
|
1418
|
+
) {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
return opacityChain(path) >= 0.2;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function shaftDashHidden(path) {
|
|
1425
|
+
if (typeof path.getTotalLength !== "function") return false;
|
|
1426
|
+
let total;
|
|
1427
|
+
try {
|
|
1428
|
+
total = path.getTotalLength();
|
|
1429
|
+
} catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
if (!Number.isFinite(total) || total <= 0) return false;
|
|
1433
|
+
const style = getComputedStyle(path);
|
|
1434
|
+
const offset = Number.parseFloat(style.strokeDashoffset || "0");
|
|
1435
|
+
const dash = Number.parseFloat(String(style.strokeDasharray || "").split(/[\s,]+/)[0] || "0");
|
|
1436
|
+
return offset >= total * 0.9 && dash >= total * 0.9;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function connectorEndpointCandidates(root, rootRect) {
|
|
1440
|
+
const candidates = [];
|
|
1441
|
+
const rootArea = rectArea(rootRect);
|
|
1442
|
+
for (const element of Array.from(root.querySelectorAll("*"))) {
|
|
1443
|
+
if (element.closest("svg") || IGNORE_TAGS.has(element.tagName) || hasIgnoreFlag(element))
|
|
1444
|
+
continue;
|
|
1445
|
+
const style = getComputedStyle(element);
|
|
1446
|
+
const opaque = RASTER_TAGS.has(element.tagName) || hasOpaqueBackground(style);
|
|
1447
|
+
if (!opaque && !textContentFor(element)) continue;
|
|
1448
|
+
const rect = toRect(element.getBoundingClientRect());
|
|
1449
|
+
const area = rectArea(rect);
|
|
1450
|
+
if (area < 400 || area > rootArea * 0.15) continue;
|
|
1451
|
+
candidates.push({ rect, element });
|
|
1452
|
+
}
|
|
1453
|
+
return candidates;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function connectorOrphanIssues(root, rootRect, time) {
|
|
1457
|
+
const issues = [];
|
|
1458
|
+
let candidates = null;
|
|
1459
|
+
const threshold = Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
|
|
1460
|
+
for (const svg of Array.from(root.querySelectorAll("svg"))) {
|
|
1461
|
+
if (!isVisibleElement(svg)) continue;
|
|
1462
|
+
for (const path of Array.from(svg.querySelectorAll("path"))) {
|
|
1463
|
+
if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
|
|
1464
|
+
if (!isConnectorPath(svg, path)) continue;
|
|
1465
|
+
if (!shaftIsPainted(path) || shaftDashHidden(path)) continue;
|
|
1466
|
+
const user = pathUserEndpoints(path);
|
|
1467
|
+
const rendered = pathScreenEndpoints(svg, path, user);
|
|
1468
|
+
if (!user || !rendered) continue;
|
|
1469
|
+
const renderedChord = Math.hypot(
|
|
1470
|
+
rendered.end.x - rendered.start.x,
|
|
1471
|
+
rendered.end.y - rendered.start.y,
|
|
1472
|
+
);
|
|
1473
|
+
if (renderedChord < 80) continue;
|
|
1474
|
+
if (candidates === null) candidates = connectorEndpointCandidates(root, rootRect);
|
|
1475
|
+
const dark = [];
|
|
1476
|
+
for (const point of [rendered.start, rendered.end]) {
|
|
1477
|
+
let best = null;
|
|
1478
|
+
let attached = false;
|
|
1479
|
+
for (const candidate of candidates) {
|
|
1480
|
+
const gap = distanceToRect(point, candidate.rect);
|
|
1481
|
+
if (gap > threshold) continue;
|
|
1482
|
+
if (isVisibleElement(candidate.element)) {
|
|
1483
|
+
attached = true;
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
if (best === null || gap < best.gap) best = { gap, candidate };
|
|
1487
|
+
}
|
|
1488
|
+
if (!attached && best !== null) dark.push(best.candidate);
|
|
1489
|
+
}
|
|
1490
|
+
if (dark.length === 0) continue;
|
|
1491
|
+
issues.push({
|
|
1492
|
+
code: "connector_orphan",
|
|
1493
|
+
severity: "warning",
|
|
1494
|
+
time,
|
|
1495
|
+
selector: selectorFor(path),
|
|
1496
|
+
containerSelector: selectorFor(svg),
|
|
1497
|
+
message: `Connector shaft is visible while ${dark.length === 2 ? "both endpoints are" : `its endpoint ${selectorFor(dark[0].element)} is`} not on stage.`,
|
|
1343
1498
|
rect: toRect({
|
|
1344
1499
|
left: Math.min(rendered.start.x, rendered.end.x),
|
|
1345
1500
|
top: Math.min(rendered.start.y, rendered.end.y),
|
|
@@ -1349,7 +1504,7 @@
|
|
|
1349
1504
|
height: Math.abs(rendered.end.y - rendered.start.y),
|
|
1350
1505
|
}),
|
|
1351
1506
|
fixHint:
|
|
1352
|
-
"
|
|
1507
|
+
"Show the shaft only after both ends are on, and hide it with the earlier exit. Do not give the line its own clock.",
|
|
1353
1508
|
});
|
|
1354
1509
|
}
|
|
1355
1510
|
}
|
|
@@ -1444,7 +1599,7 @@
|
|
|
1444
1599
|
if (!hasOwnTextCandidate(element)) continue;
|
|
1445
1600
|
const clipped = clippedTextIssue(element, time, tolerance);
|
|
1446
1601
|
if (clipped) issues.push(clipped);
|
|
1447
|
-
issues.push(...textOverflowIssues(element, root, rootRect, time, tolerance));
|
|
1602
|
+
issues.push(...textOverflowIssues(element, root, rootRect, time, tolerance, clipped));
|
|
1448
1603
|
const occluded = occludedTextIssue(element, time, proseCoverageFloor);
|
|
1449
1604
|
if (occluded) issues.push(occluded);
|
|
1450
1605
|
const invisible = invisibleTextIssue(element, time);
|
|
@@ -1460,6 +1615,7 @@
|
|
|
1460
1615
|
issues.push(...escaped.issues);
|
|
1461
1616
|
issues.push(...panelOutOfCanvasIssues(root, rootRect, time, tolerance, escaped.flagged));
|
|
1462
1617
|
issues.push(...connectorDetachmentIssues(root, rootRect, time));
|
|
1618
|
+
issues.push(...connectorOrphanIssues(root, rootRect, time));
|
|
1463
1619
|
return issues;
|
|
1464
1620
|
};
|
|
1465
1621
|
|