@swiftbrowser/web 0.26.4 → 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.
@@ -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
@@ -161,7 +161,7 @@ function applyFrames(ctx: LayoutDomContext, result: LayoutResult, changes: BoxCh
161
161
  applyChrome(result, node, el, box, frame, scrollBottomInset(ctx, result, node, frame), previous === null, changes);
162
162
  if (node.kind === 'tab') setTabHidden(el, result.hidden.has(id));
163
163
  // A `sidebar` list draws as one (no cards or hairlines) in a regular width; in a compact one it looks like `insetGrouped`.
164
- if (node.kind === 'list') setFlag(el, 'sbListLook', node.props['style'] === 'sidebar' && isRegularWidth(ctx.env.screen) ? 'sidebar' : null);
164
+ if (node.kind === 'list') setFlag(el, 'sbListLook', (node.props['style'] === 'sidebar' || result.chrome.get(id)?.['sidebarLook']) && isRegularWidth(ctx.env.screen) ? 'sidebar' : null);
165
165
  // A child the engine did not touch kept its screen frame, but if this
166
166
  // element's box moved, the child's `left`/`top` (relative to it) did
167
167
  // not: the plus button at the bottom of a chat's composer stays put on
@@ -343,6 +343,8 @@ function placeEditControl(node: HTMLElement | null, rect: Rect | undefined, orig
343
343
  * goes, scales and fades as the items do (`TOOLBAR_TRANSITION`), in the
344
344
  * toolbar's own spring.
345
345
  */
346
+ const BAR_PLACEMENTS = new Set(['leading', 'trailing', 'bottom']);
347
+
346
348
  function syncBarGlass(
347
349
  result: LayoutResult,
348
350
  node: LayoutNode,
@@ -354,7 +356,9 @@ function syncBarGlass(
354
356
  ): void {
355
357
  for (const placement of ['leading', 'trailing', 'bottom'] as const) {
356
358
  const unmatched = new Set(screen.querySelectorAll<HTMLElement>(`:scope > .sb-bar-glass[data-sb-placement="${placement}"]:not([data-sb-exiting])`));
357
- const toolbars = node.children.filter((child) => child.kind === 'toolbar' && child.props['placement'] === placement);
359
+ // A capsule's toolbars are those whose frames lie in it, of any bar
360
+ // placement: on a Mac a bottom toolbar's items are on the trailing capsules.
361
+ const toolbars = node.children.filter((child) => child.kind === 'toolbar' && BAR_PLACEMENTS.has(String(child.props['placement'])));
358
362
  for (let count = 0, rect = chrome?.[`${placement}Glass0`]; rect; rect = chrome?.[`${placement}Glass${++count}`]) {
359
363
  const capsule = rect;
360
364
  const ids = toolbars.filter((toolbar) => centeredIn(result.frames.get(toolbar.id), capsule)).map((toolbar) => String(toolbar.id));
@@ -881,6 +885,7 @@ function applyChrome(
881
885
  // the label column's texts (chrome `formLabel<i>`) as spans in the
882
886
  // content, as many as the engine placed.
883
887
  setFlag(el, 'sbFormColumns', chrome?.['columns'] ? '' : null);
888
+ setFlag(el, 'sbFormGrouped', chrome?.['grouped'] ? '' : null);
884
889
  if (content) {
885
890
  const texts = result.chromeText.get(node.id);
886
891
  const labels = content.querySelectorAll<HTMLElement>(':scope > .sb-form-label');
@@ -1064,6 +1069,12 @@ function applyChrome(
1064
1069
  case 'navsplit': {
1065
1070
  // Phase 7: the sidebar toggle (the sidebar column draws the hairline).
1066
1071
  placeChrome(el.querySelector<HTMLElement>(':scope > .sb-navsplit-toggle'), chrome?.['toggle'], box);
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);
1067
1078
  setFlag(el, 'sbSidebarHidden', node.props['sidebarVisible'] === false ? '' : null);
1068
1079
  return;
1069
1080
  }
package/src/protocol.ts CHANGED
@@ -625,6 +625,14 @@ export interface ListProps {
625
625
  style?: ListStyle;
626
626
  /** A `Form`: on a Mac the page lays its rows out in the columns style (labels in a column before their controls). */
627
627
  form?: boolean;
628
+ /** `.formStyle(_:)` other than `automatic`: on a Mac `grouped` lays the form out in grouped sections. */
629
+ formStyle?: 'grouped' | 'columns';
630
+ /**
631
+ * `.environment(\.defaultMinListRowHeight, _)`: the rows are at least
632
+ * this tall (points). Absent, the platform's default: 44 on iOS, 24 on a
633
+ * Mac. A Mac app spaces a content list's rows like a sidebar's, 32.
634
+ */
635
+ minRowHeight?: number;
628
636
  }
629
637
  export interface SectionProps {
630
638
  header: string | null;
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
 
@@ -3372,7 +3434,14 @@ function createShell(kind: ElementKind): HTMLElement {
3372
3434
  toggle.setAttribute('aria-label', 'Toggle Sidebar');
3373
3435
  const symbol = renderSymbol('sidebar.left');
3374
3436
  if (symbol) toggle.append(symbol);
3375
- el.append(slot, toggle);
3437
+ // The Mac's floating sidebar panel, under the columns (the engine's `panel` rect).
3438
+ const panel = div('sb-navsplit-panel');
3439
+ panel.hidden = true;
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);
3376
3445
  SLOTS.set(el, slot);
3377
3446
  return el;
3378
3447
  }
@@ -3855,6 +3924,21 @@ function animateAlertOut(container: HTMLElement, alert: HTMLElement): void {
3855
3924
  afterAnimation(clone, ALERT_TRANSITION_MS, () => clone.remove());
3856
3925
  }
3857
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
+
3858
3942
  interface SliderDrag {
3859
3943
  slider: HTMLElement;
3860
3944
  track: HTMLElement;