@swiftbrowser/web 0.19.0 → 0.21.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/app/index.html +11 -7
- package/package.json +1 -1
- package/src/appFiles.ts +12 -3
- package/src/audioEngine.ts +29 -5
- package/src/deviceFrame.ts +188 -0
- package/src/dragDrop.ts +66 -10
- package/src/files.ts +68 -13
- package/src/hostRequests.ts +43 -3
- package/src/interaction.ts +6 -1
- package/src/layout/engine.ts +2 -0
- package/src/main.ts +12 -1
- package/src/page.css +65 -0
- package/src/pinnedViews.ts +38 -1
- package/src/protocol.ts +41 -1
- package/src/renderer.ts +132 -10
- package/src/scrollTransition.ts +2 -1
- package/src/styles.css +167 -7
- package/src/styles.ts +9 -6
- package/src/viewRegistry.ts +58 -0
package/src/styles.css
CHANGED
|
@@ -326,12 +326,6 @@
|
|
|
326
326
|
fill: currentColor;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
.glyph-wifi .wifi-band {
|
|
330
|
-
fill: none;
|
|
331
|
-
stroke: currentColor;
|
|
332
|
-
stroke-linecap: round;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
329
|
.glyph-battery .battery-shell {
|
|
336
330
|
fill: none;
|
|
337
331
|
stroke: currentColor;
|
|
@@ -386,6 +380,86 @@
|
|
|
386
380
|
}
|
|
387
381
|
}
|
|
388
382
|
|
|
383
|
+
/* ---------------------------------------------------------------------------
|
|
384
|
+
The preview's device (html[data-sb-frame], src/deviceFrame.ts), frame mode
|
|
385
|
+
only: the iPhone above is the default; an iPad in landscape (an 11-inch
|
|
386
|
+
iPad's 1180 x 820 points, its status bar the time and date at the leading
|
|
387
|
+
edge, no island); or no device, the app filling the page under the toolbar
|
|
388
|
+
(page.css sizes the stage).
|
|
389
|
+
------------------------------------------------------------------------- */
|
|
390
|
+
|
|
391
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) {
|
|
392
|
+
--device-screen-width: 1180px;
|
|
393
|
+
--device-screen-height: 820px;
|
|
394
|
+
--device-bezel: 20px;
|
|
395
|
+
--device-radius: 38px;
|
|
396
|
+
--device-safe-top: 24px;
|
|
397
|
+
--device-safe-bottom: 20px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .dynamic-island,
|
|
401
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .device::before,
|
|
402
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .device::after {
|
|
403
|
+
display: none;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .status-bar {
|
|
407
|
+
padding: 0 18px 0 22px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .status-time {
|
|
411
|
+
min-width: 0;
|
|
412
|
+
top: 0;
|
|
413
|
+
font-size: 13px;
|
|
414
|
+
letter-spacing: 0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.status-date {
|
|
418
|
+
display: none;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .status-date {
|
|
422
|
+
display: inline;
|
|
423
|
+
margin-left: 6px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .status-glyphs {
|
|
427
|
+
gap: 6px;
|
|
428
|
+
transform: scale(0.82);
|
|
429
|
+
transform-origin: right center;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
html[data-sb-frame="ipad"]:not([data-sb-mode="device"]) .home-indicator {
|
|
433
|
+
bottom: 7px;
|
|
434
|
+
width: 315px;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
html[data-sb-frame="none"]:not([data-sb-mode="device"]) .device:not(.sb-island) {
|
|
438
|
+
flex: 1 1 auto;
|
|
439
|
+
align-self: stretch;
|
|
440
|
+
width: auto;
|
|
441
|
+
height: auto;
|
|
442
|
+
min-height: 0;
|
|
443
|
+
padding: 0;
|
|
444
|
+
border-radius: 0;
|
|
445
|
+
background: var(--sb-color-system-background);
|
|
446
|
+
box-shadow: none;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
html[data-sb-frame="none"]:not([data-sb-mode="device"]) .device-screen {
|
|
450
|
+
width: 100%;
|
|
451
|
+
height: 100%;
|
|
452
|
+
border-radius: 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
html[data-sb-frame="none"]:not([data-sb-mode="device"]) .device::before,
|
|
456
|
+
html[data-sb-frame="none"]:not([data-sb-mode="device"]) .device::after,
|
|
457
|
+
html[data-sb-frame="none"]:not([data-sb-mode="device"]) .dynamic-island,
|
|
458
|
+
html[data-sb-frame="none"]:not([data-sb-mode="device"]) .status-bar,
|
|
459
|
+
html[data-sb-frame="none"]:not([data-sb-mode="device"]) .home-indicator {
|
|
460
|
+
display: none;
|
|
461
|
+
}
|
|
462
|
+
|
|
389
463
|
/* ---------------------------------------------------------------------------
|
|
390
464
|
Device mode (html[data-sb-mode="device"], see src/deviceMode.ts)
|
|
391
465
|
|
|
@@ -761,6 +835,24 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
761
835
|
display: none;
|
|
762
836
|
}
|
|
763
837
|
|
|
838
|
+
/* Pinned section headers (src/pinnedViews.ts): each scroll view names its
|
|
839
|
+
scroll timelines, and a header moves along its nearest one, from where it
|
|
840
|
+
starts to pin (its `animation-range`) by as far as its section lets it
|
|
841
|
+
(`--sb-pinned-to`), in step with the scroll. */
|
|
842
|
+
.sb-scrollview,
|
|
843
|
+
.sb-list {
|
|
844
|
+
scroll-timeline: --sb-pin-y block, --sb-pin-x inline;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
@keyframes sb-pinned {
|
|
848
|
+
from {
|
|
849
|
+
translate: 0 0;
|
|
850
|
+
}
|
|
851
|
+
to {
|
|
852
|
+
translate: var(--sb-pinned-to);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
764
856
|
.sb-scrollview[data-sb-axes="vertical"] {
|
|
765
857
|
overflow-y: auto;
|
|
766
858
|
}
|
|
@@ -1050,6 +1142,24 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1050
1142
|
overflow: visible;
|
|
1051
1143
|
}
|
|
1052
1144
|
|
|
1145
|
+
/* A section outside a list (a lazy stack's, its header pinned) is a plain
|
|
1146
|
+
container, as in SwiftUI: its rows draw in no card, so nothing clips them,
|
|
1147
|
+
paints under them or draws lines between them (src/layoutDom.ts leaves the
|
|
1148
|
+
slot unsized there). */
|
|
1149
|
+
:not(.sb-list-content) > .sb-section > .sb-section-rows {
|
|
1150
|
+
overflow: visible;
|
|
1151
|
+
background: none;
|
|
1152
|
+
border-radius: 0;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
:not(.sb-list-content) > .sb-section > .sb-section-rows > :is([data-sb-id], [data-sb-ghost]) {
|
|
1156
|
+
background: none;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
:not(.sb-list-content) > .sb-section > .sb-section-rows > *::after {
|
|
1160
|
+
content: none;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1053
1163
|
.sb-list-content > :not(.sb-section):first-child,
|
|
1054
1164
|
.sb-list-content > .sb-section + :not(.sb-section) {
|
|
1055
1165
|
border-top-left-radius: 26px;
|
|
@@ -3513,9 +3623,16 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
3513
3623
|
opacity: 0.4;
|
|
3514
3624
|
}
|
|
3515
3625
|
|
|
3626
|
+
/* The copy lets the pointer through to the destination under it, its own
|
|
3627
|
+
boxes too: a background or a content shape makes a box a target
|
|
3628
|
+
(pointer-events: auto) wherever it is, and the copy keeps those. */
|
|
3629
|
+
.sb-drag-preview,
|
|
3630
|
+
.sb-drag-preview * {
|
|
3631
|
+
pointer-events: none !important;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3516
3634
|
.sb-drag-preview {
|
|
3517
3635
|
z-index: 60;
|
|
3518
|
-
pointer-events: none;
|
|
3519
3636
|
opacity: 0.9;
|
|
3520
3637
|
filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.25));
|
|
3521
3638
|
will-change: transform;
|
|
@@ -4385,3 +4502,46 @@ svg.sb-path {
|
|
|
4385
4502
|
.sb-list[data-sb-list-style='sidebar'] .sb-section[data-sb-collapsible][data-sb-expanded] .sb-sectionheader::after {
|
|
4386
4503
|
transform: rotate(45deg);
|
|
4387
4504
|
}
|
|
4505
|
+
|
|
4506
|
+
/* A registered view (src/viewRegistry.ts): the package's own element fills
|
|
4507
|
+
the box; the element's children are a native view's to place. */
|
|
4508
|
+
.sb-registered {
|
|
4509
|
+
overflow: hidden;
|
|
4510
|
+
}
|
|
4511
|
+
.sb-registered > .sb-registered-content {
|
|
4512
|
+
display: block;
|
|
4513
|
+
width: 100%;
|
|
4514
|
+
height: 100%;
|
|
4515
|
+
}
|
|
4516
|
+
.sb-registered > :not(.sb-registered-content) {
|
|
4517
|
+
display: none;
|
|
4518
|
+
}
|
|
4519
|
+
.sb-registered-missing {
|
|
4520
|
+
box-sizing: border-box;
|
|
4521
|
+
border: 1px dashed currentColor;
|
|
4522
|
+
font: 11px/1.2 ui-monospace, monospace;
|
|
4523
|
+
opacity: 0.6;
|
|
4524
|
+
padding: 2px;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
/* SwiftUI's `.shadow` on a container falls on each view it draws, not on the
|
|
4528
|
+
container as a whole (renderer.ts castShadows): a view that draws itself
|
|
4529
|
+
casts the shadows of the boxes around it, set on them as
|
|
4530
|
+
`--sb-shadow-filter`, and hands none to what is inside it; nor does a
|
|
4531
|
+
`.compositingGroup()`, which casts them itself, as one. */
|
|
4532
|
+
:is([data-sb-kind="text"], [data-sb-kind="image"], [data-sb-kind="divider"], [data-sb-kind="color"], [data-sb-kind="shape"],
|
|
4533
|
+
[data-sb-kind="textfield"], [data-sb-kind="toggle"], [data-sb-kind="picker"], [data-sb-kind="gauge"],
|
|
4534
|
+
[data-sb-kind="searchfield"], [data-sb-kind="registered"], [data-sb-kind="chart"], [data-sb-kind="stepper"],
|
|
4535
|
+
[data-sb-kind="slider"], [data-sb-kind="datepicker"], [data-sb-kind="texteditor"], [data-sb-kind="video"],
|
|
4536
|
+
[data-sb-kind="canvas"]) {
|
|
4537
|
+
filter: var(--sb-shadow-filter, none);
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
:is([data-sb-kind="text"], [data-sb-kind="image"], [data-sb-kind="divider"], [data-sb-kind="color"], [data-sb-kind="shape"],
|
|
4541
|
+
[data-sb-kind="textfield"], [data-sb-kind="toggle"], [data-sb-kind="picker"], [data-sb-kind="gauge"],
|
|
4542
|
+
[data-sb-kind="searchfield"], [data-sb-kind="registered"], [data-sb-kind="chart"], [data-sb-kind="stepper"],
|
|
4543
|
+
[data-sb-kind="slider"], [data-sb-kind="datepicker"], [data-sb-kind="texteditor"], [data-sb-kind="video"],
|
|
4544
|
+
[data-sb-kind="canvas"]) *,
|
|
4545
|
+
[data-sb-composite] > * {
|
|
4546
|
+
--sb-shadow-filter: initial;
|
|
4547
|
+
}
|
package/src/styles.ts
CHANGED
|
@@ -352,7 +352,7 @@ export const PERSPECTIVE_BASE_PX = 500;
|
|
|
352
352
|
* `500px / perspective` (a smaller SwiftUI perspective is a flatter
|
|
353
353
|
* projection, as `perspective: 0.5` is in SwiftUI).
|
|
354
354
|
*/
|
|
355
|
-
export function effectsCSS(style: Style): EffectsCSS {
|
|
355
|
+
export function effectsCSS(style: Style, options: { shadow?: boolean } = {}): EffectsCSS {
|
|
356
356
|
const out: EffectsCSS = {};
|
|
357
357
|
const transforms: string[] = [];
|
|
358
358
|
if (style.offset) {
|
|
@@ -408,11 +408,7 @@ export function effectsCSS(style: Style): EffectsCSS {
|
|
|
408
408
|
filters.push(`hue-rotate(${round(style.hueRotation)}deg)`);
|
|
409
409
|
}
|
|
410
410
|
if (style.colorInvert === true) filters.push('invert(1)');
|
|
411
|
-
if (style.shadow) {
|
|
412
|
-
const { color, radius, x, y } = style.shadow;
|
|
413
|
-
const css = color ? colorToCSS(color) : 'rgba(0, 0, 0, 0.33)';
|
|
414
|
-
filters.push(`drop-shadow(${finite(x)}px ${finite(y)}px ${Math.max(0, finite(radius))}px ${css})`);
|
|
415
|
-
}
|
|
411
|
+
if (style.shadow && options.shadow !== false) filters.push(`drop-shadow(${shadowCSS(style.shadow)})`);
|
|
416
412
|
if (filters.length) out.filter = filters.join(' ');
|
|
417
413
|
// Phase 7.
|
|
418
414
|
if (style.hidden === true) out.hidden = true;
|
|
@@ -420,6 +416,13 @@ export function effectsCSS(style: Style): EffectsCSS {
|
|
|
420
416
|
return out;
|
|
421
417
|
}
|
|
422
418
|
|
|
419
|
+
/** A shadow's `x y radius color`, as `drop-shadow()` and `box-shadow` both take it. */
|
|
420
|
+
export function shadowCSS(shadow: NonNullable<Style['shadow']>): string {
|
|
421
|
+
const { color, radius, x, y } = shadow;
|
|
422
|
+
const css = color ? colorToCSS(color) : 'rgba(0, 0, 0, 0.33)';
|
|
423
|
+
return `${finite(x)}px ${finite(y)}px ${Math.max(0, finite(radius))}px ${css}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
423
426
|
/**
|
|
424
427
|
* Phase 7 `.mask { }` with a ShapeStyle: a CSS `mask-image` whose alpha is
|
|
425
428
|
* the style's. A `linearGradient` / `radialGradient` becomes the matching CSS
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's half of `ViewRegistry` (docs/ops-protocol.md, "Registered
|
|
3
|
+
* views"): a view a package draws itself, by name, which the `registered`
|
|
4
|
+
* element kind names. A native app registers a SwiftUI view under the same
|
|
5
|
+
* name (SwiftBrowserNative's `ViewRegistry`), so one component
|
|
6
|
+
* (`RegisteredView` in @swiftbrowser/react) is a SwiftUI view in the app and
|
|
7
|
+
* the package's own DOM on a page, drawing what never crosses the bridge
|
|
8
|
+
* (an audio node's frames, a map's tiles).
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* ViewRegistry.register('Meter', (context) => {
|
|
12
|
+
* const element = document.createElement('canvas');
|
|
13
|
+
* return { element, update: (props) => …, dispose: () => … };
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** What a builder gets: the element's id, its props as the app wrote them, and a way to answer (`onEvent`). */
|
|
19
|
+
export interface RegisteredViewContext {
|
|
20
|
+
readonly id: number;
|
|
21
|
+
readonly props: Record<string, unknown>;
|
|
22
|
+
/** Sends a value to the element's `onEvent` in the app. */
|
|
23
|
+
send(value: unknown): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A built view: its element, which fills the box the layout gives it, and what to do as props change or it goes. */
|
|
27
|
+
export interface RegisteredViewInstance {
|
|
28
|
+
readonly element: HTMLElement;
|
|
29
|
+
update?(props: Record<string, unknown>): void;
|
|
30
|
+
dispose?(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RegisteredViewBuilder = (context: RegisteredViewContext) => RegisteredViewInstance | HTMLElement;
|
|
34
|
+
|
|
35
|
+
const builders = new Map<string, RegisteredViewBuilder>();
|
|
36
|
+
|
|
37
|
+
export const ViewRegistry = {
|
|
38
|
+
/** Registers `builder` under `name`, replacing one registered before; elements made after use it. */
|
|
39
|
+
register(name: string, builder: RegisteredViewBuilder): void {
|
|
40
|
+
builders.set(name, builder);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
unregister(name: string): void {
|
|
44
|
+
builders.delete(name);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
isRegistered(name: string): boolean {
|
|
48
|
+
return builders.has(name);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/** The view registered under `name`, built; undefined when there is none. */
|
|
52
|
+
make(name: string, context: RegisteredViewContext): RegisteredViewInstance | undefined {
|
|
53
|
+
const builder = builders.get(name);
|
|
54
|
+
if (!builder) return undefined;
|
|
55
|
+
const made = builder(context);
|
|
56
|
+
return made instanceof HTMLElement ? { element: made } : made;
|
|
57
|
+
},
|
|
58
|
+
};
|