@swiftbrowser/web 0.4.0 → 0.5.1
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 +1 -0
- package/app/index.html +1 -0
- package/package.json +2 -1
- package/src/largeTitle.ts +4 -1
- package/src/layout/engine.ts +250 -48
- package/src/layout/index.ts +3 -3
- package/src/layoutDom.ts +23 -0
- package/src/page.css +234 -0
- package/src/protocol.ts +32 -1
- package/src/renderer.ts +79 -5
- package/src/scrollEdge.ts +26 -4
- package/src/styles.css +103 -252
- package/src/symbols.ts +3 -1
package/src/page.css
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/* ---------------------------------------------------------------------------
|
|
2
|
+
The device page (app/index.html): the page around the iPhone frame, with
|
|
3
|
+
its toolbar, its stage and its error banner. Not the renderer's: a page
|
|
4
|
+
that draws an island (@swiftbrowser/react's `SwiftUIView`) imports
|
|
5
|
+
styles.css alone and keeps its own layout.
|
|
6
|
+
------------------------------------------------------------------------- */
|
|
7
|
+
|
|
8
|
+
:root {
|
|
9
|
+
--page-bg: #e9e9ee;
|
|
10
|
+
--page-fg: #1c1c1e;
|
|
11
|
+
--page-muted: #6e6e73;
|
|
12
|
+
--page-border: rgba(0, 0, 0, 0.08);
|
|
13
|
+
--page-surface: rgba(255, 255, 255, 0.8);
|
|
14
|
+
--page-error-bg: #ffe5e5;
|
|
15
|
+
--page-error-fg: #a1000d;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@media (prefers-color-scheme: dark) {
|
|
19
|
+
:root {
|
|
20
|
+
--page-bg: #111114;
|
|
21
|
+
--page-fg: #f2f2f7;
|
|
22
|
+
--page-muted: #98989f;
|
|
23
|
+
--page-border: rgba(255, 255, 255, 0.1);
|
|
24
|
+
--page-surface: rgba(28, 28, 30, 0.85);
|
|
25
|
+
--page-error-bg: #3a0b10;
|
|
26
|
+
--page-error-fg: #ff8a8a;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
*,
|
|
31
|
+
*::before,
|
|
32
|
+
*::after {
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
html,
|
|
37
|
+
body {
|
|
38
|
+
margin: 0;
|
|
39
|
+
min-height: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
body {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
min-height: 100vh;
|
|
46
|
+
background: var(--page-bg);
|
|
47
|
+
color: var(--page-fg);
|
|
48
|
+
font-family: var(--sb-font-family);
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
-webkit-font-smoothing: antialiased;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.toolbar {
|
|
54
|
+
position: sticky;
|
|
55
|
+
top: 0;
|
|
56
|
+
z-index: 10;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: 10px;
|
|
60
|
+
padding: 10px 16px;
|
|
61
|
+
background: var(--page-surface);
|
|
62
|
+
border-bottom: 1px solid var(--page-border);
|
|
63
|
+
backdrop-filter: blur(16px);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.toolbar-brand {
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
letter-spacing: -0.01em;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.toolbar-sep {
|
|
72
|
+
width: 1px;
|
|
73
|
+
height: 18px;
|
|
74
|
+
background: var(--page-border);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.toolbar-label {
|
|
78
|
+
color: var(--page-muted);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.toolbar-value {
|
|
82
|
+
font-variant-numeric: tabular-nums;
|
|
83
|
+
padding: 2px 8px;
|
|
84
|
+
border-radius: 6px;
|
|
85
|
+
background: var(--page-border);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.toolbar-spacer {
|
|
89
|
+
flex: 1 1 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Hot-reload indicator, driven by the dev server's sb:build-* events (the
|
|
93
|
+
`sb-spin` keyframes are styles.css's, loaded with this file). */
|
|
94
|
+
.toolbar-build {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 6px;
|
|
98
|
+
color: var(--page-muted);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.toolbar-build[hidden] {
|
|
102
|
+
display: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.toolbar-build::before {
|
|
106
|
+
content: "";
|
|
107
|
+
width: 10px;
|
|
108
|
+
height: 10px;
|
|
109
|
+
border-radius: 50%;
|
|
110
|
+
border: 1.5px solid currentColor;
|
|
111
|
+
border-top-color: transparent;
|
|
112
|
+
animation: sb-spin 0.9s linear infinite;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.toolbar-build[data-state="error"] {
|
|
116
|
+
color: var(--page-error-fg);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.toolbar-build[data-state="error"]::before {
|
|
120
|
+
animation: none;
|
|
121
|
+
border-color: currentColor;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.toolbar-button {
|
|
125
|
+
font: inherit;
|
|
126
|
+
padding: 6px 12px;
|
|
127
|
+
border: 1px solid var(--page-border);
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
background: transparent;
|
|
130
|
+
color: var(--page-fg);
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.toolbar-button:hover {
|
|
135
|
+
background: var(--page-border);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.toolbar-button[aria-pressed="true"] {
|
|
139
|
+
background: var(--page-fg);
|
|
140
|
+
color: var(--page-bg);
|
|
141
|
+
border-color: transparent;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.toolbar-select {
|
|
145
|
+
font: inherit;
|
|
146
|
+
padding: 5px 8px;
|
|
147
|
+
border: 1px solid var(--page-border);
|
|
148
|
+
border-radius: 8px;
|
|
149
|
+
background: transparent;
|
|
150
|
+
color: var(--page-fg);
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.stage {
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
align-items: center;
|
|
158
|
+
gap: 16px;
|
|
159
|
+
padding: 32px 16px 48px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.status {
|
|
163
|
+
max-width: 560px;
|
|
164
|
+
margin: 0;
|
|
165
|
+
padding: 10px 14px;
|
|
166
|
+
border-radius: 10px;
|
|
167
|
+
background: var(--page-error-bg);
|
|
168
|
+
color: var(--page-error-fg);
|
|
169
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
170
|
+
font-size: 12px;
|
|
171
|
+
white-space: pre-wrap;
|
|
172
|
+
word-break: break-word;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ---------------------------------------------------------------------------
|
|
176
|
+
Device mode (html[data-sb-mode="device"], see src/deviceMode.ts): no
|
|
177
|
+
toolbar, no stage padding, the page behind the app in the OS appearance's
|
|
178
|
+
system background from the first paint (Safari tints its status bar strip
|
|
179
|
+
from it; chromeColor.ts then keeps it at the color the app draws at its
|
|
180
|
+
top edge), and the error banner floating clear of the home indicator.
|
|
181
|
+
------------------------------------------------------------------------- */
|
|
182
|
+
|
|
183
|
+
html[data-sb-mode="device"] {
|
|
184
|
+
height: 100%;
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
overscroll-behavior: none;
|
|
187
|
+
/* No double-tap zoom, no text inflation in landscape. */
|
|
188
|
+
touch-action: manipulation;
|
|
189
|
+
-webkit-text-size-adjust: 100%;
|
|
190
|
+
text-size-adjust: 100%;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
html[data-sb-mode="device"] body {
|
|
194
|
+
min-height: 100dvh;
|
|
195
|
+
overflow: hidden;
|
|
196
|
+
overscroll-behavior: none;
|
|
197
|
+
touch-action: manipulation;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* The page behind the app is the OS appearance's system background from the
|
|
201
|
+
first paint (Safari tints its status bar strip from it); chromeColor.ts
|
|
202
|
+
then keeps it at the color the app draws at its top edge. */
|
|
203
|
+
html[data-sb-mode="device"],
|
|
204
|
+
html[data-sb-mode="device"] body {
|
|
205
|
+
background: #ffffff;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@media (prefers-color-scheme: dark) {
|
|
209
|
+
html[data-sb-mode="device"],
|
|
210
|
+
html[data-sb-mode="device"] body {
|
|
211
|
+
background: #000000;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
html[data-sb-mode="device"] .toolbar {
|
|
216
|
+
display: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
html[data-sb-mode="device"] .stage {
|
|
220
|
+
padding: 0;
|
|
221
|
+
gap: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* The error banner floats over the app, clear of the home indicator. */
|
|
225
|
+
html[data-sb-mode="device"] .status {
|
|
226
|
+
position: fixed;
|
|
227
|
+
left: 16px;
|
|
228
|
+
right: 16px;
|
|
229
|
+
bottom: calc(16px + var(--sb-inset-bottom));
|
|
230
|
+
z-index: 10;
|
|
231
|
+
max-width: none;
|
|
232
|
+
max-height: 40dvh;
|
|
233
|
+
overflow: auto;
|
|
234
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -803,6 +803,16 @@ export interface LazyVGridProps {
|
|
|
803
803
|
spacing: number | null;
|
|
804
804
|
alignment: FrameAlignment;
|
|
805
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* `LazyHGrid`: `LazyVGrid` turned on its side. The `rows` share the height
|
|
808
|
+
* the grid is proposed; the cells fill them column by column, `spacing`
|
|
809
|
+
* (default 8) apart. Children are the cells (flattened). Nothing is lazy.
|
|
810
|
+
*/
|
|
811
|
+
export interface LazyHGridProps {
|
|
812
|
+
rows: GridItem[];
|
|
813
|
+
spacing: number | null;
|
|
814
|
+
alignment: FrameAlignment;
|
|
815
|
+
}
|
|
806
816
|
export type GaugeStyle =
|
|
807
817
|
| 'automatic'
|
|
808
818
|
| 'linearCapacity'
|
|
@@ -850,7 +860,7 @@ export interface DecoratedProps {
|
|
|
850
860
|
export interface MenuProps {
|
|
851
861
|
deferred?: boolean;
|
|
852
862
|
}
|
|
853
|
-
export type ToolbarPlacement = 'trailing' | 'leading' | 'principal' | 'bottom';
|
|
863
|
+
export type ToolbarPlacement = 'trailing' | 'leading' | 'principal' | 'bottom' | 'secondary';
|
|
854
864
|
/**
|
|
855
865
|
* `.toolbar { ToolbarItem(placement:) { ... } }`, an extra child of a
|
|
856
866
|
* `navscreen` after its content. The children are the items, laid out at
|
|
@@ -859,6 +869,12 @@ export type ToolbarPlacement = 'trailing' | 'leading' | 'principal' | 'bottom';
|
|
|
859
869
|
*/
|
|
860
870
|
export interface ToolbarProps {
|
|
861
871
|
placement: ToolbarPlacement;
|
|
872
|
+
/**
|
|
873
|
+
* A `ToolbarSpacer` (no children): it separates glass groups, and in a
|
|
874
|
+
* bottom bar flexible ones share the room left over and push the groups
|
|
875
|
+
* apart; fixed ones are 8 wide.
|
|
876
|
+
*/
|
|
877
|
+
spacer?: 'flexible' | 'fixed';
|
|
862
878
|
}
|
|
863
879
|
/**
|
|
864
880
|
* `.searchable(text:)`, an extra child of a `navscreen`: a 36pt rounded
|
|
@@ -920,6 +936,7 @@ export interface ElementPropsByKind {
|
|
|
920
936
|
grid: GridProps;
|
|
921
937
|
gridrow: GridRowProps;
|
|
922
938
|
lazyvgrid: LazyVGridProps;
|
|
939
|
+
lazyhgrid: LazyHGridProps;
|
|
923
940
|
gauge: GaugeProps;
|
|
924
941
|
decorated: DecoratedProps;
|
|
925
942
|
menu: MenuProps;
|
|
@@ -1316,6 +1333,11 @@ export interface GeometryEvent {
|
|
|
1316
1333
|
y?: number;
|
|
1317
1334
|
/** With `reportsFrame`: how far the device's safe area reaches into the box on each edge. */
|
|
1318
1335
|
safeArea?: { top: number; leading: number; bottom: number; trailing: number };
|
|
1336
|
+
/**
|
|
1337
|
+
* With `reportsFrame`, inside a scroll view or list: its visible rectangle
|
|
1338
|
+
* in the root's coordinates (whole px) and the axis it scrolls along.
|
|
1339
|
+
*/
|
|
1340
|
+
scrollView?: { x: number; y: number; width: number; height: number; axis: 'horizontal' | 'vertical' };
|
|
1319
1341
|
}
|
|
1320
1342
|
|
|
1321
1343
|
/** Host measurements of one subview of a `layout` element (points, 2 decimals; `"infinity"` for an unbounded axis). */
|
|
@@ -2161,6 +2183,15 @@ export interface Style {
|
|
|
2161
2183
|
contentTransition?: { kind: 'numericText' | 'opacity' | 'interpolate'; countsDown?: boolean };
|
|
2162
2184
|
}
|
|
2163
2185
|
|
|
2186
|
+
export interface Style {
|
|
2187
|
+
/**
|
|
2188
|
+
* `.containerRelativeFrame(_:…)`: on the marked axes the box is
|
|
2189
|
+
* `a × container + b` long, the container being the nearest scroll view's
|
|
2190
|
+
* visible area (or the screen), and its content is aligned in that frame.
|
|
2191
|
+
*/
|
|
2192
|
+
containerRelativeFrame?: { horizontal?: boolean; vertical?: boolean; a: number; b: number; alignment: FrameAlignment };
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2164
2195
|
/**
|
|
2165
2196
|
* A mouse (or pen) entered (`value: true`) or left a box whose style has
|
|
2166
2197
|
* `hover`. Touch never hovers.
|
package/src/renderer.ts
CHANGED
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
type HStackProps,
|
|
87
87
|
type LayoutEvent,
|
|
88
88
|
type LazyVGridProps,
|
|
89
|
+
type LazyHGridProps,
|
|
89
90
|
type ImageProps,
|
|
90
91
|
type InspectorProps,
|
|
91
92
|
type ListProps,
|
|
@@ -223,6 +224,7 @@ const ALL_KINDS: ReadonlySet<string> = new Set<ElementKind>([
|
|
|
223
224
|
'grid',
|
|
224
225
|
'gridrow',
|
|
225
226
|
'lazyvgrid',
|
|
227
|
+
'lazyhgrid',
|
|
226
228
|
'gauge',
|
|
227
229
|
'decorated',
|
|
228
230
|
'menu',
|
|
@@ -335,6 +337,8 @@ export class Renderer {
|
|
|
335
337
|
/** A frame check of the `reportsFrame` readers is queued for the next animation frame (after a scroll). */
|
|
336
338
|
private frameCheckQueued = false;
|
|
337
339
|
private readonly hostContents = new Map<ElementId, HTMLElement>();
|
|
340
|
+
/** The `host` elements, adopted or still waiting for their content. */
|
|
341
|
+
private readonly hosts = new Set<ElementId>();
|
|
338
342
|
private readonly hostSizes = new WeakMap<HTMLElement, { width: number; height: number }>();
|
|
339
343
|
private readonly hostMeasures = new Map<string, { width: number; height: number }>();
|
|
340
344
|
private readonly stopHostChanges: () => void;
|
|
@@ -397,7 +401,7 @@ export class Renderer {
|
|
|
397
401
|
// `closest` picks the innermost control, so a button inside a
|
|
398
402
|
// `tapgesture` (or a menu label) handles the click alone.
|
|
399
403
|
const target = (event.target as HTMLElement | null)?.closest<HTMLElement>(
|
|
400
|
-
'.sb-button, .sb-navlink, .sb-navbar-back, .sb-navsplit-toggle, .sb-switch, .sb-sheet-scrim, ' +
|
|
404
|
+
'.sb-button, .sb-navlink, .sb-navbar-back, .sb-navbar-more, .sb-navsplit-toggle, .sb-switch, .sb-sheet-scrim, ' +
|
|
401
405
|
'.sb-tabbar-item, .sb-segment, .sb-picker-option, .sb-picker-menu-item, .sb-picker[data-sb-style="menu"], ' +
|
|
402
406
|
'.sb-menu, .sb-tapgesture, .sb-stepper-button, .sb-alert-scrim, .sb-alert-ok, ' +
|
|
403
407
|
// Phase 8: swipe action buttons, the edit-mode row control and selectable rows (the row sends `tap` itself)
|
|
@@ -506,6 +510,16 @@ export class Renderer {
|
|
|
506
510
|
this.onEvent({ type: 'tap', id });
|
|
507
511
|
return;
|
|
508
512
|
}
|
|
513
|
+
if (target.classList.contains('sb-navbar-more')) {
|
|
514
|
+
// The collapsed `secondary` items, as a menu under the button.
|
|
515
|
+
const screen = target.closest<HTMLElement>('.sb-navscreen');
|
|
516
|
+
const id = Number(screen?.dataset[ID_ATTR]);
|
|
517
|
+
if (!screen || !Number.isInteger(id)) return;
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
if (this.menu?.anchorId === id) this.closeMenu();
|
|
520
|
+
else this.openMenu(target, id, this.collapsedSecondaryRows(id));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
509
523
|
if (target.classList.contains('sb-navbar-back')) {
|
|
510
524
|
const id = Number(target.dataset['sbTarget']);
|
|
511
525
|
if (!Number.isInteger(id)) return;
|
|
@@ -835,10 +849,19 @@ export class Renderer {
|
|
|
835
849
|
|
|
836
850
|
/**
|
|
837
851
|
* A `host` element's content changed (the page said so, or it resized):
|
|
838
|
-
* the hosts are measured again and the tree laid out.
|
|
852
|
+
* the hosts are measured again and the tree laid out. A host whose content
|
|
853
|
+
* was not registered when its element was created (a page that registers
|
|
854
|
+
* from an effect, after its commit) adopts it now.
|
|
839
855
|
*/
|
|
840
856
|
private hostContentChanged(): void {
|
|
841
|
-
if (this.commits === 0
|
|
857
|
+
if (this.commits === 0) return;
|
|
858
|
+
for (const id of this.hosts) {
|
|
859
|
+
if (this.hostContents.has(id)) continue;
|
|
860
|
+
const el = this.elements.get(id);
|
|
861
|
+
const node = this.nodes.get(id);
|
|
862
|
+
if (el && node) this.adoptHostContent(id, el, node.props as unknown as HostProps);
|
|
863
|
+
}
|
|
864
|
+
if (this.hostContents.size === 0) return;
|
|
842
865
|
for (const id of this.hostContents.keys()) {
|
|
843
866
|
const node = this.nodes.get(id);
|
|
844
867
|
if (node) this.engine?.invalidate(node);
|
|
@@ -1173,6 +1196,20 @@ export class Renderer {
|
|
|
1173
1196
|
event.y = y;
|
|
1174
1197
|
event.safeArea = safeAreaOverlap({ x, y, width, height }, env.screen, env.safeArea);
|
|
1175
1198
|
key += `@${x},${y}/${event.safeArea.top},${event.safeArea.leading},${event.safeArea.bottom},${event.safeArea.trailing}`;
|
|
1199
|
+
// The visible rectangle of the scroll view (or list) the reader is in.
|
|
1200
|
+
const scroller = el?.parentElement?.closest<HTMLElement>('.sb-scrollview, .sb-list');
|
|
1201
|
+
if (scroller) {
|
|
1202
|
+
const view = scroller.getBoundingClientRect();
|
|
1203
|
+
event.scrollView = {
|
|
1204
|
+
x: Math.round(view.left - rootRect.left),
|
|
1205
|
+
y: Math.round(view.top - rootRect.top),
|
|
1206
|
+
width: Math.round(view.width),
|
|
1207
|
+
height: Math.round(view.height),
|
|
1208
|
+
axis: scroller.dataset['sbAxes'] === 'horizontal' ? 'horizontal' : 'vertical',
|
|
1209
|
+
};
|
|
1210
|
+
const v = event.scrollView;
|
|
1211
|
+
key += `/${v.x},${v.y},${v.width},${v.height},${v.axis}`;
|
|
1212
|
+
}
|
|
1176
1213
|
}
|
|
1177
1214
|
const last = this.reportedGeometry.get(id);
|
|
1178
1215
|
if (last && last.key === key) continue;
|
|
@@ -1225,6 +1262,21 @@ export class Renderer {
|
|
|
1225
1262
|
* and navlinks become tap rows, pickers a checked section, dividers and
|
|
1226
1263
|
* sections separators and captions, texts disabled captions.
|
|
1227
1264
|
*/
|
|
1265
|
+
/**
|
|
1266
|
+
* A navscreen's More menu: its `secondary` toolbar items the engine
|
|
1267
|
+
* collapsed (those it gave no frame), in order.
|
|
1268
|
+
*/
|
|
1269
|
+
private collapsedSecondaryRows(id: ElementId): MenuRow[] {
|
|
1270
|
+
const screen = this.nodes.get(id);
|
|
1271
|
+
if (!screen) return [];
|
|
1272
|
+
const frames = this.layoutResult?.frames;
|
|
1273
|
+
const items = screen.children
|
|
1274
|
+
.filter((child) => child.kind === 'toolbar' && child.props['placement'] === 'secondary')
|
|
1275
|
+
.flatMap((toolbar) => toolbar.children)
|
|
1276
|
+
.filter((item) => !frames?.has(item.id));
|
|
1277
|
+
return collectMenuRows(items);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1228
1280
|
private menuRows(id: ElementId): MenuRow[] {
|
|
1229
1281
|
const node = this.nodes.get(id);
|
|
1230
1282
|
return node ? collectMenuRows(node.children.slice(1)) : [];
|
|
@@ -1386,7 +1438,10 @@ export class Renderer {
|
|
|
1386
1438
|
applyProps(el, kind, props, this.assets);
|
|
1387
1439
|
this.noteScroll(id, kind, props);
|
|
1388
1440
|
if (kind === 'lazyspacer') this.lazySpacers.add(id);
|
|
1389
|
-
if (kind === 'host')
|
|
1441
|
+
if (kind === 'host') {
|
|
1442
|
+
this.hosts.add(id);
|
|
1443
|
+
this.adoptHostContent(id, el, props as HostProps);
|
|
1444
|
+
}
|
|
1390
1445
|
}
|
|
1391
1446
|
|
|
1392
1447
|
/** A `host` element takes the page's content element (src/hostContent.ts) and watches its size. */
|
|
@@ -1699,6 +1754,7 @@ export class Renderer {
|
|
|
1699
1754
|
this.hostObserver?.unobserve(content);
|
|
1700
1755
|
this.hostContents.delete(id);
|
|
1701
1756
|
}
|
|
1757
|
+
this.hosts.delete(id);
|
|
1702
1758
|
this.elements.delete(id);
|
|
1703
1759
|
this.boxes.delete(id);
|
|
1704
1760
|
this.dirty.delete(id);
|
|
@@ -1982,7 +2038,16 @@ function createShell(kind: ElementKind): HTMLElement {
|
|
|
1982
2038
|
if (chevron) back.append(chevron);
|
|
1983
2039
|
back.append(span('sb-navbar-back-label'));
|
|
1984
2040
|
const inlineTitle = span('sb-navbar-title');
|
|
1985
|
-
|
|
2041
|
+
// The More button of the collapsed `secondary` items (chrome.more).
|
|
2042
|
+
const more = document.createElement('button');
|
|
2043
|
+
more.type = 'button';
|
|
2044
|
+
more.className = 'sb-navbar-more';
|
|
2045
|
+
more.setAttribute('aria-label', 'More');
|
|
2046
|
+
more.setAttribute('aria-haspopup', 'menu');
|
|
2047
|
+
more.hidden = true;
|
|
2048
|
+
const ellipsis = renderSymbol('ellipsis');
|
|
2049
|
+
if (ellipsis) more.append(ellipsis);
|
|
2050
|
+
bar.append(back, inlineTitle, div('sb-navbar-trailing'), more);
|
|
1986
2051
|
const body = div('sb-navscreen-body');
|
|
1987
2052
|
const slot = div('sb-slot sb-navscreen-content');
|
|
1988
2053
|
body.append(div('sb-navbar-large-title'), slot);
|
|
@@ -2737,8 +2802,11 @@ function applyProps(el: HTMLElement, kind: ElementKind, props: ElementProps, ass
|
|
|
2737
2802
|
}
|
|
2738
2803
|
case 'toolbar': {
|
|
2739
2804
|
const placement = (props as ToolbarProps).placement;
|
|
2805
|
+
// `secondary` items shown in the bar look like the trailing ones (glass capsules).
|
|
2740
2806
|
el.dataset['sbPlacement'] =
|
|
2741
2807
|
placement === 'leading' || placement === 'principal' || placement === 'bottom' ? placement : 'trailing';
|
|
2808
|
+
// A `ToolbarSpacer`: draws nothing.
|
|
2809
|
+
el.toggleAttribute('data-sb-spacer', typeof (props as ToolbarProps).spacer === 'string');
|
|
2742
2810
|
break;
|
|
2743
2811
|
}
|
|
2744
2812
|
case 'searchfield':
|
|
@@ -2825,6 +2893,12 @@ function applyProps(el: HTMLElement, kind: ElementKind, props: ElementProps, ass
|
|
|
2825
2893
|
el.dataset['sbColumns'] = String(Array.isArray(grid.columns) ? grid.columns.length : 0);
|
|
2826
2894
|
break;
|
|
2827
2895
|
}
|
|
2896
|
+
case 'lazyhgrid': {
|
|
2897
|
+
const grid = props as LazyHGridProps;
|
|
2898
|
+
el.dataset['sbAlignment'] = grid.alignment ?? 'center';
|
|
2899
|
+
el.dataset['sbRows'] = String(Array.isArray(grid.rows) ? grid.rows.length : 0);
|
|
2900
|
+
break;
|
|
2901
|
+
}
|
|
2828
2902
|
case 'gauge':
|
|
2829
2903
|
applyGauge(el, props as GaugeProps);
|
|
2830
2904
|
break;
|
package/src/scrollEdge.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* has its content inset by the bar (the engine's `bottomInset` chrome rect,
|
|
5
5
|
* layout/engine.ts). While content is under the bar (more of it below) its
|
|
6
6
|
* screen, or the tab it fills, shows the effect over it, behind the bar:
|
|
7
|
-
* `data-sb-scrolled-bottom` with `--sb-bottom-edge` the bar's height
|
|
8
|
-
*
|
|
7
|
+
* `data-sb-scrolled-bottom` with `--sb-bottom-edge` the bar's height and
|
|
8
|
+
* `--sb-edge-bottom` its strength, which ramps down over the last
|
|
9
|
+
* `EDGE_RAMP` px of content (styles.css). The top edge's effect is the
|
|
10
|
+
* bar's own (largeTitle.ts).
|
|
9
11
|
*
|
|
10
12
|
* `.scrollEdgeEffectStyle(_:for:)` / `.scrollEdgeEffectHidden(_:for:)` reach
|
|
11
13
|
* the scroll view as `edgeEffect` props, which become `data-sb-edge-top` /
|
|
@@ -16,6 +18,24 @@
|
|
|
16
18
|
import type { EdgeEffectProps } from './protocol';
|
|
17
19
|
import type { Rect } from './layout';
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* How far content has to be under a bar for its edge effect to be at full
|
|
23
|
+
* strength: the effect ramps up over this scroll distance (and back down as
|
|
24
|
+
* the end comes clear of a bottom bar) rather than switching on, which stood
|
|
25
|
+
* out over content that is not the background color (a photo).
|
|
26
|
+
*/
|
|
27
|
+
export const EDGE_RAMP = 40;
|
|
28
|
+
|
|
29
|
+
/** The strength (0…1) of an edge effect over `distance` px of content under its bar, as a CSS value. */
|
|
30
|
+
export function edgeStrength(distance: number): string {
|
|
31
|
+
return String(Math.round(Math.min(1, Math.max(0, distance / EDGE_RAMP)) * 1000) / 1000);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Sets a CSS custom property, untouched when unchanged. */
|
|
35
|
+
export function setProperty(el: HTMLElement, name: string, value: string): void {
|
|
36
|
+
if (el.style.getPropertyValue(name) !== value) el.style.setProperty(name, value);
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
const bound = new WeakSet<HTMLElement>();
|
|
20
40
|
/** The scroll view whose state each screen's (or tab's) bottom effect shows. */
|
|
21
41
|
const claimedBy = new WeakMap<HTMLElement, HTMLElement>();
|
|
@@ -63,8 +83,9 @@ export function syncScrollEdgeBottom(scroller: HTMLElement): void {
|
|
|
63
83
|
claimedBy.set(owner, scroller);
|
|
64
84
|
// Content is under the bar until the end has scrolled clear of it (the
|
|
65
85
|
// inset is room past the content's end).
|
|
66
|
-
const
|
|
67
|
-
setValue(owner, 'sbScrolledBottom',
|
|
86
|
+
const remaining = scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop;
|
|
87
|
+
setValue(owner, 'sbScrolledBottom', remaining > 1 ? '' : null);
|
|
88
|
+
setProperty(owner, '--sb-edge-bottom', edgeStrength(remaining));
|
|
68
89
|
setValue(owner, 'sbEdgeBottom', scroller.dataset['sbEdgeBottom'] ?? null);
|
|
69
90
|
const height = scroller.style.getPropertyValue('--sb-bottom-edge');
|
|
70
91
|
if (owner.style.getPropertyValue('--sb-bottom-edge') !== height) owner.style.setProperty('--sb-bottom-edge', height);
|
|
@@ -73,6 +94,7 @@ export function syncScrollEdgeBottom(scroller: HTMLElement): void {
|
|
|
73
94
|
function release(owner: HTMLElement): void {
|
|
74
95
|
claimedBy.delete(owner);
|
|
75
96
|
setValue(owner, 'sbScrolledBottom', null);
|
|
97
|
+
owner.style.removeProperty('--sb-edge-bottom');
|
|
76
98
|
setValue(owner, 'sbEdgeBottom', null);
|
|
77
99
|
owner.style.removeProperty('--sb-bottom-edge');
|
|
78
100
|
}
|