@swiftbrowser/web 0.2.0 → 0.3.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/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 engine measures footers upper-cased like headers; iOS footers
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
- args: [requestedApp],
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.478, b: 1, a: 1 };
385
- const RED = { r: 1, g: 0.231, b: 0.188, a: 1 };
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
- /** Diameter of the slider thumb (Phase 8); keep in sync with the engine and styles.css. */
232
- const SLIDER_THUMB = 27;
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
- const svg = symbol ? renderSymbol(symbol) : null;
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
- s.border = `${width}px solid ${color}`;
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
- if (inlineTitle) inlineTitle.textContent = large ? '' : title;
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
- s.setProperty('--sb-tint', colorToCSS(style.tint));
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 ? '#007aff' : 'rgba(255,255,255,.25)'
118
+ primary ? '#0088ff' : 'rgba(255,255,255,.25)'
119
119
  };backdrop-filter:blur(12px)`;
120
120
  return button;
121
121
  }