@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.
- package/README.md +141 -63
- package/app/index.html +21 -10
- package/node/dist/node/apps.d.ts +16 -0
- package/node/dist/node/apps.d.ts.map +1 -1
- package/node/dist/node/apps.js +9 -0
- package/node/dist/node/apps.js.map +1 -1
- package/node/dist/node/config.d.ts +14 -1
- package/node/dist/node/config.d.ts.map +1 -1
- package/node/dist/node/config.js +20 -3
- package/node/dist/node/config.js.map +1 -1
- package/node/dist/node/examples.d.ts +2 -1
- package/node/dist/node/examples.d.ts.map +1 -1
- package/node/dist/node/examples.js +12 -2
- package/node/dist/node/examples.js.map +1 -1
- package/node/dist/plugins/app-manifests.d.ts +4 -1
- package/node/dist/plugins/app-manifests.d.ts.map +1 -1
- package/node/dist/plugins/app-manifests.js +3 -2
- package/node/dist/plugins/app-manifests.js.map +1 -1
- package/node/dist/plugins/asset-catalogs.d.ts +15 -3
- package/node/dist/plugins/asset-catalogs.d.ts.map +1 -1
- package/node/dist/plugins/asset-catalogs.js +111 -13
- package/node/dist/plugins/asset-catalogs.js.map +1 -1
- package/node/dist/plugins/included-sites.d.ts +26 -0
- package/node/dist/plugins/included-sites.d.ts.map +1 -0
- package/node/dist/plugins/included-sites.js +70 -0
- package/node/dist/plugins/included-sites.js.map +1 -0
- package/node/dist/plugins/pdfImages.d.ts +27 -0
- package/node/dist/plugins/pdfImages.d.ts.map +1 -0
- package/node/dist/plugins/pdfImages.js +145 -0
- package/node/dist/plugins/pdfImages.js.map +1 -0
- package/node/dist/plugins/swift-wasm.d.ts +19 -1
- package/node/dist/plugins/swift-wasm.d.ts.map +1 -1
- package/node/dist/plugins/swift-wasm.js +45 -7
- package/node/dist/plugins/swift-wasm.js.map +1 -1
- package/node/dist/plugins/symbolTemplates.d.ts +22 -0
- package/node/dist/plugins/symbolTemplates.d.ts.map +1 -0
- package/node/dist/plugins/symbolTemplates.js +83 -0
- package/node/dist/plugins/symbolTemplates.js.map +1 -0
- package/package.json +4 -2
- package/plugins/app-manifests.ts +3 -2
- package/plugins/asset-catalogs.ts +119 -14
- package/plugins/included-sites.ts +74 -0
- package/plugins/pdfImages.ts +163 -0
- package/plugins/swift-wasm.ts +52 -6
- package/plugins/symbolTemplates.ts +88 -0
- package/public/_headers +7 -0
- package/public/app/oauth-callback.html +62 -0
- package/src/animation.ts +235 -8
- package/src/appFiles.ts +201 -0
- package/src/base64.ts +43 -0
- package/src/bitmapPlan.ts +143 -0
- package/src/bitmapStore.ts +206 -0
- package/src/bitmapWorker.ts +41 -0
- package/src/bitmapWorkerProtocol.ts +15 -0
- package/src/bitmaps.ts +283 -0
- package/src/blockedHosts.ts +29 -0
- package/src/canvasDrawing.ts +361 -0
- package/src/chartDom.ts +2 -2
- package/src/chromeColor.ts +179 -0
- package/src/clipboard.ts +78 -0
- package/src/colorMultiply.ts +93 -0
- package/src/colorPicker.ts +44 -0
- package/src/corsProxy.ts +45 -0
- package/src/gaugeRing.ts +74 -0
- package/src/gif.ts +297 -0
- package/src/hostRequests.ts +426 -0
- package/src/imageOps.ts +592 -0
- package/src/interaction.ts +68 -3
- package/src/landing.ts +2 -2
- package/src/largeTitle.ts +79 -0
- package/src/layout/chart.ts +9 -3
- package/src/layout/engine.ts +681 -174
- package/src/layout/index.ts +3 -1
- package/src/layout/text.ts +217 -1
- package/src/layout/types.ts +31 -0
- package/src/layout/typography.ts +24 -7
- package/src/layout/video.ts +24 -0
- package/src/layoutDom.ts +66 -6
- package/src/main.ts +127 -4
- package/src/media.ts +132 -0
- package/src/mock.ts +4 -3
- package/src/notifications.ts +109 -0
- package/src/pathShape.ts +156 -0
- package/src/protocol.ts +782 -13
- package/src/renderer.ts +603 -44
- package/src/richText.ts +65 -0
- package/src/scanner.ts +234 -0
- package/src/scrollTo.ts +123 -0
- package/src/styles.css +521 -100
- package/src/styles.ts +61 -0
- package/src/swiftData.ts +128 -0
- package/src/symbols.ts +658 -28
- package/src/translation.ts +126 -0
- package/src/video.ts +299 -0
- package/src/vite-env.d.ts +6 -0
- package/src/wasm.ts +227 -32
- package/src/webAuth.ts +267 -0
- package/src/webSocket.ts +148 -0
- 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)})`;
|
package/src/swiftData.ts
ADDED
|
@@ -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
|
+
}
|