@swiftbrowser/web 0.3.1 → 0.5.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
@@ -1,10 +1,12 @@
1
1
  /* ---------------------------------------------------------------------------
2
2
  iOS design tokens. Light values live on the device frame; dark ones switch
3
- with the frame's data-theme so the page chrome and the "phone" are
4
- independent.
3
+ with the frame's data-theme so the page around it (page.css) and the
4
+ "phone" are independent.
5
5
  ------------------------------------------------------------------------- */
6
6
 
7
7
  :root {
8
+ /* SF Pro's ascender minus its cap height: where a line's capitals start. */
9
+ --sb-cap-inset: 0.2476em;
8
10
  /* The UINavigationController push curve: fast start, long settle. */
9
11
  --sb-nav-easing: cubic-bezier(0.32, 0.72, 0, 1);
10
12
  --sb-font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter,
@@ -13,15 +15,6 @@
13
15
  overrides this with the current size (see .device below). */
14
16
  --sb-font-size-body: 17px;
15
17
 
16
- /* Page chrome (not iOS) */
17
- --page-bg: #e9e9ee;
18
- --page-fg: #1c1c1e;
19
- --page-muted: #6e6e73;
20
- --page-border: rgba(0, 0, 0, 0.08);
21
- --page-surface: rgba(255, 255, 255, 0.8);
22
- --page-error-bg: #ffe5e5;
23
- --page-error-fg: #a1000d;
24
-
25
18
  /* Device dimensions (iPhone 15 Pro logical points) */
26
19
  --device-screen-width: 393px;
27
20
  --device-screen-height: 852px;
@@ -39,18 +32,6 @@
39
32
  --sb-inset-left: env(safe-area-inset-left, 0px);
40
33
  }
41
34
 
42
- @media (prefers-color-scheme: dark) {
43
- :root {
44
- --page-bg: #111114;
45
- --page-fg: #f2f2f7;
46
- --page-muted: #98989f;
47
- --page-border: rgba(255, 255, 255, 0.1);
48
- --page-surface: rgba(28, 28, 30, 0.85);
49
- --page-error-bg: #3a0b10;
50
- --page-error-fg: #ff8a8a;
51
- }
52
- }
53
-
54
35
  .device {
55
36
  /* Dynamic Type: main.ts writes --sb-ts-<style>-size / -lh / -scale on the
56
37
  device frame for the selected size (typography.ts); the fallbacks are the
@@ -162,159 +143,28 @@
162
143
  }
163
144
 
164
145
  /* ---------------------------------------------------------------------------
165
- Page layout
146
+ The renderer's boxes size by their borders. Only its own DOM: a page that
147
+ draws an island (@swiftbrowser/react) keeps its own box model.
166
148
  ------------------------------------------------------------------------- */
167
149
 
168
- *,
169
- *::before,
170
- *::after {
150
+ .device,
151
+ .device *,
152
+ .device *::before,
153
+ .device *::after {
171
154
  box-sizing: border-box;
172
155
  }
173
156
 
174
- html,
175
- body {
176
- margin: 0;
177
- min-height: 100%;
178
- }
179
-
180
- body {
181
- display: flex;
182
- flex-direction: column;
183
- min-height: 100vh;
184
- background: var(--page-bg);
185
- color: var(--page-fg);
186
- font-family: var(--sb-font-family);
187
- font-size: 14px;
188
- -webkit-font-smoothing: antialiased;
189
- }
190
-
191
- .toolbar {
192
- position: sticky;
193
- top: 0;
194
- z-index: 10;
195
- display: flex;
196
- align-items: center;
197
- gap: 10px;
198
- padding: 10px 16px;
199
- background: var(--page-surface);
200
- border-bottom: 1px solid var(--page-border);
201
- backdrop-filter: blur(16px);
202
- }
203
-
204
- .toolbar-brand {
205
- font-weight: 600;
206
- letter-spacing: -0.01em;
207
- }
208
-
209
- .toolbar-sep {
210
- width: 1px;
211
- height: 18px;
212
- background: var(--page-border);
213
- }
214
-
215
- .toolbar-label {
216
- color: var(--page-muted);
217
- }
218
-
219
- .toolbar-value {
220
- font-variant-numeric: tabular-nums;
221
- padding: 2px 8px;
222
- border-radius: 6px;
223
- background: var(--page-border);
224
- }
225
-
226
- .toolbar-spacer {
227
- flex: 1 1 0;
228
- }
229
-
230
- /* Hot-reload indicator, driven by the dev server's sb:build-* events. */
231
- .toolbar-build {
232
- display: inline-flex;
233
- align-items: center;
234
- gap: 6px;
235
- color: var(--page-muted);
236
- }
237
-
238
- .toolbar-build[hidden] {
239
- display: none;
240
- }
241
-
242
- .toolbar-build::before {
243
- content: "";
244
- width: 10px;
245
- height: 10px;
246
- border-radius: 50%;
247
- border: 1.5px solid currentColor;
248
- border-top-color: transparent;
249
- animation: sb-spin 0.9s linear infinite;
250
- }
251
-
252
- .toolbar-build[data-state="error"] {
253
- color: var(--page-error-fg);
254
- }
255
-
256
- .toolbar-build[data-state="error"]::before {
257
- animation: none;
258
- border-color: currentColor;
259
- }
260
-
261
- .toolbar-button {
262
- font: inherit;
263
- padding: 6px 12px;
264
- border: 1px solid var(--page-border);
265
- border-radius: 8px;
266
- background: transparent;
267
- color: var(--page-fg);
268
- cursor: pointer;
269
- }
270
-
271
- .toolbar-button:hover {
272
- background: var(--page-border);
273
- }
274
-
275
- .toolbar-button[aria-pressed="true"] {
276
- background: var(--page-fg);
277
- color: var(--page-bg);
278
- border-color: transparent;
279
- }
280
-
281
- .toolbar-select {
282
- font: inherit;
283
- padding: 5px 8px;
284
- border: 1px solid var(--page-border);
285
- border-radius: 8px;
286
- background: transparent;
287
- color: var(--page-fg);
288
- cursor: pointer;
289
- }
290
-
291
- .stage {
292
- display: flex;
293
- flex-direction: column;
294
- align-items: center;
295
- gap: 16px;
296
- padding: 32px 16px 48px;
297
- }
298
-
299
- .status {
300
- max-width: 560px;
301
- margin: 0;
302
- padding: 10px 14px;
303
- border-radius: 10px;
304
- background: var(--page-error-bg);
305
- color: var(--page-error-fg);
306
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
307
- font-size: 12px;
308
- white-space: pre-wrap;
309
- word-break: break-word;
310
- }
311
-
312
157
  /* ---------------------------------------------------------------------------
313
158
  Device frame
314
159
  ------------------------------------------------------------------------- */
315
160
 
316
161
  .device {
317
162
  position: relative;
163
+ }
164
+
165
+ /* The iPhone's size and bezel: the frame's only. An island (`.sb-island`,
166
+ below) is the page's box, sized by the page's own CSS. */
167
+ .device:not(.sb-island) {
318
168
  flex: 0 0 auto;
319
169
  width: calc(var(--device-screen-width) + 2 * var(--device-bezel));
320
170
  height: calc(var(--device-screen-height) + 2 * var(--device-bezel));
@@ -483,53 +333,13 @@ body {
483
333
  /* ---------------------------------------------------------------------------
484
334
  Device mode (html[data-sb-mode="device"], see src/deviceMode.ts)
485
335
 
486
- On a real phone there is no toolbar and no fake bezel: the OS draws the
487
- status bar and #screen fills the viewport. The layout engine still gets a
488
- status-bar / home-indicator safe area, but from env(safe-area-inset-*)
489
- (via --sb-inset-*), so it is only non-zero in standalone mode / landscape.
336
+ On a real phone there is no fake bezel: the OS draws the status bar and
337
+ #screen fills the viewport (the page's part, its toolbar and background,
338
+ is page.css). The layout engine still gets a status-bar / home-indicator
339
+ safe area, but from env(safe-area-inset-*) (via --sb-inset-*), so it is
340
+ only non-zero in standalone mode / landscape.
490
341
  ------------------------------------------------------------------------- */
491
342
 
492
- html[data-sb-mode="device"] {
493
- height: 100%;
494
- overflow: hidden;
495
- overscroll-behavior: none;
496
- /* No double-tap zoom, no text inflation in landscape. */
497
- touch-action: manipulation;
498
- -webkit-text-size-adjust: 100%;
499
- text-size-adjust: 100%;
500
- }
501
-
502
- html[data-sb-mode="device"] body {
503
- min-height: 100dvh;
504
- overflow: hidden;
505
- overscroll-behavior: none;
506
- touch-action: manipulation;
507
- }
508
-
509
- /* The page behind the app is the OS appearance's system background from the
510
- first paint (Safari tints its status bar strip from it); chromeColor.ts
511
- then keeps it at the color the app draws at its top edge. */
512
- html[data-sb-mode="device"],
513
- html[data-sb-mode="device"] body {
514
- background: #ffffff;
515
- }
516
-
517
- @media (prefers-color-scheme: dark) {
518
- html[data-sb-mode="device"],
519
- html[data-sb-mode="device"] body {
520
- background: #000000;
521
- }
522
- }
523
-
524
- html[data-sb-mode="device"] .toolbar {
525
- display: none;
526
- }
527
-
528
- html[data-sb-mode="device"] .stage {
529
- padding: 0;
530
- gap: 0;
531
- }
532
-
533
343
  html[data-sb-mode="device"] .device {
534
344
  position: fixed;
535
345
  top: 0;
@@ -556,18 +366,6 @@ html[data-sb-mode="device"] .device-screen {
556
366
  border-radius: 0;
557
367
  }
558
368
 
559
- /* The error banner floats over the app, clear of the home indicator. */
560
- html[data-sb-mode="device"] .status {
561
- position: fixed;
562
- left: 16px;
563
- right: 16px;
564
- bottom: calc(16px + var(--sb-inset-bottom));
565
- z-index: 10;
566
- max-width: none;
567
- max-height: 40dvh;
568
- overflow: auto;
569
- }
570
-
571
369
  /* ---------------------------------------------------------------------------
572
370
  Rendered elements
573
371
 
@@ -701,11 +499,20 @@ html[data-sb-mode="device"] .status {
701
499
  line-height: 1;
702
500
  }
703
501
 
502
+ /* A symbol fills its box as SF Symbols do: at a text size a circle symbol is
503
+ about 1.28× the font size (21.7 pt at body's 17), and the glyphs here
504
+ occupy 20 of their 24-unit grid (a little more with their stroke), so
505
+ the SVG is 1.43em, centred in the
506
+ line-height box the engine gives the image (the box is unchanged; the
507
+ glyph overflows it a little, as a symbol's bounds do). The stroke, 1.4
508
+ units, is then 1.5 px at body, the weight iOS draws regular symbols in. */
704
509
  .sb-image > svg {
705
510
  display: block;
706
- width: 1em;
707
- height: 1em;
511
+ flex: none;
512
+ width: 1.43em;
513
+ height: 1.43em;
708
514
  stroke: currentColor;
515
+ stroke-width: 1.4;
709
516
  }
710
517
 
711
518
  /* An indeterminate ProgressView: the activity indicator (symbols.ts), in
@@ -949,6 +756,19 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
949
756
  text-align: left;
950
757
  }
951
758
 
759
+ /* SF Pro's capitals start 0.2476em below the top of a line SwiftUI lays out
760
+ (ascender 0.952em − cap height 0.705em): trim the first line to its cap
761
+ height and put it there, so text the engine places lands where iOS draws
762
+ it, whatever font the page has. */
763
+ @supports (text-box: trim-start cap alphabetic) {
764
+ .sb-text,
765
+ .sb-section-header,
766
+ .sb-section-footer {
767
+ text-box: trim-start cap alphabetic;
768
+ padding-top: var(--sb-cap-inset);
769
+ }
770
+ }
771
+
952
772
  .sb-section-header[hidden],
953
773
  .sb-section-footer[hidden] {
954
774
  display: none;
@@ -1259,11 +1079,16 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1259
1079
  }
1260
1080
 
1261
1081
  /* The large title scrolls with the screen's root list or scroll view
1262
- (src/largeTitle.ts moves it); drags on it scroll that view. */
1082
+ (src/largeTitle.ts moves it into it); drags on it scroll that view. There
1083
+ it is content, under the bar. */
1263
1084
  .sb-navbar-large-title {
1264
1085
  pointer-events: none;
1265
1086
  }
1266
1087
 
1088
+ [data-sb-under-bar] > .sb-navbar-large-title {
1089
+ z-index: auto;
1090
+ }
1091
+
1267
1092
  /* Until the large title has scrolled under the bar, the inline title (whose
1268
1093
  rect the engine reports for a title that can collapse) stays hidden. */
1269
1094
  .sb-navscreen[data-sb-display="large"] > .sb-navbar > .sb-navbar-title {
@@ -1275,11 +1100,122 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1275
1100
  opacity: 1;
1276
1101
  }
1277
1102
 
1278
- /* iOS 26's scroll edge effect, simplified: scrolled content fades in over
1279
- 16px below the bar instead of being cut off at it (src/largeTitle.ts). */
1280
- [data-sb-scrolls-title][data-sb-scrolled] {
1281
- -webkit-mask-image: linear-gradient(to bottom, transparent, #000 16px);
1282
- mask-image: linear-gradient(to bottom, transparent, #000 16px);
1103
+ /* iOS 26's scroll edge effect: a screen's root list or scroll view runs up
1104
+ under the bar (the engine insets its content by the bar), and once content
1105
+ is under it (`data-sb-scrolled`, src/largeTitle.ts) the bar blurs it and
1106
+ fades it into the background toward the top of the screen, a little past
1107
+ the bar's bottom edge. The bar's items stay on their glass. */
1108
+ .sb-navscreen {
1109
+ --sb-edge-color: var(--sb-color-system-background);
1110
+ }
1111
+
1112
+ .sb-navscreen:has(> .sb-navscreen-body .sb-list:is([data-sb-under-bar], [data-sb-under-bottom]):not([data-sb-list-style="plain"])) {
1113
+ --sb-edge-color: var(--sb-color-grouped-background);
1114
+ }
1115
+
1116
+ .sb-navbar::before {
1117
+ content: "";
1118
+ position: absolute;
1119
+ inset: 0 0 -20px;
1120
+ z-index: -1;
1121
+ pointer-events: none;
1122
+ opacity: 0;
1123
+ transition: opacity 0.2s ease;
1124
+ background: linear-gradient(
1125
+ to bottom,
1126
+ var(--sb-edge-color) 0%,
1127
+ color-mix(in srgb, var(--sb-edge-color) 75%, transparent) 45%,
1128
+ transparent 100%
1129
+ );
1130
+ -webkit-backdrop-filter: blur(6px);
1131
+ backdrop-filter: blur(6px);
1132
+ -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent);
1133
+ mask-image: linear-gradient(to bottom, #000 60%, transparent);
1134
+ }
1135
+
1136
+ /* Its strength follows the content under the bar, ramping up over the
1137
+ first 40px scrolled (`--sb-edge-top`, src/scrollEdge.ts); the transition
1138
+ smooths a scroll that jumps. */
1139
+ .sb-navscreen[data-sb-scrolled] > .sb-navbar::before {
1140
+ opacity: var(--sb-edge-top, 1);
1141
+ transition: opacity 0.12s linear;
1142
+ }
1143
+
1144
+ /* At the bottom: a list or scroll view runs under the tab bar, or its
1145
+ screen's bottom toolbar or search field (the engine insets its content by
1146
+ the bar, `--sb-bottom-edge` tall), and while content is under it
1147
+ (`data-sb-scrolled-bottom`, src/scrollEdge.ts) its screen (over the
1148
+ screen's content: the body is static, so its pseudo-element is placed in
1149
+ the screen, whose own `::after` dims it under a pushed screen), or the
1150
+ tab it fills, blurs it and fades it into the background toward the bottom
1151
+ of the screen, from a little above the bar. The bars stay on top. */
1152
+ .sb-tab {
1153
+ --sb-edge-color: var(--sb-color-system-background);
1154
+ }
1155
+
1156
+ .sb-tab:has(> .sb-list[data-sb-under-bottom]:not([data-sb-list-style="plain"])) {
1157
+ --sb-edge-color: var(--sb-color-grouped-background);
1158
+ }
1159
+
1160
+ .sb-navscreen > .sb-navscreen-body::after,
1161
+ .sb-tab::after {
1162
+ content: "";
1163
+ position: absolute;
1164
+ left: 0;
1165
+ right: 0;
1166
+ bottom: 0;
1167
+ height: calc(var(--sb-bottom-edge, 0px) + 20px);
1168
+ z-index: 1;
1169
+ pointer-events: none;
1170
+ opacity: 0;
1171
+ transition: opacity 0.2s ease;
1172
+ /* iOS 26 mostly fades the content here (it stays faintly legible under
1173
+ the bar) and barely blurs it. */
1174
+ background: linear-gradient(
1175
+ to top,
1176
+ color-mix(in srgb, var(--sb-edge-color) 70%, transparent) 0%,
1177
+ color-mix(in srgb, var(--sb-edge-color) 55%, transparent) 45%,
1178
+ transparent 100%
1179
+ );
1180
+ -webkit-backdrop-filter: blur(1.5px);
1181
+ backdrop-filter: blur(1.5px);
1182
+ -webkit-mask-image: linear-gradient(to top, #000 60%, transparent);
1183
+ mask-image: linear-gradient(to top, #000 60%, transparent);
1184
+ }
1185
+
1186
+ .sb-navscreen[data-sb-scrolled-bottom] > .sb-navscreen-body::after,
1187
+ .sb-tab[data-sb-scrolled-bottom]::after {
1188
+ opacity: var(--sb-edge-bottom, 1);
1189
+ transition: opacity 0.12s linear;
1190
+ }
1191
+
1192
+ /* `.scrollEdgeEffectStyle(.hard, for:)`: an opaque backing exactly behind
1193
+ the bar with a dividing line, no fade; `.scrollEdgeEffectHidden`: none. */
1194
+ .sb-navscreen[data-sb-edge-top="hard"] > .sb-navbar::before {
1195
+ inset: 0;
1196
+ background: var(--sb-edge-color);
1197
+ -webkit-backdrop-filter: none;
1198
+ backdrop-filter: none;
1199
+ -webkit-mask-image: none;
1200
+ mask-image: none;
1201
+ border-bottom: 0.5px solid var(--sb-color-separator);
1202
+ }
1203
+
1204
+ .sb-navscreen[data-sb-edge-bottom="hard"] > .sb-navscreen-body::after,
1205
+ .sb-tab[data-sb-edge-bottom="hard"]::after {
1206
+ height: var(--sb-bottom-edge, 0px);
1207
+ background: var(--sb-edge-color);
1208
+ -webkit-backdrop-filter: none;
1209
+ backdrop-filter: none;
1210
+ -webkit-mask-image: none;
1211
+ mask-image: none;
1212
+ border-top: 0.5px solid var(--sb-color-separator);
1213
+ }
1214
+
1215
+ .sb-navscreen[data-sb-edge-top="hidden"] > .sb-navbar::before,
1216
+ .sb-navscreen[data-sb-edge-bottom="hidden"] > .sb-navscreen-body::after,
1217
+ .sb-tab[data-sb-edge-bottom="hidden"]::after {
1218
+ display: none;
1283
1219
  }
1284
1220
 
1285
1221
  /* NavigationLink: a tappable label. Inside a list it is a row with a chevron
@@ -1539,6 +1475,27 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1539
1475
  background: var(--sb-color-system-background);
1540
1476
  }
1541
1477
 
1478
+ /* `sidebar` (iPadOS; a NavigationSplitView's sidebar list): the rows sit on
1479
+ the list's background, without cards or hairlines, and only a selected
1480
+ row is filled (its rounded fill, below). The engine lays it out like
1481
+ `insetGrouped` with 44pt rows. */
1482
+ .sb-list[data-sb-list-style="sidebar"] .sb-section-rows,
1483
+ .sb-list[data-sb-list-style="sidebar"] .sb-section-rows > [data-sb-id],
1484
+ .sb-list[data-sb-list-style="sidebar"] .sb-section-rows > [data-sb-ghost],
1485
+ .sb-list[data-sb-list-style="sidebar"] > .sb-list-content > :not(.sb-section) {
1486
+ background: transparent;
1487
+ border-radius: 0;
1488
+ }
1489
+
1490
+ .sb-list[data-sb-list-style="sidebar"] .sb-section-rows > *::after,
1491
+ .sb-list[data-sb-list-style="sidebar"] > .sb-list-content > :not(.sb-section)::after {
1492
+ display: none;
1493
+ }
1494
+
1495
+ .sb-list[data-sb-list-style="sidebar"] :is(.sb-section-rows, .sb-list-content) > :is(input, textarea) {
1496
+ background-image: none;
1497
+ }
1498
+
1542
1499
  /* ---------------------------------------------------------------------------
1543
1500
  Phase 4: tab views. The tabs are stacked in the content slot (every tab has
1544
1501
  the tab view's frame); the selected one is visible, the others keep their
@@ -1885,6 +1842,7 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1885
1842
  .sb-grid,
1886
1843
  .sb-gridrow,
1887
1844
  .sb-lazyvgrid,
1845
+ .sb-lazyhgrid,
1888
1846
  .sb-decorated {
1889
1847
  display: block;
1890
1848
  }
@@ -2152,30 +2110,50 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2152
2110
  --sb-color-accent: var(--sb-color-primary);
2153
2111
  }
2154
2112
 
2155
- /* Their symbols are drawn larger and heavier, as iOS 26 draws bar buttons. */
2156
- .sb-toolbar:is([data-sb-placement="leading"], [data-sb-placement="trailing"]) .sb-image > svg {
2157
- transform: scale(1.25);
2158
- stroke-width: 2.1;
2113
+ /* Toolbar items in the bar are laid out in the screen's body, under the bar
2114
+ element (z 1): they stack above it, so its scroll edge effect blurs the
2115
+ content under the bar and not them. */
2116
+ .sb-toolbar:is([data-sb-placement="leading"], [data-sb-placement="trailing"], [data-sb-placement="principal"]) {
2117
+ z-index: 2;
2118
+ }
2119
+
2120
+ /* Their symbols are drawn a little larger and heavier (semibold), as iOS 26
2121
+ draws bar buttons: a plus is 17.7 pt with 2.33 pt bars there. */
2122
+ .sb-toolbar:is([data-sb-placement="leading"], [data-sb-placement="trailing"], [data-sb-placement="bottom"]) .sb-image > svg {
2123
+ width: 1.57em;
2124
+ height: 1.57em;
2125
+ stroke-width: 1.85;
2159
2126
  }
2160
2127
 
2128
+ /* A bottom toolbar's symbols are as large but regular weight. */
2129
+ .sb-toolbar[data-sb-placement="bottom"] .sb-image > svg {
2130
+ stroke-width: 1.5;
2131
+ }
2132
+
2133
+ /* iOS 26: a bottom toolbar's items float over the content on Liquid Glass
2134
+ capsules (engine `bottomGlass0` …, src/layoutDom.ts), drawn in the label
2135
+ color unless tinted, above the scroll edge effect. */
2161
2136
  .sb-toolbar[data-sb-placement="bottom"] {
2162
2137
  z-index: 3;
2163
- background: var(--sb-color-tabbar-background);
2164
- backdrop-filter: blur(20px) saturate(180%);
2165
- -webkit-backdrop-filter: blur(20px) saturate(180%);
2138
+ --sb-color-accent: var(--sb-color-primary);
2166
2139
  }
2167
2140
 
2168
- .sb-toolbar[data-sb-placement="bottom"]::before {
2169
- content: "";
2141
+ .sb-bottom-glass {
2170
2142
  position: absolute;
2171
- left: 0;
2172
- right: 0;
2173
- top: 0;
2174
- height: 1px;
2175
- background: var(--sb-color-separator);
2143
+ z-index: 2;
2144
+ border-radius: 24px;
2145
+ background: var(--sb-glass-background);
2146
+ box-shadow: var(--sb-glass-shadow);
2147
+ backdrop-filter: blur(12px) saturate(180%);
2148
+ -webkit-backdrop-filter: blur(12px) saturate(180%);
2176
2149
  pointer-events: none;
2177
2150
  }
2178
2151
 
2152
+ /* A ToolbarSpacer draws nothing (the engine also hides it). */
2153
+ .sb-toolbar[data-sb-spacer] {
2154
+ display: none;
2155
+ }
2156
+
2179
2157
  /* Search field: a capsule (engine `searchField` rect) with a
2180
2158
  magnifier leading; the input sends `text` events like a textfield. */
2181
2159
  .sb-searchfield {
@@ -2254,12 +2232,61 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2254
2232
  background: var(--sb-color-system-background);
2255
2233
  }
2256
2234
 
2235
+ /* `.inspector`: the content and a trailing panel (protocol children placed by
2236
+ the engine). The panel draws the hairline before it, in the 1pt gap the
2237
+ engine leaves, so the hairline slides with it. */
2238
+ .sb-inspector {
2239
+ overflow: hidden;
2240
+ }
2241
+
2242
+ .sb-inspectorpanel {
2243
+ background: var(--sb-color-system-background);
2244
+ box-shadow: -1px 0 0 var(--sb-color-separator);
2245
+ }
2246
+
2257
2247
  .sb-navsplit-separator {
2258
2248
  position: absolute;
2259
2249
  background: var(--sb-color-separator);
2260
2250
  pointer-events: none;
2261
2251
  }
2262
2252
 
2253
+ /* iPadOS: the sidebar toggle, a 44pt glass circle at the leading edge of
2254
+ the bar row (the sidebar's, or the detail's while the sidebar is hidden),
2255
+ over the columns. */
2256
+ .sb-navsplit-toggle {
2257
+ position: absolute;
2258
+ z-index: 2;
2259
+ appearance: none;
2260
+ display: inline-flex;
2261
+ align-items: center;
2262
+ justify-content: center;
2263
+ margin: 0;
2264
+ padding: 0;
2265
+ border: 0;
2266
+ border-radius: 22px;
2267
+ background: var(--sb-glass-background);
2268
+ box-shadow: var(--sb-glass-shadow);
2269
+ backdrop-filter: blur(12px) saturate(180%);
2270
+ -webkit-backdrop-filter: blur(12px) saturate(180%);
2271
+ color: var(--sb-color-primary);
2272
+ cursor: pointer;
2273
+ touch-action: manipulation;
2274
+ -webkit-tap-highlight-color: transparent;
2275
+ }
2276
+
2277
+ .sb-navsplit-toggle[hidden] {
2278
+ display: none;
2279
+ }
2280
+
2281
+ .sb-navsplit-toggle:active {
2282
+ opacity: 0.3;
2283
+ }
2284
+
2285
+ .sb-navsplit-toggle > svg {
2286
+ width: 22px;
2287
+ height: 22px;
2288
+ }
2289
+
2263
2290
  /* A selected `navlink` row (`List(selection:)`): the sidebar-style rounded
2264
2291
  fill. The engine omits the chevron inside a navsplit sidebar; elsewhere
2265
2292
  the chevron stays. The list-row selectors outrank the per-style row
@@ -2429,6 +2456,7 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2429
2456
  .sb-grid,
2430
2457
  .sb-gridrow,
2431
2458
  .sb-lazyvgrid,
2459
+ .sb-lazyhgrid,
2432
2460
  .sb-viewthatfits,
2433
2461
  .sb-decorated,
2434
2462
  .sb-gesture,
@@ -2436,8 +2464,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2436
2464
  pointer-events: none;
2437
2465
  }
2438
2466
 
2439
- :is(.sb-vstack, .sb-hstack, .sb-zstack, .sb-styled, .sb-geometry, .sb-layout, .sb-grid, .sb-gridrow, .sb-lazyvgrid, .sb-viewthatfits, .sb-decorated, .sb-gesture, .sb-tapgesture)
2440
- > [data-sb-kind]:not(.sb-vstack, .sb-hstack, .sb-zstack, .sb-spacer, .sb-styled, .sb-geometry, .sb-layout, .sb-grid, .sb-gridrow, .sb-lazyvgrid, .sb-viewthatfits, .sb-decorated, .sb-gesture, .sb-tapgesture) {
2467
+ :is(.sb-vstack, .sb-hstack, .sb-zstack, .sb-styled, .sb-geometry, .sb-layout, .sb-grid, .sb-gridrow, .sb-lazyvgrid, .sb-lazyhgrid, .sb-viewthatfits, .sb-decorated, .sb-gesture, .sb-tapgesture)
2468
+ > [data-sb-kind]:not(.sb-vstack, .sb-hstack, .sb-zstack, .sb-spacer, .sb-styled, .sb-geometry, .sb-layout, .sb-grid, .sb-gridrow, .sb-lazyvgrid, .sb-lazyhgrid, .sb-viewthatfits, .sb-decorated, .sb-gesture, .sb-tapgesture) {
2441
2469
  pointer-events: auto;
2442
2470
  }
2443
2471
 
@@ -2563,7 +2591,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2563
2591
  }
2564
2592
 
2565
2593
  /* `listrow`: the edit-mode control at the engine's `editControl` rect (hidden
2566
- otherwise); a selected row reuses the Phase 7 selected-row fill. */
2594
+ otherwise; its 22px circle is centred 28 from the card's edge, as on
2595
+ iOS); a selected row reuses the Phase 7 selected-row fill. */
2567
2596
  .sb-listrow-control {
2568
2597
  appearance: none;
2569
2598
  position: absolute;
@@ -2578,6 +2607,12 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2578
2607
  cursor: pointer;
2579
2608
  touch-action: manipulation;
2580
2609
  -webkit-tap-highlight-color: transparent;
2610
+ }
2611
+
2612
+ /* The controls fade in as the list starts editing (layoutDom marks them
2613
+ while they appear); a row moved in the DOM by a reorder keeps them put. */
2614
+ .sb-listrow-control[data-sb-appear],
2615
+ .sb-listrow-grip[data-sb-appear] {
2581
2616
  animation: sb-fade-in 0.25s ease-out both;
2582
2617
  }
2583
2618
 
@@ -2585,6 +2620,89 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2585
2620
  display: none;
2586
2621
  }
2587
2622
 
2623
+ /* `listrow`: the reorder grip of a movable row at the engine's
2624
+ `reorderControl` rect (edit mode only): three gray lines, which a press
2625
+ lifts the row by (src/dragDrop.ts). */
2626
+ .sb-listrow-grip {
2627
+ position: absolute;
2628
+ z-index: 2;
2629
+ display: flex;
2630
+ align-items: center;
2631
+ justify-content: center;
2632
+ color: var(--sb-color-tertiary-label);
2633
+ cursor: grab;
2634
+ touch-action: none;
2635
+ user-select: none;
2636
+ -webkit-user-select: none;
2637
+ -webkit-touch-callout: none;
2638
+ }
2639
+
2640
+ .sb-listrow-grip[hidden] {
2641
+ display: none;
2642
+ }
2643
+
2644
+ .sb-listrow-grip > svg {
2645
+ width: 21px;
2646
+ height: 11px;
2647
+ overflow: visible;
2648
+ stroke: currentColor;
2649
+ stroke-width: 1.33;
2650
+ }
2651
+
2652
+ /* A row being reordered: its neighbours slide out of its way (the
2653
+ transition), it follows the pointer above them with a shadow, and settles
2654
+ into its slot with the same slide. The rows are composited for the whole
2655
+ reorder (`will-change`): promoting one as its slide starts would
2656
+ re-rasterize it, a blink of its text and icons. */
2657
+ [data-sb-reordering] > .sb-listrow[data-sb-movable] {
2658
+ transition: transform 0.3s var(--sb-nav-easing);
2659
+ will-change: transform;
2660
+ }
2661
+
2662
+ .sb-listrow[data-sb-lifting] {
2663
+ z-index: 5;
2664
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
2665
+ cursor: grabbing;
2666
+ }
2667
+
2668
+ .sb-listrow[data-sb-lifting]:not([data-sb-settling]) {
2669
+ transition: none;
2670
+ }
2671
+
2672
+ /* `draggable` / `dropdestination`: transparent wrappers; a lifted view's
2673
+ copy follows the pointer over everything, scaled up a little with a
2674
+ shadow, while the original dims; letting go off a destination slides the
2675
+ copy back. A targeted destination shows nothing of its own (the app's
2676
+ `isTargeted` does), only that the copy may drop there. */
2677
+ .sb-draggable,
2678
+ .sb-dropdestination {
2679
+ -webkit-touch-callout: none;
2680
+ }
2681
+
2682
+ .sb-draggable {
2683
+ cursor: grab;
2684
+ }
2685
+
2686
+ .sb-draggable[data-sb-dragging] {
2687
+ opacity: 0.4;
2688
+ }
2689
+
2690
+ .sb-drag-preview {
2691
+ z-index: 60;
2692
+ pointer-events: none;
2693
+ opacity: 0.9;
2694
+ filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.25));
2695
+ will-change: transform;
2696
+ }
2697
+
2698
+ .sb-drag-preview[data-sb-settling] {
2699
+ transition: transform 0.3s var(--sb-nav-easing);
2700
+ }
2701
+
2702
+ [data-sb-dragging] .sb-dropdestination {
2703
+ cursor: copy;
2704
+ }
2705
+
2588
2706
  .sb-listrow-circle {
2589
2707
  display: flex;
2590
2708
  align-items: center;
@@ -2625,6 +2743,14 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2625
2743
 
2626
2744
  .sb-listrow[data-sb-deletable]:not([data-sb-selectable]) .sb-listrow-circle > .sb-listrow-minus {
2627
2745
  display: block;
2746
+ transition: transform 0.3s var(--sb-nav-easing);
2747
+ }
2748
+
2749
+ /* While the row shows its Delete button (the minus was tapped, or the row
2750
+ swiped) the minus stands upright, as on iOS; the content, and the grip in
2751
+ it, slid aside. */
2752
+ .sb-listrow[data-sb-swipe-open="trailing"] .sb-listrow-circle > .sb-listrow-minus {
2753
+ transform: rotate(-90deg);
2628
2754
  }
2629
2755
 
2630
2756
  .sb-listrow[data-sb-selectable] {
@@ -2638,6 +2764,26 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2638
2764
  border-radius: 10px;
2639
2765
  }
2640
2766
 
2767
+ /* The fill is the row's alone: its swipe content (which inherits the row's
2768
+ background to cover the actions behind it) and a selected link inside it
2769
+ would each stack another translucent layer, a darker pill around the
2770
+ label. */
2771
+ .sb-listrow[data-sb-selected] > .sb-swipe-content {
2772
+ background-color: transparent;
2773
+ }
2774
+
2775
+ .sb-listrow .sb-navlink[data-sb-selected] {
2776
+ background: transparent;
2777
+ }
2778
+
2779
+ /* Like iOS, a selected row's fill has no hairline under it or above it. */
2780
+ .sb-section-rows > [data-sb-selected]::after,
2781
+ .sb-list-content > [data-sb-selected]::after,
2782
+ .sb-section-rows > :has(+ [data-sb-selected])::after,
2783
+ .sb-list-content > :has(+ [data-sb-selected])::after {
2784
+ display: none;
2785
+ }
2786
+
2641
2787
  @keyframes sb-fade-in {
2642
2788
  from {
2643
2789
  opacity: 0;
@@ -2654,7 +2800,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2654
2800
  position: absolute;
2655
2801
  left: 0;
2656
2802
  right: 0;
2657
- top: 0;
2803
+ /* Under the bar a scroll view's content starts below it. */
2804
+ top: var(--sb-top-inset, 0px);
2658
2805
  height: 40px;
2659
2806
  display: flex;
2660
2807
  align-items: center;
@@ -2682,17 +2829,20 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2682
2829
  }
2683
2830
 
2684
2831
  .sb-list[data-sb-refreshable] > .sb-list-content,
2685
- .sb-scrollview[data-sb-refreshable] > .sb-scroll-content {
2832
+ .sb-scrollview[data-sb-refreshable] > .sb-scroll-content,
2833
+ [data-sb-refreshable] > .sb-navbar-large-title {
2686
2834
  transition: transform 0.3s var(--sb-nav-easing);
2687
2835
  }
2688
2836
 
2689
2837
  [data-sb-pulling] > .sb-list-content,
2690
- [data-sb-pulling] > .sb-scroll-content {
2838
+ [data-sb-pulling] > .sb-scroll-content,
2839
+ [data-sb-pulling] > .sb-navbar-large-title {
2691
2840
  transition: none;
2692
2841
  }
2693
2842
 
2694
2843
  [data-sb-refreshing] > .sb-list-content,
2695
- [data-sb-refreshing] > .sb-scroll-content {
2844
+ [data-sb-refreshing] > .sb-scroll-content,
2845
+ [data-sb-refreshing] > .sb-navbar-large-title {
2696
2846
  transform: translateY(40px);
2697
2847
  }
2698
2848
 
@@ -3270,3 +3420,59 @@ svg.sb-path {
3270
3420
  display: block;
3271
3421
  overflow: hidden;
3272
3422
  }
3423
+
3424
+ /* ---------------------------------------------------------------------------
3425
+ `host` elements (src/hostContent.ts): the page's own DOM in a box the
3426
+ engine sizes. The content element is positioned at the box's origin and
3427
+ takes its width; in `fill` sizing its height too. The box clips.
3428
+ ------------------------------------------------------------------------- */
3429
+ .sb-host {
3430
+ display: block;
3431
+ overflow: hidden;
3432
+ }
3433
+
3434
+ .sb-host > .sb-host-content {
3435
+ position: absolute;
3436
+ left: 0;
3437
+ top: 0;
3438
+ width: 100%;
3439
+ box-sizing: border-box;
3440
+ }
3441
+
3442
+ .sb-host[data-sb-sizing="fill"] > .sb-host-content {
3443
+ height: 100%;
3444
+ }
3445
+
3446
+ /* ---------------------------------------------------------------------------
3447
+ Islands (@swiftbrowser/react's `SwiftUIView`): a SwiftUI root inside a
3448
+ page's own layout, with the device's tokens and none of its frame: the
3449
+ frame's size, bezel and shadow are `.device:not(.sb-island)`'s above, so
3450
+ the page's own CSS, however specific, gives the island its width and
3451
+ height. In `content` fit the renderer sets the root's height
3452
+ from the layout.
3453
+ ------------------------------------------------------------------------- */
3454
+ .device.sb-island::before,
3455
+ .device.sb-island::after {
3456
+ display: none;
3457
+ }
3458
+
3459
+ .sb-island .device-screen {
3460
+ width: 100%;
3461
+ height: 100%;
3462
+ border-radius: 0;
3463
+ background: none;
3464
+ }
3465
+
3466
+ .sb-island[data-sb-fit="content"],
3467
+ .sb-island[data-sb-fit="content"] .device-screen,
3468
+ .sb-island[data-sb-fit="content"] .screen {
3469
+ position: relative;
3470
+ inset: auto;
3471
+ height: auto;
3472
+ }
3473
+
3474
+ /* `.onHover` (pointerEffects.ts): the whole box hovers, as SwiftUI uses the
3475
+ view's frame, so a hover box is hit in its empty space too. */
3476
+ .sb-styled[data-sb-hover] {
3477
+ pointer-events: auto;
3478
+ }