hyperframes 0.7.57 → 0.7.58
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 +1766 -1376
- package/dist/commands/contrast-audit.browser.js +69 -19
- package/dist/commands/layout-audit.browser.js +114 -5
- package/dist/hyperframe-runtime.js +22 -22
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +22 -22
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/skills/hyperframes-cli/SKILL.md +2 -2
- package/dist/skills/hyperframes-cli/references/preview-render.md +3 -3
- package/dist/studio/assets/{hyperframes-player--Z69cEkE.js → hyperframes-player-CtTDO63S.js} +1 -1
- package/dist/studio/assets/{index-DfmYkU44.js → index-B_UvTX3E.js} +204 -204
- package/dist/studio/assets/{index-KsfE1bUu.js → index-C47jAC3Q.js} +1 -1
- package/dist/studio/assets/{index-Bf1x1y8H.js → index-DeQPzqwH.js} +1 -1
- package/dist/studio/assets/index-uahwWkgw.css +1 -0
- package/dist/studio/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/studio/chunk-OBAG3GWK.js.map +1 -0
- package/dist/studio/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/studio/index.d.ts +15 -4
- package/dist/studio/index.html +2 -2
- package/dist/studio/index.js +5954 -4474
- package/dist/studio/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/studio/assets/index-_pqzyxB1.css +0 -1
- package/dist/studio/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/studio/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -58,28 +58,29 @@ window.__contrastAuditPrepare = function () {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function parseColor(c) {
|
|
61
|
-
var m = c.match(
|
|
62
|
-
if (!m)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
61
|
+
var m = (c || "").match(/^rgba?\(([^)]+)\)$/i);
|
|
62
|
+
if (!m) {
|
|
63
|
+
var mix = (c || "").match(
|
|
64
|
+
/^color-mix\(\s*in\s+srgb\s*,\s*(rgba?\([^)]+\))\s+(\d+(?:\.\d+)?|\.\d+)%\s*,\s*(rgba?\([^)]+\))\s+(\d+(?:\.\d+)?|\.\d+)%\s*\)$/i,
|
|
65
|
+
);
|
|
66
|
+
if (!mix) return null;
|
|
67
|
+
var first = parseColor(mix[1]);
|
|
68
|
+
var second = parseColor(mix[3]);
|
|
69
|
+
var firstWeight = parseFloat(mix[2]);
|
|
70
|
+
var secondWeight = parseFloat(mix[4]);
|
|
71
|
+
var weightTotal = firstWeight + secondWeight;
|
|
72
|
+
if (!first || !second || !isFinite(weightTotal) || weightTotal <= 0) return null;
|
|
73
|
+
return first.map(function (channel, index) {
|
|
74
|
+
return (channel * firstWeight + second[index] * secondWeight) / weightTotal;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
77
|
var p = m[1].split(",").map(function (s) {
|
|
78
78
|
return parseFloat(s.trim());
|
|
79
79
|
});
|
|
80
80
|
if (
|
|
81
|
+
(p.length !== 3 && p.length !== 4) ||
|
|
81
82
|
p.some(function (v) {
|
|
82
|
-
return
|
|
83
|
+
return !isFinite(v);
|
|
83
84
|
})
|
|
84
85
|
)
|
|
85
86
|
return null;
|
|
@@ -218,8 +219,12 @@ window.__contrastAuditPrepare = function () {
|
|
|
218
219
|
// `color` rather than crashing parseColor or reporting a fabricated
|
|
219
220
|
// black.
|
|
220
221
|
var isSvgText = isSvgTextElement(el);
|
|
221
|
-
var fg = isSvgText ?
|
|
222
|
+
var fg = isSvgText ? parseColor(cs.fill) || parseColor(cs.color) : parseColor(cs.color);
|
|
223
|
+
if (!fg) continue;
|
|
222
224
|
if (fg[3] <= 0.01) continue;
|
|
225
|
+
var strokeWidth = parseFloat(cs.webkitTextStrokeWidth || "0");
|
|
226
|
+
var stroke = strokeWidth > 0 ? parseColor(cs.webkitTextStrokeColor || "") : null;
|
|
227
|
+
if (stroke && stroke[3] <= 0.01) stroke = null;
|
|
223
228
|
|
|
224
229
|
var fontSize = parseFloat(cs.fontSize);
|
|
225
230
|
var fontWeight = Number(cs.fontWeight) || 400;
|
|
@@ -251,6 +256,13 @@ window.__contrastAuditPrepare = function () {
|
|
|
251
256
|
origFillPriority = el.style.getPropertyPriority("fill");
|
|
252
257
|
el.style.setProperty("fill", "transparent", "important");
|
|
253
258
|
}
|
|
259
|
+
var origStrokeColor = null,
|
|
260
|
+
origStrokeColorPriority = null;
|
|
261
|
+
if (stroke) {
|
|
262
|
+
origStrokeColor = el.style.getPropertyValue("-webkit-text-stroke-color");
|
|
263
|
+
origStrokeColorPriority = el.style.getPropertyPriority("-webkit-text-stroke-color");
|
|
264
|
+
el.style.setProperty("-webkit-text-stroke-color", "transparent", "important");
|
|
265
|
+
}
|
|
254
266
|
restores.push({
|
|
255
267
|
el: el,
|
|
256
268
|
origTransition: origTransition,
|
|
@@ -259,6 +271,9 @@ window.__contrastAuditPrepare = function () {
|
|
|
259
271
|
origColorPriority: origColorPriority,
|
|
260
272
|
origFill: origFill,
|
|
261
273
|
origFillPriority: origFillPriority,
|
|
274
|
+
origStrokeColor: origStrokeColor,
|
|
275
|
+
origStrokeColorPriority: origStrokeColorPriority,
|
|
276
|
+
hasStroke: !!stroke,
|
|
262
277
|
isSvgText: isSvgText,
|
|
263
278
|
});
|
|
264
279
|
|
|
@@ -266,6 +281,7 @@ window.__contrastAuditPrepare = function () {
|
|
|
266
281
|
selector: selectorOf(el),
|
|
267
282
|
text: (el.textContent || "").trim().slice(0, 50),
|
|
268
283
|
fg: fg,
|
|
284
|
+
stroke: stroke,
|
|
269
285
|
fontSize: fontSize,
|
|
270
286
|
fontWeight: fontWeight,
|
|
271
287
|
large: large,
|
|
@@ -287,6 +303,15 @@ function __contrastAuditRestoreAll() {
|
|
|
287
303
|
if (r.origFill) r.el.style.setProperty("fill", r.origFill, r.origFillPriority);
|
|
288
304
|
else r.el.style.removeProperty("fill");
|
|
289
305
|
}
|
|
306
|
+
if (r.hasStroke) {
|
|
307
|
+
if (r.origStrokeColor)
|
|
308
|
+
r.el.style.setProperty(
|
|
309
|
+
"-webkit-text-stroke-color",
|
|
310
|
+
r.origStrokeColor,
|
|
311
|
+
r.origStrokeColorPriority,
|
|
312
|
+
);
|
|
313
|
+
else r.el.style.removeProperty("-webkit-text-stroke-color");
|
|
314
|
+
}
|
|
290
315
|
if (r.origTransition)
|
|
291
316
|
r.el.style.setProperty("transition", r.origTransition, r.origTransitionPriority);
|
|
292
317
|
else r.el.style.removeProperty("transition");
|
|
@@ -371,17 +396,25 @@ window.__contrastAuditFinish = async function (imgBase64, time, candidates) {
|
|
|
371
396
|
var stepY = Math.max(1, Math.floor((y1 - y0) / 6));
|
|
372
397
|
var rr = [],
|
|
373
398
|
gg = [],
|
|
374
|
-
bb = []
|
|
399
|
+
bb = [],
|
|
400
|
+
aa = [];
|
|
375
401
|
for (var y = y0; y <= y1; y += stepY) {
|
|
376
402
|
for (var x = x0; x <= x1; x += stepX) {
|
|
377
403
|
var idx = (y * w + x) * 4;
|
|
378
404
|
rr.push(px[idx]);
|
|
379
405
|
gg.push(px[idx + 1]);
|
|
380
406
|
bb.push(px[idx + 2]);
|
|
407
|
+
aa.push(px[idx + 3]);
|
|
381
408
|
}
|
|
382
409
|
}
|
|
383
410
|
if (rr.length === 0) continue;
|
|
384
411
|
|
|
412
|
+
// A transparent sampled backdrop is supplied by the downstream editor,
|
|
413
|
+
// not this composition. WCAG contrast cannot be inferred until that live
|
|
414
|
+
// video/image is known, so do not invent an opaque browser default and
|
|
415
|
+
// hard-fail an otherwise valid alpha overlay.
|
|
416
|
+
if (median(aa) < 255) continue;
|
|
417
|
+
|
|
385
418
|
var bgR = median(rr),
|
|
386
419
|
bgG = median(gg),
|
|
387
420
|
bgB = median(bb);
|
|
@@ -394,6 +427,23 @@ window.__contrastAuditFinish = async function (imgBase64, time, candidates) {
|
|
|
394
427
|
|
|
395
428
|
var ratio = +wcagRatio(compR, compG, compB, bgR, bgG, bgB).toFixed(2);
|
|
396
429
|
|
|
430
|
+
// A solid text stroke is part of the visible glyph paint. Use whichever
|
|
431
|
+
// of fill or stroke provides the stronger edge contrast, rather than
|
|
432
|
+
// failing readable outlined captions based on fill alone.
|
|
433
|
+
if (c.stroke) {
|
|
434
|
+
var stroke = c.stroke;
|
|
435
|
+
var strokeR = Math.round(stroke[0] * stroke[3] + bgR * (1 - stroke[3]));
|
|
436
|
+
var strokeG = Math.round(stroke[1] * stroke[3] + bgG * (1 - stroke[3]));
|
|
437
|
+
var strokeB = Math.round(stroke[2] * stroke[3] + bgB * (1 - stroke[3]));
|
|
438
|
+
var strokeRatio = +wcagRatio(strokeR, strokeG, strokeB, bgR, bgG, bgB).toFixed(2);
|
|
439
|
+
if (strokeRatio > ratio) {
|
|
440
|
+
compR = strokeR;
|
|
441
|
+
compG = strokeG;
|
|
442
|
+
compB = strokeB;
|
|
443
|
+
ratio = strokeRatio;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
397
447
|
out.push({
|
|
398
448
|
time: time,
|
|
399
449
|
selector: c.selector,
|
|
@@ -105,6 +105,10 @@
|
|
|
105
105
|
return !!element.closest("[data-layout-allow-overflow]");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function hasTextClipOptOut(element) {
|
|
109
|
+
return hasAllowOverflowFlag(element) || element.hasAttribute("data-layout-bleed");
|
|
110
|
+
}
|
|
111
|
+
|
|
108
112
|
function opacityChain(element) {
|
|
109
113
|
let opacity = 1;
|
|
110
114
|
for (let current = element; current; current = current.parentElement) {
|
|
@@ -394,6 +398,7 @@
|
|
|
394
398
|
}
|
|
395
399
|
|
|
396
400
|
function clippedTextIssue(element, time, tolerance) {
|
|
401
|
+
if (hasTextClipOptOut(element)) return null;
|
|
397
402
|
const style = getComputedStyle(element);
|
|
398
403
|
if (!clipsOverflow(style)) return null;
|
|
399
404
|
const overflowX = element.scrollWidth - element.clientWidth;
|
|
@@ -455,7 +460,7 @@
|
|
|
455
460
|
const containerOverflow = overflowFor(textRect, containerRect, tolerance, verticalTolerance);
|
|
456
461
|
if (
|
|
457
462
|
containerOverflow &&
|
|
458
|
-
!
|
|
463
|
+
!hasTextClipOptOut(element) &&
|
|
459
464
|
!clippedByAncestor(element, container)
|
|
460
465
|
) {
|
|
461
466
|
const style = elementStyle;
|
|
@@ -481,7 +486,7 @@
|
|
|
481
486
|
}
|
|
482
487
|
|
|
483
488
|
const canvasOverflow = overflowFor(textRect, rootRect, tolerance);
|
|
484
|
-
if (canvasOverflow && !
|
|
489
|
+
if (canvasOverflow && !hasTextClipOptOut(element)) {
|
|
485
490
|
issues.push({
|
|
486
491
|
code: "canvas_overflow",
|
|
487
492
|
severity: "info",
|
|
@@ -730,13 +735,117 @@
|
|
|
730
735
|
|
|
731
736
|
const RASTER_TAGS = new Set(["IMG", "VIDEO", "CANVAS"]);
|
|
732
737
|
const FRAME_MEDIA_TAGS = new Set([...RASTER_TAGS, "SVG"]);
|
|
738
|
+
const imageAlphaCanvases = new WeakMap();
|
|
739
|
+
|
|
740
|
+
function objectPositionOffset(value, freeSpace) {
|
|
741
|
+
const token = String(value || "50%")
|
|
742
|
+
.trim()
|
|
743
|
+
.split(/\s+/)[0];
|
|
744
|
+
if (token === "left" || token === "top") return 0;
|
|
745
|
+
if (token === "right" || token === "bottom") return freeSpace;
|
|
746
|
+
if (token === "center") return freeSpace / 2;
|
|
747
|
+
if (token.endsWith("%")) return (freeSpace * parseFloat(token)) / 100;
|
|
748
|
+
const pixels = parseFloat(token);
|
|
749
|
+
return Number.isFinite(pixels) ? pixels : freeSpace / 2;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function objectPositionOffsets(value, freeX, freeY) {
|
|
753
|
+
const tokens = String(value || "50% 50%")
|
|
754
|
+
.trim()
|
|
755
|
+
.split(/\s+/)
|
|
756
|
+
.slice(0, 2);
|
|
757
|
+
let x = "50%";
|
|
758
|
+
let y = "50%";
|
|
759
|
+
if (tokens.length === 1) {
|
|
760
|
+
if (tokens[0] === "top" || tokens[0] === "bottom") y = tokens[0];
|
|
761
|
+
else x = tokens[0];
|
|
762
|
+
} else {
|
|
763
|
+
for (const token of tokens) {
|
|
764
|
+
if (token === "top" || token === "bottom") y = token;
|
|
765
|
+
else if (token === "left" || token === "right") x = token;
|
|
766
|
+
else if (x === "50%") x = token;
|
|
767
|
+
else y = token;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return { x: objectPositionOffset(x, freeX), y: objectPositionOffset(y, freeY) };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Return the alpha painted by an <img> at a viewport point. `null` means the
|
|
774
|
+
// browser would not let us inspect the image (not loaded or cross-origin), in
|
|
775
|
+
// which case callers preserve the conservative opaque fallback.
|
|
776
|
+
function imageAlphaAt(element, x, y) {
|
|
777
|
+
const sourceWidth = element.naturalWidth;
|
|
778
|
+
const sourceHeight = element.naturalHeight;
|
|
779
|
+
const rect = element.getBoundingClientRect();
|
|
780
|
+
if (!sourceWidth || !sourceHeight || !rect.width || !rect.height) return null;
|
|
781
|
+
|
|
782
|
+
const style = getComputedStyle(element);
|
|
783
|
+
const fit = style.objectFit || "fill";
|
|
784
|
+
let scaleX = rect.width / sourceWidth;
|
|
785
|
+
let scaleY = rect.height / sourceHeight;
|
|
786
|
+
if (fit !== "fill") {
|
|
787
|
+
const contain = Math.min(scaleX, scaleY);
|
|
788
|
+
const cover = Math.max(scaleX, scaleY);
|
|
789
|
+
const scale =
|
|
790
|
+
fit === "cover"
|
|
791
|
+
? cover
|
|
792
|
+
: fit === "none"
|
|
793
|
+
? 1
|
|
794
|
+
: fit === "scale-down"
|
|
795
|
+
? Math.min(1, contain)
|
|
796
|
+
: contain;
|
|
797
|
+
scaleX = scale;
|
|
798
|
+
scaleY = scale;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const paintedWidth = sourceWidth * scaleX;
|
|
802
|
+
const paintedHeight = sourceHeight * scaleY;
|
|
803
|
+
const offsets = objectPositionOffsets(
|
|
804
|
+
style.objectPosition,
|
|
805
|
+
rect.width - paintedWidth,
|
|
806
|
+
rect.height - paintedHeight,
|
|
807
|
+
);
|
|
808
|
+
const localX = x - rect.left - offsets.x;
|
|
809
|
+
const localY = y - rect.top - offsets.y;
|
|
810
|
+
if (localX < 0 || localY < 0 || localX >= paintedWidth || localY >= paintedHeight) return 0;
|
|
811
|
+
|
|
812
|
+
try {
|
|
813
|
+
let cached = imageAlphaCanvases.get(element);
|
|
814
|
+
const source = element.currentSrc || element.src;
|
|
815
|
+
if (
|
|
816
|
+
!cached ||
|
|
817
|
+
cached.width !== sourceWidth ||
|
|
818
|
+
cached.height !== sourceHeight ||
|
|
819
|
+
cached.source !== source
|
|
820
|
+
) {
|
|
821
|
+
const canvas = document.createElement("canvas");
|
|
822
|
+
canvas.width = sourceWidth;
|
|
823
|
+
canvas.height = sourceHeight;
|
|
824
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
825
|
+
if (!context) return null;
|
|
826
|
+
context.drawImage(element, 0, 0, sourceWidth, sourceHeight);
|
|
827
|
+
cached = { context, width: sourceWidth, height: sourceHeight, source };
|
|
828
|
+
imageAlphaCanvases.set(element, cached);
|
|
829
|
+
}
|
|
830
|
+
const sourceX = Math.min(sourceWidth - 1, Math.max(0, Math.floor(localX / scaleX)));
|
|
831
|
+
const sourceY = Math.min(sourceHeight - 1, Math.max(0, Math.floor(localY / scaleY)));
|
|
832
|
+
return cached.context.getImageData(sourceX, sourceY, 1, 1).data[3] / 255;
|
|
833
|
+
} catch {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
733
837
|
|
|
734
838
|
// An element hides text beneath it when it paints opaque pixels at near-full
|
|
735
839
|
// opacity: raster content (img/video/canvas), a background image, or a solid
|
|
736
840
|
// background colour. Low-opacity overlays (grain, scrims) do not occlude.
|
|
737
|
-
function isOpaqueOccluder(element) {
|
|
738
|
-
|
|
841
|
+
function isOpaqueOccluder(element, x, y) {
|
|
842
|
+
const opacity = opacityChain(element);
|
|
843
|
+
if (opacity < 0.6) return false;
|
|
739
844
|
if (IGNORE_TAGS.has(element.tagName)) return false;
|
|
845
|
+
if (element.tagName === "IMG") {
|
|
846
|
+
const alpha = imageAlphaAt(element, x, y);
|
|
847
|
+
if (alpha !== null) return alpha * opacity >= 0.6;
|
|
848
|
+
}
|
|
740
849
|
if (RASTER_TAGS.has(element.tagName)) return true;
|
|
741
850
|
return hasOpaqueBackground(getComputedStyle(element));
|
|
742
851
|
}
|
|
@@ -798,7 +907,7 @@
|
|
|
798
907
|
// Pair-specific exemptions excuse this hit only; keep walking for deeper occluders.
|
|
799
908
|
if (sharedPreserve3d(element, hit)) continue;
|
|
800
909
|
if (isCrossSceneTransitionOverlap(element, hit)) continue;
|
|
801
|
-
if (isOpaqueOccluder(hit)) return hit;
|
|
910
|
+
if (isOpaqueOccluder(hit, x, y)) return hit;
|
|
802
911
|
}
|
|
803
912
|
return null;
|
|
804
913
|
}
|