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.
- package/LICENSE +674 -0
- package/README.md +71 -0
- package/dist/runtime/accountReport.d.ts +20 -0
- package/dist/runtime/accountReport.js +26 -0
- package/dist/runtime/accountReport.js.map +1 -0
- package/dist/runtime/domHub.d.ts +37 -0
- package/dist/runtime/domHub.js +134 -0
- package/dist/runtime/domHub.js.map +1 -0
- package/dist/runtime/linkRouting.d.ts +13 -0
- package/dist/runtime/linkRouting.js +35 -0
- package/dist/runtime/linkRouting.js.map +1 -0
- package/dist/runtime/netHub.d.ts +55 -0
- package/dist/runtime/netHub.js +268 -0
- package/dist/runtime/netHub.js.map +1 -0
- package/dist/runtime/paneNav.d.ts +35 -0
- package/dist/runtime/paneNav.js +311 -0
- package/dist/runtime/paneNav.js.map +1 -0
- package/dist/runtime/pluginApi.d.ts +59 -0
- package/dist/runtime/pluginApi.js +279 -0
- package/dist/runtime/pluginApi.js.map +1 -0
- package/dist/runtime/site.d.ts +32 -0
- package/dist/runtime/site.js +20 -0
- package/dist/runtime/site.js.map +1 -0
- package/dist/runtime/themeTokens.d.ts +64 -0
- package/dist/runtime/themeTokens.js +175 -0
- package/dist/runtime/themeTokens.js.map +1 -0
- package/dist/shell/addressModel.d.ts +34 -0
- package/dist/shell/addressModel.js +66 -0
- package/dist/shell/addressModel.js.map +1 -0
- package/dist/shell/chrome.d.ts +3 -0
- package/dist/shell/chrome.js +6 -0
- package/dist/shell/chrome.js.map +1 -0
- package/dist/shell/linkDrop.d.ts +29 -0
- package/dist/shell/linkDrop.js +57 -0
- package/dist/shell/linkDrop.js.map +1 -0
- package/dist/shell/palette.d.ts +13 -0
- package/dist/shell/palette.js +51 -0
- package/dist/shell/palette.js.map +1 -0
- package/dist/shell/paneModel.d.ts +33 -0
- package/dist/shell/paneModel.js +56 -0
- package/dist/shell/paneModel.js.map +1 -0
- package/dist/shell/profileModel.d.ts +52 -0
- package/dist/shell/profileModel.js +91 -0
- package/dist/shell/profileModel.js.map +1 -0
- package/dist/shell/updateModel.d.ts +35 -0
- package/dist/shell/updateModel.js +32 -0
- package/dist/shell/updateModel.js.map +1 -0
- package/dist/shell/windowControls.d.ts +24 -0
- package/dist/shell/windowControls.js +22 -0
- package/dist/shell/windowControls.js.map +1 -0
- package/package.json +112 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# waybetter
|
|
2
|
+
|
|
3
|
+
The framework behind the wayBetter apps: apps that extend the apps you already use in place, the
|
|
4
|
+
way BetterDiscord extends Discord. A website gets a native window around it; a desktop app built
|
|
5
|
+
on web tech is extended from the inside. Both share this package.
|
|
6
|
+
|
|
7
|
+
- **`waybetter/runtime/*`**: the page runtime. One DOM-mutation/animation-frame hub, one
|
|
8
|
+
fetch/XHR hub, a plugin API core, an OKLab theme color model, split-pane rail hiding, link
|
|
9
|
+
routing and account change reporting.
|
|
10
|
+
- **`waybetter/shell/*`**: pure models for the app chrome around the pages: address bar chips,
|
|
11
|
+
dock/split-pane rules, dragged links, account profiles, the update chip, command palette
|
|
12
|
+
ranking and window controls. The Tauri window API is passed in by the caller, never imported.
|
|
13
|
+
|
|
14
|
+
Plain ESM with `.d.ts` types. Zero dependencies.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install waybetter
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Every module is its own subpath export, so you only pull in what you use:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { configureRuntime, type SiteAdapter } from 'waybetter/runtime/site';
|
|
28
|
+
import { watchDom } from 'waybetter/runtime/domHub';
|
|
29
|
+
import { buildColorModel, deriveColors } from 'waybetter/runtime/themeTokens';
|
|
30
|
+
import { parseAddress } from 'waybetter/shell/addressModel';
|
|
31
|
+
import { fuzzyScore, rankItems } from 'waybetter/shell/palette';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`runtime/site` is the composition root: an app is mostly one adapter object built from these
|
|
35
|
+
pieces. The source comments document each module's contract.
|
|
36
|
+
|
|
37
|
+
## Modules
|
|
38
|
+
|
|
39
|
+
**Runtime**
|
|
40
|
+
|
|
41
|
+
- `accountReport`: reports the signed-in account seen in the page's own UI.
|
|
42
|
+
- `domHub`: one `MutationObserver` and one animation frame shared by every DOM feature.
|
|
43
|
+
- `linkRouting`: routes clicks on the app's own links per the "links to app pages" setting.
|
|
44
|
+
- `netHub`: one shared fetch/XHR hook for URL rewrites, gates and passive JSON taps.
|
|
45
|
+
- `paneNav`: hides a redundant navigation rail when two panes show the same account.
|
|
46
|
+
- `pluginApi`: small helpers shared by user JS plugins.
|
|
47
|
+
- `site`: the page-side composition root for an adapter.
|
|
48
|
+
- `themeTokens`: the OKLab color model themes are built on.
|
|
49
|
+
|
|
50
|
+
**Shell**
|
|
51
|
+
|
|
52
|
+
- `addressModel`: `parseAddress(url, site)` for the address bar's breadcrumb chips.
|
|
53
|
+
- `chrome`: layout constants for the custom titlebar and toolbar.
|
|
54
|
+
- `linkDrop`: a dragged link dropped into the dock opens as a tab or split pane.
|
|
55
|
+
- `palette`: command palette fuzzy scoring, ranking and selection movement.
|
|
56
|
+
- `paneModel`: pane geometry: where a "beside" link opens, which panes keep the rail.
|
|
57
|
+
- `profileModel`: the account profile model.
|
|
58
|
+
- `updateModel`: the titlebar update chip model.
|
|
59
|
+
- `windowControls`: native window calls for the titlebar, resize grips and caption buttons.
|
|
60
|
+
|
|
61
|
+
## Projects using wayBetter
|
|
62
|
+
|
|
63
|
+
| Project | Target | What it is |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| [wayBetter for X](https://waybetter.crt.fyi/twttr/) | x.com (website) | Desktop app around the live X: tabs, split panes, real accounts side by side, themes, plugins, drafts and fixes for X's own bugs. The first wayBetter app. |
|
|
66
|
+
|
|
67
|
+
Built something on wayBetter? Add it to this list with a pull request: name and link, what it extends, one line on what it does.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
GPL-3.0-or-later. See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SeenAccount = {
|
|
2
|
+
handle: string;
|
|
3
|
+
name: string;
|
|
4
|
+
avatar: string | null;
|
|
5
|
+
};
|
|
6
|
+
export type AccountReport = {
|
|
7
|
+
account: SeenAccount;
|
|
8
|
+
} | {
|
|
9
|
+
signedOut: true;
|
|
10
|
+
};
|
|
11
|
+
export type AccountSite = {
|
|
12
|
+
/** The account the site's UI shows, or null when there is none (yet). Pure over the DOM. */
|
|
13
|
+
read(root: ParentNode): SeenAccount | null;
|
|
14
|
+
/** The site shows its signed-out page. */
|
|
15
|
+
signedOut(root: ParentNode): boolean;
|
|
16
|
+
};
|
|
17
|
+
export type Invoke = (command: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
18
|
+
export declare function installAccountReport(win: Window, site: AccountSite, invoke?: Invoke): import("./domHub.ts").DomWatchHandle | {
|
|
19
|
+
dispose(): void;
|
|
20
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Tells the app which account is signed in inside this tab's profile, so the account switcher
|
|
2
|
+
// lists real logins (avatar, name, handle) instead of hand-named slots. Read from the site's own
|
|
3
|
+
// UI, which the site renders anyway; nothing is requested. Only a change is reported (`tab_report`),
|
|
4
|
+
// and native code accepts it only from a webview that is on the site, for that webview's own profile.
|
|
5
|
+
import { watchDom } from "./domHub.js";
|
|
6
|
+
export function installAccountReport(win, site, invoke) {
|
|
7
|
+
if (!invoke)
|
|
8
|
+
return { dispose() { } };
|
|
9
|
+
let last = '';
|
|
10
|
+
const send = (report) => {
|
|
11
|
+
const key = JSON.stringify(report);
|
|
12
|
+
if (key === last)
|
|
13
|
+
return;
|
|
14
|
+
last = key;
|
|
15
|
+
invoke('tab_report', { report }).catch(() => { last = ''; });
|
|
16
|
+
};
|
|
17
|
+
const check = () => {
|
|
18
|
+
const account = site.read(win.document);
|
|
19
|
+
if (account)
|
|
20
|
+
send({ account });
|
|
21
|
+
else if (site.signedOut(win.document))
|
|
22
|
+
send({ signedOut: true });
|
|
23
|
+
};
|
|
24
|
+
return watchDom(win, check);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=accountReport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accountReport.js","sourceRoot":"","sources":["../../../runtime/src/accountReport.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,iGAAiG;AACjG,qGAAqG;AACrG,sGAAsG;AAEtG,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAcvC,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,IAAiB,EAAE,MAAe;IAClF,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,KAAI,CAAC,EAAE,CAAC;IACrC,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,MAAM,IAAI,GAAG,CAAC,MAAqB,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO;QACzB,IAAI,GAAG,GAAG,CAAC;QACX,MAAM,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,OAAO;YAAE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;aAC1B,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC;IACF,OAAO,QAAQ,CAAC,GAAgD,EAAE,KAAK,CAAC,CAAC;AAC3E,CAAC","sourcesContent":["// Tells the app which account is signed in inside this tab's profile, so the account switcher\n// lists real logins (avatar, name, handle) instead of hand-named slots. Read from the site's own\n// UI, which the site renders anyway; nothing is requested. Only a change is reported (`tab_report`),\n// and native code accepts it only from a webview that is on the site, for that webview's own profile.\n\nimport { watchDom } from './domHub.ts';\n\nexport type SeenAccount = { handle: string; name: string; avatar: string | null };\nexport type AccountReport = { account: SeenAccount } | { signedOut: true };\n\nexport type AccountSite = {\n /** The account the site's UI shows, or null when there is none (yet). Pure over the DOM. */\n read(root: ParentNode): SeenAccount | null;\n /** The site shows its signed-out page. */\n signedOut(root: ParentNode): boolean;\n};\n\nexport type Invoke = (command: string, args: Record<string, unknown>) => Promise<unknown>;\n\nexport function installAccountReport(win: Window, site: AccountSite, invoke?: Invoke) {\n if (!invoke) return { dispose() {} };\n let last = '';\n const send = (report: AccountReport) => {\n const key = JSON.stringify(report);\n if (key === last) return;\n last = key;\n invoke('tab_report', { report }).catch(() => { last = ''; });\n };\n const check = () => {\n const account = site.read(win.document);\n if (account) send({ account });\n else if (site.signedOut(win.document)) send({ signedOut: true });\n };\n return watchDom(win as unknown as Parameters<typeof watchDom>[0], check);\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type DomWatch = {
|
|
2
|
+
/** Also run on text changes (characterData). */
|
|
3
|
+
characterData?: boolean;
|
|
4
|
+
/** Also run when one of these attributes changes. */
|
|
5
|
+
attributes?: string[];
|
|
6
|
+
/** Skip on the site's heavy pages (`DomHubConfig.heavyPath`). */
|
|
7
|
+
offOnHeavy?: boolean;
|
|
8
|
+
/** 'read' features (layout reads) run before 'write' features (the default) in a frame. */
|
|
9
|
+
phase?: 'read' | 'write';
|
|
10
|
+
};
|
|
11
|
+
export type DomWatchHandle = {
|
|
12
|
+
schedule(): void;
|
|
13
|
+
dispose(): void;
|
|
14
|
+
};
|
|
15
|
+
export type DomHubConfig = {
|
|
16
|
+
/** Console prefix for feature failures, e.g. "wayBetter for X". */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Pathnames where `offOnHeavy` features are skipped; null = none. */
|
|
19
|
+
heavyPath: RegExp | null;
|
|
20
|
+
};
|
|
21
|
+
export type DomWindow = {
|
|
22
|
+
document: Document;
|
|
23
|
+
location: {
|
|
24
|
+
pathname: string;
|
|
25
|
+
};
|
|
26
|
+
MutationObserver: typeof MutationObserver;
|
|
27
|
+
requestAnimationFrame(callback: FrameRequestCallback): number;
|
|
28
|
+
};
|
|
29
|
+
/** Set once by the site adapter, before features register. */
|
|
30
|
+
export declare function configureDomHub(next: Partial<DomHubConfig>): void;
|
|
31
|
+
export declare function isHeavyPath(pathname: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Runs `run` in the next animation frame after body mutations of the watched kinds (child list
|
|
34
|
+
* always), or whenever `schedule()` is called (scroll, popstate, input, ...). Several calls in one
|
|
35
|
+
* frame run it once.
|
|
36
|
+
*/
|
|
37
|
+
export declare function watchDom(win: DomWindow, run: () => void, opts?: DomWatch): DomWatchHandle;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// One MutationObserver and one animation frame per page for every DOM feature of a site runtime.
|
|
2
|
+
// Without it, each feature (nav items, tweaks, buttons, fixes, viewers) observes the whole <body>
|
|
3
|
+
// subtree and schedules its own frame, so every mutation the site makes costs one callback and one
|
|
4
|
+
// frame of scans per feature. Here a mutation marks the features interested in its kind (child list,
|
|
5
|
+
// text, a named attribute) and they run together in the next frame: layout readers first, then DOM
|
|
6
|
+
// writers, so a reader never forces a synchronous layout behind another feature's write. Features
|
|
7
|
+
// marked `offOnHeavy` do not run on the site's heavy pages (see `configureDomHub`), e.g. a WASM chat.
|
|
8
|
+
const config = { label: 'wayBetter', heavyPath: null };
|
|
9
|
+
const hubs = new WeakMap();
|
|
10
|
+
/** Set once by the site adapter, before features register. */
|
|
11
|
+
export function configureDomHub(next) {
|
|
12
|
+
Object.assign(config, next);
|
|
13
|
+
}
|
|
14
|
+
export function isHeavyPath(pathname) {
|
|
15
|
+
return !!config.heavyPath && config.heavyPath.test(pathname);
|
|
16
|
+
}
|
|
17
|
+
function hubFor(win) {
|
|
18
|
+
const existing = hubs.get(win);
|
|
19
|
+
if (existing)
|
|
20
|
+
return existing;
|
|
21
|
+
const doc = win.document;
|
|
22
|
+
let frameRequested = false;
|
|
23
|
+
let observer = null;
|
|
24
|
+
let observing = '';
|
|
25
|
+
const frame = () => {
|
|
26
|
+
frameRequested = false;
|
|
27
|
+
const heavy = isHeavyPath(win.location.pathname ?? '');
|
|
28
|
+
for (const phase of ['read', 'write']) {
|
|
29
|
+
for (const sub of hub.subs.slice()) {
|
|
30
|
+
if (!sub.pending || (sub.opts.phase ?? 'write') !== phase)
|
|
31
|
+
continue;
|
|
32
|
+
sub.pending = false;
|
|
33
|
+
if (heavy && sub.opts.offOnHeavy)
|
|
34
|
+
continue;
|
|
35
|
+
try {
|
|
36
|
+
sub.run();
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error(`[${config.label}] DOM feature failed`, error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const onRecords = (records) => {
|
|
45
|
+
let childList = false;
|
|
46
|
+
let characterData = false;
|
|
47
|
+
let attributes = null;
|
|
48
|
+
const known = Array.isArray(records) && records.length > 0;
|
|
49
|
+
if (known) {
|
|
50
|
+
for (const record of records) {
|
|
51
|
+
if (record.type === 'childList')
|
|
52
|
+
childList = true;
|
|
53
|
+
else if (record.type === 'characterData')
|
|
54
|
+
characterData = true;
|
|
55
|
+
else if (record.type === 'attributes' && record.attributeName)
|
|
56
|
+
(attributes ??= new Set()).add(record.attributeName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const sub of hub.subs) {
|
|
60
|
+
if (!known || childList || (characterData && sub.opts.characterData)
|
|
61
|
+
|| (attributes && sub.opts.attributes?.some(name => attributes.has(name))))
|
|
62
|
+
hub.mark(sub);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const start = () => {
|
|
66
|
+
if (!doc.body || !hub.subs.length)
|
|
67
|
+
return;
|
|
68
|
+
const characterData = hub.subs.some(sub => sub.opts.characterData);
|
|
69
|
+
const filter = [...new Set(hub.subs.flatMap(sub => sub.opts.attributes ?? []))].sort();
|
|
70
|
+
const key = `${characterData}|${filter.join(',')}`;
|
|
71
|
+
if (observer && observing === key)
|
|
72
|
+
return;
|
|
73
|
+
observer ??= new win.MutationObserver(onRecords);
|
|
74
|
+
observing = key;
|
|
75
|
+
// Re-observing the same node replaces its options.
|
|
76
|
+
observer.observe(doc.body, {
|
|
77
|
+
childList: true, subtree: true,
|
|
78
|
+
...(characterData ? { characterData: true } : {}),
|
|
79
|
+
...(filter.length ? { attributes: true, attributeFilter: filter } : {}),
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const hub = {
|
|
83
|
+
subs: [],
|
|
84
|
+
mark(sub) {
|
|
85
|
+
sub.pending = true;
|
|
86
|
+
if (!frameRequested) {
|
|
87
|
+
frameRequested = true;
|
|
88
|
+
win.requestAnimationFrame(frame);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
refresh() {
|
|
92
|
+
if (!hub.subs.length) {
|
|
93
|
+
observer?.disconnect();
|
|
94
|
+
observer = null;
|
|
95
|
+
observing = '';
|
|
96
|
+
doc.removeEventListener('DOMContentLoaded', start);
|
|
97
|
+
hubs.delete(win);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (doc.body)
|
|
101
|
+
start();
|
|
102
|
+
else
|
|
103
|
+
doc.addEventListener('DOMContentLoaded', start, { once: true });
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
hubs.set(win, hub);
|
|
107
|
+
return hub;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Runs `run` in the next animation frame after body mutations of the watched kinds (child list
|
|
111
|
+
* always), or whenever `schedule()` is called (scroll, popstate, input, ...). Several calls in one
|
|
112
|
+
* frame run it once.
|
|
113
|
+
*/
|
|
114
|
+
export function watchDom(win, run, opts = {}) {
|
|
115
|
+
const hub = hubFor(win);
|
|
116
|
+
const sub = { run, opts, pending: false };
|
|
117
|
+
hub.subs.push(sub);
|
|
118
|
+
hub.refresh();
|
|
119
|
+
let active = true;
|
|
120
|
+
return {
|
|
121
|
+
schedule: () => { if (active)
|
|
122
|
+
hub.mark(sub); },
|
|
123
|
+
dispose() {
|
|
124
|
+
if (!active)
|
|
125
|
+
return;
|
|
126
|
+
active = false;
|
|
127
|
+
const index = hub.subs.indexOf(sub);
|
|
128
|
+
if (index >= 0)
|
|
129
|
+
hub.subs.splice(index, 1);
|
|
130
|
+
hub.refresh();
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=domHub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domHub.js","sourceRoot":"","sources":["../../../runtime/src/domHub.ts"],"names":[],"mappings":"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","sourcesContent":["// One MutationObserver and one animation frame per page for every DOM feature of a site runtime.\n// Without it, each feature (nav items, tweaks, buttons, fixes, viewers) observes the whole <body>\n// subtree and schedules its own frame, so every mutation the site makes costs one callback and one\n// frame of scans per feature. Here a mutation marks the features interested in its kind (child list,\n// text, a named attribute) and they run together in the next frame: layout readers first, then DOM\n// writers, so a reader never forces a synchronous layout behind another feature's write. Features\n// marked `offOnHeavy` do not run on the site's heavy pages (see `configureDomHub`), e.g. a WASM chat.\n\nexport type DomWatch = {\n /** Also run on text changes (characterData). */\n characterData?: boolean;\n /** Also run when one of these attributes changes. */\n attributes?: string[];\n /** Skip on the site's heavy pages (`DomHubConfig.heavyPath`). */\n offOnHeavy?: boolean;\n /** 'read' features (layout reads) run before 'write' features (the default) in a frame. */\n phase?: 'read' | 'write';\n};\nexport type DomWatchHandle = { schedule(): void; dispose(): void };\n\nexport type DomHubConfig = {\n /** Console prefix for feature failures, e.g. \"wayBetter for X\". */\n label: string;\n /** Pathnames where `offOnHeavy` features are skipped; null = none. */\n heavyPath: RegExp | null;\n};\n\nexport type DomWindow = {\n document: Document;\n location: { pathname: string };\n MutationObserver: typeof MutationObserver;\n requestAnimationFrame(callback: FrameRequestCallback): number;\n};\ntype Sub = { run: () => void; opts: DomWatch; pending: boolean };\ntype Hub = { subs: Sub[]; mark(sub: Sub): void; refresh(): void };\n\nconst config: DomHubConfig = { label: 'wayBetter', heavyPath: null };\nconst hubs = new WeakMap<object, Hub>();\n\n/** Set once by the site adapter, before features register. */\nexport function configureDomHub(next: Partial<DomHubConfig>): void {\n Object.assign(config, next);\n}\n\nexport function isHeavyPath(pathname: string): boolean {\n return !!config.heavyPath && config.heavyPath.test(pathname);\n}\n\nfunction hubFor(win: DomWindow): Hub {\n const existing = hubs.get(win);\n if (existing) return existing;\n const doc = win.document;\n let frameRequested = false;\n let observer: MutationObserver | null = null;\n let observing = '';\n\n const frame = () => {\n frameRequested = false;\n const heavy = isHeavyPath(win.location.pathname ?? '');\n for (const phase of ['read', 'write'] as const) {\n for (const sub of hub.subs.slice()) {\n if (!sub.pending || (sub.opts.phase ?? 'write') !== phase) continue;\n sub.pending = false;\n if (heavy && sub.opts.offOnHeavy) continue;\n try { sub.run(); } catch (error) { console.error(`[${config.label}] DOM feature failed`, error); }\n }\n }\n };\n const onRecords = (records: MutationRecord[]) => {\n let childList = false;\n let characterData = false;\n let attributes: Set<string> | null = null;\n const known = Array.isArray(records) && records.length > 0;\n if (known) {\n for (const record of records) {\n if (record.type === 'childList') childList = true;\n else if (record.type === 'characterData') characterData = true;\n else if (record.type === 'attributes' && record.attributeName) (attributes ??= new Set()).add(record.attributeName);\n }\n }\n for (const sub of hub.subs) {\n if (!known || childList || (characterData && sub.opts.characterData)\n || (attributes && sub.opts.attributes?.some(name => attributes!.has(name)))) hub.mark(sub);\n }\n };\n const start = () => {\n if (!doc.body || !hub.subs.length) return;\n const characterData = hub.subs.some(sub => sub.opts.characterData);\n const filter = [...new Set(hub.subs.flatMap(sub => sub.opts.attributes ?? []))].sort();\n const key = `${characterData}|${filter.join(',')}`;\n if (observer && observing === key) return;\n observer ??= new win.MutationObserver(onRecords);\n observing = key;\n // Re-observing the same node replaces its options.\n observer.observe(doc.body, {\n childList: true, subtree: true,\n ...(characterData ? { characterData: true } : {}),\n ...(filter.length ? { attributes: true, attributeFilter: filter } : {}),\n });\n };\n\n const hub: Hub = {\n subs: [],\n mark(sub) {\n sub.pending = true;\n if (!frameRequested) { frameRequested = true; win.requestAnimationFrame(frame); }\n },\n refresh() {\n if (!hub.subs.length) {\n observer?.disconnect();\n observer = null;\n observing = '';\n doc.removeEventListener('DOMContentLoaded', start);\n hubs.delete(win);\n return;\n }\n if (doc.body) start();\n else doc.addEventListener('DOMContentLoaded', start, { once: true });\n },\n };\n hubs.set(win, hub);\n return hub;\n}\n\n/**\n * Runs `run` in the next animation frame after body mutations of the watched kinds (child list\n * always), or whenever `schedule()` is called (scroll, popstate, input, ...). Several calls in one\n * frame run it once.\n */\nexport function watchDom(win: DomWindow, run: () => void, opts: DomWatch = {}): DomWatchHandle {\n const hub = hubFor(win);\n const sub: Sub = { run, opts, pending: false };\n hub.subs.push(sub);\n hub.refresh();\n let active = true;\n return {\n schedule: () => { if (active) hub.mark(sub); },\n dispose() {\n if (!active) return;\n active = false;\n const index = hub.subs.indexOf(sub);\n if (index >= 0) hub.subs.splice(index, 1);\n hub.refresh();\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type LinkMode = 'same' | 'pane' | 'tab' | 'browser';
|
|
2
|
+
export type LinkRoutingHandle = {
|
|
3
|
+
configure(value: unknown): void;
|
|
4
|
+
dispose(): void;
|
|
5
|
+
};
|
|
6
|
+
export type LinkSite = {
|
|
7
|
+
/** The site page a click on `target` would open in place, when it is one to divert; else null. */
|
|
8
|
+
clickedUrl(target: Element | null, location: Location): string | null;
|
|
9
|
+
/** The key of the mode in the native `configure` payload (`{ [configKey]: mode }`). */
|
|
10
|
+
configKey: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const LINK_MODES: readonly LinkMode[];
|
|
13
|
+
export declare function installLinkRouting(win: Window, site: LinkSite): LinkRoutingHandle;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// "Links to site pages" setting: when it is not "same", a plain click on one of the site's own pages
|
|
2
|
+
// (a post, a profile, a channel, ...) opens that page beside the pane, in a new tab or in the system
|
|
3
|
+
// browser instead of replacing what you are reading. The click becomes an ordinary new-window
|
|
4
|
+
// request; native code (`new_window_mode`) decides where it opens. Which clicks count is the site
|
|
5
|
+
// adapter's `clickedUrl`; everything else (navigation, tabs, menus, buttons) is never touched.
|
|
6
|
+
export const LINK_MODES = ['same', 'pane', 'tab', 'browser'];
|
|
7
|
+
export function installLinkRouting(win, site) {
|
|
8
|
+
let mode = 'same';
|
|
9
|
+
const onClick = (event) => {
|
|
10
|
+
if (mode === 'same' || event.button !== 0 || event.defaultPrevented)
|
|
11
|
+
return;
|
|
12
|
+
// Modified clicks keep the site's own meaning (Ctrl/Cmd+click already opens a new tab).
|
|
13
|
+
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)
|
|
14
|
+
return;
|
|
15
|
+
if (win.getSelection?.()?.toString().trim())
|
|
16
|
+
return;
|
|
17
|
+
const url = site.clickedUrl(event.target instanceof Element ? event.target : null, win.location);
|
|
18
|
+
if (!url)
|
|
19
|
+
return;
|
|
20
|
+
// Before the site's own handlers (frameworks listen below window): the in-place navigation never starts.
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
event.stopPropagation();
|
|
23
|
+
win.open(url, '_blank', 'noopener');
|
|
24
|
+
};
|
|
25
|
+
win.addEventListener('click', onClick, true);
|
|
26
|
+
return {
|
|
27
|
+
configure(value) {
|
|
28
|
+
const next = value?.[site.configKey];
|
|
29
|
+
if (typeof next === 'string' && LINK_MODES.includes(next))
|
|
30
|
+
mode = next;
|
|
31
|
+
},
|
|
32
|
+
dispose() { win.removeEventListener('click', onClick, true); },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=linkRouting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkRouting.js","sourceRoot":"","sources":["../../../runtime/src/linkRouting.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,qGAAqG;AACrG,8FAA8F;AAC9F,kGAAkG;AAClG,+FAA+F;AAY/F,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;AAElF,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,IAAc;IAC5D,IAAI,IAAI,GAAa,MAAM,CAAC;IAC5B,MAAM,OAAO,GAAG,CAAC,KAAiB,EAAE,EAAE;QACpC,IAAI,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,gBAAgB;YAAE,OAAO;QAC5E,wFAAwF;QACxF,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO;QAC7E,IAAI,GAAG,CAAC,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE;YAAE,OAAO;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjG,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,yGAAyG;QACzG,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,KAAK,CAAC,eAAe,EAAE,CAAC;QACxB,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IACtC,CAAC,CAAC;IACF,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7C,OAAO;QACL,SAAS,CAAC,KAAK;YACb,MAAM,IAAI,GAAI,KAAwC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACzE,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAK,UAAgC,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,IAAI,GAAG,IAAgB,CAAC;QAC5G,CAAC;QACD,OAAO,KAAK,GAAG,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;KAC/D,CAAC;AACJ,CAAC","sourcesContent":["// \"Links to site pages\" setting: when it is not \"same\", a plain click on one of the site's own pages\n// (a post, a profile, a channel, ...) opens that page beside the pane, in a new tab or in the system\n// browser instead of replacing what you are reading. The click becomes an ordinary new-window\n// request; native code (`new_window_mode`) decides where it opens. Which clicks count is the site\n// adapter's `clickedUrl`; everything else (navigation, tabs, menus, buttons) is never touched.\n\nexport type LinkMode = 'same' | 'pane' | 'tab' | 'browser';\nexport type LinkRoutingHandle = { configure(value: unknown): void; dispose(): void };\n\nexport type LinkSite = {\n /** The site page a click on `target` would open in place, when it is one to divert; else null. */\n clickedUrl(target: Element | null, location: Location): string | null;\n /** The key of the mode in the native `configure` payload (`{ [configKey]: mode }`). */\n configKey: string;\n};\n\nexport const LINK_MODES: readonly LinkMode[] = ['same', 'pane', 'tab', 'browser'];\n\nexport function installLinkRouting(win: Window, site: LinkSite): LinkRoutingHandle {\n let mode: LinkMode = 'same';\n const onClick = (event: MouseEvent) => {\n if (mode === 'same' || event.button !== 0 || event.defaultPrevented) return;\n // Modified clicks keep the site's own meaning (Ctrl/Cmd+click already opens a new tab).\n if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;\n if (win.getSelection?.()?.toString().trim()) return;\n const url = site.clickedUrl(event.target instanceof Element ? event.target : null, win.location);\n if (!url) return;\n // Before the site's own handlers (frameworks listen below window): the in-place navigation never starts.\n event.preventDefault();\n event.stopPropagation();\n win.open(url, '_blank', 'noopener');\n };\n win.addEventListener('click', onClick, true);\n return {\n configure(value) {\n const next = (value as Record<string, unknown> | null)?.[site.configKey];\n if (typeof next === 'string' && (LINK_MODES as readonly string[]).includes(next)) mode = next as LinkMode;\n },\n dispose() { win.removeEventListener('click', onClick, true); },\n };\n}\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** `input`/`init` are fetch's arguments; returns the rewritten URL, or null to leave the request alone. */
|
|
2
|
+
export type Rewrite = (input: unknown, init: RequestInit | undefined) => string | null;
|
|
3
|
+
/** Returns null when the request is not the gate's; otherwise the promise the page gets instead of `send()`. */
|
|
4
|
+
export type Gate = (input: unknown, init: RequestInit | undefined, send: () => Promise<Response>) => Promise<Response> | null;
|
|
5
|
+
export type Tap = {
|
|
6
|
+
match(url: string): boolean;
|
|
7
|
+
maxBytes: number;
|
|
8
|
+
onJson(body: unknown): void;
|
|
9
|
+
};
|
|
10
|
+
/** XHR counterpart of `Gate` (many web clients still send their API calls with XMLHttpRequest). */
|
|
11
|
+
export type XhrGate = {
|
|
12
|
+
/**
|
|
13
|
+
* The page (re)opens this XHR object; called before the native `open`. Returning a string opens
|
|
14
|
+
* that URL instead (a request rewrite, like `Rewrite` for fetch).
|
|
15
|
+
*/
|
|
16
|
+
open?(xhr: XMLHttpRequest, request: {
|
|
17
|
+
method: string;
|
|
18
|
+
url: string;
|
|
19
|
+
}): string | void;
|
|
20
|
+
/**
|
|
21
|
+
* The page calls `send(body)` on an API (or XHR-mark) request. Returns true when the gate took it
|
|
22
|
+
* over: it then calls `sendNative()` itself (now or later; `sendNative(body)` sends that body
|
|
23
|
+
* instead of the page's) or completes the XHR on its own.
|
|
24
|
+
*/
|
|
25
|
+
send(xhr: XMLHttpRequest, request: {
|
|
26
|
+
method: string;
|
|
27
|
+
url: string;
|
|
28
|
+
}, body: unknown, sendNative: (...body: [] | [unknown]) => void): boolean;
|
|
29
|
+
};
|
|
30
|
+
export type NetHubConfig = {
|
|
31
|
+
/** URL substrings of the site's API calls; only these reach rewrites, gates and taps. */
|
|
32
|
+
apiMarks: string[];
|
|
33
|
+
/** Extra URL substrings that reach XHR gates only (e.g. a chunked media upload endpoint). */
|
|
34
|
+
xhrMarks: string[];
|
|
35
|
+
};
|
|
36
|
+
type HubWindow = {
|
|
37
|
+
fetch: typeof fetch;
|
|
38
|
+
XMLHttpRequest?: typeof XMLHttpRequest;
|
|
39
|
+
requestIdleCallback?: (callback: () => void, options?: {
|
|
40
|
+
timeout: number;
|
|
41
|
+
}) => number;
|
|
42
|
+
};
|
|
43
|
+
/** Set once by the site adapter, before stages register. */
|
|
44
|
+
export declare function configureNetHub(next: Partial<NetHubConfig>): void;
|
|
45
|
+
/** Whether `url` is one of the site's API calls (plain substring checks, no URL parsing). */
|
|
46
|
+
export declare function isApiUrl(url: string): boolean;
|
|
47
|
+
/** The request URL as given (string, URL or Request), without parsing it. */
|
|
48
|
+
export declare function rawUrl(input: unknown): string;
|
|
49
|
+
/** Whether the shared hook is installed as `target.fetch` (for callers that report `installed`). */
|
|
50
|
+
export declare function fetchHooked(target: HubWindow): boolean;
|
|
51
|
+
export declare function addRewrite(target: HubWindow, rewrite: Rewrite): () => void;
|
|
52
|
+
export declare function addGate(target: HubWindow, gate: Gate): () => void;
|
|
53
|
+
export declare function addTap(target: HubWindow, tap: Tap): () => void;
|
|
54
|
+
export declare function addXhrGate(target: HubWindow, gate: XhrGate): () => void;
|
|
55
|
+
export {};
|