markswift 0.0.0-stage → 2.0.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 (146) hide show
  1. package/CHANGELOG.md +234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +592 -2
  4. package/dist/cli-jdrbzqfa.js +2975 -0
  5. package/dist/cli-qchh0m4d.js +14041 -0
  6. package/dist/cli.js +637 -0
  7. package/dist/compiler/index.js +20 -0
  8. package/dist/index.js +22 -0
  9. package/dist/types/analysis/alignment.d.ts +12 -0
  10. package/dist/types/analysis/bands.d.ts +53 -0
  11. package/dist/types/analysis/breakpoints.d.ts +22 -0
  12. package/dist/types/analysis/class-hints.d.ts +21 -0
  13. package/dist/types/analysis/context.d.ts +49 -0
  14. package/dist/types/analysis/controls.d.ts +97 -0
  15. package/dist/types/analysis/flex-row.d.ts +14 -0
  16. package/dist/types/analysis/flex-sizing.d.ts +5 -0
  17. package/dist/types/analysis/flow-layout.d.ts +18 -0
  18. package/dist/types/analysis/generated.d.ts +16 -0
  19. package/dist/types/analysis/geometry.d.ts +50 -0
  20. package/dist/types/analysis/grid-tracks.d.ts +44 -0
  21. package/dist/types/analysis/grid.d.ts +5 -0
  22. package/dist/types/analysis/infer.d.ts +18 -0
  23. package/dist/types/analysis/invisible.d.ts +6 -0
  24. package/dist/types/analysis/layers.d.ts +15 -0
  25. package/dist/types/analysis/measured-layout.d.ts +6 -0
  26. package/dist/types/analysis/motion.d.ts +29 -0
  27. package/dist/types/analysis/naming.d.ts +6 -0
  28. package/dist/types/analysis/node-base.d.ts +13 -0
  29. package/dist/types/analysis/size-constraints.d.ts +5 -0
  30. package/dist/types/analysis/sizing.d.ts +24 -0
  31. package/dist/types/analysis/style-background.d.ts +24 -0
  32. package/dist/types/analysis/style-border.d.ts +7 -0
  33. package/dist/types/analysis/style-effects.d.ts +4 -0
  34. package/dist/types/analysis/style.d.ts +9 -0
  35. package/dist/types/analysis/svg/attributes.d.ts +10 -0
  36. package/dist/types/analysis/svg/build.d.ts +6 -0
  37. package/dist/types/analysis/svg/context.d.ts +31 -0
  38. package/dist/types/analysis/svg/geometry.d.ts +11 -0
  39. package/dist/types/analysis/svg/gradients.d.ts +6 -0
  40. package/dist/types/analysis/svg/markup.d.ts +23 -0
  41. package/dist/types/analysis/svg/text.d.ts +7 -0
  42. package/dist/types/analysis/svg/transforms.d.ts +19 -0
  43. package/dist/types/analysis/svg/traversal.d.ts +4 -0
  44. package/dist/types/analysis/table-borders.d.ts +19 -0
  45. package/dist/types/analysis/table.d.ts +36 -0
  46. package/dist/types/analysis/text-layout.d.ts +65 -0
  47. package/dist/types/analysis/text-runs.d.ts +30 -0
  48. package/dist/types/analysis/text.d.ts +9 -0
  49. package/dist/types/analysis/tooltips.d.ts +16 -0
  50. package/dist/types/analysis/typography.d.ts +21 -0
  51. package/dist/types/analysis/wrapping.d.ts +5 -0
  52. package/dist/types/api/bands.d.ts +58 -0
  53. package/dist/types/api/build.d.ts +51 -0
  54. package/dist/types/api/convert.d.ts +13 -0
  55. package/dist/types/api/types.d.ts +69 -0
  56. package/dist/types/browser/capture.d.ts +89 -0
  57. package/dist/types/browser/conditions.d.ts +25 -0
  58. package/dist/types/browser/fonts.d.ts +34 -0
  59. package/dist/types/browser/images.d.ts +3 -0
  60. package/dist/types/browser/layers.d.ts +132 -0
  61. package/dist/types/browser/motion.d.ts +44 -0
  62. package/dist/types/browser/settle.d.ts +27 -0
  63. package/dist/types/browser/sizing.d.ts +10 -0
  64. package/dist/types/browser/snapshot.d.ts +155 -0
  65. package/dist/types/browser/states.d.ts +36 -0
  66. package/dist/types/cli/args.d.ts +31 -0
  67. package/dist/types/cli/config.d.ts +78 -0
  68. package/dist/types/cli/css.d.ts +9 -0
  69. package/dist/types/cli/manifest.d.ts +20 -0
  70. package/dist/types/cli/output.d.ts +27 -0
  71. package/dist/types/cli/run.d.ts +4 -0
  72. package/dist/types/cli.d.ts +2 -0
  73. package/dist/types/compiler/index.d.ts +91 -0
  74. package/dist/types/compiler/naming.d.ts +1 -0
  75. package/dist/types/css/background.d.ts +16 -0
  76. package/dist/types/css/blend.d.ts +9 -0
  77. package/dist/types/css/color.d.ts +14 -0
  78. package/dist/types/css/counter.d.ts +10 -0
  79. package/dist/types/css/easing.d.ts +4 -0
  80. package/dist/types/css/effects.d.ts +18 -0
  81. package/dist/types/css/grid.d.ts +29 -0
  82. package/dist/types/css/index.d.ts +15 -0
  83. package/dist/types/css/media.d.ts +16 -0
  84. package/dist/types/css/paint.d.ts +33 -0
  85. package/dist/types/css/radius.d.ts +5 -0
  86. package/dist/types/css/raster.d.ts +10 -0
  87. package/dist/types/css/shadows.d.ts +3 -0
  88. package/dist/types/css/syntax.d.ts +12 -0
  89. package/dist/types/css/transforms.d.ts +45 -0
  90. package/dist/types/css/typography.d.ts +8 -0
  91. package/dist/types/fonts/converter.d.ts +16 -0
  92. package/dist/types/fonts/metadata.d.ts +6 -0
  93. package/dist/types/fonts/package.d.ts +12 -0
  94. package/dist/types/fonts/registration.d.ts +13 -0
  95. package/dist/types/fonts/source.d.ts +14 -0
  96. package/dist/types/fonts/types.d.ts +28 -0
  97. package/dist/types/html/elements.d.ts +8 -0
  98. package/dist/types/html/index.d.ts +4 -0
  99. package/dist/types/html/inputs.d.ts +4 -0
  100. package/dist/types/html/policy.d.ts +3 -0
  101. package/dist/types/html/types.d.ts +12 -0
  102. package/dist/types/index.d.ts +8 -0
  103. package/dist/types/input/identity.d.ts +3 -0
  104. package/dist/types/input/mime.d.ts +1 -0
  105. package/dist/types/input/root.d.ts +1 -0
  106. package/dist/types/input/server.d.ts +11 -0
  107. package/dist/types/input/source.d.ts +9 -0
  108. package/dist/types/input/types.d.ts +54 -0
  109. package/dist/types/model/fonts.d.ts +26 -0
  110. package/dist/types/model/interactive.d.ts +10 -0
  111. package/dist/types/model/layout.d.ts +528 -0
  112. package/dist/types/model/semantic.d.ts +984 -0
  113. package/dist/types/model/values.d.ts +125 -0
  114. package/dist/types/model/version.d.ts +4 -0
  115. package/dist/types/swift/borders.d.ts +13 -0
  116. package/dist/types/swift/components.d.ts +115 -0
  117. package/dist/types/swift/context.d.ts +184 -0
  118. package/dist/types/swift/control-state.d.ts +19 -0
  119. package/dist/types/swift/controls.d.ts +62 -0
  120. package/dist/types/swift/diagnostics.d.ts +3 -0
  121. package/dist/types/swift/effects.d.ts +23 -0
  122. package/dist/types/swift/emit.d.ts +6 -0
  123. package/dist/types/swift/format.d.ts +91 -0
  124. package/dist/types/swift/frames.d.ts +17 -0
  125. package/dist/types/swift/images.d.ts +25 -0
  126. package/dist/types/swift/layout.d.ts +5 -0
  127. package/dist/types/swift/measured-layout.d.ts +4 -0
  128. package/dist/types/swift/modifiers.d.ts +30 -0
  129. package/dist/types/swift/motion.d.ts +20 -0
  130. package/dist/types/swift/nodes.d.ts +4 -0
  131. package/dist/types/swift/paint.d.ts +22 -0
  132. package/dist/types/swift/runtime.d.ts +6 -0
  133. package/dist/types/swift/shadows.d.ts +7 -0
  134. package/dist/types/swift/states.d.ts +3 -0
  135. package/dist/types/swift/svg.d.ts +14 -0
  136. package/dist/types/swift/text-box.d.ts +12 -0
  137. package/dist/types/swift/text.d.ts +3 -0
  138. package/dist/types/swift/tokens/emit.d.ts +26 -0
  139. package/dist/types/swift/tokens/extract.d.ts +21 -0
  140. package/dist/types/swift/tokens/naming.d.ts +22 -0
  141. package/dist/types/swift/tokens/slots.d.ts +18 -0
  142. package/dist/types/swift/tokens/types.d.ts +77 -0
  143. package/dist/types/swift/tokens.d.ts +6 -0
  144. package/dist/types/swift/traversal.d.ts +2 -0
  145. package/dist/types/swift/wrapping.d.ts +4 -0
  146. package/package.json +66 -4
@@ -0,0 +1,984 @@
1
+ /**
2
+ * Semantic IR — what the design *means*, target-agnostic. One tree per screen.
3
+ * Every node keeps its measured frame (for verification and absolute fallback)
4
+ * plus a `confidence` telling emitters how faithful the mapping is.
5
+ * No SwiftUI / Compose vocabulary here.
6
+ */
7
+ import type { BlendMode, Color, Paint, Shadow, Transform2D } from "./values.ts";
8
+ import type { Rect, Theme } from "./layout.ts";
9
+ export type Confidence = "mapped" | "approximated" | "unmapped";
10
+ export type DiagnosticLevel = "approximated" | "unmapped";
11
+ /**
12
+ * One captured feature the generated view does not reproduce faithfully. `feature` is the id of a
13
+ * row in tests/conformance/inventory (`font-family`, `display: grid`, `svg:textPath`, `img`), so
14
+ * reports group by the product decision for that feature; `message` says what was lost, without a
15
+ * level prefix.
16
+ */
17
+ export interface Diagnostic {
18
+ level: DiagnosticLevel;
19
+ feature: string;
20
+ message: string;
21
+ }
22
+ /** Appends the diagnostic unless an identical one is already listed. */
23
+ export declare function addDiagnostic(list: Diagnostic[], level: DiagnosticLevel, feature: string, message: string): void;
24
+ /** The only confidence rule: the worst level present, `mapped` when there is none. */
25
+ export declare function confidenceOf(list: readonly Diagnostic[]): Confidence;
26
+ /**
27
+ * One per-conversion message, not tied to a node. `capture`: page errors, unreadable resources
28
+ * and viewport-dependent CSS the view does not follow; `fonts`: font packaging; `adaptive`:
29
+ * light/dark pairing; `build`: `markswift build` (plan 033); `palette`: palette mapping (plan 034);
30
+ * `breakpoints`: merging the width-band captures of a breakpoint conversion.
31
+ * `level` and `feature` are set together when the message is a design loss, with the same
32
+ * meaning as on `Diagnostic` (`feature` is an inventory row id). `component` names the
33
+ * component a package build was converting when the message arose.
34
+ */
35
+ export interface ConversionWarning {
36
+ source: "capture" | "fonts" | "adaptive" | "build" | "palette" | "breakpoints";
37
+ level?: DiagnosticLevel;
38
+ feature?: string;
39
+ component?: string;
40
+ message: string;
41
+ }
42
+ export type Size =
43
+ /**
44
+ * `hug`: CSS sizes the box to its content; the captured px only keeps a filling descendant from making it greedy
45
+ * (fidelity pins it, app mode fits it to its native content).
46
+ */
47
+ {
48
+ kind: "fixed";
49
+ px: number;
50
+ hug?: true;
51
+ } | {
52
+ kind: "fill";
53
+ } | {
54
+ kind: "hug";
55
+ }
56
+ /** CSS percentage width of the containing block; `px` is the captured width (fidelity keeps it). */
57
+ | {
58
+ kind: "fraction";
59
+ fraction: number;
60
+ px: number;
61
+ };
62
+ /** Captured content extent and child border boxes for layouts whose native rules differ. */
63
+ export interface MeasuredLayout {
64
+ width: number;
65
+ height: number;
66
+ frames: Rect[];
67
+ }
68
+ /** One authored grid column track the runtime can size: px, fr (`min`: the px floor of `minmax(<min>px, fr)`), or content (`auto`). */
69
+ export type GridTrack = {
70
+ kind: "px";
71
+ px: number;
72
+ } | {
73
+ kind: "fr";
74
+ fr: number;
75
+ min?: number;
76
+ } | {
77
+ kind: "auto";
78
+ };
79
+ /**
80
+ * An authored `grid-template-columns` that `MarkswiftTracks` reproduces. Either explicit `tracks`, or
81
+ * `autoRepeat` (`repeat(auto-fill|auto-fit, minmax(<min>px, 1fr))`, equal columns, count from the width).
82
+ */
83
+ export interface GridTemplate {
84
+ tracks: GridTrack[];
85
+ autoRepeat?: {
86
+ mode: "fill" | "fit";
87
+ min: number;
88
+ };
89
+ }
90
+ /** One grid row MarkswiftTracks sizes: `auto` (also min-/max-content) is as tall as its items; px is fixed. */
91
+ export type GridRowTrack = {
92
+ kind: "px";
93
+ px: number;
94
+ } | {
95
+ kind: "auto";
96
+ };
97
+ /** Where an admitted grid item sits: 1-based CSS lines (areas and auto-placement resolved), spans count tracks. */
98
+ export interface GridPlacement {
99
+ /** Sized in px on both axes: keeps its frame (CSS does not stretch it). */
100
+ fixed: boolean;
101
+ colStart: number;
102
+ colSpan: number;
103
+ rowStart: number;
104
+ rowSpan: number;
105
+ /** Resolved `justify-self` (`auto` → the container's `justify-items`; `normal` → stretch). */
106
+ justify: "stretch" | "start" | "center" | "end";
107
+ }
108
+ /** An admitted grid item: its placement, `GridNode.children` index and [top, bottom] margin (runtime padding). */
109
+ export interface GridItem extends GridPlacement {
110
+ child: number;
111
+ margin: [number, number];
112
+ }
113
+ /**
114
+ * A `table-layout: fixed` column: border-box px; `fraction` of the available width (content box minus every
115
+ * `border-spacing`) plus the first-row cell's horizontal padding and border (`plus`); or an equal share of the rest.
116
+ */
117
+ export type TableColumn = {
118
+ kind: "px";
119
+ px: number;
120
+ } | {
121
+ kind: "percent";
122
+ fraction: number;
123
+ plus: number;
124
+ } | {
125
+ kind: "auto";
126
+ };
127
+ /**
128
+ * A CSS table laid out by `MarkswiftTable` (plan 049). `slots[i]` places `children[i]` (048's 1-based GridPlacement
129
+ * fields); the first `bands` children are row-group/row paint (spanning every column), the rest are the cells in DOM
130
+ * row-major order. `widths`/`heights` are the captured column border-box widths and row heights; `fixed`: admitted
131
+ * `table-layout: fixed` columns.
132
+ */
133
+ export interface TableLayout {
134
+ widths: number[];
135
+ heights: number[];
136
+ slots: Array<Pick<GridPlacement, "rowStart" | "colStart" | "rowSpan" | "colSpan">>;
137
+ bands: number;
138
+ collapse: boolean;
139
+ fixed?: TableColumn[];
140
+ }
141
+ /**
142
+ * One CSS flex item MarkswiftFlexRow sizes: `basis` the border-box px of a px/0/0% basis (CSS adds padding and
143
+ * border to the flex base size), or null = content (measured natively). `fraction`: a percentage basis, resolved as
144
+ * `basis + fraction × row width`. `min`/`max`: border-box min-width/max-width. `inset`: horizontal padding and
145
+ * border, the floor of the box and excluded from the shrink weight (CSS scales shrink by the inner basis). Optional
146
+ * fields are omitted when 0 / absent.
147
+ */
148
+ export interface FlexItem {
149
+ grow: number;
150
+ shrink: number;
151
+ basis: number | null;
152
+ min?: number;
153
+ max?: number;
154
+ inset?: number;
155
+ fraction?: number;
156
+ }
157
+ /** An admitted nowrap flex row; app mode emits MarkswiftFlexRow, fidelity keeps the children's captured widths. */
158
+ export interface FlexRow {
159
+ items: FlexItem[];
160
+ align: "start" | "center" | "end" | "stretch";
161
+ /** What fidelity still reports (the pinned allocation), absent when today's output reported nothing. */
162
+ fidelityNote?: {
163
+ feature: "flex-grow" | "flex-shrink" | "flex-basis";
164
+ message: string;
165
+ };
166
+ }
167
+ export type Axis = "v" | "h";
168
+ /** A width breakpoint: the next band starts at `px` (`inclusive`) or just above it (`max-width`, `width >`). */
169
+ export interface BandEdge {
170
+ px: number;
171
+ inclusive: boolean;
172
+ }
173
+ /** Cross-axis alignment of children inside a stack. */
174
+ export type CrossAlign = "start" | "center" | "end" | "stretch" | "baseline";
175
+ /** Main-axis distribution. */
176
+ export type Distribution = "start" | "center" | "end" | "between" | "around" | "evenly";
177
+ export type BorderStyle = "solid" | "dashed" | "dotted" | "double";
178
+ /** One border edge as the browser resolved it. */
179
+ export interface Edge {
180
+ width: number;
181
+ color: Color;
182
+ style: BorderStyle;
183
+ }
184
+ /** CSS `filter` list entry, in source order. */
185
+ export type Filter = {
186
+ kind: "blur";
187
+ radius: number;
188
+ } | {
189
+ kind: "drop-shadow";
190
+ shadow: Shadow;
191
+ } | {
192
+ kind: "grayscale" | "brightness" | "contrast" | "saturate" | "invert" | "sepia" | "opacity";
193
+ amount: number;
194
+ } | {
195
+ kind: "hue-rotate";
196
+ degrees: number;
197
+ };
198
+ /** `clip-path` basic shapes, px relative to the node's top-left. */
199
+ export type ClipShape = {
200
+ kind: "circle";
201
+ cx: number;
202
+ cy: number;
203
+ r: number;
204
+ } | {
205
+ kind: "ellipse";
206
+ cx: number;
207
+ cy: number;
208
+ rx: number;
209
+ ry: number;
210
+ } | {
211
+ kind: "inset";
212
+ top: number;
213
+ right: number;
214
+ bottom: number;
215
+ left: number;
216
+ radius?: number;
217
+ } | {
218
+ kind: "polygon";
219
+ points: Array<[number, number]>;
220
+ };
221
+ export interface Style {
222
+ /** Background layers, bottom-most first. */
223
+ fills: Paint[];
224
+ /** Corner radii: top-left, top-right, bottom-right, bottom-left (px). */
225
+ radius?: [number, number, number, number];
226
+ /** All four corners meet at half-width/half-height: CSS 50% radii form an ellipse. */
227
+ ellipse?: true;
228
+ /** Uniform border (all four edges equal). */
229
+ border?: {
230
+ width: number;
231
+ color: Color;
232
+ dashed?: boolean;
233
+ };
234
+ /** Per-edge borders when the edges differ; `border` is unset then. Missing edge = none. */
235
+ borderSides?: {
236
+ top?: Edge;
237
+ right?: Edge;
238
+ bottom?: Edge;
239
+ left?: Edge;
240
+ };
241
+ /**
242
+ * `outline-*`: a stroke ring `offset` px outside the border edge (negative = inside),
243
+ * following the node's radius. `outline-style: auto` arrives as `solid`.
244
+ */
245
+ outline?: {
246
+ width: number;
247
+ color: Color;
248
+ style: BorderStyle;
249
+ offset: number;
250
+ };
251
+ shadows: Shadow[];
252
+ opacity?: number;
253
+ /** Children clipped to the rounded box. */
254
+ clip?: boolean;
255
+ /** Gaussian blur applied to the node itself (px). */
256
+ blur?: number;
257
+ /** Full CSS filter list; `blur` stays as the fast path when it is the only function. */
258
+ filters?: Filter[];
259
+ transform?: Transform2D & {
260
+ origin?: [number, number];
261
+ };
262
+ /** `mix-blend-mode`, never `normal`. */
263
+ blend?: BlendMode;
264
+ /** `clip-path` basic shape. */
265
+ clipPath?: ClipShape;
266
+ /** `mask-image` gradient (luminance/alpha mask). */
267
+ mask?: Paint;
268
+ /** `visibility: hidden` — occupies space, paints nothing. */
269
+ hidden?: boolean;
270
+ /** Token hints: CSS custom properties the declared style referenced, plus role-qualified class hints (`bg:white`). */
271
+ tokens?: string[];
272
+ }
273
+ /**
274
+ * Paint deltas of an interaction state on top of `style`; only changed fields are present; a field the state
275
+ * removes is present in its transparent/identity form. `runColors` is index-aligned with a text node's runs.
276
+ */
277
+ export type StateStyle = Partial<Pick<Style, "fills" | "border" | "borderSides" | "outline" | "shadows" | "opacity" | "transform" | "filters">> & {
278
+ runColors?: Array<Color | undefined>;
279
+ };
280
+ /** A CSS easing function: `cubic-bezier()` (and its keywords), `steps(n, jump-*)`, `linear()` (outputs, inputs 0…1, completed). */
281
+ export type Easing = {
282
+ bezier: [number, number, number, number];
283
+ } | {
284
+ steps: [number, "start" | "end" | "none" | "both"];
285
+ } | {
286
+ points: [number[], number[]];
287
+ };
288
+ /** Animatable channels: opacity, translate x/y (px), scale x/y, rotation (deg). */
289
+ export type MotionChannel = "opacity" | "tx" | "ty" | "sx" | "sy" | "deg";
290
+ /** One channel's keyframes: the value at offset 0, then one eased segment per later keyframe (`until` = offset 0…1). */
291
+ export interface MotionTrack {
292
+ from: number;
293
+ segments: Array<{
294
+ to: number;
295
+ until: number;
296
+ easing: Easing;
297
+ }>;
298
+ }
299
+ /** One CSS `@keyframes` animation in CSS terms. Times in ms; `delay` may be negative. */
300
+ export interface KeyframeMotion {
301
+ name: string;
302
+ duration: number;
303
+ delay: number;
304
+ iterations: number | "infinite";
305
+ direction: "normal" | "reverse" | "alternate" | "alternate-reverse";
306
+ fill: "none" | "forwards" | "backwards" | "both";
307
+ tracks: Partial<Record<MotionChannel, MotionTrack>>;
308
+ }
309
+ export interface Motion {
310
+ /** Disjoint channels. */
311
+ animations?: KeyframeMotion[];
312
+ /** Under Reduce Motion: absent = same as `animations`; `[]` = the base look. */
313
+ reduced?: KeyframeMotion[];
314
+ /** `transform-origin` as fractions of the border box; absent = centre. */
315
+ anchor?: [number, number];
316
+ /** Static values of the driven channels (the settled computed look); the emitter drops their static modifiers. */
317
+ base?: Partial<Record<MotionChannel, number>>;
318
+ /** Pressed/focused looks' CSS transitions (`analysis/motion.ts` `transitionsOf`). */
319
+ transitions?: {
320
+ pressed?: StateTransition;
321
+ focused?: StateTransition;
322
+ };
323
+ /** Overlay layer roots: the exit animation, played first → last keyframe as the removal transition (066). */
324
+ exit?: {
325
+ motion: KeyframeMotion;
326
+ approximated?: true;
327
+ };
328
+ /** In-flow layer content (or a `<details>` disclosure): the `height` 0 ↔ auto timing of opening and closing (066). */
329
+ reveal?: {
330
+ open?: TransitionSpec;
331
+ close?: TransitionSpec;
332
+ };
333
+ }
334
+ export type StateField = keyof StateStyle;
335
+ /** One paint field's transition. Times in ms (`delay` ≥ 0). */
336
+ export interface TransitionSpec {
337
+ duration: number;
338
+ delay: number;
339
+ easing: Easing;
340
+ }
341
+ /** Absent field = it jumps. */
342
+ export type TransitionMap = Partial<Record<StateField, TransitionSpec>>;
343
+ /** `in`: into the state; `out`: back to rest; `reduced`: under Reduce Motion, resolved by analysis (D-MOTION). */
344
+ export interface StateTransition {
345
+ in: TransitionMap;
346
+ out: TransitionMap;
347
+ reduced: {
348
+ in: TransitionMap;
349
+ out: TransitionMap;
350
+ };
351
+ }
352
+ export interface FontSpec {
353
+ /**
354
+ * `system` families map to platform fonts by design; `custom` is a bundled family name.
355
+ * `static`: the bundled files have no `wght` axis, so each weight is a separate face.
356
+ */
357
+ family: {
358
+ kind: "system";
359
+ design: "default" | "rounded" | "monospaced" | "serif";
360
+ } | {
361
+ kind: "custom";
362
+ name: string;
363
+ static?: true;
364
+ };
365
+ size: number;
366
+ weight: number;
367
+ /** Optical size axis value when the family has one (Chrome uses font-size). */
368
+ opsz?: number;
369
+ /** System families: CSS `font-stretch` % (or a `wdth` variation) when not 100. */
370
+ stretch?: number;
371
+ /** Bundled families: variation axes other than wght/opsz the file declares, clamped to its ranges (`wdth` from font-stretch). */
372
+ axes?: Record<string, number>;
373
+ /** CSS line-height in px when explicitly set; undefined = normal. */
374
+ lineHeight?: number;
375
+ }
376
+ export type TextTransform = "uppercase" | "lowercase" | "capitalize";
377
+ export type DecorationLine = "underline" | "line-through" | "overline";
378
+ export interface TextDecoration {
379
+ lines: DecorationLine[];
380
+ color?: Color;
381
+ style?: "solid" | "double" | "dotted" | "dashed" | "wavy";
382
+ }
383
+ /** CSS counter styles a list marker or `counter()` is regenerated in (`src/css/counter.ts`). */
384
+ export type CounterStyle = "decimal" | "decimal-leading-zero" | "lower-alpha" | "upper-alpha" | "lower-roman" | "upper-roman";
385
+ export interface TextRun {
386
+ text: string;
387
+ font: FontSpec;
388
+ color: Color;
389
+ kerning?: number;
390
+ /** CSS `text-transform`; emitters apply it (`.textCase` when every run shares it, else the literal). */
391
+ transform?: TextTransform;
392
+ italic?: boolean;
393
+ decoration?: TextDecoration;
394
+ /** `text-shadow`. */
395
+ shadow?: Shadow;
396
+ /** Gradient/image glyph fill (`background-clip: text`); replaces `color` when present. */
397
+ paint?: Paint;
398
+ tabularNums?: boolean;
399
+ /**
400
+ * Baseline shift in px, positive = up (`vertical-align: super`/`sub`, `<sup>`/`<sub>`),
401
+ * measured against the baseline of the surrounding text on the same line. The smaller
402
+ * font a shifted run usually carries is already in `font.size`.
403
+ */
404
+ baselineOffset?: number;
405
+ /**
406
+ * Character offsets into `text` where the browser started a new line box. Fidelity emit
407
+ * turns them into hard breaks so the measured layout is reproduced instead of re-wrapped;
408
+ * app emit ignores them and lets the platform break lines. Noted on the node whenever a
409
+ * division is forced on a multi-line paragraph — whether the platform would have broken
410
+ * identically cannot be known offline.
411
+ */
412
+ breaks?: number[];
413
+ /**
414
+ * A run that paints no glyphs and only advances the line by this many points: the horizontal
415
+ * padding/border of an inline element (`kbd`, `mark`, `code`), which a native text run cannot
416
+ * carry as a box.
417
+ */
418
+ gap?: number;
419
+ /** Inline `<a href>`: the run is a link. `action` is set for an href the app must resolve (fragment, relative path). */
420
+ link?: {
421
+ href: string;
422
+ action?: Action;
423
+ };
424
+ /**
425
+ * A generated image (`content: url()`, an inside `list-style-image`) inside the text: drawn at its captured CSS size
426
+ * on the baseline, as CSS sizes and seats it. `text` is U+FFFC. `alt` from `content: … / "alt"`; none = decorative.
427
+ */
428
+ image?: {
429
+ src: string;
430
+ w: number;
431
+ h: number;
432
+ vector?: SvgNode;
433
+ alt?: string;
434
+ };
435
+ /**
436
+ * The run is a counter the browser printed (`::marker` of a counter `list-style-type`, `content: counter()`):
437
+ * `text === prefix + counterText(value, style) + suffix`. Only component extraction reads it (rows derive the
438
+ * marker from their index); emitters print `text`.
439
+ */
440
+ counter?: {
441
+ style: CounterStyle;
442
+ value: number;
443
+ prefix: string;
444
+ suffix: string;
445
+ };
446
+ }
447
+ /**
448
+ * Paint server resolved from `<linearGradient>`/`<radialGradient>`. Coordinates are viewBox
449
+ * user units — `objectBoundingBox` gradients are resolved against the painted path's
450
+ * bounding box when the IR is built. `radius` is per axis (an object-bounding-box radial
451
+ * gradient on a non-square box is an ellipse).
452
+ */
453
+ export type SvgGradient = {
454
+ stops: Array<{
455
+ color: Color;
456
+ pos: number;
457
+ }>;
458
+ } & ({
459
+ kind: "linear";
460
+ start: [number, number];
461
+ end: [number, number];
462
+ } | {
463
+ kind: "radial";
464
+ center: [number, number];
465
+ radius: [number, number];
466
+ });
467
+ /** One absolute path command after `unshort`/`unarc`/`abs`: viewBox user units. */
468
+ export type PathSegment = ["M" | "L", number, number] | ["H" | "V", number] | ["C", number, number, number, number, number, number] | ["Q", number, number, number, number] | ["Z"];
469
+ export interface SvgPath {
470
+ /** Absolute segments in viewBox user units; ancestor and own `transform`s are baked in. */
471
+ path: PathSegment[];
472
+ fill?: Color;
473
+ /** Gradient fill (`fill="url(#id)"`); set instead of `fill`. */
474
+ gradient?: SvgGradient;
475
+ /** `fill-opacity` when below 1. */
476
+ fillOpacity?: number;
477
+ /** `fill-rule: evenodd`. */
478
+ eoFill?: true;
479
+ stroke?: Color;
480
+ /** Gradient stroke (`stroke="url(#id)"`); set instead of `stroke`. */
481
+ strokeGradient?: SvgGradient;
482
+ strokeWidth?: number;
483
+ /** `stroke-opacity` when below 1. */
484
+ strokeOpacity?: number;
485
+ lineCap?: "butt" | "round" | "square";
486
+ lineJoin?: "miter" | "round" | "bevel";
487
+ /** `stroke-miterlimit` when declared (the SVG default 4 differs from native defaults). */
488
+ miterLimit?: number;
489
+ /** `stroke-dasharray` lengths and `stroke-dashoffset`, viewBox units; unset for the ring idiom. */
490
+ dash?: number[];
491
+ dashPhase?: number;
492
+ /** Element `opacity` times ancestor group opacities, when below 1. */
493
+ opacity?: number;
494
+ /**
495
+ * Progress-ring idiom: a viewBox-centred circle whose single dash length covers its
496
+ * circumference. `to` = visible fraction 0..1, `startAngle` in degrees clockwise from
497
+ * 3 o'clock (the baked rotation).
498
+ */
499
+ ring?: {
500
+ r: number;
501
+ to: number;
502
+ startAngle: number;
503
+ };
504
+ /** `clip-path="url(#id)"`: the clipPath's shapes as one path in the same space as `path`. */
505
+ clip?: {
506
+ path: PathSegment[];
507
+ eoFill?: true;
508
+ };
509
+ /** `mask="url(#id)"`: the mask's painted shapes; their luminance becomes this layer's alpha. */
510
+ mask?: SvgPath[];
511
+ }
512
+ /** One `<text>`/`<tspan>` text run, placed where the browser laid it out. */
513
+ export interface SvgText {
514
+ text: string;
515
+ /** Anchor point from the `x`/`y` attributes (viewBox units) and `text-anchor`. */
516
+ x: number;
517
+ y: number;
518
+ anchor: "start" | "middle" | "end";
519
+ font: FontSpec;
520
+ fill: Color;
521
+ /** Measured glyph box relative to the svg frame (px). */
522
+ box: Rect;
523
+ }
524
+ export interface Overlay {
525
+ node: SemNode;
526
+ /** Painted behind (`background`) or in front of (`overlay`) the in-flow content. */
527
+ layer: "background" | "overlay";
528
+ /** Offset of the node's top-left from the host's top-left (px). */
529
+ offset: [number, number];
530
+ /** True when the node covers exactly the host box (inset: 0). */
531
+ covers: boolean;
532
+ /** `position: fixed`: attached to the screen, so app mode keeps it outside the root's ScrollView. */
533
+ fixed?: true;
534
+ /** App-mode attachment when CSS anchored the right and/or bottom edge: the host corner kept, and the node's offset from it. `offset` (top-left) stays the exact capture placement used by fidelity. */
535
+ anchor?: {
536
+ corner: "topTrailing" | "bottomLeading" | "bottomTrailing";
537
+ offset: [number, number];
538
+ };
539
+ /** App mode: a root of a drawn presentation (dialog, sheet, popover, hover card); every root of one layer shares one
540
+ * object, and such overlays are always `fixed` (`analysis/layers.ts`, plan 060). */
541
+ presentation?: Presentation;
542
+ }
543
+ /** A layer drawn over the component while the screen's `is<Layer>Presented` is true (D-PRESENT). */
544
+ export interface Presentation {
545
+ /** Layer id (`edit-profile`): joins light/dark and names the `@open:<id>` capture. */
546
+ id: string;
547
+ /** SemNode id of the trigger. */
548
+ trigger: string;
549
+ style: "dialog" | "alertdialog" | "sheet" | "popover" | "hovercard";
550
+ placement: {
551
+ kind: "center";
552
+ } | {
553
+ kind: "edge";
554
+ edge: "top" | "bottom" | "leading" | "trailing";
555
+ }
556
+ /** `from`: unit point on the trigger, `to`: unit point on the content, `gap`: content `to` − trigger `from` (px). */
557
+ | {
558
+ kind: "anchored";
559
+ from: [number, number];
560
+ to: [number, number];
561
+ gap: [number, number];
562
+ }
563
+ /** Unclassified: the captured top-left offset (`Overlay.offset`). */
564
+ | {
565
+ kind: "offset";
566
+ };
567
+ /** Native `::backdrop` colour (top-layer dialogs); a Radix scrim stays its own root node. */
568
+ backdrop?: Color;
569
+ modal: boolean;
570
+ /** `buttons`: SemNode ids of the content's own buttons that close it. */
571
+ dismiss: {
572
+ escape: boolean;
573
+ outside: boolean;
574
+ buttons: string[];
575
+ };
576
+ }
577
+ /** One entry of a native menu (plan 061); `id` is the item's LayoutNode id. */
578
+ export type MenuItem = {
579
+ kind: "button";
580
+ id: string;
581
+ label: string;
582
+ action?: Action;
583
+ disabled?: true;
584
+ destructive?: true;
585
+ } | {
586
+ kind: "toggle";
587
+ id: string;
588
+ label: string;
589
+ checked: boolean;
590
+ disabled?: true;
591
+ }
592
+ /** `selected`: index into `options`, −1 when none is checked. */
593
+ | {
594
+ kind: "radio";
595
+ id: string;
596
+ options: PickerOption[];
597
+ selected: number;
598
+ } | {
599
+ kind: "divider";
600
+ } | {
601
+ kind: "section";
602
+ label: string;
603
+ items: MenuItem[];
604
+ } | {
605
+ kind: "submenu";
606
+ id: string;
607
+ label: string;
608
+ };
609
+ /** A captured menu layer the trigger presents natively (plan 061, D-MENUS). */
610
+ export interface MenuSpec {
611
+ layer: string;
612
+ /** Opened by a right click: `.contextMenu` on the trigger instead of `Menu`. */
613
+ context?: true;
614
+ items: MenuItem[];
615
+ /** Radix select: the inline Picker's seed (`value`), shown live in the trigger's single-run TextNode `valueText`
616
+ * (absent: the trigger's text stays static, reported). */
617
+ select?: {
618
+ valueText?: string;
619
+ value: string;
620
+ };
621
+ }
622
+ /** CSS min/max constraints expressed on the painted border box, in captured CSS pixels. */
623
+ export interface SizeConstraints {
624
+ minWidth?: number;
625
+ maxWidth?: number;
626
+ minHeight?: number;
627
+ maxHeight?: number;
628
+ }
629
+ /** A callback the generated view exposes for something the design lets the user activate. */
630
+ export interface Action {
631
+ /** One member per key: the handler source (`save()`), `#<target>` for an in-app link, else `node:<id>`. */
632
+ key: string;
633
+ /** Words the member is named from: handler name, `id`, `aria-label`, caption or link target — the first non-empty. */
634
+ words: string;
635
+ /** One line quoting the design, the member's doc comment. */
636
+ origin: string;
637
+ /** The element contains another control: a tap gesture on its box, not a Button (SwiftUI must not nest Buttons). */
638
+ gesture?: true;
639
+ }
640
+ export interface SemBase extends SizeConstraints {
641
+ /** Template path id; stable across regenerations. */
642
+ id: string;
643
+ frame: Rect;
644
+ width: Size;
645
+ height: Size;
646
+ /**
647
+ * Horizontal placement inside a column/block parent's content box: asymmetric/auto margins or
648
+ * `align-self`. Default: the parent's alignment.
649
+ */
650
+ selfAlign?: "start" | "center" | "end";
651
+ /** Vertical placement inside a flex row from `align-self` when it differs from the row's `align-items`. */
652
+ selfAlignV?: "start" | "center" | "end";
653
+ /**
654
+ * `position: relative` used offset [x, y] in px, drawn with `.offset`; `frame` is the in-flow box (the capture lays the
655
+ * box out without it). Absent when zero.
656
+ */
657
+ relativeOffset?: [number, number];
658
+ /**
659
+ * Base writing direction of a text node whose computed `direction` is `rtl`: CoreText resolves
660
+ * neutral characters against it. Only text carries it — container order is already measured,
661
+ * so overriding the direction on a stack would mirror it a second time.
662
+ */
663
+ layoutDirection?: "rtl";
664
+ /** top, right, bottom, left — includes border width so content lands where the browser put it. */
665
+ padding: [number, number, number, number];
666
+ style: Style;
667
+ confidence: Confidence;
668
+ diagnostics: Diagnostic[];
669
+ overlays: Overlay[];
670
+ /** What assistive technology is told about this node, beyond what the element itself implies. */
671
+ a11y?: {
672
+ /** `aria-labelledby` text, else `aria-label`, else the text of the `<label>` that names this control. */
673
+ label?: string;
674
+ /** A text-only element of a container role (tabpanel, region, …): `label` names a group around the Text, not its words. */
675
+ container?: true;
676
+ /** `aria-hidden="true"`. */
677
+ hidden?: true;
678
+ /** `<h1>`…`<h6>`, or `role="heading"` with `aria-level` (default 2). */
679
+ heading?: 1 | 2 | 3 | 4 | 5 | 6;
680
+ /** `role` = button | link | img on an element that is not already that control; `<th>` or role columnheader/rowheader → isHeader. */
681
+ trait?: "isButton" | "isLink" | "isImage" | "isHeader";
682
+ /** `aria-valuetext`, else `aria-valuenow`; `aria-expanded` → `expanded`/`collapsed`, `aria-checked=mixed` → `mixed` when there is no value. */
683
+ value?: string;
684
+ /** Text of the `aria-describedby` elements, else `aria-description`. */
685
+ hint?: string;
686
+ /** `aria-selected`/`aria-checked`/`aria-pressed` = `true`, or an `aria-current` other than `false`. */
687
+ selected?: true;
688
+ /** role checkbox | switch | radio | menuitemcheckbox | menuitemradio. */
689
+ toggle?: true;
690
+ /** `role=radio` inside a `role=radiogroup`: the group element's id. Its radios share one selection. */
691
+ group?: string;
692
+ /** `aria-disabled="true"`. */
693
+ disabled?: true;
694
+ };
695
+ /** `title`, inline-run titles and captured tooltip layers (D-HOVER): `.help` + an accessibility hint; never drawn. */
696
+ tooltip?: string;
697
+ /** `<label>`: id of the toggle/radio its tap changes, or of the text field it focuses. */
698
+ labelFor?: string;
699
+ /** Words a generated Swift name may use, from the element's own markup; see `analysis/naming.ts`. Never a path. */
700
+ nameHint?: {
701
+ words: string;
702
+ source: "component" | "id" | "slot" | "aria" | "class" | "tag";
703
+ };
704
+ /** `data-markswift-field`: the design names the data field this node's content becomes. */
705
+ field?: string;
706
+ /**
707
+ * What activating this element does; set on Buttons, in-app links, text fields inside a
708
+ * `<form onsubmit>` (Return), and activatable non-controls.
709
+ */
710
+ action?: Action;
711
+ /**
712
+ * `pointer-events: none` on this overlay's whole element subtree: taps reach what is painted underneath. Set on the
713
+ * outermost such overlay only; its action stays (assistive technology still activates it, as in Chrome).
714
+ */
715
+ hitTesting?: false;
716
+ /** Paint deltas of an interaction state on top of `style` (pressed: `:hover`+`:active`; focused: `:focus-visible`;
717
+ * open: while a layer this node or an ancestor `opens` is shown). */
718
+ states?: {
719
+ pressed?: StateStyle;
720
+ focused?: StateStyle;
721
+ open?: StateStyle;
722
+ };
723
+ /** App mode: this node's tap toggles the in-flow layer `layer`, or presents the drawn presentation `layer`
724
+ * (`longPress`: a hover/context-menu layer opens on long-press; `anchor`: the content is anchored to this node). */
725
+ opens?: {
726
+ layer: string;
727
+ kind: "inflow";
728
+ } | {
729
+ layer: string;
730
+ kind: "presentation";
731
+ longPress?: true;
732
+ anchor?: true;
733
+ };
734
+ /** App mode: id of the in-flow layer whose open state shows this node. */
735
+ shownBy?: string;
736
+ /** App mode: this node presents the captured menu natively (plan 061). */
737
+ menu?: MenuSpec;
738
+ /** Breakpoint conversions: the bands (ascending indices) this node shows in. Set only on members of `alternatives`;
739
+ * inside a `containerBands` host they are that host's container bands. */
740
+ bands?: number[];
741
+ /**
742
+ * Breakpoint conversions, `stack`/`grid` only: a query container (`container-type`) whose width `@container` queries
743
+ * switch its content. The emitter nests `MarkswiftBands` inside its padding, so `edges` (content-box px) apply to the
744
+ * width offered to its content; `names` as `Screen.breakpoints.names` (`""`, `cq_md`, `cq_w600`).
745
+ */
746
+ containerBands?: {
747
+ edges: BandEdge[];
748
+ names: string[];
749
+ };
750
+ /**
751
+ * Breakpoint conversions: this node is a holder — kind `box`, no paint, no children — for one DOM element whose
752
+ * authored inputs differ between bands. The emitter shows the alternative whose `bands` holds the current band.
753
+ * `frame`, `width`, `height`, `selfAlign`, `selfAlignV` are the first alternative's, so the parent lays it out as
754
+ * that band.
755
+ */
756
+ alternatives?: SemNode[];
757
+ /** CSS `@keyframes` animations (app output plays them; fidelity renders the settled look). */
758
+ motion?: Motion;
759
+ }
760
+ export interface StackNode extends SemBase {
761
+ kind: "stack";
762
+ axis: Axis;
763
+ /** Uniform spacing, or one gap per adjacent pair when they differ. */
764
+ spacing: number | number[];
765
+ align: CrossAlign;
766
+ distribution: Distribution;
767
+ /** Leading offset before the first child that padding does not explain. */
768
+ leadInset: number;
769
+ /** Gaps taken by a CSS auto margin in a flex container — index i is before child i (children.length = after the last). Emitted as flexible Spacers; the gap's fixed part (`spacing`, or `leadInset` for 0) is their minimum. */
770
+ autoGaps?: number[];
771
+ /** `space-between`: the CSS gap, the Spacers' minimum length. */
772
+ minGap?: number;
773
+ scroll?: Axis;
774
+ /** Fixed-size row items that reflow natively when available width changes. */
775
+ wrap?: {
776
+ columnGap: number;
777
+ rowGap: number;
778
+ };
779
+ /** Captured positions for wrapping combinations without a native layout mapping. */
780
+ measured?: MeasuredLayout;
781
+ /** Admitted CSS flex allocation (row, nowrap); see analysis/flex-row.ts. */
782
+ flex?: FlexRow;
783
+ children: SemNode[];
784
+ }
785
+ export interface GridNode extends SemBase {
786
+ kind: "grid";
787
+ columns: number;
788
+ hSpacing: number;
789
+ vSpacing: number;
790
+ measured?: MeasuredLayout;
791
+ /**
792
+ * Admitted authored template: app mode emits MarkswiftTracks; fidelity keeps the captured geometry and reports
793
+ * `fidelityNote`.
794
+ */
795
+ template?: GridTemplate & {
796
+ stretch: boolean;
797
+ fidelityNote: string;
798
+ /** Explicit rows (`grid-template-rows`) and `grid-auto-rows`; absent when every row is content-sized. */
799
+ rows?: GridRowTrack[];
800
+ autoRow?: GridRowTrack;
801
+ /** The grid items in DOM order (MarkswiftTracks subview order). */
802
+ items: GridItem[];
803
+ };
804
+ /** An admitted CSS table (plan 049); `hSpacing`/`vSpacing` are its used border-spacing. */
805
+ table?: TableLayout;
806
+ /** Same as StackNode.scroll: the overflow axis a ScrollView covers. */
807
+ scroll?: Axis;
808
+ children: SemNode[];
809
+ }
810
+ export interface TextNode extends SemBase {
811
+ kind: "text";
812
+ /** Source element, retained so emitters can scope text-layout strategies safely. */
813
+ sourceTag?: string;
814
+ runs: TextRun[];
815
+ align: "leading" | "center" | "trailing";
816
+ lineLimit?: number;
817
+ truncate?: boolean;
818
+ /**
819
+ * The browser did not soft-wrap this text (`text-wrap-mode: nowrap`, `white-space: pre`), so
820
+ * `lineLimit` only caps its hard breaks and a long line overflows its box rather than
821
+ * re-flowing. Unset means normal wrapping.
822
+ */
823
+ noWrap?: true;
824
+ /** One word the browser cannot soft-wrap (overflow-wrap normal, no spaces, no space-free script): CSS overflows it rather than breaking mid-word, so app mode must too. */
825
+ unbreakable?: true;
826
+ /** CSS `text-wrap: balance`: app mode narrows the native lines to equal lengths (`markswiftBalance`). */
827
+ balance?: true;
828
+ /** Measured height of the laid-out text; lets verifiers spot line-break drift. */
829
+ lines: number;
830
+ /**
831
+ * Line-box growth the browser measured beyond `lines × line-height` (sub/sup, larger inline
832
+ * runs, `kbd` borders) on `edge`. `padded`: over 2 px, already included in `padding` (both
833
+ * modes); smaller growth is app-only. App mode subtracts the growth native text adds on its own,
834
+ * so it is not counted twice.
835
+ */
836
+ lineBoxGrowth?: {
837
+ edge: "top" | "bottom";
838
+ px: number;
839
+ padded: boolean;
840
+ };
841
+ }
842
+ export interface SvgNode extends SemBase {
843
+ kind: "svg";
844
+ viewBox: [number, number, number, number];
845
+ paths: SvgPath[];
846
+ /** Text runs drawn over the shapes. */
847
+ texts?: SvgText[];
848
+ /** `<title>` → accessibility label. */
849
+ label?: string;
850
+ /** `preserveAspectRatio="none"`: paths stretch to the frame instead of uniform scale + centring. */
851
+ stretch?: true;
852
+ }
853
+ export interface ImageNode extends SemBase {
854
+ kind: "image";
855
+ src: string;
856
+ /** A `data:image/svg+xml` source, parsed during analysis. */
857
+ vector?: SvgNode;
858
+ /** CSS `object-fit` (`scale-down` arrives as `contain`, noted): fill stretches, contain letterboxes, cover crops, none keeps the natural size. */
859
+ contentMode: "fill" | "contain" | "cover" | "none";
860
+ /** `object-position` as fractions of the free space (0 = left/top), when not centred. */
861
+ position?: [number, number];
862
+ /** `<img alt>`: the accessibility label, and the text the browser paints when the source is broken. */
863
+ alt?: string;
864
+ /** Computed font of the element; used when the alt text stands in for a source that cannot render. */
865
+ font?: FontSpec;
866
+ /** Computed `color` of the element, for that same alt text. */
867
+ color?: Color;
868
+ }
869
+ /** Empty decorative box (blobs, dividers, progress tracks). */
870
+ export interface BoxNode extends SemBase {
871
+ kind: "box";
872
+ }
873
+ /**
874
+ * The interactive thing an element *is*, independent of how it is painted. Chosen from the tag
875
+ * and its attributes (`src/analysis/controls.ts`), never from geometry: `input[type=range]` is a
876
+ * slider whether the design styles it as a track or a dial.
877
+ */
878
+ export type ControlKind = "button" | "link" | "textField" | "secureField" | "textEditor" | "toggle" | "radio" | "slider" | "stepper" | "picker" | "datePicker" | "colorPicker" | "progress" | "gauge" | "disclosure";
879
+ /**
880
+ * A form control, link or media indicator. Its box (fill, border, radius, padding, frame) is the
881
+ * measured one every other node carries — the emitter paints it exactly as it would a stack — and
882
+ * `control` says which native view goes inside it.
883
+ */
884
+ export interface ControlNode extends SemBase {
885
+ kind: "control";
886
+ control: ControlKind;
887
+ /** The control's own title: `<summary>` for a disclosure, `<label>` text where one wraps it. */
888
+ label?: SemNode;
889
+ /** Laid-out children in document order: a button's icon and caption, a link card's rows. */
890
+ content: SemNode[];
891
+ /** Initial text/number the design shows (`@value`, `<textarea>` text, the selected `<option>`). */
892
+ value?: string;
893
+ /** `colorPicker` only: the initial colour (`@value`; HTML's default for a missing/invalid value is black). */
894
+ swatch?: Color;
895
+ /** `@placeholder`: the prompt drawn while the field is empty. */
896
+ placeholder?: string;
897
+ /** The placeholder's paint from `::placeholder` (fill colour × opacity); absent for Chrome's UA grey, which keeps the platform prompt grey. */
898
+ promptColor?: Color;
899
+ /** `<input type>` of a text field whose keyboard and autofill differ from plain text. */
900
+ inputType?: "email" | "tel" | "url" | "number" | "search" | "password";
901
+ /** WHATWG autofill field name from `autocomplete`, e.g. `one-time-code`. */
902
+ autofill?: string;
903
+ /** `@inputmode`: the virtual keyboard the design asks for. */
904
+ inputMode?: string;
905
+ /** `@enterkeyhint`: the Return key's label. */
906
+ enterKey?: string;
907
+ /** `@checked`: initial on state of a toggle/radio. */
908
+ checked?: boolean;
909
+ /** Radio `@name`; radios with the same `form` and group share one selection. */
910
+ group?: string;
911
+ /** Grouped radio's form owner (the `<form>`'s node id; absent outside a form). */
912
+ form?: string;
913
+ /** `@disabled`, or inside a `<fieldset disabled>`: the control is inert (Chrome also dims it, so the measured style already shows it). */
914
+ disabled?: boolean;
915
+ /** Numeric domain of a slider/progress/gauge, resolved from `@min`/`@max`/`@step`. */
916
+ range?: {
917
+ min: number;
918
+ max: number;
919
+ step?: number;
920
+ };
921
+ /**
922
+ * `<meter>` only: where the value falls against `low`/`high`/`optimum` (HTML's gauge algorithm).
923
+ * Browsers colour the bar by this state — green, amber, red — and so does the emitter.
924
+ */
925
+ level?: "optimum" | "suboptimal" | "poor";
926
+ /** Picker choices in document order: the `<option>`s a menu lists (`hidden` ones are left out). */
927
+ options?: PickerOption[];
928
+ /** `<a href>` as authored; fragments and relative paths have no runtime destination. */
929
+ href?: string;
930
+ /** `<details open>`: initial disclosure state. */
931
+ expanded?: boolean;
932
+ /**
933
+ * `<input type=file>`: the element is a button that presents the system file picker, so the
934
+ * presence of this field — not a control kind of its own — is what attaches `.fileImporter`.
935
+ * `accept` holds the attribute's tokens as authored (`.png`, `image/png`, `image/*`); they
936
+ * name types in the system's database, so they are resolved at runtime, not here.
937
+ */
938
+ files?: {
939
+ accept: string[];
940
+ multiple?: boolean;
941
+ };
942
+ /** Font of the control's own text (the field's value, the picker's selection) — the element's computed font. */
943
+ font?: FontSpec;
944
+ /** Colour of that text (the element's computed `color`). */
945
+ color?: Color;
946
+ /** `datePicker` only: the `<input type>` it came from, which decides the components it shows. */
947
+ dateKind?: "date" | "time" | "datetime-local" | "month" | "week";
948
+ /** CSS `accent-color` (absent for `auto`): the tint Chrome gives checkboxes, radios, ranges and progress bars. */
949
+ accent?: Color;
950
+ /** Computed `appearance: none`: the author removed the native widget chrome (no select arrow). */
951
+ appearance?: "none";
952
+ }
953
+ /** A `<select>` choice: its label, whether it (or its `<optgroup>`) is disabled, and that group's `label`. */
954
+ export interface PickerOption {
955
+ label: string;
956
+ disabled?: boolean;
957
+ group?: string;
958
+ }
959
+ export type SemNode = StackNode | GridNode | TextNode | SvgNode | ImageNode | BoxNode | ControlNode;
960
+ export interface Screen {
961
+ version: 1;
962
+ name: string;
963
+ source: string;
964
+ theme: Theme;
965
+ size: {
966
+ w: number;
967
+ h: number;
968
+ };
969
+ /** Backdrop behind the root, from the design. */
970
+ canvas: Color;
971
+ root: SemNode;
972
+ /** Breakpoint conversions with at least one holder: `edges` in root-proposal px (the CSS edges minus the root's
973
+ * offset from the viewport), `cssEdges` as captured, `widths` = capture viewport width per band, `names` = each
974
+ * band's name (band 0 `""`; `md`, `w600`) for property names and previews. */
975
+ breakpoints?: {
976
+ edges: BandEdge[];
977
+ cssEdges: BandEdge[];
978
+ widths: number[];
979
+ names: string[];
980
+ };
981
+ /** The root's computed `color-scheme` when it names exactly one scheme: the view forces that appearance. */
982
+ colorScheme?: "light" | "dark";
983
+ }
984
+ export declare function walk(n: SemNode, fn: (n: SemNode, depth: number) => void, depth?: number): void;