@swiftbrowser/web 0.5.4 → 0.5.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swiftbrowser/web",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "description": "SwiftBrowser web renderer and dev server: applies render ops from a Swift/Wasm app to the DOM inside an iPhone frame.",
6
6
  "repository": {
package/src/chartDom.ts CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  type GradientStop,
46
46
  type ShapeStyle,
47
47
  } from './protocol';
48
- import { MATERIAL_ALPHA, colorToCSS, styleToCSS } from './styles';
48
+ import { colorToCSS, materialAlpha, styleToCSS } from './styles';
49
49
 
50
50
  const SVG_NS = 'http://www.w3.org/2000/svg';
51
51
  const LINE_WIDTH_DEFAULT = 2;
@@ -330,7 +330,7 @@ export function paintFor(doc: Document, defs: SVGDefsElement, style: ShapeStyle
330
330
  if (isColor(style)) return colorToCSS(style);
331
331
  if (isHierarchicalStyle(style)) return styleToCSS(style, 'paint').value;
332
332
  if (isMaterialStyle(style)) {
333
- const alpha = (MATERIAL_ALPHA[style.material] ?? MATERIAL_ALPHA.regular) * clamp01(style.opacity ?? 1);
333
+ const alpha = materialAlpha(style.material) * clamp01(style.opacity ?? 1);
334
334
  return `rgba(${MATERIAL_GRAY}, ${r2(alpha * 0.5)})`;
335
335
  }
336
336
  const opacity = clamp01(style.opacity ?? 1);
@@ -276,7 +276,8 @@ function navSlot(item: LayoutNode, width: number): number {
276
276
  // and 18 either side, 6 between slots; a lone icon is a 48 circle. An item
277
277
  // with no text (symbols, a symbol and a chevron, a custom view) is an icon
278
278
  // at any width: at least 50, else its width and 8 either side, so a little
279
- // more width gives it a little more room (not text's 18 a side).
279
+ // more width gives it a little more room (not text's 18 a side). Alone in its
280
+ // group it is a circle only if it fits an icon's slot; wider, a capsule.
280
281
  const BOTTOM_GLASS_HEIGHT = 48;
281
282
  const BOTTOM_GLASS_MARGIN_H = 28;
282
283
  const BOTTOM_ICON_SLOT = 50;
@@ -1724,7 +1725,7 @@ export class LayoutPass {
1724
1725
 
1725
1726
  private buttonIsBordered(node: LayoutNode): boolean {
1726
1727
  const style = str(node.props['style']);
1727
- return style === 'bordered' || style === 'borderedProminent';
1728
+ return style === 'bordered' || style === 'borderedProminent' || style === 'glass';
1728
1729
  }
1729
1730
 
1730
1731
  private measureButton(node: LayoutNode, ctx: Context, p: Proposal): Size {
@@ -3428,7 +3429,8 @@ export class LayoutPass {
3428
3429
  const close = (): void => {
3429
3430
  if (open.length === 0) return;
3430
3431
  const icon = (item: Item): boolean => item.size.width < NAV_GLASS_SIZE;
3431
- const lone = open.length === 1 && icon(open[0] as Item);
3432
+ // a lone icon is a circle; a wider one (a symbol and a chevron) keeps its capsule
3433
+ const lone = open.length === 1 && icon(open[0] as Item) && (open[0] as Item).slot <= BOTTOM_ICON_SLOT;
3432
3434
  if (lone) (open[0] as Item).slot = BOTTOM_GLASS_HEIGHT;
3433
3435
  const slots = open.reduce((sum, item) => sum + item.slot, 0);
3434
3436
  tokens.push({ kind: 'group', items: open, width: slots + (open.length - 1) * BOTTOM_ITEM_GAP });
package/src/protocol.ts CHANGED
@@ -156,7 +156,8 @@ export interface ColorGradientStyle extends StyleOpacity {
156
156
  colorGradient: Color;
157
157
  }
158
158
 
159
- export type MaterialName = 'ultraThin' | 'thin' | 'regular' | 'thick' | 'ultraThick';
159
+ /** `glass` / `glassClear`: iOS 26 Liquid Glass (`.glassEffect`), the bar buttons' fill with the glass rim. */
160
+ export type MaterialName = 'ultraThin' | 'thin' | 'regular' | 'thick' | 'ultraThick' | 'glass' | 'glassClear';
160
161
  /** A translucent material: the system background at some opacity over a blurred backdrop. */
161
162
  export interface MaterialStyle extends StyleOpacity {
162
163
  material: MaterialName;
@@ -463,7 +464,7 @@ export interface HStackProps {
463
464
  export interface SpacerProps {
464
465
  minLength: number | null;
465
466
  }
466
- export type ButtonStyle = 'automatic' | 'plain' | 'bordered' | 'borderedProminent';
467
+ export type ButtonStyle = 'automatic' | 'plain' | 'bordered' | 'borderedProminent' | 'glass';
467
468
  /** Phase 8: `cancel` is the role an `alert` draws apart (bold, or in its own card). */
468
469
  export type ButtonRole = 'destructive' | 'cancel' | null;
469
470
  /**
package/src/renderer.ts CHANGED
@@ -583,7 +583,10 @@ export class Renderer {
583
583
  const active = document.activeElement;
584
584
  for (const input of this.root.querySelectorAll<HTMLElement>('.sb-textfield[data-sb-focus], .sb-texteditor[data-sb-focus]')) {
585
585
  const wanted = input.dataset['sbFocus'] === 'true';
586
- if (wanted && active !== input) input.focus();
586
+ // `preventScroll`: a field in a sheet still sliding in sits below the
587
+ // screen, and the browser would scroll the whole screen to reveal it,
588
+ // blanking the page until something scrolls it back.
589
+ if (wanted && active !== input) input.focus({ preventScroll: true });
587
590
  else if (!wanted && active === input) input.blur();
588
591
  }
589
592
  }
@@ -3223,6 +3226,8 @@ function applyPaint(el: HTMLElement, paint: PaintCSS): void {
3223
3226
  s.setProperty('backdrop-filter', paint.backdropFilter);
3224
3227
  s.setProperty('-webkit-backdrop-filter', paint.backdropFilter);
3225
3228
  }
3229
+ // Liquid Glass gets the rim (styles.css `[data-sb-glass]`).
3230
+ el.toggleAttribute('data-sb-glass', paint.glass === true);
3226
3231
  }
3227
3232
 
3228
3233
  // Stacks, spacers and zstacks are pure layout: the engine reads their props
package/src/styles.css CHANGED
@@ -108,7 +108,10 @@
108
108
  --sb-glass-selected: rgba(120, 120, 128, 0.16);
109
109
  --sb-color-segment-track: rgba(118, 118, 128, 0.12);
110
110
  --sb-color-segment-selected: #ffffff;
111
- --sb-color-menu-background: rgba(250, 250, 250, 0.96);
111
+ /* Menus, floating sheets and alerts are Liquid Glass too: translucent
112
+ over a heavier backdrop blur, with the glass rim. */
113
+ --sb-color-menu-background: rgba(250, 250, 250, 0.82);
114
+ --sb-sheet-glass: rgba(255, 255, 255, 0.55);
112
115
  }
113
116
 
114
117
  .device[data-theme="dark"] {
@@ -167,7 +170,9 @@
167
170
  --sb-color-tabbar-background: rgba(29, 29, 29, 0.94);
168
171
  --sb-color-segment-track: rgba(118, 118, 128, 0.24);
169
172
  --sb-color-segment-selected: #5a5a5f;
170
- --sb-color-menu-background: rgba(37, 37, 37, 0.96);
173
+ --sb-color-menu-background: rgba(37, 37, 37, 0.82);
174
+ /* The iPhone's dark floating sheet reads 42 over black: dark gray glass. */
175
+ --sb-sheet-glass: rgba(60, 60, 60, 0.7);
171
176
  }
172
177
 
173
178
  /* ---------------------------------------------------------------------------
@@ -500,14 +505,30 @@ html[data-sb-mode="device"] .device-screen {
500
505
  color: #ffffff;
501
506
  }
502
507
 
508
+ /* `.buttonStyle(.glass)`: a Liquid Glass capsule (the bar buttons' fill,
509
+ blur and rim) in the tint. */
510
+ .sb-button[data-sb-style="glass"] {
511
+ border-radius: 9999px;
512
+ background: var(--sb-glass-background);
513
+ box-shadow: var(--sb-glass-shadow);
514
+ backdrop-filter: blur(12px) saturate(180%);
515
+ -webkit-backdrop-filter: blur(12px) saturate(180%);
516
+ color: var(--sb-button-color, var(--sb-tint, var(--sb-color-accent)));
517
+ }
518
+
519
+ /* `.glassEffect`: a shape painted with the `glass` material (renderer). */
520
+ [data-sb-glass] {
521
+ box-shadow: var(--sb-glass-shadow);
522
+ }
523
+
503
524
  /* iOS 26 draws a disabled bordered button gray rather than faded
504
525
  (`.disabled` marks the element around the button and dims it inline; the
505
526
  renderer keeps the undimmed opacity in `--sb-undimmed-opacity`). */
506
- [data-sb-disabled]:has(> .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"])) {
527
+ [data-sb-disabled]:has(> .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"], [data-sb-style="glass"])) {
507
528
  opacity: var(--sb-undimmed-opacity, 1) !important;
508
529
  }
509
530
 
510
- [data-sb-disabled] > .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"]) {
531
+ [data-sb-disabled] > .sb-button:is([data-sb-style="bordered"], [data-sb-style="borderedProminent"], [data-sb-style="glass"]) {
511
532
  background: var(--sb-color-secondary-fill);
512
533
  color: var(--sb-color-tertiary-label);
513
534
  }
@@ -1386,9 +1407,10 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1386
1407
  rounded all around, over a lighter dim. */
1387
1408
  .sb-sheet[data-sb-floating] > .sb-sheet-card {
1388
1409
  border-radius: 38px;
1389
- background: color-mix(in srgb, var(--sb-color-sheet-background) 55%, transparent);
1390
- backdrop-filter: blur(24px) saturate(180%);
1391
- -webkit-backdrop-filter: blur(24px) saturate(180%);
1410
+ /* Liquid Glass: the content behind shows as soft blobs under a heavy blur. */
1411
+ background: var(--sb-sheet-glass);
1412
+ backdrop-filter: blur(40px) saturate(180%);
1413
+ -webkit-backdrop-filter: blur(40px) saturate(180%);
1392
1414
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.18);
1393
1415
  }
1394
1416
 
@@ -1832,11 +1854,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
1832
1854
  z-index: 6;
1833
1855
  border-radius: 13px;
1834
1856
  background: var(--sb-color-menu-background);
1835
- backdrop-filter: blur(30px) saturate(180%);
1836
- -webkit-backdrop-filter: blur(30px) saturate(180%);
1837
- box-shadow:
1838
- 0 0 0 0.5px rgba(0, 0, 0, 0.08),
1839
- 0 10px 40px rgba(0, 0, 0, 0.22);
1857
+ backdrop-filter: blur(40px) saturate(180%);
1858
+ -webkit-backdrop-filter: blur(40px) saturate(180%);
1859
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.22);
1840
1860
  overflow: hidden;
1841
1861
  transform-origin: 50% 0;
1842
1862
  animation: sb-menu-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1) both;
@@ -2211,7 +2231,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2211
2231
  dimmest at the top-right and bottom-left). A gradient-filled pseudo-element
2212
2232
  the size of the surface, masked to its outer 1px (the content box cut out
2213
2233
  of the padding box); it takes no input and sits above the surface's items,
2214
- which never reach the edge. */
2234
+ which never reach the edge. The surfaces: bar buttons and capsules, the
2235
+ tab bar, the search field, the sidebar toggle, menus, floating sheets,
2236
+ alert and dialog cards, `.glass` buttons and `.glassEffect` shapes. */
2215
2237
  .sb-navbar-back::after,
2216
2238
  .sb-navbar-more::after,
2217
2239
  .sb-tabbar::after,
@@ -2219,7 +2241,14 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
2219
2241
  .sb-toolbar[data-sb-placement="trailing"]::after,
2220
2242
  .sb-bottom-glass::after,
2221
2243
  .sb-searchfield::after,
2222
- .sb-navsplit-toggle::after {
2244
+ .sb-navsplit-toggle::after,
2245
+ .sb-picker-menu::after,
2246
+ .sb-picker-menu-exit::after,
2247
+ .sb-sheet[data-sb-floating] > .sb-sheet-card::after,
2248
+ .sb-alert-card::after,
2249
+ .sb-alert-cancel-card::after,
2250
+ .sb-button[data-sb-style="glass"]::after,
2251
+ [data-sb-glass]::after {
2223
2252
  content: "";
2224
2253
  position: absolute;
2225
2254
  inset: 0;
@@ -3139,8 +3168,9 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
3139
3168
  box-sizing: border-box;
3140
3169
  border-radius: 14px;
3141
3170
  background: color-mix(in srgb, var(--sb-color-secondary-system-background) 85%, transparent);
3142
- -webkit-backdrop-filter: blur(24px) saturate(180%);
3143
- backdrop-filter: blur(24px) saturate(180%);
3171
+ /* Liquid Glass: a heavy blur (what is behind an iPhone's alert is a formless glow). */
3172
+ -webkit-backdrop-filter: blur(60px) saturate(180%);
3173
+ backdrop-filter: blur(60px) saturate(180%);
3144
3174
  overflow: hidden;
3145
3175
  }
3146
3176
 
package/src/styles.ts CHANGED
@@ -106,6 +106,8 @@ export type PaintRole = 'text' | 'paint';
106
106
  export interface PaintCSS {
107
107
  /** `color` for a flat style, `gradient` for a CSS `<image>`, `material` for a translucent fill. */
108
108
  kind: 'color' | 'gradient' | 'material';
109
+ /** Liquid Glass: the element also gets the glass rim (`data-sb-glass`). */
110
+ glass?: boolean;
109
111
  /**
110
112
  * What to paint with: a `<color>` (`color`, `material`) or an `<image>`
111
113
  * (`gradient`). Valid as a `background` shorthand value either way.
@@ -137,7 +139,7 @@ const HIERARCHICAL_PAINT: Record<1 | 2 | 3 | 4, string> = {
137
139
  };
138
140
 
139
141
  /** Opacity of the system background each material shows over the blurred backdrop. */
140
- export const MATERIAL_ALPHA: Record<MaterialName, number> = {
142
+ export const MATERIAL_ALPHA: Record<Exclude<MaterialName, 'glass' | 'glassClear'>, number> = {
141
143
  ultraThin: 0.55,
142
144
  thin: 0.7,
143
145
  regular: 0.82,
@@ -145,6 +147,15 @@ export const MATERIAL_ALPHA: Record<MaterialName, number> = {
145
147
  ultraThick: 0.96,
146
148
  };
147
149
  export const MATERIAL_BLUR = 'blur(20px)';
150
+ /** Liquid Glass (`glass`, `glassClear`): the bar buttons' fill (`clear` at half of it) and blur; the renderer adds the rim. */
151
+ export const GLASS_BLUR = 'blur(20px) saturate(180%)';
152
+
153
+ /** The opacity a material's fill has (glass: the light fill's 70%, clear glass half of it). */
154
+ export function materialAlpha(name: MaterialName): number {
155
+ if (name === 'glass') return 0.7;
156
+ if (name === 'glassClear') return 0.35;
157
+ return MATERIAL_ALPHA[name] ?? MATERIAL_ALPHA.regular;
158
+ }
148
159
 
149
160
  /**
150
161
  * Resolve a ShapeStyle for a role. Unknown shapes resolve to magenta so they
@@ -180,8 +191,12 @@ export function styleToCSS(style: ShapeStyle, role: PaintRole): PaintCSS {
180
191
  const dark = withOpacity(`color-mix(in srgb, ${base}, black 12%)`, opacity);
181
192
  return { kind: 'gradient', value: `linear-gradient(to bottom, ${light}, ${dark})`, solid: withOpacity(base, opacity) };
182
193
  }
194
+ if (isMaterialStyle(style) && (style.material === 'glass' || style.material === 'glassClear')) {
195
+ const value = style.material === 'glass' ? 'var(--sb-glass-background)' : 'color-mix(in srgb, var(--sb-glass-background) 50%, transparent)';
196
+ return { kind: 'material', value, solid: value, backdropFilter: GLASS_BLUR, glass: true };
197
+ }
183
198
  if (isMaterialStyle(style)) {
184
- const alpha = MATERIAL_ALPHA[style.material] ?? MATERIAL_ALPHA.regular;
199
+ const alpha = MATERIAL_ALPHA[style.material as keyof typeof MATERIAL_ALPHA] ?? MATERIAL_ALPHA.regular;
185
200
  const value = `rgba(var(--sb-color-material-base), ${round(alpha * (opacity == null || !Number.isFinite(opacity) ? 1 : clamp01(opacity)))})`;
186
201
  return { kind: 'material', value, solid: value, backdropFilter: MATERIAL_BLUR };
187
202
  }