@swiftbrowser/web 0.26.5 → 0.26.6

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.26.5",
3
+ "version": "0.26.6",
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": {
@@ -475,6 +475,8 @@ const MAC_LIST_FOOTER_BOTTOM = 6;
475
475
  // the bar and 10 above the rows. A screen not at the window's leading edge
476
476
  // starts its toolbar 10 in.
477
477
  const MAC_NAVSPLIT_SIDEBAR_WIDTH = 140;
478
+ /** The width of the strip astride the sidebar panel's trailing edge that drags the sidebar's width. */
479
+ const MAC_NAVSPLIT_DIVIDER_GRIP = 8;
478
480
  const MAC_NAVSPLIT_SIDEBAR_INSET = 8;
479
481
  const MAC_NAVSPLIT_TOGGLE_INSET = 6;
480
482
  const MAC_SIDEBAR_SEARCH_ABOVE = 9;
@@ -1031,6 +1033,32 @@ function cellStyle(node: LayoutNode, key: string): unknown {
1031
1033
  return undefined;
1032
1034
  }
1033
1035
 
1036
+ /**
1037
+ * A split view column's `.navigationSplitViewColumnWidth(min:ideal:max:)`:
1038
+ * the `splitColumnWidth` hint (`{ min?, ideal, max? }`) on the column's
1039
+ * content, read through its `styled` wrappers, its `navstack` and the root
1040
+ * screen of that stack (an app puts the modifier on the sidebar's list).
1041
+ */
1042
+ function splitColumnWidthOf(column: LayoutNode): { min?: number; ideal: number; max?: number } | null {
1043
+ let current: LayoutNode | undefined = column;
1044
+ while (current) {
1045
+ const hint = cellStyle(current, 'splitColumnWidth');
1046
+ if (hint && typeof hint === 'object') {
1047
+ const spec = hint as Record<string, unknown>;
1048
+ const ideal = num(spec['ideal']);
1049
+ if (ideal === null) return null;
1050
+ const min = num(spec['min']);
1051
+ const max = num(spec['max']);
1052
+ return { ...(min !== null ? { min } : {}), ideal, ...(max !== null ? { max } : {}) };
1053
+ }
1054
+ const inner = unwrapStyled(current);
1055
+ if (inner.kind === 'navstack') current = inner.children[0] as LayoutNode | undefined;
1056
+ else if (inner.kind === 'navscreen') current = inner.children.find((child) => child.kind !== 'toolbar' && child.kind !== 'searchfield');
1057
+ else return null;
1058
+ }
1059
+ return null;
1060
+ }
1061
+
1034
1062
  /** The default vertical padding above and below a list row's content. */
1035
1063
  function listRowVPad(row: LayoutNode, sidebar: boolean, mac: boolean): number {
1036
1064
  if (mac) return row.kind === 'button' ? MAC_LIST_BUTTON_ROW_VPAD : sidebar ? MAC_LIST_SIDEBAR_ROW_VPAD : MAC_LIST_ROW_VPAD;
@@ -5438,7 +5466,18 @@ export class LayoutPass {
5438
5466
  const trailing = x + size.width >= screen.width - EPSILON ? safeArea.trailing : 0;
5439
5467
  const usable = Math.max(0, size.width - leading - trailing);
5440
5468
  const mac = this.env.platform === 'macos';
5441
- const sidebarWidth = Math.min(mac ? MAC_NAVSPLIT_SIDEBAR_WIDTH : NAVSPLIT_SIDEBAR_WIDTH, usable * NAVSPLIT_SIDEBAR_MAX_FRACTION);
5469
+ const [sidebar, detail, ...rest] = node.children;
5470
+ // The sidebar's width: the app's `.navigationSplitViewColumnWidth` (a
5471
+ // `splitColumnWidth` hint on the sidebar's content, its ideal width
5472
+ // between its min and max), else the platform's default; on a Mac the
5473
+ // user drags the column's edge (`env.sidebarWidths`) between the same
5474
+ // bounds. Never more than 40% of the width.
5475
+ const requested = sidebar ? splitColumnWidthOf(sidebar) : null;
5476
+ const fallback = mac ? MAC_NAVSPLIT_SIDEBAR_WIDTH : NAVSPLIT_SIDEBAR_WIDTH;
5477
+ const minWidth = requested?.min ?? Math.min(fallback, requested?.ideal ?? fallback);
5478
+ const maxWidth = Math.min(requested?.max ?? Infinity, usable * NAVSPLIT_SIDEBAR_MAX_FRACTION);
5479
+ const dragged = mac ? this.env.sidebarWidths?.[node.id] : undefined;
5480
+ const sidebarWidth = Math.max(0, Math.min(maxWidth, Math.max(minWidth, dragged ?? requested?.ideal ?? fallback)));
5442
5481
  const visible = node.props['sidebarVisible'] !== false;
5443
5482
  // A Mac's sidebar is a panel inset from the window's edges, with no separator.
5444
5483
  const panelInset = mac ? MAC_NAVSPLIT_SIDEBAR_INSET : 0;
@@ -5465,9 +5504,11 @@ export class LayoutPass {
5465
5504
  };
5466
5505
  // The Mac's sidebar panel (the page draws it), inset from the window's edges.
5467
5506
  if (mac) chrome['panel'] = { x: sidebarX, y: y + panelInset, width: sidebarWidth, height: Math.max(0, size.height - 2 * panelInset) };
5507
+ // The Mac's column divider (the page's drag handle for the sidebar's
5508
+ // width), a strip astride the panel's trailing edge while it shows.
5509
+ if (mac && visible) chrome['divider'] = { x: separatorX - MAC_NAVSPLIT_DIVIDER_GRIP / 2, y, width: MAC_NAVSPLIT_DIVIDER_GRIP, height: size.height };
5468
5510
  this.setChrome(node, chrome);
5469
5511
  const toggleRoom = toggleSize + (mac ? MAC_BAR_GROUP_GAP : NAV_GROUP_SPACING);
5470
- const [sidebar, detail, ...rest] = node.children;
5471
5512
  if (sidebar) {
5472
5513
  const proposal: Proposal = { width: sidebarWidth, height: size.height };
5473
5514
  // On a Mac the toggle is at the sidebar's trailing end: its screens' items start where they would.
@@ -127,6 +127,12 @@ export interface LayoutEnvironment {
127
127
  * shows its `keyboard` toolbars above the keyboard.
128
128
  */
129
129
  focusedField?: number;
130
+ /**
131
+ * The sidebar widths the user dragged a `navsplit`'s column divider to (a
132
+ * Mac), by the `navsplit`'s element id. The engine keeps each between the
133
+ * column's min and max (its `splitColumnWidth` hint, else the defaults).
134
+ */
135
+ sidebarWidths?: Record<number, number>;
130
136
  }
131
137
 
132
138
  export type TextAlignment = 'leading' | 'center' | 'trailing';
package/src/layoutDom.ts CHANGED
@@ -1070,6 +1070,11 @@ function applyChrome(
1070
1070
  // Phase 7: the sidebar toggle (the sidebar column draws the hairline).
1071
1071
  placeChrome(el.querySelector<HTMLElement>(':scope > .sb-navsplit-toggle'), chrome?.['toggle'], box);
1072
1072
  placeChrome(el.querySelector<HTMLElement>(':scope > .sb-navsplit-panel'), chrome?.['panel'], box);
1073
+ // The Mac's column divider, astride the panel's trailing edge; it
1074
+ // carries the sidebar's width for the drag that resizes it (renderer.ts).
1075
+ const divider = el.querySelector<HTMLElement>(':scope > .sb-navsplit-divider');
1076
+ placeChrome(divider, chrome?.['divider'], box, false);
1077
+ if (divider && chrome?.['panel']) divider.dataset['sbWidth'] = String(chrome['panel'].width);
1073
1078
  setFlag(el, 'sbSidebarHidden', node.props['sidebarVisible'] === false ? '' : null);
1074
1079
  return;
1075
1080
  }
package/src/renderer.ts CHANGED
@@ -1015,6 +1015,64 @@ export class Renderer {
1015
1015
  if (slider && this.root.contains(slider)) this.beginSliderDrag(slider, event);
1016
1016
  };
1017
1017
 
1018
+ /**
1019
+ * The sidebar widths the user dragged each `navsplit`'s column divider to
1020
+ * (a Mac), by element id: the engine lays the split out with them, clamped
1021
+ * to the column's bounds (`LayoutEnvironment.sidebarWidths`).
1022
+ */
1023
+ private readonly sidebarWidths: Record<number, number> = {};
1024
+ private sidebarDrag: SidebarDrag | null = null;
1025
+
1026
+ // A Mac split view's divider: the pointer down that starts resizing the sidebar.
1027
+ private readonly onDividerPointerDown = (event: PointerEvent): void => {
1028
+ if (event.button !== 0 || this.sidebarDrag) return;
1029
+ const target = event.target as HTMLElement | null;
1030
+ const divider = target?.closest<HTMLElement>('.sb-navsplit-divider');
1031
+ const split = divider?.parentElement;
1032
+ if (!divider || !split || !this.root.contains(divider)) return;
1033
+ const id = Number(split.dataset[ID_ATTR]);
1034
+ const width = Number(divider.dataset['sbWidth']);
1035
+ if (!Number.isInteger(id) || !Number.isFinite(width)) return;
1036
+ event.preventDefault();
1037
+ // The presenting tree may be scaled (behind a sheet): pointer distances are in screen px.
1038
+ const rect = split.getBoundingClientRect();
1039
+ const scale = split.offsetWidth > 0 ? rect.width / split.offsetWidth : 1;
1040
+ this.sidebarDrag = { id, pointerId: event.pointerId, startX: event.clientX, startWidth: width, scale, frame: null, pending: width };
1041
+ this.root.classList.add('sb-resizing-sidebar');
1042
+ window.addEventListener('pointermove', this.onDividerMove);
1043
+ window.addEventListener('pointerup', this.onDividerUp);
1044
+ window.addEventListener('pointercancel', this.onDividerUp);
1045
+ };
1046
+
1047
+ private readonly onDividerMove = (event: PointerEvent): void => {
1048
+ const drag = this.sidebarDrag;
1049
+ if (!drag || event.pointerId !== drag.pointerId) return;
1050
+ drag.pending = Math.round(drag.startWidth + (event.clientX - drag.startX) / drag.scale);
1051
+ if (drag.frame !== null) return;
1052
+ // One layout per frame, however many pointer moves arrive.
1053
+ drag.frame = window.requestAnimationFrame(() => {
1054
+ drag.frame = null;
1055
+ if (this.sidebarWidths[drag.id] === drag.pending) return;
1056
+ this.sidebarWidths[drag.id] = drag.pending;
1057
+ if (this.commits > 0) this.layout();
1058
+ });
1059
+ };
1060
+
1061
+ private readonly onDividerUp = (event: PointerEvent): void => {
1062
+ const drag = this.sidebarDrag;
1063
+ if (!drag || event.pointerId !== drag.pointerId) return;
1064
+ if (drag.frame !== null) window.cancelAnimationFrame(drag.frame);
1065
+ this.sidebarDrag = null;
1066
+ this.root.classList.remove('sb-resizing-sidebar');
1067
+ window.removeEventListener('pointermove', this.onDividerMove);
1068
+ window.removeEventListener('pointerup', this.onDividerUp);
1069
+ window.removeEventListener('pointercancel', this.onDividerUp);
1070
+ if (this.sidebarWidths[drag.id] !== drag.pending) {
1071
+ this.sidebarWidths[drag.id] = drag.pending;
1072
+ if (this.commits > 0) this.layout();
1073
+ }
1074
+ };
1075
+
1018
1076
  constructor(root: HTMLElement, onEvent: EventCallback) {
1019
1077
  this.root = root;
1020
1078
  this.onEvent = onEvent;
@@ -1079,6 +1137,7 @@ export class Renderer {
1079
1137
  });
1080
1138
  root.addEventListener('change', this.onChange);
1081
1139
  root.addEventListener('pointerdown', this.onSliderPointerDown);
1140
+ root.addEventListener('pointerdown', this.onDividerPointerDown);
1082
1141
  if (typeof ResizeObserver !== 'undefined') {
1083
1142
  this.resizeObserver = new ResizeObserver(() => {
1084
1143
  const before = this.rootSize;
@@ -1605,6 +1664,7 @@ export class Renderer {
1605
1664
  ...(this.contentHeight ? { contentHeight: true } : {}),
1606
1665
  ...(this.keyboard > 0 ? { keyboard: this.keyboard } : {}),
1607
1666
  ...(focused !== null ? { focusedField: focused } : {}),
1667
+ ...(Object.keys(this.sidebarWidths).length > 0 ? { sidebarWidths: { ...this.sidebarWidths } } : {}),
1608
1668
  };
1609
1669
  }
1610
1670
 
@@ -2114,6 +2174,8 @@ export class Renderer {
2114
2174
  this.root.removeEventListener('scroll', drawScrollTransitions, true);
2115
2175
  this.root.removeEventListener('change', this.onChange);
2116
2176
  this.root.removeEventListener('pointerdown', this.onSliderPointerDown);
2177
+ this.root.removeEventListener('pointerdown', this.onDividerPointerDown);
2178
+ if (this.sidebarDrag) this.onDividerUp({ pointerId: this.sidebarDrag.pointerId } as PointerEvent);
2117
2179
  this.reset();
2118
2180
  }
2119
2181
 
@@ -3375,7 +3437,11 @@ function createShell(kind: ElementKind): HTMLElement {
3375
3437
  // The Mac's floating sidebar panel, under the columns (the engine's `panel` rect).
3376
3438
  const panel = div('sb-navsplit-panel');
3377
3439
  panel.hidden = true;
3378
- el.append(panel, slot, toggle);
3440
+ // The Mac's column divider (the engine's `divider` rect): dragging it
3441
+ // resizes the sidebar, as SwiftUI's does.
3442
+ const divider = div('sb-navsplit-divider');
3443
+ divider.hidden = true;
3444
+ el.append(panel, slot, divider, toggle);
3379
3445
  SLOTS.set(el, slot);
3380
3446
  return el;
3381
3447
  }
@@ -3858,6 +3924,21 @@ function animateAlertOut(container: HTMLElement, alert: HTMLElement): void {
3858
3924
  afterAnimation(clone, ALERT_TRANSITION_MS, () => clone.remove());
3859
3925
  }
3860
3926
 
3927
+ /** A drag of a Mac split view's column divider (`.sb-navsplit-divider`). */
3928
+ interface SidebarDrag {
3929
+ /** The `navsplit` element. */
3930
+ id: ElementId;
3931
+ pointerId: number;
3932
+ startX: number;
3933
+ /** The sidebar's width when the drag began (the divider's `data-sb-width`). */
3934
+ startWidth: number;
3935
+ /** Screen px per layout px of the split view (the presenting tree may be scaled). */
3936
+ scale: number;
3937
+ frame: number | null;
3938
+ /** The width the pointer asks for, laid out at the next animation frame. */
3939
+ pending: number;
3940
+ }
3941
+
3861
3942
  interface SliderDrag {
3862
3943
  slider: HTMLElement;
3863
3944
  track: HTMLElement;
package/src/styles.css CHANGED
@@ -5602,6 +5602,29 @@ html[data-sb-platform="macos"] .sb-navsplit-panel:not([hidden]) {
5602
5602
  display: block;
5603
5603
  }
5604
5604
 
5605
+ /* The Mac's column divider: an invisible strip astride the sidebar panel's
5606
+ trailing edge (the engine's `divider` rect) that drags the sidebar's width,
5607
+ as SwiftUI's split view lets the user. Over the columns, under the toggle. */
5608
+ .sb-navsplit-divider {
5609
+ display: none;
5610
+ }
5611
+
5612
+ html[data-sb-platform="macos"] .sb-navsplit-divider:not([hidden]) {
5613
+ display: block;
5614
+ position: absolute;
5615
+ z-index: 1;
5616
+ cursor: col-resize;
5617
+ touch-action: none;
5618
+ }
5619
+
5620
+ /* While a divider is dragged the whole page keeps its cursor and selects no text. */
5621
+ .sb-resizing-sidebar,
5622
+ .sb-resizing-sidebar * {
5623
+ cursor: col-resize !important;
5624
+ user-select: none;
5625
+ -webkit-user-select: none;
5626
+ }
5627
+
5605
5628
  html[data-sb-platform="macos"] .sb-navsplit-columns > [data-sb-id]:first-child,
5606
5629
  html[data-sb-platform="macos"] .sb-navsplit-columns > [data-sb-ghost]:first-child {
5607
5630
  box-shadow: none;