@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.
Files changed (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +319 -0
  4. package/dist/alignment.d.ts +9 -0
  5. package/dist/alignment.js +100 -0
  6. package/dist/alignment.js.map +1 -0
  7. package/dist/analyze.d.ts +20 -0
  8. package/dist/analyze.js +132 -0
  9. package/dist/analyze.js.map +1 -0
  10. package/dist/browser-agent-entry.d.ts +2 -0
  11. package/dist/browser-agent-entry.js +8 -0
  12. package/dist/browser-agent-entry.js.map +1 -0
  13. package/dist/browser-agent.bundle.js +3587 -0
  14. package/dist/browser-agent.d.ts +37 -0
  15. package/dist/browser-agent.js +130 -0
  16. package/dist/browser-agent.js.map +1 -0
  17. package/dist/bundle.d.ts +3 -0
  18. package/dist/bundle.js +20 -0
  19. package/dist/bundle.js.map +1 -0
  20. package/dist/compare.d.ts +4 -0
  21. package/dist/compare.js +49 -0
  22. package/dist/compare.js.map +1 -0
  23. package/dist/findings.d.ts +5 -0
  24. package/dist/findings.js +285 -0
  25. package/dist/findings.js.map +1 -0
  26. package/dist/focus.d.ts +17 -0
  27. package/dist/focus.js +84 -0
  28. package/dist/focus.js.map +1 -0
  29. package/dist/graph.d.ts +11 -0
  30. package/dist/graph.js +145 -0
  31. package/dist/graph.js.map +1 -0
  32. package/dist/hierarchy.d.ts +4 -0
  33. package/dist/hierarchy.js +171 -0
  34. package/dist/hierarchy.js.map +1 -0
  35. package/dist/index.d.ts +12 -0
  36. package/dist/index.js +8 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/math.d.ts +24 -0
  39. package/dist/math.js +124 -0
  40. package/dist/math.js.map +1 -0
  41. package/dist/measure.d.ts +15 -0
  42. package/dist/measure.js +216 -0
  43. package/dist/measure.js.map +1 -0
  44. package/dist/model.d.ts +346 -0
  45. package/dist/model.js +2 -0
  46. package/dist/model.js.map +1 -0
  47. package/dist/paint.d.ts +12 -0
  48. package/dist/paint.js +192 -0
  49. package/dist/paint.js.map +1 -0
  50. package/dist/patterns.d.ts +10 -0
  51. package/dist/patterns.js +148 -0
  52. package/dist/patterns.js.map +1 -0
  53. package/dist/playwright.d.ts +27 -0
  54. package/dist/playwright.js +108 -0
  55. package/dist/playwright.js.map +1 -0
  56. package/dist/report.d.ts +17 -0
  57. package/dist/report.js +126 -0
  58. package/dist/report.js.map +1 -0
  59. package/dist/spacing.d.ts +4 -0
  60. package/dist/spacing.js +112 -0
  61. package/dist/spacing.js.map +1 -0
  62. package/dist/telemetry.d.ts +4 -0
  63. package/dist/telemetry.js +73 -0
  64. package/dist/telemetry.js.map +1 -0
  65. package/mark.svg +30 -0
  66. package/package.json +62 -0
  67. package/skills/variance-presentation/SKILL.md +463 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # @variance-authority/presentation
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5b2a28f: Carry the caller-established font identities onto the presentation report.
8
+
9
+ `sensePresentation` accepted a `fonts` option, threaded it into the capture it
10
+ builds, then analyzed that capture and dropped it — so through the live door the
11
+ option had no observable consequence: three different font lists produced a
12
+ byte-identical report. The report now carries them, from the capture, so the
13
+ pure entry point answers the same way.
14
+
15
+ Fonts are a render input of the more consequential kind — a substitution moves
16
+ every metric on the page without changing a byte of code — so the identities
17
+ participate in `digest` and a reading taken under different ones is a different
18
+ reading. `contentDigest` is untouched: content identity is what has to survive
19
+ presentation moving. Absent rather than empty when none were established, and
20
+ sorted, because the order a caller listed them in is not part of their identity.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [1d402d1]
25
+ - Updated dependencies [f800342]
26
+ - Updated dependencies [26ae9ed]
27
+ - Updated dependencies [e8fee66]
28
+ - Updated dependencies [5c34e6d]
29
+ - @variance-authority/report@0.1.0
30
+ - @variance-authority/dom@0.1.0
31
+ - @variance-authority/react@0.1.0
32
+ - @variance-authority/core@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Machine Garden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,319 @@
1
+ <p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
2
+
3
+ # @variance-authority/presentation
4
+
5
+ > Sense spacing, alignment, prominence and repetition inside one live interface, as evidence an agent can read.
6
+
7
+ Sense one rendered subject and return a machine-readable presentation graph. The
8
+ report keeps semantic anchors attached to concrete element references, measures
9
+ geometry and presentation relationships, and exposes deterministic collapse (the
10
+ gap that should separate repeated instances, or the size difference that should
11
+ separate a heading from body text, disappearing) and drift (one instance's
12
+ position, baseline, or presentation diverging from the dominant pattern) —
13
+ without recommending a layout or assigning a UI quality score.
14
+
15
+ It reads a `RawCapture` — a normalized, serializable snapshot of one rendered DOM
16
+ subject, produced by a collector such as this package's Playwright entry point.
17
+ The layout-derived evidence arrives only when that capture's profile — a
18
+ declaration of what its collector could observe (ARIA, style, layout, pixels) —
19
+ includes computed style and layout.
20
+
21
+ This is a sensing and support surface, not a visual-regression assertion. It does
22
+ not create a baseline, approve a change, or produce a pass/fail verdict; pair it
23
+ with `@variance-authority/report` (or another comparison layer) when a run needs
24
+ one.
25
+
26
+ ```bash
27
+ npm install --save-dev @variance-authority/presentation @playwright/test
28
+ npx playwright install chromium
29
+ ```
30
+
31
+ ## Sense once, then choose the structural owner
32
+
33
+ ```ts
34
+ import type { Page } from '@playwright/test';
35
+ import { focusPresentation } from '@variance-authority/presentation';
36
+ import { sensePresentation } from '@variance-authority/presentation/playwright';
37
+
38
+ declare const page: Page;
39
+
40
+ const report = await sensePresentation(page, page.getByRole('main'));
41
+ const pattern = report.patterns?.find((candidate) => candidate.instances.length >= 3);
42
+ if (pattern === undefined) throw new Error('the subject has no repeated presentation pattern');
43
+
44
+ const ownerReading = focusPresentation(report, pattern.parent, {
45
+ paint: ['repetition', 'findings'],
46
+ });
47
+
48
+ console.log(ownerReading.owner, ownerReading.findings, ownerReading.nested);
49
+ ```
50
+
51
+ Playwright supplies the live layout and its ARIA snapshot. The browser agent
52
+ can paint from the same report it returns, but acquisition is normally left
53
+ unpainted. Its default settlement — the waits, for fonts, images, and
54
+ animations, that hold a page still before it is measured — covers images inside
55
+ the subject and its React portals, not unrelated images elsewhere in the
56
+ document.
57
+ `focusPresentation` reads one owner from that report without touching
58
+ the page again. Its default `owner` depth includes the owner and its immediate
59
+ children; evidence owned by nested boxes is counted in `nested` rather than
60
+ folded into the current reading. Use `depth: 'subtree'` only when the product
61
+ question deliberately treats the complete composition as one subject.
62
+
63
+ Focus options are `depth`, `paint`, and `findings`. `depth` defaults to `owner`;
64
+ `paint` limits retained layers; `findings` limits the reading to report-local
65
+ finding ids owned at the selected depth.
66
+
67
+ Every finding — a detected rule violation such as `SEPARATION_COLLISION`, tying
68
+ specific graph nodes to a measurement — has a stable report-local `id` and the
69
+ graph-node `owner` whose relationship produced it. Pass `findings: [id]` to
70
+ isolate one question. Paint that focused evidence without another acquisition:
71
+
72
+ ```ts
73
+ import type { Page } from '@playwright/test';
74
+ import { focusPresentation, type PresentationReport } from '@variance-authority/presentation';
75
+ import { paintPresentationFocus } from '@variance-authority/presentation/playwright';
76
+
77
+ declare const page: Page;
78
+ declare const report: PresentationReport;
79
+
80
+ const finding = report.findings?.[0];
81
+ if (finding !== undefined) {
82
+ const isolated = focusPresentation(report, finding.owner, {
83
+ findings: [finding.id],
84
+ paint: ['findings'],
85
+ });
86
+ await paintPresentationFocus(page, isolated);
87
+ }
88
+ ```
89
+
90
+ Call `clearPresentationPaint(page)` to remove the overlay. No ARIA, or a partial
91
+ ARIA tree with no parent or children, remains an observed value rather than an
92
+ acquisition error.
93
+
94
+ `subjectId` and `title` identify the sensed boundary in the returned report.
95
+ `fonts` records the browser fonts whose identities the caller has established;
96
+ the report carries them back, and a substitution changes its digest while
97
+ content identity — a digest of semantic classes, names, text, state, and the
98
+ browser ARIA reading, independent of layout — holds.
99
+ `suspense` controls settlement of React boundaries before sensing. `paint` is
100
+ either `true` for every diagnostic layer (`semantic`, `spacing`, `axes`,
101
+ `baselines`, `surfaces`, `prominence`, `repetition`, `findings`) or a list of
102
+ named layers; omitting it leaves the page unpainted.
103
+
104
+ `stabilize` replaces the default collection recipe by intervention id. Pass an
105
+ empty list only for a caller-owned static document such as an MHTML archive whose
106
+ page clock cannot advance; a live page normally needs the default recipe.
107
+
108
+ Use the pure entry point below when another collector already supplies a
109
+ `RawCapture`.
110
+
111
+ ## Inspect a visual flow across nested boxes
112
+
113
+ Use this when peers — a brand mark and navigation controls, say — live in
114
+ different DOM wrappers but should be read as one visual flow: select their graph
115
+ node ids explicitly. A node id encodes a path from a capture root, so
116
+ `r0:0/1/2` is capture root `0`, its child `1`, and that child's child `2`; get
117
+ real ids from `report.graph.nodes` rather than hardcoding them as this example
118
+ does:
119
+
120
+ ```ts
121
+ import type { Page } from '@playwright/test';
122
+ import {
123
+ inspectPresentationAlignment,
124
+ type PresentationReport,
125
+ } from '@variance-authority/presentation';
126
+ import { paintPresentationAlignment } from '@variance-authority/presentation/playwright';
127
+
128
+ declare const page: Page;
129
+ declare const report: PresentationReport;
130
+
131
+ const reading = inspectPresentationAlignment(
132
+ report,
133
+ 'r0:0',
134
+ ['r0:0/0/0', 'r0:0/1/0', 'r0:0/1/1', 'r0:0/1/2'],
135
+ 'vertical-center',
136
+ );
137
+
138
+ console.log(reading.coordinatePx, reading.spreadPx, reading.members);
139
+ await paintPresentationAlignment(page, reading);
140
+ ```
141
+
142
+ The owner must contain every selected member, and at least two distinct members
143
+ are required. Members must sit at one structural level — a wrapper and its own
144
+ nested control sharing a semantic role do not count as two peers. The result
145
+ reports coordinates and deviations; it does not add a finding. Its `paint`
146
+ marks the median axis and every selected member with its signed deviation;
147
+ painting reuses the report and does not acquire the page.
148
+
149
+ ## Inspect spacing at a composition owner
150
+
151
+ Select consecutive immediate children at the owner that arranges them — they
152
+ don't need matching semantic shape to belong to one composition:
153
+
154
+ ```ts
155
+ import {
156
+ inspectPresentationSpacing,
157
+ type PresentationReport,
158
+ } from '@variance-authority/presentation';
159
+ import type { Page } from '@playwright/test';
160
+ import { paintPresentationSpacing } from '@variance-authority/presentation/playwright';
161
+
162
+ declare const browserPage: Page;
163
+ declare const report: PresentationReport;
164
+
165
+ const page = report.graph.nodes.find((node) => node.parent === undefined)!;
166
+ const reading = inspectPresentationSpacing(report, page.id, page.children, 'vertical');
167
+
168
+ console.log(reading.distance, reading.boundary, reading.separations);
169
+ await paintPresentationSpacing(browserPage, reading);
170
+ ```
171
+
172
+ The reading preserves every adjacent distance, boundary strength and spacing
173
+ cluster, plus min/median/max summaries. Members must be consecutive immediate
174
+ children in the owner's structural order. The result is evidence — distances and
175
+ clusters — not a preferred gap or an automatic finding.
176
+
177
+ ## Declare relationship roles instead of trusting tokens
178
+
179
+ Declare which measured separations in an existing report stand for which
180
+ product-defined role, independent of the design-system token that implemented
181
+ the spacing:
182
+
183
+ ```ts
184
+ import {
185
+ inspectPresentationHierarchy,
186
+ type PresentationHierarchyContract,
187
+ type PresentationReport,
188
+ } from '@variance-authority/presentation';
189
+ import type { Page } from '@playwright/test';
190
+ import { paintPresentationHierarchy } from '@variance-authority/presentation/playwright';
191
+
192
+ declare const page: Page;
193
+ declare const report: PresentationReport;
194
+
195
+ const contract: PresentationHierarchyContract = {
196
+ id: 'demand-record',
197
+ owner: 'r0:0',
198
+ axis: 'vertical',
199
+ levels: [
200
+ { role: 'owner-boundary', relations: [{ from: 'r0:0/0', to: 'r0:0/1' }] },
201
+ { role: 'leading-to-body', relations: [{ from: 'r0:0/1/0', to: 'r0:0/1/1' }] },
202
+ { role: 'body-peer', relations: [{ from: 'r0:0/1/1', to: 'r0:0/1/2' }] },
203
+ { role: 'content-internal', relations: [{ from: 'r0:0/1/1/0', to: 'r0:0/1/1/1' }] },
204
+ ],
205
+ };
206
+ const hierarchy = inspectPresentationHierarchy(report, contract);
207
+
208
+ console.log(hierarchy.levels, hierarchy.collisions, hierarchy.findings);
209
+ await paintPresentationHierarchy(page, hierarchy);
210
+ ```
211
+
212
+ Roles are ordered outside-in and may not repeat. Every relationship must be a
213
+ measured separation inside the declared owner, and one pair cannot hold two
214
+ roles. Adjacent roles occupying the same spacing cluster or calibrated
215
+ distribution produce `SPACING_HIERARCHY_COLLISION` — a finding that two declared
216
+ roles are not actually distinguishable in the rendered spacing; the result does
217
+ not choose a replacement value. Paint labels each declared role.
218
+
219
+ ## Analyze one capture
220
+
221
+ ```ts
222
+ import type { AccessibilitySnapshot, RawCapture } from '@variance-authority/core';
223
+ import { analyzePresentation } from '@variance-authority/presentation';
224
+
225
+ declare const capture: RawCapture;
226
+ declare const browserAccessibility: AccessibilitySnapshot;
227
+
228
+ const report = analyzePresentation(capture, {
229
+ accessibility: browserAccessibility,
230
+ });
231
+
232
+ console.log(report.telemetry, report.patterns, report.findings);
233
+ ```
234
+
235
+ `telemetry` records content volume, dimensions, utilization, and density as
236
+ neutral context. It never creates a finding independently. `patterns` describe
237
+ repeated semantic shapes and their dominant presentation signatures. `findings`
238
+ contain measurements before prose: the involved nodes, pattern, coordinates,
239
+ ratios, distances, or cluster identities.
240
+
241
+ The browser accessibility snapshot is retained as an independent semantic
242
+ reading. The `accessibility` option omits that reading only when the browser did
243
+ not observe it. An empty root or a root with no parent or children is an observed
244
+ value and remains present without repair.
245
+
246
+ A capture whose profile cannot observe layout still produces content telemetry
247
+ and semantic anchors. Its layout-derived `patterns`, `findings`, clusters, and
248
+ paint instructions are absent rather than empty. A layout-capable capture that
249
+ omits an element rect is refused.
250
+
251
+ ## Re-sense after an edit
252
+
253
+ ```ts
254
+ import type { PresentationReport } from '@variance-authority/presentation';
255
+ import { comparePresentation } from '@variance-authority/presentation';
256
+
257
+ declare const before: PresentationReport;
258
+ declare const after: PresentationReport;
259
+
260
+ const change = comparePresentation(before, after);
261
+ console.log(change.findings, change.information);
262
+ ```
263
+
264
+ `change.findings` and `change.information` are reported separately, never
265
+ folded into a global score or verdict. `information` holds the content identity
266
+ plus the element, character, and repeated-object counts. A matching content
267
+ identity proves content held while presentation moved; it is derived before
268
+ layout analysis, so the same check works for a capture whose layout is
269
+ unobserved. Re-sensing can therefore show fewer findings resolved alongside an
270
+ unchanged content identity, and no stored baseline is required.
271
+
272
+ ## Carry presentation impact into a run report
273
+
274
+ Use `presentationSignal` when the same consequence must survive the browser
275
+ session and travel with a general regression report:
276
+
277
+ ```ts
278
+ import type { ObservationRecord } from '@variance-authority/report';
279
+ import {
280
+ presentationSignal,
281
+ type PresentationHierarchyReading,
282
+ type PresentationReport,
283
+ } from '@variance-authority/presentation';
284
+
285
+ declare const before: PresentationReport;
286
+ declare const after: PresentationReport;
287
+ declare const beforeHierarchy: PresentationHierarchyReading;
288
+ declare const afterHierarchy: PresentationHierarchyReading;
289
+
290
+ const presentation = presentationSignal(before, after, {
291
+ beforeHierarchy: [beforeHierarchy],
292
+ afterHierarchy: [afterHierarchy],
293
+ });
294
+
295
+ const observation: ObservationRecord = {
296
+ subject: 'underwriting:demands',
297
+ verdict: 'changed',
298
+ because: 'the candidate differs from its baseline',
299
+ changedPixels: 320,
300
+ regions: [],
301
+ signals: { presentation },
302
+ };
303
+
304
+ console.log(observation.signals?.presentation);
305
+ ```
306
+
307
+ The function combines automatic findings with the supplied product-owned
308
+ hierarchy readings, matching relationship identities across the two reports and
309
+ recording introduced, resolved, and measurement-changing effects.
310
+ `beforeHierarchy` and `afterHierarchy` are optional; omit both when automatic
311
+ findings are the complete evidence for the subject. Missing reports or layout
312
+ findings produce `incomparable`, never an empty clean list. The content
313
+ identity and information counts travel alongside the effects.
314
+
315
+ The stored signal reports consequence only, not severity: renderer impact —
316
+ whether a changed CSS property can only repaint or must also reflow the page —
317
+ still answers how far a change can reach, and project policy still decides
318
+ whether any finding blocks a run. A collector that participates in the general
319
+ regression pipeline returns this value as its optional `presentation` field.
@@ -0,0 +1,9 @@
1
+ import type { PresentationAlignmentKind, PresentationAlignmentReading, PresentationReport } from './model.js';
2
+ /**
3
+ * Measure an explicitly chosen visual flow, including peers nested in different boxes.
4
+ *
5
+ * Selection remains with the product-aware caller. The reading supplies coordinates
6
+ * and deviations without converting their spread into a design verdict.
7
+ */
8
+ export declare function inspectPresentationAlignment(report: PresentationReport, ownerId: string, members: readonly string[], kind: PresentationAlignmentKind): PresentationAlignmentReading;
9
+ //# sourceMappingURL=alignment.d.ts.map
@@ -0,0 +1,100 @@
1
+ import { codeUnitCompare, median, round } from './math.js';
2
+ /**
3
+ * Measure an explicitly chosen visual flow, including peers nested in different boxes.
4
+ *
5
+ * Selection remains with the product-aware caller. The reading supplies coordinates
6
+ * and deviations without converting their spread into a design verdict.
7
+ */
8
+ export function inspectPresentationAlignment(report, ownerId, members, kind) {
9
+ const byId = new Map(report.graph.nodes.map((node) => [node.id, node]));
10
+ const owner = byId.get(ownerId);
11
+ if (owner === undefined)
12
+ throw new Error(`presentation owner ${ownerId} is not in report ${report.digest}`);
13
+ const uniqueMembers = [...new Set(members)].sort(codeUnitCompare);
14
+ if (uniqueMembers.length < 2)
15
+ throw new Error('presentation alignment needs at least two distinct members');
16
+ const descendants = new Set(descendantIds(owner, byId));
17
+ const selected = uniqueMembers.map((id) => {
18
+ const node = byId.get(id);
19
+ if (node === undefined)
20
+ throw new Error(`presentation alignment member ${id} is not in report ${report.digest}`);
21
+ if (!descendants.has(id))
22
+ throw new Error(`presentation alignment member ${id} is not inside owner ${ownerId}`);
23
+ return node;
24
+ });
25
+ const values = selected.map((node) => coordinate(node, kind));
26
+ const center = median(values);
27
+ const membersWithCoordinates = selected.map((node) => {
28
+ const coordinatePx = coordinate(node, kind);
29
+ return { node, coordinatePx: round(coordinatePx, 2), deviationPx: round(coordinatePx - center, 2) };
30
+ });
31
+ const horizontal = ['top', 'bottom', 'vertical-center'].includes(kind);
32
+ const readingId = `alignment:${kind}:${uniqueMembers.join('+')}`;
33
+ return {
34
+ formatVersion: 1,
35
+ report: report.digest,
36
+ owner,
37
+ kind,
38
+ coordinatePx: round(center, 2),
39
+ spreadPx: round(Math.max(...values) - Math.min(...values), 2),
40
+ members: membersWithCoordinates,
41
+ paint: [
42
+ {
43
+ id: readingId,
44
+ owner: ownerId,
45
+ nodes: uniqueMembers,
46
+ layer: 'axes',
47
+ shape: 'line',
48
+ color: '#70ff70',
49
+ label: `${kind} ${round(center, 2)}px`,
50
+ line: horizontal
51
+ ? {
52
+ x1: Math.min(...selected.map((node) => node.rect.x)),
53
+ y1: round(center, 2),
54
+ x2: Math.max(...selected.map((node) => node.rect.x + node.rect.width)),
55
+ y2: round(center, 2),
56
+ }
57
+ : {
58
+ x1: round(center, 2),
59
+ y1: Math.min(...selected.map((node) => node.rect.y)),
60
+ x2: round(center, 2),
61
+ y2: Math.max(...selected.map((node) => node.rect.y + node.rect.height)),
62
+ },
63
+ },
64
+ ...membersWithCoordinates.map(({ node, coordinatePx, deviationPx }) => ({
65
+ id: `${readingId}:${node.id}`,
66
+ owner: ownerId,
67
+ nodes: [node.id],
68
+ layer: 'axes',
69
+ shape: 'rect',
70
+ color: '#70ff70',
71
+ label: `${coordinatePx}px (${deviationPx >= 0 ? '+' : ''}${deviationPx}px)`,
72
+ rect: node.rect,
73
+ })),
74
+ ],
75
+ };
76
+ }
77
+ function coordinate(node, kind) {
78
+ if (kind === 'left')
79
+ return node.rect.x;
80
+ if (kind === 'right')
81
+ return node.rect.x + node.rect.width;
82
+ if (kind === 'horizontal-center')
83
+ return node.rect.x + node.rect.width / 2;
84
+ if (kind === 'top')
85
+ return node.rect.y;
86
+ if (kind === 'bottom')
87
+ return node.rect.y + node.rect.height;
88
+ return node.rect.y + node.rect.height / 2;
89
+ }
90
+ function descendantIds(owner, byId) {
91
+ const result = [];
92
+ for (const id of owner.children) {
93
+ const child = byId.get(id);
94
+ if (child === undefined)
95
+ continue;
96
+ result.push(id, ...descendantIds(child, byId));
97
+ }
98
+ return result;
99
+ }
100
+ //# sourceMappingURL=alignment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alignment.js","sourceRoot":"","sources":["../src/alignment.ts"],"names":[],"mappings":"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","sourcesContent":["import { codeUnitCompare, median, round } from './math.js';\nimport type {\n PresentationAlignmentKind,\n PresentationAlignmentReading,\n PresentationNode,\n PresentationReport,\n} from './model.js';\n\n/**\n * Measure an explicitly chosen visual flow, including peers nested in different boxes.\n *\n * Selection remains with the product-aware caller. The reading supplies coordinates\n * and deviations without converting their spread into a design verdict.\n */\nexport function inspectPresentationAlignment(\n report: PresentationReport,\n ownerId: string,\n members: readonly string[],\n kind: PresentationAlignmentKind,\n): PresentationAlignmentReading {\n const byId = new Map(report.graph.nodes.map((node) => [node.id, node]));\n const owner = byId.get(ownerId);\n if (owner === undefined) throw new Error(`presentation owner ${ownerId} is not in report ${report.digest}`);\n const uniqueMembers = [...new Set(members)].sort(codeUnitCompare);\n if (uniqueMembers.length < 2) throw new Error('presentation alignment needs at least two distinct members');\n const descendants = new Set(descendantIds(owner, byId));\n const selected = uniqueMembers.map((id) => {\n const node = byId.get(id);\n if (node === undefined) throw new Error(`presentation alignment member ${id} is not in report ${report.digest}`);\n if (!descendants.has(id)) throw new Error(`presentation alignment member ${id} is not inside owner ${ownerId}`);\n return node;\n });\n const values = selected.map((node) => coordinate(node, kind));\n const center = median(values)!;\n const membersWithCoordinates = selected.map((node) => {\n const coordinatePx = coordinate(node, kind);\n return { node, coordinatePx: round(coordinatePx, 2), deviationPx: round(coordinatePx - center, 2) };\n });\n const horizontal = ['top', 'bottom', 'vertical-center'].includes(kind);\n const readingId = `alignment:${kind}:${uniqueMembers.join('+')}`;\n return {\n formatVersion: 1,\n report: report.digest,\n owner,\n kind,\n coordinatePx: round(center, 2),\n spreadPx: round(Math.max(...values) - Math.min(...values), 2),\n members: membersWithCoordinates,\n paint: [\n {\n id: readingId,\n owner: ownerId,\n nodes: uniqueMembers,\n layer: 'axes',\n shape: 'line',\n color: '#70ff70',\n label: `${kind} ${round(center, 2)}px`,\n line: horizontal\n ? {\n x1: Math.min(...selected.map((node) => node.rect.x)),\n y1: round(center, 2),\n x2: Math.max(...selected.map((node) => node.rect.x + node.rect.width)),\n y2: round(center, 2),\n }\n : {\n x1: round(center, 2),\n y1: Math.min(...selected.map((node) => node.rect.y)),\n x2: round(center, 2),\n y2: Math.max(...selected.map((node) => node.rect.y + node.rect.height)),\n },\n },\n ...membersWithCoordinates.map(({ node, coordinatePx, deviationPx }) => ({\n id: `${readingId}:${node.id}`,\n owner: ownerId,\n nodes: [node.id],\n layer: 'axes' as const,\n shape: 'rect' as const,\n color: '#70ff70',\n label: `${coordinatePx}px (${deviationPx >= 0 ? '+' : ''}${deviationPx}px)`,\n rect: node.rect,\n })),\n ],\n };\n}\n\nfunction coordinate(node: PresentationNode, kind: PresentationAlignmentKind): number {\n if (kind === 'left') return node.rect.x;\n if (kind === 'right') return node.rect.x + node.rect.width;\n if (kind === 'horizontal-center') return node.rect.x + node.rect.width / 2;\n if (kind === 'top') return node.rect.y;\n if (kind === 'bottom') return node.rect.y + node.rect.height;\n return node.rect.y + node.rect.height / 2;\n}\n\nfunction descendantIds(owner: PresentationNode, byId: ReadonlyMap<string, PresentationNode>): string[] {\n const result: string[] = [];\n for (const id of owner.children) {\n const child = byId.get(id);\n if (child === undefined) continue;\n result.push(id, ...descendantIds(child, byId));\n }\n return result;\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { type AccessibilitySnapshot, type RawCapture } from '@variance-authority/core';
2
+ import type { PresentationReport } from './model.js';
3
+ export interface AnalyzePresentationOptions {
4
+ /**
5
+ * Browser-computed ARIA evidence observed beside this capture.
6
+ *
7
+ * Omit only when it was not observed. `roots: ['']` and partial roots are
8
+ * readings and remain byte-for-byte present in the report.
9
+ */
10
+ readonly accessibility?: AccessibilitySnapshot;
11
+ }
12
+ /**
13
+ * Derive machine-readable presentation relationships from one browser capture.
14
+ *
15
+ * Telemetry is descriptive and never creates a finding by itself. Findings are
16
+ * limited to measured relationships; this function does not recommend a layout,
17
+ * remove information, or produce an overall quality score.
18
+ */
19
+ export declare function analyzePresentation(capture: RawCapture, options?: AnalyzePresentationOptions): PresentationReport;
20
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1,132 @@
1
+ import { digestValue, } from '@variance-authority/core';
2
+ import { detectFindings } from './findings.js';
3
+ import { buildGraph } from './graph.js';
4
+ import { measureGraph } from './measure.js';
5
+ import { paintInstructions } from './paint.js';
6
+ import { inferPatterns } from './patterns.js';
7
+ import { telemetryOf } from './telemetry.js';
8
+ /**
9
+ * Derive machine-readable presentation relationships from one browser capture.
10
+ *
11
+ * Telemetry is descriptive and never creates a finding by itself. Findings are
12
+ * limited to measured relationships; this function does not recommend a layout,
13
+ * remove information, or produce an overall quality score.
14
+ */
15
+ export function analyzePresentation(capture, options = {}) {
16
+ const anchors = semanticAnchors(capture);
17
+ const contentDigest = digestValue(contentEvidence(capture));
18
+ if (!capture.profile.layout) {
19
+ const body = {
20
+ formatVersion: 1,
21
+ contentDigest,
22
+ subject: capture.subject,
23
+ ...fontsOf(capture),
24
+ semantic: {
25
+ anchors,
26
+ ...(options.accessibility === undefined ? {} : { browserAccessibility: options.accessibility }),
27
+ },
28
+ telemetry: telemetryOf(capture, [], undefined),
29
+ graph: { nodes: [], relations: [] },
30
+ };
31
+ return { ...body, digest: digestValue(body) };
32
+ }
33
+ assertLayout(capture);
34
+ const graph = buildGraph(capture);
35
+ const measured = measureGraph(graph);
36
+ const inferred = inferPatterns(graph, measured);
37
+ const findings = detectFindings(graph, measured, inferred.patterns);
38
+ const relations = [...measured.relations, ...inferred.peerRelations];
39
+ const paint = paintInstructions({
40
+ nodes: measured.nodes,
41
+ relations,
42
+ axes: measured.axes,
43
+ baselines: measured.baselines,
44
+ prominence: measured.prominence,
45
+ surfaces: measured.surfaces,
46
+ patterns: inferred.patterns,
47
+ findings,
48
+ });
49
+ const body = {
50
+ formatVersion: 1,
51
+ contentDigest,
52
+ subject: capture.subject,
53
+ ...fontsOf(capture),
54
+ semantic: {
55
+ anchors,
56
+ ...(options.accessibility === undefined ? {} : { browserAccessibility: options.accessibility }),
57
+ },
58
+ telemetry: telemetryOf(capture, measured.nodes, inferred.patterns),
59
+ graph: { nodes: measured.nodes, relations },
60
+ spacing: measured.spacing,
61
+ axes: measured.axes,
62
+ baselines: measured.baselines,
63
+ prominence: measured.prominence,
64
+ surfaces: measured.surfaces,
65
+ patterns: inferred.patterns,
66
+ findings,
67
+ paint,
68
+ };
69
+ return { ...body, digest: digestValue(body) };
70
+ }
71
+ /**
72
+ * Carry the caller-established font identities onto the report.
73
+ *
74
+ * The live entry accepts a `fonts` option and threads it into the capture, and
75
+ * the capture is then analyzed and dropped — so through that door the option
76
+ * had no observable consequence at all. It has one here. Fonts are a render
77
+ * input of the more consequential kind: a substitution moves every metric on
78
+ * the page without changing a byte of code, so a reading taken under different
79
+ * pinned identities is a different reading and the digest says so.
80
+ *
81
+ * Sorted, because the order a caller happened to list them in is not part of
82
+ * their identity — the same position `normalize` takes on the environment key.
83
+ * Absent rather than empty when none were established, matching the capture,
84
+ * which says the same thing a second way with its `unverified-fonts` caveat.
85
+ * The content digest is deliberately untouched: font identity is presentation,
86
+ * and content identity is what has to survive presentation moving.
87
+ */
88
+ function fontsOf(capture) {
89
+ const fonts = capture.environment.fonts;
90
+ return fonts.length === 0 ? {} : { fonts: [...fonts].sort() };
91
+ }
92
+ function contentEvidence(capture) {
93
+ const read = (node) => ({
94
+ tag: node.tag,
95
+ ...(node.aria === undefined ? {} : { aria: node.aria }),
96
+ ...(node.text === undefined ? {} : { text: node.text }),
97
+ children: node.children.map(read),
98
+ ...(node.shadowChildren === undefined ? {} : { shadowChildren: node.shadowChildren.map(read) }),
99
+ });
100
+ return {
101
+ root: read(capture.root),
102
+ ...(capture.portals === undefined ? {} : { portals: capture.portals.map(read) }),
103
+ };
104
+ }
105
+ function semanticAnchors(capture) {
106
+ const anchors = [];
107
+ [capture.root, ...(capture.portals ?? [])].forEach((root, rootIndex) => walk(root, rootIndex, '0'));
108
+ return anchors;
109
+ function walk(node, root, path) {
110
+ if (node.tag !== '#text' &&
111
+ node.aria !== undefined &&
112
+ (node.aria.role !== null || node.aria.name !== null)) {
113
+ const role = node.aria?.role ?? node.tag;
114
+ const name = node.aria?.name ?? '';
115
+ anchors.push(`r${root}:${path} ${role}${name.length > 0 ? ` "${name}"` : ''}`);
116
+ }
117
+ node.children.forEach((child, index) => walk(child, root, `${path}/${index}`));
118
+ node.shadowChildren?.forEach((child, index) => walk(child, root, `${path}/shadow:${index}`));
119
+ }
120
+ }
121
+ function assertLayout(capture) {
122
+ [capture.root, ...(capture.portals ?? [])].forEach((root, rootIndex) => walk(root, `r${rootIndex}:0`));
123
+ function walk(node, path) {
124
+ if (node.tag !== '#text' && node.rect === undefined) {
125
+ throw new Error(`the ${capture.profile.id} capture claims layout is observable but ${path} has no rect; ` +
126
+ 'an absent measurement cannot be analyzed as an empty box');
127
+ }
128
+ node.children.forEach((child, index) => walk(child, `${path}/${index}`));
129
+ node.shadowChildren?.forEach((child, index) => walk(child, `${path}/shadow:${index}`));
130
+ }
131
+ }
132
+ //# sourceMappingURL=analyze.js.map