@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.
- package/package.json +1 -1
- package/src/deviceFrame.ts +2 -2
- package/src/layout/engine.ts +407 -173
- package/src/layout/types.ts +6 -0
- package/src/layoutDom.ts +13 -2
- package/src/protocol.ts +8 -0
- package/src/renderer.ts +85 -1
- package/src/styles.css +414 -146
- package/src/symbols.ts +3 -0
package/src/layout/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|