@swiftbrowser/web 0.18.3 → 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.
@@ -10,6 +10,12 @@
10
10
  * scroll event each header is moved by a transform along the axis. The
11
11
  * laid-out positions are left alone (offsets, not client rects), so the
12
12
  * transform never feeds back into where the header is reckoned to be.
13
+ *
14
+ * Where the browser has scroll-driven animations, a header is instead moved
15
+ * by one (styles.css `sb-pinned`) along its scroll view's timeline, from the
16
+ * scroll offset where it starts to pin to the one where its section's end
17
+ * stops it. The browser then moves it in step with the scroll itself, where a
18
+ * scroll event, a frame late on iOS, would leave it jittering.
13
19
  */
14
20
 
15
21
  import type { PinnedView } from './protocol';
@@ -43,6 +49,9 @@ export function pinnedFooterOffset(viewEnd: number, footer: Span, sectionStart:
43
49
 
44
50
  const registered = new Map<HTMLElement, Set<PinnedView>>();
45
51
 
52
+ /** Whether headers pin by a scroll-driven animation (styles.css), not by script on each scroll event. */
53
+ const scrollDriven = typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('animation-timeline', '--sb-pin-y');
54
+
46
55
  /** Called by the renderer for every stack and grid: registers, updates or forgets its pinned views. */
47
56
  export function setPinnedViews(el: HTMLElement, pinned: PinnedView[] | undefined): void {
48
57
  const views = new Set<PinnedView>(Array.isArray(pinned) ? pinned.filter((view) => view === 'sectionHeaders' || view === 'sectionFooters') : []);
@@ -55,6 +64,7 @@ export function setPinnedViews(el: HTMLElement, pinned: PinnedView[] | undefined
55
64
  if (view) {
56
65
  view.style.transform = '';
57
66
  view.style.zIndex = '';
67
+ unpin(view);
58
68
  }
59
69
  }
60
70
  }
@@ -98,7 +108,8 @@ function draw(stack: HTMLElement, views: Set<PinnedView>): void {
98
108
  if (header) {
99
109
  const at = layoutOrigin(header, scroller);
100
110
  const span: Span = { start: horizontal ? (at?.x ?? sectionStart) : (at?.y ?? sectionStart), length: horizontal ? header.offsetWidth : header.offsetHeight };
101
- move(header, horizontal, pinnedHeaderOffset(viewStart, span, sectionEnd));
111
+ if (scrollDriven) pin(header, horizontal, span.start - (horizontal ? 0 : topInset(scroller)), pinnedHeaderOffset(Infinity, span, sectionEnd));
112
+ else move(header, horizontal, pinnedHeaderOffset(viewStart, span, sectionEnd));
102
113
  }
103
114
  }
104
115
  if (views.has('sectionFooters')) {
@@ -119,6 +130,32 @@ function move(view: HTMLElement, horizontal: boolean, offset: number): void {
119
130
  view.style.zIndex = '1';
120
131
  }
121
132
 
133
+ /**
134
+ * Pins a header by a scroll-driven animation: from the scroll offset `from`, where its view's start reaches it, it
135
+ * moves with the scroll for `room` and stays there. The scroll view names its timelines (styles.css).
136
+ */
137
+ function pin(view: HTMLElement, horizontal: boolean, from: number, room: number): void {
138
+ view.style.transform = '';
139
+ view.style.zIndex = '1';
140
+ if (room <= 0) {
141
+ unpin(view);
142
+ return;
143
+ }
144
+ const start = Math.round(from * 100) / 100;
145
+ const end = Math.round((from + room) * 100) / 100;
146
+ view.style.animation = 'sb-pinned linear both';
147
+ view.style.setProperty('animation-timeline', horizontal ? '--sb-pin-x' : '--sb-pin-y');
148
+ view.style.setProperty('animation-range', `${start}px ${end}px`);
149
+ view.style.setProperty('--sb-pinned-to', horizontal ? `${room}px 0` : `0 ${room}px`);
150
+ }
151
+
152
+ function unpin(view: HTMLElement): void {
153
+ view.style.animation = '';
154
+ view.style.removeProperty('animation-timeline');
155
+ view.style.removeProperty('animation-range');
156
+ view.style.removeProperty('--sb-pinned-to');
157
+ }
158
+
122
159
  /** Moves every pinned view to its place: after a layout, and on every scroll event. */
123
160
  export function drawPinnedViews(): void {
124
161
  for (const [stack, views] of registered) {
package/src/protocol.ts CHANGED
@@ -359,6 +359,8 @@ export interface Style {
359
359
  animationToken?: number;
360
360
  /** This box is a `Label`'s icon: a list row tints it (its title keeps the row's color). */
361
361
  labelIcon?: boolean;
362
+ /** `.textSelection(.enabled)`: the text below can be selected (iOS shows none selectable otherwise). */
363
+ textSelection?: boolean;
362
364
  /** How this subtree appears and disappears inside an animated commit. */
363
365
  transition?: Transition;
364
366
  // Phase 5
@@ -376,6 +378,11 @@ export interface Style {
376
378
  /** Unit point the scale grows from (default 0.5/0.5). */
377
379
  scaleAnchor?: UnitPoint;
378
380
  shadow?: Shadow;
381
+ /**
382
+ * `.compositingGroup()`: the box is drawn as one layer before the effects around it apply, so a `shadow` (here or
383
+ * on a box around it) is cast by the box as a whole rather than by each view it draws.
384
+ */
385
+ compositingGroup?: boolean;
379
386
  /** `.clipped()`: `overflow: hidden` on the box. */
380
387
  clipped?: boolean;
381
388
 
@@ -612,13 +619,21 @@ export interface ScrollViewProps {
612
619
  * Phase 4: `insetGrouped` (the default when omitted, as Phase 2 modules send
613
620
  * `{}`), `grouped` (full-width sections with hairlines; `Form`) or `plain`.
614
621
  */
615
- export type ListStyle = 'plain' | 'insetGrouped' | 'grouped';
622
+ /** `sidebar`: a `NavigationSplitView`'s sidebar list (and `.listStyle(.sidebar)`): rows on the background, collapsible sections. */
623
+ export type ListStyle = 'plain' | 'insetGrouped' | 'grouped' | 'sidebar';
616
624
  export interface ListProps {
617
625
  style?: ListStyle;
618
626
  }
619
627
  export interface SectionProps {
620
628
  header: string | null;
621
629
  footer: string | null;
630
+ /** `Section(isExpanded:)`: the section collapses in a sidebar-style list (as on iOS); a `tap` on it toggles. The rows come whatever the state. */
631
+ collapsible?: boolean;
632
+ expanded?: boolean;
633
+ }
634
+ /** `DisclosureGroup`: open or not; its children are the label (first, a `zstack`) and the content, laid out when expanded; a `tap` toggles. */
635
+ export interface DisclosureGroupProps {
636
+ expanded: boolean;
622
637
  }
623
638
  export type TextFieldStyle = 'plain' | 'roundedBorder';
624
639
  export interface TextFieldProps {
@@ -667,6 +682,16 @@ export interface NavScreenProps {
667
682
  displayMode: NavDisplayMode;
668
683
  /** 0 for the root screen; the renderer draws a back button when > 0. */
669
684
  depth: number;
685
+ /**
686
+ * `.toolbar(.hidden, for: .navigationBar)`: no navigation bar (nor large
687
+ * title, back button or bar items); the content starts under the status
688
+ * bar. A change fades the bar in an animated commit.
689
+ */
690
+ navigationBarHidden?: boolean;
691
+ /** `.toolbar(.hidden, for: .bottomBar)`: the `bottom` toolbars draw nothing. */
692
+ bottomBarHidden?: boolean;
693
+ /** `.statusBarHidden()`: natively the status bar hides with the screen; the page's frame mode hides the status bar it draws (the safe area stays). */
694
+ statusBarHidden?: boolean;
670
695
  }
671
696
  /** Phase 7 adds `selected` (see the block at the end of this file). */
672
697
  export interface NavLinkProps {
@@ -1017,6 +1042,7 @@ export interface ElementPropsByKind {
1017
1042
  scrollview: ScrollViewProps;
1018
1043
  list: ListProps;
1019
1044
  section: SectionProps;
1045
+ disclosuregroup: DisclosureGroupProps;
1020
1046
  textfield: TextFieldProps;
1021
1047
  toggle: ToggleProps;
1022
1048
  navstack: NavStackProps;
@@ -1041,6 +1067,19 @@ export interface ElementPropsByKind {
1041
1067
  searchfield: SearchFieldProps;
1042
1068
  tapgesture: TapGestureProps;
1043
1069
  host: HostProps;
1070
+ registered: RegisteredProps;
1071
+ }
1072
+
1073
+ /**
1074
+ * A view a package registered under `name` with `ViewRegistry`: on a page
1075
+ * its own DOM (src/viewRegistry.ts), in a native app a SwiftUI view. It
1076
+ * takes the proposal, 10 on an unconstrained axis, as `GeometryReader`
1077
+ * does; its children, if any, are the native view's to place.
1078
+ */
1079
+ export interface RegisteredProps {
1080
+ name: string;
1081
+ /** JSON, as the app wrote it, for the view. */
1082
+ props: Record<string, unknown>;
1044
1083
  }
1045
1084
 
1046
1085
  export type ElementKind = keyof ElementPropsByKind;
@@ -1531,7 +1570,17 @@ export type RendererEvent =
1531
1570
  | ScrollGeometryEvent
1532
1571
  | AnimationCompleteEvent
1533
1572
  // The page's undo and redo keys, for the app's undo manager (src/undo.ts)
1534
- | UndoEvent;
1573
+ | UndoEvent
1574
+ // A registered view's own answer (src/viewRegistry.ts)
1575
+ | RegisteredEvent;
1576
+
1577
+ /** What a registered view sent (`context.send(value)`), for the element's `onEvent`. */
1578
+ export interface RegisteredEvent {
1579
+ type: 'registered';
1580
+ id: ElementId;
1581
+ name: string;
1582
+ value: unknown;
1583
+ }
1535
1584
 
1536
1585
  /**
1537
1586
  * Phase 13: the page's answer to the `request` op with this `id`: `value`
@@ -2230,7 +2279,19 @@ export interface DropItem {
2230
2279
  type: string;
2231
2280
  text?: string;
2232
2281
  name?: string;
2282
+ /** A file's bytes as base64 (a Swift app's destination; `file` for a React one). */
2233
2283
  data?: string;
2284
+ /**
2285
+ * The file as the browser holds it, by reference (a React host asks for
2286
+ * `reference` drops, src/dragDrop.ts): never serialized, so a dropped
2287
+ * folder of large files costs no memory until a file is read.
2288
+ */
2289
+ file?: File;
2290
+ /** The file's path inside a dropped folder, the folder's name first (`Photos/2025/a.jpg`); absent for a file dropped on its own. */
2291
+ path?: string;
2292
+ /** Natively: a dropped file still on disk, its `file:` URL and size, read on demand through the files bridge. */
2293
+ url?: string;
2294
+ size?: number;
2234
2295
  }
2235
2296
  /**
2236
2297
  * A drag over a `dropdestination`: it `entered`, `exited`, or `performed`
@@ -2513,8 +2574,12 @@ export interface TextFieldProps {
2513
2574
  export interface ListProps {
2514
2575
  /** `.scrollContentBackground(.hidden)`: the list draws no background of its own (rows keep theirs). */
2515
2576
  background?: 'hidden';
2516
- /** `.scrollDismissesKeyboard(.immediately | .interactively)`: scrolling blurs the text input being edited inside. */
2517
- dismissesKeyboard?: boolean;
2577
+ /**
2578
+ * `.scrollDismissesKeyboard(_:)`: `true` or `immediately` dismisses the
2579
+ * keyboard as scrolling starts, `interactively` lets it follow the finger
2580
+ * down (natively; a page blurs the text input being edited inside either way).
2581
+ */
2582
+ dismissesKeyboard?: boolean | 'immediately' | 'interactively';
2518
2583
  /**
2519
2584
  * `.scrollEdgeEffectStyle(_:for:)` / `.scrollEdgeEffectHidden(_:for:)`: the
2520
2585
  * scroll edge effect where the list runs under its screen's bar (`top`) or
@@ -2530,7 +2595,7 @@ export interface EdgeEffectProps {
2530
2595
 
2531
2596
  export interface ScrollViewProps {
2532
2597
  /** As for `list`. */
2533
- dismissesKeyboard?: boolean;
2598
+ dismissesKeyboard?: boolean | 'immediately' | 'interactively';
2534
2599
  /** As for `list`. */
2535
2600
  edgeEffect?: EdgeEffectProps;
2536
2601
  /**