@variance-authority/presentation 0.1.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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/dist/alignment.d.ts +9 -0
- package/dist/alignment.js +100 -0
- package/dist/alignment.js.map +1 -0
- package/dist/analyze.d.ts +20 -0
- package/dist/analyze.js +132 -0
- package/dist/analyze.js.map +1 -0
- package/dist/browser-agent-entry.d.ts +2 -0
- package/dist/browser-agent-entry.js +8 -0
- package/dist/browser-agent-entry.js.map +1 -0
- package/dist/browser-agent.bundle.js +3587 -0
- package/dist/browser-agent.d.ts +37 -0
- package/dist/browser-agent.js +130 -0
- package/dist/browser-agent.js.map +1 -0
- package/dist/bundle.d.ts +3 -0
- package/dist/bundle.js +20 -0
- package/dist/bundle.js.map +1 -0
- package/dist/compare.d.ts +4 -0
- package/dist/compare.js +49 -0
- package/dist/compare.js.map +1 -0
- package/dist/findings.d.ts +5 -0
- package/dist/findings.js +285 -0
- package/dist/findings.js.map +1 -0
- package/dist/focus.d.ts +17 -0
- package/dist/focus.js +84 -0
- package/dist/focus.js.map +1 -0
- package/dist/graph.d.ts +11 -0
- package/dist/graph.js +145 -0
- package/dist/graph.js.map +1 -0
- package/dist/hierarchy.d.ts +4 -0
- package/dist/hierarchy.js +171 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +24 -0
- package/dist/math.js +124 -0
- package/dist/math.js.map +1 -0
- package/dist/measure.d.ts +15 -0
- package/dist/measure.js +216 -0
- package/dist/measure.js.map +1 -0
- package/dist/model.d.ts +346 -0
- package/dist/model.js +2 -0
- package/dist/model.js.map +1 -0
- package/dist/paint.d.ts +12 -0
- package/dist/paint.js +192 -0
- package/dist/paint.js.map +1 -0
- package/dist/patterns.d.ts +10 -0
- package/dist/patterns.js +148 -0
- package/dist/patterns.js.map +1 -0
- package/dist/playwright.d.ts +27 -0
- package/dist/playwright.js +108 -0
- package/dist/playwright.js.map +1 -0
- package/dist/report.d.ts +17 -0
- package/dist/report.js +126 -0
- package/dist/report.js.map +1 -0
- package/dist/spacing.d.ts +4 -0
- package/dist/spacing.js +112 -0
- package/dist/spacing.js.map +1 -0
- package/dist/telemetry.d.ts +4 -0
- package/dist/telemetry.js +73 -0
- package/dist/telemetry.js.map +1 -0
- package/mark.svg +30 -0
- package/package.json +62 -0
- package/skills/variance-presentation/SKILL.md +463 -0
package/dist/patterns.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { codeUnitCompare, median, round } from './math.js';
|
|
2
|
+
export function inferPatterns(graph, measured) {
|
|
3
|
+
const patterns = [];
|
|
4
|
+
const peerRelations = [];
|
|
5
|
+
const surfaceByNode = new Map();
|
|
6
|
+
for (const group of measured.surfaces)
|
|
7
|
+
for (const member of group.members)
|
|
8
|
+
surfaceByNode.set(member, group.id);
|
|
9
|
+
for (const parent of measured.nodes) {
|
|
10
|
+
const groups = new Map();
|
|
11
|
+
for (const childId of parent.children) {
|
|
12
|
+
const child = measured.nodes.find((candidate) => candidate.id === childId);
|
|
13
|
+
if (child === undefined)
|
|
14
|
+
continue;
|
|
15
|
+
const shape = semanticShape(child, graph);
|
|
16
|
+
const members = groups.get(shape) ?? [];
|
|
17
|
+
members.push(child);
|
|
18
|
+
groups.set(shape, members);
|
|
19
|
+
}
|
|
20
|
+
for (const [shape, instances] of [...groups.entries()].sort(([a], [b]) => codeUnitCompare(a, b))) {
|
|
21
|
+
if (instances.length < 2)
|
|
22
|
+
continue;
|
|
23
|
+
const id = `R${patterns.length + 1}`;
|
|
24
|
+
const signatures = instances.map((node) => signatureOf(node, measured, surfaceByNode));
|
|
25
|
+
const dominant = dominantSignature(signatures);
|
|
26
|
+
const deviations = signatures.map((signature) => signatureDeviation(signature, dominant));
|
|
27
|
+
const stateMode = mode(instances.map((node) => node.stateSignature));
|
|
28
|
+
const dominantInstances = instances
|
|
29
|
+
.filter((_node, index) => deviations[index] <= 0.3)
|
|
30
|
+
.map((node) => node.id);
|
|
31
|
+
const outliers = instances.flatMap((node, index) => {
|
|
32
|
+
const deviation = deviations[index];
|
|
33
|
+
return deviation <= 0.3
|
|
34
|
+
? []
|
|
35
|
+
: [{
|
|
36
|
+
node: node.id,
|
|
37
|
+
deviation: round(deviation),
|
|
38
|
+
explainedByState: node.stateSignature !== stateMode && node.stateSignature.length > 0,
|
|
39
|
+
}];
|
|
40
|
+
});
|
|
41
|
+
const similarity = Math.max(0, 1 - (median(deviations) ?? 0));
|
|
42
|
+
patterns.push({
|
|
43
|
+
id,
|
|
44
|
+
parent: parent.id,
|
|
45
|
+
semanticShape: shape,
|
|
46
|
+
instances: instances.map((node) => node.id),
|
|
47
|
+
recurringLabels: recurringLabels(instances, graph),
|
|
48
|
+
presentationSimilarity: round(similarity),
|
|
49
|
+
dominant,
|
|
50
|
+
dominantInstances,
|
|
51
|
+
outliers,
|
|
52
|
+
});
|
|
53
|
+
for (let index = 0; index < instances.length - 1; index += 1) {
|
|
54
|
+
const from = instances[index].id;
|
|
55
|
+
const to = instances[index + 1].id;
|
|
56
|
+
peerRelations.push({ id: `peer:${id}:${from}:${to}`, kind: 'semantic-peer', from, to });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { patterns, peerRelations };
|
|
61
|
+
}
|
|
62
|
+
function semanticShape(node, graph) {
|
|
63
|
+
const children = node.children
|
|
64
|
+
.map((id) => graph.byId.get(id)?.semanticClass)
|
|
65
|
+
.filter((value) => value !== undefined);
|
|
66
|
+
return `${node.semanticClass}[${children.join(',')}]`;
|
|
67
|
+
}
|
|
68
|
+
function signatureOf(node, measured, surfaceByNode) {
|
|
69
|
+
const internal = measured.relations.filter((relation) => relation.kind === 'separates' &&
|
|
70
|
+
node.children.includes(relation.from) &&
|
|
71
|
+
node.children.includes(relation.to));
|
|
72
|
+
const external = measured.relations.filter((relation) => relation.kind === 'separates' && (relation.from === node.id || relation.to === node.id));
|
|
73
|
+
const internalGapPx = median(internal.flatMap((relation) => relation.distancePx ?? []));
|
|
74
|
+
const boundaryStrength = median(external.flatMap((relation) => relation.boundaryStrength ?? []));
|
|
75
|
+
return {
|
|
76
|
+
widthPx: node.rect.width,
|
|
77
|
+
heightPx: node.rect.height,
|
|
78
|
+
leftPx: node.rect.x,
|
|
79
|
+
...(internalGapPx === undefined ? {} : { internalGapPx: round(internalGapPx, 2) }),
|
|
80
|
+
...(boundaryStrength === undefined ? {} : { boundaryStrength: round(boundaryStrength) }),
|
|
81
|
+
...(node.prominence.cluster === undefined ? {} : { prominenceCluster: node.prominence.cluster }),
|
|
82
|
+
...(surfaceByNode.get(node.id) === undefined ? {} : { surfaceGroup: surfaceByNode.get(node.id) }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function dominantSignature(signatures) {
|
|
86
|
+
const numbers = (select) => median(signatures.flatMap((signature) => select(signature) ?? []));
|
|
87
|
+
const internalGapPx = numbers((signature) => signature.internalGapPx);
|
|
88
|
+
const boundaryStrength = numbers((signature) => signature.boundaryStrength);
|
|
89
|
+
const prominenceCluster = mode(signatures.map((signature) => signature.prominenceCluster ?? ''));
|
|
90
|
+
const surfaceGroup = mode(signatures.map((signature) => signature.surfaceGroup ?? ''));
|
|
91
|
+
return {
|
|
92
|
+
widthPx: round(numbers((signature) => signature.widthPx) ?? 0, 2),
|
|
93
|
+
heightPx: round(numbers((signature) => signature.heightPx) ?? 0, 2),
|
|
94
|
+
leftPx: round(numbers((signature) => signature.leftPx) ?? 0, 2),
|
|
95
|
+
...(internalGapPx === undefined ? {} : { internalGapPx: round(internalGapPx, 2) }),
|
|
96
|
+
...(boundaryStrength === undefined ? {} : { boundaryStrength: round(boundaryStrength) }),
|
|
97
|
+
...(prominenceCluster.length === 0 ? {} : { prominenceCluster }),
|
|
98
|
+
...(surfaceGroup.length === 0 ? {} : { surfaceGroup }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function signatureDeviation(actual, dominant) {
|
|
102
|
+
const relative = (left, right, floor) => Math.abs(left - right) / Math.max(floor, Math.abs(right));
|
|
103
|
+
const values = [
|
|
104
|
+
relative(actual.widthPx, dominant.widthPx, 8),
|
|
105
|
+
relative(actual.heightPx, dominant.heightPx, 16) * 0.35,
|
|
106
|
+
Math.abs(actual.leftPx - dominant.leftPx) / 8,
|
|
107
|
+
];
|
|
108
|
+
if (actual.internalGapPx !== undefined && dominant.internalGapPx !== undefined) {
|
|
109
|
+
values.push(relative(actual.internalGapPx, dominant.internalGapPx, 4));
|
|
110
|
+
}
|
|
111
|
+
if (actual.prominenceCluster !== dominant.prominenceCluster)
|
|
112
|
+
values.push(0.5);
|
|
113
|
+
if (actual.surfaceGroup !== dominant.surfaceGroup)
|
|
114
|
+
values.push(0.5);
|
|
115
|
+
return Math.max(...values);
|
|
116
|
+
}
|
|
117
|
+
function recurringLabels(instances, graph) {
|
|
118
|
+
const labelSets = instances.map((instance) => {
|
|
119
|
+
const labels = descendants(instance, graph)
|
|
120
|
+
.flatMap((node) => node.name ?? node.text ?? [])
|
|
121
|
+
.map((label) => label.trim())
|
|
122
|
+
.filter((label) => label.length > 0 && label.length <= 80);
|
|
123
|
+
return new Set(labels);
|
|
124
|
+
});
|
|
125
|
+
const first = labelSets[0];
|
|
126
|
+
if (first === undefined)
|
|
127
|
+
return [];
|
|
128
|
+
return [...first]
|
|
129
|
+
.filter((label) => labelSets.every((labels) => labels.has(label)))
|
|
130
|
+
.sort(codeUnitCompare);
|
|
131
|
+
}
|
|
132
|
+
export function descendants(node, graph) {
|
|
133
|
+
const result = [];
|
|
134
|
+
for (const id of node.children) {
|
|
135
|
+
const child = graph.byId.get(id);
|
|
136
|
+
if (child === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
result.push(child, ...descendants(child, graph));
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
function mode(values) {
|
|
143
|
+
const counts = new Map();
|
|
144
|
+
for (const value of values)
|
|
145
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
146
|
+
return [...counts.entries()].sort((left, right) => right[1] - left[1] || codeUnitCompare(left[0], right[0]))[0]?.[0] ?? '';
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../src/patterns.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BuiltGraph } from './graph.js';\nimport type { Measurements } from './measure.js';\nimport type {\n PresentationNode,\n PresentationRelation,\n PresentationSignature,\n RepeatedPattern,\n} from './model.js';\nimport { codeUnitCompare, median, round } from './math.js';\n\nexport interface PatternResult {\n readonly patterns: RepeatedPattern[];\n readonly peerRelations: PresentationRelation[];\n}\n\nexport function inferPatterns(graph: BuiltGraph, measured: Measurements): PatternResult {\n const patterns: RepeatedPattern[] = [];\n const peerRelations: PresentationRelation[] = [];\n const surfaceByNode = new Map<string, string>();\n for (const group of measured.surfaces) for (const member of group.members) surfaceByNode.set(member, group.id);\n\n for (const parent of measured.nodes) {\n const groups = new Map<string, PresentationNode[]>();\n for (const childId of parent.children) {\n const child = measured.nodes.find((candidate) => candidate.id === childId);\n if (child === undefined) continue;\n const shape = semanticShape(child, graph);\n const members = groups.get(shape) ?? [];\n members.push(child);\n groups.set(shape, members);\n }\n for (const [shape, instances] of [...groups.entries()].sort(([a], [b]) => codeUnitCompare(a, b))) {\n if (instances.length < 2) continue;\n const id = `R${patterns.length + 1}`;\n const signatures = instances.map((node) => signatureOf(node, measured, surfaceByNode));\n const dominant = dominantSignature(signatures);\n const deviations = signatures.map((signature) => signatureDeviation(signature, dominant));\n const stateMode = mode(instances.map((node) => node.stateSignature));\n const dominantInstances = instances\n .filter((_node, index) => deviations[index]! <= 0.3)\n .map((node) => node.id);\n const outliers = instances.flatMap((node, index) => {\n const deviation = deviations[index]!;\n return deviation <= 0.3\n ? []\n : [{\n node: node.id,\n deviation: round(deviation),\n explainedByState: node.stateSignature !== stateMode && node.stateSignature.length > 0,\n }];\n });\n const similarity = Math.max(0, 1 - (median(deviations) ?? 0));\n patterns.push({\n id,\n parent: parent.id,\n semanticShape: shape,\n instances: instances.map((node) => node.id),\n recurringLabels: recurringLabels(instances, graph),\n presentationSimilarity: round(similarity),\n dominant,\n dominantInstances,\n outliers,\n });\n for (let index = 0; index < instances.length - 1; index += 1) {\n const from = instances[index]!.id;\n const to = instances[index + 1]!.id;\n peerRelations.push({ id: `peer:${id}:${from}:${to}`, kind: 'semantic-peer', from, to });\n }\n }\n }\n return { patterns, peerRelations };\n}\n\nfunction semanticShape(node: PresentationNode, graph: BuiltGraph): string {\n const children = node.children\n .map((id) => graph.byId.get(id)?.semanticClass)\n .filter((value): value is string => value !== undefined);\n return `${node.semanticClass}[${children.join(',')}]`;\n}\n\nfunction signatureOf(\n node: PresentationNode,\n measured: Measurements,\n surfaceByNode: ReadonlyMap<string, string>,\n): PresentationSignature {\n const internal = measured.relations.filter(\n (relation) =>\n relation.kind === 'separates' &&\n node.children.includes(relation.from) &&\n node.children.includes(relation.to),\n );\n const external = measured.relations.filter(\n (relation) =>\n relation.kind === 'separates' && (relation.from === node.id || relation.to === node.id),\n );\n const internalGapPx = median(internal.flatMap((relation) => relation.distancePx ?? []));\n const boundaryStrength = median(external.flatMap((relation) => relation.boundaryStrength ?? []));\n return {\n widthPx: node.rect.width,\n heightPx: node.rect.height,\n leftPx: node.rect.x,\n ...(internalGapPx === undefined ? {} : { internalGapPx: round(internalGapPx, 2) }),\n ...(boundaryStrength === undefined ? {} : { boundaryStrength: round(boundaryStrength) }),\n ...(node.prominence.cluster === undefined ? {} : { prominenceCluster: node.prominence.cluster }),\n ...(surfaceByNode.get(node.id) === undefined ? {} : { surfaceGroup: surfaceByNode.get(node.id)! }),\n };\n}\n\nfunction dominantSignature(signatures: readonly PresentationSignature[]): PresentationSignature {\n const numbers = (select: (signature: PresentationSignature) => number | undefined): number | undefined =>\n median(signatures.flatMap((signature) => select(signature) ?? []));\n const internalGapPx = numbers((signature) => signature.internalGapPx);\n const boundaryStrength = numbers((signature) => signature.boundaryStrength);\n const prominenceCluster = mode(signatures.map((signature) => signature.prominenceCluster ?? ''));\n const surfaceGroup = mode(signatures.map((signature) => signature.surfaceGroup ?? ''));\n return {\n widthPx: round(numbers((signature) => signature.widthPx) ?? 0, 2),\n heightPx: round(numbers((signature) => signature.heightPx) ?? 0, 2),\n leftPx: round(numbers((signature) => signature.leftPx) ?? 0, 2),\n ...(internalGapPx === undefined ? {} : { internalGapPx: round(internalGapPx, 2) }),\n ...(boundaryStrength === undefined ? {} : { boundaryStrength: round(boundaryStrength) }),\n ...(prominenceCluster.length === 0 ? {} : { prominenceCluster }),\n ...(surfaceGroup.length === 0 ? {} : { surfaceGroup }),\n };\n}\n\nfunction signatureDeviation(actual: PresentationSignature, dominant: PresentationSignature): number {\n const relative = (left: number, right: number, floor: number): number =>\n Math.abs(left - right) / Math.max(floor, Math.abs(right));\n const values = [\n relative(actual.widthPx, dominant.widthPx, 8),\n relative(actual.heightPx, dominant.heightPx, 16) * 0.35,\n Math.abs(actual.leftPx - dominant.leftPx) / 8,\n ];\n if (actual.internalGapPx !== undefined && dominant.internalGapPx !== undefined) {\n values.push(relative(actual.internalGapPx, dominant.internalGapPx, 4));\n }\n if (actual.prominenceCluster !== dominant.prominenceCluster) values.push(0.5);\n if (actual.surfaceGroup !== dominant.surfaceGroup) values.push(0.5);\n return Math.max(...values);\n}\n\nfunction recurringLabels(instances: readonly PresentationNode[], graph: BuiltGraph): string[] {\n const labelSets = instances.map((instance) => {\n const labels = descendants(instance, graph)\n .flatMap((node) => node.name ?? node.text ?? [])\n .map((label) => label.trim())\n .filter((label) => label.length > 0 && label.length <= 80);\n return new Set(labels);\n });\n const first = labelSets[0];\n if (first === undefined) return [];\n return [...first]\n .filter((label) => labelSets.every((labels) => labels.has(label)))\n .sort(codeUnitCompare);\n}\n\nexport function descendants(node: PresentationNode, graph: BuiltGraph): PresentationNode[] {\n const result: PresentationNode[] = [];\n for (const id of node.children) {\n const child = graph.byId.get(id);\n if (child === undefined) continue;\n result.push(child, ...descendants(child, graph));\n }\n return result;\n}\n\nfunction mode(values: readonly string[]): string {\n const counts = new Map<string, number>();\n for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);\n return [...counts.entries()].sort((left, right) => right[1] - left[1] || codeUnitCompare(left[0], right[0]))[0]?.[0] ?? '';\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Locator, Page } from '@playwright/test';
|
|
2
|
+
import type { PaintLayer, PresentationAlignmentReading, PresentationFocus, PresentationHierarchyReading, PresentationReport, PresentationSpacingReading } from './model.js';
|
|
3
|
+
export interface SensePresentationOptions {
|
|
4
|
+
readonly subjectId?: string;
|
|
5
|
+
readonly title?: string;
|
|
6
|
+
readonly fonts?: readonly string[];
|
|
7
|
+
readonly suspense?: {
|
|
8
|
+
readonly timeoutMs?: number;
|
|
9
|
+
};
|
|
10
|
+
/** Override stabilization ids; use `[]` only when the caller owns a static document. */
|
|
11
|
+
readonly stabilize?: readonly string[];
|
|
12
|
+
/** `true` paints every layer; a layer list paints only those measurements. */
|
|
13
|
+
readonly paint?: boolean | readonly PaintLayer[];
|
|
14
|
+
}
|
|
15
|
+
/** Sense and optionally paint one locator without creating a baseline or verdict. */
|
|
16
|
+
export declare function sensePresentation(page: Page, locator: Locator, options?: SensePresentationOptions): Promise<PresentationReport>;
|
|
17
|
+
/** Remove diagnostic paint without changing the sensed application. */
|
|
18
|
+
export declare function clearPresentationPaint(page: Page): Promise<void>;
|
|
19
|
+
/** Paint one already-focused structural reading without acquiring the page again. */
|
|
20
|
+
export declare function paintPresentationFocus(page: Page, focus: PresentationFocus, layers?: readonly PaintLayer[]): Promise<number>;
|
|
21
|
+
/** Paint one explicit alignment reading without acquiring the page again. */
|
|
22
|
+
export declare function paintPresentationAlignment(page: Page, reading: PresentationAlignmentReading): Promise<number>;
|
|
23
|
+
/** Paint one explicit spacing reading without acquiring the page again. */
|
|
24
|
+
export declare function paintPresentationSpacing(page: Page, reading: PresentationSpacingReading): Promise<number>;
|
|
25
|
+
/** Paint one product-declared hierarchy reading without acquiring the page again. */
|
|
26
|
+
export declare function paintPresentationHierarchy(page: Page, reading: PresentationHierarchyReading): Promise<number>;
|
|
27
|
+
//# sourceMappingURL=playwright.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { accessibilitySnapshot } from '@variance-authority/core';
|
|
2
|
+
import { analyzePresentation } from './analyze.js';
|
|
3
|
+
import { PRESENTATION_AGENT, PRESENTATION_ROOT_ATTRIBUTE, } from './browser-agent.js';
|
|
4
|
+
import { bundlePresentationAgent } from './bundle.js';
|
|
5
|
+
/** Sense and optionally paint one locator without creating a baseline or verdict. */
|
|
6
|
+
export async function sensePresentation(page, locator, options = {}) {
|
|
7
|
+
await install(page);
|
|
8
|
+
const viewport = page.viewportSize() ?? (await page.evaluate(() => ({ width: innerWidth, height: innerHeight })));
|
|
9
|
+
const environment = await page.evaluate(() => ({
|
|
10
|
+
deviceScaleFactor: devicePixelRatio,
|
|
11
|
+
colorScheme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
|
12
|
+
}));
|
|
13
|
+
const browser = page.context().browser();
|
|
14
|
+
const request = {
|
|
15
|
+
subject: {
|
|
16
|
+
id: options.subjectId ?? 'presentation',
|
|
17
|
+
kind: 'fixture',
|
|
18
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
19
|
+
},
|
|
20
|
+
viewport: { ...viewport, ...environment },
|
|
21
|
+
engine: browser === null ? 'browser@unknown' : `${browser.browserType().name()}@${browser.version()}`,
|
|
22
|
+
...(options.fonts === undefined ? {} : { fonts: options.fonts }),
|
|
23
|
+
...(options.suspense === undefined ? {} : { suspense: options.suspense }),
|
|
24
|
+
...(options.stabilize === undefined ? {} : { stabilize: options.stabilize }),
|
|
25
|
+
};
|
|
26
|
+
const raw = await locator.evaluate((element, [global, sent]) => {
|
|
27
|
+
const agent = globalThis[global];
|
|
28
|
+
if (agent === undefined)
|
|
29
|
+
throw new Error(`the presentation sensing agent is not installed at ${global}`);
|
|
30
|
+
return agent.acquire(element, sent);
|
|
31
|
+
}, [PRESENTATION_AGENT, request]);
|
|
32
|
+
const acquired = JSON.parse(raw);
|
|
33
|
+
try {
|
|
34
|
+
const roots = [await locator.ariaSnapshot()];
|
|
35
|
+
for (const portal of acquired.portals) {
|
|
36
|
+
roots.push(await page.locator(`[${PRESENTATION_ROOT_ATTRIBUTE}="${portal.marker}"]`).ariaSnapshot());
|
|
37
|
+
}
|
|
38
|
+
const report = analyzePresentation(acquired.capture, {
|
|
39
|
+
accessibility: accessibilitySnapshot(request.engine, roots),
|
|
40
|
+
});
|
|
41
|
+
if (options.paint !== undefined && options.paint !== false) {
|
|
42
|
+
const layers = options.paint === true ? undefined : options.paint;
|
|
43
|
+
await page.evaluate(([global, instructions, selected]) => {
|
|
44
|
+
const agent = globalThis[global];
|
|
45
|
+
if (agent === undefined)
|
|
46
|
+
throw new Error(`the presentation sensing agent is not installed at ${global}`);
|
|
47
|
+
return agent.paint(instructions, selected);
|
|
48
|
+
}, [PRESENTATION_AGENT, report.paint ?? [], layers]);
|
|
49
|
+
}
|
|
50
|
+
return report;
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await restorePortals(page, acquired);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Remove diagnostic paint without changing the sensed application. */
|
|
57
|
+
export async function clearPresentationPaint(page) {
|
|
58
|
+
await page.evaluate((global) => {
|
|
59
|
+
const agent = globalThis[global];
|
|
60
|
+
agent?.clear();
|
|
61
|
+
}, PRESENTATION_AGENT);
|
|
62
|
+
}
|
|
63
|
+
/** Paint one already-focused structural reading without acquiring the page again. */
|
|
64
|
+
export async function paintPresentationFocus(page, focus, layers) {
|
|
65
|
+
return paintPresentationInstructions(page, focus.paint, layers);
|
|
66
|
+
}
|
|
67
|
+
/** Paint one explicit alignment reading without acquiring the page again. */
|
|
68
|
+
export async function paintPresentationAlignment(page, reading) {
|
|
69
|
+
return paintPresentationInstructions(page, reading.paint, ['axes']);
|
|
70
|
+
}
|
|
71
|
+
/** Paint one explicit spacing reading without acquiring the page again. */
|
|
72
|
+
export async function paintPresentationSpacing(page, reading) {
|
|
73
|
+
return paintPresentationInstructions(page, reading.paint, ['spacing']);
|
|
74
|
+
}
|
|
75
|
+
/** Paint one product-declared hierarchy reading without acquiring the page again. */
|
|
76
|
+
export async function paintPresentationHierarchy(page, reading) {
|
|
77
|
+
return paintPresentationInstructions(page, reading.paint, ['findings']);
|
|
78
|
+
}
|
|
79
|
+
async function paintPresentationInstructions(page, instructions, layers) {
|
|
80
|
+
await install(page);
|
|
81
|
+
return page.evaluate(([global, instructions, selected]) => {
|
|
82
|
+
const agent = globalThis[global];
|
|
83
|
+
if (agent === undefined)
|
|
84
|
+
throw new Error(`the presentation sensing agent is not installed at ${global}`);
|
|
85
|
+
return agent.paint(instructions, selected);
|
|
86
|
+
}, [PRESENTATION_AGENT, instructions, layers]);
|
|
87
|
+
}
|
|
88
|
+
async function install(page) {
|
|
89
|
+
const bundle = await bundlePresentationAgent();
|
|
90
|
+
await page.addInitScript(bundle);
|
|
91
|
+
const installed = await page.evaluate((global) => typeof globalThis[global] === 'object', PRESENTATION_AGENT);
|
|
92
|
+
if (!installed)
|
|
93
|
+
await page.evaluate(bundle);
|
|
94
|
+
}
|
|
95
|
+
async function restorePortals(page, acquired) {
|
|
96
|
+
await page.evaluate(([attribute, portals]) => {
|
|
97
|
+
for (const portal of portals) {
|
|
98
|
+
const element = document.querySelector(`[${attribute}="${portal.marker}"]`);
|
|
99
|
+
if (element === null)
|
|
100
|
+
continue;
|
|
101
|
+
if (portal.previous === undefined)
|
|
102
|
+
element.removeAttribute(attribute);
|
|
103
|
+
else
|
|
104
|
+
element.setAttribute(attribute, portal.previous);
|
|
105
|
+
}
|
|
106
|
+
}, [PRESENTATION_ROOT_ATTRIBUTE, acquired.portals]);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=playwright.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playwright.js","sourceRoot":"","sources":["../src/playwright.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Locator, Page } from '@playwright/test';\nimport { accessibilitySnapshot } from '@variance-authority/core';\nimport type {\n PaintInstruction,\n PaintLayer,\n PresentationAlignmentReading,\n PresentationFocus,\n PresentationHierarchyReading,\n PresentationReport,\n PresentationSpacingReading,\n} from './model.js';\nimport { analyzePresentation } from './analyze.js';\nimport {\n PRESENTATION_AGENT,\n PRESENTATION_ROOT_ATTRIBUTE,\n type InstalledPresentationAgent,\n type PresentationAcquireRequest,\n type PresentationAcquired,\n} from './browser-agent.js';\nimport { bundlePresentationAgent } from './bundle.js';\n\nexport interface SensePresentationOptions {\n readonly subjectId?: string;\n readonly title?: string;\n readonly fonts?: readonly string[];\n readonly suspense?: { readonly timeoutMs?: number };\n /** Override stabilization ids; use `[]` only when the caller owns a static document. */\n readonly stabilize?: readonly string[];\n /** `true` paints every layer; a layer list paints only those measurements. */\n readonly paint?: boolean | readonly PaintLayer[];\n}\n\n/** Sense and optionally paint one locator without creating a baseline or verdict. */\nexport async function sensePresentation(\n page: Page,\n locator: Locator,\n options: SensePresentationOptions = {},\n): Promise<PresentationReport> {\n await install(page);\n const viewport = page.viewportSize() ?? (await page.evaluate(() => ({ width: innerWidth, height: innerHeight })));\n const environment = await page.evaluate(() => ({\n deviceScaleFactor: devicePixelRatio,\n colorScheme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' as const : 'light' as const,\n }));\n const browser = page.context().browser();\n const request: PresentationAcquireRequest = {\n subject: {\n id: options.subjectId ?? 'presentation',\n kind: 'fixture',\n ...(options.title === undefined ? {} : { title: options.title }),\n },\n viewport: { ...viewport, ...environment },\n engine: browser === null ? 'browser@unknown' : `${browser.browserType().name()}@${browser.version()}`,\n ...(options.fonts === undefined ? {} : { fonts: options.fonts }),\n ...(options.suspense === undefined ? {} : { suspense: options.suspense }),\n ...(options.stabilize === undefined ? {} : { stabilize: options.stabilize }),\n };\n const raw = await locator.evaluate(\n (element, [global, sent]: readonly [string, PresentationAcquireRequest]) => {\n const agent = (globalThis as unknown as Record<string, InstalledPresentationAgent | undefined>)[global];\n if (agent === undefined) throw new Error(`the presentation sensing agent is not installed at ${global}`);\n return agent.acquire(element, sent);\n },\n [PRESENTATION_AGENT, request] as const,\n );\n const acquired = JSON.parse(raw) as PresentationAcquired;\n try {\n const roots = [await locator.ariaSnapshot()];\n for (const portal of acquired.portals) {\n roots.push(await page.locator(`[${PRESENTATION_ROOT_ATTRIBUTE}=\"${portal.marker}\"]`).ariaSnapshot());\n }\n const report = analyzePresentation(acquired.capture, {\n accessibility: accessibilitySnapshot(request.engine, roots),\n });\n if (options.paint !== undefined && options.paint !== false) {\n const layers = options.paint === true ? undefined : options.paint;\n await page.evaluate(\n ([global, instructions, selected]) => {\n const agent = (globalThis as unknown as Record<string, InstalledPresentationAgent | undefined>)[global];\n if (agent === undefined) throw new Error(`the presentation sensing agent is not installed at ${global}`);\n return agent.paint(instructions, selected);\n },\n [PRESENTATION_AGENT, report.paint ?? [], layers] as const,\n );\n }\n return report;\n } finally {\n await restorePortals(page, acquired);\n }\n}\n\n/** Remove diagnostic paint without changing the sensed application. */\nexport async function clearPresentationPaint(page: Page): Promise<void> {\n await page.evaluate((global) => {\n const agent = (globalThis as unknown as Record<string, InstalledPresentationAgent | undefined>)[global];\n agent?.clear();\n }, PRESENTATION_AGENT);\n}\n\n/** Paint one already-focused structural reading without acquiring the page again. */\nexport async function paintPresentationFocus(\n page: Page,\n focus: PresentationFocus,\n layers?: readonly PaintLayer[],\n): Promise<number> {\n return paintPresentationInstructions(page, focus.paint, layers);\n}\n\n/** Paint one explicit alignment reading without acquiring the page again. */\nexport async function paintPresentationAlignment(\n page: Page,\n reading: PresentationAlignmentReading,\n): Promise<number> {\n return paintPresentationInstructions(page, reading.paint, ['axes']);\n}\n\n/** Paint one explicit spacing reading without acquiring the page again. */\nexport async function paintPresentationSpacing(\n page: Page,\n reading: PresentationSpacingReading,\n): Promise<number> {\n return paintPresentationInstructions(page, reading.paint, ['spacing']);\n}\n\n/** Paint one product-declared hierarchy reading without acquiring the page again. */\nexport async function paintPresentationHierarchy(\n page: Page,\n reading: PresentationHierarchyReading,\n): Promise<number> {\n return paintPresentationInstructions(page, reading.paint, ['findings']);\n}\n\nasync function paintPresentationInstructions(\n page: Page,\n instructions: readonly PaintInstruction[],\n layers?: readonly PaintLayer[],\n): Promise<number> {\n await install(page);\n return page.evaluate(\n ([global, instructions, selected]) => {\n const agent = (globalThis as unknown as Record<string, InstalledPresentationAgent | undefined>)[global];\n if (agent === undefined) throw new Error(`the presentation sensing agent is not installed at ${global}`);\n return agent.paint(instructions, selected);\n },\n [PRESENTATION_AGENT, instructions, layers] as const,\n );\n}\n\nasync function install(page: Page): Promise<void> {\n const bundle = await bundlePresentationAgent();\n await page.addInitScript(bundle);\n const installed = await page.evaluate(\n (global) => typeof (globalThis as unknown as Record<string, unknown>)[global] === 'object',\n PRESENTATION_AGENT,\n );\n if (!installed) await page.evaluate(bundle);\n}\n\nasync function restorePortals(page: Page, acquired: PresentationAcquired): Promise<void> {\n await page.evaluate(\n ([attribute, portals]) => {\n for (const portal of portals) {\n const element = document.querySelector(`[${attribute}=\"${portal.marker}\"]`);\n if (element === null) continue;\n if (portal.previous === undefined) element.removeAttribute(attribute);\n else element.setAttribute(attribute, portal.previous);\n }\n },\n [PRESENTATION_ROOT_ATTRIBUTE, acquired.portals] as const,\n );\n}\n"]}
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PresentationSignalRecord } from '@variance-authority/report';
|
|
2
|
+
import type { PresentationHierarchyReading, PresentationReport } from './model.js';
|
|
3
|
+
export interface PresentationSignalOptions {
|
|
4
|
+
/** Product-owned hierarchy readings evaluated against the before report. */
|
|
5
|
+
readonly beforeHierarchy?: readonly PresentationHierarchyReading[];
|
|
6
|
+
/** Product-owned hierarchy readings evaluated against the after report. */
|
|
7
|
+
readonly afterHierarchy?: readonly PresentationHierarchyReading[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Project presentation evidence into the durable regression-report signal.
|
|
11
|
+
*
|
|
12
|
+
* Missing reports or layout findings are incomparable, never clean. The result
|
|
13
|
+
* records consequences without changing the regression verdict or prescribing
|
|
14
|
+
* a design response.
|
|
15
|
+
*/
|
|
16
|
+
export declare function presentationSignal(before: PresentationReport | undefined, after: PresentationReport | undefined, options?: PresentationSignalOptions): PresentationSignalRecord;
|
|
17
|
+
//# sourceMappingURL=report.d.ts.map
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { digestValue } from '@variance-authority/core';
|
|
2
|
+
import { comparePresentation } from './compare.js';
|
|
3
|
+
import { codeUnitCompare } from './math.js';
|
|
4
|
+
/**
|
|
5
|
+
* Project presentation evidence into the durable regression-report signal.
|
|
6
|
+
*
|
|
7
|
+
* Missing reports or layout findings are incomparable, never clean. The result
|
|
8
|
+
* records consequences without changing the regression verdict or prescribing
|
|
9
|
+
* a design response.
|
|
10
|
+
*/
|
|
11
|
+
export function presentationSignal(before, after, options = {}) {
|
|
12
|
+
if (before === undefined || after === undefined) {
|
|
13
|
+
return {
|
|
14
|
+
verdict: 'incomparable',
|
|
15
|
+
because: before === undefined && after === undefined
|
|
16
|
+
? 'neither side supplied a presentation reading'
|
|
17
|
+
: `${before === undefined ? 'the baseline' : 'the candidate'} supplied no presentation reading`,
|
|
18
|
+
...(before === undefined ? {} : { before: before.digest }),
|
|
19
|
+
...(after === undefined ? {} : { after: after.digest }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const beforeFindings = findingsOf(before, options.beforeHierarchy ?? []);
|
|
23
|
+
const afterFindings = findingsOf(after, options.afterHierarchy ?? []);
|
|
24
|
+
if (beforeFindings === undefined || afterFindings === undefined) {
|
|
25
|
+
return {
|
|
26
|
+
verdict: 'incomparable',
|
|
27
|
+
because: beforeFindings === undefined && afterFindings === undefined
|
|
28
|
+
? 'neither presentation reading contained layout findings'
|
|
29
|
+
: `${beforeFindings === undefined ? 'the baseline' : 'the candidate'} contained no layout findings`,
|
|
30
|
+
before: before.digest,
|
|
31
|
+
after: after.digest,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const comparison = comparePresentation(before, after);
|
|
35
|
+
const information = {
|
|
36
|
+
contentPreserved: comparison.information.content.preserved,
|
|
37
|
+
characters: difference(comparison.information.characters),
|
|
38
|
+
elements: difference(comparison.information.elements),
|
|
39
|
+
repeatedObjects: difference(comparison.information.repeatedObjects),
|
|
40
|
+
};
|
|
41
|
+
const effects = effectsOf(beforeFindings, afterFindings);
|
|
42
|
+
const informationChanged = !information.contentPreserved ||
|
|
43
|
+
information.characters.delta !== 0 ||
|
|
44
|
+
information.elements.delta !== 0 ||
|
|
45
|
+
information.repeatedObjects.delta !== 0;
|
|
46
|
+
return {
|
|
47
|
+
verdict: effects.length > 0 || informationChanged ? 'changed' : 'unchanged',
|
|
48
|
+
before: before.digest,
|
|
49
|
+
after: after.digest,
|
|
50
|
+
information,
|
|
51
|
+
effects,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function findingsOf(report, hierarchy) {
|
|
55
|
+
for (const reading of hierarchy) {
|
|
56
|
+
if (reading.report !== report.digest) {
|
|
57
|
+
throw new Error(`presentation hierarchy ${reading.contract.id} reads ${reading.report}, not ${report.digest}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (report.findings === undefined && hierarchy.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return [...(report.findings ?? []), ...hierarchy.flatMap((reading) => reading.findings)];
|
|
63
|
+
}
|
|
64
|
+
function effectsOf(before, after) {
|
|
65
|
+
const left = indexFindings(before, 'baseline');
|
|
66
|
+
const right = indexFindings(after, 'candidate');
|
|
67
|
+
const identities = [...new Set([...left.keys(), ...right.keys()])].sort(codeUnitCompare);
|
|
68
|
+
const effects = [];
|
|
69
|
+
for (const identity of identities) {
|
|
70
|
+
const previous = left.get(identity);
|
|
71
|
+
const current = right.get(identity);
|
|
72
|
+
const finding = current ?? previous;
|
|
73
|
+
const common = {
|
|
74
|
+
rule: finding.rule,
|
|
75
|
+
owner: finding.owner,
|
|
76
|
+
nodes: [...finding.nodes].sort(codeUnitCompare),
|
|
77
|
+
...(finding.pattern === undefined ? {} : { pattern: finding.pattern }),
|
|
78
|
+
...(finding.contract === undefined ? {} : { contract: finding.contract }),
|
|
79
|
+
};
|
|
80
|
+
if (previous === undefined) {
|
|
81
|
+
effects.push({ ...common, transition: 'introduced', after: evidence(current) });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (current === undefined) {
|
|
85
|
+
effects.push({ ...common, transition: 'resolved', before: evidence(previous) });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (digestValue(previous.measurements) === digestValue(current.measurements)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
effects.push({
|
|
92
|
+
...common,
|
|
93
|
+
transition: 'persisted',
|
|
94
|
+
before: evidence(previous),
|
|
95
|
+
after: evidence(current),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return effects;
|
|
99
|
+
}
|
|
100
|
+
function indexFindings(findings, side) {
|
|
101
|
+
const result = new Map();
|
|
102
|
+
for (const finding of findings) {
|
|
103
|
+
const identity = identityOf(finding);
|
|
104
|
+
if (result.has(identity)) {
|
|
105
|
+
throw new Error(`${side} presentation has duplicate finding identity ${identity}`);
|
|
106
|
+
}
|
|
107
|
+
result.set(identity, finding);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
function identityOf(finding) {
|
|
112
|
+
return [
|
|
113
|
+
finding.rule,
|
|
114
|
+
finding.owner,
|
|
115
|
+
finding.pattern ?? '',
|
|
116
|
+
finding.contract ?? '',
|
|
117
|
+
[...finding.nodes].sort(codeUnitCompare).join(','),
|
|
118
|
+
].join('\0');
|
|
119
|
+
}
|
|
120
|
+
function evidence(finding) {
|
|
121
|
+
return { finding: finding.id, measurements: finding.measurements };
|
|
122
|
+
}
|
|
123
|
+
function difference(value) {
|
|
124
|
+
return { ...value, delta: value.after - value.before };
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["import { digestValue } from '@variance-authority/core';\nimport type {\n PresentationEffectEvidence,\n PresentationEffectRecord,\n PresentationInformationRecord,\n PresentationSignalRecord,\n} from '@variance-authority/report';\nimport { comparePresentation } from './compare.js';\nimport { codeUnitCompare } from './math.js';\nimport type {\n PresentationFinding,\n PresentationHierarchyReading,\n PresentationReport,\n} from './model.js';\n\nexport interface PresentationSignalOptions {\n /** Product-owned hierarchy readings evaluated against the before report. */\n readonly beforeHierarchy?: readonly PresentationHierarchyReading[];\n /** Product-owned hierarchy readings evaluated against the after report. */\n readonly afterHierarchy?: readonly PresentationHierarchyReading[];\n}\n\n/**\n * Project presentation evidence into the durable regression-report signal.\n *\n * Missing reports or layout findings are incomparable, never clean. The result\n * records consequences without changing the regression verdict or prescribing\n * a design response.\n */\nexport function presentationSignal(\n before: PresentationReport | undefined,\n after: PresentationReport | undefined,\n options: PresentationSignalOptions = {},\n): PresentationSignalRecord {\n if (before === undefined || after === undefined) {\n return {\n verdict: 'incomparable',\n because:\n before === undefined && after === undefined\n ? 'neither side supplied a presentation reading'\n : `${before === undefined ? 'the baseline' : 'the candidate'} supplied no presentation reading`,\n ...(before === undefined ? {} : { before: before.digest }),\n ...(after === undefined ? {} : { after: after.digest }),\n };\n }\n\n const beforeFindings = findingsOf(before, options.beforeHierarchy ?? []);\n const afterFindings = findingsOf(after, options.afterHierarchy ?? []);\n if (beforeFindings === undefined || afterFindings === undefined) {\n return {\n verdict: 'incomparable',\n because:\n beforeFindings === undefined && afterFindings === undefined\n ? 'neither presentation reading contained layout findings'\n : `${beforeFindings === undefined ? 'the baseline' : 'the candidate'} contained no layout findings`,\n before: before.digest,\n after: after.digest,\n };\n }\n\n const comparison = comparePresentation(before, after);\n const information: PresentationInformationRecord = {\n contentPreserved: comparison.information.content.preserved,\n characters: difference(comparison.information.characters),\n elements: difference(comparison.information.elements),\n repeatedObjects: difference(comparison.information.repeatedObjects),\n };\n const effects = effectsOf(beforeFindings, afterFindings);\n const informationChanged =\n !information.contentPreserved ||\n information.characters.delta !== 0 ||\n information.elements.delta !== 0 ||\n information.repeatedObjects.delta !== 0;\n\n return {\n verdict: effects.length > 0 || informationChanged ? 'changed' : 'unchanged',\n before: before.digest,\n after: after.digest,\n information,\n effects,\n };\n}\n\nfunction findingsOf(\n report: PresentationReport,\n hierarchy: readonly PresentationHierarchyReading[],\n): readonly PresentationFinding[] | undefined {\n for (const reading of hierarchy) {\n if (reading.report !== report.digest) {\n throw new Error(\n `presentation hierarchy ${reading.contract.id} reads ${reading.report}, not ${report.digest}`,\n );\n }\n }\n if (report.findings === undefined && hierarchy.length === 0) return undefined;\n return [...(report.findings ?? []), ...hierarchy.flatMap((reading) => reading.findings)];\n}\n\nfunction effectsOf(\n before: readonly PresentationFinding[],\n after: readonly PresentationFinding[],\n): PresentationEffectRecord[] {\n const left = indexFindings(before, 'baseline');\n const right = indexFindings(after, 'candidate');\n const identities = [...new Set([...left.keys(), ...right.keys()])].sort(codeUnitCompare);\n\n const effects: PresentationEffectRecord[] = [];\n for (const identity of identities) {\n const previous = left.get(identity);\n const current = right.get(identity);\n const finding = current ?? previous!;\n const common = {\n rule: finding.rule,\n owner: finding.owner,\n nodes: [...finding.nodes].sort(codeUnitCompare),\n ...(finding.pattern === undefined ? {} : { pattern: finding.pattern }),\n ...(finding.contract === undefined ? {} : { contract: finding.contract }),\n };\n if (previous === undefined) {\n effects.push({ ...common, transition: 'introduced', after: evidence(current!) });\n continue;\n }\n if (current === undefined) {\n effects.push({ ...common, transition: 'resolved', before: evidence(previous) });\n continue;\n }\n if (digestValue(previous.measurements) === digestValue(current.measurements)) {\n continue;\n }\n effects.push({\n ...common,\n transition: 'persisted',\n before: evidence(previous),\n after: evidence(current),\n });\n }\n return effects;\n}\n\nfunction indexFindings(\n findings: readonly PresentationFinding[],\n side: 'baseline' | 'candidate',\n): ReadonlyMap<string, PresentationFinding> {\n const result = new Map<string, PresentationFinding>();\n for (const finding of findings) {\n const identity = identityOf(finding);\n if (result.has(identity)) {\n throw new Error(`${side} presentation has duplicate finding identity ${identity}`);\n }\n result.set(identity, finding);\n }\n return result;\n}\n\nfunction identityOf(finding: PresentationFinding): string {\n return [\n finding.rule,\n finding.owner,\n finding.pattern ?? '',\n finding.contract ?? '',\n [...finding.nodes].sort(codeUnitCompare).join(','),\n ].join('\\0');\n}\n\nfunction evidence(finding: PresentationFinding): PresentationEffectEvidence {\n return { finding: finding.id, measurements: finding.measurements };\n}\n\nfunction difference(value: { readonly before: number; readonly after: number }): {\n readonly before: number;\n readonly after: number;\n readonly delta: number;\n} {\n return { ...value, delta: value.after - value.before };\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PresentationReport, PresentationSpacingAxis, PresentationSpacingReading } from './model.js';
|
|
2
|
+
/** Measure spacing across consecutive immediate children of one box or composition. */
|
|
3
|
+
export declare function inspectPresentationSpacing(report: PresentationReport, ownerId: string, members: readonly string[], axis: PresentationSpacingAxis): PresentationSpacingReading;
|
|
4
|
+
//# sourceMappingURL=spacing.d.ts.map
|
package/dist/spacing.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { median, round } from './math.js';
|
|
2
|
+
/** Measure spacing across consecutive immediate children of one box or composition. */
|
|
3
|
+
export function inspectPresentationSpacing(report, ownerId, members, axis) {
|
|
4
|
+
const byId = new Map(report.graph.nodes.map((node) => [node.id, node]));
|
|
5
|
+
const owner = byId.get(ownerId);
|
|
6
|
+
if (owner === undefined)
|
|
7
|
+
throw new Error(`presentation owner ${ownerId} is not in report ${report.digest}`);
|
|
8
|
+
const uniqueMembers = [...new Set(members)];
|
|
9
|
+
if (uniqueMembers.length < 2)
|
|
10
|
+
throw new Error('presentation spacing needs at least two distinct members');
|
|
11
|
+
const selected = uniqueMembers.map((id) => {
|
|
12
|
+
const node = byId.get(id);
|
|
13
|
+
if (node === undefined)
|
|
14
|
+
throw new Error(`presentation spacing member ${id} is not in report ${report.digest}`);
|
|
15
|
+
if (node.parent !== ownerId) {
|
|
16
|
+
throw new Error(`presentation spacing member ${id} is not an immediate child of owner ${ownerId}`);
|
|
17
|
+
}
|
|
18
|
+
return node;
|
|
19
|
+
});
|
|
20
|
+
const positions = uniqueMembers.map((id) => owner.children.indexOf(id));
|
|
21
|
+
const ordered = positions.every((position, index) => index === 0 || position === positions[index - 1] + 1);
|
|
22
|
+
if (!ordered)
|
|
23
|
+
throw new Error('presentation spacing members must be consecutive in owner child order');
|
|
24
|
+
const separations = selected.slice(0, -1).map((from, index) => {
|
|
25
|
+
const to = selected[index + 1];
|
|
26
|
+
const relation = report.graph.relations.find((candidate) => candidate.kind === 'separates' && candidate.from === from.id && candidate.to === to.id);
|
|
27
|
+
if (relation === undefined)
|
|
28
|
+
throw new Error(`presentation spacing has no separation ${from.id} → ${to.id}`);
|
|
29
|
+
const orderedOverlap = relation.axis === 'overlap' && (axis === 'vertical'
|
|
30
|
+
? to.rect.y >= from.rect.y
|
|
31
|
+
: to.rect.x >= from.rect.x);
|
|
32
|
+
if (relation.axis !== axis && !orderedOverlap) {
|
|
33
|
+
throw new Error(`presentation spacing separation ${from.id} → ${to.id} is ${relation.axis}, not ${axis}`);
|
|
34
|
+
}
|
|
35
|
+
if (relation.distancePx === undefined || relation.boundaryStrength === undefined) {
|
|
36
|
+
throw new Error(`presentation spacing separation ${from.id} → ${to.id} is incomplete`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
from,
|
|
40
|
+
to,
|
|
41
|
+
distancePx: relation.distancePx,
|
|
42
|
+
boundaryStrength: relation.boundaryStrength,
|
|
43
|
+
...(relation.spacingCluster === undefined ? {} : { spacingCluster: relation.spacingCluster }),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const distances = separations.map((separation) => separation.distancePx);
|
|
47
|
+
const boundaries = separations.map((separation) => separation.boundaryStrength);
|
|
48
|
+
return {
|
|
49
|
+
formatVersion: 1,
|
|
50
|
+
report: report.digest,
|
|
51
|
+
owner,
|
|
52
|
+
axis,
|
|
53
|
+
members: selected,
|
|
54
|
+
separations,
|
|
55
|
+
distance: pixelRange(distances),
|
|
56
|
+
boundary: scalarRange(boundaries),
|
|
57
|
+
paint: separations.map((separation) => paint(ownerId, axis, separation)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function pixelRange(values) {
|
|
61
|
+
return {
|
|
62
|
+
minPx: round(Math.min(...values), 2),
|
|
63
|
+
medianPx: round(median(values), 2),
|
|
64
|
+
maxPx: round(Math.max(...values), 2),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function scalarRange(values) {
|
|
68
|
+
return {
|
|
69
|
+
min: round(Math.min(...values)),
|
|
70
|
+
median: round(median(values)),
|
|
71
|
+
max: round(Math.max(...values)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function paint(owner, axis, separation) {
|
|
75
|
+
const label = `${separation.distancePx}px · boundary ${separation.boundaryStrength}`;
|
|
76
|
+
const id = `spacing:${separation.from.id}:${separation.to.id}`;
|
|
77
|
+
if (axis === 'vertical') {
|
|
78
|
+
const coordinate = round((separation.from.rect.y + separation.from.rect.height + separation.to.rect.y) / 2, 2);
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
owner,
|
|
82
|
+
nodes: [separation.from.id, separation.to.id],
|
|
83
|
+
layer: 'spacing',
|
|
84
|
+
shape: 'line',
|
|
85
|
+
color: '#ffca55',
|
|
86
|
+
label,
|
|
87
|
+
line: {
|
|
88
|
+
x1: Math.min(separation.from.rect.x, separation.to.rect.x),
|
|
89
|
+
y1: coordinate,
|
|
90
|
+
x2: Math.max(separation.from.rect.x + separation.from.rect.width, separation.to.rect.x + separation.to.rect.width),
|
|
91
|
+
y2: coordinate,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const coordinate = round((separation.from.rect.x + separation.from.rect.width + separation.to.rect.x) / 2, 2);
|
|
96
|
+
return {
|
|
97
|
+
id,
|
|
98
|
+
owner,
|
|
99
|
+
nodes: [separation.from.id, separation.to.id],
|
|
100
|
+
layer: 'spacing',
|
|
101
|
+
shape: 'line',
|
|
102
|
+
color: '#ffca55',
|
|
103
|
+
label,
|
|
104
|
+
line: {
|
|
105
|
+
x1: coordinate,
|
|
106
|
+
y1: Math.min(separation.from.rect.y, separation.to.rect.y),
|
|
107
|
+
x2: coordinate,
|
|
108
|
+
y2: Math.max(separation.from.rect.y + separation.from.rect.height, separation.to.rect.y + separation.to.rect.height),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=spacing.js.map
|