@swiftbrowser/web 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 (84) hide show
  1. package/README.md +600 -0
  2. package/app/index.html +125 -0
  3. package/index.html +56 -0
  4. package/node/dist/node/apps.d.ts +58 -0
  5. package/node/dist/node/apps.d.ts.map +1 -0
  6. package/node/dist/node/apps.js +60 -0
  7. package/node/dist/node/apps.js.map +1 -0
  8. package/node/dist/node/config.d.ts +42 -0
  9. package/node/dist/node/config.d.ts.map +1 -0
  10. package/node/dist/node/config.js +99 -0
  11. package/node/dist/node/config.js.map +1 -0
  12. package/node/dist/node/examples.d.ts +39 -0
  13. package/node/dist/node/examples.d.ts.map +1 -0
  14. package/node/dist/node/examples.js +116 -0
  15. package/node/dist/node/examples.js.map +1 -0
  16. package/node/dist/node/index.d.ts +47 -0
  17. package/node/dist/node/index.d.ts.map +1 -0
  18. package/node/dist/node/index.js +52 -0
  19. package/node/dist/node/index.js.map +1 -0
  20. package/node/dist/plugins/app-manifests.d.ts +61 -0
  21. package/node/dist/plugins/app-manifests.d.ts.map +1 -0
  22. package/node/dist/plugins/app-manifests.js +168 -0
  23. package/node/dist/plugins/app-manifests.js.map +1 -0
  24. package/node/dist/plugins/asset-catalogs.d.ts +119 -0
  25. package/node/dist/plugins/asset-catalogs.d.ts.map +1 -0
  26. package/node/dist/plugins/asset-catalogs.js +488 -0
  27. package/node/dist/plugins/asset-catalogs.js.map +1 -0
  28. package/node/dist/plugins/bundle-resources.d.ts +49 -0
  29. package/node/dist/plugins/bundle-resources.d.ts.map +1 -0
  30. package/node/dist/plugins/bundle-resources.js +172 -0
  31. package/node/dist/plugins/bundle-resources.js.map +1 -0
  32. package/node/dist/plugins/swift-wasm.d.ts +33 -0
  33. package/node/dist/plugins/swift-wasm.d.ts.map +1 -0
  34. package/node/dist/plugins/swift-wasm.js +241 -0
  35. package/node/dist/plugins/swift-wasm.js.map +1 -0
  36. package/node/dist/src/assets.d.ts +60 -0
  37. package/node/dist/src/assets.d.ts.map +1 -0
  38. package/node/dist/src/assets.js +54 -0
  39. package/node/dist/src/assets.js.map +1 -0
  40. package/node/dist/src/devEvents.d.ts +31 -0
  41. package/node/dist/src/devEvents.d.ts.map +1 -0
  42. package/node/dist/src/devEvents.js +9 -0
  43. package/node/dist/src/devEvents.js.map +1 -0
  44. package/package.json +56 -0
  45. package/plugins/app-manifests.ts +192 -0
  46. package/plugins/asset-catalogs.ts +558 -0
  47. package/plugins/bundle-resources.ts +195 -0
  48. package/plugins/swift-wasm.ts +259 -0
  49. package/public/_headers +23 -0
  50. package/public/apple-touch-icon.png +0 -0
  51. package/public/icon.svg +14 -0
  52. package/public/manifest.webmanifest +14 -0
  53. package/src/animation.ts +521 -0
  54. package/src/assets.ts +86 -0
  55. package/src/chartDom.ts +481 -0
  56. package/src/devEvents.ts +35 -0
  57. package/src/deviceMode.ts +80 -0
  58. package/src/ghost.ts +25 -0
  59. package/src/interaction.ts +1113 -0
  60. package/src/landing.css +261 -0
  61. package/src/landing.ts +141 -0
  62. package/src/layout/chart.ts +477 -0
  63. package/src/layout/controls.ts +116 -0
  64. package/src/layout/engine.ts +3581 -0
  65. package/src/layout/index.ts +91 -0
  66. package/src/layout/measure.ts +86 -0
  67. package/src/layout/text.ts +124 -0
  68. package/src/layout/types.ts +153 -0
  69. package/src/layout/typography.ts +188 -0
  70. package/src/layoutDom.ts +549 -0
  71. package/src/layoutEvents.ts +110 -0
  72. package/src/locale.ts +64 -0
  73. package/src/main.ts +554 -0
  74. package/src/mock.ts +752 -0
  75. package/src/nodeQueries.ts +60 -0
  76. package/src/protocol.ts +1421 -0
  77. package/src/renderer.ts +2882 -0
  78. package/src/styles.css +2834 -0
  79. package/src/styles.ts +373 -0
  80. package/src/swipe.ts +110 -0
  81. package/src/symbols.ts +595 -0
  82. package/src/typography.ts +76 -0
  83. package/src/vite-env.d.ts +26 -0
  84. package/src/wasm.ts +400 -0
@@ -0,0 +1,1421 @@
1
+ /**
2
+ * TypeScript mirror of docs/ops-protocol.md.
3
+ *
4
+ * The Swift core emits a stream of ops that the renderer applies to the DOM.
5
+ * Element ids are integers assigned by Swift; id 0 is the root container (the
6
+ * device screen) and is never created or removed.
7
+ */
8
+
9
+ export type ElementId = number;
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Colors
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Explicit sRGB color; every component is in 0...1. */
16
+ export interface RGBAColor {
17
+ r: number;
18
+ g: number;
19
+ b: number;
20
+ a: number;
21
+ }
22
+
23
+ export type SemanticColorName =
24
+ | 'primary'
25
+ | 'secondary'
26
+ | 'accent'
27
+ | 'systemBackground'
28
+ | 'secondarySystemBackground'
29
+ | 'clear';
30
+
31
+ /**
32
+ * Semantic color the renderer resolves for the current color scheme. `a`
33
+ * (0...1) is an optional opacity multiplier applied on top of the token.
34
+ */
35
+ export interface NamedColor {
36
+ name: SemanticColorName;
37
+ a?: number;
38
+ }
39
+
40
+ /**
41
+ * Phase 5: a color from the app's asset catalog (a `.colorset`, possibly
42
+ * namespaced like `dough/plain-bg`). The renderer resolves it for the current
43
+ * color scheme through the catalog manifest (src/assets.ts); `opacity`
44
+ * (0...1) multiplies the catalog alpha. Unknown names render magenta.
45
+ */
46
+ export interface AssetColor {
47
+ named: string;
48
+ opacity: number;
49
+ }
50
+
51
+ export type Color = RGBAColor | NamedColor | AssetColor;
52
+
53
+ // Phase 6 (docs/phase6/styles.md): ShapeStyle, everything a view can be
54
+ // painted with. A `Color` is one; the others resolve to CSS that depends on
55
+ // the role (text or paint), see src/styles.ts.
56
+
57
+ /** A unit point in the view's bounds (0...1 on each axis). */
58
+ export interface UnitPoint {
59
+ x: number;
60
+ y: number;
61
+ }
62
+
63
+ /**
64
+ * Every non-color ShapeStyle may carry an `opacity` multiplier (0...1) at
65
+ * its top level (`.quaternary.opacity(0.5)`, `.thinMaterial.opacity(0.8)`),
66
+ * applied like a semantic color's `a`.
67
+ */
68
+ export interface StyleOpacity {
69
+ opacity?: number;
70
+ }
71
+
72
+ /**
73
+ * `.primary` ... `.quaternary`. As a foreground (text) the label colors; as a
74
+ * background, fill or stroke the iOS fill colors of the same level.
75
+ */
76
+ export interface HierarchicalStyle extends StyleOpacity {
77
+ hierarchical: 1 | 2 | 3 | 4;
78
+ }
79
+
80
+ export interface GradientStop {
81
+ color: Color;
82
+ /** 0...1 along the gradient. */
83
+ location: number;
84
+ }
85
+
86
+ export interface LinearGradient {
87
+ stops: GradientStop[];
88
+ start: UnitPoint;
89
+ end: UnitPoint;
90
+ }
91
+ export interface LinearGradientStyle extends StyleOpacity {
92
+ linearGradient: LinearGradient;
93
+ }
94
+
95
+ export interface RadialGradient {
96
+ stops: GradientStop[];
97
+ center: UnitPoint;
98
+ /** Points. */
99
+ startRadius: number;
100
+ endRadius: number;
101
+ }
102
+ export interface RadialGradientStyle extends StyleOpacity {
103
+ radialGradient: RadialGradient;
104
+ }
105
+
106
+ /** `Color.gradient`: a subtle vertical gradient derived from one color. */
107
+ export interface ColorGradientStyle extends StyleOpacity {
108
+ colorGradient: Color;
109
+ }
110
+
111
+ export type MaterialName = 'ultraThin' | 'thin' | 'regular' | 'thick' | 'ultraThick';
112
+ /** A translucent material: the system background at some opacity over a blurred backdrop. */
113
+ export interface MaterialStyle extends StyleOpacity {
114
+ material: MaterialName;
115
+ }
116
+
117
+ export type ShapeStyle =
118
+ | Color
119
+ | HierarchicalStyle
120
+ | LinearGradientStyle
121
+ | RadialGradientStyle
122
+ | ColorGradientStyle
123
+ | MaterialStyle;
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Style
127
+ // ---------------------------------------------------------------------------
128
+
129
+ export interface EdgeInsets {
130
+ top?: number;
131
+ leading?: number;
132
+ bottom?: number;
133
+ trailing?: number;
134
+ }
135
+
136
+ export type FontWeight =
137
+ | 'ultraLight'
138
+ | 'thin'
139
+ | 'light'
140
+ | 'regular'
141
+ | 'medium'
142
+ | 'semibold'
143
+ | 'bold'
144
+ | 'heavy'
145
+ | 'black';
146
+ export type FontDesign = 'default' | 'serif' | 'rounded' | 'monospaced';
147
+
148
+ /** iOS text styles; resolved by the renderer for the current Dynamic Type size. */
149
+ export type TextStyle =
150
+ | 'largeTitle'
151
+ | 'title'
152
+ | 'title2'
153
+ | 'title3'
154
+ | 'headline'
155
+ | 'subheadline'
156
+ | 'body'
157
+ | 'callout'
158
+ | 'footnote'
159
+ | 'caption'
160
+ | 'caption2';
161
+
162
+ /**
163
+ * Every field is optional. `size` and `textStyle` are mutually exclusive: a
164
+ * `textStyle` scales with Dynamic Type, a fixed `size` does not unless
165
+ * `relativeTo` names the text style it should scale like. `headline` implies
166
+ * `semibold` unless `weight` is given.
167
+ */
168
+ export interface Font {
169
+ textStyle?: TextStyle;
170
+ size?: number;
171
+ relativeTo?: TextStyle;
172
+ weight?: FontWeight;
173
+ design?: FontDesign;
174
+ }
175
+
176
+ export type FrameAlignment =
177
+ | 'center'
178
+ | 'leading'
179
+ | 'trailing'
180
+ | 'top'
181
+ | 'bottom'
182
+ | 'topLeading'
183
+ | 'topTrailing'
184
+ | 'bottomLeading'
185
+ | 'bottomTrailing';
186
+
187
+ /** A frame dimension: points, or `"infinity"` to fill the available space. */
188
+ export type Dimension = number | 'infinity';
189
+
190
+ export interface Frame {
191
+ width?: number | null;
192
+ height?: number | null;
193
+ minWidth?: Dimension | null;
194
+ maxWidth?: Dimension | null;
195
+ minHeight?: Dimension | null;
196
+ maxHeight?: Dimension | null;
197
+ /** Positions the single child inside the frame; defaults to `center`. */
198
+ alignment?: FrameAlignment;
199
+ }
200
+
201
+ // Phase 3 animation (docs/ops-protocol.md, "Phase 3 additions").
202
+
203
+ export type AnimationType = 'default' | 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'spring';
204
+
205
+ /**
206
+ * `duration` defaults to 0.35s. For `spring` it is the perceptual duration
207
+ * and `bounce` (0 = critically damped, 0.3 = bouncy) shapes the curve.
208
+ */
209
+ export interface Animation {
210
+ type: AnimationType;
211
+ duration?: number;
212
+ bounce?: number;
213
+ delay?: number;
214
+ }
215
+
216
+ export type TransitionKind = 'opacity' | 'scale' | 'slide' | 'move' | 'offset' | 'identity' | 'asymmetric';
217
+ export type Edge = 'top' | 'leading' | 'bottom' | 'trailing';
218
+
219
+ export interface TransitionPart {
220
+ kind: TransitionKind;
221
+ /** `move` only. */
222
+ edge?: Edge;
223
+ /** `scale` only; defaults to 0.5. */
224
+ scale?: number;
225
+ /** `offset` only: the translation (points) the view enters from / exits to. */
226
+ x?: number;
227
+ y?: number;
228
+ /**
229
+ * Phase 6, `asymmetric` only (`.asymmetric(insertion:removal:)`): the
230
+ * transition played when the view is inserted, and the one when removed.
231
+ */
232
+ insertion?: Transition;
233
+ removal?: Transition;
234
+ }
235
+
236
+ /** One transition, or several combined. */
237
+ export type Transition = TransitionPart | TransitionPart[];
238
+
239
+ export type TextAlignment = 'leading' | 'center' | 'trailing';
240
+
241
+ export interface FixedSize {
242
+ horizontal: boolean;
243
+ vertical: boolean;
244
+ }
245
+
246
+ // Phase 5 (docs/ops-protocol.md, "Phase 5 additions").
247
+
248
+ export type ContentMode = 'fit' | 'fill';
249
+
250
+ /**
251
+ * `.aspectRatio(_:contentMode:)`. `ratio: null` keeps the child's own ideal
252
+ * aspect ratio (`.scaledToFit()` is `{ ratio: null, contentMode: "fit" }`,
253
+ * `.scaledToFill()` the `fill` form).
254
+ */
255
+ export interface AspectRatio {
256
+ ratio: number | null;
257
+ contentMode: ContentMode;
258
+ }
259
+
260
+ /** `.shadow(color:radius:x:y:)`; rendered as a `drop-shadow` filter. */
261
+ export interface Shadow {
262
+ color: Color;
263
+ radius: number;
264
+ x: number;
265
+ y: number;
266
+ }
267
+
268
+ /** Every field is optional. */
269
+ export interface Style {
270
+ padding?: EdgeInsets;
271
+ font?: Font;
272
+ /** Phase 6: any ShapeStyle (text role). Gradients paint the text itself. */
273
+ foreground?: ShapeStyle;
274
+ /** Phase 6: any ShapeStyle (paint role). */
275
+ background?: ShapeStyle;
276
+ frame?: Frame;
277
+ cornerRadius?: number;
278
+ opacity?: number;
279
+ /** Dims the subtree (`opacity: 0.4`) and blocks input (`pointer-events: none`). */
280
+ disabled?: boolean;
281
+ // Phase 3
282
+ /** Stack space distribution priority (default 0). */
283
+ layoutPriority?: number;
284
+ /** Maximum lines of text in the subtree; `null` lifts an inherited limit. */
285
+ lineLimit?: number | null;
286
+ multilineTextAlignment?: TextAlignment;
287
+ /** The child is proposed `nil` on the fixed axes and takes its ideal size. */
288
+ fixedSize?: FixedSize;
289
+ /**
290
+ * `.animation(_:value:)`: whenever `animationToken` changes in an `update`,
291
+ * the layout and style changes of this subtree in that commit animate with
292
+ * `animation`.
293
+ */
294
+ animation?: Animation;
295
+ animationToken?: number;
296
+ /** How this subtree appears and disappears inside an animated commit. */
297
+ transition?: Transition;
298
+ // Phase 5
299
+ /** Sizes the child to an aspect ratio inside the proposal (see `AspectRatio`). */
300
+ aspectRatio?: AspectRatio;
301
+ // Phase 6 visual effects (docs/phase6/styles.md): CSS only, no layout effect.
302
+ /** `.offset(x:y:)`: translation in points. */
303
+ offset?: { x: number; y: number };
304
+ /** `.rotationEffect`: degrees, clockwise positive. */
305
+ rotation?: number;
306
+ /** Unit point the rotation turns around (default 0.5/0.5). */
307
+ rotationAnchor?: UnitPoint;
308
+ /** `.scaleEffect`: per-axis scale factors. */
309
+ scale?: { x: number; y: number };
310
+ /** Unit point the scale grows from (default 0.5/0.5). */
311
+ scaleAnchor?: UnitPoint;
312
+ shadow?: Shadow;
313
+ /** `.clipped()`: `overflow: hidden` on the box. */
314
+ clipped?: boolean;
315
+
316
+ // Phase 6 (docs/phase6/layout-containers.md)
317
+ /** `.gridCellColumns(_:)`: how many `grid` columns the cell spans (default 1). */
318
+ gridCellColumns?: number;
319
+ /** `.gridCellAnchor(_:)`: unit point the cell is aligned by inside its cell box. */
320
+ gridCellAnchor?: UnitPoint;
321
+ /** `.tint(_:)`: sets `--sb-tint` on the box so gauges, switches and prominent buttons below pick it up. */
322
+ tint?: Color;
323
+ /**
324
+ * `.position(x:y:)`: the box takes the whole proposal (the child's ideal
325
+ * size on nil axes) and centers the child, measured with a nil proposal,
326
+ * at (x, y) inside it.
327
+ */
328
+ position?: Point;
329
+
330
+ // Phase 6 (docs/phase6/chrome.md)
331
+ /**
332
+ * `.badge(_:)`: when the styled element is a list row, this text is drawn
333
+ * trailing in the row in the secondary color (17pt) and its width is
334
+ * reserved, so the row content is proposed less width. `null` clears it.
335
+ */
336
+ badge?: string | null;
337
+ /**
338
+ * `.listRowInsets(_:)`: replaces the row's default insets (16 horizontal,
339
+ * 11 vertical) for the list row this styled element is.
340
+ */
341
+ listRowInsets?: EdgeInsets;
342
+ /**
343
+ * `.imageScale(_:)`: symbol images in the subtree (`image` with
344
+ * `systemName`, and catalog `symbol` images) measure 0.8 / 1 / 1.25 of the
345
+ * font line height.
346
+ */
347
+ imageScale?: ImageScale;
348
+ }
349
+
350
+
351
+ export interface Point {
352
+ x: number;
353
+ y: number;
354
+ }
355
+
356
+ export type ImageScale = 'small' | 'medium' | 'large';
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // Element kinds and props
360
+ // ---------------------------------------------------------------------------
361
+
362
+ export type VStackAlignment = 'leading' | 'center' | 'trailing';
363
+ export type HStackAlignment = 'top' | 'center' | 'bottom';
364
+
365
+ export interface TextProps {
366
+ text: string;
367
+ }
368
+ export interface VStackProps {
369
+ spacing: number | null;
370
+ alignment: VStackAlignment;
371
+ /** Phase 12 `.scrollTargetLayout()`: the children are the enclosing scroll view's snap points. */
372
+ scrollTarget?: boolean;
373
+ }
374
+ export interface HStackProps {
375
+ spacing: number | null;
376
+ alignment: HStackAlignment;
377
+ scrollTarget?: boolean;
378
+ }
379
+ export interface SpacerProps {
380
+ minLength: number | null;
381
+ }
382
+ export type ButtonStyle = 'automatic' | 'plain' | 'bordered' | 'borderedProminent';
383
+ /** Phase 8: `cancel` is the role an `alert` draws apart (bold, or in its own card). */
384
+ export type ButtonRole = 'destructive' | 'cancel' | null;
385
+ /**
386
+ * The label is the button's child. Phase 1 modules send `{}`; both fields are
387
+ * therefore optional and default to `automatic` / no role.
388
+ */
389
+ export interface ButtonProps {
390
+ style?: ButtonStyle;
391
+ role?: ButtonRole;
392
+ }
393
+ export interface StyledProps {
394
+ style: Style;
395
+ }
396
+
397
+ // Phase 2 kinds (docs/ops-protocol.md, "Phase 2 additions").
398
+
399
+ /** Same alignment names as `Frame.alignment`; later children draw on top. */
400
+ export interface ZStackProps {
401
+ alignment: FrameAlignment;
402
+ }
403
+ /** `systemName` is an SF Symbol name; the renderer picks the closest glyph. */
404
+ export interface SymbolImageProps {
405
+ systemName: string;
406
+ }
407
+ /**
408
+ * Phase 5: an image from the app's asset catalog. `name` is the catalog
409
+ * name, possibly namespaced (`dough/plain-full`). A non-resizable image is
410
+ * always its intrinsic point size; a `.resizable()` one fills the proposal
411
+ * (the intrinsic size on unconstrained axes). The renderer looks the name up
412
+ * in the catalog manifest (src/assets.ts).
413
+ */
414
+ export interface AssetImageProps {
415
+ name: string;
416
+ resizable: boolean;
417
+ }
418
+ export type ImageProps = SymbolImageProps | AssetImageProps;
419
+ export type DividerProps = Record<string, never>;
420
+ export type ScrollAxes = 'vertical' | 'horizontal' | 'both';
421
+ export interface ScrollViewProps {
422
+ axes: ScrollAxes;
423
+ }
424
+ /**
425
+ * Phase 4: `insetGrouped` (the default when omitted, as Phase 2 modules send
426
+ * `{}`), `grouped` (full-width sections with hairlines; `Form`) or `plain`.
427
+ */
428
+ export type ListStyle = 'plain' | 'insetGrouped' | 'grouped';
429
+ export interface ListProps {
430
+ style?: ListStyle;
431
+ }
432
+ export interface SectionProps {
433
+ header: string | null;
434
+ footer: string | null;
435
+ }
436
+ export type TextFieldStyle = 'plain' | 'roundedBorder';
437
+ export interface TextFieldProps {
438
+ text: string;
439
+ placeholder: string;
440
+ style: TextFieldStyle;
441
+ secure: boolean;
442
+ /** Phase 12 `.focused(...)`: whether the field should have focus; present only on bound fields, which report `focus` events. */
443
+ focused?: boolean;
444
+ /** Phase 12 `.onSubmit`: the Enter key sends a `submit` event. */
445
+ submit?: boolean;
446
+ /** Phase 12 `.submitLabel`: the return key's label (`enterkeyhint`). */
447
+ submitLabel?: string;
448
+ }
449
+ export interface ToggleProps {
450
+ isOn: boolean;
451
+ }
452
+ export type NavStackProps = Record<string, never>;
453
+ export type NavDisplayMode = 'automatic' | 'large' | 'inline';
454
+ export interface NavScreenProps {
455
+ title: string;
456
+ displayMode: NavDisplayMode;
457
+ /** 0 for the root screen; the renderer draws a back button when > 0. */
458
+ depth: number;
459
+ }
460
+ /** Phase 7 adds `selected` (see the block at the end of this file). */
461
+ export interface NavLinkProps {
462
+ selected?: boolean;
463
+ }
464
+
465
+ // Phase 3 kinds (docs/ops-protocol.md, "Phase 3 additions").
466
+
467
+ /** A box filled with `color`; fills its proposal. */
468
+ export interface ColorProps {
469
+ color: Color;
470
+ }
471
+ /** Phase 6 adds `containerRelative` (`ContainerRelativeShape`): a rounded rectangle with `cornerRadius`. */
472
+ export type ShapeKind = 'rectangle' | 'roundedRectangle' | 'circle' | 'capsule' | 'ellipse' | 'containerRelative';
473
+ export interface Stroke {
474
+ /** Phase 6: any ShapeStyle; a gradient stroke falls back to its first stop. */
475
+ color: ShapeStyle;
476
+ lineWidth: number;
477
+ /**
478
+ * Phase 6: `.strokeBorder` draws the stroke inside the shape's bounds. The
479
+ * renderer draws every stroke inside the box; `inset` is recorded as
480
+ * `data-sb-stroke`.
481
+ */
482
+ inset?: boolean;
483
+ }
484
+ /** `fill: null` means the current foreground color; fills its proposal. */
485
+ export interface ShapeProps {
486
+ shape: ShapeKind;
487
+ cornerRadius: number;
488
+ /** Phase 6: any ShapeStyle. */
489
+ fill: ShapeStyle | null;
490
+ stroke: Stroke | null;
491
+ }
492
+ /** Phase 8: `full` is `.fullScreenCover` (the card fills the screen). */
493
+ export type Detent = 'medium' | 'large' | 'full';
494
+ /**
495
+ * Always a root-level element (child of id 0) placed after the app's tree. A
496
+ * `tap` with the sheet's own id is a dismiss request.
497
+ */
498
+ export interface SheetProps {
499
+ detents: Detent[];
500
+ }
501
+
502
+ // Phase 4 kinds (docs/ops-protocol.md, "Phase 4 additions").
503
+
504
+ /** Fills the screen; a 49pt tab bar plus the bottom safe area at the bottom. */
505
+ export interface TabViewProps {
506
+ /** Index of the selected `tab` child (authoritative; a tap sends `select`). */
507
+ selected: number;
508
+ }
509
+ /** One tab of a `tabview`: its content is the child; the bar item shows `title` and `systemImage`. */
510
+ export interface TabProps {
511
+ title: string;
512
+ systemImage: string | null;
513
+ }
514
+ export type PickerStyle = 'segmented' | 'menu' | 'inline';
515
+ /**
516
+ * `segmented`: an iOS segmented control (32pt). `menu`: a row with `label`
517
+ * leading and the selected option plus a chevron trailing that opens a menu.
518
+ * `inline` (Phase 6): one 44pt row per option with a checkmark on the
519
+ * selected one, preceded by a dim `label` row when the label is non-empty;
520
+ * inside a `menu` element the options become a section of the popover.
521
+ * Choosing sends `select` with the index.
522
+ */
523
+ export interface PickerProps {
524
+ style: PickerStyle;
525
+ label: string;
526
+ options: string[];
527
+ selected: number;
528
+ }
529
+ /**
530
+ * `GeometryReader`: fills its proposal and reports its size through a
531
+ * `geometry` event whenever it changes. Exactly one child.
532
+ */
533
+ export type GeometryProps = Record<string, never>;
534
+
535
+ // Phase 6 kinds (docs/phase6/layout-containers.md).
536
+
537
+ /** A proposal axis as Swift sees it: points, `"infinity"` or `null` (nil). */
538
+ export type ProposalDimension = number | 'infinity' | null;
539
+ export interface LayoutProposal {
540
+ width: ProposalDimension;
541
+ height: ProposalDimension;
542
+ }
543
+ export interface LayoutSize {
544
+ width: number;
545
+ height: number;
546
+ }
547
+ export interface LayoutFrame {
548
+ x: number;
549
+ y: number;
550
+ width: number;
551
+ height: number;
552
+ }
553
+ /**
554
+ * A custom SwiftUI `Layout` evaluated on the Swift side with host-measured
555
+ * subviews. The children are the subviews, in order. `size` and `frames`
556
+ * (relative to the element's origin, one per child) are Swift's answer for
557
+ * `proposal`, or `null` before the first answer; the renderer measures the
558
+ * children and reports them through a `layout` event.
559
+ */
560
+ export interface LayoutProps {
561
+ proposal: LayoutProposal | null;
562
+ size: LayoutSize | null;
563
+ frames: LayoutFrame[] | null;
564
+ /**
565
+ * The layout's own `sizeThatFits` for a zero, unspecified and infinite
566
+ * proposal (`"infinity"` for an unbounded dimension), sent with every
567
+ * answer. A parent measuring the layout with a proposal other than the
568
+ * answered one reads its flexibility from these instead of guessing.
569
+ */
570
+ sizes?: LayoutSizes | null;
571
+ }
572
+ export interface LayoutSizes {
573
+ min: LayoutFlexSize;
574
+ ideal: LayoutFlexSize;
575
+ max: LayoutFlexSize;
576
+ }
577
+ /** A size whose dimensions may be unbounded. */
578
+ export interface LayoutFlexSize {
579
+ width: number | 'infinity';
580
+ height: number | 'infinity';
581
+ }
582
+ /** `Grid`: `horizontalSpacing`/`verticalSpacing` default to 8. Children are `gridrow`s or single full-span cells. */
583
+ export interface GridProps {
584
+ horizontalSpacing: number | null;
585
+ verticalSpacing: number | null;
586
+ alignment: FrameAlignment;
587
+ }
588
+ export type GridRowAlignment = 'top' | 'center' | 'bottom' | 'firstTextBaseline' | 'lastTextBaseline';
589
+ /** `GridRow`: the children are the cells. `alignment: null` uses the grid's. */
590
+ export interface GridRowProps {
591
+ alignment: GridRowAlignment | null;
592
+ }
593
+ export type GridItemSize =
594
+ | { kind: 'fixed'; value: number }
595
+ | { kind: 'flexible'; minimum: number; maximum: Dimension }
596
+ | { kind: 'adaptive'; minimum: number; maximum: Dimension };
597
+ /** `GridItem`: `spacing` (to the next column) defaults to 8, `alignment` to the grid's. */
598
+ export interface GridItem {
599
+ size: GridItemSize;
600
+ spacing: number | null;
601
+ alignment: FrameAlignment | null;
602
+ }
603
+ /** `LazyVGrid`: `spacing` between rows defaults to 8. Children are the cells (flattened). Nothing is lazy. */
604
+ export interface LazyVGridProps {
605
+ columns: GridItem[];
606
+ spacing: number | null;
607
+ alignment: FrameAlignment;
608
+ }
609
+ export type GaugeStyle =
610
+ | 'automatic'
611
+ | 'linearCapacity'
612
+ | 'accessoryLinear'
613
+ | 'accessoryCircular'
614
+ | 'accessoryCircularCapacity';
615
+ /**
616
+ * `Gauge`: `value` is normalized to 0...1. Children in order: the label (when
617
+ * `hasLabel`) and the current value label (when `hasCurrentValueLabel`).
618
+ * Circular styles render as linear for now.
619
+ */
620
+ export interface GaugeProps {
621
+ value: number;
622
+ style: GaugeStyle;
623
+ hasLabel: boolean;
624
+ hasCurrentValueLabel: boolean;
625
+ }
626
+ export type DecorationRole = 'overlay' | 'background';
627
+ /**
628
+ * `.overlay { }` / `.background { }` with a view: children are
629
+ * `[content, decoration]`. The decoration is proposed the content's size,
630
+ * aligned inside it and never affects the size.
631
+ */
632
+ export interface DecoratedProps {
633
+ role: DecorationRole;
634
+ alignment: FrameAlignment;
635
+ }
636
+ // Phase 6 kinds (docs/phase6/chrome.md).
637
+
638
+ /**
639
+ * `Menu { items } label: { label }`: children are `[label, ...items]`. Only
640
+ * the label is laid out (the element is the label's size); tapping it opens
641
+ * a popover built from the current items (`button`/`navlink` rows that send
642
+ * `tap`, `picker` sections that send `select`, `divider`, `section`, `text`).
643
+ */
644
+ export type MenuProps = Record<string, never>;
645
+ export type ToolbarPlacement = 'trailing' | 'leading' | 'principal' | 'bottom';
646
+ /**
647
+ * `.toolbar { ToolbarItem(placement:) { ... } }`, an extra child of a
648
+ * `navscreen` after its content. The children are the items, laid out at
649
+ * their ideal size in a 16pt-spaced hstack inside the navigation bar
650
+ * (`bottom`: a 49pt bar plus the bottom safe area, items spread evenly).
651
+ */
652
+ export interface ToolbarProps {
653
+ placement: ToolbarPlacement;
654
+ }
655
+ /**
656
+ * `.searchable(text:)`, an extra child of a `navscreen`: a 36pt rounded
657
+ * search bar under the title area (52pt added to the bar). Typing sends
658
+ * `text` events exactly like a `textfield`.
659
+ */
660
+ export interface SearchFieldProps {
661
+ text: string;
662
+ /** Defaults to "Search". */
663
+ placeholder: string;
664
+ }
665
+ /** `.onTapGesture`: one child, the child's size; a click anywhere on it sends `tap`. */
666
+ export type TapGestureProps = Record<string, never>;
667
+
668
+ export interface ElementPropsByKind {
669
+ text: TextProps;
670
+ vstack: VStackProps;
671
+ hstack: HStackProps;
672
+ spacer: SpacerProps;
673
+ button: ButtonProps;
674
+ styled: StyledProps;
675
+ zstack: ZStackProps;
676
+ image: ImageProps;
677
+ divider: DividerProps;
678
+ scrollview: ScrollViewProps;
679
+ list: ListProps;
680
+ section: SectionProps;
681
+ textfield: TextFieldProps;
682
+ toggle: ToggleProps;
683
+ navstack: NavStackProps;
684
+ navscreen: NavScreenProps;
685
+ navlink: NavLinkProps;
686
+ color: ColorProps;
687
+ shape: ShapeProps;
688
+ sheet: SheetProps;
689
+ tabview: TabViewProps;
690
+ tab: TabProps;
691
+ picker: PickerProps;
692
+ geometry: GeometryProps;
693
+ layout: LayoutProps;
694
+ grid: GridProps;
695
+ gridrow: GridRowProps;
696
+ lazyvgrid: LazyVGridProps;
697
+ gauge: GaugeProps;
698
+ decorated: DecoratedProps;
699
+ menu: MenuProps;
700
+ toolbar: ToolbarProps;
701
+ searchfield: SearchFieldProps;
702
+ tapgesture: TapGestureProps;
703
+ }
704
+
705
+ export type ElementKind = keyof ElementPropsByKind;
706
+
707
+ export type ElementProps = ElementPropsByKind[ElementKind];
708
+
709
+ /** Discriminated `kind` + matching `props` pair. */
710
+ export type ElementDescriptor = {
711
+ [K in ElementKind]: { kind: K; props: ElementPropsByKind[K] };
712
+ }[ElementKind];
713
+
714
+ // ---------------------------------------------------------------------------
715
+ // Ops
716
+ // ---------------------------------------------------------------------------
717
+
718
+ export type CreateOp = { op: 'create'; id: ElementId } & ElementDescriptor;
719
+
720
+ export interface UpdateOp {
721
+ op: 'update';
722
+ id: ElementId;
723
+ /** Replaces the element's props entirely. */
724
+ props: ElementProps;
725
+ }
726
+
727
+ export interface InsertOp {
728
+ op: 'insert';
729
+ parent: ElementId;
730
+ id: ElementId;
731
+ /** Insert as child `index` of `parent`, moving the element if already attached. */
732
+ index: number;
733
+ }
734
+
735
+ export interface RemoveOp {
736
+ op: 'remove';
737
+ id: ElementId;
738
+ }
739
+
740
+ /**
741
+ * End of a render pass. Renderers may batch DOM work until this. With
742
+ * `animation` (from `withAnimation`) every frame and style change of the pass
743
+ * animates, inserted elements transition in and removed ones out.
744
+ */
745
+ export interface CommitOp {
746
+ op: 'commit';
747
+ animation?: Animation;
748
+ }
749
+
750
+ /**
751
+ * Phase 11: the app's `UserDefaults`, whole, after a change. The host keeps
752
+ * them (localStorage, per app) and hands them back at the next launch through
753
+ * the `SB_DEFAULTS` environment variable. Not a rendering op: the loader
754
+ * consumes it before the renderer sees the batch.
755
+ */
756
+ export interface DefaultsOp {
757
+ op: 'defaults';
758
+ values: Record<string, unknown>;
759
+ }
760
+
761
+ export type Op = CreateOp | UpdateOp | InsertOp | RemoveOp | CommitOp | DefaultsOp;
762
+
763
+ // ---------------------------------------------------------------------------
764
+ // Events (renderer → Swift)
765
+ // ---------------------------------------------------------------------------
766
+
767
+ export type ColorScheme = 'light' | 'dark';
768
+
769
+ export type DynamicTypeSize =
770
+ | 'xSmall'
771
+ | 'small'
772
+ | 'medium'
773
+ | 'large'
774
+ | 'xLarge'
775
+ | 'xxLarge'
776
+ | 'xxxLarge'
777
+ | 'accessibility1'
778
+ | 'accessibility2'
779
+ | 'accessibility3'
780
+ | 'accessibility4'
781
+ | 'accessibility5';
782
+
783
+ export const DYNAMIC_TYPE_SIZES: readonly DynamicTypeSize[] = [
784
+ 'xSmall',
785
+ 'small',
786
+ 'medium',
787
+ 'large',
788
+ 'xLarge',
789
+ 'xxLarge',
790
+ 'xxxLarge',
791
+ 'accessibility1',
792
+ 'accessibility2',
793
+ 'accessibility3',
794
+ 'accessibility4',
795
+ 'accessibility5',
796
+ ];
797
+
798
+ export function isDynamicTypeSize(value: unknown): value is DynamicTypeSize {
799
+ return typeof value === 'string' && (DYNAMIC_TYPE_SIZES as readonly string[]).includes(value);
800
+ }
801
+
802
+ /**
803
+ * A `button`, `navlink`, `tapgesture`, (for the back button) `navscreen` or
804
+ * (for a dismiss request) `sheet` was tapped.
805
+ */
806
+ export interface TapEvent {
807
+ type: 'tap';
808
+ id: ElementId;
809
+ }
810
+ /** A `textfield` or `searchfield` received input. */
811
+ export interface TextEvent {
812
+ type: 'text';
813
+ id: ElementId;
814
+ value: string;
815
+ }
816
+ /** A `toggle` was switched. */
817
+ export interface ToggleEvent {
818
+ type: 'toggle';
819
+ id: ElementId;
820
+ value: boolean;
821
+ }
822
+ /**
823
+ * The device appearance or text size changed (also sent once right after
824
+ * `_start`). Either field may be omitted; this renderer always sends both.
825
+ */
826
+ export interface EnvironmentEvent {
827
+ type: 'environment';
828
+ colorScheme?: ColorScheme;
829
+ dynamicTypeSize?: DynamicTypeSize;
830
+ }
831
+
832
+ /** A `tabview` bar item or a `picker` option was chosen; `value` is the index. */
833
+ export interface SelectEvent {
834
+ type: 'select';
835
+ id: ElementId;
836
+ value: number;
837
+ }
838
+ /**
839
+ * A `geometry` element's laid-out size (whole CSS px), sent after a layout
840
+ * pass only when it differs from the last reported size.
841
+ */
842
+ export interface GeometryEvent {
843
+ type: 'geometry';
844
+ id: ElementId;
845
+ width: number;
846
+ height: number;
847
+ }
848
+
849
+ /** Host measurements of one subview of a `layout` element (points, 2 decimals; `"infinity"` for an unbounded axis). */
850
+ export interface LayoutChildMeasurement {
851
+ min: { width: Dimension; height: Dimension };
852
+ ideal: { width: Dimension; height: Dimension };
853
+ max: { width: Dimension; height: Dimension };
854
+ }
855
+ /**
856
+ * Phase 6: sent after a layout pass for every `layout` element whose record
857
+ * (the proposal it was placed with and its children's min / ideal / max sizes)
858
+ * differs from the last one sent for that id. Swift answers by updating the
859
+ * element's `size` and `frames` props.
860
+ */
861
+ export interface LayoutEvent {
862
+ type: 'layout';
863
+ id: ElementId;
864
+ proposal: LayoutProposal;
865
+ children: LayoutChildMeasurement[];
866
+ }
867
+
868
+ /**
869
+ * Everything the renderer sends to Swift. Taps travel through `sb_event(id)`,
870
+ * the rest as UTF-8 JSON through `sb_alloc` + `sb_event_json`.
871
+ */
872
+ export type RendererEvent =
873
+ | TapEvent
874
+ | TextEvent
875
+ | ToggleEvent
876
+ | EnvironmentEvent
877
+ | SelectEvent
878
+ | GeometryEvent
879
+ | LayoutEvent
880
+ | BatchEvent
881
+ // Phase 8: gestures, rows, navigation (see the block at the end of this file)
882
+ | InteractionEvent
883
+ // Phase 8: alerts, controls (see the block at the end of this file)
884
+ | Phase8ControlEvent;
885
+
886
+ /**
887
+ * Several events delivered in one `sb_event_json` call: Swift applies each
888
+ * and renders once. The renderer sends one per layout pass for the pass's
889
+ * `geometry` and `layout` events, so a screen with many measured elements
890
+ * costs one render instead of one per element. Never nested.
891
+ */
892
+ export interface BatchEvent {
893
+ type: 'batch';
894
+ events: Exclude<RendererEvent, BatchEvent>[];
895
+ }
896
+
897
+ /** Headless-mode echo of a replayed event; never sent to the browser renderer. */
898
+ export type EventEchoOp = { op: 'event' } & RendererEvent;
899
+
900
+ export const ROOT_ID: ElementId = 0;
901
+
902
+ /** `spacing: null` means "system default", which the renderer treats as 8px. */
903
+ export const DEFAULT_SPACING = 8;
904
+
905
+ export function isNamedColor(color: ShapeStyle): color is NamedColor {
906
+ return typeof (color as NamedColor).name === 'string';
907
+ }
908
+
909
+ export function isAssetColor(color: ShapeStyle): color is AssetColor {
910
+ return typeof (color as AssetColor).named === 'string';
911
+ }
912
+
913
+ export function isRGBAColor(style: ShapeStyle): style is RGBAColor {
914
+ const c = style as RGBAColor;
915
+ return typeof c.r === 'number' && typeof c.g === 'number' && typeof c.b === 'number';
916
+ }
917
+
918
+ /** True for the three `Color` forms (explicit, semantic, catalog). */
919
+ export function isColor(style: ShapeStyle): style is Color {
920
+ return isRGBAColor(style) || isNamedColor(style) || isAssetColor(style);
921
+ }
922
+
923
+ export function isHierarchicalStyle(style: ShapeStyle): style is HierarchicalStyle {
924
+ return typeof (style as HierarchicalStyle).hierarchical === 'number';
925
+ }
926
+
927
+ export function isLinearGradientStyle(style: ShapeStyle): style is LinearGradientStyle {
928
+ const g = (style as LinearGradientStyle).linearGradient;
929
+ return typeof g === 'object' && g !== null;
930
+ }
931
+
932
+ export function isRadialGradientStyle(style: ShapeStyle): style is RadialGradientStyle {
933
+ const g = (style as RadialGradientStyle).radialGradient;
934
+ return typeof g === 'object' && g !== null;
935
+ }
936
+
937
+ export function isColorGradientStyle(style: ShapeStyle): style is ColorGradientStyle {
938
+ const c = (style as ColorGradientStyle).colorGradient;
939
+ return typeof c === 'object' && c !== null;
940
+ }
941
+
942
+ export function isMaterialStyle(style: ShapeStyle): style is MaterialStyle {
943
+ return typeof (style as MaterialStyle).material === 'string';
944
+ }
945
+
946
+ /** True for the asset catalog form of `image` props (`{ name, resizable }`). */
947
+ export function isAssetImage(props: ImageProps): props is AssetImageProps {
948
+ return typeof (props as AssetImageProps).name === 'string';
949
+ }
950
+
951
+ /** Cheap structural check used when parsing an untrusted JSON buffer. */
952
+ export function isOp(value: unknown): value is Op {
953
+ if (typeof value !== 'object' || value === null) return false;
954
+ const op = (value as { op?: unknown }).op;
955
+ return (
956
+ op === 'create' || op === 'update' || op === 'insert' || op === 'remove' || op === 'commit' || op === 'defaults'
957
+ );
958
+ }
959
+
960
+ // ---------------------------------------------------------------------------
961
+ // Phase 7: navigation, ViewThatFits, styles
962
+ // (docs/ops-protocol.md, "Phase 7"; charts live in their own block)
963
+ // ---------------------------------------------------------------------------
964
+
965
+ /** `NavigationSplitView` in the regular size class: children are `[sidebar, detail]`, each a `navstack`. */
966
+ export type NavSplitProps = Record<string, never>;
967
+
968
+ export type ViewThatFitsAxes = 'horizontal' | 'vertical' | 'both';
969
+ /**
970
+ * `ViewThatFits`: the children are the candidates, in order. The engine shows
971
+ * the first whose ideal size fits the proposal on `axes` (default `both`), or
972
+ * the last when none fits; the others get no frame and are hidden.
973
+ */
974
+ export interface ViewThatFitsProps {
975
+ axes?: ViewThatFitsAxes;
976
+ }
977
+
978
+ export interface ElementPropsByKind {
979
+ navsplit: NavSplitProps;
980
+ viewthatfits: ViewThatFitsProps;
981
+ }
982
+
983
+ export interface Style {
984
+ /** `.hidden()`: the box keeps its layout but draws nothing and takes no input. */
985
+ hidden?: true;
986
+ /**
987
+ * `.mask { Gradient / Color }`: a CSS `mask-image` built from the style
988
+ * (gradients as CSS gradients, a `Color` as a solid mask with its alpha).
989
+ */
990
+ mask?: ShapeStyle;
991
+ }
992
+
993
+ /** `UserInterfaceSizeClass`: `regular` when the screen is at least 700 CSS px wide. */
994
+ export type HorizontalSizeClass = 'compact' | 'regular';
995
+ /** Threshold (CSS px) between the compact and regular horizontal size class. */
996
+ export const REGULAR_WIDTH_MIN = 700;
997
+
998
+ export interface EnvironmentEvent {
999
+ horizontalSizeClass?: HorizontalSizeClass;
1000
+ }
1001
+
1002
+ /** The `horizontalSizeClass` for a screen `width` CSS px wide. */
1003
+ export function horizontalSizeClassFor(width: number): HorizontalSizeClass {
1004
+ return Number.isFinite(width) && width >= REGULAR_WIDTH_MIN ? 'regular' : 'compact';
1005
+ }
1006
+
1007
+ // ---------------------------------------------------------------------------
1008
+ // Phase 7: charts (docs/ops-protocol.md, "Phase 7"). Swift turns data into
1009
+ // unit coordinates (0...1 of the plot rectangle, y top → bottom); the renderer
1010
+ // measures labels, derives the plot rectangle and draws the marks as SVG.
1011
+ // ---------------------------------------------------------------------------
1012
+
1013
+ export type ChartInterpolation =
1014
+ | 'linear'
1015
+ | 'cardinal'
1016
+ | 'catmullRom'
1017
+ | 'monotone'
1018
+ | 'stepStart'
1019
+ | 'stepCenter'
1020
+ | 'stepEnd';
1021
+
1022
+ export type ChartSymbol = 'circle' | 'square' | 'triangle' | 'diamond' | 'cross' | 'plus' | 'asterisk';
1023
+
1024
+ /** Fields every mark carries. */
1025
+ export interface ChartMarkBase {
1026
+ style: ShapeStyle;
1027
+ /** 0...1, default 1. */
1028
+ opacity?: number;
1029
+ /** An `area` or `rect` mark whose geometry clips this mark (an SVG `clipPath`). */
1030
+ mask?: ChartMark | null;
1031
+ }
1032
+ /** `BarMark` / `RectangleMark`: a filled `<rect>` over the unit rect. */
1033
+ export interface ChartRectMark extends ChartMarkBase {
1034
+ kind: 'rect';
1035
+ x0: number;
1036
+ x1: number;
1037
+ y0: number;
1038
+ y1: number;
1039
+ /** Px, default 0. */
1040
+ cornerRadius?: number;
1041
+ /** Px: the rect is this wide, centered on `(x0 + x1) / 2`. */
1042
+ fixedWidth?: number | null;
1043
+ }
1044
+ export interface ChartLinePoint {
1045
+ x: number;
1046
+ y: number;
1047
+ }
1048
+ /** `LineMark`: a stroked `<path>` plus one symbol glyph per point. */
1049
+ export interface ChartLineMark extends ChartMarkBase {
1050
+ kind: 'line';
1051
+ points: ChartLinePoint[];
1052
+ /** Px, default 2. */
1053
+ lineWidth?: number;
1054
+ interpolation?: ChartInterpolation;
1055
+ symbol?: ChartSymbol | null;
1056
+ /** Px, default 8. */
1057
+ symbolSize?: number;
1058
+ }
1059
+ export interface ChartAreaPoint {
1060
+ x: number;
1061
+ /** `y0 <= y1` (y runs top → bottom, so `y0` is the upper edge). */
1062
+ y0: number;
1063
+ y1: number;
1064
+ }
1065
+ /** `AreaMark`: a filled `<path>` between the two curves. */
1066
+ export interface ChartAreaMark extends ChartMarkBase {
1067
+ kind: 'area';
1068
+ points: ChartAreaPoint[];
1069
+ interpolation?: ChartInterpolation;
1070
+ }
1071
+ /** `PointMark`: one symbol glyph. */
1072
+ export interface ChartPointMark extends ChartMarkBase {
1073
+ kind: 'point';
1074
+ x: number;
1075
+ y: number;
1076
+ symbol: ChartSymbol;
1077
+ /** Px, default 8. */
1078
+ symbolSize?: number;
1079
+ }
1080
+ /** `RuleMark`: a stroked line between the two points. */
1081
+ export interface ChartRuleMark extends ChartMarkBase {
1082
+ kind: 'rule';
1083
+ x0: number;
1084
+ x1: number;
1085
+ y0: number;
1086
+ y1: number;
1087
+ /** Px, default 1. */
1088
+ lineWidth?: number;
1089
+ }
1090
+ export type ChartMark = ChartRectMark | ChartLineMark | ChartAreaMark | ChartPointMark | ChartRuleMark;
1091
+
1092
+ export type ChartXAxisPosition = 'bottom' | 'top';
1093
+ export type ChartYAxisPosition = 'trailing' | 'leading';
1094
+ export interface ChartAxisLabel {
1095
+ /** Unit position along the axis (y: 0 is the top). */
1096
+ at: number;
1097
+ text: string;
1098
+ }
1099
+ /** `null` for the whole axis means hidden. Positions are unit values along the axis. */
1100
+ export interface ChartAxis {
1101
+ position: ChartXAxisPosition | ChartYAxisPosition;
1102
+ gridLines: number[];
1103
+ ticks: number[];
1104
+ /** Default text labels; view labels are `chartitem` children with role `xLabel` / `yLabel`. */
1105
+ labels: ChartAxisLabel[];
1106
+ }
1107
+
1108
+ export type ChartLegendPosition = 'top' | 'bottom' | 'leading' | 'trailing';
1109
+ export interface ChartLegendItem {
1110
+ label: string;
1111
+ style: ShapeStyle;
1112
+ /** `null`: an 18 × 3 line glyph instead of a symbol. */
1113
+ symbol: ChartSymbol | null;
1114
+ }
1115
+ export interface ChartLegend {
1116
+ position: ChartLegendPosition;
1117
+ items: ChartLegendItem[];
1118
+ }
1119
+
1120
+ /** `Chart`: children are `chartitem`s (annotations and view axis labels), in any order. */
1121
+ export interface ChartProps {
1122
+ marks: ChartMark[];
1123
+ xAxis: ChartAxis | null;
1124
+ yAxis: ChartAxis | null;
1125
+ legend: ChartLegend | null;
1126
+ }
1127
+
1128
+ export type ChartItemRole = 'annotation' | 'xLabel' | 'yLabel';
1129
+ export type ChartItemPosition = 'top' | 'bottom' | 'leading' | 'trailing' | 'overlay';
1130
+ /**
1131
+ * One view inside a `chart`: an annotation placed against the unit rect
1132
+ * `x0…x1 × y0…y1` of its mark, or an axis label view whose tick is
1133
+ * `x0 == x1` (x) / `y0 == y1` (y). Exactly one child.
1134
+ */
1135
+ export interface ChartItemProps {
1136
+ role: ChartItemRole;
1137
+ x0: number;
1138
+ x1: number;
1139
+ y0: number;
1140
+ y1: number;
1141
+ position: ChartItemPosition;
1142
+ alignment: FrameAlignment;
1143
+ /** Px between the mark's rect and the annotation (default 0). */
1144
+ spacing: number;
1145
+ }
1146
+
1147
+ // Declaration merging adds the Phase 7 kinds to `ElementPropsByKind` (and so
1148
+ // to `ElementKind`, `ElementProps`, `ElementDescriptor` and `CreateOp`)
1149
+ // without touching the Phase 1-6 table above.
1150
+ export interface ElementPropsByKind {
1151
+ chart: ChartProps;
1152
+ chartitem: ChartItemProps;
1153
+ }
1154
+
1155
+ export const CHART_KINDS: readonly ElementKind[] = ['chart', 'chartitem'];
1156
+
1157
+ // ---------------------------------------------------------------------------
1158
+ // Phase 8: gestures, rows, navigation (docs/ops-protocol.md, "Phase 8:
1159
+ // interaction"). `gesture`, `swipeactions`, `contextmenu` and `listrow` are
1160
+ // layout-transparent wrappers; the renderer tracks pointers on them
1161
+ // (src/interaction.ts). Declaration merging adds the kinds, the `Style` hint
1162
+ // and the `list` / `scrollview` fields without touching the tables above.
1163
+ // ---------------------------------------------------------------------------
1164
+
1165
+ /**
1166
+ * `.gesture(DragGesture())` / `.onLongPressGesture`: exactly one child, the
1167
+ * child's size. With `drag`, a pointer that moves `minimumDistance` (default
1168
+ * 10, 0 for an immediate gesture) starts a drag and the element receives
1169
+ * `drag` events; with `longPress`, a pointer held `minimumDuration` (default
1170
+ * 0.5 s) without moving 10 px sends `longpress`.
1171
+ */
1172
+ export interface GestureProps {
1173
+ drag?: boolean;
1174
+ longPress?: boolean;
1175
+ minimumDistance?: number;
1176
+ minimumDuration?: number;
1177
+ }
1178
+
1179
+ /**
1180
+ * `.swipeActions`: children are `[content, ...trailing actions, ...leading
1181
+ * actions]` (`trailing` and `leading` count them); the actions are `button`s
1182
+ * (a `tint` style colors one, role `destructive` is red). A horizontal pan on
1183
+ * the row reveals 74 pt buttons behind the content; tapping one sends `tap`
1184
+ * with the button's id. A full swipe (allowed by default) past the actions'
1185
+ * width + 60 performs the first declared action of that edge.
1186
+ */
1187
+ export interface SwipeActionsProps {
1188
+ trailing: number;
1189
+ leading: number;
1190
+ fullSwipeTrailing?: boolean;
1191
+ fullSwipeLeading?: boolean;
1192
+ }
1193
+
1194
+ /** `.contextMenu { }`: `[content, ...items]`; a long press or right click lists the items (as in a `menu`) in a popover. */
1195
+ export type ContextMenuProps = Record<string, never>;
1196
+
1197
+ /**
1198
+ * A `List` row that can be selected (`selectable`, `selected`) or deleted
1199
+ * (`deletable`, from `ForEach.onDelete`). A tap on a selectable row sends
1200
+ * `tap` with the row id; swipe-to-delete or the edit-mode minus sends `delete`.
1201
+ */
1202
+ export interface ListRowProps {
1203
+ selectable?: boolean;
1204
+ selected?: boolean;
1205
+ deletable?: boolean;
1206
+ }
1207
+
1208
+ export interface ElementPropsByKind {
1209
+ gesture: GestureProps;
1210
+ swipeactions: SwipeActionsProps;
1211
+ contextmenu: ContextMenuProps;
1212
+ listrow: ListRowProps;
1213
+ }
1214
+
1215
+ export const INTERACTION_KINDS: readonly ElementKind[] = ['gesture', 'swipeactions', 'contextmenu', 'listrow'];
1216
+
1217
+ export interface ListProps {
1218
+ /** `EditButton` / `editMode`: every `listrow` gains a 44 pt leading control and its content shifts right 44. */
1219
+ editing?: boolean;
1220
+ /** `.refreshable`: a pull past 60 pt at the top sends `refresh`. */
1221
+ refreshable?: boolean;
1222
+ /** Swift sets it while the refresh action runs; a 40 pt spinner row shows above the content. */
1223
+ refreshing?: boolean;
1224
+ }
1225
+
1226
+ export interface ScrollViewProps {
1227
+ refreshable?: boolean;
1228
+ refreshing?: boolean;
1229
+ /** Phase 12 `.scrollTargetBehavior`: CSS scroll snapping on the axis. */
1230
+ snap?: 'viewAligned' | 'paging';
1231
+ }
1232
+
1233
+ export interface Style {
1234
+ /** `.allowsHitTesting(false)`: `pointer-events: none` on the subtree. */
1235
+ hitTesting?: boolean;
1236
+ }
1237
+
1238
+ export type DragPhase = 'began' | 'changed' | 'ended' | 'cancelled';
1239
+
1240
+ /**
1241
+ * A `gesture` with `drag`: coalesced to one per animation frame while
1242
+ * `changed`. Coordinates are points relative to the element's box;
1243
+ * `velocity` is in points per second (`ended` only, else 0).
1244
+ */
1245
+ export interface DragEvent {
1246
+ type: 'drag';
1247
+ id: ElementId;
1248
+ phase: DragPhase;
1249
+ translation: { width: number; height: number };
1250
+ location: Point;
1251
+ startLocation: Point;
1252
+ velocity: { width: number; height: number };
1253
+ }
1254
+ /** A `gesture` with `longPress` was held `minimumDuration` without moving 10 px. */
1255
+ export interface LongPressEvent {
1256
+ type: 'longpress';
1257
+ id: ElementId;
1258
+ }
1259
+ /** A `listrow` was swiped to delete or its edit-mode Delete button tapped. */
1260
+ export interface DeleteEvent {
1261
+ type: 'delete';
1262
+ id: ElementId;
1263
+ }
1264
+ /** A `list` / `scrollview` with `refreshable` was pulled past 60 pt. */
1265
+ export interface RefreshEvent {
1266
+ type: 'refresh';
1267
+ id: ElementId;
1268
+ }
1269
+
1270
+ /** The Phase 8 events (merged into `RendererEvent` above). */
1271
+ export type InteractionEvent = DragEvent | LongPressEvent | DeleteEvent | RefreshEvent;
1272
+ // Phase 8: alerts, controls, effects
1273
+ // (docs/ops-protocol.md, "Phase 8: interaction"; gestures, swipe actions,
1274
+ // context menus, list rows and refresh live in their own block)
1275
+ // ---------------------------------------------------------------------------
1276
+
1277
+ export type AlertStyle = 'alert' | 'dialog';
1278
+ /**
1279
+ * A root-level element (child of id 0) placed after the tree, like `sheet`.
1280
+ * The children are the `button`s in order; a `cancel` role is drawn apart.
1281
+ * With no button children the renderer draws an "OK" that sends `tap` with
1282
+ * the alert's own id; so does a tap on a dialog's scrim.
1283
+ */
1284
+ export interface AlertProps {
1285
+ title: string;
1286
+ message: string | null;
1287
+ style: AlertStyle;
1288
+ }
1289
+
1290
+ /** The label (may be empty) is the child; a tap on a half sends `step` ±1. */
1291
+ export interface StepperProps {
1292
+ canIncrement: boolean;
1293
+ canDecrement: boolean;
1294
+ }
1295
+
1296
+ export type SliderLabel = 'label' | 'min' | 'max';
1297
+ /**
1298
+ * The children are `[label?, minimumValueLabel?, maximumValueLabel?]`, those
1299
+ * present in that order, named by `labels`. Dragging sends `slide` with the
1300
+ * value in `min…max` snapped to `step`; `value` is authoritative.
1301
+ */
1302
+ export interface SliderProps {
1303
+ value: number;
1304
+ min: number;
1305
+ max: number;
1306
+ step: number | null;
1307
+ labels?: SliderLabel[];
1308
+ }
1309
+
1310
+ export type DatePickerComponents = 'date' | 'hourAndMinute' | 'dateAndTime';
1311
+ /**
1312
+ * Compact style: the label child leads, one or two capsules trail showing
1313
+ * `seconds` (since 1970, UTC) formatted in US English. A change in the native
1314
+ * picker sends `date` with the combined seconds.
1315
+ */
1316
+ export interface DatePickerProps {
1317
+ seconds: number;
1318
+ components: DatePickerComponents;
1319
+ min: number | null;
1320
+ max: number | null;
1321
+ /** `.labelsHidden()`: the label child is not laid out. */
1322
+ labelsHidden?: boolean;
1323
+ }
1324
+
1325
+ /** A `<textarea>` filling the proposal; input sends `text` like a `textfield`. */
1326
+ export interface TextEditorProps {
1327
+ text: string;
1328
+ }
1329
+
1330
+ /** A `stepper` half was tapped: +1 or −1. */
1331
+ export interface StepEvent {
1332
+ type: 'step';
1333
+ id: ElementId;
1334
+ value: number;
1335
+ }
1336
+ /** A `slider` was dragged or tapped: the value in its range, snapped to `step`. */
1337
+ export interface SlideEvent {
1338
+ type: 'slide';
1339
+ id: ElementId;
1340
+ value: number;
1341
+ }
1342
+ /** A `datepicker` changed: seconds since 1970, UTC. */
1343
+ export interface DateEvent {
1344
+ type: 'date';
1345
+ id: ElementId;
1346
+ value: number;
1347
+ }
1348
+ export type Phase8ControlEvent = StepEvent | SlideEvent | DateEvent | FocusEvent | SubmitEvent;
1349
+
1350
+ /** Phase 12: a bound `textfield` (one with `focused` in its props) gained or lost focus. */
1351
+ export interface FocusEvent {
1352
+ type: 'focus';
1353
+ id: ElementId;
1354
+ value: boolean;
1355
+ }
1356
+
1357
+ /** Phase 12: the Enter key in a `textfield` with `submit`. */
1358
+ export interface SubmitEvent {
1359
+ type: 'submit';
1360
+ id: ElementId;
1361
+ }
1362
+
1363
+ /** Phase 12: `.preferredColorScheme` arrives as an `update` of the root with this; `null` returns to the viewer's setting. */
1364
+ export interface RootProps {
1365
+ colorScheme?: 'light' | 'dark' | null;
1366
+ }
1367
+
1368
+ export interface ElementPropsByKind {
1369
+ alert: AlertProps;
1370
+ stepper: StepperProps;
1371
+ slider: SliderProps;
1372
+ datepicker: DatePickerProps;
1373
+ texteditor: TextEditorProps;
1374
+ }
1375
+
1376
+ export const PHASE8_CONTROL_KINDS: readonly ElementKind[] = ['alert', 'stepper', 'slider', 'datepicker', 'texteditor'];
1377
+
1378
+ /** `.rotation3DEffect(_:axis:anchor:perspective:)`. */
1379
+ export interface Rotation3D {
1380
+ /** Degrees. */
1381
+ angle: number;
1382
+ axis: { x: number; y: number; z: number };
1383
+ /** Unit point the rotation turns around (default 0.5/0.5). */
1384
+ anchor?: UnitPoint;
1385
+ /** SwiftUI's perspective factor (default 1 → a 500px CSS perspective). */
1386
+ perspective?: number;
1387
+ }
1388
+
1389
+ export interface Style {
1390
+ /**
1391
+ * CSS `perspective(500px / perspective) rotate3d(x, y, z, angle)`, composed
1392
+ * after the 2D rotation and before the scale (see styles.ts `effectsCSS`).
1393
+ */
1394
+ rotation3D?: Rotation3D;
1395
+ /** `.blur(radius:)`: `filter: blur(<radius>px)`, combined with `shadow` in one `filter`. */
1396
+ blur?: number;
1397
+ /**
1398
+ * `.ignoresSafeArea(edges:)`: the engine extends the box to the screen
1399
+ * edge on each listed side when the box touches that safe-area edge. The
1400
+ * child's proposal and position are unchanged (the extension is background).
1401
+ */
1402
+ ignoresSafeArea?: 'all' | Edge[];
1403
+ }
1404
+
1405
+ /** `.repeatCount(_:autoreverses:)` / `.repeatForever(autoreverses:)`. */
1406
+ export interface AnimationRepeat {
1407
+ count: number | 'forever';
1408
+ autoreverses: boolean;
1409
+ }
1410
+
1411
+ export interface Animation {
1412
+ /** WAAPI `iterations` (Infinity for `forever`) and `direction: alternate` when autoreversing. */
1413
+ repeat?: AnimationRepeat;
1414
+ }
1415
+
1416
+ /** `active` when visible and focused, `inactive` when visible but unfocused, `background` when hidden. */
1417
+ export type ScenePhase = 'active' | 'inactive' | 'background';
1418
+
1419
+ export interface EnvironmentEvent {
1420
+ scenePhase?: ScenePhase;
1421
+ }