@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.
- package/app/index.html +11 -7
- package/package.json +1 -1
- package/src/appFiles.ts +12 -3
- package/src/audioEngine.ts +1467 -0
- package/src/chartDom.ts +1 -1
- package/src/deviceFrame.ts +188 -0
- package/src/dragDrop.ts +66 -10
- package/src/files.ts +68 -13
- package/src/hostRequests.ts +53 -3
- package/src/interaction.ts +6 -1
- package/src/languageModel.ts +309 -0
- package/src/layout/engine.ts +128 -48
- package/src/layoutDom.ts +43 -3
- package/src/main.ts +12 -1
- package/src/page.css +65 -0
- package/src/pinnedViews.ts +38 -1
- package/src/protocol.ts +70 -5
- package/src/renderer.ts +299 -53
- package/src/scrollTransition.ts +2 -1
- package/src/styles.css +374 -42
- package/src/styles.ts +13 -15
- package/src/symbols.ts +6 -0
- package/src/viewRegistry.ts +58 -0
package/src/pinnedViews.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
2517
|
-
|
|
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
|
/**
|