@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 +1 -1
- package/src/layout/engine.ts +43 -2
- package/src/layout/types.ts +6 -0
- package/src/layoutDom.ts +5 -0
- package/src/renderer.ts +82 -1
- package/src/styles.css +23 -0
package/package.json
CHANGED
package/src/layout/engine.ts
CHANGED
|
@@ -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
|
|
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.
|
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
|
@@ -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
|
-
|
|
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;
|