@swiftbrowser/react 0.0.0-stage → 0.5.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 (94) hide show
  1. package/README.md +490 -2
  2. package/dist/binding.d.ts +9 -0
  3. package/dist/binding.d.ts.map +1 -0
  4. package/dist/bridge.d.ts +17 -0
  5. package/dist/bridge.d.ts.map +1 -0
  6. package/dist/components/animators.d.ts +40 -0
  7. package/dist/components/animators.d.ts.map +1 -0
  8. package/dist/components/canvas.d.ts +138 -0
  9. package/dist/components/canvas.d.ts.map +1 -0
  10. package/dist/components/charts.d.ts +100 -0
  11. package/dist/components/charts.d.ts.map +1 -0
  12. package/dist/components/containers.d.ts +78 -0
  13. package/dist/components/containers.d.ts.map +1 -0
  14. package/dist/components/controls.d.ts +106 -0
  15. package/dist/components/controls.d.ts.map +1 -0
  16. package/dist/components/geometry.d.ts +161 -0
  17. package/dist/components/geometry.d.ts.map +1 -0
  18. package/dist/components/host.d.ts +24 -0
  19. package/dist/components/host.d.ts.map +1 -0
  20. package/dist/components/images.d.ts +18 -0
  21. package/dist/components/images.d.ts.map +1 -0
  22. package/dist/components/lists.d.ts +26 -0
  23. package/dist/components/lists.d.ts.map +1 -0
  24. package/dist/components/media.d.ts +105 -0
  25. package/dist/components/media.d.ts.map +1 -0
  26. package/dist/components/menus.d.ts +20 -0
  27. package/dist/components/menus.d.ts.map +1 -0
  28. package/dist/components/navigation.d.ts +22 -0
  29. package/dist/components/navigation.d.ts.map +1 -0
  30. package/dist/components/row.d.ts +34 -0
  31. package/dist/components/row.d.ts.map +1 -0
  32. package/dist/components/shapes.d.ts +107 -0
  33. package/dist/components/shapes.d.ts.map +1 -0
  34. package/dist/components/split.d.ts +25 -0
  35. package/dist/components/split.d.ts.map +1 -0
  36. package/dist/components/stacks.d.ts +159 -0
  37. package/dist/components/stacks.d.ts.map +1 -0
  38. package/dist/components/system.d.ts +25 -0
  39. package/dist/components/system.d.ts.map +1 -0
  40. package/dist/components/table.d.ts +36 -0
  41. package/dist/components/table.d.ts.map +1 -0
  42. package/dist/components/tabs.d.ts +20 -0
  43. package/dist/components/tabs.d.ts.map +1 -0
  44. package/dist/components/text.d.ts +63 -0
  45. package/dist/components/text.d.ts.map +1 -0
  46. package/dist/coordinateSpace.d.ts +20 -0
  47. package/dist/coordinateSpace.d.ts.map +1 -0
  48. package/dist/dragDrop.d.ts +16 -0
  49. package/dist/dragDrop.d.ts.map +1 -0
  50. package/dist/environment.d.ts +54 -0
  51. package/dist/environment.d.ts.map +1 -0
  52. package/dist/host/instance.d.ts +67 -0
  53. package/dist/host/instance.d.ts.map +1 -0
  54. package/dist/host/reconciler.d.ts +9 -0
  55. package/dist/host/reconciler.d.ts.map +1 -0
  56. package/dist/host/root.d.ts +29 -0
  57. package/dist/host/root.d.ts.map +1 -0
  58. package/dist/host/sb.d.ts +5 -0
  59. package/dist/host/sb.d.ts.map +1 -0
  60. package/dist/host/tree.d.ts +35 -0
  61. package/dist/host/tree.d.ts.map +1 -0
  62. package/dist/index.d.ts +43 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +4379 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/inspector.d.ts +11 -0
  67. package/dist/inspector.d.ts.map +1 -0
  68. package/dist/island.d.ts +25 -0
  69. package/dist/island.d.ts.map +1 -0
  70. package/dist/lazy.d.ts +19 -0
  71. package/dist/lazy.d.ts.map +1 -0
  72. package/dist/lifecycle.d.ts +11 -0
  73. package/dist/lifecycle.d.ts.map +1 -0
  74. package/dist/modifiers.d.ts +379 -0
  75. package/dist/modifiers.d.ts.map +1 -0
  76. package/dist/mount.d.ts +41 -0
  77. package/dist/mount.d.ts.map +1 -0
  78. package/dist/navigation-context.d.ts +15 -0
  79. package/dist/navigation-context.d.ts.map +1 -0
  80. package/dist/path.d.ts +48 -0
  81. package/dist/path.d.ts.map +1 -0
  82. package/dist/pointer.d.ts +65 -0
  83. package/dist/pointer.d.ts.map +1 -0
  84. package/dist/presentation.d.ts +38 -0
  85. package/dist/presentation.d.ts.map +1 -0
  86. package/dist/scrollEnvironment.d.ts +50 -0
  87. package/dist/scrollEnvironment.d.ts.map +1 -0
  88. package/dist/scrollReader.d.ts +27 -0
  89. package/dist/scrollReader.d.ts.map +1 -0
  90. package/dist/values.d.ts +118 -0
  91. package/dist/values.d.ts.map +1 -0
  92. package/dist/visualEffect.d.ts +120 -0
  93. package/dist/visualEffect.d.ts.map +1 -0
  94. package/package.json +48 -4
package/README.md CHANGED
@@ -1,3 +1,491 @@
1
- # Temporary Holding Version
1
+ # @swiftbrowser/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ SwiftUI's components for React. A pure React/TypeScript web app writes its
4
+ screens with `List`, `NavigationStack`, `Toggle`, `Picker` and the rest, and
5
+ gets the same iOS UI a SwiftUI app gets in SwiftBrowser: the components emit
6
+ the [render ops](../../docs/ops-protocol.md) the Swift shim emits, and the
7
+ [web renderer](../../Web/README.md) draws them with its iOS layout engine,
8
+ typography, animations, navigation bars, tab bars, sheets and alerts. No
9
+ Swift, no WebAssembly: the package is React components and a renderer host.
10
+
11
+ It works in both directions with ordinary React, and the two nest: a
12
+ SwiftUI tree can live inside a `react-dom` page, and ordinary React DOM can
13
+ live inside a SwiftUI element (see [Mixing with ordinary React](#mixing-with-ordinary-react)).
14
+
15
+ The package is experimental.
16
+
17
+ ```sh
18
+ npm install @swiftbrowser/react @swiftbrowser/web react react-dom react-reconciler
19
+ ```
20
+
21
+ `react-reconciler` is a peer dependency, so the app picks the release that
22
+ matches its React: 0.31 for React 19.0, 0.32 for 19.1, 0.33 for 19.2, 0.34
23
+ for 19.3 (`npm install react-reconciler@0.33` on React 19.2, say); the
24
+ package works with all four.
25
+
26
+ It is built for a bundler (Vite, or any that compiles TypeScript in
27
+ dependencies): the renderer it drives comes from `@swiftbrowser/web`'s
28
+ TypeScript sources, as does the stylesheet the page imports. The stylesheet
29
+ is the renderer's alone (the device's tokens, the frame, the elements): it
30
+ leaves the page's own `body`, classes and box model as they are, and an
31
+ island is as wide and as tall as the page's CSS makes it. With Vite, list
32
+ `@swiftbrowser/web` in `optimizeDeps.exclude` so the renderer's bitmap
33
+ worker resolves from its sources.
34
+
35
+ ## Getting started
36
+
37
+ ### A whole page
38
+
39
+ The simplest app is a page whose root is a SwiftUI tree. The page provides
40
+ the renderer's stylesheet and a root element, and `mount` draws the tree in
41
+ it. In the device frame (the markup of `index.html` here, with the iPhone
42
+ bezel, status bar and the theme controls) the root is the `#screen` element;
43
+ on a phone the renderer can also draw full screen.
44
+
45
+ ```tsx
46
+ import '@swiftbrowser/web/src/styles.css';
47
+ import { useState } from 'react';
48
+ import { mount, Label, List, NavigationLink, NavigationStack, Section, Text, Toggle } from '@swiftbrowser/react';
49
+
50
+ function Library() {
51
+ const [downloads, setDownloads] = useState(true);
52
+ return (
53
+ <List navigationTitle="Library">
54
+ <Section header="Reading">
55
+ <NavigationLink destination={<Text>Detail</Text>}>
56
+ <Text>The Swift Programming Language</Text>
57
+ </NavigationLink>
58
+ </Section>
59
+ <Section header="Settings">
60
+ <Toggle isOn={[downloads, setDownloads]}>
61
+ <Label title="Automatic Downloads" systemImage="arrow.down.circle" />
62
+ </Toggle>
63
+ </Section>
64
+ </List>
65
+ );
66
+ }
67
+
68
+ mount(document.getElementById('screen')!, <NavigationStack><Library /></NavigationStack>);
69
+ ```
70
+
71
+ `mount` returns the root and the renderer; `setEnvironment({ colorScheme,
72
+ dynamicTypeSize })` on it switches dark mode and the Dynamic Type size, as
73
+ the demo page's controls do.
74
+
75
+ ### Inside an existing React app
76
+
77
+ In a `react-dom` app, `SwiftUIView` is a normal component that draws its
78
+ children with the renderer in a box of the page's own layout, with the
79
+ device's design tokens and none of its frame:
80
+
81
+ ```tsx
82
+ import '@swiftbrowser/web/src/styles.css';
83
+ import { SwiftUIView, List, Section, Toggle } from '@swiftbrowser/react';
84
+
85
+ function Settings({ dark, setDark }) {
86
+ return (
87
+ <SwiftUIView fit="content" colorScheme={dark ? 'dark' : 'light'} className="settings">
88
+ <List>
89
+ <Section header="Appearance">
90
+ <Toggle title="Dark Mode" isOn={[dark, setDark]} />
91
+ </Section>
92
+ </List>
93
+ </SwiftUIView>
94
+ );
95
+ }
96
+ ```
97
+
98
+ `fit="content"` makes the island as tall as its content, like a block in
99
+ the page's flow; `fit="fill"` (the default) draws in the box the page sizes
100
+ with CSS, which is what navigation stacks and tab views want, since they fill
101
+ their screen. `colorScheme` and `dynamicTypeSize` are props. The island is
102
+ a React root of its own: the page's React state reaches it through props as
103
+ usual, and its contexts through `contexts={[…]}` (see
104
+ [Contexts across roots](#contexts-across-roots)).
105
+
106
+ ### Try the demos
107
+
108
+ ```sh
109
+ npm install
110
+ cd packages/react
111
+ npm run dev # http://localhost:5174/ — Examples/Fidelity, in React
112
+ # http://localhost:5174/island.html — islands in a react-dom page
113
+ # http://localhost:5174/wide.html — a regular-width app: split view, table, inspector
114
+ npm run test:unit # the React Fidelity tabs emit the same tree as the Swift app
115
+ ```
116
+
117
+ `?screen=<name>` on the first page opens one of the Fidelity screens alone
118
+ (`list`, `detail`, `form`, `controls`, `typography`, `shapes`, `layout`,
119
+ `tabs`, `sheet`, `alert`, `search`, `post`, `settings`, `reorder`), like the
120
+ Swift app's `-screen` argument; `extras`, `more` and `web` show the
121
+ components beyond it.
122
+
123
+ ## Writing views
124
+
125
+ ### Components
126
+
127
+ The components are named as in SwiftUI and take the same arguments as
128
+ props: `<VStack alignment="leading" spacing={8}>`, `<Text font="headline">`,
129
+ `<Image systemName="heart.fill" />`, `<Button title="Save" action={save} />`,
130
+ `<Picker label="Plan" selection={[plan, setPlan]} options={plans} />`,
131
+ `<Section header="Reading" footer="…">`. Children are the content closure.
132
+ `ForEach` takes `data` and a render function, with `id`, `onDelete` and
133
+ `onMove` as in SwiftUI; `Group` applies its modifiers to each child.
134
+
135
+ Text is plain strings and numbers inside `<Text>`; nested `<Text>`s and
136
+ inline `<Image>`s make a rich text (`Text + Text`), and `italic`,
137
+ `underline`, `strikethrough` and `tracking` are its own props.
138
+
139
+ ### Modifiers as props
140
+
141
+ Every component takes SwiftUI's modifiers as props, and applies them **in
142
+ the order they are written, innermost first**, so a chain reads the same:
143
+
144
+ ```tsx
145
+ <Image
146
+ systemName="swift"
147
+ font={system(17, 'semibold')}
148
+ foregroundStyle="white"
149
+ frame={{ width: 32, height: 32 }}
150
+ background={{ fill: 'orange', in: <RoundedRectangle cornerRadius={8} /> }}
151
+ />
152
+ ```
153
+
154
+ is `Image(systemName: "swift").font(.system(size: 17, weight: .semibold))
155
+ .foregroundStyle(.white).frame(width: 32, height: 32).background(.orange, in:
156
+ RoundedRectangle(cornerRadius: 8))`. Each modifier wraps the view in the same
157
+ `styled` element the Swift shim emits, so the renderer lays it out
158
+ identically. A modifier that must apply twice goes in `modifiers={[…]}`, an
159
+ ordered list. The environment modifiers (`buttonStyle`, `controlSize`,
160
+ `pickerStyle`, `labelsHidden`) are React contexts the views below read, and
161
+ `navigationTitle`, `toolbar`, `searchable`, `sheet`, `alert`,
162
+ `presentationDetents` and `preferredColorScheme` attach to the enclosing
163
+ screen, sheet or page, as the SwiftUI preferences do. Effects (`blur`,
164
+ `grayscale`, `saturation`, `brightness`, `contrast`, `hueRotation`,
165
+ `colorInvert`, `colorMultiply`, `blendMode`, `mask`, `rotation3DEffect`),
166
+ layout hints (`aspectRatio`, `scaledToFit`, `position`, `zIndex`,
167
+ `alignmentGuide`, `ignoresSafeArea`, `allowsHitTesting`, `lineLimit` ranges)
168
+ and the lifecycle (`onAppear`, `task` with an `AbortSignal` for
169
+ cancellation, `onChangeOf`) are modifiers too, as are `draggable` and
170
+ `dropDestination`.
171
+
172
+ Values are plain data: colors by name (`"red"`, `"secondary"`,
173
+ `"secondarySystemBackground"`) or through `Color` (`Color.opacity('black',
174
+ 0.2)`, `Color.gradient('teal')`, `Color.hex('#336699')`); fonts by text
175
+ style (`"title2"`) or `{ textStyle: 'title2', weight: 'bold' }` and
176
+ `system(size, weight, design)`; frames as `{ width, height, maxWidth:
177
+ Infinity, alignment }`; padding as `true`, a number, `['vertical', 6]` or
178
+ insets; gradients through `LinearGradient`, `RadialGradient` and
179
+ `AngularGradient`.
180
+
181
+ ### Bindings
182
+
183
+ A binding is a `useState` pair, so `isOn={[downloads, setDownloads]}` reads
184
+ like `isOn: $downloads`. Everything that takes one (`Toggle`, `Picker`,
185
+ `Slider`, `Stepper`, `TextField`, `TextEditor`, `DatePicker`, `TabView`'s
186
+ `selection`, `List`'s `selection` and `editing`, `NavigationStack`'s `path`,
187
+ the `sheet` and `alert` modifiers' `isPresented`) also takes a plain value
188
+ with an `onChange` handler. The setter can be anything with that signature.
189
+
190
+ When the renderer sends an event (a tap, a switch, a selection), the
191
+ handler's state updates render synchronously and the renderer gets the next
192
+ pass inside the event, the way the Swift core answers one.
193
+
194
+ ### Navigation and presentation
195
+
196
+ `NavigationStack` holds the pushed values (or takes a `path` binding).
197
+ `NavigationLink` pushes a `value`, which the enclosing screen's
198
+ `navigationDestination` builds a screen for, or a `destination` view
199
+ directly. `TabView` holds `Tab`s. `.sheet` and `.alert` are modifiers whose
200
+ content and actions render as root-level elements while presented;
201
+ `presentationDetents` inside a sheet sets its detents. `useDismiss()` inside
202
+ a pushed screen or a sheet pops or dismisses it.
203
+
204
+ ### Lists
205
+
206
+ `List` and `Form` hold `Section`s and rows. `selection={[…]}` makes rows with
207
+ a `tag` (or a `NavigationLink` value) selectable, one value or a `Set`;
208
+ `editing` is the edit mode, which `EditButton` flips. `ForEach`'s `onDelete`
209
+ makes its rows deletable (a swipe, or edit mode's minus) and `onMove` makes
210
+ them reorder with a grip, with `moveItems` to apply the move to an array.
211
+ `swipeActions` and `contextMenu` are modifiers on a row.
212
+
213
+ ### Size classes
214
+
215
+ `useHorizontalSizeClass()` is `compact` below 700 points of width (a phone)
216
+ and `regular` from it (a tablet, a desktop window); `mount` and
217
+ `SwiftUIView` follow the root's width as it changes. The views that adapt do
218
+ it as on iOS: `NavigationSplitView` is two columns when regular (the
219
+ sidebar's toggle button hides and shows the sidebar) and one stack when
220
+ compact, which pushes the detail while the sidebar's `List(selection:)` has
221
+ a selection and clears it on back; `.inspector` is a trailing panel when
222
+ regular and a sheet when compact; `Table` shows every column under sortable
223
+ titles when regular and the first column when compact.
224
+
225
+ ```tsx
226
+ <NavigationSplitView
227
+ sidebar={<List selection={[team, setTeam]}>{/* rows with a tag */}</List>}
228
+ detail={<Table data={people} columns={columns} sortOrder={[order, setOrder]} />}
229
+ />
230
+ ```
231
+
232
+ ### Scrolling
233
+
234
+ `List`, `LazyVStack` and `LazyHStack` build a long `ForEach` a batch of
235
+ rows at a time as they scroll into view. `ScrollViewReader` hands a proxy
236
+ whose `scrollTo(id, anchor)` scrolls to the view marked `scrollID={id}`,
237
+ building lazy rows up to it first. `refreshable` (on a `List` or
238
+ `ScrollView`, or above one) runs an async action on a pull, with the
239
+ spinner while it runs. `scrollTargetBehavior="paging"` or `"viewAligned"` on
240
+ a `ScrollView` snaps, with `scrollTargetLayout` on the stack inside.
241
+ Name a coordinate space with `coordinateSpace="page"` on the `ScrollView`
242
+ (or any view), and a `GeometryReader` inside reads its frame there with
243
+ `proxy.frame({ named: 'page' })`, updated as the page scrolls.
244
+
245
+ `scrollTransition` draws effects as a view leaves the visible area of its
246
+ scroll view and comes back. The closure gets the effect to build on and the
247
+ phase (`topLeading`, `identity` or `bottomTrailing`, with `value` -1, 0 or
248
+ 1). By default the effect follows the scrolling, drawn partway while the
249
+ view is partly out; `configuration: 'animated'` switches phases with an
250
+ animation instead. `containerRelativeFrame` sizes a view against the scroll
251
+ view's visible area, so a carousel of cards is:
252
+
253
+ ```tsx
254
+ <ScrollView axes="horizontal" contentMargins={['horizontal', 20]} scrollEdgeEffectStyle="hard">
255
+ <HStack spacing={12}>
256
+ {cards.map((card) => (
257
+ <CardView
258
+ key={card.id}
259
+ card={card}
260
+ containerRelativeFrame={{ axes: 'horizontal', count: 5, span: 2, spacing: 12 }}
261
+ scrollTransition={(effect, phase) => effect.scaleEffect(phase.isIdentity ? 1 : 0.85).opacity(phase.isIdentity ? 1 : 0.4)}
262
+ />
263
+ ))}
264
+ </HStack>
265
+ </ScrollView>
266
+ ```
267
+
268
+ `contentMargins` pads the content inside the scroll views below (it scrolls
269
+ under the margin). `scrollEdgeEffectStyle` (`hard`, `soft`, `automatic`, or
270
+ `["hard", "top"]` for one edge) and `scrollEdgeEffectHidden` set the iOS 26
271
+ edge effect where a list or scroll view runs under a bar. All three apply to
272
+ the scroll view they are written on and pass down to the ones inside, as
273
+ SwiftUI's environment does. `LazyHGrid` lays its cells into `rows` column by
274
+ column, for a horizontal `ScrollView`.
275
+
276
+ ### Drawing and media
277
+
278
+ `Shape` takes a `path` function of the rectangle it is laid out in and
279
+ draws a `Path` (`move`, `addLine`, `addCurve`, `addArc`, `addRoundedRect`…);
280
+ `UnevenRoundedRectangle` and `ContainerRelativeShape` are built on it.
281
+ `Canvas` hands its renderer a `GraphicsContext` (fill, stroke, text, images,
282
+ transforms, opacity, clips, filters, gradients in its own coordinates), and
283
+ `TimelineView` re-renders on a schedule, so a clock is a `TimelineView`
284
+ around a `Canvas`. Views passed in the `symbols` prop (`{ star: <Image
285
+ systemName="star.fill" /> }`) come back from `resolveSymbol` and draw with
286
+ `drawSymbol` at a point, under the context's transform and opacity; they are
287
+ real views laid over the canvas, so they draw above its other commands. `useAVPlayer(url)` is an `AVPlayer` (play, pause, seek,
288
+ mute, volume, rate) that `VideoPlayer` shows in the page's `<video>`; the
289
+ position is read or observed without re-rendering.
290
+
291
+ ### Mouse, keyboard and effects
292
+
293
+ A web app may have a mouse and a keyboard, so the modifiers SwiftUI keeps
294
+ for iPad and Mac work here: `onHover` (a mouse entering and leaving the
295
+ view's frame), `help` (a tooltip), `pointerStyle` (`link`, `text`,
296
+ `grabIdle`… or any CSS cursor), `keyboardShortcut` on a `Button` (`⌘` is
297
+ Ctrl off Apple platforms) and `onKeyPress` (keys pressed while no text field
298
+ has the focus). `symbolEffect` draws SF Symbols' effects (`bounce`, `pulse`,
299
+ `variableColor`, `rotate`, `wiggle`, `breathe`, `scale`, `appear`), running
300
+ while active or once per change of a value. `visualEffect={(effect, proxy) =>
301
+ …}` draws effects (offset, scale, rotation, opacity, blur, color
302
+ adjustments) from the view's geometry without changing its layout; the
303
+ proxy's `frame('global')` and `bounds('scrollView')` stay current as the page
304
+ scrolls, so a parallax or a fade by position is one closure. `matchedGeometryEffect` with a
305
+ namespace from `useNamespace()` moves a view into the frame of the one it
306
+ replaces in an animated change. `contentTransition="numericText"` rolls a
307
+ text's old value out and the new one in when it changes in an animated pass,
308
+ upward or downward as the number grows or shrinks (`{ numericText: {
309
+ countsDown } }` fixes the direction); the whole text rolls, not each digit.
310
+ `opacity` and `interpolate` crossfade. `sensoryFeedback` vibrates where the
311
+ device can.
312
+
313
+ ### Animation
314
+
315
+ `withAnimation(animation, () => …)` animates the pass a state change makes,
316
+ and the `animation` modifier a view's changes when a value changes.
317
+ `PhaseAnimator` moves its content through phases, each change animated;
318
+ `KeyframeAnimator` plays tracks of keyframes (`linear`, `cubic`, `spring`,
319
+ `move`) and renders its content with the values each frame.
320
+
321
+ ### Charts
322
+
323
+ `Chart` takes `BarMark`, `LineMark`, `AreaMark`, `PointMark` and `RuleMark`
324
+ with their values in `x` and `y`: categories, numbers or dates. The
325
+ component builds the scales and ticks the Swift Charts module builds
326
+ (bands, nice numeric domains that include zero, calendar strides), groups
327
+ lines and areas by series, colors `foregroundStyleBy` series from the chart
328
+ palette with a legend, and places `annotation`s.
329
+
330
+ ## Mixing with ordinary React
331
+
332
+ Both directions work, and they nest: a `react-dom` page can hold a SwiftUI
333
+ island, and the island can hold ordinary React DOM inside one of its rows.
334
+ The island page of the demo (`island.html`) does exactly that.
335
+
336
+ ### A SwiftUI tree inside a react-dom app
337
+
338
+ `SwiftUIView`, shown in [Getting started](#inside-an-existing-react-app):
339
+ a component whose children are a SwiftUI tree, drawn in a box of the page.
340
+
341
+ ### React DOM inside a SwiftUI element
342
+
343
+ `Host` puts ordinary React DOM, with its own components, stylesheets and
344
+ event handlers, inside a SwiftUI tree. Its children render with `react-dom`
345
+ in a `host` element the layout engine sizes, so the DOM sits inside list
346
+ rows, stacks and screens like any other view:
347
+
348
+ ```tsx
349
+ function Composer() {
350
+ const [notes, setNotes] = useState('');
351
+ return (
352
+ <List>
353
+ <Section header="Appearance">
354
+ <LabeledContent label="Notes" value={`${notes.length} characters`} />
355
+ </Section>
356
+ <Section header="Composer" footer="A textarea and a counter, rendered by react-dom inside the list row.">
357
+ <Host className="composer">
358
+ <textarea value={notes} placeholder="Write something…" onChange={(e) => setNotes(e.target.value)} />
359
+ <div className="count">{280 - notes.length}</div>
360
+ </Host>
361
+ </Section>
362
+ <Section header="Map">
363
+ <Host sizing="fill" frame={{ height: 160 }}>
364
+ <MapView />
365
+ </Host>
366
+ </Section>
367
+ </List>
368
+ );
369
+ }
370
+ ```
371
+
372
+ Typing in the textarea updates `notes` in the React component above, the
373
+ SwiftUI row shows the count, and the row grows with the text.
374
+
375
+ - **Sizing.** `sizing="content"` (the default) measures the DOM like text:
376
+ as wide as the content wants, up to the width the parent proposes, and as
377
+ tall as it is at that width. Whenever the content changes size (more
378
+ lines, an image that loaded, a state change), the tree is laid out again
379
+ around it. `sizing="fill"` takes the proposal, so a `frame` (or the
380
+ parent's size) decides the box and the content fills it; `height: 100%`
381
+ in the content reaches the box.
382
+ - **Styling.** The content inherits the device's font and text color, and
383
+ the iOS tokens are CSS custom properties it can use:
384
+ `var(--sb-color-secondary)`, `var(--sb-color-accent)`,
385
+ `var(--sb-color-separator)`, the Dynamic Type sizes, and so on; they
386
+ follow dark mode. `className` and `style` land on the wrapper the
387
+ children render in.
388
+ - **Events.** The DOM handles its own events. Pointer downs inside a host
389
+ never start the renderer's gestures (no swipe, no drag), so text
390
+ selection, scrolling inside the host and native controls work; the row
391
+ around it still swipes when the pan starts outside the host.
392
+ - **Hooks.** The content can use this package's hooks: `useDismiss()` from a
393
+ sheet or a pushed screen, `useNavigation()` to push, `useColorScheme()`.
394
+
395
+ A host is a React root of its own, so it needs the `react-dom` peer
396
+ dependency, and React context does not cross into it by itself; see below.
397
+ It survives `StrictMode`'s simulated unmount and remount, as does
398
+ `SwiftUIView` (`island.html?strict` in the demos runs under it).
399
+
400
+ ### Contexts across roots
401
+
402
+ `SwiftUIView` and `Host` each start a React root, and React context stops at
403
+ a root boundary. This package's own contexts (the environment, navigation,
404
+ dismiss, the view environment) are bridged automatically. For the page's
405
+ own contexts, name them on the component and they are read on the outer
406
+ side and provided again inside:
407
+
408
+ ```tsx
409
+ <SwiftUIView contexts={[ThemeContext, RouterContext]}>…</SwiftUIView>
410
+ <Host contexts={[ThemeContext]}>…</Host>
411
+ ```
412
+
413
+ The list must be the same on every render of that component (each entry is
414
+ read with a hook). Props, callbacks and external stores need no bridging.
415
+
416
+ ## How it works
417
+
418
+ - `src/host/`: a [react-reconciler](https://www.npmjs.com/package/react-reconciler)
419
+ host whose instances are protocol elements. After each React commit,
420
+ `HostTree.flush` resolves two virtual kinds (a *preference* merges props
421
+ into the enclosing `navscreen` or `sheet`; a *hoist* moves children to the
422
+ enclosing screen or to the root, as `.toolbar`, `.sheet` and `.alert` do),
423
+ diffs the result against the last pass and emits one render pass: creates,
424
+ updates, inserts (parents before children), removes, `commit`. Events from
425
+ the renderer reach the element's handlers and flush synchronously.
426
+ - `createSwiftUIRoot(sink)` renders a tree into any ops sink; `mount(screen,
427
+ element)` pairs a root with a `Renderer` from `@swiftbrowser/web/src/renderer`.
428
+ `withAnimation(() => setCount(1))` stamps the pass's `commit` with the
429
+ animation, as `withAnimation { }` does.
430
+ - `src/modifiers.tsx` applies the modifier props; `src/components/` holds the
431
+ components; `src/components/host.tsx` and `src/island.tsx` the two mixing
432
+ components, over the renderer's `host` element kind and its island layout
433
+ mode ([docs/ops-protocol.md](../../docs/ops-protocol.md#host-elements-the-pages-own-dom)).
434
+ - `test/fidelity.test.tsx` renders `Examples/Fidelity`'s tab view through the
435
+ host and compares the element tree with `Examples/Fidelity/__snapshots__/fidelity.jsonl`,
436
+ the ops the Swift app emits for the same screens; `test/extras.test.tsx`
437
+ covers the components beyond the Swift app.
438
+
439
+ ## What is covered
440
+
441
+ - **Layout**: stacks, `LazyVStack`, `LazyHStack`, `Spacer`, `Divider`,
442
+ `ScrollView` (with `contentMargins` and scroll edge effects),
443
+ `ScrollViewReader`, `Group`, `ForEach` (with `onDelete` and `onMove`),
444
+ `Grid`, `LazyVGrid`, `LazyHGrid`, `ViewThatFits`, `GeometryReader` (its
445
+ size, its frame on screen or in a `coordinateSpace` named on an ancestor,
446
+ the visible bounds of its scroll view, and its safe-area insets), custom
447
+ `Layout` (with `layoutValue`s, priorities, view spacing, explicit
448
+ alignment guides and a cache), `containerRelativeFrame`, `safeAreaInset`.
449
+ - **Content**: `Text` (with `Text + Text` runs, inline images, italic,
450
+ underline, strikethrough, tracking), `Image` (symbols, catalog images
451
+ with the `assets` option, remote images and `AsyncImage`), `Label` and its
452
+ styles, `LabeledContent`, `Link`, `Color` as a view, shapes (built-in,
453
+ custom paths, `UnevenRoundedRectangle`, `ContainerRelativeShape`) with
454
+ fills, strokes and gradients, `Canvas`, `TimelineView`, `VideoPlayer`,
455
+ `Gauge`, `ProgressView`, Swift Charts.
456
+ - **Controls**: `Button` (styles, roles, sizes), `Toggle`, `Picker`,
457
+ `Slider`, `Stepper`, `TextField` and `SecureField` (focus bindings, submit
458
+ labels), `TextEditor`, `DatePicker`, `ColorPicker`, `ShareLink`, `Menu`,
459
+ `EditButton`.
460
+ - **Containers**: `List` (selection, edit mode, swipe actions, deletion,
461
+ reordering, pull to refresh), `Form`, `Section`, `Table`, `GroupBox`,
462
+ `DisclosureGroup`, `ControlGroup`, `ContentUnavailableView`.
463
+ - **Navigation and presentation**: `NavigationStack` (value and destination
464
+ links, a `path` binding, nested stacks flattened), `NavigationSplitView`,
465
+ `TabView`/`Tab`, toolbars, search fields, sheets with detents, popovers
466
+ (as sheets), alerts and dialogs, inspectors.
467
+ - **Interaction**: `onTapGesture`, `dragGesture`, `onLongPressGesture`,
468
+ `contextMenu`, `draggable` and `dropDestination` (in the app, and text or
469
+ files from outside the page), `onHover`, `help`, `pointerStyle`,
470
+ `keyboardShortcut`, `onKeyPress`, `sensoryFeedback`.
471
+ - **Animation and effects**: `withAnimation`, `animation`, transitions,
472
+ `PhaseAnimator`, `KeyframeAnimator`, `matchedGeometryEffect`,
473
+ `symbolEffect`, `contentTransition`, `visualEffect`, `scrollTransition`,
474
+ `redacted` placeholders, `textCase`.
475
+ - **Mixing**: `Host` and `SwiftUIView`.
476
+
477
+ ## Not yet
478
+
479
+ - `contentTransition="numericText"` rolls the whole text rather than only
480
+ the digits that changed, and `Canvas` symbols always draw above the
481
+ canvas's other commands.
482
+ - `scrollTransition` takes no thresholds: the effect starts as soon as any
483
+ of the view is out of the visible area and is complete when all of it is.
484
+ `containerRelativeFrame` measures against the scroll view's whole visible
485
+ area, content margins included, and its `length` closure is read as a
486
+ linear function of the container's length.
487
+ - The Swift-only bridges (SwiftData, URLSession, notifications, Core ML,
488
+ StoreKit) have no React counterpart, by design: a web app uses the
489
+ platform's own.
490
+ - The ops protocol is the contract with the renderer and is versioned with it:
491
+ this package imports the renderer's sources from `@swiftbrowser/web/src/*`.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A `Binding<T>`: a `useState` pair, so `isOn={[downloads, setDownloads]}`
3
+ * reads like `isOn: $downloads`. A plain value with an `onChange` handler is
4
+ * accepted everywhere a binding is.
5
+ */
6
+ export type Binding<T> = readonly [T, (value: T) => void];
7
+ export type BindingValue<T> = Binding<T> | T;
8
+ export declare function readBinding<T>(value: BindingValue<T>, onChange?: (value: T) => void): [T, (next: T) => void];
9
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC;AAE1D,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AAE7C,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC,CAM5G"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * React context does not cross a root boundary, and a `Host` (a react-dom
3
+ * root inside the SwiftUI tree) or a `SwiftUIView` (a SwiftUI root inside a
4
+ * react-dom tree) is one. The bridge reads the contexts on the outer side and
5
+ * provides them again on the inner side. This package's own contexts always
6
+ * travel; a component names any others it needs.
7
+ */
8
+ import { type Context, type ReactNode } from 'react';
9
+ /**
10
+ * Returns a function that wraps `children` in providers of the current
11
+ * values of this package's contexts and of `contexts`. The list of contexts
12
+ * must not change between renders (each is read with a hook).
13
+ */
14
+ export declare function useContextBridge(contexts?: readonly Context<unknown>[], options?: {
15
+ environment?: boolean;
16
+ }): (children: ReactNode) => ReactNode;
17
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAA6B,KAAK,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAWhF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,GAAE,SAAS,OAAO,CAAC,OAAO,CAAC,EAAO,EAC1C,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,CAAC,QAAQ,EAAE,SAAS,KAAK,SAAS,CASpC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `PhaseAnimator` and `KeyframeAnimator`. A phase animator moves its content
3
+ * through phases, each change in one animated pass that the renderer tweens
4
+ * (as the shim does); a keyframe animator computes its values frame by frame
5
+ * along tracks of keyframes and renders them, as SwiftUI evaluates its
6
+ * content per frame.
7
+ */
8
+ import type { Animation } from '@swiftbrowser/web/src/protocol';
9
+ import { type ReactNode } from 'react';
10
+ export interface PhaseAnimatorProps<Phase> {
11
+ phases: readonly Phase[];
12
+ /** Each change runs through the phases once, back to the first; without one the phases cycle while shown. */
13
+ trigger?: unknown;
14
+ /** The animation into a phase (`default` by default). */
15
+ animation?: (phase: Phase) => Animation;
16
+ children: (phase: Phase) => ReactNode;
17
+ }
18
+ export declare function PhaseAnimator<Phase>({ phases, trigger, animation, children }: PhaseAnimatorProps<Phase>): ReactNode;
19
+ export type KeyframeCurve = 'linear' | 'cubic' | 'spring' | 'move';
20
+ /** One keyframe of a track: the value it reaches, in how long (seconds), along which curve. */
21
+ export interface Keyframe {
22
+ to: number;
23
+ duration: number;
24
+ curve?: KeyframeCurve;
25
+ }
26
+ export type KeyframeTracks<Value extends Record<string, number>> = {
27
+ [K in keyof Value]?: readonly Keyframe[];
28
+ };
29
+ export interface KeyframeAnimatorProps<Value extends Record<string, number>> {
30
+ initialValue: Value;
31
+ keyframes: KeyframeTracks<Value>;
32
+ /** Each change plays the keyframes once; without one they repeat while shown (`repeating`). */
33
+ trigger?: unknown;
34
+ repeating?: boolean;
35
+ children: (value: Value) => ReactNode;
36
+ }
37
+ /** The tracks' values `elapsed` seconds in (each track from the initial value through its keyframes). */
38
+ export declare function keyframeValues<Value extends Record<string, number>>(initial: Value, tracks: KeyframeTracks<Value>, elapsed: number): Value;
39
+ export declare function KeyframeAnimator<Value extends Record<string, number>>({ initialValue, keyframes, trigger, repeating, children, }: KeyframeAnimatorProps<Value>): ReactNode;
40
+ //# sourceMappingURL=animators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animators.d.ts","sourceRoot":"","sources":["../../src/components/animators.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAOpE,MAAM,WAAW,kBAAkB,CAAC,KAAK;IACvC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,6GAA6G;IAC7G,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC;IACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC;CACvC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,kBAAkB,CAAC,KAAK,CAAC,GAAG,SAAS,CAuCnH;AAMD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEnE,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,KAAK,CAAC,CAAC,EAAE,SAAS,QAAQ,EAAE;CAAE,CAAC;AAEhH,MAAM,WAAW,qBAAqB,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IACzE,YAAY,EAAE,KAAK,CAAC;IACpB,SAAS,EAAE,cAAc,CAAC,KAAK,CAAC,CAAC;IACjC,+FAA+F;IAC/F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC;CACvC;AAkBD,yGAAyG;AACzG,wBAAgB,cAAc,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,KAAK,CAsB1I;AAMD,wBAAgB,gBAAgB,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EACrE,YAAY,EACZ,SAAS,EACT,OAAO,EACP,SAAS,EACT,QAAQ,GACT,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,SAAS,CA+B1C"}