@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/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
+ };