hyperframes 0.8.31 → 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 +2661 -1736
- package/dist/commands/layout-audit.browser.js +161 -12
- package/dist/fontLocalizeCli.js +2599 -1970
- 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-0QzsrRGz.js → hyperframes-player-CroojEG1.js} +5 -1
- package/dist/studio/assets/{index-DdTv4TqQ.js → index-CSyaU1m2.js} +1 -1
- package/dist/studio/assets/{index-Dyhj0geW.js → index-D0AjUHNT.js} +1 -1
- package/dist/studio/assets/{index-CfQexXc6.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
|
|
|
@@ -594,8 +627,8 @@
|
|
|
594
627
|
const blocks = [];
|
|
595
628
|
for (const element of Array.from(root.querySelectorAll("*"))) {
|
|
596
629
|
if (!isSolidTextBlock(element)) continue;
|
|
597
|
-
const rects =
|
|
598
|
-
const rect =
|
|
630
|
+
const rects = visibleTextClientRects(element, true);
|
|
631
|
+
const rect = unionRects(rects);
|
|
599
632
|
if (rect) blocks.push({ element, rect, rects });
|
|
600
633
|
}
|
|
601
634
|
return blocks;
|
|
@@ -1300,7 +1333,7 @@
|
|
|
1300
1333
|
const threshold = Math.max(32, Math.min(rootRect.width, rootRect.height) * 0.02);
|
|
1301
1334
|
const MIN_CONNECTOR_CHORD_PX = 8;
|
|
1302
1335
|
for (const svg of Array.from(root.querySelectorAll("svg"))) {
|
|
1303
|
-
if (!isVisibleElement(svg)
|
|
1336
|
+
if (!isVisibleElement(svg)) continue;
|
|
1304
1337
|
for (const path of Array.from(svg.querySelectorAll("path"))) {
|
|
1305
1338
|
if (path.closest(CONNECTOR_SKIP_CONTAINERS)) continue;
|
|
1306
1339
|
if (!isConnectorPath(svg, path)) continue;
|
|
@@ -1333,7 +1366,16 @@
|
|
|
1333
1366
|
// Paste-into-`d` bug: both raw endpoints land on distinct anchors as screen pixels.
|
|
1334
1367
|
const userStartKey = attachmentKey(user.start);
|
|
1335
1368
|
const userEndKey = attachmentKey(user.end);
|
|
1336
|
-
|
|
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;
|
|
1337
1379
|
const gap = Math.round(
|
|
1338
1380
|
Math.min(
|
|
1339
1381
|
Math.min(...anchors.compact.map((a) => distanceToRect(rendered.start, a.rect))),
|
|
@@ -1346,7 +1388,113 @@
|
|
|
1346
1388
|
time,
|
|
1347
1389
|
selector: selectorFor(path),
|
|
1348
1390
|
containerSelector: selectorFor(svg),
|
|
1349
|
-
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.`,
|
|
1350
1498
|
rect: toRect({
|
|
1351
1499
|
left: Math.min(rendered.start.x, rendered.end.x),
|
|
1352
1500
|
top: Math.min(rendered.start.y, rendered.end.y),
|
|
@@ -1356,7 +1504,7 @@
|
|
|
1356
1504
|
height: Math.abs(rendered.end.y - rendered.start.y),
|
|
1357
1505
|
}),
|
|
1358
1506
|
fixHint:
|
|
1359
|
-
"
|
|
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.",
|
|
1360
1508
|
});
|
|
1361
1509
|
}
|
|
1362
1510
|
}
|
|
@@ -1467,6 +1615,7 @@
|
|
|
1467
1615
|
issues.push(...escaped.issues);
|
|
1468
1616
|
issues.push(...panelOutOfCanvasIssues(root, rootRect, time, tolerance, escaped.flagged));
|
|
1469
1617
|
issues.push(...connectorDetachmentIssues(root, rootRect, time));
|
|
1618
|
+
issues.push(...connectorOrphanIssues(root, rootRect, time));
|
|
1470
1619
|
return issues;
|
|
1471
1620
|
};
|
|
1472
1621
|
|