@swiftbrowser/web 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -57
- package/app/index.html +10 -10
- package/node/dist/node/examples.d.ts.map +1 -1
- package/node/dist/node/examples.js +1 -0
- package/node/dist/node/examples.js.map +1 -1
- package/node/dist/plugins/asset-catalogs.js +10 -10
- package/node/dist/plugins/asset-catalogs.js.map +1 -1
- package/package.json +1 -1
- package/plugins/asset-catalogs.ts +10 -10
- package/src/interaction.ts +50 -2
- package/src/largeTitle.ts +79 -0
- package/src/layout/engine.ts +491 -164
- package/src/layoutDom.ts +13 -3
- package/src/main.ts +4 -1
- package/src/mock.ts +2 -2
- package/src/protocol.ts +22 -2
- package/src/renderer.ts +19 -8
- package/src/scanner.ts +1 -1
- package/src/styles.css +330 -97
- package/src/styles.ts +30 -0
- package/src/symbols.ts +97 -10
- package/src/webAuth.ts +1 -1
package/src/layoutDom.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type { LayoutEnvironment, LayoutNode, LayoutResult, Rect, ResolvedFont, T
|
|
|
37
37
|
import { applyChartChrome } from './chartDom';
|
|
38
38
|
import { bitmapLaidOut } from './bitmaps';
|
|
39
39
|
import { drawRichText } from './richText';
|
|
40
|
+
import { bindLargeTitle } from './largeTitle';
|
|
40
41
|
|
|
41
42
|
export interface LayoutDomContext {
|
|
42
43
|
root: HTMLElement;
|
|
@@ -342,6 +343,13 @@ function applyChrome(
|
|
|
342
343
|
// it, or above it (the previous row's line; styles.css).
|
|
343
344
|
setFlag(el, 'sbSeparatorBottom', chrome?.['separatorBottomHidden'] ? 'hidden' : null);
|
|
344
345
|
setFlag(el, 'sbSeparatorTop', chrome?.['separatorTopHidden'] ? 'hidden' : null);
|
|
346
|
+
// iOS 26: the separator under a list row starts at its first text (styles.css).
|
|
347
|
+
const separator = chrome?.['separator'];
|
|
348
|
+
const leading = separator ? `${separator.x}px` : '';
|
|
349
|
+
if (el.style.getPropertyValue('--sb-separator-leading') !== leading) {
|
|
350
|
+
if (leading) el.style.setProperty('--sb-separator-leading', leading);
|
|
351
|
+
else el.style.removeProperty('--sb-separator-leading');
|
|
352
|
+
}
|
|
345
353
|
switch (node.kind) {
|
|
346
354
|
case 'styled':
|
|
347
355
|
applyBadge(result, node, el, box);
|
|
@@ -454,9 +462,7 @@ function applyChrome(
|
|
|
454
462
|
placeChrome(footer, chrome?.['footer'], frame);
|
|
455
463
|
if (header && texts?.['header']) setTextLayout(header, texts['header']);
|
|
456
464
|
if (footer && texts?.['footer']) {
|
|
457
|
-
// The
|
|
458
|
-
// keep their case, so draw the prop text in the engine's font and
|
|
459
|
-
// rect and let it wrap there.
|
|
465
|
+
// The prop text in the engine's font and rect, wrapped there.
|
|
460
466
|
setFont(footer, texts['footer'].font);
|
|
461
467
|
footer.style.textAlign = 'left';
|
|
462
468
|
footer.style.whiteSpace = 'pre-wrap';
|
|
@@ -473,6 +479,7 @@ function applyChrome(
|
|
|
473
479
|
// Extra scrollable space at the end (the engine's frames are unchanged).
|
|
474
480
|
content.style.height = `${size.height + bottomInset}px`;
|
|
475
481
|
}
|
|
482
|
+
bindLargeTitle(el, chrome?.['largeTitle'] !== undefined);
|
|
476
483
|
return;
|
|
477
484
|
}
|
|
478
485
|
case 'scrollview': {
|
|
@@ -487,6 +494,7 @@ function applyChrome(
|
|
|
487
494
|
s.marginLeft = `${rect ? rect.x - frame.x : 0}px`;
|
|
488
495
|
s.marginTop = `${rect ? rect.y - frame.y : 0}px`;
|
|
489
496
|
}
|
|
497
|
+
bindLargeTitle(el, chrome?.['largeTitle'] !== undefined);
|
|
490
498
|
return;
|
|
491
499
|
}
|
|
492
500
|
case 'navscreen': {
|
|
@@ -541,6 +549,8 @@ function applyChrome(
|
|
|
541
549
|
const card = el.querySelector<HTMLElement>(':scope > .sb-sheet-card');
|
|
542
550
|
const grabber = card?.querySelector<HTMLElement>(':scope > .sb-sheet-grabber') ?? null;
|
|
543
551
|
placeChrome(grabber, chrome?.['grabber'], frame);
|
|
552
|
+
// iOS 26: a sheet at a partial detent floats as a rounded card (styles.css).
|
|
553
|
+
setFlag(el, 'sbFloating', chrome?.['floating'] ? '' : null);
|
|
544
554
|
return;
|
|
545
555
|
}
|
|
546
556
|
case 'tabview': {
|
package/src/main.ts
CHANGED
|
@@ -579,7 +579,10 @@ async function boot(): Promise<void> {
|
|
|
579
579
|
// The standard directories a new home gets are saved with its first write.
|
|
580
580
|
const corsProxy = registeredApp()?.corsProxy;
|
|
581
581
|
app = await loadApp(url, onOps, {
|
|
582
|
-
|
|
582
|
+
// `?arg=` (repeatable) adds launch arguments after the app's name, as
|
|
583
|
+
// `simctl launch <app> <args>` does: CI's iOS fidelity workflow opens a
|
|
584
|
+
// screen of Examples/Fidelity with `?arg=-screen&arg=<name>`.
|
|
585
|
+
args: [requestedApp, ...params.getAll('arg')],
|
|
583
586
|
env,
|
|
584
587
|
bundle,
|
|
585
588
|
home,
|
package/src/mock.ts
CHANGED
|
@@ -381,8 +381,8 @@ export function galleryTextStyleIds(style: TextStyle): { styled: number; text: n
|
|
|
381
381
|
return { styled: GALLERY_IDS.firstTextStyle + 2 * i, text: GALLERY_IDS.firstTextStyle + 2 * i + 1 };
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
const BLUE = { r: 0, g: 0.
|
|
385
|
-
const RED = { r: 1, g: 0.
|
|
384
|
+
const BLUE = { r: 0, g: 0.533, b: 1, a: 1 };
|
|
385
|
+
const RED = { r: 1, g: 0.22, b: 0.235, a: 1 };
|
|
386
386
|
const GREEN = { r: 0.204, g: 0.78, b: 0.349, a: 1 };
|
|
387
387
|
|
|
388
388
|
/** The animated box: `styled{frame}` > `shape`; expanded doubles the width and swaps the fill. */
|
package/src/protocol.ts
CHANGED
|
@@ -140,6 +140,17 @@ export interface RadialGradientStyle extends StyleOpacity {
|
|
|
140
140
|
radialGradient: RadialGradient;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export interface AngularGradient {
|
|
144
|
+
stops: GradientStop[];
|
|
145
|
+
center: UnitPoint;
|
|
146
|
+
/** Degrees, clockwise from the positive x axis (3 o'clock); the stops span start...end. */
|
|
147
|
+
startAngle: number;
|
|
148
|
+
endAngle: number;
|
|
149
|
+
}
|
|
150
|
+
export interface AngularGradientStyle extends StyleOpacity {
|
|
151
|
+
angularGradient: AngularGradient;
|
|
152
|
+
}
|
|
153
|
+
|
|
143
154
|
/** `Color.gradient`: a subtle vertical gradient derived from one color. */
|
|
144
155
|
export interface ColorGradientStyle extends StyleOpacity {
|
|
145
156
|
colorGradient: Color;
|
|
@@ -156,6 +167,7 @@ export type ShapeStyle =
|
|
|
156
167
|
| HierarchicalStyle
|
|
157
168
|
| LinearGradientStyle
|
|
158
169
|
| RadialGradientStyle
|
|
170
|
+
| AngularGradientStyle
|
|
159
171
|
| ColorGradientStyle
|
|
160
172
|
| MaterialStyle;
|
|
161
173
|
|
|
@@ -622,10 +634,11 @@ export interface Stroke {
|
|
|
622
634
|
* `data-sb-stroke`.
|
|
623
635
|
*/
|
|
624
636
|
inset?: boolean;
|
|
625
|
-
/** A custom shape's `StrokeStyle` (a built-in shape's border has none of these). */
|
|
637
|
+
/** A custom shape's `StrokeStyle` (a built-in shape's border has none of these but `dash`). */
|
|
626
638
|
lineCap?: 'butt' | 'round' | 'square';
|
|
627
639
|
lineJoin?: 'miter' | 'round' | 'bevel';
|
|
628
640
|
miterLimit?: number;
|
|
641
|
+
/** A built-in shape draws any dash pattern as CSS dashes. */
|
|
629
642
|
dash?: number[];
|
|
630
643
|
dashPhase?: number;
|
|
631
644
|
}
|
|
@@ -787,7 +800,9 @@ export type GaugeStyle =
|
|
|
787
800
|
| 'linearCapacity'
|
|
788
801
|
| 'accessoryLinear'
|
|
789
802
|
| 'accessoryCircular'
|
|
790
|
-
| 'accessoryCircularCapacity'
|
|
803
|
+
| 'accessoryCircularCapacity'
|
|
804
|
+
/** A determinate `ProgressView`: the label above the bar, the current value label under it. */
|
|
805
|
+
| 'progress';
|
|
791
806
|
/**
|
|
792
807
|
* `Gauge`: `value` is normalized to 0...1. Children in order: the label (when
|
|
793
808
|
* `hasLabel`) and the current value label (when `hasCurrentValueLabel`).
|
|
@@ -1383,6 +1398,11 @@ export function isRadialGradientStyle(style: ShapeStyle): style is RadialGradien
|
|
|
1383
1398
|
return typeof g === 'object' && g !== null;
|
|
1384
1399
|
}
|
|
1385
1400
|
|
|
1401
|
+
export function isAngularGradientStyle(style: ShapeStyle): style is AngularGradientStyle {
|
|
1402
|
+
const g = (style as AngularGradientStyle).angularGradient;
|
|
1403
|
+
return typeof g === 'object' && g !== null;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1386
1406
|
export function isColorGradientStyle(style: ShapeStyle): style is ColorGradientStyle {
|
|
1387
1407
|
const c = (style as ColorGradientStyle).colorGradient;
|
|
1388
1408
|
return typeof c === 'object' && c !== null;
|
package/src/renderer.ts
CHANGED
|
@@ -137,7 +137,7 @@ import {
|
|
|
137
137
|
snapSliderValue,
|
|
138
138
|
timeInputValue,
|
|
139
139
|
} from './layout';
|
|
140
|
-
import { renderSymbol } from './symbols';
|
|
140
|
+
import { filledSymbolName, renderSymbol } from './symbols';
|
|
141
141
|
import { applyPathShape, hasPathFrames, playPathFrames } from './pathShape';
|
|
142
142
|
import { VideoEvents, applyVideo, stopVideos } from './video';
|
|
143
143
|
import { applyCanvas, createCanvasElement } from './canvasDrawing';
|
|
@@ -228,8 +228,8 @@ const ALL_KINDS: ReadonlySet<string> = new Set<ElementKind>([
|
|
|
228
228
|
|
|
229
229
|
/** Duration of the alert / dialog fade (Phase 8); keep in sync with styles.css. */
|
|
230
230
|
export const ALERT_TRANSITION_MS = 250;
|
|
231
|
-
/**
|
|
232
|
-
const SLIDER_THUMB =
|
|
231
|
+
/** Width of the slider thumb (iOS 26: a 38×24 capsule); keep in sync with the engine and styles.css. */
|
|
232
|
+
const SLIDER_THUMB = 38;
|
|
233
233
|
|
|
234
234
|
/** Width of the menu a `menu` picker (or a Phase 6 `menu`) opens, like an iOS context menu. */
|
|
235
235
|
export const PICKER_MENU_WIDTH = 250;
|
|
@@ -2202,7 +2202,8 @@ function refreshTabBar(tabview: HTMLElement): void {
|
|
|
2202
2202
|
const icon = item.querySelector<HTMLElement>(':scope > .sb-tabbar-icon');
|
|
2203
2203
|
if (icon && icon.dataset['sbSymbol'] !== symbol) {
|
|
2204
2204
|
icon.dataset['sbSymbol'] = symbol;
|
|
2205
|
-
|
|
2205
|
+
// iOS draws tab bar symbols in their filled form.
|
|
2206
|
+
const svg = symbol ? renderSymbol(filledSymbolName(symbol)) : null;
|
|
2206
2207
|
if (svg) icon.replaceChildren(svg);
|
|
2207
2208
|
else icon.replaceChildren();
|
|
2208
2209
|
icon.toggleAttribute('data-sb-missing', Boolean(symbol) && !svg);
|
|
@@ -2761,7 +2762,8 @@ function applyShape(el: HTMLElement, props: ShapeProps): void {
|
|
|
2761
2762
|
if (stroke) {
|
|
2762
2763
|
const width = Number.isFinite(stroke.lineWidth) ? Math.max(0, stroke.lineWidth) : 1;
|
|
2763
2764
|
const color = stroke.color ? styleToCSS(stroke.color, 'paint').solid : 'currentColor';
|
|
2764
|
-
|
|
2765
|
+
// A dash pattern is drawn as CSS dashes (their lengths are the browser's).
|
|
2766
|
+
s.border = `${width}px ${stroke.dash?.length ? 'dashed' : 'solid'} ${color}`;
|
|
2765
2767
|
el.dataset['sbStroke'] = stroke.inset ? 'inset' : 'center';
|
|
2766
2768
|
}
|
|
2767
2769
|
// A fill with a drop shadow (`.shadow(.drop(…))` on the style).
|
|
@@ -3050,7 +3052,9 @@ function applyNavScreen(el: HTMLElement, props: NavScreenProps): void {
|
|
|
3050
3052
|
el.dataset['sbDisplay'] = large ? 'large' : 'inline';
|
|
3051
3053
|
const inlineTitle = el.querySelector<HTMLElement>(':scope > .sb-navbar > .sb-navbar-title');
|
|
3052
3054
|
const largeTitle = el.querySelector<HTMLElement>(':scope > .sb-navscreen-body > .sb-navbar-large-title');
|
|
3053
|
-
|
|
3055
|
+
// A large title that scrolls away collapses into the inline one (shown
|
|
3056
|
+
// with `data-sb-collapsed`, src/largeTitle.ts).
|
|
3057
|
+
if (inlineTitle) inlineTitle.textContent = title;
|
|
3054
3058
|
if (largeTitle) {
|
|
3055
3059
|
largeTitle.textContent = large ? title : '';
|
|
3056
3060
|
largeTitle.hidden = !large;
|
|
@@ -3309,11 +3313,15 @@ export function applyStyle(el: HTMLElement, style: Style): void {
|
|
|
3309
3313
|
let opacity = style.opacity;
|
|
3310
3314
|
if (style.disabled) {
|
|
3311
3315
|
// `.disabled(true)`: dim the subtree and block input.
|
|
3316
|
+
// The undimmed opacity stays available to styles.css, which draws a
|
|
3317
|
+
// disabled bordered button gray instead of faded.
|
|
3318
|
+
s.setProperty('--sb-undimmed-opacity', String(opacity ?? 1));
|
|
3312
3319
|
opacity = (opacity ?? 1) * 0.4;
|
|
3313
3320
|
s.pointerEvents = 'none';
|
|
3314
3321
|
el.dataset['sbDisabled'] = '';
|
|
3315
3322
|
el.setAttribute('aria-disabled', 'true');
|
|
3316
3323
|
} else {
|
|
3324
|
+
s.removeProperty('--sb-undimmed-opacity');
|
|
3317
3325
|
el.removeAttribute('aria-disabled');
|
|
3318
3326
|
}
|
|
3319
3327
|
if (opacity != null) {
|
|
@@ -3354,8 +3362,11 @@ export function applyStyle(el: HTMLElement, style: Style): void {
|
|
|
3354
3362
|
|
|
3355
3363
|
// Phase 6 (docs/phase6/layout-containers.md).
|
|
3356
3364
|
if (style.tint) {
|
|
3357
|
-
// `.tint`: gauges, switches and prominent buttons below read `--sb-tint
|
|
3358
|
-
|
|
3365
|
+
// `.tint`: gauges, switches and prominent buttons below read `--sb-tint`;
|
|
3366
|
+
// a bordered button fills with the tint, faintly (`--sb-tint-fill`).
|
|
3367
|
+
const tint = colorToCSS(style.tint);
|
|
3368
|
+
s.setProperty('--sb-tint', tint);
|
|
3369
|
+
s.setProperty('--sb-tint-fill', `color-mix(in srgb, ${tint} var(--sb-tint-fill-amount), transparent)`);
|
|
3359
3370
|
}
|
|
3360
3371
|
if (style.position) {
|
|
3361
3372
|
const { x, y } = style.position;
|
package/src/scanner.ts
CHANGED
|
@@ -115,7 +115,7 @@ function pillButton(label: string, primary: boolean): HTMLButtonElement {
|
|
|
115
115
|
button.type = 'button';
|
|
116
116
|
button.textContent = label;
|
|
117
117
|
button.style.cssText = `padding:12px 28px;border:0;border-radius:999px;font:600 17px ${FONT};color:#fff;background:${
|
|
118
|
-
primary ? '#
|
|
118
|
+
primary ? '#0088ff' : 'rgba(255,255,255,.25)'
|
|
119
119
|
};backdrop-filter:blur(12px)`;
|
|
120
120
|
return button;
|
|
121
121
|
}
|