@swiftbrowser/web 0.1.0 → 0.3.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 (99) hide show
  1. package/README.md +141 -63
  2. package/app/index.html +21 -10
  3. package/node/dist/node/apps.d.ts +16 -0
  4. package/node/dist/node/apps.d.ts.map +1 -1
  5. package/node/dist/node/apps.js +9 -0
  6. package/node/dist/node/apps.js.map +1 -1
  7. package/node/dist/node/config.d.ts +14 -1
  8. package/node/dist/node/config.d.ts.map +1 -1
  9. package/node/dist/node/config.js +20 -3
  10. package/node/dist/node/config.js.map +1 -1
  11. package/node/dist/node/examples.d.ts +2 -1
  12. package/node/dist/node/examples.d.ts.map +1 -1
  13. package/node/dist/node/examples.js +12 -2
  14. package/node/dist/node/examples.js.map +1 -1
  15. package/node/dist/plugins/app-manifests.d.ts +4 -1
  16. package/node/dist/plugins/app-manifests.d.ts.map +1 -1
  17. package/node/dist/plugins/app-manifests.js +3 -2
  18. package/node/dist/plugins/app-manifests.js.map +1 -1
  19. package/node/dist/plugins/asset-catalogs.d.ts +15 -3
  20. package/node/dist/plugins/asset-catalogs.d.ts.map +1 -1
  21. package/node/dist/plugins/asset-catalogs.js +111 -13
  22. package/node/dist/plugins/asset-catalogs.js.map +1 -1
  23. package/node/dist/plugins/included-sites.d.ts +26 -0
  24. package/node/dist/plugins/included-sites.d.ts.map +1 -0
  25. package/node/dist/plugins/included-sites.js +70 -0
  26. package/node/dist/plugins/included-sites.js.map +1 -0
  27. package/node/dist/plugins/pdfImages.d.ts +27 -0
  28. package/node/dist/plugins/pdfImages.d.ts.map +1 -0
  29. package/node/dist/plugins/pdfImages.js +145 -0
  30. package/node/dist/plugins/pdfImages.js.map +1 -0
  31. package/node/dist/plugins/swift-wasm.d.ts +19 -1
  32. package/node/dist/plugins/swift-wasm.d.ts.map +1 -1
  33. package/node/dist/plugins/swift-wasm.js +45 -7
  34. package/node/dist/plugins/swift-wasm.js.map +1 -1
  35. package/node/dist/plugins/symbolTemplates.d.ts +22 -0
  36. package/node/dist/plugins/symbolTemplates.d.ts.map +1 -0
  37. package/node/dist/plugins/symbolTemplates.js +83 -0
  38. package/node/dist/plugins/symbolTemplates.js.map +1 -0
  39. package/package.json +4 -2
  40. package/plugins/app-manifests.ts +3 -2
  41. package/plugins/asset-catalogs.ts +119 -14
  42. package/plugins/included-sites.ts +74 -0
  43. package/plugins/pdfImages.ts +163 -0
  44. package/plugins/swift-wasm.ts +52 -6
  45. package/plugins/symbolTemplates.ts +88 -0
  46. package/public/_headers +7 -0
  47. package/public/app/oauth-callback.html +62 -0
  48. package/src/animation.ts +235 -8
  49. package/src/appFiles.ts +201 -0
  50. package/src/base64.ts +43 -0
  51. package/src/bitmapPlan.ts +143 -0
  52. package/src/bitmapStore.ts +206 -0
  53. package/src/bitmapWorker.ts +41 -0
  54. package/src/bitmapWorkerProtocol.ts +15 -0
  55. package/src/bitmaps.ts +283 -0
  56. package/src/blockedHosts.ts +29 -0
  57. package/src/canvasDrawing.ts +361 -0
  58. package/src/chartDom.ts +2 -2
  59. package/src/chromeColor.ts +179 -0
  60. package/src/clipboard.ts +78 -0
  61. package/src/colorMultiply.ts +93 -0
  62. package/src/colorPicker.ts +44 -0
  63. package/src/corsProxy.ts +45 -0
  64. package/src/gaugeRing.ts +74 -0
  65. package/src/gif.ts +297 -0
  66. package/src/hostRequests.ts +426 -0
  67. package/src/imageOps.ts +592 -0
  68. package/src/interaction.ts +68 -3
  69. package/src/landing.ts +2 -2
  70. package/src/largeTitle.ts +79 -0
  71. package/src/layout/chart.ts +9 -3
  72. package/src/layout/engine.ts +681 -174
  73. package/src/layout/index.ts +3 -1
  74. package/src/layout/text.ts +217 -1
  75. package/src/layout/types.ts +31 -0
  76. package/src/layout/typography.ts +24 -7
  77. package/src/layout/video.ts +24 -0
  78. package/src/layoutDom.ts +66 -6
  79. package/src/main.ts +127 -4
  80. package/src/media.ts +132 -0
  81. package/src/mock.ts +4 -3
  82. package/src/notifications.ts +109 -0
  83. package/src/pathShape.ts +156 -0
  84. package/src/protocol.ts +782 -13
  85. package/src/renderer.ts +603 -44
  86. package/src/richText.ts +65 -0
  87. package/src/scanner.ts +234 -0
  88. package/src/scrollTo.ts +123 -0
  89. package/src/styles.css +521 -100
  90. package/src/styles.ts +61 -0
  91. package/src/swiftData.ts +128 -0
  92. package/src/symbols.ts +658 -28
  93. package/src/translation.ts +126 -0
  94. package/src/video.ts +299 -0
  95. package/src/vite-env.d.ts +6 -0
  96. package/src/wasm.ts +227 -32
  97. package/src/webAuth.ts +267 -0
  98. package/src/webSocket.ts +148 -0
  99. package/src/zoneinfo.ts +269 -0
package/src/styles.ts CHANGED
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { assetColorVar } from './assets';
17
17
  import {
18
+ isAngularGradientStyle,
18
19
  isAssetColor,
19
20
  isColor,
20
21
  isColorGradientStyle,
@@ -41,7 +42,20 @@ const SEMANTIC_COLOR_VAR: Record<string, string> = {
41
42
  accent: 'var(--sb-color-accent)',
42
43
  systemBackground: 'var(--sb-color-system-background)',
43
44
  secondarySystemBackground: 'var(--sb-color-secondary-system-background)',
45
+ // UIKit's semantic colors (`Color(uiColor:)`) with a token of another name;
46
+ // the rest map by name (`tertiaryLabel` → `--sb-color-tertiary-label`).
47
+ label: 'var(--sb-color-primary)',
48
+ secondaryLabel: 'var(--sb-color-secondary)',
49
+ systemGroupedBackground: 'var(--sb-color-grouped-background)',
50
+ secondarySystemGroupedBackground: 'var(--sb-color-grouped-cell)',
51
+ placeholderText: 'var(--sb-color-placeholder)',
52
+ systemFill: 'var(--sb-color-fill)',
53
+ secondarySystemFill: 'var(--sb-color-secondary-fill)',
54
+ tertiarySystemFill: 'var(--sb-color-tertiary-fill)',
55
+ quaternarySystemFill: 'var(--sb-color-quaternary-fill)',
44
56
  clear: 'transparent',
57
+ // `.tint` as a style: the nearest `.tint(_:)` (styled `tint` sets `--sb-tint`), else the accent.
58
+ tint: 'var(--sb-tint, var(--sb-color-accent))',
45
59
  };
46
60
 
47
61
  /** CSS `<color>` for a protocol `Color`. */
@@ -156,6 +170,10 @@ export function styleToCSS(style: ShapeStyle, role: PaintRole): PaintCSS {
156
170
  const { stops, center, startRadius, endRadius } = style.radialGradient;
157
171
  return gradient(radialGradientCSS(stops ?? [], center, startRadius, endRadius, opacity), stops, opacity);
158
172
  }
173
+ if (isAngularGradientStyle(style)) {
174
+ const { stops, center, startAngle, endAngle } = style.angularGradient;
175
+ return gradient(angularGradientCSS(stops ?? [], center, startAngle, endAngle, opacity), stops, opacity);
176
+ }
159
177
  if (isColorGradientStyle(style)) {
160
178
  const base = colorToCSS(style.colorGradient);
161
179
  const light = withOpacity(`color-mix(in srgb, ${base}, white 12%)`, opacity);
@@ -230,6 +248,27 @@ export function radialGradientCSS(
230
248
  return `radial-gradient(circle ${size} at ${round(cx * 100)}% ${round(cy * 100)}%, ${list})`;
231
249
  }
232
250
 
251
+ /**
252
+ * `conic-gradient(from <angle> at <cx>% <cy>%, ...)`. SwiftUI measures angles
253
+ * clockwise from 3 o'clock and CSS from 12 o'clock, so the CSS start is the
254
+ * start angle plus 90°; the stops span start...end as a share of the turn.
255
+ */
256
+ export function angularGradientCSS(
257
+ stops: readonly GradientStop[],
258
+ center: UnitPoint | undefined,
259
+ startAngle: number | undefined,
260
+ endAngle: number | undefined,
261
+ opacity?: number,
262
+ ): string {
263
+ const cx = unit(center?.x, 0.5);
264
+ const cy = unit(center?.y, 0.5);
265
+ const start = unit(startAngle, 0);
266
+ const end = unit(endAngle, start + 360);
267
+ const span = end === start ? 360 : end - start;
268
+ const list = stopsCSS(stops, (location) => (location * span) / 360, opacity);
269
+ return `conic-gradient(from ${round(start + 90)}deg at ${round(cx * 100)}% ${round(cy * 100)}%, ${list})`;
270
+ }
271
+
233
272
  /**
234
273
  * `color pct%, ...`, with `map` turning a stop location into a 0...1 position
235
274
  * and `opacity` multiplying every stop. Degenerate lists are padded to two stops.
@@ -325,6 +364,24 @@ export function effectsCSS(style: Style): EffectsCSS {
325
364
  if (style.blur != null && Number.isFinite(style.blur) && style.blur > 0) {
326
365
  filters.push(`blur(${round(style.blur)}px)`);
327
366
  }
367
+ // `.brightness` (SwiftUI adds the amount; CSS scales by 1 + amount),
368
+ // `.saturation`, `.contrast`, `.grayscale`, `.hueRotation`, `.colorInvert`.
369
+ if (style.brightness != null && Number.isFinite(style.brightness) && style.brightness !== 0) {
370
+ filters.push(`brightness(${round(Math.max(0, 1 + style.brightness))})`);
371
+ }
372
+ if (style.saturation != null && Number.isFinite(style.saturation) && style.saturation !== 1) {
373
+ filters.push(`saturate(${round(Math.max(0, style.saturation))})`);
374
+ }
375
+ if (style.contrast != null && Number.isFinite(style.contrast) && style.contrast !== 1) {
376
+ filters.push(`contrast(${round(Math.max(0, style.contrast))})`);
377
+ }
378
+ if (style.grayscale != null && Number.isFinite(style.grayscale) && style.grayscale > 0) {
379
+ filters.push(`grayscale(${round(Math.min(1, style.grayscale))})`);
380
+ }
381
+ if (style.hueRotation != null && Number.isFinite(style.hueRotation) && style.hueRotation !== 0) {
382
+ filters.push(`hue-rotate(${round(style.hueRotation)}deg)`);
383
+ }
384
+ if (style.colorInvert === true) filters.push('invert(1)');
328
385
  if (style.shadow) {
329
386
  const { color, radius, x, y } = style.shadow;
330
387
  const css = color ? colorToCSS(color) : 'rgba(0, 0, 0, 0.33)';
@@ -355,6 +412,10 @@ export function maskImageCSS(style: ShapeStyle): string {
355
412
  const { stops, center, startRadius, endRadius } = style.radialGradient;
356
413
  return radialGradientCSS(stops ?? [], center, startRadius, endRadius, style.opacity);
357
414
  }
415
+ if (isAngularGradientStyle(style)) {
416
+ const { stops, center, startAngle, endAngle } = style.angularGradient;
417
+ return angularGradientCSS(stops ?? [], center, startAngle, endAngle, style.opacity);
418
+ }
358
419
  if (isColorGradientStyle(style)) return styleToCSS(style, 'paint').value;
359
420
  const alpha = isColor(style) ? colorAlpha(style) : clamp01(style.opacity ?? 1);
360
421
  const solid = `rgba(0, 0, 0, ${round(alpha)})`;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Phase 18: SwiftData's stores in the browser's IndexedDB.
3
+ *
4
+ * The app keeps its models in memory (Sources/SwiftData) and the page keeps
5
+ * them across launches: before the module starts, every record of the app's
6
+ * database is read and handed in as `SB_SWIFTDATA`
7
+ * (`{"<store>": [{id, entity, values}, …]}`), and each `swiftdata` op (one
8
+ * save) is written back in one transaction. One database per app,
9
+ * `sb-swiftdata:<App>`, with one object store, `records`, keyed by
10
+ * `[store, id]`. See docs/ops-protocol.md, "Phase 18".
11
+ */
12
+
13
+ import type { SwiftDataOp, SwiftDataRecord } from './protocol';
14
+
15
+ /** Every store of an app: store name → its records, in key order. */
16
+ export type SwiftDataStores = Record<string, SwiftDataRecord[]>;
17
+
18
+ /** What is kept in the `records` object store. */
19
+ interface StoredRecord extends SwiftDataRecord {
20
+ store: string;
21
+ }
22
+
23
+ const RECORDS = 'records';
24
+
25
+ export function databaseName(app: string): string {
26
+ return `sb-swiftdata:${app}`;
27
+ }
28
+
29
+ /** `stores` with one save applied, as the database applies it (for tests and the headless runner). */
30
+ export function applySwiftDataOp(stores: SwiftDataStores, op: SwiftDataOp): SwiftDataStores {
31
+ const records = new Map<number, SwiftDataRecord>();
32
+ if (!op.clear) for (const record of stores[op.store] ?? []) records.set(record.id, record);
33
+ for (const id of op.delete) records.delete(id);
34
+ for (const record of op.put) records.set(record.id, { id: record.id, entity: record.entity, values: record.values });
35
+ return { ...stores, [op.store]: [...records.values()].sort((a, b) => a.id - b.id) };
36
+ }
37
+
38
+ /** The stores as the module reads them from `SB_SWIFTDATA`; null when there is nothing to hand in. */
39
+ export function launchValue(stores: SwiftDataStores): string | null {
40
+ const nonEmpty = Object.entries(stores).filter(([, records]) => records.length > 0);
41
+ if (nonEmpty.length === 0) return null;
42
+ return JSON.stringify(Object.fromEntries(nonEmpty));
43
+ }
44
+
45
+ /** Groups stored records by store, each in key order. */
46
+ export function groupRecords(rows: readonly StoredRecord[]): SwiftDataStores {
47
+ const stores: SwiftDataStores = {};
48
+ for (const row of rows) {
49
+ (stores[row.store] ??= []).push({ id: row.id, entity: row.entity, values: row.values });
50
+ }
51
+ for (const records of Object.values(stores)) records.sort((a, b) => a.id - b.id);
52
+ return stores;
53
+ }
54
+
55
+ /** Where an app's SwiftData lives between launches. */
56
+ export interface SwiftDataPersistence {
57
+ /** Every store, read once before the module starts. */
58
+ load(): Promise<SwiftDataStores>;
59
+ /** Writes one save; saves are written in the order they were made. */
60
+ apply(op: SwiftDataOp): Promise<void>;
61
+ /** Deletes the app's data (every store). */
62
+ clear(): Promise<void>;
63
+ }
64
+
65
+ function request<T>(req: IDBRequest<T>): Promise<T> {
66
+ return new Promise((resolve, reject) => {
67
+ req.onsuccess = () => resolve(req.result);
68
+ req.onerror = () => reject(req.error ?? new Error('IndexedDB request failed'));
69
+ });
70
+ }
71
+
72
+ function done(transaction: IDBTransaction): Promise<void> {
73
+ return new Promise((resolve, reject) => {
74
+ transaction.oncomplete = () => resolve();
75
+ transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB transaction aborted'));
76
+ transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB transaction failed'));
77
+ });
78
+ }
79
+
80
+ /** The app's database in IndexedDB (`factory` is the browser's unless a test passes one). */
81
+ export function indexedDBPersistence(app: string, factory: IDBFactory = indexedDB): SwiftDataPersistence {
82
+ let opened: Promise<IDBDatabase> | null = null;
83
+ const database = (): Promise<IDBDatabase> => {
84
+ opened ??= new Promise((resolve, reject) => {
85
+ const open = factory.open(databaseName(app), 1);
86
+ open.onupgradeneeded = () => {
87
+ if (!open.result.objectStoreNames.contains(RECORDS)) open.result.createObjectStore(RECORDS, { keyPath: ['store', 'id'] });
88
+ };
89
+ open.onsuccess = () => resolve(open.result);
90
+ open.onerror = () => reject(open.error ?? new Error(`could not open ${databaseName(app)}`));
91
+ open.onblocked = () => reject(new Error(`${databaseName(app)} is blocked by another tab`));
92
+ });
93
+ return opened;
94
+ };
95
+ // Saves are chained so that each one's transaction starts after the
96
+ // previous one's was created, whatever the database's opening takes.
97
+ let queue: Promise<void> = Promise.resolve();
98
+
99
+ return {
100
+ async load() {
101
+ const db = await database();
102
+ const transaction = db.transaction(RECORDS, 'readonly');
103
+ const rows = await request(transaction.objectStore(RECORDS).getAll() as IDBRequest<StoredRecord[]>);
104
+ return groupRecords(rows);
105
+ },
106
+ apply(op) {
107
+ queue = queue.then(async () => {
108
+ const db = await database();
109
+ const transaction = db.transaction(RECORDS, 'readwrite');
110
+ const records = transaction.objectStore(RECORDS);
111
+ if (op.clear) records.delete(IDBKeyRange.bound([op.store, -Infinity], [op.store, Infinity]));
112
+ for (const id of op.delete) records.delete([op.store, id]);
113
+ for (const record of op.put) {
114
+ const row: StoredRecord = { store: op.store, id: record.id, entity: record.entity, values: record.values };
115
+ records.put(row);
116
+ }
117
+ await done(transaction);
118
+ });
119
+ return queue;
120
+ },
121
+ async clear() {
122
+ const db = await database();
123
+ const transaction = db.transaction(RECORDS, 'readwrite');
124
+ transaction.objectStore(RECORDS).clear();
125
+ await done(transaction);
126
+ },
127
+ };
128
+ }