@swiftbrowser/web 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app/index.html CHANGED
@@ -40,6 +40,9 @@
40
40
  var coarse = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
41
41
  var device = mode === 'device' || (mode !== 'frame' && (coarse || site.pageMode === 'device'));
42
42
  document.documentElement.setAttribute('data-sb-mode', device ? 'device' : 'frame');
43
+ // ?embed=1: the page is in another's frame (the playground's
44
+ // simulator), which has its own controls: the device alone.
45
+ if (new URLSearchParams(location.search).get('embed') === '1') document.documentElement.setAttribute('data-sb-embed', '');
43
46
  })();
44
47
  // Point the manifest, the home-screen icon and the titles at the app
45
48
  // this page runs (?app=, else the site's default), so Share -> Add to
@@ -2,7 +2,7 @@
2
2
  * The playground page (playground/index.html): Swift compiled in the browser
3
3
  * by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
4
4
  * Only this repository's site has it: its code imports
5
- * @swiftbrowser/wasm-compiler and the CLI's browser entry from their sources
5
+ * @swiftbrowser/wasm-compiler, the CLI's browser entry and @swiftbrowser/react from their sources
6
6
  * (the aliases below). The page fetches the npm packages it reads
7
7
  * (src/playground/files.ts) from a CDN that serves them file by file, at one
8
8
  * version, except those the site serves itself (`localPackages`): the kit
@@ -1 +1 @@
1
- {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAQL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAGpC,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,qEAAqE;AACrE,eAAO,MAAM,WAAW,sBAAsB,CAAC;AAC/C;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAmB,CAAC;AAIhD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK1E;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQvE;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAGxE;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAQjF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CActG;AAMD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,SAAkB,EAAE,YAAY,SAAqB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0DnM;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CA0BvF"}
1
+ {"version":3,"file":"playground.d.ts","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,EAQL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAC;AAGpC,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,qEAAqE;AACrE,eAAO,MAAM,WAAW,sBAAsB,CAAC;AAC/C;;;GAGG;AACH,eAAO,MAAM,eAAe,QAAmB,CAAC;AAIhD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAY,CAAC;AAE5C,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO1E;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQvE;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAGxE;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,QAAQ,EAAE,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAQjF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CActG;AAMD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,SAAkB,EAAE,YAAY,SAAqB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0DnM;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CA0BvF"}
@@ -2,7 +2,7 @@
2
2
  * The playground page (playground/index.html): Swift compiled in the browser
3
3
  * by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
4
4
  * Only this repository's site has it: its code imports
5
- * @swiftbrowser/wasm-compiler and the CLI's browser entry from their sources
5
+ * @swiftbrowser/wasm-compiler, the CLI's browser entry and @swiftbrowser/react from their sources
6
6
  * (the aliases below). The page fetches the npm packages it reads
7
7
  * (src/playground/files.ts) from a CDN that serves them file by file, at one
8
8
  * version, except those the site serves itself (`localPackages`): the kit
@@ -41,6 +41,8 @@ export function playgroundAliases(repoRoot) {
41
41
  return {
42
42
  '@swiftbrowser/wasm-compiler': path.join(repoRoot, 'packages/wasm-compiler/src/index.ts'),
43
43
  '@swiftbrowser/cli/browser': path.join(repoRoot, 'packages/cli/src/browser.ts'),
44
+ // The page's own UI is SwiftUI views from @swiftbrowser/react.
45
+ '@swiftbrowser/react': path.join(repoRoot, 'packages/react/src/index.ts'),
44
46
  };
45
47
  }
46
48
  /** The workspaces' package directories by package name. */
@@ -1 +1 @@
1
- {"version":3,"file":"playground.js","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"playground.js","sourceRoot":"","sources":["../../../plugins/playground.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swiftbrowser/web",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
5
  "description": "SwiftBrowser web renderer and dev server: applies render ops from a Swift/Wasm app to the DOM inside an iPhone frame.",
6
6
  "repository": {
@@ -56,7 +56,12 @@
56
56
  "@playwright/test": "^1.56.0",
57
57
  "@types/node": "^26.6.4",
58
58
  "@types/qrcode": "^1.5.6",
59
+ "@types/react": "^19.3.0",
60
+ "@types/react-dom": "^19.3.0",
59
61
  "brotli-dec-wasm": "^2.3.2",
62
+ "react": "^19.3.1",
63
+ "react-dom": "^19.3.1",
64
+ "react-reconciler": "^0.34.0",
60
65
  "typescript": "^5.9.2",
61
66
  "vitest": "^5.0.3"
62
67
  }
@@ -2,7 +2,7 @@
2
2
  * The playground page (playground/index.html): Swift compiled in the browser
3
3
  * by the WebAssembly compiler (docs/in-browser-compiler.md, Milestone 3).
4
4
  * Only this repository's site has it: its code imports
5
- * @swiftbrowser/wasm-compiler and the CLI's browser entry from their sources
5
+ * @swiftbrowser/wasm-compiler, the CLI's browser entry and @swiftbrowser/react from their sources
6
6
  * (the aliases below). The page fetches the npm packages it reads
7
7
  * (src/playground/files.ts) from a CDN that serves them file by file, at one
8
8
  * version, except those the site serves itself (`localPackages`): the kit
@@ -58,6 +58,8 @@ export function playgroundAliases(repoRoot: string): Record<string, string> {
58
58
  return {
59
59
  '@swiftbrowser/wasm-compiler': path.join(repoRoot, 'packages/wasm-compiler/src/index.ts'),
60
60
  '@swiftbrowser/cli/browser': path.join(repoRoot, 'packages/cli/src/browser.ts'),
61
+ // The page's own UI is SwiftUI views from @swiftbrowser/react.
62
+ '@swiftbrowser/react': path.join(repoRoot, 'packages/react/src/index.ts'),
61
63
  };
62
64
  }
63
65
 
@@ -3130,6 +3130,9 @@ export class LayoutPass {
3130
3130
  this.placeDatePicker(node, ctx, p, x, y, size);
3131
3131
  return;
3132
3132
  case 'texteditor':
3133
+ // It draws in the context font, as a field does (the page's own
3134
+ // font otherwise: a serif where the page sets none).
3135
+ if (node.id >= 0) this.fonts.set(node.id, ctx.font);
3133
3136
  return;
3134
3137
  case 'swipeactions':
3135
3138
  case 'contextmenu': {
package/src/layoutDom.ts CHANGED
@@ -492,6 +492,30 @@ function padIntoFrame(el: HTMLElement, box: Rect, frame: Rect, shift = 0): void
492
492
  if (s.paddingBottom !== paddingBottom) s.paddingBottom = paddingBottom;
493
493
  }
494
494
 
495
+ /**
496
+ * A vertical field's textarea draws every line a leading apart, where the
497
+ * engine sized it as a Text: its first line a line height tall, the rest a
498
+ * leading apart. The textarea's lines need the difference more (2 px for
499
+ * the body style); without it they overflow by that much, and the textarea
500
+ * scrolls its text up while it is edited. The difference comes out of the
501
+ * padding below the lines (a list row's, a rounded border's), else the box
502
+ * reaches that much further down (into the space below it, which its lines
503
+ * leave empty).
504
+ */
505
+ function fitFieldLines(el: HTMLTextAreaElement, font: ResolvedFont, box: Rect, frame: Rect): void {
506
+ const extra = Math.ceil(font.leading - font.lineHeight - 0.01);
507
+ if (extra <= 0) return;
508
+ const s = el.style;
509
+ const below = box.y + box.height - (frame.y + frame.height);
510
+ const padding = below > 0 ? below : el.dataset['sbStyle'] === 'roundedBorder' ? ROUNDED_FIELD_INSET : 0;
511
+ s.paddingBottom = `${Math.max(0, padding - extra)}px`;
512
+ const height = `${box.height + Math.max(0, extra - padding)}px`;
513
+ if (s.height !== height) s.height = height;
514
+ }
515
+
516
+ /** A rounded border field's padding above and below its lines (styles.css: the engine's inset of 6, less the 1 px border). */
517
+ const ROUNDED_FIELD_INSET = 5;
518
+
495
519
  const fieldCapShifts = new Map<string, number>();
496
520
 
497
521
  /**
@@ -597,6 +621,7 @@ function applyChrome(
597
621
  ? fieldCapShift(font) + (line ? line.y - frame.y - (frame.height - font.lineHeight) / 2 : 0)
598
622
  : 0;
599
623
  padIntoFrame(el, box, frame, shift);
624
+ if (el instanceof HTMLTextAreaElement && node.kind === 'textfield' && font) fitFieldLines(el, font, box, frame);
600
625
  return;
601
626
  }
602
627
  // Phase 8 controls: their chrome sits at the engine's rects inside the element's box.
package/src/main.ts CHANGED
@@ -147,11 +147,15 @@ function applyTheme(theme: Theme, persist: boolean): void {
147
147
  }
148
148
  }
149
149
 
150
- /** The OS appearance (device mode follows it; the toolbar toggle is hidden there). */
150
+ /**
151
+ * The OS appearance: device mode follows it, and an embedded page
152
+ * (`?embed=1`, the playground's simulator) too; the toolbar toggle is
153
+ * hidden in both.
154
+ */
151
155
  const darkScheme = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null;
152
156
  const systemTheme = (): Theme => (darkScheme?.matches ? 'dark' : 'light');
153
157
 
154
- if (deviceMode) {
158
+ if (deviceMode || document.documentElement.hasAttribute('data-sb-embed')) {
155
159
  applyTheme(systemTheme(), false);
156
160
  darkScheme?.addEventListener('change', () => {
157
161
  applyTheme(systemTheme(), false);
package/src/page.css CHANGED
@@ -232,3 +232,19 @@ html[data-sb-mode="device"] .status {
232
232
  max-height: 40dvh;
233
233
  overflow: auto;
234
234
  }
235
+
236
+ /* Embedded (?embed=1, app/index.html): in another page's frame, which has
237
+ its own controls and background. The device alone, at the top. */
238
+ html[data-sb-embed] .toolbar {
239
+ display: none;
240
+ }
241
+ html[data-sb-embed],
242
+ html[data-sb-embed] body {
243
+ min-height: 0;
244
+ background: transparent;
245
+ overflow: hidden;
246
+ }
247
+ /* Room for the device's rings and shadow, which the frame would clip. */
248
+ html[data-sb-embed] .stage {
249
+ padding: 16px;
250
+ }
@@ -0,0 +1,311 @@
1
+ // The playground's window, drawn with @swiftbrowser/react's SwiftUI views
2
+ // as Xcode lays one out: the project's files in a sidebar, then the editor
3
+ // beside the simulator. In a compact width (a phone) the code is a stack's
4
+ // root, the simulator a screen it pushes, and the files an inspector. The
5
+ // editor and the frame the app runs in are ordinary DOM in `Host`s.
6
+
7
+ import {
8
+ Button,
9
+ Divider,
10
+ HStack,
11
+ Host,
12
+ Label,
13
+ List,
14
+ NavigationSplitView,
15
+ NavigationStack,
16
+ Section,
17
+ ToolbarItem,
18
+ ZStack,
19
+ useHorizontalSizeClass,
20
+ } from '@swiftbrowser/react';
21
+ import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type KeyboardEvent } from 'react';
22
+ import type { Session, SessionState } from './session';
23
+
24
+ /** The value a phone's stack pushes for the simulator's screen. */
25
+ const SIMULATOR = 'simulator';
26
+
27
+ export interface PlaygroundProps {
28
+ session: Session;
29
+ appName: string;
30
+ /** Path relative to the app's Sources/ → text. */
31
+ files: Record<string, string>;
32
+ }
33
+
34
+ export function Playground({ session, appName, files: initialFiles }: PlaygroundProps) {
35
+ const state = useSyncExternalStore(session.subscribe, () => session.state);
36
+ const [files, setFiles] = useState(initialFiles);
37
+ const names = Object.keys(files).sort();
38
+ const [selection, setSelection] = useState<string | null>(names[0] ?? null);
39
+ // The file the editor shows: the sidebar's selection, or the last one
40
+ // when it has none.
41
+ const [shown, setShown] = useState(names[0] ?? '');
42
+ const select = (file: string | null): void => {
43
+ setSelection(file);
44
+ if (file) setShown(file);
45
+ };
46
+ const compact = useHorizontalSizeClass() === 'compact';
47
+ // A phone's stack: empty, or the simulator pushed over the code.
48
+ const [path, setPath] = useState<readonly unknown[]>([]);
49
+ const [filesShown, setFilesShown] = useState(false);
50
+ const latest = useRef(files);
51
+ latest.current = files;
52
+ const run = (): void => session.compile(latest.current);
53
+
54
+ // The first run loads the compiler and the SDK too.
55
+ useEffect(() => run(), []);
56
+ // On a phone a linked app comes into view.
57
+ useEffect(() => {
58
+ if (state.linked > 0 && compact) setPath((stack) => (stack.length > 0 ? stack : [SIMULATOR]));
59
+ }, [state.linked]);
60
+
61
+ const editor = (
62
+ <Host sizing="fill" className="pg-pane" frame={{ maxWidth: Infinity, maxHeight: Infinity }}>
63
+ <Editor
64
+ file={shown}
65
+ text={files[shown] ?? ''}
66
+ onChange={(text) => setFiles((all) => ({ ...all, [shown]: text }))}
67
+ onRun={run}
68
+ state={state}
69
+ />
70
+ </Host>
71
+ );
72
+ const preview = (
73
+ <Host sizing="fill" className="pg-pane" frame={compact ? { maxWidth: Infinity, maxHeight: Infinity } : { width: 430, maxHeight: Infinity }}>
74
+ <Simulator session={session} state={state} />
75
+ </Host>
76
+ );
77
+ const runButton = (
78
+ <ToolbarItem placement="primaryAction">
79
+ <Button action={run} disabled={state.busy}>
80
+ <Label title="Run" systemImage="play.fill" labelStyle={compact ? 'iconOnly' : 'titleAndIcon'} />
81
+ </Button>
82
+ </ToolbarItem>
83
+ );
84
+
85
+ const sources = (
86
+ <Section header="Sources">
87
+ {names.map((name) => (
88
+ <Label key={name} tag={name} title={name} systemImage="swift" />
89
+ ))}
90
+ </Section>
91
+ );
92
+ // Picking a file in the phone's inspector opens it and closes the inspector.
93
+ const pick = (file: string | null): void => {
94
+ if (!file) return;
95
+ select(file);
96
+ setFilesShown(false);
97
+ };
98
+
99
+ if (compact) {
100
+ // A phone: one stack. The code is its root, with the files in an
101
+ // inspector (a sheet in a compact width) and the simulator a screen the
102
+ // bar's iPhone button pushes, as does a build that links. Popping back
103
+ // unloads the app, which leaves its memory to the compiler.
104
+ return (
105
+ <NavigationStack
106
+ path={[path, setPath]}
107
+ destination={() => (
108
+ <ZStack navigationTitle={appName} navigationBarTitleDisplayMode="inline" toolbar={runButton}>
109
+ {preview}
110
+ </ZStack>
111
+ )}
112
+ >
113
+ <ZStack
114
+ navigationTitle={shown}
115
+ navigationBarTitleDisplayMode="inline"
116
+ toolbar={
117
+ <>
118
+ <ToolbarItem placement="topBarLeading">
119
+ <Button action={() => setFilesShown(true)}>
120
+ <Label title="Files" systemImage="folder" labelStyle="iconOnly" />
121
+ </Button>
122
+ </ToolbarItem>
123
+ {runButton}
124
+ <ToolbarItem placement="primaryAction">
125
+ <Button action={() => setPath([SIMULATOR])}>
126
+ <Label title="Simulator" systemImage="iphone" labelStyle="iconOnly" />
127
+ </Button>
128
+ </ToolbarItem>
129
+ </>
130
+ }
131
+ inspector={{
132
+ isPresented: [filesShown, setFilesShown],
133
+ content: (
134
+ <NavigationStack>
135
+ <List
136
+ selection={[shown, pick]}
137
+ navigationTitle="Files"
138
+ navigationBarTitleDisplayMode="inline"
139
+ presentationDetents={['medium', 'large']}
140
+ toolbar={
141
+ <ToolbarItem placement="confirmationAction">
142
+ <Button title="Done" action={() => setFilesShown(false)} />
143
+ </ToolbarItem>
144
+ }
145
+ >
146
+ {sources}
147
+ </List>
148
+ </NavigationStack>
149
+ ),
150
+ }}
151
+ >
152
+ {editor}
153
+ </ZStack>
154
+ </NavigationStack>
155
+ );
156
+ }
157
+
158
+ return (
159
+ <NavigationSplitView
160
+ sidebar={
161
+ <List selection={[selection, select]} navigationTitle={appName}>
162
+ {sources}
163
+ </List>
164
+ }
165
+ detail={
166
+ <HStack spacing={0} navigationTitle={shown} navigationBarTitleDisplayMode="inline" toolbar={runButton}>
167
+ {editor}
168
+ <Divider />
169
+ {preview}
170
+ </HStack>
171
+ }
172
+ />
173
+ );
174
+ }
175
+
176
+ interface EditorProps {
177
+ file: string;
178
+ text: string;
179
+ onChange: (text: string) => void;
180
+ onRun: () => void;
181
+ state: SessionState;
182
+ }
183
+
184
+ /** The source editor: a textarea with a line-number gutter, the issues below it, and a status bar. */
185
+ function Editor({ file, text, onChange, onRun, state }: EditorProps) {
186
+ const gutter = useRef<HTMLPreElement>(null);
187
+ const area = useRef<HTMLTextAreaElement>(null);
188
+ const [cursor, setCursor] = useState({ line: 1, column: 1 });
189
+ const lines = text.split('\n').length;
190
+ const syncScroll = (): void => {
191
+ if (gutter.current && area.current) gutter.current.scrollTop = area.current.scrollTop;
192
+ };
193
+ useLayoutEffect(syncScroll, [text, file]);
194
+ const track = (): void => {
195
+ const element = area.current;
196
+ if (!element) return;
197
+ const before = element.value.slice(0, element.selectionStart).split('\n');
198
+ setCursor({ line: before.length, column: before.at(-1)!.length + 1 });
199
+ };
200
+ const keyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {
201
+ // ⌘R or ⌘↩ (Ctrl on other systems) runs, as in Xcode.
202
+ if ((event.metaKey || event.ctrlKey) && (event.key === 'r' || event.key === 'Enter')) {
203
+ event.preventDefault();
204
+ onRun();
205
+ } else if (event.key === 'Tab' && !event.shiftKey) {
206
+ event.preventDefault();
207
+ const element = event.currentTarget;
208
+ const { selectionStart, selectionEnd, value } = element;
209
+ onChange(`${value.slice(0, selectionStart)} ${value.slice(selectionEnd)}`);
210
+ requestAnimationFrame(() => element.setSelectionRange(selectionStart + 4, selectionStart + 4));
211
+ }
212
+ };
213
+ const issues = state.output.trim();
214
+ const count = (kind: string): number => (issues.match(new RegExp(`^[^\\s:][^:]*:\\d+:\\d+: ${kind}:`, 'gm')) ?? []).length;
215
+ const errors = count('error');
216
+ const warnings = count('warning');
217
+ // Issues open by themselves when a build fails; warnings wait to be asked for.
218
+ const [open, setOpen] = useState(false);
219
+ useEffect(() => setOpen(state.failed), [state.output, state.failed]);
220
+ const summary = [errors && `${errors} error${errors === 1 ? '' : 's'}`, warnings && `${warnings} warning${warnings === 1 ? '' : 's'}`].filter(Boolean).join(', ') || (state.failed ? 'Failed' : 'Output');
221
+ return (
222
+ <div className="pg-editor">
223
+ {state.warning && (
224
+ <p id="warning" role="alert">
225
+ {state.warning}
226
+ </p>
227
+ )}
228
+ <div className="pg-code">
229
+ <pre ref={gutter} className="pg-gutter" aria-hidden="true">
230
+ {Array.from({ length: lines }, (_, index) => `${index + 1}\n`).join('')}
231
+ </pre>
232
+ <textarea
233
+ ref={area}
234
+ id="source"
235
+ aria-label={file}
236
+ spellCheck={false}
237
+ autoCapitalize="off"
238
+ autoComplete="off"
239
+ autoCorrect="off"
240
+ wrap="off"
241
+ value={text}
242
+ onChange={(event) => onChange(event.target.value)}
243
+ onScroll={syncScroll}
244
+ onKeyDown={keyDown}
245
+ onKeyUp={track}
246
+ onClick={track}
247
+ />
248
+ </div>
249
+ <pre id="diagnostics" className={state.failed ? 'pg-errors' : ''} hidden={issues === '' || !open}>
250
+ {issues}
251
+ </pre>
252
+ <div className="pg-statusbar">
253
+ <span id="status" role="status" className={state.busy ? 'pg-busy' : ''}>
254
+ {state.status}
255
+ </span>
256
+ {issues !== '' && (
257
+ <button type="button" className={`pg-issues${errors || state.failed ? ' pg-has-errors' : ''}`} aria-expanded={open} onClick={() => setOpen(!open)}>
258
+ {summary}
259
+ </button>
260
+ )}
261
+ <span className="pg-position">{`Line ${cursor.line}, Col ${cursor.column}`}</span>
262
+ </div>
263
+ </div>
264
+ );
265
+ }
266
+
267
+ /** Whether this is a touch device, where the app runs full screen in the simulator's pane. */
268
+ export const TOUCH = matchMedia('(pointer: coarse)').matches;
269
+ /**
270
+ * The embedded app page's size: its iPhone, bezel included (styles.css's
271
+ * --device-*), and 16 around it for the shadow (page.css).
272
+ */
273
+ const DEVICE = { width: 417 + 32, height: 876 + 32 };
274
+ /** The simulator's margin around that, beyond the shadow's room. */
275
+ const MARGIN = 4;
276
+
277
+ /**
278
+ * The simulator: the frame the app runs in, and the compiler's progress
279
+ * over it while it runs. Off a touch device the frame is the iPhone's size,
280
+ * scaled to fit the pane; the app inside never knows, so its taps and
281
+ * scrolling are exact.
282
+ */
283
+ function Simulator({ session, state }: { session: Session; state: SessionState }) {
284
+ const box = useRef<HTMLDivElement>(null);
285
+ const [fit, setFit] = useState({ scale: 1, top: MARGIN });
286
+ useLayoutEffect(() => {
287
+ const element = box.current;
288
+ if (TOUCH || !element) return;
289
+ const observer = new ResizeObserver(() => {
290
+ const { clientWidth: width, clientHeight: height } = element;
291
+ const scale = Math.max(0.2, Math.min(1, (width - 2 * MARGIN) / DEVICE.width, (height - 2 * MARGIN) / DEVICE.height));
292
+ setFit({ scale, top: Math.max(MARGIN, (height - DEVICE.height * scale) / 2) });
293
+ });
294
+ observer.observe(element);
295
+ return () => observer.disconnect();
296
+ }, []);
297
+ const style = TOUCH
298
+ ? undefined
299
+ : { width: DEVICE.width, height: DEVICE.height, top: fit.top, transform: `translateX(-50%) scale(${fit.scale})` };
300
+ return (
301
+ <div ref={box} className={TOUCH ? 'pg-simulator pg-full' : 'pg-simulator'}>
302
+ <iframe ref={session.attachFrame} id="app" title="The app" src="about:blank" style={style} />
303
+ {state.busy && (
304
+ <div className="pg-activity" aria-hidden="true">
305
+ <span className="pg-spinner" />
306
+ {state.status}
307
+ </div>
308
+ )}
309
+ </div>
310
+ );
311
+ }
@@ -0,0 +1,45 @@
1
+ /// <reference path="../vite-env.d.ts" />
2
+ // The playground page: an app's sources in an editor, compiled and linked in
3
+ // a worker by the WebAssembly Swift compiler (session.ts, worker.ts), and run
4
+ // by the renderer's own page in a frame. The page itself is SwiftUI views
5
+ // from @swiftbrowser/react (app.tsx). Counter's sources come with the page,
6
+ // so after the first visit's downloads nothing reaches a server.
7
+
8
+ import '../styles.css';
9
+ import './playground.css';
10
+ import { SwiftUIView } from '@swiftbrowser/react';
11
+ import { useSyncExternalStore } from 'react';
12
+ import { createRoot } from 'react-dom/client';
13
+ import { Playground, TOUCH } from './app';
14
+ import { Session } from './session';
15
+
16
+ const sources = import.meta.glob('../../../Examples/Counter/Sources/**/*.swift', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
17
+ const APP = 'Counter';
18
+
19
+ const source = {
20
+ ...__SB_PLAYGROUND__,
21
+ npm: new URL(__SB_PLAYGROUND__.npm, location.href).href,
22
+ ...(__SB_PLAYGROUND__.packages
23
+ ? { packages: Object.fromEntries(Object.entries(__SB_PLAYGROUND__.packages).map(([name, base]) => [name, new URL(base, location.href).href])) }
24
+ : {}),
25
+ };
26
+ const files = Object.fromEntries(Object.entries(sources).map(([file, text]) => [file.replace(/^.*\/Sources\//, ''), text]));
27
+ // A touch device runs the app full screen in its pane; elsewhere the
28
+ // simulator shows the app page's iPhone (app.tsx).
29
+ const session = new Session(source, APP, TOUCH ? 'device' : 'frame');
30
+ const scheme = matchMedia('(prefers-color-scheme: dark)');
31
+ const watchScheme = (change: () => void): (() => void) => {
32
+ scheme.addEventListener('change', change);
33
+ return () => scheme.removeEventListener('change', change);
34
+ };
35
+
36
+ function Root() {
37
+ const dark = useSyncExternalStore(watchScheme, () => scheme.matches);
38
+ return (
39
+ <SwiftUIView className="pg-root" colorScheme={dark ? 'dark' : 'light'} safeArea={0}>
40
+ <Playground session={session} appName={APP} files={files} />
41
+ </SwiftUIView>
42
+ );
43
+ }
44
+
45
+ createRoot(document.getElementById('root')!).render(<Root />);
@@ -1,31 +1,194 @@
1
- :root {
2
- --bg: #f5f5f7;
3
- --panel: #ffffff;
4
- --text: #1d1d1f;
5
- --muted: #6e6e73;
6
- --accent: #0071e3;
7
- --error: #c62828;
8
- --border: #d2d2d7;
9
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
1
+ /* The playground's window (app.tsx): SwiftUI views fill it; the editor and
2
+ the simulator are DOM in their hosts, styled with the renderer's tokens so
3
+ they follow dark mode. */
4
+ html,
5
+ body,
6
+ #root,
7
+ .pg-root {
8
+ margin: 0;
9
+ height: 100%;
10
+ }
11
+ /* The page behind the views, which a phone's browser tints its bars with:
12
+ the system background of the appearance the views follow (main.tsx). The
13
+ tokens are the island's, so not the body's to read. */
14
+ body {
15
+ background: #ffffff;
16
+ overscroll-behavior: none;
10
17
  }
11
18
  @media (prefers-color-scheme: dark) {
12
- :root { --bg: #000; --panel: #1c1c1e; --text: #f5f5f7; --muted: #98989d; --accent: #2997ff; --error: #ff6b6b; --border: #38383a; }
13
- }
14
- body { margin: 0; background: var(--bg); color: var(--text); }
15
- header { padding: 20px 24px 8px; }
16
- header h1 { margin: 0; font-size: 22px; }
17
- header p { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
18
- main { display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: 16px; padding: 16px 24px 24px; }
19
- @media (max-width: 900px) { main { grid-template-columns: 1fr; } }
20
- .editor { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
21
- #files { display: flex; gap: 4px; flex-wrap: wrap; }
22
- #files button { border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 6px; padding: 4px 10px; font: 13px ui-monospace, SFMono-Regular, Menlo, monospace; cursor: pointer; }
23
- #files button[aria-current='true'] { border-color: var(--accent); color: var(--accent); }
24
- #source { min-height: 480px; resize: vertical; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); color: var(--text); font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; tab-size: 4; }
25
- .actions { display: flex; align-items: center; gap: 12px; }
26
- #run { background: var(--accent); color: white; border: none; border-radius: 8px; padding: 8px 20px; font-size: 15px; font-weight: 600; cursor: pointer; }
27
- #run:disabled { opacity: 0.5; cursor: default; }
28
- #status { color: var(--muted); font-size: 13px; }
29
- #diagnostics { margin: 0; padding: 12px; border: 1px solid var(--error); border-radius: 8px; background: var(--panel); color: var(--error); font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow: auto; max-height: 320px; }
30
- #warning { margin: 0; padding: 8px 12px; border: 1px solid var(--error); border-radius: 8px; background: var(--panel); font-size: 13px; }
31
- .preview iframe { width: 100%; height: 860px; border: none; border-radius: 12px; background: var(--panel); }
19
+ body {
20
+ background: #000000;
21
+ }
22
+ }
23
+
24
+ .pg-pane {
25
+ height: 100%;
26
+ }
27
+
28
+ /* The editor: a gutter and the text, issues below, a status bar at the foot. */
29
+ .pg-editor {
30
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
31
+ display: flex;
32
+ flex-direction: column;
33
+ height: 100%;
34
+ min-width: 0;
35
+ background: var(--sb-color-system-background);
36
+ color: var(--sb-color-primary);
37
+ --pg-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
38
+ }
39
+ .pg-code {
40
+ position: relative;
41
+ display: flex;
42
+ flex: 1;
43
+ min-height: 0;
44
+ }
45
+ .pg-gutter,
46
+ .pg-code textarea {
47
+ margin: 0;
48
+ padding: 12px 0;
49
+ font: 13px/20px var(--pg-mono);
50
+ }
51
+ .pg-gutter {
52
+ flex: none;
53
+ width: 44px;
54
+ padding-right: 10px;
55
+ overflow: hidden;
56
+ text-align: right;
57
+ color: var(--sb-color-tertiary-label);
58
+ user-select: none;
59
+ white-space: pre;
60
+ }
61
+ .pg-code textarea {
62
+ flex: 1;
63
+ min-width: 0;
64
+ padding-left: 6px;
65
+ padding-right: 12px;
66
+ border: none;
67
+ outline: none;
68
+ resize: none;
69
+ background: transparent;
70
+ color: inherit;
71
+ tab-size: 4;
72
+ white-space: pre;
73
+ caret-color: var(--sb-color-accent);
74
+ }
75
+ #warning {
76
+ margin: 0;
77
+ padding: 8px 12px;
78
+ background: color-mix(in srgb, var(--sb-color-yellow) 18%, transparent);
79
+ border-bottom: 1px solid var(--sb-color-separator);
80
+ font-size: 13px;
81
+ }
82
+ #diagnostics {
83
+ flex: none;
84
+ max-height: 38%;
85
+ margin: 0;
86
+ padding: 10px 12px;
87
+ overflow: auto;
88
+ border-top: 1px solid var(--sb-color-separator);
89
+ background: var(--sb-color-secondary-system-background);
90
+ font: 12px/1.45 var(--pg-mono);
91
+ white-space: pre-wrap;
92
+ }
93
+ #diagnostics.pg-errors {
94
+ color: var(--sb-color-red);
95
+ }
96
+ .pg-statusbar {
97
+ flex: none;
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 12px;
101
+ height: 26px;
102
+ padding: 0 12px;
103
+ border-top: 1px solid var(--sb-color-separator);
104
+ background: var(--sb-color-secondary-system-background);
105
+ color: var(--sb-color-secondary);
106
+ font-size: 11px;
107
+ white-space: nowrap;
108
+ }
109
+ #status {
110
+ flex: 1;
111
+ min-width: 0;
112
+ overflow: hidden;
113
+ text-overflow: ellipsis;
114
+ }
115
+ .pg-issues {
116
+ flex: none;
117
+ padding: 1px 8px;
118
+ border: none;
119
+ border-radius: 999px;
120
+ background: color-mix(in srgb, var(--sb-color-yellow) 28%, transparent);
121
+ color: var(--sb-color-primary);
122
+ font: inherit;
123
+ cursor: pointer;
124
+ }
125
+ .pg-issues.pg-has-errors {
126
+ background: color-mix(in srgb, var(--sb-color-red) 22%, transparent);
127
+ }
128
+ .pg-issues[aria-expanded='true'] {
129
+ outline: 1px solid var(--sb-color-separator);
130
+ }
131
+ .pg-position {
132
+ font-variant-numeric: tabular-nums;
133
+ }
134
+
135
+ /* The simulator: the app's own page (its iPhone, or full screen on a touch
136
+ device) in a frame, with the compiler's progress floating over it. */
137
+ .pg-simulator {
138
+ position: relative;
139
+ height: 100%;
140
+ background: var(--sb-color-secondary-system-background);
141
+ }
142
+ .pg-simulator {
143
+ overflow: hidden;
144
+ }
145
+ .pg-simulator iframe {
146
+ position: absolute;
147
+ left: 50%;
148
+ display: block;
149
+ border: none;
150
+ transform-origin: top center;
151
+ color-scheme: normal;
152
+ }
153
+ .pg-simulator.pg-full iframe {
154
+ inset: 0;
155
+ left: 0;
156
+ width: 100%;
157
+ height: 100%;
158
+ }
159
+ .pg-activity {
160
+ position: absolute;
161
+ top: 12px;
162
+ left: 50%;
163
+ transform: translateX(-50%);
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ max-width: calc(100% - 32px);
168
+ padding: 8px 14px;
169
+ border-radius: 999px;
170
+ background: var(--sb-color-material-base, rgba(255, 255, 255, 0.8));
171
+ -webkit-backdrop-filter: blur(20px) saturate(1.8);
172
+ backdrop-filter: blur(20px) saturate(1.8);
173
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
174
+ color: var(--sb-color-primary);
175
+ font-size: 13px;
176
+ white-space: nowrap;
177
+ overflow: hidden;
178
+ text-overflow: ellipsis;
179
+ pointer-events: none;
180
+ }
181
+ .pg-spinner {
182
+ flex: none;
183
+ width: 12px;
184
+ height: 12px;
185
+ border: 2px solid var(--sb-color-tertiary-label);
186
+ border-top-color: var(--sb-color-accent);
187
+ border-radius: 50%;
188
+ animation: pg-spin 0.8s linear infinite;
189
+ }
190
+ @keyframes pg-spin {
191
+ to {
192
+ transform: rotate(360deg);
193
+ }
194
+ }
@@ -0,0 +1,117 @@
1
+ // The playground's compiles: an app's sources sent to the compiler's worker
2
+ // (worker.ts), the module it links run by the renderer's own page in the
3
+ // frame (`?module=`, a blob: URL), and what the page shows meanwhile. The
4
+ // UI (app.tsx) reads `state` and calls `compile`; it hands over the frame
5
+ // with `attachFrame` whenever it mounts one.
6
+
7
+ import { frozenCopy, restoreScroll } from './freeze';
8
+ import type { PlaygroundSource } from './files';
9
+ import type { FromWorker, ToWorker } from './worker';
10
+
11
+ export interface SessionState {
12
+ /** A compile is under way. */
13
+ busy: boolean;
14
+ /** One line on what the compiler is doing or did. */
15
+ status: string;
16
+ /** The compiler's diagnostics, or the error the worker failed with. */
17
+ output: string;
18
+ /** Whether the last compile failed (its output is errors). */
19
+ failed: boolean;
20
+ /** Why the kit may not suit this page's renderer, when it may not. */
21
+ warning: string | null;
22
+ /** How many compiles have linked an app. */
23
+ linked: number;
24
+ }
25
+
26
+ export class Session {
27
+ state: SessionState = { busy: false, status: 'Loading the compiler…', output: '', failed: false, warning: null, linked: 0 };
28
+ private readonly worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
29
+ private readonly listeners = new Set<() => void>();
30
+ private nextId = 1;
31
+ private started = 0;
32
+ private frame: HTMLIFrameElement | null = null;
33
+ /** The last linked app, and the still copy of it a compile shows instead (freeze.ts). */
34
+ private moduleUrl: string | null = null;
35
+ private frozenUrl: string | null = null;
36
+
37
+ /**
38
+ * `frame`: the app page in an embedded frame mode (its iPhone, without
39
+ * its toolbar) for the simulator to scale; `device`: full screen.
40
+ */
41
+ constructor(
42
+ private readonly source: PlaygroundSource,
43
+ private readonly appName: string,
44
+ private readonly appMode: 'frame' | 'device',
45
+ ) {
46
+ this.worker.onmessage = (event: MessageEvent<FromWorker>) => this.receive(event.data);
47
+ }
48
+
49
+ subscribe = (listener: () => void): (() => void) => {
50
+ this.listeners.add(listener);
51
+ return () => this.listeners.delete(listener);
52
+ };
53
+
54
+ private set(changes: Partial<SessionState>): void {
55
+ this.state = { ...this.state, ...changes };
56
+ for (const listener of this.listeners) listener();
57
+ }
58
+
59
+ /** The frame the app runs in, or null when the UI unmounts it; a new one shows what the last had. */
60
+ attachFrame = (frame: HTMLIFrameElement | null): void => {
61
+ this.frame = frame;
62
+ if (!frame) return;
63
+ frame.addEventListener('load', () => {
64
+ if (this.frozenUrl && frame.contentDocument && frame.contentWindow?.location.href === this.frozenUrl) restoreScroll(frame.contentDocument);
65
+ });
66
+ const url = this.frozenUrl ?? (this.moduleUrl ? this.appUrl(this.moduleUrl) : null);
67
+ if (url) frame.src = url;
68
+ };
69
+
70
+ private appUrl(moduleUrl: string): string {
71
+ const mode = this.appMode === 'frame' ? 'mode=frame&embed=1' : 'mode=device';
72
+ return `${import.meta.env.BASE_URL}app/?app=${this.appName}&${mode}&module=${encodeURIComponent(moduleUrl)}`;
73
+ }
74
+
75
+ compile(files: Record<string, string>): void {
76
+ if (this.state.busy) return;
77
+ // The last build's app goes while the compiler runs: its page, its module
78
+ // and its memory would stay beside the linker's, which a phone has not got.
79
+ // A still copy of it stays in its place.
80
+ if (this.moduleUrl) {
81
+ let copy: string | null = null;
82
+ try {
83
+ copy = this.frame?.contentDocument ? frozenCopy(this.frame.contentDocument) : null;
84
+ } catch {
85
+ // No copy: the frame goes blank.
86
+ }
87
+ if (this.frozenUrl) URL.revokeObjectURL(this.frozenUrl);
88
+ this.frozenUrl = copy ? URL.createObjectURL(new Blob([copy], { type: 'text/html' })) : null;
89
+ if (this.frame) this.frame.src = this.frozenUrl ?? 'about:blank';
90
+ URL.revokeObjectURL(this.moduleUrl);
91
+ this.moduleUrl = null;
92
+ }
93
+ this.started = performance.now();
94
+ this.set({ busy: true, status: 'Compiling…' });
95
+ const message: ToWorker = { type: 'compile', id: this.nextId++, source: this.source, app: { name: this.appName, files: { ...files } } };
96
+ this.worker.postMessage(message);
97
+ }
98
+
99
+ private receive(message: FromWorker): void {
100
+ if (message.type === 'progress') return this.set({ status: message.text });
101
+ if (message.type === 'warning') return this.set({ warning: message.text });
102
+ if (message.type === 'failure') return this.set({ busy: false, status: 'The compiler failed.', output: message.error, failed: true });
103
+ const seconds = (performance.now() - this.started) / 1000;
104
+ // Each tool's memory too: what a phone's browser runs out of first.
105
+ const steps = message.steps
106
+ .map((step) => `${step.tool} ${step.seconds.toFixed(1)} s${step.memoryBytes ? `, ${Math.round(step.memoryBytes / 1e6)} MB` : ''}`)
107
+ .join('; ');
108
+ if (!message.ok || !message.wasm) {
109
+ return this.set({ busy: false, status: `Did not compile (${steps}).`, output: message.output, failed: true });
110
+ }
111
+ if (this.frozenUrl) URL.revokeObjectURL(this.frozenUrl);
112
+ this.frozenUrl = null;
113
+ this.moduleUrl = URL.createObjectURL(new Blob([message.wasm], { type: 'application/wasm' }));
114
+ if (this.frame) this.frame.src = this.appUrl(this.moduleUrl);
115
+ this.set({ busy: false, status: `Compiled and linked in ${seconds.toFixed(1)} s (${steps}).`, output: message.output, failed: false, linked: this.state.linked + 1 });
116
+ }
117
+ }
package/src/renderer.ts CHANGED
@@ -433,10 +433,35 @@ export class Renderer {
433
433
  /** The highest `scrollTo` serial seen: a command keeps its serial until the reader re-runs, and runs once. */
434
434
  private scrollSerial = 0;
435
435
 
436
+ /**
437
+ * A row that taps its button (`rowAction`) highlights while pressed, as a
438
+ * row that is a button does (`:active`, styles.css): until the pointer is
439
+ * released or the press turns into a scroll.
440
+ */
441
+ private readonly onRowPress = (event: PointerEvent): void => {
442
+ const hit = event.target as HTMLElement | null;
443
+ if (!hit || !event.isPrimary || hit.closest('.sb-button, .sb-navlink')) return;
444
+ const button = rowAction(hit, this.root);
445
+ const row = button ? listRowOf(button, this.root) : null;
446
+ if (!row) return;
447
+ row.dataset['sbRowPressed'] = '';
448
+ const release = (): void => {
449
+ delete row.dataset['sbRowPressed'];
450
+ window.removeEventListener('pointerup', release, true);
451
+ window.removeEventListener('pointercancel', release, true);
452
+ this.root.removeEventListener('scroll', release, true);
453
+ };
454
+ window.addEventListener('pointerup', release, true);
455
+ window.addEventListener('pointercancel', release, true);
456
+ this.root.addEventListener('scroll', release, true);
457
+ };
458
+
436
459
  private readonly onClick = (event: MouseEvent): void => {
437
460
  // `closest` picks the innermost control, so a button inside a
438
- // `tapgesture` (or a menu label) handles the click alone.
439
- const target = (event.target as HTMLElement | null)?.closest<HTMLElement>(
461
+ // `tapgesture` (or a menu label) handles the click alone. Elsewhere in
462
+ // a list row, the row's one button (`rowAction`).
463
+ const hit = event.target as HTMLElement | null;
464
+ const target = hit?.closest<HTMLElement>(
440
465
  '.sb-button, .sb-navlink, .sb-navbar-back, .sb-navbar-more, .sb-navsplit-toggle, .sb-switch, .sb-sheet-scrim, ' +
441
466
  '.sb-tabbar-item, .sb-segment, .sb-picker-option, .sb-picker-menu-item, .sb-picker[data-sb-style="menu"], ' +
442
467
  '.sb-menu, .sb-tapgesture, .sb-stepper-button, .sb-alert-scrim, .sb-alert-ok, ' +
@@ -444,7 +469,7 @@ export class Renderer {
444
469
  '.sb-swipe-action, .sb-listrow-control, .sb-listrow[data-sb-selectable], ' +
445
470
  // A link in a rich text: innermost, so it wins over a tappable row around the text
446
471
  '.sb-link',
447
- );
472
+ ) ?? (hit ? rowAction(hit, this.root) : null);
448
473
  if (!target || !this.root.contains(target)) return;
449
474
  if (target.classList.contains('sb-link')) {
450
475
  const text = target.closest<HTMLElement>('.sb-text');
@@ -820,6 +845,7 @@ export class Renderer {
820
845
  // Delegated listeners instead of one per control: elements can be
821
846
  // created detached and moved around freely.
822
847
  root.addEventListener('click', this.onClick);
848
+ root.addEventListener('pointerdown', this.onRowPress);
823
849
  root.addEventListener('keydown', this.onKeyDown);
824
850
  root.addEventListener('input', this.onInput);
825
851
  root.addEventListener('focusin', this.onFocusChange);
@@ -1502,6 +1528,7 @@ export class Renderer {
1502
1528
  dispose(): void {
1503
1529
  this.closeMenu();
1504
1530
  this.root.removeEventListener('click', this.onClick);
1531
+ this.root.removeEventListener('pointerdown', this.onRowPress);
1505
1532
  this.root.removeEventListener('keydown', this.onKeyDown);
1506
1533
  this.root.removeEventListener('input', this.onInput);
1507
1534
  this.root.removeEventListener('focusin', this.onFocusChange);
@@ -1688,12 +1715,17 @@ export class Renderer {
1688
1715
  // props produced before applying the new ones. The layout box is kept:
1689
1716
  // the engine re-lays out at `commit`, and removals recorded later in the
1690
1717
  // pass measure their old position against intact ancestors.
1691
- const { left, top, width, height } = el.style;
1692
- el.removeAttribute('style');
1693
- if (left) el.style.left = left;
1694
- if (top) el.style.top = top;
1695
- if (width) el.style.width = width;
1696
- if (height) el.style.height = height;
1718
+ // A text input's props produce no inline style (layout sets its box,
1719
+ // font and padding), and it is updated on every keystroke while it is
1720
+ // being edited: its style stays as it is.
1721
+ if (kind !== 'textfield' && kind !== 'texteditor') {
1722
+ const { left, top, width, height } = el.style;
1723
+ el.removeAttribute('style');
1724
+ if (left) el.style.left = left;
1725
+ if (top) el.style.top = top;
1726
+ if (width) el.style.width = width;
1727
+ if (height) el.style.height = height;
1728
+ }
1697
1729
  for (const key of Object.keys(el.dataset)) {
1698
1730
  if (key !== ID_ATTR && key !== KIND_ATTR) delete el.dataset[key];
1699
1731
  }
@@ -2510,6 +2542,38 @@ function createShell(kind: ElementKind): HTMLElement {
2510
2542
  }
2511
2543
  }
2512
2544
 
2545
+ /** The list row `el` is in (a direct child of a section's rows or a list's content), or null. */
2546
+ function listRowOf(el: HTMLElement, root: HTMLElement): HTMLElement | null {
2547
+ for (let node: HTMLElement | null = el; node && node !== root; node = node.parentElement) {
2548
+ const parent = node.parentElement;
2549
+ if (parent?.classList.contains('sb-section-rows') || parent?.classList.contains('sb-list-content')) return node;
2550
+ }
2551
+ return null;
2552
+ }
2553
+
2554
+ /** Controls a row may hold besides its button for the row to stay the button's: none. */
2555
+ const ROW_CONTROLS =
2556
+ 'input, textarea, .sb-navlink, .sb-switch, .sb-picker, .sb-stepper, .sb-slider, .sb-menu, .sb-tapgesture, .sb-link, .sb-datepicker';
2557
+
2558
+ /**
2559
+ * A list row holding one button (with other views beside it, or under a
2560
+ * modifier) taps the button wherever it is tapped, as a UITableView cell
2561
+ * does on iOS; a row that is the button itself is that already. Not a
2562
+ * `.plain` or `.borderless` button (both arrive as `plain`), which SwiftUI
2563
+ * keeps to its own label in a row, a disabled one, or a row with other
2564
+ * controls.
2565
+ */
2566
+ function rowAction(el: HTMLElement, root: HTMLElement): HTMLElement | null {
2567
+ const row = listRowOf(el, root);
2568
+ if (!row || row.matches('.sb-button, .sb-navlink, .sb-listrow[data-sb-selectable]')) return null;
2569
+ const buttons = row.querySelectorAll<HTMLElement>('.sb-button');
2570
+ if (buttons.length !== 1) return null;
2571
+ const button = buttons[0]!;
2572
+ if (button.dataset['sbStyle'] === 'plain' || button.closest('[data-sb-disabled]')) return null;
2573
+ if (row.querySelector(ROW_CONTROLS)) return null;
2574
+ return button;
2575
+ }
2576
+
2513
2577
  /** A vertical `textfield`: a `<textarea>` the layout sizes to its lines. */
2514
2578
  function createMultilineField(): HTMLTextAreaElement {
2515
2579
  const el = document.createElement('textarea');
@@ -3565,7 +3629,8 @@ function applyTextField(input: HTMLInputElement | HTMLTextAreaElement, props: Te
3565
3629
  // A vertical field is a `<textarea>`, which has no type.
3566
3630
  if (input instanceof HTMLInputElement) input.type = props.secure ? 'password' : 'text';
3567
3631
  else input.dataset['sbAxis'] = 'vertical';
3568
- input.placeholder = props.placeholder ?? '';
3632
+ const placeholder = props.placeholder ?? '';
3633
+ if (input.placeholder !== placeholder) input.placeholder = placeholder;
3569
3634
  input.dataset['sbStyle'] = props.style ?? 'plain';
3570
3635
  const text = props.text ?? '';
3571
3636
  // Leave the caret alone when Swift echoes the value the field already shows.
package/src/styles.css CHANGED
@@ -196,6 +196,14 @@
196
196
 
197
197
  .device {
198
198
  position: relative;
199
+ /* iOS Safari inflates the text of wide multi-line blocks it judges hard to
200
+ read ("text autosizing"): a wrapped section footer or paragraph would draw
201
+ a size up from the one the engine measured it at and run into what
202
+ follows. page.css pins this on the document in device mode; an island
203
+ (@swiftbrowser/react) lives in a page that may not, so pin it here too.
204
+ Inherited, so it covers everything the frame draws. */
205
+ -webkit-text-size-adjust: 100%;
206
+ text-size-adjust: 100%;
199
207
  }
200
208
 
201
209
  /* The iPhone's size and bezel: the frame's only. An island (`.sb-island`,
@@ -759,6 +767,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
759
767
  color: var(--sb-color-placeholder);
760
768
  }
761
769
 
770
+ /* A UITextField's rounded rect: the same with the focus as without (iOS
771
+ draws no focus ring or tinted border on it). */
762
772
  .sb-textfield[data-sb-style="roundedBorder"] {
763
773
  padding: 0 7px;
764
774
  border: 1px solid var(--sb-color-field-border);
@@ -766,11 +776,6 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
766
776
  background: var(--sb-color-field-background);
767
777
  }
768
778
 
769
- .sb-textfield[data-sb-style="roundedBorder"]:focus-visible {
770
- border-color: var(--sb-color-accent);
771
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--sb-color-accent) 30%, transparent);
772
- }
773
-
774
779
  /* Toggle: the label children are positioned by their frames; the switch sits
775
780
  at the engine's `switch` rect (51×31 at the trailing edge). */
776
781
  .sb-switch {
@@ -969,6 +974,12 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
969
974
  background: var(--sb-color-fill);
970
975
  }
971
976
 
977
+ /* A row whose one button it taps (renderer.ts `rowAction`), while pressed. */
978
+ .sb-section-rows > [data-sb-row-pressed],
979
+ .sb-list-content > [data-sb-row-pressed] {
980
+ background: var(--sb-color-fill);
981
+ }
982
+
972
983
  /* ---------------------------------------------------------------------------
973
984
  Phase 2: navigation
974
985
  ------------------------------------------------------------------------- */
@@ -2901,6 +2912,14 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2901
2912
  pointer-events: auto;
2902
2913
  }
2903
2914
 
2915
+ /* A list row is content however it is made (a stack, a modifier box): its
2916
+ empty space is the cell's, and a tap there taps the row's one button
2917
+ (renderer.ts `rowAction`), as on iOS. */
2918
+ .sb-section-rows > [data-sb-kind],
2919
+ .sb-list-content > [data-sb-kind] {
2920
+ pointer-events: auto;
2921
+ }
2922
+
2904
2923
  .sb-styled[data-sb-background] {
2905
2924
  pointer-events: auto;
2906
2925
  }
@@ -3749,14 +3768,17 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
3749
3768
  }
3750
3769
 
3751
3770
  /* ---------------------------------------------------------------------------
3752
- Text editor: a borderless textarea filling its frame in the body font (its
3753
- parent provides any frame or background).
3771
+ Text editor: a borderless textarea filling its frame in its context's font
3772
+ (layoutDom.ts; its parent provides any frame or background). Its text is
3773
+ inset as a UITextView's is: 8 above and below (`textContainerInset`), 5 at
3774
+ the sides (`lineFragmentPadding`).
3754
3775
  ------------------------------------------------------------------------- */
3755
3776
  .sb-texteditor {
3756
3777
  appearance: none;
3757
3778
  display: block;
3779
+ box-sizing: border-box;
3758
3780
  margin: 0;
3759
- padding: 0;
3781
+ padding: 8px 5px;
3760
3782
  border: 0;
3761
3783
  outline: none;
3762
3784
  resize: none;
package/src/symbols.ts CHANGED
@@ -577,6 +577,20 @@ const QUESTIONMARK: IconNode = [
577
577
  ['path', { d: 'M12 20.5h.01' }],
578
578
  ];
579
579
 
580
+ /**
581
+ * `tray.fill`: the tray's silhouette (lucide's `Inbox`) filled, split along
582
+ * its opening's edge, the line with the notch, by a gap: the well above,
583
+ * the front below. A real gap, not a stroke in the background color, so
584
+ * whatever is behind the glyph (a selected tab's capsule) shows through.
585
+ * The pieces' edges along it sit 1.75 from that line, so with their 2 wide
586
+ * stroke they leave 1.5 between them, the notch's slants offset square to
587
+ * their direction.
588
+ */
589
+ const TRAY_FILL: IconNode = [
590
+ ['path', { d: 'M2.88 10.25 5.45 5.11A2 2 0 0 1 7.24 4h9.52a2 2 0 0 1 1.79 1.11l2.57 5.14h-6.06l-2 3h-2.12l-2-3z' }],
591
+ ['path', { d: 'M2 13.75h5.06l2 3h5.88l2-3H22V18a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z' }],
592
+ ];
593
+
580
594
  const outline = (icon: IconNode): SymbolGlyph => ({ icon, fill: 'outline' });
581
595
  const knockout = (icon: IconNode): SymbolGlyph => ({ icon, fill: 'knockout' });
582
596
  const solid = (icon: IconNode): SymbolGlyph => ({ icon, fill: 'solid' });
@@ -1083,7 +1097,7 @@ export const SF_SYMBOLS: Readonly<Record<string, SymbolGlyph>> = {
1083
1097
  'envelope.badge': outline(MailWarning),
1084
1098
  'envelope.open.fill': outline(MailOpen),
1085
1099
  'tray.full': outline(Inbox),
1086
- 'tray.fill': solid(Inbox),
1100
+ 'tray.fill': solid(TRAY_FILL),
1087
1101
  'tray.and.arrow.down': outline(Inbox),
1088
1102
  'tray.and.arrow.up': outline(Inbox),
1089
1103
  'phone.arrow.up.right': outline(PhoneOutgoing),
@@ -1,133 +0,0 @@
1
- /// <reference path="../vite-env.d.ts" />
2
- // The playground page: an app's sources in an editor, compiled and linked in
3
- // a worker by the WebAssembly Swift compiler (worker.ts), and run by the
4
- // renderer's own page in the frame, which takes the module as a blob: URL
5
- // (`?module=`, main.ts). Counter's sources come with the page, so after the
6
- // first visit's downloads nothing reaches a server.
7
-
8
- import { frozenCopy, restoreScroll } from './freeze';
9
- import type { FromWorker, ToWorker } from './worker';
10
-
11
- const sources = import.meta.glob('../../../Examples/Counter/Sources/**/*.swift', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
12
- const APP = 'Counter';
13
-
14
- function $<T extends HTMLElement>(selector: string): T {
15
- const element = document.querySelector<T>(selector);
16
- if (!element) throw new Error(`missing ${selector}`);
17
- return element;
18
- }
19
- const filesNav = $<HTMLElement>('#files');
20
- const editor = $<HTMLTextAreaElement>('#source');
21
- const run = $<HTMLButtonElement>('#run');
22
- const status = $<HTMLElement>('#status');
23
- const diagnostics = $<HTMLPreElement>('#diagnostics');
24
- const frame = $<HTMLIFrameElement>('#app');
25
- const warning = $<HTMLElement>('#warning');
26
-
27
- // Path relative to Sources/ → text, as edited.
28
- const files: Record<string, string> = Object.fromEntries(
29
- Object.entries(sources).map(([file, text]) => [file.replace(/^.*\/Sources\//, ''), text]),
30
- );
31
- let current = Object.keys(files).sort()[0]!;
32
-
33
- function showFiles(): void {
34
- filesNav.replaceChildren(...Object.keys(files).sort().map((file) => {
35
- const button = document.createElement('button');
36
- button.type = 'button';
37
- button.textContent = file;
38
- button.setAttribute('aria-current', String(file === current));
39
- button.addEventListener('click', () => {
40
- files[current] = editor.value;
41
- current = file;
42
- editor.value = files[file]!;
43
- showFiles();
44
- });
45
- return button;
46
- }));
47
- }
48
- editor.value = files[current]!;
49
- editor.addEventListener('input', () => (files[current] = editor.value));
50
- showFiles();
51
-
52
- const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
53
- const source = {
54
- ...__SB_PLAYGROUND__,
55
- npm: new URL(__SB_PLAYGROUND__.npm, location.href).href,
56
- ...(__SB_PLAYGROUND__.packages
57
- ? { packages: Object.fromEntries(Object.entries(__SB_PLAYGROUND__.packages).map(([name, base]) => [name, new URL(base, location.href).href])) }
58
- : {}),
59
- };
60
- let nextId = 1;
61
- let moduleUrl: string | null = null;
62
- /** The still copy of the last app the frame shows while a compile runs. */
63
- let frozenUrl: string | null = null;
64
- frame.addEventListener('load', () => {
65
- if (frozenUrl && frame.contentDocument && frame.contentWindow?.location.href === frozenUrl) restoreScroll(frame.contentDocument);
66
- });
67
- const started = new Map<number, number>();
68
-
69
- worker.onmessage = (event: MessageEvent<FromWorker>) => {
70
- const message = event.data;
71
- if (message.type === 'progress') {
72
- status.textContent = message.text;
73
- return;
74
- }
75
- if (message.type === 'warning') {
76
- warning.hidden = false;
77
- warning.textContent = message.text;
78
- return;
79
- }
80
- run.disabled = false;
81
- if (message.type === 'failure') {
82
- status.textContent = 'The compiler failed.';
83
- diagnostics.hidden = false;
84
- diagnostics.textContent = message.error;
85
- return;
86
- }
87
- const seconds = (performance.now() - (started.get(message.id) ?? performance.now())) / 1000;
88
- // Each tool's memory too: what a phone's browser runs out of first.
89
- const steps = message.steps
90
- .map((step) => `${step.tool} ${step.seconds.toFixed(1)} s${step.memoryBytes ? `, ${Math.round(step.memoryBytes / 1e6)} MB` : ''}`)
91
- .join('; ');
92
- diagnostics.hidden = message.ok || message.output === '';
93
- diagnostics.textContent = message.output;
94
- if (!message.ok || !message.wasm) {
95
- status.textContent = `Did not compile (${steps}).`;
96
- diagnostics.hidden = false;
97
- return;
98
- }
99
- status.textContent = `Compiled and linked in ${seconds.toFixed(1)} s (${steps}).`;
100
- if (frozenUrl) URL.revokeObjectURL(frozenUrl);
101
- frozenUrl = null;
102
- moduleUrl = URL.createObjectURL(new Blob([message.wasm], { type: 'application/wasm' }));
103
- frame.src = `${import.meta.env.BASE_URL}app/?app=${APP}&module=${encodeURIComponent(moduleUrl)}`;
104
- };
105
-
106
- function compile(): void {
107
- run.disabled = true;
108
- // The last build's app goes while the compiler runs: its page, its module
109
- // and its memory would stay beside the linker's, which a phone has not got.
110
- // A still copy of it stays in its place (freeze.ts).
111
- if (moduleUrl) {
112
- let copy: string | null = null;
113
- try {
114
- copy = frame.contentDocument ? frozenCopy(frame.contentDocument) : null;
115
- } catch {
116
- // No copy: the frame goes blank.
117
- }
118
- if (frozenUrl) URL.revokeObjectURL(frozenUrl);
119
- frozenUrl = copy ? URL.createObjectURL(new Blob([copy], { type: 'text/html' })) : null;
120
- frame.src = frozenUrl ?? 'about:blank';
121
- URL.revokeObjectURL(moduleUrl);
122
- moduleUrl = null;
123
- }
124
- diagnostics.hidden = true;
125
- status.textContent = 'Compiling…';
126
- const id = nextId++;
127
- started.set(id, performance.now());
128
- const message: ToWorker = { type: 'compile', id, source, app: { name: APP, files: { ...files } } };
129
- worker.postMessage(message);
130
- }
131
- run.addEventListener('click', compile);
132
- // The first run loads the compiler and the SDK too.
133
- compile();