@swiftbrowser/web 0.3.1 → 0.4.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/README.md +3 -1
- package/package.json +4 -2
- package/src/animation.ts +154 -3
- package/src/dragDrop.ts +549 -0
- package/src/hostContent.ts +37 -0
- package/src/interaction.ts +128 -22
- package/src/largeTitle.ts +95 -54
- package/src/layout/engine.ts +415 -88
- package/src/layout/index.ts +5 -2
- package/src/layout/measure.ts +2 -0
- package/src/layout/text.ts +6 -3
- package/src/layout/types.ts +16 -1
- package/src/layout/typography.ts +30 -10
- package/src/layoutDom.ts +66 -16
- package/src/pointerEffects.ts +182 -0
- package/src/protocol.ts +188 -9
- package/src/renderer.ts +316 -15
- package/src/scrollEdge.ts +87 -0
- package/src/scrollTo.ts +6 -0
- package/src/styles.css +413 -16
- package/src/typography.ts +5 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS 26's scroll edge effect at the bottom: a list or scroll view that runs
|
|
3
|
+
* down under a tab bar, or under its screen's bottom toolbar or search field,
|
|
4
|
+
* has its content inset by the bar (the engine's `bottomInset` chrome rect,
|
|
5
|
+
* layout/engine.ts). While content is under the bar (more of it below) its
|
|
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
|
+
* (styles.css). The top edge's effect is the bar's own (largeTitle.ts).
|
|
9
|
+
*
|
|
10
|
+
* `.scrollEdgeEffectStyle(_:for:)` / `.scrollEdgeEffectHidden(_:for:)` reach
|
|
11
|
+
* the scroll view as `edgeEffect` props, which become `data-sb-edge-top` /
|
|
12
|
+
* `data-sb-edge-bottom` on it (`hard` or `hidden`; soft is the default) and
|
|
13
|
+
* are copied to the element that draws that edge's effect.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { EdgeEffectProps } from './protocol';
|
|
17
|
+
import type { Rect } from './layout';
|
|
18
|
+
|
|
19
|
+
const bound = new WeakSet<HTMLElement>();
|
|
20
|
+
/** The scroll view whose state each screen's (or tab's) bottom effect shows. */
|
|
21
|
+
const claimedBy = new WeakMap<HTMLElement, HTMLElement>();
|
|
22
|
+
|
|
23
|
+
/** A list's or scroll view's `edgeEffect` props, as `data-sb-edge-top` / `data-sb-edge-bottom`. */
|
|
24
|
+
export function applyEdgeEffect(el: HTMLElement, effect: EdgeEffectProps | undefined): void {
|
|
25
|
+
setValue(el, 'sbEdgeTop', edgeValue(effect?.top));
|
|
26
|
+
setValue(el, 'sbEdgeBottom', edgeValue(effect?.bottom));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Soft is the default effect; only the others are marked. */
|
|
30
|
+
function edgeValue(value: string | undefined): string | null {
|
|
31
|
+
return value === 'hard' || value === 'hidden' ? value : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The element that draws the bottom effect over a scroll view: its screen, else the tab it fills. */
|
|
35
|
+
function ownerOf(scroller: HTMLElement): HTMLElement | null {
|
|
36
|
+
return scroller.closest<HTMLElement>('.sb-navscreen') ?? scroller.closest<HTMLElement>('.sb-tab');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Called for every list and scroll view the layout reaches with the engine's
|
|
41
|
+
* `bottomInset` rect (absent when no bar covers its bottom).
|
|
42
|
+
*/
|
|
43
|
+
export function bindScrollEdgeBottom(scroller: HTMLElement, covered: Rect | undefined): void {
|
|
44
|
+
const owner = ownerOf(scroller);
|
|
45
|
+
if (covered === undefined || covered.height <= 0) {
|
|
46
|
+
if (scroller.dataset['sbUnderBottom'] !== undefined) delete scroller.dataset['sbUnderBottom'];
|
|
47
|
+
if (owner && claimedBy.get(owner) === scroller) release(owner);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setValue(scroller, 'sbUnderBottom', '');
|
|
51
|
+
scroller.style.setProperty('--sb-bottom-edge', `${covered.height}px`);
|
|
52
|
+
if (!bound.has(scroller)) {
|
|
53
|
+
bound.add(scroller);
|
|
54
|
+
scroller.addEventListener('scroll', () => syncScrollEdgeBottom(scroller), { passive: true });
|
|
55
|
+
}
|
|
56
|
+
syncScrollEdgeBottom(scroller);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function syncScrollEdgeBottom(scroller: HTMLElement): void {
|
|
60
|
+
if (scroller.dataset['sbUnderBottom'] === undefined) return;
|
|
61
|
+
const owner = ownerOf(scroller);
|
|
62
|
+
if (!owner) return;
|
|
63
|
+
claimedBy.set(owner, scroller);
|
|
64
|
+
// Content is under the bar until the end has scrolled clear of it (the
|
|
65
|
+
// inset is room past the content's end).
|
|
66
|
+
const under = scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop > 1;
|
|
67
|
+
setValue(owner, 'sbScrolledBottom', under ? '' : null);
|
|
68
|
+
setValue(owner, 'sbEdgeBottom', scroller.dataset['sbEdgeBottom'] ?? null);
|
|
69
|
+
const height = scroller.style.getPropertyValue('--sb-bottom-edge');
|
|
70
|
+
if (owner.style.getPropertyValue('--sb-bottom-edge') !== height) owner.style.setProperty('--sb-bottom-edge', height);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function release(owner: HTMLElement): void {
|
|
74
|
+
claimedBy.delete(owner);
|
|
75
|
+
setValue(owner, 'sbScrolledBottom', null);
|
|
76
|
+
setValue(owner, 'sbEdgeBottom', null);
|
|
77
|
+
owner.style.removeProperty('--sb-bottom-edge');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Sets a data attribute (by dataset key) to `value`, or removes it for null; untouched when unchanged. */
|
|
81
|
+
export function setValue(el: HTMLElement, key: string, value: string | null): void {
|
|
82
|
+
if (value === null) {
|
|
83
|
+
if (el.dataset[key] !== undefined) delete el.dataset[key];
|
|
84
|
+
} else if (el.dataset[key] !== value) {
|
|
85
|
+
el.dataset[key] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/scrollTo.ts
CHANGED
|
@@ -95,6 +95,12 @@ export function scrollToTarget(
|
|
|
95
95
|
const viewport: Rect = { ...frame };
|
|
96
96
|
const covered = result.chrome.get(at)?.['bottomInset'];
|
|
97
97
|
if (covered) viewport.height = Math.max(0, covered.y - frame.y);
|
|
98
|
+
// Under its screen's bar the visible area starts below the bar.
|
|
99
|
+
const under = result.chrome.get(at)?.['topInset'];
|
|
100
|
+
if (under) {
|
|
101
|
+
viewport.y += under.height;
|
|
102
|
+
viewport.height = Math.max(0, viewport.height - under.height);
|
|
103
|
+
}
|
|
98
104
|
let left = el.scrollLeft;
|
|
99
105
|
let top = el.scrollTop;
|
|
100
106
|
let changed = false;
|
package/src/styles.css
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
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,
|
|
@@ -701,11 +703,20 @@ html[data-sb-mode="device"] .status {
|
|
|
701
703
|
line-height: 1;
|
|
702
704
|
}
|
|
703
705
|
|
|
706
|
+
/* A symbol fills its box as SF Symbols do: at a text size a circle symbol is
|
|
707
|
+
about 1.28× the font size (21.7 pt at body's 17), and the glyphs here
|
|
708
|
+
occupy 20 of their 24-unit grid (a little more with their stroke), so
|
|
709
|
+
the SVG is 1.43em, centred in the
|
|
710
|
+
line-height box the engine gives the image (the box is unchanged; the
|
|
711
|
+
glyph overflows it a little, as a symbol's bounds do). The stroke, 1.4
|
|
712
|
+
units, is then 1.5 px at body, the weight iOS draws regular symbols in. */
|
|
704
713
|
.sb-image > svg {
|
|
705
714
|
display: block;
|
|
706
|
-
|
|
707
|
-
|
|
715
|
+
flex: none;
|
|
716
|
+
width: 1.43em;
|
|
717
|
+
height: 1.43em;
|
|
708
718
|
stroke: currentColor;
|
|
719
|
+
stroke-width: 1.4;
|
|
709
720
|
}
|
|
710
721
|
|
|
711
722
|
/* An indeterminate ProgressView: the activity indicator (symbols.ts), in
|
|
@@ -949,6 +960,19 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
949
960
|
text-align: left;
|
|
950
961
|
}
|
|
951
962
|
|
|
963
|
+
/* SF Pro's capitals start 0.2476em below the top of a line SwiftUI lays out
|
|
964
|
+
(ascender 0.952em − cap height 0.705em): trim the first line to its cap
|
|
965
|
+
height and put it there, so text the engine places lands where iOS draws
|
|
966
|
+
it, whatever font the page has. */
|
|
967
|
+
@supports (text-box: trim-start cap alphabetic) {
|
|
968
|
+
.sb-text,
|
|
969
|
+
.sb-section-header,
|
|
970
|
+
.sb-section-footer {
|
|
971
|
+
text-box: trim-start cap alphabetic;
|
|
972
|
+
padding-top: var(--sb-cap-inset);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
952
976
|
.sb-section-header[hidden],
|
|
953
977
|
.sb-section-footer[hidden] {
|
|
954
978
|
display: none;
|
|
@@ -1259,11 +1283,16 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1259
1283
|
}
|
|
1260
1284
|
|
|
1261
1285
|
/* 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.
|
|
1286
|
+
(src/largeTitle.ts moves it into it); drags on it scroll that view. There
|
|
1287
|
+
it is content, under the bar. */
|
|
1263
1288
|
.sb-navbar-large-title {
|
|
1264
1289
|
pointer-events: none;
|
|
1265
1290
|
}
|
|
1266
1291
|
|
|
1292
|
+
[data-sb-under-bar] > .sb-navbar-large-title {
|
|
1293
|
+
z-index: auto;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1267
1296
|
/* Until the large title has scrolled under the bar, the inline title (whose
|
|
1268
1297
|
rect the engine reports for a title that can collapse) stays hidden. */
|
|
1269
1298
|
.sb-navscreen[data-sb-display="large"] > .sb-navbar > .sb-navbar-title {
|
|
@@ -1275,11 +1304,115 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1275
1304
|
opacity: 1;
|
|
1276
1305
|
}
|
|
1277
1306
|
|
|
1278
|
-
/* iOS 26's scroll edge effect
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1307
|
+
/* iOS 26's scroll edge effect: a screen's root list or scroll view runs up
|
|
1308
|
+
under the bar (the engine insets its content by the bar), and once content
|
|
1309
|
+
is under it (`data-sb-scrolled`, src/largeTitle.ts) the bar blurs it and
|
|
1310
|
+
fades it into the background toward the top of the screen, a little past
|
|
1311
|
+
the bar's bottom edge. The bar's items stay on their glass. */
|
|
1312
|
+
.sb-navscreen {
|
|
1313
|
+
--sb-edge-color: var(--sb-color-system-background);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
.sb-navscreen:has(> .sb-navscreen-body .sb-list:is([data-sb-under-bar], [data-sb-under-bottom]):not([data-sb-list-style="plain"])) {
|
|
1317
|
+
--sb-edge-color: var(--sb-color-grouped-background);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.sb-navbar::before {
|
|
1321
|
+
content: "";
|
|
1322
|
+
position: absolute;
|
|
1323
|
+
inset: 0 0 -20px;
|
|
1324
|
+
z-index: -1;
|
|
1325
|
+
pointer-events: none;
|
|
1326
|
+
opacity: 0;
|
|
1327
|
+
transition: opacity 0.2s ease;
|
|
1328
|
+
background: linear-gradient(
|
|
1329
|
+
to bottom,
|
|
1330
|
+
var(--sb-edge-color) 0%,
|
|
1331
|
+
color-mix(in srgb, var(--sb-edge-color) 75%, transparent) 45%,
|
|
1332
|
+
transparent 100%
|
|
1333
|
+
);
|
|
1334
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1335
|
+
backdrop-filter: blur(6px);
|
|
1336
|
+
-webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent);
|
|
1337
|
+
mask-image: linear-gradient(to bottom, #000 60%, transparent);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.sb-navscreen[data-sb-scrolled] > .sb-navbar::before {
|
|
1341
|
+
opacity: 1;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/* At the bottom: a list or scroll view runs under the tab bar, or its
|
|
1345
|
+
screen's bottom toolbar or search field (the engine insets its content by
|
|
1346
|
+
the bar, `--sb-bottom-edge` tall), and while content is under it
|
|
1347
|
+
(`data-sb-scrolled-bottom`, src/scrollEdge.ts) its screen (over the
|
|
1348
|
+
screen's content: the body is static, so its pseudo-element is placed in
|
|
1349
|
+
the screen, whose own `::after` dims it under a pushed screen), or the
|
|
1350
|
+
tab it fills, blurs it and fades it into the background toward the bottom
|
|
1351
|
+
of the screen, from a little above the bar. The bars stay on top. */
|
|
1352
|
+
.sb-tab {
|
|
1353
|
+
--sb-edge-color: var(--sb-color-system-background);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.sb-tab:has(> .sb-list[data-sb-under-bottom]:not([data-sb-list-style="plain"])) {
|
|
1357
|
+
--sb-edge-color: var(--sb-color-grouped-background);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
.sb-navscreen > .sb-navscreen-body::after,
|
|
1361
|
+
.sb-tab::after {
|
|
1362
|
+
content: "";
|
|
1363
|
+
position: absolute;
|
|
1364
|
+
left: 0;
|
|
1365
|
+
right: 0;
|
|
1366
|
+
bottom: 0;
|
|
1367
|
+
height: calc(var(--sb-bottom-edge, 0px) + 20px);
|
|
1368
|
+
z-index: 1;
|
|
1369
|
+
pointer-events: none;
|
|
1370
|
+
opacity: 0;
|
|
1371
|
+
transition: opacity 0.2s ease;
|
|
1372
|
+
background: linear-gradient(
|
|
1373
|
+
to top,
|
|
1374
|
+
var(--sb-edge-color) 0%,
|
|
1375
|
+
color-mix(in srgb, var(--sb-edge-color) 75%, transparent) 45%,
|
|
1376
|
+
transparent 100%
|
|
1377
|
+
);
|
|
1378
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1379
|
+
backdrop-filter: blur(6px);
|
|
1380
|
+
-webkit-mask-image: linear-gradient(to top, #000 60%, transparent);
|
|
1381
|
+
mask-image: linear-gradient(to top, #000 60%, transparent);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
.sb-navscreen[data-sb-scrolled-bottom] > .sb-navscreen-body::after,
|
|
1385
|
+
.sb-tab[data-sb-scrolled-bottom]::after {
|
|
1386
|
+
opacity: 1;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/* `.scrollEdgeEffectStyle(.hard, for:)`: an opaque backing exactly behind
|
|
1390
|
+
the bar with a dividing line, no fade; `.scrollEdgeEffectHidden`: none. */
|
|
1391
|
+
.sb-navscreen[data-sb-edge-top="hard"] > .sb-navbar::before {
|
|
1392
|
+
inset: 0;
|
|
1393
|
+
background: var(--sb-edge-color);
|
|
1394
|
+
-webkit-backdrop-filter: none;
|
|
1395
|
+
backdrop-filter: none;
|
|
1396
|
+
-webkit-mask-image: none;
|
|
1397
|
+
mask-image: none;
|
|
1398
|
+
border-bottom: 0.5px solid var(--sb-color-separator);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
.sb-navscreen[data-sb-edge-bottom="hard"] > .sb-navscreen-body::after,
|
|
1402
|
+
.sb-tab[data-sb-edge-bottom="hard"]::after {
|
|
1403
|
+
height: var(--sb-bottom-edge, 0px);
|
|
1404
|
+
background: var(--sb-edge-color);
|
|
1405
|
+
-webkit-backdrop-filter: none;
|
|
1406
|
+
backdrop-filter: none;
|
|
1407
|
+
-webkit-mask-image: none;
|
|
1408
|
+
mask-image: none;
|
|
1409
|
+
border-top: 0.5px solid var(--sb-color-separator);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
.sb-navscreen[data-sb-edge-top="hidden"] > .sb-navbar::before,
|
|
1413
|
+
.sb-navscreen[data-sb-edge-bottom="hidden"] > .sb-navscreen-body::after,
|
|
1414
|
+
.sb-tab[data-sb-edge-bottom="hidden"]::after {
|
|
1415
|
+
display: none;
|
|
1283
1416
|
}
|
|
1284
1417
|
|
|
1285
1418
|
/* NavigationLink: a tappable label. Inside a list it is a row with a chevron
|
|
@@ -1539,6 +1672,27 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1539
1672
|
background: var(--sb-color-system-background);
|
|
1540
1673
|
}
|
|
1541
1674
|
|
|
1675
|
+
/* `sidebar` (iPadOS; a NavigationSplitView's sidebar list): the rows sit on
|
|
1676
|
+
the list's background, without cards or hairlines, and only a selected
|
|
1677
|
+
row is filled (its rounded fill, below). The engine lays it out like
|
|
1678
|
+
`insetGrouped` with 44pt rows. */
|
|
1679
|
+
.sb-list[data-sb-list-style="sidebar"] .sb-section-rows,
|
|
1680
|
+
.sb-list[data-sb-list-style="sidebar"] .sb-section-rows > [data-sb-id],
|
|
1681
|
+
.sb-list[data-sb-list-style="sidebar"] .sb-section-rows > [data-sb-ghost],
|
|
1682
|
+
.sb-list[data-sb-list-style="sidebar"] > .sb-list-content > :not(.sb-section) {
|
|
1683
|
+
background: transparent;
|
|
1684
|
+
border-radius: 0;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
.sb-list[data-sb-list-style="sidebar"] .sb-section-rows > *::after,
|
|
1688
|
+
.sb-list[data-sb-list-style="sidebar"] > .sb-list-content > :not(.sb-section)::after {
|
|
1689
|
+
display: none;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
.sb-list[data-sb-list-style="sidebar"] :is(.sb-section-rows, .sb-list-content) > :is(input, textarea) {
|
|
1693
|
+
background-image: none;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1542
1696
|
/* ---------------------------------------------------------------------------
|
|
1543
1697
|
Phase 4: tab views. The tabs are stacked in the content slot (every tab has
|
|
1544
1698
|
the tab view's frame); the selected one is visible, the others keep their
|
|
@@ -2152,10 +2306,19 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2152
2306
|
--sb-color-accent: var(--sb-color-primary);
|
|
2153
2307
|
}
|
|
2154
2308
|
|
|
2155
|
-
/*
|
|
2309
|
+
/* Toolbar items in the bar are laid out in the screen's body, under the bar
|
|
2310
|
+
element (z 1): they stack above it, so its scroll edge effect blurs the
|
|
2311
|
+
content under the bar and not them. */
|
|
2312
|
+
.sb-toolbar:is([data-sb-placement="leading"], [data-sb-placement="trailing"], [data-sb-placement="principal"]) {
|
|
2313
|
+
z-index: 2;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/* Their symbols are drawn a little larger and heavier (semibold), as iOS 26
|
|
2317
|
+
draws bar buttons: a plus is 17.7 pt with 2.33 pt bars there. */
|
|
2156
2318
|
.sb-toolbar:is([data-sb-placement="leading"], [data-sb-placement="trailing"]) .sb-image > svg {
|
|
2157
|
-
|
|
2158
|
-
|
|
2319
|
+
width: 1.57em;
|
|
2320
|
+
height: 1.57em;
|
|
2321
|
+
stroke-width: 1.85;
|
|
2159
2322
|
}
|
|
2160
2323
|
|
|
2161
2324
|
.sb-toolbar[data-sb-placement="bottom"] {
|
|
@@ -2254,12 +2417,61 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2254
2417
|
background: var(--sb-color-system-background);
|
|
2255
2418
|
}
|
|
2256
2419
|
|
|
2420
|
+
/* `.inspector`: the content and a trailing panel (protocol children placed by
|
|
2421
|
+
the engine). The panel draws the hairline before it, in the 1pt gap the
|
|
2422
|
+
engine leaves, so the hairline slides with it. */
|
|
2423
|
+
.sb-inspector {
|
|
2424
|
+
overflow: hidden;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
.sb-inspectorpanel {
|
|
2428
|
+
background: var(--sb-color-system-background);
|
|
2429
|
+
box-shadow: -1px 0 0 var(--sb-color-separator);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2257
2432
|
.sb-navsplit-separator {
|
|
2258
2433
|
position: absolute;
|
|
2259
2434
|
background: var(--sb-color-separator);
|
|
2260
2435
|
pointer-events: none;
|
|
2261
2436
|
}
|
|
2262
2437
|
|
|
2438
|
+
/* iPadOS: the sidebar toggle, a 44pt glass circle at the leading edge of
|
|
2439
|
+
the bar row (the sidebar's, or the detail's while the sidebar is hidden),
|
|
2440
|
+
over the columns. */
|
|
2441
|
+
.sb-navsplit-toggle {
|
|
2442
|
+
position: absolute;
|
|
2443
|
+
z-index: 2;
|
|
2444
|
+
appearance: none;
|
|
2445
|
+
display: inline-flex;
|
|
2446
|
+
align-items: center;
|
|
2447
|
+
justify-content: center;
|
|
2448
|
+
margin: 0;
|
|
2449
|
+
padding: 0;
|
|
2450
|
+
border: 0;
|
|
2451
|
+
border-radius: 22px;
|
|
2452
|
+
background: var(--sb-glass-background);
|
|
2453
|
+
box-shadow: var(--sb-glass-shadow);
|
|
2454
|
+
backdrop-filter: blur(12px) saturate(180%);
|
|
2455
|
+
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
2456
|
+
color: var(--sb-color-primary);
|
|
2457
|
+
cursor: pointer;
|
|
2458
|
+
touch-action: manipulation;
|
|
2459
|
+
-webkit-tap-highlight-color: transparent;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
.sb-navsplit-toggle[hidden] {
|
|
2463
|
+
display: none;
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
.sb-navsplit-toggle:active {
|
|
2467
|
+
opacity: 0.3;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
.sb-navsplit-toggle > svg {
|
|
2471
|
+
width: 22px;
|
|
2472
|
+
height: 22px;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2263
2475
|
/* A selected `navlink` row (`List(selection:)`): the sidebar-style rounded
|
|
2264
2476
|
fill. The engine omits the chevron inside a navsplit sidebar; elsewhere
|
|
2265
2477
|
the chevron stays. The list-row selectors outrank the per-style row
|
|
@@ -2563,7 +2775,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2563
2775
|
}
|
|
2564
2776
|
|
|
2565
2777
|
/* `listrow`: the edit-mode control at the engine's `editControl` rect (hidden
|
|
2566
|
-
otherwise
|
|
2778
|
+
otherwise; its 22px circle is centred 28 from the card's edge, as on
|
|
2779
|
+
iOS); a selected row reuses the Phase 7 selected-row fill. */
|
|
2567
2780
|
.sb-listrow-control {
|
|
2568
2781
|
appearance: none;
|
|
2569
2782
|
position: absolute;
|
|
@@ -2578,6 +2791,12 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2578
2791
|
cursor: pointer;
|
|
2579
2792
|
touch-action: manipulation;
|
|
2580
2793
|
-webkit-tap-highlight-color: transparent;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
/* The controls fade in as the list starts editing (layoutDom marks them
|
|
2797
|
+
while they appear); a row moved in the DOM by a reorder keeps them put. */
|
|
2798
|
+
.sb-listrow-control[data-sb-appear],
|
|
2799
|
+
.sb-listrow-grip[data-sb-appear] {
|
|
2581
2800
|
animation: sb-fade-in 0.25s ease-out both;
|
|
2582
2801
|
}
|
|
2583
2802
|
|
|
@@ -2585,6 +2804,89 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2585
2804
|
display: none;
|
|
2586
2805
|
}
|
|
2587
2806
|
|
|
2807
|
+
/* `listrow`: the reorder grip of a movable row at the engine's
|
|
2808
|
+
`reorderControl` rect (edit mode only): three gray lines, which a press
|
|
2809
|
+
lifts the row by (src/dragDrop.ts). */
|
|
2810
|
+
.sb-listrow-grip {
|
|
2811
|
+
position: absolute;
|
|
2812
|
+
z-index: 2;
|
|
2813
|
+
display: flex;
|
|
2814
|
+
align-items: center;
|
|
2815
|
+
justify-content: center;
|
|
2816
|
+
color: var(--sb-color-tertiary-label);
|
|
2817
|
+
cursor: grab;
|
|
2818
|
+
touch-action: none;
|
|
2819
|
+
user-select: none;
|
|
2820
|
+
-webkit-user-select: none;
|
|
2821
|
+
-webkit-touch-callout: none;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
.sb-listrow-grip[hidden] {
|
|
2825
|
+
display: none;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
.sb-listrow-grip > svg {
|
|
2829
|
+
width: 21px;
|
|
2830
|
+
height: 11px;
|
|
2831
|
+
overflow: visible;
|
|
2832
|
+
stroke: currentColor;
|
|
2833
|
+
stroke-width: 1.33;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
/* A row being reordered: its neighbours slide out of its way (the
|
|
2837
|
+
transition), it follows the pointer above them with a shadow, and settles
|
|
2838
|
+
into its slot with the same slide. The rows are composited for the whole
|
|
2839
|
+
reorder (`will-change`): promoting one as its slide starts would
|
|
2840
|
+
re-rasterize it, a blink of its text and icons. */
|
|
2841
|
+
[data-sb-reordering] > .sb-listrow[data-sb-movable] {
|
|
2842
|
+
transition: transform 0.3s var(--sb-nav-easing);
|
|
2843
|
+
will-change: transform;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
.sb-listrow[data-sb-lifting] {
|
|
2847
|
+
z-index: 5;
|
|
2848
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
|
|
2849
|
+
cursor: grabbing;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
.sb-listrow[data-sb-lifting]:not([data-sb-settling]) {
|
|
2853
|
+
transition: none;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
/* `draggable` / `dropdestination`: transparent wrappers; a lifted view's
|
|
2857
|
+
copy follows the pointer over everything, scaled up a little with a
|
|
2858
|
+
shadow, while the original dims; letting go off a destination slides the
|
|
2859
|
+
copy back. A targeted destination shows nothing of its own (the app's
|
|
2860
|
+
`isTargeted` does), only that the copy may drop there. */
|
|
2861
|
+
.sb-draggable,
|
|
2862
|
+
.sb-dropdestination {
|
|
2863
|
+
-webkit-touch-callout: none;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
.sb-draggable {
|
|
2867
|
+
cursor: grab;
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
.sb-draggable[data-sb-dragging] {
|
|
2871
|
+
opacity: 0.4;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
.sb-drag-preview {
|
|
2875
|
+
z-index: 60;
|
|
2876
|
+
pointer-events: none;
|
|
2877
|
+
opacity: 0.9;
|
|
2878
|
+
filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.25));
|
|
2879
|
+
will-change: transform;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
.sb-drag-preview[data-sb-settling] {
|
|
2883
|
+
transition: transform 0.3s var(--sb-nav-easing);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
[data-sb-dragging] .sb-dropdestination {
|
|
2887
|
+
cursor: copy;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2588
2890
|
.sb-listrow-circle {
|
|
2589
2891
|
display: flex;
|
|
2590
2892
|
align-items: center;
|
|
@@ -2625,6 +2927,14 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2625
2927
|
|
|
2626
2928
|
.sb-listrow[data-sb-deletable]:not([data-sb-selectable]) .sb-listrow-circle > .sb-listrow-minus {
|
|
2627
2929
|
display: block;
|
|
2930
|
+
transition: transform 0.3s var(--sb-nav-easing);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
/* While the row shows its Delete button (the minus was tapped, or the row
|
|
2934
|
+
swiped) the minus stands upright, as on iOS; the content, and the grip in
|
|
2935
|
+
it, slid aside. */
|
|
2936
|
+
.sb-listrow[data-sb-swipe-open="trailing"] .sb-listrow-circle > .sb-listrow-minus {
|
|
2937
|
+
transform: rotate(-90deg);
|
|
2628
2938
|
}
|
|
2629
2939
|
|
|
2630
2940
|
.sb-listrow[data-sb-selectable] {
|
|
@@ -2638,6 +2948,26 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2638
2948
|
border-radius: 10px;
|
|
2639
2949
|
}
|
|
2640
2950
|
|
|
2951
|
+
/* The fill is the row's alone: its swipe content (which inherits the row's
|
|
2952
|
+
background to cover the actions behind it) and a selected link inside it
|
|
2953
|
+
would each stack another translucent layer, a darker pill around the
|
|
2954
|
+
label. */
|
|
2955
|
+
.sb-listrow[data-sb-selected] > .sb-swipe-content {
|
|
2956
|
+
background-color: transparent;
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
.sb-listrow .sb-navlink[data-sb-selected] {
|
|
2960
|
+
background: transparent;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
/* Like iOS, a selected row's fill has no hairline under it or above it. */
|
|
2964
|
+
.sb-section-rows > [data-sb-selected]::after,
|
|
2965
|
+
.sb-list-content > [data-sb-selected]::after,
|
|
2966
|
+
.sb-section-rows > :has(+ [data-sb-selected])::after,
|
|
2967
|
+
.sb-list-content > :has(+ [data-sb-selected])::after {
|
|
2968
|
+
display: none;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2641
2971
|
@keyframes sb-fade-in {
|
|
2642
2972
|
from {
|
|
2643
2973
|
opacity: 0;
|
|
@@ -2654,7 +2984,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2654
2984
|
position: absolute;
|
|
2655
2985
|
left: 0;
|
|
2656
2986
|
right: 0;
|
|
2657
|
-
|
|
2987
|
+
/* Under the bar a scroll view's content starts below it. */
|
|
2988
|
+
top: var(--sb-top-inset, 0px);
|
|
2658
2989
|
height: 40px;
|
|
2659
2990
|
display: flex;
|
|
2660
2991
|
align-items: center;
|
|
@@ -2682,17 +3013,20 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2682
3013
|
}
|
|
2683
3014
|
|
|
2684
3015
|
.sb-list[data-sb-refreshable] > .sb-list-content,
|
|
2685
|
-
.sb-scrollview[data-sb-refreshable] > .sb-scroll-content
|
|
3016
|
+
.sb-scrollview[data-sb-refreshable] > .sb-scroll-content,
|
|
3017
|
+
[data-sb-refreshable] > .sb-navbar-large-title {
|
|
2686
3018
|
transition: transform 0.3s var(--sb-nav-easing);
|
|
2687
3019
|
}
|
|
2688
3020
|
|
|
2689
3021
|
[data-sb-pulling] > .sb-list-content,
|
|
2690
|
-
[data-sb-pulling] > .sb-scroll-content
|
|
3022
|
+
[data-sb-pulling] > .sb-scroll-content,
|
|
3023
|
+
[data-sb-pulling] > .sb-navbar-large-title {
|
|
2691
3024
|
transition: none;
|
|
2692
3025
|
}
|
|
2693
3026
|
|
|
2694
3027
|
[data-sb-refreshing] > .sb-list-content,
|
|
2695
|
-
[data-sb-refreshing] > .sb-scroll-content
|
|
3028
|
+
[data-sb-refreshing] > .sb-scroll-content,
|
|
3029
|
+
[data-sb-refreshing] > .sb-navbar-large-title {
|
|
2696
3030
|
transform: translateY(40px);
|
|
2697
3031
|
}
|
|
2698
3032
|
|
|
@@ -3270,3 +3604,66 @@ svg.sb-path {
|
|
|
3270
3604
|
display: block;
|
|
3271
3605
|
overflow: hidden;
|
|
3272
3606
|
}
|
|
3607
|
+
|
|
3608
|
+
/* ---------------------------------------------------------------------------
|
|
3609
|
+
`host` elements (src/hostContent.ts): the page's own DOM in a box the
|
|
3610
|
+
engine sizes. The content element is positioned at the box's origin and
|
|
3611
|
+
takes its width; in `fill` sizing its height too. The box clips.
|
|
3612
|
+
------------------------------------------------------------------------- */
|
|
3613
|
+
.sb-host {
|
|
3614
|
+
display: block;
|
|
3615
|
+
overflow: hidden;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
.sb-host > .sb-host-content {
|
|
3619
|
+
position: absolute;
|
|
3620
|
+
left: 0;
|
|
3621
|
+
top: 0;
|
|
3622
|
+
width: 100%;
|
|
3623
|
+
box-sizing: border-box;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
.sb-host[data-sb-sizing="fill"] > .sb-host-content {
|
|
3627
|
+
height: 100%;
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
/* ---------------------------------------------------------------------------
|
|
3631
|
+
Islands (@swiftbrowser/react's `SwiftUIView`): a SwiftUI root inside a
|
|
3632
|
+
page's own layout, with the device's tokens and none of its frame. The
|
|
3633
|
+
page sizes the island; in `content` fit the renderer sets the root's height
|
|
3634
|
+
from the layout.
|
|
3635
|
+
------------------------------------------------------------------------- */
|
|
3636
|
+
.device.sb-island {
|
|
3637
|
+
width: auto;
|
|
3638
|
+
padding: 0;
|
|
3639
|
+
border-radius: 0;
|
|
3640
|
+
background: none;
|
|
3641
|
+
box-shadow: none;
|
|
3642
|
+
flex: initial;
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
.device.sb-island::before,
|
|
3646
|
+
.device.sb-island::after {
|
|
3647
|
+
display: none;
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
.sb-island .device-screen {
|
|
3651
|
+
width: 100%;
|
|
3652
|
+
height: 100%;
|
|
3653
|
+
border-radius: 0;
|
|
3654
|
+
background: none;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
.sb-island[data-sb-fit="content"],
|
|
3658
|
+
.sb-island[data-sb-fit="content"] .device-screen,
|
|
3659
|
+
.sb-island[data-sb-fit="content"] .screen {
|
|
3660
|
+
position: relative;
|
|
3661
|
+
inset: auto;
|
|
3662
|
+
height: auto;
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
/* `.onHover` (pointerEffects.ts): the whole box hovers, as SwiftUI uses the
|
|
3666
|
+
view's frame, so a hover box is hit in its empty space too. */
|
|
3667
|
+
.sb-styled[data-sb-hover] {
|
|
3668
|
+
pointer-events: auto;
|
|
3669
|
+
}
|
package/src/typography.ts
CHANGED
|
@@ -22,7 +22,10 @@ export {
|
|
|
22
22
|
DYNAMIC_TYPE_SIZES,
|
|
23
23
|
FONT_FAMILIES,
|
|
24
24
|
FONT_WEIGHTS,
|
|
25
|
-
|
|
25
|
+
LINE_HEIGHT_RATIO,
|
|
26
|
+
LEADINGS,
|
|
27
|
+
leadingFor,
|
|
28
|
+
textHeight,
|
|
26
29
|
TEXT_STYLE_SIZES,
|
|
27
30
|
defaultFont,
|
|
28
31
|
lineHeightFor,
|
|
@@ -70,7 +73,7 @@ export function applyTypeSizeVariables(target: HTMLElement, size: DynamicTypeSiz
|
|
|
70
73
|
for (const style of TEXT_STYLES) {
|
|
71
74
|
const { fontSize, lineHeight } = resolveTextStyle(style, size);
|
|
72
75
|
target.style.setProperty(textStyleVar(style, 'size'), `${fontSize}px`);
|
|
73
|
-
target.style.setProperty(textStyleVar(style, 'lh'), `${lineHeight}px`);
|
|
76
|
+
target.style.setProperty(textStyleVar(style, 'lh'), `${Math.round(lineHeight * 100) / 100}px`);
|
|
74
77
|
target.style.setProperty(`--sb-ts-${style}-scale`, String(relativeScale(style, size)));
|
|
75
78
|
}
|
|
76
79
|
}
|