@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/src/styles.css CHANGED
@@ -67,6 +67,9 @@
67
67
  --sb-color-tertiary-system-background: #ffffff;
68
68
  --sb-color-tertiary-system-grouped-background: #f2f2f7;
69
69
  --sb-color-opaque-separator: #c6c6c8;
70
+ /* The stepper's capsule and the line between its halves. */
71
+ --sb-stepper-fill: var(--sb-color-tertiary-fill);
72
+ --sb-stepper-divider: var(--sb-color-opaque-separator);
70
73
  --sb-color-system-gray2: #aeaeb2;
71
74
  --sb-color-system-gray3: #c7c7cc;
72
75
  --sb-color-system-gray4: #d1d1d6;
@@ -115,7 +118,7 @@
115
118
  --sb-color-segment-selected: #ffffff;
116
119
  /* Menus, floating sheets and alerts are Liquid Glass too: translucent
117
120
  over a heavier backdrop blur, with the glass rim. */
118
- --sb-color-menu-background: rgba(250, 250, 250, 0.82);
121
+ --sb-color-menu-background: rgba(250, 250, 250, 0.97);
119
122
  --sb-sheet-glass: rgba(255, 255, 255, 0.55);
120
123
  }
121
124
 
@@ -141,6 +144,11 @@
141
144
  --sb-color-tertiary-system-background: #2c2c2e;
142
145
  --sb-color-tertiary-system-grouped-background: #2c2c2e;
143
146
  --sb-color-opaque-separator: #38383a;
147
+ /* iOS 26 in dark mode, measured: a fainter capsule than the tertiary fill
148
+ (rgba(120,120,128,.15) reads (18,18,19) over black) and a divider in the
149
+ tertiary label color ((83,83,86) over it). */
150
+ --sb-stepper-fill: rgba(120, 120, 128, 0.15);
151
+ --sb-stepper-divider: rgba(235, 235, 245, 0.3);
144
152
  --sb-color-system-gray2: #636366;
145
153
  --sb-color-system-gray3: #48484a;
146
154
  --sb-color-system-gray4: #3a3a3c;
@@ -177,7 +185,7 @@
177
185
  --sb-color-tabbar-background: rgba(29, 29, 29, 0.94);
178
186
  --sb-color-segment-track: rgba(118, 118, 128, 0.24);
179
187
  --sb-color-segment-selected: #5a5a5f;
180
- --sb-color-menu-background: rgba(37, 37, 37, 0.82);
188
+ --sb-color-menu-background: rgba(44, 44, 46, 0.97);
181
189
  /* The iPhone's dark floating sheet reads 42 over black: dark gray glass. */
182
190
  --sb-sheet-glass: rgba(60, 60, 60, 0.7);
183
191
  }
@@ -318,12 +326,6 @@
318
326
  fill: currentColor;
319
327
  }
320
328
 
321
- .glyph-wifi .wifi-band {
322
- fill: none;
323
- stroke: currentColor;
324
- stroke-linecap: round;
325
- }
326
-
327
329
  .glyph-battery .battery-shell {
328
330
  fill: none;
329
331
  stroke: currentColor;
@@ -378,6 +380,86 @@
378
380
  }
379
381
  }
380
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
+
381
463
  /* ---------------------------------------------------------------------------
382
464
  Device mode (html[data-sb-mode="device"], see src/deviceMode.ts)
383
465
 
@@ -417,6 +499,16 @@ html[data-sb-mode="device"] .device {
417
499
  }
418
500
 
419
501
  html[data-sb-mode="device"] .device::before,
502
+ /* `.statusBarHidden` on the screen showing: the drawn status bar fades, as
503
+ iOS fades the real one (the safe area stays, as it does under a notch). */
504
+ .status-bar {
505
+ transition: opacity 0.25s ease;
506
+ }
507
+
508
+ html[data-sb-status-hidden] .status-bar {
509
+ opacity: 0;
510
+ }
511
+
420
512
  html[data-sb-mode="device"] .device::after,
421
513
  html[data-sb-mode="device"] .dynamic-island,
422
514
  html[data-sb-mode="device"] .status-bar,
@@ -585,6 +677,21 @@ html[data-sb-mode="device"] .device-screen {
585
677
  user-select: none;
586
678
  }
587
679
 
680
+ /* Text is not selectable, as on iOS, unless `.textSelection(.enabled)` is
681
+ on it or above it (the renderer marks that box). Fields and editors are
682
+ inputs, selectable as ever. */
683
+ .sb-text {
684
+ user-select: none;
685
+ -webkit-user-select: none;
686
+ }
687
+
688
+ [data-sb-text-selection] .sb-text,
689
+ .sb-text[data-sb-text-selection] {
690
+ user-select: text;
691
+ -webkit-user-select: text;
692
+ -webkit-touch-callout: default;
693
+ }
694
+
588
695
  /* Image(systemName:): a 1em glyph centered in its line-height square; the
589
696
  renderer sets font-size from the resolved font. */
590
697
  .sb-image {
@@ -728,6 +835,24 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
728
835
  display: none;
729
836
  }
730
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
+
731
856
  .sb-scrollview[data-sb-axes="vertical"] {
732
857
  overflow-y: auto;
733
858
  }
@@ -1010,6 +1135,31 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1010
1135
  background: transparent;
1011
1136
  }
1012
1137
 
1138
+ /* A card of clear rows alone paints nothing, so it clips nothing either: a
1139
+ search field drawn in such a row (Notes', Reminders', Files') keeps its own
1140
+ corners instead of the card's larger ones cutting them. */
1141
+ .sb-section-rows:not(:has(> [data-sb-id]:not([data-sb-row-background='clear']))) {
1142
+ overflow: visible;
1143
+ }
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
+
1013
1163
  .sb-list-content > :not(.sb-section):first-child,
1014
1164
  .sb-list-content > .sb-section + :not(.sb-section) {
1015
1165
  border-top-left-radius: 26px;
@@ -1127,6 +1277,12 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1127
1277
  background: var(--sb-color-grouped-background);
1128
1278
  }
1129
1279
 
1280
+ /* So does a tab with a list as its content: the list is inset from the
1281
+ horizontal safe area (a phone on its side), the tab fills it. */
1282
+ .sb-tab:has(> .sb-list:not([data-sb-background="hidden"])) {
1283
+ background: var(--sb-color-grouped-background);
1284
+ }
1285
+
1130
1286
  /* Screens below the top one slide partially left and are hidden once the
1131
1287
  transition has finished. */
1132
1288
  .sb-navscreen[data-sb-nav-position="below"] {
@@ -1220,6 +1376,19 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1220
1376
  pointer-events: auto;
1221
1377
  }
1222
1378
 
1379
+ /* `.toolbar(.hidden, for: .navigationBar)`: the bar fades where it was (its
1380
+ items are gone from the layout; the capsules play their exit). */
1381
+ .sb-navbar {
1382
+ transition:
1383
+ opacity 0.25s ease,
1384
+ visibility 0.25s;
1385
+ }
1386
+
1387
+ .sb-navscreen[data-sb-bar-hidden] > .sb-navbar {
1388
+ opacity: 0;
1389
+ visibility: hidden;
1390
+ }
1391
+
1223
1392
  .sb-navbar-trailing:empty {
1224
1393
  pointer-events: none;
1225
1394
  }
@@ -1810,24 +1979,26 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1810
1979
  background: var(--sb-color-system-background);
1811
1980
  }
1812
1981
 
1813
- /* `sidebar` (iPadOS; a NavigationSplitView's sidebar list): the rows sit on
1814
- the list's background, without cards or hairlines, and only a selected
1815
- row is filled (its rounded fill, below). The engine lays it out like
1816
- `insetGrouped` with 44pt rows. */
1817
- .sb-list[data-sb-list-style="sidebar"] .sb-section-rows,
1818
- .sb-list[data-sb-list-style="sidebar"] .sb-section-rows > [data-sb-id],
1819
- .sb-list[data-sb-list-style="sidebar"] .sb-section-rows > [data-sb-ghost],
1820
- .sb-list[data-sb-list-style="sidebar"] > .sb-list-content > :not(.sb-section) {
1982
+ /* `sidebar` (iPadOS; a NavigationSplitView's sidebar list) in a regular
1983
+ width (layoutDom's `data-sb-list-look`): the rows sit on the list's
1984
+ background, without cards or hairlines, and only a selected row is filled
1985
+ (its rounded fill, below). The engine lays it out like `insetGrouped` with
1986
+ 44pt rows. In a compact width the list looks like `insetGrouped`; its
1987
+ sections still fold (above). */
1988
+ .sb-list[data-sb-list-look="sidebar"] .sb-section-rows,
1989
+ .sb-list[data-sb-list-look="sidebar"] .sb-section-rows > [data-sb-id],
1990
+ .sb-list[data-sb-list-look="sidebar"] .sb-section-rows > [data-sb-ghost],
1991
+ .sb-list[data-sb-list-look="sidebar"] > .sb-list-content > :not(.sb-section) {
1821
1992
  background: transparent;
1822
1993
  border-radius: 0;
1823
1994
  }
1824
1995
 
1825
- .sb-list[data-sb-list-style="sidebar"] .sb-section-rows > *::after,
1826
- .sb-list[data-sb-list-style="sidebar"] > .sb-list-content > :not(.sb-section)::after {
1996
+ .sb-list[data-sb-list-look="sidebar"] .sb-section-rows > *::after,
1997
+ .sb-list[data-sb-list-look="sidebar"] > .sb-list-content > :not(.sb-section)::after {
1827
1998
  display: none;
1828
1999
  }
1829
2000
 
1830
- .sb-list[data-sb-list-style="sidebar"] :is(.sb-section-rows, .sb-list-content) > :is(input, textarea) {
2001
+ .sb-list[data-sb-list-look="sidebar"] :is(.sb-section-rows, .sb-list-content) > :is(input, textarea) {
1831
2002
  background-image: none;
1832
2003
  }
1833
2004
 
@@ -2118,13 +2289,17 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2118
2289
  stroke-width: 2.5;
2119
2290
  }
2120
2291
 
2121
- /* The option menu: anchored under (or above) the control by the renderer.
2292
+ /* The option menu, as iOS 26 draws one: an inset panel (10px above and
2293
+ below the rows, 12px beside them) with large rounded corners, anchored
2294
+ under (or above) the control by the renderer, over a toolbar button.
2122
2295
  Closed, it fades out where it was (`.sb-picker-menu-exit`) and leaves. */
2123
2296
  .sb-picker-menu,
2124
2297
  .sb-picker-menu-exit {
2125
2298
  position: absolute;
2126
2299
  z-index: 6;
2127
- border-radius: 13px;
2300
+ box-sizing: border-box;
2301
+ padding: 10px 12px;
2302
+ border-radius: 26px;
2128
2303
  background: var(--sb-color-menu-background);
2129
2304
  backdrop-filter: blur(40px) saturate(180%);
2130
2305
  -webkit-backdrop-filter: blur(40px) saturate(180%);
@@ -2154,12 +2329,13 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2154
2329
  appearance: none;
2155
2330
  display: flex;
2156
2331
  align-items: center;
2157
- gap: 8px;
2332
+ gap: 13px;
2158
2333
  width: 100%;
2159
2334
  height: 44px;
2160
2335
  margin: 0;
2161
2336
  padding: 0 16px;
2162
2337
  border: 0;
2338
+ border-radius: 14px;
2163
2339
  background: none;
2164
2340
  color: var(--sb-color-primary);
2165
2341
  font: 400 var(--sb-font-size-body) / 1.3 var(--sb-font-family);
@@ -2169,19 +2345,19 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2169
2345
  -webkit-tap-highlight-color: transparent;
2170
2346
  }
2171
2347
 
2172
- :is(.sb-picker-menu-item, .sb-picker-menu-static) + :is(.sb-picker-menu-item, .sb-picker-menu-static) {
2173
- box-shadow: inset 0 1px 0 var(--sb-color-separator);
2174
- }
2175
-
2176
2348
  .sb-picker-menu-item:active {
2177
2349
  background: var(--sb-color-fill);
2178
2350
  }
2179
2351
 
2352
+ /* The checkmark's column (iOS 26): in a menu with checkable rows every item
2353
+ keeps it, empty unless checked, so titles and symbols line up; it sits
2354
+ closer to what follows than a symbol does. */
2180
2355
  .sb-picker-menu-check {
2181
2356
  display: inline-flex;
2182
2357
  align-items: center;
2183
2358
  justify-content: center;
2184
- width: 20px;
2359
+ width: 16px;
2360
+ margin-right: -4px;
2185
2361
  flex-shrink: 0;
2186
2362
  visibility: hidden;
2187
2363
  }
@@ -2191,8 +2367,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2191
2367
  }
2192
2368
 
2193
2369
  .sb-picker-menu-check > svg {
2194
- width: 1em;
2195
- height: 1em;
2370
+ width: 0.95em;
2371
+ height: 0.95em;
2196
2372
  stroke: currentColor;
2197
2373
  stroke-width: 2.5;
2198
2374
  }
@@ -2204,6 +2380,47 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2204
2380
  white-space: nowrap;
2205
2381
  }
2206
2382
 
2383
+ /* A menu with a check column (iOS 26): the check sits 24px in from the
2384
+ panel's edge, plain titles and section titles start together after it, and
2385
+ a symbol's column starts a little past them, its title further in. */
2386
+ .sb-picker-menu[data-sb-checks] .sb-picker-menu-item {
2387
+ padding-left: 12px;
2388
+ }
2389
+
2390
+ .sb-picker-menu[data-sb-checks] .sb-picker-menu-check {
2391
+ margin-right: -5px;
2392
+ }
2393
+
2394
+ .sb-picker-menu[data-sb-checks] .sb-picker-menu-symbol {
2395
+ width: 28px;
2396
+ margin: 0 4px 0 2px;
2397
+ }
2398
+
2399
+ .sb-picker-menu[data-sb-checks] .sb-picker-menu-caption {
2400
+ padding-left: 36px;
2401
+ }
2402
+
2403
+ /* A title with a subtitle under it, in a taller row. */
2404
+ .sb-picker-menu-item[data-sb-subtitled] {
2405
+ height: 58px;
2406
+ }
2407
+
2408
+ .sb-picker-menu-titles {
2409
+ display: flex;
2410
+ flex: 1 1 auto;
2411
+ flex-direction: column;
2412
+ min-width: 0;
2413
+ }
2414
+
2415
+ .sb-picker-menu-subtitle {
2416
+ overflow: hidden;
2417
+ color: var(--sb-color-secondary);
2418
+ font-size: 15px;
2419
+ line-height: 20px;
2420
+ text-overflow: ellipsis;
2421
+ white-space: nowrap;
2422
+ }
2423
+
2207
2424
  @keyframes sb-menu-in {
2208
2425
  from {
2209
2426
  opacity: 0;
@@ -2342,15 +2559,15 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2342
2559
  border-radius: 6px;
2343
2560
  }
2344
2561
 
2345
- /* Popover rows for menu items: a tap row shows its symbol trailing; a
2346
- destructive role is red; a disabled item dims; section headers are 32px
2347
- captions; `text` items are static 44px rows; groups are split by 8px bands. */
2562
+ /* Popover rows for menu items (iOS 26): a tap row's symbol leads its title;
2563
+ a destructive role is red; a disabled item dims; section headers are 30px
2564
+ captions; `text` items are static 44px rows; groups are split by a hairline
2565
+ with 9px above and below, inset from the panel's edges. No lines between items. */
2348
2566
  .sb-picker-menu-symbol {
2349
2567
  display: inline-flex;
2350
2568
  align-items: center;
2351
2569
  justify-content: center;
2352
- width: 22px;
2353
- margin-left: auto;
2570
+ width: 24px;
2354
2571
  flex-shrink: 0;
2355
2572
  }
2356
2573
 
@@ -2372,10 +2589,10 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2372
2589
  .sb-picker-menu-caption {
2373
2590
  display: flex;
2374
2591
  align-items: center;
2375
- height: 32px;
2592
+ height: 30px;
2376
2593
  padding: 0 16px;
2377
2594
  color: var(--sb-color-secondary);
2378
- font: 400 13px/16px var(--sb-font-family);
2595
+ font: 400 15px/20px var(--sb-font-family);
2379
2596
  white-space: nowrap;
2380
2597
  overflow: hidden;
2381
2598
  text-overflow: ellipsis;
@@ -2394,8 +2611,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2394
2611
  }
2395
2612
 
2396
2613
  .sb-picker-menu-separator {
2397
- height: 8px;
2398
- background: var(--sb-color-fill);
2614
+ height: 1px;
2615
+ margin: 9px 12px;
2616
+ background: var(--sb-color-separator);
2399
2617
  }
2400
2618
 
2401
2619
  /* Inline picker: a dim label row (when the label is non-empty) and 44px
@@ -3405,9 +3623,16 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
3405
3623
  opacity: 0.4;
3406
3624
  }
3407
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
+
3408
3634
  .sb-drag-preview {
3409
3635
  z-index: 60;
3410
- pointer-events: none;
3411
3636
  opacity: 0.9;
3412
3637
  filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.25));
3413
3638
  will-change: transform;
@@ -3656,7 +3881,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
3656
3881
  .sb-alert-layer > [hidden],
3657
3882
  .sb-datepicker > [hidden],
3658
3883
  .sb-stepper > [hidden],
3659
- .sb-slider > [hidden] {
3884
+ .sb-slider > [hidden],
3885
+ .sb-slider-labels > [hidden] {
3660
3886
  display: none;
3661
3887
  }
3662
3888
 
@@ -3897,7 +4123,7 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
3897
4123
  display: flex;
3898
4124
  box-sizing: border-box;
3899
4125
  border-radius: 16px;
3900
- background: var(--sb-color-tertiary-fill);
4126
+ background: var(--sb-stepper-fill);
3901
4127
  overflow: hidden;
3902
4128
  }
3903
4129
 
@@ -3930,7 +4156,7 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
3930
4156
  top: 7px;
3931
4157
  bottom: 7px;
3932
4158
  width: 1px;
3933
- background: var(--sb-color-opaque-separator);
4159
+ background: var(--sb-stepper-divider);
3934
4160
  }
3935
4161
 
3936
4162
  .sb-stepper-button > svg {
@@ -4213,3 +4439,109 @@ svg.sb-path {
4213
4439
  .sb-styled[data-sb-hover] {
4214
4440
  pointer-events: auto;
4215
4441
  }
4442
+
4443
+ /* ---------------------------------------------------------------------------
4444
+ `disclosuregroup`: the label (the first child) with a chevron at its
4445
+ trailing edge that turns down as the content opens below it; a button
4446
+ over the label takes the tap (layoutDom places both from the label's
4447
+ frame). `section` headers of `Section(isExpanded:)` get the chevron too,
4448
+ in a sidebar-style list, where the section collapses as on iOS.
4449
+ --------------------------------------------------------------------------- */
4450
+ .sb-disclosure-toggle {
4451
+ position: absolute;
4452
+ margin: 0;
4453
+ padding: 0;
4454
+ border: 0;
4455
+ background: transparent;
4456
+ cursor: pointer;
4457
+ -webkit-tap-highlight-color: transparent;
4458
+ }
4459
+
4460
+ .sb-disclosure-chevron {
4461
+ position: absolute;
4462
+ stroke-width: 2.5;
4463
+ color: var(--sb-color-accent);
4464
+ pointer-events: none;
4465
+ transition: transform 0.25s ease-in-out;
4466
+ }
4467
+
4468
+ .sb-disclosuregroup[data-sb-expanded] > .sb-disclosure-chevron {
4469
+ transform: rotate(90deg);
4470
+ }
4471
+
4472
+ /* Collapsed: the content (a disclosure group's, a sidebar section's rows) is hidden by the engine, without a frame. */
4473
+ .sb-disclosuregroup > .sb-slot > [data-sb-id][hidden],
4474
+ .sb-disclosuregroup > .sb-slot > [data-sb-ghost][hidden],
4475
+ .sb-section[data-sb-collapsible] .sb-section-rows > [data-sb-id][hidden],
4476
+ .sb-section[data-sb-collapsible] .sb-section-rows > [data-sb-ghost][hidden] {
4477
+ display: none;
4478
+ }
4479
+
4480
+ .sb-list[data-sb-list-style='sidebar'] .sb-section[data-sb-collapsible] > .sb-section-header,
4481
+ .sb-list[data-sb-list-style='sidebar'] .sb-section[data-sb-collapsible] .sb-sectionheader {
4482
+ cursor: pointer;
4483
+ }
4484
+
4485
+ .sb-list[data-sb-list-style='sidebar'] .sb-section[data-sb-collapsible] > .sb-section-header::after,
4486
+ .sb-list[data-sb-list-style='sidebar'] .sb-section[data-sb-collapsible] .sb-sectionheader::after {
4487
+ /* Centered 10 in from the content's trailing edge, over the rows' own chevrons (the engine's 20pt chevron box). */
4488
+ content: '';
4489
+ position: absolute;
4490
+ right: 6.5px;
4491
+ top: 50%;
4492
+ width: 7px;
4493
+ height: 7px;
4494
+ margin-top: -5px;
4495
+ border-right: 2px solid var(--sb-color-accent);
4496
+ border-bottom: 2px solid var(--sb-color-accent);
4497
+ transform: rotate(-45deg);
4498
+ transition: transform 0.25s ease-in-out;
4499
+ }
4500
+
4501
+ .sb-list[data-sb-list-style='sidebar'] .sb-section[data-sb-collapsible][data-sb-expanded] > .sb-section-header::after,
4502
+ .sb-list[data-sb-list-style='sidebar'] .sb-section[data-sb-collapsible][data-sb-expanded] .sb-sectionheader::after {
4503
+ transform: rotate(45deg);
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
@@ -147,13 +147,6 @@ const HIERARCHICAL_TEXT: Record<1 | 2 | 3 | 4, string> = {
147
147
  * colors' ratio has it).
148
148
  */
149
149
  export const HIERARCHY_OPACITY: Record<1 | 2 | 3 | 4, number> = { 1: 1, 2: 0.5, 3: 0.25, 4: 0.18 };
150
- const HIERARCHICAL_PAINT: Record<1 | 2 | 3 | 4, string> = {
151
- 1: 'var(--sb-color-fill)',
152
- 2: 'var(--sb-color-secondary-fill)',
153
- 3: 'var(--sb-color-tertiary-fill)',
154
- 4: 'var(--sb-color-quaternary-fill)',
155
- };
156
-
157
150
  /** Opacity of the system background each material shows over the blurred backdrop. */
158
151
  export const MATERIAL_ALPHA: Record<Exclude<MaterialName, 'glass' | 'glassClear'>, number> = {
159
152
  ultraThin: 0.55,
@@ -178,7 +171,7 @@ export function materialAlpha(name: MaterialName): number {
178
171
  * stand out. A non-color style's top-level `opacity` multiplies the result
179
172
  * (each stop of a gradient, the material's alpha) like a semantic color's `a`.
180
173
  */
181
- export function styleToCSS(style: ShapeStyle, role: PaintRole): PaintCSS {
174
+ export function styleToCSS(style: ShapeStyle, _role: PaintRole): PaintCSS {
182
175
  if (isColor(style)) {
183
176
  const value = colorToCSS(style);
184
177
  return { kind: 'color', value, solid: value };
@@ -186,7 +179,9 @@ export function styleToCSS(style: ShapeStyle, role: PaintRole): PaintCSS {
186
179
  const opacity = style.opacity;
187
180
  if (isHierarchicalStyle(style)) {
188
181
  const level = hierarchicalLevel(style.hierarchical);
189
- const value = withOpacity(role === 'text' ? HIERARCHICAL_TEXT[level] : HIERARCHICAL_PAINT[level], opacity);
182
+ // The levels of the foreground, as SwiftUI paints them in any role: a
183
+ // shape filled or stroked with `.primary` is the label color, not a fill gray.
184
+ const value = withOpacity(HIERARCHICAL_TEXT[level], opacity);
190
185
  return { kind: 'color', value, solid: value };
191
186
  }
192
187
  if (isLinearGradientStyle(style)) {
@@ -357,7 +352,7 @@ export const PERSPECTIVE_BASE_PX = 500;
357
352
  * `500px / perspective` (a smaller SwiftUI perspective is a flatter
358
353
  * projection, as `perspective: 0.5` is in SwiftUI).
359
354
  */
360
- export function effectsCSS(style: Style): EffectsCSS {
355
+ export function effectsCSS(style: Style, options: { shadow?: boolean } = {}): EffectsCSS {
361
356
  const out: EffectsCSS = {};
362
357
  const transforms: string[] = [];
363
358
  if (style.offset) {
@@ -413,11 +408,7 @@ export function effectsCSS(style: Style): EffectsCSS {
413
408
  filters.push(`hue-rotate(${round(style.hueRotation)}deg)`);
414
409
  }
415
410
  if (style.colorInvert === true) filters.push('invert(1)');
416
- if (style.shadow) {
417
- const { color, radius, x, y } = style.shadow;
418
- const css = color ? colorToCSS(color) : 'rgba(0, 0, 0, 0.33)';
419
- filters.push(`drop-shadow(${finite(x)}px ${finite(y)}px ${Math.max(0, finite(radius))}px ${css})`);
420
- }
411
+ if (style.shadow && options.shadow !== false) filters.push(`drop-shadow(${shadowCSS(style.shadow)})`);
421
412
  if (filters.length) out.filter = filters.join(' ');
422
413
  // Phase 7.
423
414
  if (style.hidden === true) out.hidden = true;
@@ -425,6 +416,13 @@ export function effectsCSS(style: Style): EffectsCSS {
425
416
  return out;
426
417
  }
427
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
+
428
426
  /**
429
427
  * Phase 7 `.mask { }` with a ShapeStyle: a CSS `mask-image` whose alpha is
430
428
  * the style's. A `linearGradient` / `radialGradient` becomes the matching CSS