waybetter 0.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +71 -0
  3. package/dist/runtime/accountReport.d.ts +20 -0
  4. package/dist/runtime/accountReport.js +26 -0
  5. package/dist/runtime/accountReport.js.map +1 -0
  6. package/dist/runtime/domHub.d.ts +37 -0
  7. package/dist/runtime/domHub.js +134 -0
  8. package/dist/runtime/domHub.js.map +1 -0
  9. package/dist/runtime/linkRouting.d.ts +13 -0
  10. package/dist/runtime/linkRouting.js +35 -0
  11. package/dist/runtime/linkRouting.js.map +1 -0
  12. package/dist/runtime/netHub.d.ts +55 -0
  13. package/dist/runtime/netHub.js +268 -0
  14. package/dist/runtime/netHub.js.map +1 -0
  15. package/dist/runtime/paneNav.d.ts +35 -0
  16. package/dist/runtime/paneNav.js +311 -0
  17. package/dist/runtime/paneNav.js.map +1 -0
  18. package/dist/runtime/pluginApi.d.ts +59 -0
  19. package/dist/runtime/pluginApi.js +279 -0
  20. package/dist/runtime/pluginApi.js.map +1 -0
  21. package/dist/runtime/site.d.ts +32 -0
  22. package/dist/runtime/site.js +20 -0
  23. package/dist/runtime/site.js.map +1 -0
  24. package/dist/runtime/themeTokens.d.ts +64 -0
  25. package/dist/runtime/themeTokens.js +175 -0
  26. package/dist/runtime/themeTokens.js.map +1 -0
  27. package/dist/shell/addressModel.d.ts +34 -0
  28. package/dist/shell/addressModel.js +66 -0
  29. package/dist/shell/addressModel.js.map +1 -0
  30. package/dist/shell/chrome.d.ts +3 -0
  31. package/dist/shell/chrome.js +6 -0
  32. package/dist/shell/chrome.js.map +1 -0
  33. package/dist/shell/linkDrop.d.ts +29 -0
  34. package/dist/shell/linkDrop.js +57 -0
  35. package/dist/shell/linkDrop.js.map +1 -0
  36. package/dist/shell/palette.d.ts +13 -0
  37. package/dist/shell/palette.js +51 -0
  38. package/dist/shell/palette.js.map +1 -0
  39. package/dist/shell/paneModel.d.ts +33 -0
  40. package/dist/shell/paneModel.js +56 -0
  41. package/dist/shell/paneModel.js.map +1 -0
  42. package/dist/shell/profileModel.d.ts +52 -0
  43. package/dist/shell/profileModel.js +91 -0
  44. package/dist/shell/profileModel.js.map +1 -0
  45. package/dist/shell/updateModel.d.ts +35 -0
  46. package/dist/shell/updateModel.js +32 -0
  47. package/dist/shell/updateModel.js.map +1 -0
  48. package/dist/shell/windowControls.d.ts +24 -0
  49. package/dist/shell/windowControls.js +22 -0
  50. package/dist/shell/windowControls.js.map +1 -0
  51. package/package.json +112 -0
@@ -0,0 +1,268 @@
1
+ // One shared fetch/XHR hook per window for everything a site runtime does with the site's own
2
+ // requests: URL rewrites, gates (e.g. serializing racing mutations) and passive JSON taps. Each
3
+ // feature registers a stage instead of stacking its own wrapper. A request whose URL contains none
4
+ // of the site's API marks (images, video, WASM, scripts, telemetry) takes one substring check and
5
+ // goes straight to the wrapped fetch, and a tapped response is cloned, read and JSON-parsed once for
6
+ // all taps. Semantics are unchanged: the page gets the very promise (and Response) the inner fetch
7
+ // returned unless a gate answers a request itself, rejections and aborts pass through, and nothing
8
+ // here ever issues a request of its own.
9
+ const config = { apiMarks: [], xhrMarks: [] };
10
+ const hubs = new WeakMap();
11
+ /** Set once by the site adapter, before stages register. */
12
+ export function configureNetHub(next) {
13
+ if (next.apiMarks)
14
+ config.apiMarks = [...next.apiMarks];
15
+ if (next.xhrMarks)
16
+ config.xhrMarks = [...next.xhrMarks];
17
+ }
18
+ const hasMark = (url, marks) => {
19
+ for (const mark of marks)
20
+ if (url.includes(mark))
21
+ return true;
22
+ return false;
23
+ };
24
+ /** Whether `url` is one of the site's API calls (plain substring checks, no URL parsing). */
25
+ export function isApiUrl(url) {
26
+ return hasMark(url, config.apiMarks);
27
+ }
28
+ /** The request URL as given (string, URL or Request), without parsing it. */
29
+ export function rawUrl(input) {
30
+ try {
31
+ if (typeof input === 'string')
32
+ return input;
33
+ if (input instanceof URL)
34
+ return input.href;
35
+ if (input && typeof input.url === 'string')
36
+ return input.url;
37
+ }
38
+ catch { /* ignore */ }
39
+ return '';
40
+ }
41
+ function hubFor(target) {
42
+ const existing = hubs.get(target);
43
+ if (existing)
44
+ return existing;
45
+ let inner = null;
46
+ let wrapper = null;
47
+ const proto = target.XMLHttpRequest?.prototype;
48
+ let originalOpen;
49
+ let openWrapper;
50
+ let originalSend;
51
+ let sendWrapper;
52
+ const opened = new WeakMap();
53
+ const later = (run) => {
54
+ // Parsing a large body is the expensive part; keep it out of the page's own response handling.
55
+ const idle = target.requestIdleCallback;
56
+ if (typeof idle === 'function') {
57
+ try {
58
+ idle.call(target, run, { timeout: 200 });
59
+ return;
60
+ }
61
+ catch { /* fall through */ }
62
+ }
63
+ run();
64
+ };
65
+ const deliver = (taps, text) => {
66
+ const live = taps.filter(tap => hub.taps.includes(tap) && text.length <= tap.maxBytes);
67
+ if (!live.length)
68
+ return;
69
+ later(() => {
70
+ let body;
71
+ try {
72
+ body = JSON.parse(text);
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ for (const tap of live) {
78
+ if (!hub.taps.includes(tap))
79
+ continue;
80
+ try {
81
+ tap.onJson(body);
82
+ }
83
+ catch { /* A consumer failure never reaches the page. */ }
84
+ }
85
+ });
86
+ };
87
+ const matchingTaps = (url) => hub.taps.length ? hub.taps.filter(tap => { try {
88
+ return tap.match(url);
89
+ }
90
+ catch {
91
+ return false;
92
+ } }) : [];
93
+ const fetchWrapper = function (...args) {
94
+ const url = rawUrl(args[0]);
95
+ if (!hasMark(url, config.apiMarks))
96
+ return Reflect.apply(inner, this, args);
97
+ let [input, init] = args;
98
+ for (const rewrite of hub.rewrites) {
99
+ const next = rewrite(input, init);
100
+ if (next != null)
101
+ input = next;
102
+ }
103
+ const call = input === args[0] ? args : [input, init];
104
+ const send = () => Reflect.apply(inner, this, call);
105
+ let result = null;
106
+ for (const gate of hub.gates) {
107
+ result = gate(input, init, send);
108
+ if (result)
109
+ break;
110
+ }
111
+ result ??= send();
112
+ const taps = matchingTaps(rawUrl(input));
113
+ if (taps.length) {
114
+ result.then(response => {
115
+ if (!response.ok)
116
+ return;
117
+ const length = Number(response.headers.get('content-length') ?? 0);
118
+ if (!taps.some(tap => hub.taps.includes(tap) && length <= tap.maxBytes))
119
+ return;
120
+ return response.clone().text().then(text => deliver(taps, text));
121
+ }).catch(() => { });
122
+ }
123
+ return result;
124
+ };
125
+ const onLoad = function (taps) {
126
+ try {
127
+ if (this.status < 200 || this.status >= 300)
128
+ return;
129
+ if (this.responseType === '' || this.responseType === 'text')
130
+ deliver(taps, this.responseText);
131
+ else if (this.responseType === 'json' && this.response) {
132
+ const body = this.response;
133
+ for (const tap of taps)
134
+ if (hub.taps.includes(tap)) {
135
+ try {
136
+ tap.onJson(body);
137
+ }
138
+ catch { /* ignore */ }
139
+ }
140
+ }
141
+ }
142
+ catch { /* ignore */ }
143
+ };
144
+ const install = () => {
145
+ if (!wrapper && (hub.rewrites.length || hub.gates.length || hub.taps.length)) {
146
+ inner = target.fetch;
147
+ try {
148
+ target.fetch = fetchWrapper;
149
+ wrapper = fetchWrapper;
150
+ }
151
+ catch { /* Readonly runtime. */ }
152
+ }
153
+ if (!proto)
154
+ return;
155
+ if (!openWrapper && (hub.taps.length || hub.xhrGates.length)) {
156
+ const open = originalOpen = proto.open;
157
+ const candidate = function (...args) {
158
+ try {
159
+ let url = String(args[1]);
160
+ if (hub.xhrGates.length) {
161
+ const method = String(args[0]).toUpperCase();
162
+ for (const gate of hub.xhrGates) {
163
+ const next = gate.open?.(this, { method, url });
164
+ if (typeof next === 'string') {
165
+ url = next;
166
+ args = [args[0], next, ...args.slice(2)];
167
+ }
168
+ }
169
+ opened.set(this, { method, url });
170
+ }
171
+ else
172
+ opened.delete(this);
173
+ if (hasMark(url, config.apiMarks)) {
174
+ const taps = matchingTaps(url);
175
+ if (taps.length)
176
+ this.addEventListener('load', function () { onLoad.call(this, taps); }, { once: true });
177
+ }
178
+ }
179
+ catch { /* ignore */ }
180
+ return Reflect.apply(open, this, args);
181
+ };
182
+ try {
183
+ proto.open = candidate;
184
+ openWrapper = candidate;
185
+ }
186
+ catch { /* ignore */ }
187
+ }
188
+ if (!sendWrapper && hub.xhrGates.length) {
189
+ const send = originalSend = proto.send;
190
+ const candidate = function (...args) {
191
+ const request = opened.get(this);
192
+ if (request && (hasMark(request.url, config.apiMarks) || hasMark(request.url, config.xhrMarks))) {
193
+ const sendNative = (...body) => { Reflect.apply(send, this, body.length ? body : args); };
194
+ for (const gate of hub.xhrGates)
195
+ if (gate.send(this, request, args[0], sendNative))
196
+ return;
197
+ }
198
+ return Reflect.apply(send, this, args);
199
+ };
200
+ try {
201
+ proto.send = candidate;
202
+ sendWrapper = candidate;
203
+ }
204
+ catch { /* ignore */ }
205
+ }
206
+ };
207
+ const uninstall = () => {
208
+ // Anyone still holding a wrapper (another script wrapped on top) gets plain pass-through.
209
+ if (proto && openWrapper && !hub.taps.length && !hub.xhrGates.length && proto.open === openWrapper) {
210
+ proto.open = originalOpen;
211
+ openWrapper = undefined;
212
+ }
213
+ if (proto && sendWrapper && !hub.xhrGates.length && proto.send === sendWrapper) {
214
+ proto.send = originalSend;
215
+ sendWrapper = undefined;
216
+ }
217
+ if (!hub.rewrites.length && !hub.gates.length && !hub.taps.length && wrapper) {
218
+ if (target.fetch === wrapper) {
219
+ target.fetch = inner;
220
+ wrapper = null;
221
+ }
222
+ }
223
+ };
224
+ const hub = {
225
+ rewrites: [],
226
+ gates: [],
227
+ taps: [],
228
+ xhrGates: [],
229
+ hooked: () => wrapper !== null && target.fetch === wrapper,
230
+ add(list, item) {
231
+ list.push(item);
232
+ install();
233
+ let removed = false;
234
+ return () => {
235
+ if (removed)
236
+ return;
237
+ removed = true;
238
+ const index = list.indexOf(item);
239
+ if (index >= 0)
240
+ list.splice(index, 1);
241
+ uninstall();
242
+ };
243
+ },
244
+ };
245
+ hubs.set(target, hub);
246
+ return hub;
247
+ }
248
+ /** Whether the shared hook is installed as `target.fetch` (for callers that report `installed`). */
249
+ export function fetchHooked(target) {
250
+ return hubs.get(target)?.hooked() ?? false;
251
+ }
252
+ export function addRewrite(target, rewrite) {
253
+ const hub = hubFor(target);
254
+ return hub.add(hub.rewrites, rewrite);
255
+ }
256
+ export function addGate(target, gate) {
257
+ const hub = hubFor(target);
258
+ return hub.add(hub.gates, gate);
259
+ }
260
+ export function addTap(target, tap) {
261
+ const hub = hubFor(target);
262
+ return hub.add(hub.taps, tap);
263
+ }
264
+ export function addXhrGate(target, gate) {
265
+ const hub = hubFor(target);
266
+ return hub.add(hub.xhrGates, gate);
267
+ }
268
+ //# sourceMappingURL=netHub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"netHub.js","sourceRoot":"","sources":["../../../runtime/src/netHub.ts"],"names":[],"mappings":"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","sourcesContent":["// One shared fetch/XHR hook per window for everything a site runtime does with the site's own\n// requests: URL rewrites, gates (e.g. serializing racing mutations) and passive JSON taps. Each\n// feature registers a stage instead of stacking its own wrapper. A request whose URL contains none\n// of the site's API marks (images, video, WASM, scripts, telemetry) takes one substring check and\n// goes straight to the wrapped fetch, and a tapped response is cloned, read and JSON-parsed once for\n// all taps. Semantics are unchanged: the page gets the very promise (and Response) the inner fetch\n// returned unless a gate answers a request itself, rejections and aborts pass through, and nothing\n// here ever issues a request of its own.\n\n/** `input`/`init` are fetch's arguments; returns the rewritten URL, or null to leave the request alone. */\nexport type Rewrite = (input: unknown, init: RequestInit | undefined) => string | null;\n/** Returns null when the request is not the gate's; otherwise the promise the page gets instead of `send()`. */\nexport type Gate = (input: unknown, init: RequestInit | undefined, send: () => Promise<Response>) => Promise<Response> | null;\nexport type Tap = { match(url: string): boolean; maxBytes: number; onJson(body: unknown): void };\n/** XHR counterpart of `Gate` (many web clients still send their API calls with XMLHttpRequest). */\nexport type XhrGate = {\n /**\n * The page (re)opens this XHR object; called before the native `open`. Returning a string opens\n * that URL instead (a request rewrite, like `Rewrite` for fetch).\n */\n open?(xhr: XMLHttpRequest, request: { method: string; url: string }): string | void;\n /**\n * The page calls `send(body)` on an API (or XHR-mark) request. Returns true when the gate took it\n * over: it then calls `sendNative()` itself (now or later; `sendNative(body)` sends that body\n * instead of the page's) or completes the XHR on its own.\n */\n send(xhr: XMLHttpRequest, request: { method: string; url: string }, body: unknown, sendNative: (...body: [] | [unknown]) => void): boolean;\n};\n\nexport type NetHubConfig = {\n /** URL substrings of the site's API calls; only these reach rewrites, gates and taps. */\n apiMarks: string[];\n /** Extra URL substrings that reach XHR gates only (e.g. a chunked media upload endpoint). */\n xhrMarks: string[];\n};\n\ntype HubWindow = {\n fetch: typeof fetch;\n XMLHttpRequest?: typeof XMLHttpRequest;\n requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;\n};\n\ntype Hub = {\n rewrites: Rewrite[];\n gates: Gate[];\n taps: Tap[];\n xhrGates: XhrGate[];\n hooked(): boolean;\n add<T>(list: T[], item: T): () => void;\n};\n\nconst config: NetHubConfig = { apiMarks: [], xhrMarks: [] };\nconst hubs = new WeakMap<object, Hub>();\n\n/** Set once by the site adapter, before stages register. */\nexport function configureNetHub(next: Partial<NetHubConfig>): void {\n if (next.apiMarks) config.apiMarks = [...next.apiMarks];\n if (next.xhrMarks) config.xhrMarks = [...next.xhrMarks];\n}\n\nconst hasMark = (url: string, marks: string[]) => {\n for (const mark of marks) if (url.includes(mark)) return true;\n return false;\n};\n\n/** Whether `url` is one of the site's API calls (plain substring checks, no URL parsing). */\nexport function isApiUrl(url: string): boolean {\n return hasMark(url, config.apiMarks);\n}\n\n/** The request URL as given (string, URL or Request), without parsing it. */\nexport function rawUrl(input: unknown): string {\n try {\n if (typeof input === 'string') return input;\n if (input instanceof URL) return input.href;\n if (input && typeof (input as Request).url === 'string') return (input as Request).url;\n } catch { /* ignore */ }\n return '';\n}\n\nfunction hubFor(target: HubWindow): Hub {\n const existing = hubs.get(target);\n if (existing) return existing;\n\n let inner: typeof fetch | null = null;\n let wrapper: typeof fetch | null = null;\n const proto: XMLHttpRequest | undefined = target.XMLHttpRequest?.prototype;\n let originalOpen: XMLHttpRequest['open'] | undefined;\n let openWrapper: XMLHttpRequest['open'] | undefined;\n let originalSend: XMLHttpRequest['send'] | undefined;\n let sendWrapper: XMLHttpRequest['send'] | undefined;\n const opened = new WeakMap<XMLHttpRequest, { method: string; url: string }>();\n\n const later = (run: () => void) => {\n // Parsing a large body is the expensive part; keep it out of the page's own response handling.\n const idle = target.requestIdleCallback;\n if (typeof idle === 'function') { try { idle.call(target, run, { timeout: 200 }); return; } catch { /* fall through */ } }\n run();\n };\n const deliver = (taps: Tap[], text: string) => {\n const live = taps.filter(tap => hub.taps.includes(tap) && text.length <= tap.maxBytes);\n if (!live.length) return;\n later(() => {\n let body: unknown;\n try { body = JSON.parse(text); } catch { return; }\n for (const tap of live) {\n if (!hub.taps.includes(tap)) continue;\n try { tap.onJson(body); } catch { /* A consumer failure never reaches the page. */ }\n }\n });\n };\n const matchingTaps = (url: string) => hub.taps.length ? hub.taps.filter(tap => { try { return tap.match(url); } catch { return false; } }) : [];\n\n const fetchWrapper = function (this: unknown, ...args: Parameters<typeof fetch>): Promise<Response> {\n const url = rawUrl(args[0]);\n if (!hasMark(url, config.apiMarks)) return Reflect.apply(inner!, this, args) as Promise<Response>;\n let [input, init] = args;\n for (const rewrite of hub.rewrites) {\n const next = rewrite(input, init);\n if (next != null) input = next;\n }\n const call = input === args[0] ? args : [input, init] as Parameters<typeof fetch>;\n const send = () => Reflect.apply(inner!, this, call) as Promise<Response>;\n let result: Promise<Response> | null = null;\n for (const gate of hub.gates) {\n result = gate(input, init, send);\n if (result) break;\n }\n result ??= send();\n const taps = matchingTaps(rawUrl(input));\n if (taps.length) {\n result.then(response => {\n if (!response.ok) return;\n const length = Number(response.headers.get('content-length') ?? 0);\n if (!taps.some(tap => hub.taps.includes(tap) && length <= tap.maxBytes)) return;\n return response.clone().text().then(text => deliver(taps, text));\n }).catch(() => {});\n }\n return result;\n };\n\n const onLoad = function (this: XMLHttpRequest, taps: Tap[]) {\n try {\n if (this.status < 200 || this.status >= 300) return;\n if (this.responseType === '' || this.responseType === 'text') deliver(taps, this.responseText);\n else if (this.responseType === 'json' && this.response) {\n const body = this.response;\n for (const tap of taps) if (hub.taps.includes(tap)) { try { tap.onJson(body); } catch { /* ignore */ } }\n }\n } catch { /* ignore */ }\n };\n\n const install = () => {\n if (!wrapper && (hub.rewrites.length || hub.gates.length || hub.taps.length)) {\n inner = target.fetch;\n try { target.fetch = fetchWrapper; wrapper = fetchWrapper; } catch { /* Readonly runtime. */ }\n }\n if (!proto) return;\n if (!openWrapper && (hub.taps.length || hub.xhrGates.length)) {\n const open = originalOpen = proto.open;\n const candidate = function (this: XMLHttpRequest, ...args: unknown[]) {\n try {\n let url = String(args[1]);\n if (hub.xhrGates.length) {\n const method = String(args[0]).toUpperCase();\n for (const gate of hub.xhrGates) {\n const next = gate.open?.(this, { method, url });\n if (typeof next === 'string') { url = next; args = [args[0], next, ...args.slice(2)]; }\n }\n opened.set(this, { method, url });\n } else opened.delete(this);\n if (hasMark(url, config.apiMarks)) {\n const taps = matchingTaps(url);\n if (taps.length) this.addEventListener('load', function (this: XMLHttpRequest) { onLoad.call(this, taps); }, { once: true });\n }\n } catch { /* ignore */ }\n return Reflect.apply(open, this, args);\n } as XMLHttpRequest['open'];\n try { proto.open = candidate; openWrapper = candidate; } catch { /* ignore */ }\n }\n if (!sendWrapper && hub.xhrGates.length) {\n const send = originalSend = proto.send;\n const candidate: XMLHttpRequest['send'] = function (this: XMLHttpRequest, ...args) {\n const request = opened.get(this);\n if (request && (hasMark(request.url, config.apiMarks) || hasMark(request.url, config.xhrMarks))) {\n const sendNative = (...body: [] | [unknown]) => { Reflect.apply(send, this, body.length ? body : args); };\n for (const gate of hub.xhrGates) if (gate.send(this, request, args[0], sendNative)) return;\n }\n return Reflect.apply(send, this, args);\n };\n try { proto.send = candidate; sendWrapper = candidate; } catch { /* ignore */ }\n }\n };\n const uninstall = () => {\n // Anyone still holding a wrapper (another script wrapped on top) gets plain pass-through.\n if (proto && openWrapper && !hub.taps.length && !hub.xhrGates.length && proto.open === openWrapper) {\n proto.open = originalOpen!;\n openWrapper = undefined;\n }\n if (proto && sendWrapper && !hub.xhrGates.length && proto.send === sendWrapper) {\n proto.send = originalSend!;\n sendWrapper = undefined;\n }\n if (!hub.rewrites.length && !hub.gates.length && !hub.taps.length && wrapper) {\n if (target.fetch === wrapper) { target.fetch = inner!; wrapper = null; }\n }\n };\n\n const hub: Hub = {\n rewrites: [],\n gates: [],\n taps: [],\n xhrGates: [],\n hooked: () => wrapper !== null && target.fetch === wrapper,\n add(list, item) {\n list.push(item);\n install();\n let removed = false;\n return () => {\n if (removed) return;\n removed = true;\n const index = list.indexOf(item);\n if (index >= 0) list.splice(index, 1);\n uninstall();\n };\n },\n };\n hubs.set(target, hub);\n return hub;\n}\n\n/** Whether the shared hook is installed as `target.fetch` (for callers that report `installed`). */\nexport function fetchHooked(target: HubWindow): boolean {\n return hubs.get(target)?.hooked() ?? false;\n}\n\nexport function addRewrite(target: HubWindow, rewrite: Rewrite): () => void {\n const hub = hubFor(target);\n return hub.add(hub.rewrites, rewrite);\n}\nexport function addGate(target: HubWindow, gate: Gate): () => void {\n const hub = hubFor(target);\n return hub.add(hub.gates, gate);\n}\nexport function addTap(target: HubWindow, tap: Tap): () => void {\n const hub = hubFor(target);\n return hub.add(hub.taps, tap);\n}\nexport function addXhrGate(target: HubWindow, gate: XhrGate): () => void {\n const hub = hubFor(target);\n return hub.add(hub.xhrGates, gate);\n}\n"]}
@@ -0,0 +1,35 @@
1
+ export type PaneNavHandle = {
2
+ set(state: unknown): void;
3
+ setEnabled(enabled: boolean): void;
4
+ dispose(): void;
5
+ };
6
+ export type RailPaint = {
7
+ width: number;
8
+ background: string;
9
+ line: string;
10
+ accent: string;
11
+ rtl: boolean;
12
+ };
13
+ export type RailSite = {
14
+ /** Prefix of the ids/attributes this module owns (`<prefix>-pane-nav`, `data-<prefix>-pane-nav`, ...). */
15
+ prefix: string;
16
+ /** The site's classic rail element. */
17
+ rail: string;
18
+ /** A link inside the rail, used to find it when the classic rail element is absent. */
19
+ anchor: string;
20
+ /** The content column that re-centers once the rail is parked. */
21
+ main: string;
22
+ /** An element whose background is the site accent (for the edge handle), if any. */
23
+ accentSource?: string;
24
+ /** Open while a modal sheet is up: the rail never slides in over it. */
25
+ modalOpen: string;
26
+ /** One of the site's menus (portalled out of the rail) is open. */
27
+ menuOpen: string;
28
+ /** Paint before the page has been read. */
29
+ paint: RailPaint;
30
+ };
31
+ /** Pure: the stylesheet for a pane whose rail is parked. RTL pages keep the rail on the right. */
32
+ export declare function buildPaneNavCss(site: RailSite, paint?: RailPaint): string;
33
+ /** `rgb(r, g, b)` / `rgba(...)` at a new alpha; anything else falls back to a neutral line. */
34
+ export declare function withAlpha(color: string, alpha: number): string;
35
+ export declare function installPaneNav(win: Window, site: RailSite): PaneNavHandle;
@@ -0,0 +1,311 @@
1
+ // Split view: site panes of one account side by side each draw the site's whole navigation rail.
2
+ // Native code tells every site pane whether another pane already shows it (`redundantNav`, the
3
+ // leftmost pane of that account wins). With the feature on, such a pane lifts the rail out of the
4
+ // layout (the content re-centers in the freed width), parks it off-screen, and slides it back in
5
+ // over the page while the pointer rests on the pane's left edge. Which element is the rail is the
6
+ // site adapter's `RailSite`.
7
+ import { watchDom } from "./domHub.js";
8
+ /** Resting on the edge this long slides the rail in; crossing it on the way to the divider does not. */
9
+ const PEEK_DELAY = 140;
10
+ /** The rail stays this long after the pointer leaves it (and while one of its menus is open). */
11
+ const LEAVE_DELAY = 280;
12
+ const names = (site) => ({
13
+ style: `${site.prefix}-pane-nav`,
14
+ edge: `${site.prefix}-rail-edge`,
15
+ /** On <html>: "hidden" (rail parked off-screen) or "peek" (rail slid in over the page). */
16
+ state: `data-${site.prefix}-pane-nav`,
17
+ /** Marks the rail when the site draws it as something other than its classic rail element. */
18
+ rail: `data-${site.prefix}-rail`,
19
+ });
20
+ /** Pure: the stylesheet for a pane whose rail is parked. RTL pages keep the rail on the right. */
21
+ export function buildPaneNavCss(site, paint = site.paint) {
22
+ const { edge, state, rail } = names(site);
23
+ const RAIL = `:is(${site.rail}, [${rail}])`;
24
+ const ease = 'cubic-bezier(.2, .8, .2, 1)';
25
+ const [side, other, sign] = paint.rtl ? ['right', 'left', 1] : ['left', 'right', -1];
26
+ return `
27
+ html[${state}] ${RAIL} {
28
+ position: fixed !important; top: 0 !important; bottom: 0 !important; ${side}: 0 !important; ${other}: auto !important;
29
+ width: ${paint.width}px !important; min-width: 0 !important; max-width: none !important; height: 100vh !important;
30
+ flex: none !important; margin: 0 !important; overflow: visible !important; z-index: 40 !important;
31
+ background-color: ${paint.background} !important;
32
+ transform: translateX(${sign * 100}%); visibility: hidden;
33
+ transition: transform .2s ${ease}, box-shadow .2s ease, visibility 0s linear .2s;
34
+ }
35
+ html[${state}="peek"] ${RAIL} {
36
+ transform: translateX(0); visibility: visible;
37
+ box-shadow: ${-sign}px 0 0 ${paint.line}, ${-sign * 16}px 0 40px rgba(0, 0, 0, .3);
38
+ transition: transform .2s ${ease}, box-shadow .2s ease;
39
+ }
40
+ html[${state}] ${RAIL} ~ ${site.main} { align-items: center !important; }
41
+ #${edge} { position: fixed; ${side}: 0; top: 0; bottom: 0; width: 10px; z-index: 41; }
42
+ #${edge}::after {
43
+ content: ""; position: absolute; ${side}: 0; top: 50%; width: 3px; height: 56px; margin-top: -28px;
44
+ border-radius: ${paint.rtl ? '3px 0 0 3px' : '0 3px 3px 0'}; background: ${paint.accent}; opacity: 0; transform: scaleY(.5);
45
+ transition: opacity .15s ease, transform .15s ease;
46
+ }
47
+ #${edge}:hover::after { opacity: .9; transform: scaleY(1); }
48
+ html[${state}="peek"] #${edge} { pointer-events: none; }
49
+ @media (prefers-reduced-motion: reduce) {
50
+ html[${state}] ${RAIL}, #${edge}::after { transition: none !important; }
51
+ }`.trim();
52
+ }
53
+ const TRANSPARENT = /^(transparent|rgba\([^)]*,\s*0\))$/;
54
+ const isTransparent = (color) => !color || TRANSPARENT.test(color.replace(/\s+/g, ' ').trim());
55
+ /** `rgb(r, g, b)` / `rgba(...)` at a new alpha; anything else falls back to a neutral line. */
56
+ export function withAlpha(color, alpha) {
57
+ const parts = color.match(/\d+(?:\.\d+)?/g);
58
+ return parts && parts.length >= 3 ? `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${alpha})` : `rgba(128, 128, 128, ${alpha})`;
59
+ }
60
+ export function installPaneNav(win, site) {
61
+ const doc = win.document;
62
+ const ids = names(site);
63
+ // Installed at document start, before the parser has created <html>: never cache it.
64
+ const root = () => doc.documentElement;
65
+ let redundant = false;
66
+ let enabled = true;
67
+ let active = false;
68
+ let disposed = false;
69
+ let rail = null;
70
+ let edge = null;
71
+ let watch = null;
72
+ let paint = { ...site.paint };
73
+ let peekTimer = 0;
74
+ let leaveTimer = 0;
75
+ let resizeFrame = 0;
76
+ /** A press inside the rail may have opened one of its menus (portalled elsewhere). */
77
+ let railMenu = false;
78
+ const isRtl = () => root()?.getAttribute('dir') === 'rtl';
79
+ const ensureStyle = () => {
80
+ let style = doc.getElementById(ids.style);
81
+ if (!style) {
82
+ const parent = doc.head || root();
83
+ if (!parent)
84
+ return;
85
+ style = doc.createElement('style');
86
+ style.id = ids.style;
87
+ parent.appendChild(style);
88
+ }
89
+ const css = buildPaneNavCss(site, paint);
90
+ if (style.textContent !== css)
91
+ style.textContent = css;
92
+ };
93
+ /** The site's rail: its classic element, else the sibling of <main> holding the anchor link. */
94
+ const findRail = () => {
95
+ const classic = doc.querySelector(site.rail);
96
+ if (classic)
97
+ return classic;
98
+ if (rail?.isConnected)
99
+ return rail;
100
+ const main = doc.querySelector('main');
101
+ const anchor = doc.querySelector(site.anchor);
102
+ if (!main || !anchor)
103
+ return null;
104
+ for (let el = anchor.parentElement; el && el !== doc.body; el = el.parentElement) {
105
+ const parent = el.parentElement;
106
+ if (!parent?.contains(main) || el.contains(main))
107
+ continue;
108
+ // Only a tall column at the start edge: never a bottom tab bar or a page-wide wrapper.
109
+ const box = el.getBoundingClientRect();
110
+ const tall = box.height >= win.innerHeight * 0.5;
111
+ const atStart = isRtl() ? box.right >= win.innerWidth - 8 : box.left <= 8;
112
+ return tall && atStart && box.width <= win.innerWidth * 0.45 ? el : null;
113
+ }
114
+ return null;
115
+ };
116
+ const railWidth = (el) => {
117
+ const column = el.firstElementChild?.getBoundingClientRect().width ?? 0;
118
+ const nav = el.querySelector('nav')?.getBoundingClientRect().width ?? 0;
119
+ const width = Math.round(Math.max(column, nav));
120
+ return width >= 48 && width <= 400 ? width : paint.width;
121
+ };
122
+ /** Reads the page colors for the slid-in rail (layout reads: only on state changes, never per mutation). */
123
+ const repaint = () => {
124
+ try {
125
+ const body = doc.body ? win.getComputedStyle(doc.body) : null;
126
+ const html = root() ? win.getComputedStyle(root()) : null;
127
+ let background = body?.backgroundColor ?? '';
128
+ if (isTransparent(background))
129
+ background = html?.backgroundColor ?? '';
130
+ if (isTransparent(background))
131
+ background = /dark/.test(html?.colorScheme ?? '') ? 'rgb(0, 0, 0)' : 'rgb(255, 255, 255)';
132
+ const button = site.accentSource ? doc.querySelector(site.accentSource) : null;
133
+ const fill = button ? win.getComputedStyle(button).backgroundColor : '';
134
+ paint = {
135
+ width: rail ? railWidth(rail) : paint.width,
136
+ background,
137
+ line: withAlpha(body?.color ?? '', 0.16),
138
+ accent: isTransparent(fill) ? site.paint.accent : fill,
139
+ rtl: isRtl(),
140
+ };
141
+ }
142
+ catch { /* keep the last paint */ }
143
+ ensureStyle();
144
+ };
145
+ const state = () => root()?.getAttribute(ids.state) ?? null;
146
+ const setState = (value) => { if (state() !== value)
147
+ root()?.setAttribute(ids.state, value); };
148
+ const menuOpen = () => !!doc.querySelector(site.menuOpen);
149
+ const unpeek = () => {
150
+ win.clearTimeout(peekTimer);
151
+ win.clearTimeout(leaveTimer);
152
+ railMenu = false;
153
+ if (active)
154
+ setState('hidden');
155
+ };
156
+ const tryLeave = () => {
157
+ if (state() !== 'peek')
158
+ return;
159
+ // Only the rail's own menus hold it out; sites portal them like every other menu, so "open" is
160
+ // judged only after a press inside the rail and ends when that menu closes.
161
+ const holding = railMenu && menuOpen();
162
+ if (!holding)
163
+ railMenu = false;
164
+ if (rail?.matches(':hover') || holding) {
165
+ leaveTimer = win.setTimeout(tryLeave, LEAVE_DELAY);
166
+ return;
167
+ }
168
+ unpeek();
169
+ };
170
+ const peek = () => {
171
+ win.clearTimeout(peekTimer);
172
+ // Never over one of the site's modal sheets: the rail would sit above them.
173
+ if (!active || !rail || doc.querySelector(site.modalOpen))
174
+ return;
175
+ repaint();
176
+ setState('peek');
177
+ win.clearTimeout(leaveTimer);
178
+ leaveTimer = win.setTimeout(tryLeave, LEAVE_DELAY * 4);
179
+ };
180
+ const onEdgeEnter = () => { win.clearTimeout(peekTimer); peekTimer = win.setTimeout(peek, PEEK_DELAY); };
181
+ const onEdgeLeave = () => { if (state() !== 'peek')
182
+ win.clearTimeout(peekTimer); };
183
+ const onRailEnter = () => win.clearTimeout(leaveTimer);
184
+ const onRailPress = () => { railMenu = true; };
185
+ const onRailLeave = () => { win.clearTimeout(leaveTimer); leaveTimer = win.setTimeout(tryLeave, LEAVE_DELAY); };
186
+ // A nav link navigates or opens a sheet: get out of the way.
187
+ const onRailClick = (event) => {
188
+ if (event.target?.closest?.('a[href]'))
189
+ win.setTimeout(unpeek, 0);
190
+ };
191
+ const onKey = (event) => { if (event.key === 'Escape' && state() === 'peek')
192
+ unpeek(); };
193
+ const onResize = () => {
194
+ if (resizeFrame)
195
+ return;
196
+ resizeFrame = win.requestAnimationFrame(() => {
197
+ resizeFrame = 0;
198
+ if (active && rail) {
199
+ paint = { ...paint, width: railWidth(rail) };
200
+ ensureStyle();
201
+ }
202
+ });
203
+ };
204
+ const bindRail = (next) => {
205
+ if (next === rail)
206
+ return;
207
+ if (rail) {
208
+ rail.removeAttribute(ids.rail);
209
+ rail.removeEventListener('pointerenter', onRailEnter);
210
+ rail.removeEventListener('pointerleave', onRailLeave);
211
+ rail.removeEventListener('pointerdown', onRailPress, true);
212
+ rail.removeEventListener('click', onRailClick, true);
213
+ }
214
+ rail = next;
215
+ if (!rail)
216
+ return;
217
+ if (!rail.matches(site.rail))
218
+ rail.setAttribute(ids.rail, '');
219
+ rail.addEventListener('pointerenter', onRailEnter);
220
+ rail.addEventListener('pointerleave', onRailLeave);
221
+ rail.addEventListener('pointerdown', onRailPress, true);
222
+ rail.addEventListener('click', onRailClick, true);
223
+ paint = { ...paint, width: railWidth(rail), rtl: isRtl() };
224
+ ensureStyle();
225
+ };
226
+ const syncEdge = () => {
227
+ if (!doc.body)
228
+ return;
229
+ if (edge?.isConnected)
230
+ return;
231
+ edge = doc.createElement('div');
232
+ edge.id = ids.edge;
233
+ edge.setAttribute('aria-hidden', 'true');
234
+ edge.addEventListener('pointerenter', onEdgeEnter);
235
+ edge.addEventListener('pointerleave', onEdgeLeave);
236
+ edge.addEventListener('click', peek);
237
+ doc.body.appendChild(edge);
238
+ };
239
+ // Sites re-render their layout on route and breakpoint changes: keep the rail marked and the edge in place.
240
+ const sync = () => {
241
+ if (!active || disposed)
242
+ return;
243
+ // The first run after <html> exists parks the rail (activation may predate the document).
244
+ ensureStyle();
245
+ if (state() !== 'peek')
246
+ setState('hidden');
247
+ bindRail(findRail());
248
+ syncEdge();
249
+ // The page direction can switch with the display language, without a reload.
250
+ if (paint.rtl !== isRtl()) {
251
+ paint = { ...paint, rtl: isRtl() };
252
+ ensureStyle();
253
+ }
254
+ };
255
+ const activate = () => {
256
+ // The rail's width is measured while it is still in the layout, before it is parked.
257
+ if (doc.body)
258
+ bindRail(findRail());
259
+ ensureStyle();
260
+ setState('hidden');
261
+ win.addEventListener('keydown', onKey, true);
262
+ win.addEventListener('resize', onResize);
263
+ watch = watchDom(win, sync);
264
+ watch.schedule();
265
+ };
266
+ const deactivate = () => {
267
+ win.clearTimeout(peekTimer);
268
+ win.clearTimeout(leaveTimer);
269
+ railMenu = false;
270
+ if (resizeFrame)
271
+ win.cancelAnimationFrame(resizeFrame);
272
+ resizeFrame = 0;
273
+ watch?.dispose();
274
+ watch = null;
275
+ win.removeEventListener('keydown', onKey, true);
276
+ win.removeEventListener('resize', onResize);
277
+ root()?.removeAttribute(ids.state);
278
+ bindRail(null);
279
+ edge?.remove();
280
+ edge = null;
281
+ doc.getElementById(ids.style)?.remove();
282
+ };
283
+ const apply = () => {
284
+ const next = enabled && redundant && !disposed;
285
+ if (next === active)
286
+ return;
287
+ active = next;
288
+ if (active)
289
+ activate();
290
+ else
291
+ deactivate();
292
+ };
293
+ return {
294
+ set(value) {
295
+ const next = value?.redundantNav;
296
+ if (typeof next !== 'boolean')
297
+ return;
298
+ redundant = next;
299
+ apply();
300
+ },
301
+ setEnabled(value) {
302
+ enabled = value;
303
+ apply();
304
+ },
305
+ dispose() {
306
+ disposed = true;
307
+ apply();
308
+ },
309
+ };
310
+ }
311
+ //# sourceMappingURL=paneNav.js.map