@swiftbrowser/web 0.6.0 → 0.6.2
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/package.json +1 -1
- package/src/animation.ts +44 -12
- package/src/chartDom.ts +6 -2
- package/src/layout/engine.ts +164 -50
- package/src/layout/index.ts +1 -1
- package/src/layoutDom.ts +40 -33
- package/src/press.ts +51 -13
- package/src/protocol.ts +16 -0
- package/src/renderer.ts +48 -1
- package/src/styles.css +109 -58
- package/src/styles.ts +18 -5
- package/src/symbols.ts +15 -7
package/src/press.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
* touch: lifting the highlight when the pointer leaves the control (plus a
|
|
9
9
|
* margin) and restoring it when it comes back.
|
|
10
10
|
*
|
|
11
|
-
* The glass under a control: a bar item's
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* The glass under a control: a bar item's capsule (the `.sb-bar-glass`
|
|
12
|
+
* under it: the glass is beside the toolbars drawn on it, not around them,
|
|
13
|
+
* so they swell along, about the capsule's center), the back and More
|
|
14
|
+
* circles, the sidebar toggle, a `.glass` button, or the `.glassEffect` box
|
|
15
|
+
* around a button. iOS swells the whole group, never one item of it: a
|
|
16
|
+
* glass-styled button in a bar group is the group's press. A control on no
|
|
17
|
+
* glass (a plain or bordered button) keeps the stylesheet's `:active` dimming.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
/** The controls a press can highlight the glass under. */
|
|
@@ -25,10 +26,15 @@ const OWN_GLASS = '.sb-navbar-back, .sb-navbar-more, .sb-navsplit-toggle, .sb-bu
|
|
|
25
26
|
/** UIKit keeps a button highlighted while the touch stays within this far outside it. */
|
|
26
27
|
export const PRESS_SLOP = 24;
|
|
27
28
|
|
|
29
|
+
/** How long the glass takes to spring back once released (styles.css). */
|
|
30
|
+
const SPRING_BACK_MS = 500;
|
|
31
|
+
|
|
28
32
|
interface Press {
|
|
29
33
|
pointerId: number;
|
|
30
34
|
control: HTMLElement;
|
|
31
35
|
surface: HTMLElement;
|
|
36
|
+
/** The toolbars drawn on the capsule, which swell with it, and their `transform-origin` before. */
|
|
37
|
+
along: { el: HTMLElement; origin: string }[];
|
|
32
38
|
/** Where the control was on the pointer down (its bounds plus the slop hold the press). */
|
|
33
39
|
bounds: DOMRect;
|
|
34
40
|
held: boolean;
|
|
@@ -52,7 +58,7 @@ export class PressEffects {
|
|
|
52
58
|
if (!control || !this.root.contains(control) || isDisabled(control)) return;
|
|
53
59
|
const surface = glassSurfaceOf(control, this.root);
|
|
54
60
|
if (!surface) return;
|
|
55
|
-
this.press = { pointerId: event.pointerId, control, surface, bounds: control.getBoundingClientRect(), held: false };
|
|
61
|
+
this.press = { pointerId: event.pointerId, control, surface, along: toolbarsOn(surface), bounds: control.getBoundingClientRect(), held: false };
|
|
56
62
|
this.hold(true);
|
|
57
63
|
window.addEventListener('pointermove', this.onPointerMove);
|
|
58
64
|
window.addEventListener('pointerup', this.onPointerUp);
|
|
@@ -82,11 +88,21 @@ export class PressEffects {
|
|
|
82
88
|
press.held = held;
|
|
83
89
|
press.surface.toggleAttribute('data-sb-pressed', held);
|
|
84
90
|
if (press.control !== press.surface) press.control.toggleAttribute('data-sb-pressed-item', held);
|
|
91
|
+
for (const { el } of press.along) el.toggleAttribute('data-sb-pressed-with', held);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
private release(): void {
|
|
88
95
|
this.hold(false);
|
|
96
|
+
const press = this.press;
|
|
89
97
|
this.press = null;
|
|
98
|
+
if (press) {
|
|
99
|
+
// The origins stay until the spring back has run; the next press sets them again.
|
|
100
|
+
const settle = (): void => {
|
|
101
|
+
if (this.press?.surface === press.surface) return;
|
|
102
|
+
for (const { el, origin } of press.along) el.style.transformOrigin = origin;
|
|
103
|
+
};
|
|
104
|
+
window.setTimeout(settle, SPRING_BACK_MS);
|
|
105
|
+
}
|
|
90
106
|
window.removeEventListener('pointermove', this.onPointerMove);
|
|
91
107
|
window.removeEventListener('pointerup', this.onPointerUp);
|
|
92
108
|
window.removeEventListener('pointercancel', this.onPointerUp);
|
|
@@ -103,26 +119,48 @@ function isDisabled(control: HTMLElement): boolean {
|
|
|
103
119
|
* top of the file).
|
|
104
120
|
*/
|
|
105
121
|
export function glassSurfaceOf(control: HTMLElement, root: HTMLElement): HTMLElement | null {
|
|
106
|
-
if (control.matches(OWN_GLASS)) return control;
|
|
107
122
|
const toolbar = control.closest<HTMLElement>('.sb-toolbar');
|
|
108
123
|
if (toolbar && root.contains(toolbar)) {
|
|
109
124
|
const placement = toolbar.dataset['sbPlacement'];
|
|
110
|
-
if (placement === 'leading' || placement === 'trailing') return toolbar;
|
|
111
|
-
if (placement === 'bottom') return bottomGlassUnder(control, toolbar);
|
|
125
|
+
if (placement === 'leading' || placement === 'trailing' || placement === 'bottom') return barGlassUnder(control, toolbar);
|
|
112
126
|
return null;
|
|
113
127
|
}
|
|
128
|
+
if (control.matches(OWN_GLASS)) return control;
|
|
114
129
|
const glass = control.closest<HTMLElement>('[data-sb-glass]');
|
|
115
130
|
return glass && root.contains(glass) ? glass : null;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The toolbars drawn on a capsule (`data-sb-toolbars`, src/layoutDom.ts),
|
|
135
|
+
* each given the capsule's center as its `transform-origin` so that it
|
|
136
|
+
* swells with the capsule rather than about itself.
|
|
137
|
+
*/
|
|
138
|
+
function toolbarsOn(surface: HTMLElement): { el: HTMLElement; origin: string }[] {
|
|
139
|
+
const ids = surface.dataset['sbToolbars'];
|
|
140
|
+
const screen = surface.parentElement;
|
|
141
|
+
if (ids === undefined || !screen) return [];
|
|
142
|
+
const g = surface.getBoundingClientRect();
|
|
143
|
+
const cx = g.left + g.width / 2;
|
|
144
|
+
const cy = g.top + g.height / 2;
|
|
145
|
+
const along: { el: HTMLElement; origin: string }[] = [];
|
|
146
|
+
for (const id of ids.split(' ')) {
|
|
147
|
+
const el = id ? screen.querySelector<HTMLElement>(`.sb-toolbar[data-sb-id="${id}"]`) : null;
|
|
148
|
+
if (!el) continue;
|
|
149
|
+
const r = el.getBoundingClientRect();
|
|
150
|
+
along.push({ el, origin: el.style.transformOrigin });
|
|
151
|
+
el.style.transformOrigin = `${cx - r.left}px ${cy - r.top}px`;
|
|
152
|
+
}
|
|
153
|
+
return along;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The capsule a bar item is drawn on: the screen's `.sb-bar-glass` under its center. */
|
|
157
|
+
function barGlassUnder(control: HTMLElement, toolbar: HTMLElement): HTMLElement | null {
|
|
120
158
|
const screen = toolbar.closest<HTMLElement>('.sb-navscreen');
|
|
121
159
|
if (!screen) return null;
|
|
122
160
|
const rect = control.getBoundingClientRect();
|
|
123
161
|
const cx = rect.left + rect.width / 2;
|
|
124
162
|
const cy = rect.top + rect.height / 2;
|
|
125
|
-
for (const glass of screen.querySelectorAll<HTMLElement>(':scope > .sb-
|
|
163
|
+
for (const glass of screen.querySelectorAll<HTMLElement>(':scope > .sb-bar-glass')) {
|
|
126
164
|
const g = glass.getBoundingClientRect();
|
|
127
165
|
if (cx >= g.left && cx <= g.right && cy >= g.top && cy <= g.bottom) return glass;
|
|
128
166
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -323,6 +323,11 @@ export interface Style {
|
|
|
323
323
|
font?: Font;
|
|
324
324
|
/** Phase 6: any ShapeStyle (text role). Gradients paint the text itself. */
|
|
325
325
|
foreground?: ShapeStyle;
|
|
326
|
+
/**
|
|
327
|
+
* `.foregroundStyle(primary, secondary[, tertiary])`: the styles after the
|
|
328
|
+
* first, the `.secondary` and `.tertiary` levels below (`--sb-hierarchy-2`, `-3`).
|
|
329
|
+
*/
|
|
330
|
+
foregroundLevels?: ShapeStyle[];
|
|
326
331
|
/** Phase 6: any ShapeStyle (paint role). */
|
|
327
332
|
background?: ShapeStyle;
|
|
328
333
|
frame?: Frame;
|
|
@@ -502,6 +507,13 @@ export interface ZStackProps {
|
|
|
502
507
|
/** `systemName` is an SF Symbol name; the renderer picks the closest glyph. */
|
|
503
508
|
export interface SymbolImageProps {
|
|
504
509
|
systemName: string;
|
|
510
|
+
/**
|
|
511
|
+
* `.symbolRenderingMode(.palette)`: an enclosed filled symbol
|
|
512
|
+
* (`moon.circle.fill`) draws its enclosure in the secondary level
|
|
513
|
+
* (`--sb-hierarchy-2`) and its glyph in the primary, instead of knocking
|
|
514
|
+
* the glyph out.
|
|
515
|
+
*/
|
|
516
|
+
renderingMode?: 'palette';
|
|
505
517
|
}
|
|
506
518
|
/**
|
|
507
519
|
* Phase 5: an image from the app's asset catalog. `name` is the catalog
|
|
@@ -621,6 +633,8 @@ export interface NavScreenProps {
|
|
|
621
633
|
/** Phase 7 adds `selected` (see the block at the end of this file). */
|
|
622
634
|
export interface NavLinkProps {
|
|
623
635
|
selected?: boolean;
|
|
636
|
+
/** `.buttonStyle(.plain)` around the link (absent otherwise): its label in the foreground it is in, not the tint. */
|
|
637
|
+
style?: 'plain';
|
|
624
638
|
}
|
|
625
639
|
|
|
626
640
|
// Phase 3 kinds (docs/ops-protocol.md, "Phase 3 additions").
|
|
@@ -1675,6 +1689,8 @@ export interface ChartAxisLabel {
|
|
|
1675
1689
|
export interface ChartAxis {
|
|
1676
1690
|
position: ChartXAxisPosition | ChartYAxisPosition;
|
|
1677
1691
|
gridLines: number[];
|
|
1692
|
+
/** A date axis: its grid lines are dashed (2pt on, 2pt off), as Swift Charts draws them. */
|
|
1693
|
+
dashed?: true;
|
|
1678
1694
|
ticks: number[];
|
|
1679
1695
|
/** Default text labels; view labels are `chartitem` children with role `xLabel` / `yLabel`. */
|
|
1680
1696
|
labels: ChartAxisLabel[];
|
package/src/renderer.ts
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import {
|
|
37
37
|
Animator,
|
|
38
38
|
barToolbarOf,
|
|
39
|
+
isBarToolbar,
|
|
39
40
|
EXIT_CLONE_CLASS,
|
|
40
41
|
emptyPass,
|
|
41
42
|
setTransition,
|
|
@@ -150,7 +151,7 @@ import { VideoEvents, applyVideo, stopVideos } from './video';
|
|
|
150
151
|
import { applyCanvas, createCanvasElement } from './canvasDrawing';
|
|
151
152
|
import { scrollToTarget } from './scrollTo';
|
|
152
153
|
import { EMPTY_CATALOG, assetColorVar, type AssetCatalog } from './assets';
|
|
153
|
-
import { colorToCSS, effectsCSS, styleToCSS, type PaintCSS } from './styles';
|
|
154
|
+
import { HIERARCHY_OPACITY, colorToCSS, effectsCSS, styleToCSS, type PaintCSS } from './styles';
|
|
154
155
|
import { hostContent, onHostContentChange } from './hostContent';
|
|
155
156
|
import { HoverTracker, applyPointerAndEffects, safeAreaOverlap } from './pointerEffects';
|
|
156
157
|
import { colorMultiplyFilter } from './colorMultiply';
|
|
@@ -1578,6 +1579,11 @@ export class Renderer {
|
|
|
1578
1579
|
}
|
|
1579
1580
|
}
|
|
1580
1581
|
}
|
|
1582
|
+
// iOS 26: a bar group moved to another placement is not the same group
|
|
1583
|
+
// on screen; it leaves as it was and comes in where it goes (the Swift
|
|
1584
|
+
// runtime keys groups by placement, so this is for other hosts).
|
|
1585
|
+
const replaces = kind === 'toolbar' && node !== undefined && (props as { placement?: string }).placement !== node.props['placement'] && isBarToolbar(el);
|
|
1586
|
+
if (replaces) this.replaceToolbar(el);
|
|
1581
1587
|
if (node) {
|
|
1582
1588
|
const next = asRecord(props);
|
|
1583
1589
|
// Another Swift view now reads this element's size (a `geometry`
|
|
@@ -1626,6 +1632,8 @@ export class Renderer {
|
|
|
1626
1632
|
const animation = ANIMATIONS.get(el);
|
|
1627
1633
|
if (animation) record.animation = animation;
|
|
1628
1634
|
this.pass.updated.set(el, record);
|
|
1635
|
+
// A replaced toolbar enters anew: its style change is not tweened.
|
|
1636
|
+
if (replaces) this.pass.updated.delete(el);
|
|
1629
1637
|
this.dirty.add(id);
|
|
1630
1638
|
if (kind === 'navscreen') {
|
|
1631
1639
|
const stack = el.parentElement?.closest<HTMLElement>('.sb-navstack');
|
|
@@ -1716,6 +1724,23 @@ export class Renderer {
|
|
|
1716
1724
|
}
|
|
1717
1725
|
}
|
|
1718
1726
|
|
|
1727
|
+
/**
|
|
1728
|
+
* Hands the Animator a bar toolbar as removed (a clone of it as it is, where
|
|
1729
|
+
* it is: called before its new placement is applied) and inserted (itself),
|
|
1730
|
+
* so that at commit it scales out from its old place and in at its new one
|
|
1731
|
+
* instead of tweening between the two.
|
|
1732
|
+
*/
|
|
1733
|
+
private replaceToolbar(el: HTMLElement): void {
|
|
1734
|
+
const parent = el.parentElement;
|
|
1735
|
+
if (!parent) return;
|
|
1736
|
+
this.passRootRect ??= this.animator.rootRect();
|
|
1737
|
+
const rect = this.animator.rectOf(el, this.passRootRect);
|
|
1738
|
+
const ancestors: HTMLElement[] = [];
|
|
1739
|
+
for (let a: HTMLElement | null = parent; a && a !== this.root; a = a.parentElement) ancestors.push(a);
|
|
1740
|
+
this.pass.removed.push({ el: ghostClone(el), ancestors, rect, transition: transitionOf(el) });
|
|
1741
|
+
this.pass.inserted.add(el);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1719
1744
|
private remove(id: ElementId, op: Op): void {
|
|
1720
1745
|
if (id === ROOT_ID) throw new RendererError('Element 0 is the root and cannot be removed', op);
|
|
1721
1746
|
const el = this.elements.get(id);
|
|
@@ -2872,6 +2897,8 @@ function applyKindProps(el: HTMLElement, kind: ElementKind, props: ElementProps,
|
|
|
2872
2897
|
case 'navlink':
|
|
2873
2898
|
// Phase 7: a selected row (inside a `List(selection:)`) gets a rounded fill.
|
|
2874
2899
|
el.toggleAttribute('data-sb-selected', (props as NavLinkProps | undefined)?.selected === true);
|
|
2900
|
+
if ((props as NavLinkProps | undefined)?.style === 'plain') el.dataset['sbStyle'] = 'plain';
|
|
2901
|
+
else delete el.dataset['sbStyle'];
|
|
2875
2902
|
break;
|
|
2876
2903
|
case 'viewthatfits': {
|
|
2877
2904
|
const axes = (props as ViewThatFitsProps | undefined)?.axes;
|
|
@@ -3411,6 +3438,11 @@ function applyImage(el: HTMLElement, props: ImageProps, assets: AssetCatalog): v
|
|
|
3411
3438
|
delete el.dataset['sbResizable'];
|
|
3412
3439
|
el.dataset['sbSymbol'] = name;
|
|
3413
3440
|
const svg = renderSymbol(name);
|
|
3441
|
+
if (svg && props.renderingMode === 'palette' && svg.dataset['sbGlyphFill'] === 'knockout') {
|
|
3442
|
+
// The enclosure in the secondary level, the glyph in the primary.
|
|
3443
|
+
svg.style.color = 'var(--sb-hierarchy-2, currentColor)';
|
|
3444
|
+
svg.style.setProperty('--sb-icon-knockout', 'var(--sb-hierarchy-1, currentColor)');
|
|
3445
|
+
}
|
|
3414
3446
|
if (svg) found(name, svg);
|
|
3415
3447
|
else missing(name);
|
|
3416
3448
|
}
|
|
@@ -3667,6 +3699,21 @@ export function applyStyle(el: HTMLElement, style: Style): void {
|
|
|
3667
3699
|
// styled box wins, as it does in SwiftUI.
|
|
3668
3700
|
s.color = paint.solid;
|
|
3669
3701
|
s.setProperty('--sb-button-color', paint.solid);
|
|
3702
|
+
// `.secondary` and the other hierarchical levels below are levels of this
|
|
3703
|
+
// foreground (styles.ts); a hierarchical foreground keeps the ones above.
|
|
3704
|
+
if (!('hierarchical' in style.foreground)) {
|
|
3705
|
+
for (const level of [1, 2, 3, 4] as const) {
|
|
3706
|
+
const opacity = HIERARCHY_OPACITY[level];
|
|
3707
|
+
s.setProperty(`--sb-hierarchy-${level}`, opacity === 1 ? paint.solid : `color-mix(in srgb, ${paint.solid} ${opacity * 100}%, transparent)`);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
// `.foregroundStyle(.white, .indigo)`: the levels after the first. A
|
|
3711
|
+
// hierarchical one (`.foregroundStyle(.red, .secondary)`) stays the level
|
|
3712
|
+
// of the primary set above (it would refer to itself).
|
|
3713
|
+
style.foregroundLevels?.forEach((level, index) => {
|
|
3714
|
+
if (level && typeof level === 'object' && 'hierarchical' in level) return;
|
|
3715
|
+
s.setProperty(`--sb-hierarchy-${index + 2}`, styleToCSS(level, 'text').solid);
|
|
3716
|
+
});
|
|
3670
3717
|
if (paint.kind === 'gradient') {
|
|
3671
3718
|
// Text paints the gradient through its own background clipped to the
|
|
3672
3719
|
// glyphs (see `.sb-text` in styles.css); the custom properties inherit
|
package/src/styles.css
CHANGED
|
@@ -557,19 +557,26 @@ html[data-sb-mode="device"] .device-screen {
|
|
|
557
557
|
line-height: 1;
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
-
/* A symbol
|
|
561
|
-
about
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
units, is then 1.5 px at body, the weight iOS draws regular symbols in. */
|
|
560
|
+
/* A symbol is drawn at SF Symbols' size: at a text size a circle symbol is
|
|
561
|
+
about the font size across (measured in the Simulator: 17 pt at body's
|
|
562
|
+
17, 53 at 56), with a 1.4 pt stroke at body. The glyphs here occupy 20
|
|
563
|
+
of their 24-unit grid (a little more with their stroke), so the SVG is
|
|
564
|
+
1.08em with a 1.8-unit stroke, centred in the line-height box the engine
|
|
565
|
+
gives the image. */
|
|
567
566
|
.sb-image > svg {
|
|
568
567
|
display: block;
|
|
569
568
|
flex: none;
|
|
569
|
+
width: 1.08em;
|
|
570
|
+
height: 1.08em;
|
|
571
|
+
stroke: currentColor;
|
|
572
|
+
stroke-width: 1.8;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* A list row draws a Label's icon larger, as an icon column: a circle
|
|
576
|
+
symbol is 21.7 pt at body (1.28× the font size), its stroke 1.5 pt. */
|
|
577
|
+
.sb-list [data-sb-label-icon] .sb-image > svg {
|
|
570
578
|
width: 1.43em;
|
|
571
579
|
height: 1.43em;
|
|
572
|
-
stroke: currentColor;
|
|
573
580
|
stroke-width: 1.4;
|
|
574
581
|
}
|
|
575
582
|
|
|
@@ -1346,6 +1353,12 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1346
1353
|
-webkit-tap-highlight-color: transparent;
|
|
1347
1354
|
}
|
|
1348
1355
|
|
|
1356
|
+
/* `.buttonStyle(.plain)`: as a plain button, the label in the foreground it
|
|
1357
|
+
is in (Food Truck's donut gallery names are black on iOS). */
|
|
1358
|
+
.sb-navlink[data-sb-style="plain"] {
|
|
1359
|
+
color: var(--sb-button-color, inherit);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1349
1362
|
.sb-navlink:focus-visible {
|
|
1350
1363
|
outline: 2px solid var(--sb-color-accent);
|
|
1351
1364
|
outline-offset: 2px;
|
|
@@ -2019,7 +2032,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2019
2032
|
isolation: isolate;
|
|
2020
2033
|
}
|
|
2021
2034
|
|
|
2022
|
-
.sb-decorated[data-sb-role="background"] > [data-sb-id]:first-child
|
|
2035
|
+
.sb-decorated[data-sb-role="background"] > [data-sb-id]:first-child,
|
|
2036
|
+
.sb-decorated[data-sb-role="background"] > [data-sb-ghost]:first-child {
|
|
2023
2037
|
z-index: 1;
|
|
2024
2038
|
}
|
|
2025
2039
|
|
|
@@ -2092,7 +2106,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2092
2106
|
}
|
|
2093
2107
|
|
|
2094
2108
|
.sb-menu > [data-sb-id]:not(:first-child),
|
|
2095
|
-
.sb-menu > [data-sb-id][hidden]
|
|
2109
|
+
.sb-menu > [data-sb-id][hidden],
|
|
2110
|
+
.sb-menu > [data-sb-ghost]:not(:first-child),
|
|
2111
|
+
.sb-menu > [data-sb-ghost][hidden] {
|
|
2096
2112
|
display: none;
|
|
2097
2113
|
}
|
|
2098
2114
|
|
|
@@ -2270,7 +2286,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2270
2286
|
/* A toolbar the engine hid (a spacer, `secondary` items collapsed into the
|
|
2271
2287
|
More menu), and its items collapsed one by one, draw nothing. */
|
|
2272
2288
|
.sb-toolbar[hidden],
|
|
2273
|
-
.sb-toolbar > [data-sb-id][hidden]
|
|
2289
|
+
.sb-toolbar > [data-sb-id][hidden],
|
|
2290
|
+
.sb-toolbar > [data-sb-ghost][hidden] {
|
|
2274
2291
|
display: none;
|
|
2275
2292
|
}
|
|
2276
2293
|
|
|
@@ -2286,9 +2303,7 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2286
2303
|
.sb-navbar-back::after,
|
|
2287
2304
|
.sb-navbar-more::after,
|
|
2288
2305
|
.sb-tabbar::after,
|
|
2289
|
-
.sb-
|
|
2290
|
-
.sb-toolbar[data-sb-placement="trailing"]::after,
|
|
2291
|
-
.sb-bottom-glass::after,
|
|
2306
|
+
.sb-bar-glass::after,
|
|
2292
2307
|
.sb-searchfield::after,
|
|
2293
2308
|
.sb-navsplit-toggle::after,
|
|
2294
2309
|
.sb-picker-menu::after,
|
|
@@ -2317,24 +2332,20 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2317
2332
|
pointer-events: none;
|
|
2318
2333
|
}
|
|
2319
2334
|
|
|
2320
|
-
/* iOS 26: a bar's leading and trailing items
|
|
2321
|
-
|
|
2322
|
-
drawn in the label color
|
|
2335
|
+
/* iOS 26: a bar's leading and trailing items float on glass capsules
|
|
2336
|
+
(`.sb-bar-glass`, below: adjacent toolbars share one, the engine sizes it
|
|
2337
|
+
with each item in a slot at least 44 wide), drawn in the label color
|
|
2338
|
+
unless tinted. */
|
|
2323
2339
|
.sb-toolbar[data-sb-placement="leading"],
|
|
2324
2340
|
.sb-toolbar[data-sb-placement="trailing"] {
|
|
2325
|
-
border-radius: 22px;
|
|
2326
|
-
background: var(--sb-glass-background);
|
|
2327
|
-
box-shadow: var(--sb-glass-shadow);
|
|
2328
|
-
backdrop-filter: blur(12px) saturate(180%);
|
|
2329
|
-
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
|
2330
2341
|
--sb-color-accent: var(--sb-color-primary);
|
|
2331
2342
|
}
|
|
2332
2343
|
|
|
2333
2344
|
/* Toolbar items in the bar are laid out in the screen's body, under the bar
|
|
2334
2345
|
element (z 1): they stack above it, so its scroll edge effect blurs the
|
|
2335
|
-
content under the bar and not them. */
|
|
2346
|
+
content under the bar and not them, and above their capsules (z 2). */
|
|
2336
2347
|
.sb-toolbar:is([data-sb-placement="leading"], [data-sb-placement="trailing"], [data-sb-placement="principal"]) {
|
|
2337
|
-
z-index:
|
|
2348
|
+
z-index: 3;
|
|
2338
2349
|
}
|
|
2339
2350
|
|
|
2340
2351
|
/* Their symbols are drawn a little larger and heavier (semibold), as iOS 26
|
|
@@ -2351,17 +2362,21 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2351
2362
|
}
|
|
2352
2363
|
|
|
2353
2364
|
/* iOS 26: a bottom toolbar's items float over the content on Liquid Glass
|
|
2354
|
-
capsules
|
|
2355
|
-
|
|
2365
|
+
capsules, drawn in the label color unless tinted, above the scroll edge
|
|
2366
|
+
effect. */
|
|
2356
2367
|
.sb-toolbar[data-sb-placement="bottom"] {
|
|
2357
2368
|
z-index: 3;
|
|
2358
2369
|
--sb-color-accent: var(--sb-color-primary);
|
|
2359
2370
|
}
|
|
2360
2371
|
|
|
2361
|
-
.
|
|
2372
|
+
/* The bar's Liquid Glass capsules (src/layoutDom.ts; engine `leadingGlass0`
|
|
2373
|
+
…, `trailingGlass0` …, `bottomGlass0` …): a 44-tall capsule in the
|
|
2374
|
+
navigation bar, a 48-tall one in the bottom bar, under the items drawn on
|
|
2375
|
+
them (`data-sb-toolbars`). */
|
|
2376
|
+
.sb-bar-glass {
|
|
2362
2377
|
position: absolute;
|
|
2363
2378
|
z-index: 2;
|
|
2364
|
-
border-radius:
|
|
2379
|
+
border-radius: 22px;
|
|
2365
2380
|
background: var(--sb-glass-background);
|
|
2366
2381
|
box-shadow: var(--sb-glass-shadow);
|
|
2367
2382
|
backdrop-filter: blur(12px) saturate(180%);
|
|
@@ -2369,18 +2384,24 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2369
2384
|
pointer-events: none;
|
|
2370
2385
|
}
|
|
2371
2386
|
|
|
2387
|
+
.sb-bar-glass[data-sb-placement="bottom"] {
|
|
2388
|
+
border-radius: 24px;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2372
2391
|
/* iOS 26 press feedback on Liquid Glass (src/press.ts): while a pointer is
|
|
2373
|
-
down on a bar button or a glass button, the glass under it (a bar
|
|
2374
|
-
capsule,
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
the
|
|
2392
|
+
down on a bar button or a glass button, the glass under it (a bar
|
|
2393
|
+
capsule, the back and More circles, the sidebar toggle, a `.glass`
|
|
2394
|
+
button, a `.glassEffect` box) swells and brightens; when it lifts, the
|
|
2395
|
+
glass springs back (an overshooting ease-out). The whole group swells,
|
|
2396
|
+
as on iOS, never one item of it (the toolbars on a capsule, beside it
|
|
2397
|
+
rather than in it, swell along: `data-sb-pressed-with`, about the
|
|
2398
|
+
capsule's center); buttons on glass do not dim as plain buttons do. The
|
|
2399
|
+
swell is the `scale` property, apart from `transform`, which the renderer
|
|
2400
|
+
sets (offsets, rotations) and the Animator tweens. */
|
|
2381
2401
|
.sb-toolbar[data-sb-placement="leading"],
|
|
2382
2402
|
.sb-toolbar[data-sb-placement="trailing"],
|
|
2383
|
-
.sb-bottom
|
|
2403
|
+
.sb-toolbar[data-sb-placement="bottom"],
|
|
2404
|
+
.sb-bar-glass,
|
|
2384
2405
|
.sb-navbar-back,
|
|
2385
2406
|
.sb-navbar-more,
|
|
2386
2407
|
.sb-navsplit-toggle,
|
|
@@ -2396,25 +2417,26 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2396
2417
|
/* A capsule of several items swells less: it is wider. */
|
|
2397
2418
|
.sb-toolbar[data-sb-placement="leading"],
|
|
2398
2419
|
.sb-toolbar[data-sb-placement="trailing"],
|
|
2399
|
-
.sb-bottom
|
|
2420
|
+
.sb-toolbar[data-sb-placement="bottom"],
|
|
2421
|
+
.sb-bar-glass {
|
|
2400
2422
|
--sb-press-scale: 1.05;
|
|
2401
2423
|
}
|
|
2402
2424
|
|
|
2403
|
-
|
|
2425
|
+
/* (as specific as the rules above, so the toolbars on a capsule and a glass
|
|
2426
|
+
button swell on this curve, in step with their glass, not on the spring
|
|
2427
|
+
back's) */
|
|
2428
|
+
[data-sb-pressed],
|
|
2429
|
+
[data-sb-pressed-with],
|
|
2430
|
+
.sb-toolbar[data-sb-pressed-with],
|
|
2431
|
+
.sb-button[data-sb-style="glass"][data-sb-pressed] {
|
|
2404
2432
|
scale: var(--sb-press-scale);
|
|
2405
|
-
background-color: var(--sb-glass-pressed);
|
|
2406
2433
|
transition:
|
|
2407
2434
|
scale 0.14s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
2408
2435
|
background-color 0.14s ease;
|
|
2409
2436
|
}
|
|
2410
2437
|
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
position: absolute;
|
|
2414
|
-
inset: -11px -10px;
|
|
2415
|
-
border-radius: 9999px;
|
|
2416
|
-
background: var(--sb-glass-selected);
|
|
2417
|
-
pointer-events: none;
|
|
2438
|
+
[data-sb-pressed] {
|
|
2439
|
+
background-color: var(--sb-glass-pressed);
|
|
2418
2440
|
}
|
|
2419
2441
|
|
|
2420
2442
|
.sb-toolbar:is([data-sb-placement="leading"], [data-sb-placement="trailing"], [data-sb-placement="bottom"]) :is(.sb-button, .sb-menu):active,
|
|
@@ -2426,7 +2448,10 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2426
2448
|
}
|
|
2427
2449
|
|
|
2428
2450
|
@media (prefers-reduced-motion: reduce) {
|
|
2429
|
-
[data-sb-pressed]
|
|
2451
|
+
[data-sb-pressed],
|
|
2452
|
+
[data-sb-pressed-with],
|
|
2453
|
+
.sb-toolbar[data-sb-pressed-with],
|
|
2454
|
+
.sb-button[data-sb-style="glass"][data-sb-pressed] {
|
|
2430
2455
|
scale: none;
|
|
2431
2456
|
}
|
|
2432
2457
|
}
|
|
@@ -2457,8 +2482,10 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2457
2482
|
.sb-searchfield {
|
|
2458
2483
|
display: flex;
|
|
2459
2484
|
align-items: center;
|
|
2460
|
-
|
|
2461
|
-
|
|
2485
|
+
/* iOS 26 (FoodTruck's donuts, measured): the magnifier's ink starts
|
|
2486
|
+
19.33pt into the capsule, 16pt wide, and the text 46.67pt in. */
|
|
2487
|
+
gap: 8.5px;
|
|
2488
|
+
padding: 0 16px 0 18.5px;
|
|
2462
2489
|
/* iOS 26: a glass capsule floating at the bottom (engine `searchField`). */
|
|
2463
2490
|
border-radius: 9999px;
|
|
2464
2491
|
background: var(--sb-glass-background);
|
|
@@ -2527,14 +2554,21 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2527
2554
|
|
|
2528
2555
|
.sb-searchfield > svg {
|
|
2529
2556
|
display: block;
|
|
2530
|
-
width:
|
|
2531
|
-
height:
|
|
2557
|
+
width: 19.25px;
|
|
2558
|
+
height: 19.25px;
|
|
2532
2559
|
flex-shrink: 0;
|
|
2533
|
-
color
|
|
2560
|
+
/* On the glass the magnifier is the label color, not gray. */
|
|
2561
|
+
color: var(--sb-color-primary);
|
|
2534
2562
|
stroke: currentColor;
|
|
2535
2563
|
stroke-width: 2.25;
|
|
2536
2564
|
}
|
|
2537
2565
|
|
|
2566
|
+
.sb-navscreen[data-sb-search-top] > .sb-navscreen-body > .sb-navscreen-content > .sb-searchfield > svg {
|
|
2567
|
+
width: 17px;
|
|
2568
|
+
height: 17px;
|
|
2569
|
+
color: var(--sb-color-secondary);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2538
2572
|
.sb-searchfield-input {
|
|
2539
2573
|
flex: 1 1 auto;
|
|
2540
2574
|
min-width: 0;
|
|
@@ -2551,6 +2585,12 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2551
2585
|
}
|
|
2552
2586
|
|
|
2553
2587
|
.sb-searchfield-input::placeholder {
|
|
2588
|
+
/* Vibrant on the glass: the label color, partly see-through, so the
|
|
2589
|
+
backdrop tints it as iOS's does. */
|
|
2590
|
+
color: color-mix(in srgb, var(--sb-color-primary) 60%, transparent);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
.sb-navscreen[data-sb-search-top] > .sb-navscreen-body > .sb-navscreen-content > .sb-searchfield > .sb-searchfield-input::placeholder {
|
|
2554
2594
|
color: var(--sb-color-secondary);
|
|
2555
2595
|
}
|
|
2556
2596
|
|
|
@@ -2600,7 +2640,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2600
2640
|
box-shadow: -1px 0 0 var(--sb-color-separator);
|
|
2601
2641
|
}
|
|
2602
2642
|
|
|
2603
|
-
.sb-navsplit-columns > [data-sb-id]:first-child
|
|
2643
|
+
.sb-navsplit-columns > [data-sb-id]:first-child,
|
|
2644
|
+
.sb-navsplit-columns > [data-sb-ghost]:first-child {
|
|
2604
2645
|
box-shadow: 1px 0 0 var(--sb-color-separator);
|
|
2605
2646
|
}
|
|
2606
2647
|
|
|
@@ -2650,7 +2691,8 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2650
2691
|
|
|
2651
2692
|
/* ViewThatFits: the candidates it did not choose have no frame and carry the
|
|
2652
2693
|
`hidden` attribute (src/layoutDom.ts). */
|
|
2653
|
-
.sb-viewthatfits > [data-sb-id][hidden]
|
|
2694
|
+
.sb-viewthatfits > [data-sb-id][hidden],
|
|
2695
|
+
.sb-viewthatfits > [data-sb-ghost][hidden] {
|
|
2654
2696
|
display: none;
|
|
2655
2697
|
}
|
|
2656
2698
|
|
|
@@ -2686,10 +2728,15 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2686
2728
|
pointer-events: none;
|
|
2687
2729
|
}
|
|
2688
2730
|
|
|
2731
|
+
/* Swift Charts' grid lines are half a point wide (measured: FoodTruck's
|
|
2732
|
+
forecast); a date axis's are dashed 2pt on, 2pt off. */
|
|
2689
2733
|
.sb-chart-gridline {
|
|
2690
2734
|
stroke: var(--sb-color-separator);
|
|
2691
|
-
stroke-width:
|
|
2692
|
-
|
|
2735
|
+
stroke-width: 0.5;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
.sb-chart-gridline[data-sb-dashed] {
|
|
2739
|
+
stroke-dasharray: 2 2;
|
|
2693
2740
|
}
|
|
2694
2741
|
|
|
2695
2742
|
.sb-chart-tick {
|
|
@@ -2780,8 +2827,12 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2780
2827
|
(they are in `hidden` without frames). */
|
|
2781
2828
|
.sb-contextmenu > [data-sb-id]:not(:first-child),
|
|
2782
2829
|
.sb-contextmenu > [data-sb-id][hidden],
|
|
2830
|
+
.sb-contextmenu > [data-sb-ghost]:not(:first-child),
|
|
2831
|
+
.sb-contextmenu > [data-sb-ghost][hidden],
|
|
2783
2832
|
.sb-swipeactions > .sb-swipe-content > [data-sb-id]:not(:first-child),
|
|
2784
|
-
.sb-swipeactions > .sb-swipe-content > [data-sb-id][hidden]
|
|
2833
|
+
.sb-swipeactions > .sb-swipe-content > [data-sb-id][hidden],
|
|
2834
|
+
.sb-swipeactions > .sb-swipe-content > [data-sb-ghost]:not(:first-child),
|
|
2835
|
+
.sb-swipeactions > .sb-swipe-content > [data-sb-ghost][hidden] {
|
|
2785
2836
|
display: none;
|
|
2786
2837
|
}
|
|
2787
2838
|
|
package/src/styles.ts
CHANGED
|
@@ -127,13 +127,26 @@ export interface PaintCSS {
|
|
|
127
127
|
backdropFilter?: string;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Label colors (text role) and fill colors (paint role) per hierarchical
|
|
132
|
+
* level. In the text role a level is one of the foreground an ancestor set
|
|
133
|
+
* (`--sb-hierarchy-<level>`, set with it), else the label color: iOS 26 draws
|
|
134
|
+
* `.secondary` under `.foregroundColor(.accentColor)` as the accent at 50%
|
|
135
|
+
* (Food Truck's card header icons) and `.tertiary` at 25% (their chevrons).
|
|
136
|
+
*/
|
|
131
137
|
const HIERARCHICAL_TEXT: Record<1 | 2 | 3 | 4, string> = {
|
|
132
|
-
1: 'var(--sb-color-primary)',
|
|
133
|
-
2: 'var(--sb-color-secondary)',
|
|
134
|
-
3: 'var(--sb-color-tertiary-label)',
|
|
135
|
-
4: 'var(--sb-color-quaternary-label)',
|
|
138
|
+
1: 'var(--sb-hierarchy-1, var(--sb-color-primary))',
|
|
139
|
+
2: 'var(--sb-hierarchy-2, var(--sb-color-secondary))',
|
|
140
|
+
3: 'var(--sb-hierarchy-3, var(--sb-color-tertiary-label))',
|
|
141
|
+
4: 'var(--sb-hierarchy-4, var(--sb-color-quaternary-label))',
|
|
136
142
|
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The opacity of each hierarchical level of a foreground color (iOS 26:
|
|
146
|
+
* 50% and 25% measured; the quaternary level, not measured, as the label
|
|
147
|
+
* colors' ratio has it).
|
|
148
|
+
*/
|
|
149
|
+
export const HIERARCHY_OPACITY: Record<1 | 2 | 3 | 4, number> = { 1: 1, 2: 0.5, 3: 0.25, 4: 0.18 };
|
|
137
150
|
const HIERARCHICAL_PAINT: Record<1 | 2 | 3 | 4, string> = {
|
|
138
151
|
1: 'var(--sb-color-fill)',
|
|
139
152
|
2: 'var(--sb-color-secondary-fill)',
|