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 @@
1
+ {"version":3,"file":"paneModel.js","sourceRoot":"","sources":["../../../shell/src/paneModel.ts"],"names":[],"mappings":"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","sourcesContent":["// Pane geometry rules for the dock: where a link opened \"beside\" a pane goes, and which site panes\n// repeat a navigation rail already on screen. Two site panes of the same account side by side (or\n// stacked) both draw the site's rail; only the leftmost (then topmost) one needs it. Panes of\n// different accounts keep theirs: the rail's account button is the only in-page sign of which\n// account a pane is.\n\nexport type PaneBox = { id: string; url: string; profile: string; x: number; y: number; visible: boolean };\n\n/** `besideOf`: the source group ids whose \"beside\" links this group's tabs were opened for. */\nexport type GroupBox = { id: string; left: number; top: number; right: number; bottom: number; besideOf?: readonly string[] };\n\n/**\n * Where a link opened \"beside\" a pane goes: the side pane this source opened before (so repeated\n * links reuse one), when it is still to its right and shares some of its height; else null to split\n * a new one. A group the user opened themselves (another feed, another account) is never taken over.\n */\nexport function besideGroup(source: GroupBox, groups: readonly GroupBox[]): string | null {\n let best: GroupBox | null = null;\n for (const group of groups) {\n if (group.id === source.id || !group.besideOf?.includes(source.id)) continue;\n const overlap = Math.min(group.bottom, source.bottom) - Math.max(group.top, source.top);\n if (group.left < source.right - 4 || overlap <= 0) continue;\n if (!best || group.left < best.left || (group.left === best.left && group.top < best.top)) best = group;\n }\n return best?.id ?? null;\n}\n\n/** Ids of visible site panes whose navigation duplicates the leftmost site pane of the same account. */\nexport function redundantNavPanes(panes: readonly PaneBox[], isSiteUrl: (url: string) => boolean): Set<string> {\n const lead = new Map<string, PaneBox>();\n const redundant = new Set<string>();\n const ordered = panes\n .filter(pane => pane.visible && isSiteUrl(pane.url))\n .sort((a, b) => a.x - b.x || a.y - b.y || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));\n for (const pane of ordered) {\n if (lead.has(pane.profile)) redundant.add(pane.id);\n else lead.set(pane.profile, pane);\n }\n return redundant;\n}\n\n/**\n * The `navRedundant` flag per tab for `tabs_sync`. Only visible panes are judged (a shell overlay\n * hides them all and so changes nothing); a hidden tab keeps its last value (`memory`) so it shows\n * up again without a flicker. Tabs that are gone are forgotten.\n */\nexport function withNavRedundant<T extends PaneBox>(boxes: readonly T[], isSiteUrl: (url: string) => boolean, memory: Map<string, boolean>): Array<T & { navRedundant: boolean }> {\n const redundant = redundantNavPanes(boxes, isSiteUrl);\n const items = boxes.map(box => {\n if (box.visible) memory.set(box.id, redundant.has(box.id));\n return { ...box, navRedundant: memory.get(box.id) ?? false };\n });\n for (const id of memory.keys()) if (!boxes.some(box => box.id === id)) memory.delete(id);\n return items;\n}\n"]}
@@ -0,0 +1,52 @@
1
+ /** The login inside a profile, as native code last saw it (read-only here; see profiles.rs). */
2
+ export type Account = {
3
+ handle: string;
4
+ name: string;
5
+ avatar?: string;
6
+ signedIn: boolean;
7
+ };
8
+ export type Profile = {
9
+ id: string;
10
+ name: string;
11
+ color: string;
12
+ account?: Account;
13
+ };
14
+ export type ProfileSettings = {
15
+ profiles: Profile[];
16
+ active: string;
17
+ };
18
+ /** The site's account rules, as in the native `AccountRules`. */
19
+ export type AccountRules = {
20
+ handle: RegExp;
21
+ /** Avatar URLs must start with one of these. */
22
+ avatarPrefixes: readonly string[];
23
+ /** Shown before the handle in the switcher, e.g. '@'. */
24
+ handlePrefix: string;
25
+ };
26
+ export declare const DEFAULT_PROFILE = "default";
27
+ export declare const PROFILE_COLORS: string[];
28
+ export declare const defaultProfiles: () => ProfileSettings;
29
+ export declare const validProfileId: (id: unknown) => id is string;
30
+ export declare const validColor: (color: unknown) => color is string;
31
+ export declare function cleanName(name: string): string | null;
32
+ /** Anything from storage or IPC → a usable settings object (always has the default profile). */
33
+ export declare function normalizeProfiles(value: unknown, rules: AccountRules): ProfileSettings;
34
+ export declare function normalizeAccount(value: unknown, rules: AccountRules): Account | undefined;
35
+ /** What the switcher shows for a profile: the login once seen, the profile name before. */
36
+ export declare function accountLabel(profile: Profile, rules: Pick<AccountRules, 'handlePrefix'>): {
37
+ title: string;
38
+ detail: string;
39
+ state: 'signed-in' | 'signed-out' | 'new';
40
+ };
41
+ export declare function newProfileId(random?: () => number): string;
42
+ export declare function nextColor(settings: ProfileSettings): string;
43
+ /** Adds a profile and makes it active. Unlimited count (Rust caps absurd sizes). Its name follows the
44
+ * account once someone signs in there; until then it is a placeholder. */
45
+ export declare function addProfile(settings: ProfileSettings, name?: string, id?: string): ProfileSettings;
46
+ export declare function renameProfile(settings: ProfileSettings, id: string, name: string): ProfileSettings;
47
+ export declare function recolorProfile(settings: ProfileSettings, id: string, color: string): ProfileSettings;
48
+ /** The default profile cannot be removed; removing the active one falls back to default. */
49
+ export declare function removeProfile(settings: ProfileSettings, id: string): ProfileSettings;
50
+ export declare const setActiveProfile: (settings: ProfileSettings, id: string) => ProfileSettings;
51
+ /** The profile a tab really runs in: unknown or missing ids (old layouts) use the default. */
52
+ export declare function tabProfile(settings: ProfileSettings, id: string | undefined): Profile;
@@ -0,0 +1,91 @@
1
+ // Pure model for account profiles (no Tauri imports, so it is unit-tested under node).
2
+ // Mirrors `ProfileSettings` in waybetter-host `profiles.rs`; Rust validates again on save.
3
+ export const DEFAULT_PROFILE = 'default';
4
+ export const PROFILE_COLORS = ['#1d9bf0', '#f91880', '#00ba7c', '#ffd400', '#7856ff', '#ff7a00', '#22d3ee', '#a3a3a3'];
5
+ export const defaultProfiles = () => ({
6
+ profiles: [{ id: DEFAULT_PROFILE, name: 'Main', color: PROFILE_COLORS[0] }],
7
+ active: DEFAULT_PROFILE,
8
+ });
9
+ export const validProfileId = (id) => typeof id === 'string' && (id === DEFAULT_PROFILE || /^p[a-z0-9]{1,32}$/.test(id));
10
+ export const validColor = (color) => typeof color === 'string' && /^#[0-9a-fA-F]{6}$/.test(color);
11
+ export function cleanName(name) {
12
+ const trimmed = name.trim();
13
+ // eslint-disable-next-line no-control-regex
14
+ if (!trimmed || [...trimmed].length > 40 || /[\u0000-\u001f\u007f-\u009f]/.test(trimmed))
15
+ return null;
16
+ return trimmed;
17
+ }
18
+ /** Anything from storage or IPC → a usable settings object (always has the default profile). */
19
+ export function normalizeProfiles(value, rules) {
20
+ const input = value;
21
+ const seen = new Set();
22
+ const profiles = [];
23
+ for (const raw of Array.isArray(input?.profiles) ? input.profiles : []) {
24
+ const name = typeof raw?.name === 'string' ? cleanName(raw.name) : null;
25
+ if (!validProfileId(raw?.id) || seen.has(raw.id) || !name || !validColor(raw.color))
26
+ continue;
27
+ seen.add(raw.id);
28
+ const account = normalizeAccount(raw.account, rules);
29
+ profiles.push({ id: raw.id, name, color: raw.color.toLowerCase(), ...(account ? { account } : {}) });
30
+ }
31
+ if (!seen.has(DEFAULT_PROFILE))
32
+ profiles.unshift(defaultProfiles().profiles[0]);
33
+ const active = typeof input?.active === 'string' && profiles.some(p => p.id === input.active) ? input.active : DEFAULT_PROFILE;
34
+ return { profiles, active };
35
+ }
36
+ export function normalizeAccount(value, rules) {
37
+ const raw = value;
38
+ if (!raw || typeof raw.handle !== 'string' || !rules.handle.test(raw.handle))
39
+ return undefined;
40
+ const avatar = typeof raw.avatar === 'string' && rules.avatarPrefixes.some(prefix => raw.avatar.startsWith(prefix)) ? raw.avatar : undefined;
41
+ return { handle: raw.handle, name: typeof raw.name === 'string' && raw.name.trim() ? raw.name : raw.handle, signedIn: raw.signedIn === true, ...(avatar ? { avatar } : {}) };
42
+ }
43
+ /** What the switcher shows for a profile: the login once seen, the profile name before. */
44
+ export function accountLabel(profile, rules) {
45
+ if (!profile.account)
46
+ return { title: profile.name, detail: 'Not signed in yet', state: 'new' };
47
+ return {
48
+ title: profile.account.name,
49
+ detail: `${rules.handlePrefix}${profile.account.handle}${profile.account.signedIn ? '' : ' · signed out'}`,
50
+ state: profile.account.signedIn ? 'signed-in' : 'signed-out',
51
+ };
52
+ }
53
+ export function newProfileId(random = Math.random) {
54
+ return `p${Date.now().toString(36)}${Math.floor(random() * 36 ** 6).toString(36)}`.slice(0, 33);
55
+ }
56
+ export function nextColor(settings) {
57
+ const used = new Set(settings.profiles.map(p => p.color));
58
+ return PROFILE_COLORS.find(color => !used.has(color)) ?? PROFILE_COLORS[settings.profiles.length % PROFILE_COLORS.length];
59
+ }
60
+ /** Adds a profile and makes it active. Unlimited count (Rust caps absurd sizes). Its name follows the
61
+ * account once someone signs in there; until then it is a placeholder. */
62
+ export function addProfile(settings, name = '', id = newProfileId()) {
63
+ const clean = cleanName(name) ?? 'New account';
64
+ if (!validProfileId(id) || settings.profiles.some(p => p.id === id))
65
+ throw new Error('VALIDATION: invalid profile id');
66
+ return { profiles: [...settings.profiles, { id, name: clean, color: nextColor(settings) }], active: id };
67
+ }
68
+ export function renameProfile(settings, id, name) {
69
+ const clean = cleanName(name);
70
+ if (!clean)
71
+ return settings;
72
+ return { ...settings, profiles: settings.profiles.map(p => (p.id === id ? { ...p, name: clean } : p)) };
73
+ }
74
+ export function recolorProfile(settings, id, color) {
75
+ if (!validColor(color))
76
+ return settings;
77
+ return { ...settings, profiles: settings.profiles.map(p => (p.id === id ? { ...p, color: color.toLowerCase() } : p)) };
78
+ }
79
+ /** The default profile cannot be removed; removing the active one falls back to default. */
80
+ export function removeProfile(settings, id) {
81
+ if (id === DEFAULT_PROFILE)
82
+ return settings;
83
+ const profiles = settings.profiles.filter(p => p.id !== id);
84
+ return { profiles, active: settings.active === id ? DEFAULT_PROFILE : settings.active };
85
+ }
86
+ export const setActiveProfile = (settings, id) => settings.profiles.some(p => p.id === id) ? { ...settings, active: id } : settings;
87
+ /** The profile a tab really runs in: unknown or missing ids (old layouts) use the default. */
88
+ export function tabProfile(settings, id) {
89
+ return settings.profiles.find(p => p.id === id) ?? settings.profiles.find(p => p.id === DEFAULT_PROFILE) ?? defaultProfiles().profiles[0];
90
+ }
91
+ //# sourceMappingURL=profileModel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profileModel.js","sourceRoot":"","sources":["../../../shell/src/profileModel.ts"],"names":[],"mappings":"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","sourcesContent":["// Pure model for account profiles (no Tauri imports, so it is unit-tested under node).\n// Mirrors `ProfileSettings` in waybetter-host `profiles.rs`; Rust validates again on save.\n\n/** The login inside a profile, as native code last saw it (read-only here; see profiles.rs). */\nexport type Account = { handle: string; name: string; avatar?: string; signedIn: boolean };\nexport type Profile = { id: string; name: string; color: string; account?: Account };\nexport type ProfileSettings = { profiles: Profile[]; active: string };\n\n/** The site's account rules, as in the native `AccountRules`. */\nexport type AccountRules = {\n handle: RegExp;\n /** Avatar URLs must start with one of these. */\n avatarPrefixes: readonly string[];\n /** Shown before the handle in the switcher, e.g. '@'. */\n handlePrefix: string;\n};\n\nexport const DEFAULT_PROFILE = 'default';\nexport const PROFILE_COLORS = ['#1d9bf0', '#f91880', '#00ba7c', '#ffd400', '#7856ff', '#ff7a00', '#22d3ee', '#a3a3a3'];\n\nexport const defaultProfiles = (): ProfileSettings => ({\n profiles: [{ id: DEFAULT_PROFILE, name: 'Main', color: PROFILE_COLORS[0]! }],\n active: DEFAULT_PROFILE,\n});\n\nexport const validProfileId = (id: unknown): id is string =>\n typeof id === 'string' && (id === DEFAULT_PROFILE || /^p[a-z0-9]{1,32}$/.test(id));\n\nexport const validColor = (color: unknown): color is string => typeof color === 'string' && /^#[0-9a-fA-F]{6}$/.test(color);\n\nexport function cleanName(name: string): string | null {\n const trimmed = name.trim();\n // eslint-disable-next-line no-control-regex\n if (!trimmed || [...trimmed].length > 40 || /[\\u0000-\\u001f\\u007f-\\u009f]/.test(trimmed)) return null;\n return trimmed;\n}\n\n/** Anything from storage or IPC → a usable settings object (always has the default profile). */\nexport function normalizeProfiles(value: unknown, rules: AccountRules): ProfileSettings {\n const input = value as Partial<ProfileSettings> | null;\n const seen = new Set<string>();\n const profiles: Profile[] = [];\n for (const raw of Array.isArray(input?.profiles) ? input!.profiles : []) {\n const name = typeof raw?.name === 'string' ? cleanName(raw.name) : null;\n if (!validProfileId(raw?.id) || seen.has(raw.id) || !name || !validColor(raw.color)) continue;\n seen.add(raw.id);\n const account = normalizeAccount(raw.account, rules);\n profiles.push({ id: raw.id, name, color: raw.color.toLowerCase(), ...(account ? { account } : {}) });\n }\n if (!seen.has(DEFAULT_PROFILE)) profiles.unshift(defaultProfiles().profiles[0]!);\n const active = typeof input?.active === 'string' && profiles.some(p => p.id === input.active) ? input.active : DEFAULT_PROFILE;\n return { profiles, active };\n}\n\nexport function normalizeAccount(value: unknown, rules: AccountRules): Account | undefined {\n const raw = value as Partial<Account> | null | undefined;\n if (!raw || typeof raw.handle !== 'string' || !rules.handle.test(raw.handle)) return undefined;\n const avatar = typeof raw.avatar === 'string' && rules.avatarPrefixes.some(prefix => raw.avatar!.startsWith(prefix)) ? raw.avatar : undefined;\n return { handle: raw.handle, name: typeof raw.name === 'string' && raw.name.trim() ? raw.name : raw.handle, signedIn: raw.signedIn === true, ...(avatar ? { avatar } : {}) };\n}\n\n/** What the switcher shows for a profile: the login once seen, the profile name before. */\nexport function accountLabel(profile: Profile, rules: Pick<AccountRules, 'handlePrefix'>): { title: string; detail: string; state: 'signed-in' | 'signed-out' | 'new' } {\n if (!profile.account) return { title: profile.name, detail: 'Not signed in yet', state: 'new' };\n return {\n title: profile.account.name,\n detail: `${rules.handlePrefix}${profile.account.handle}${profile.account.signedIn ? '' : ' · signed out'}`,\n state: profile.account.signedIn ? 'signed-in' : 'signed-out',\n };\n}\n\nexport function newProfileId(random: () => number = Math.random): string {\n return `p${Date.now().toString(36)}${Math.floor(random() * 36 ** 6).toString(36)}`.slice(0, 33);\n}\n\nexport function nextColor(settings: ProfileSettings): string {\n const used = new Set(settings.profiles.map(p => p.color));\n return PROFILE_COLORS.find(color => !used.has(color)) ?? PROFILE_COLORS[settings.profiles.length % PROFILE_COLORS.length]!;\n}\n\n/** Adds a profile and makes it active. Unlimited count (Rust caps absurd sizes). Its name follows the\n * account once someone signs in there; until then it is a placeholder. */\nexport function addProfile(settings: ProfileSettings, name = '', id = newProfileId()): ProfileSettings {\n const clean = cleanName(name) ?? 'New account';\n if (!validProfileId(id) || settings.profiles.some(p => p.id === id)) throw new Error('VALIDATION: invalid profile id');\n return { profiles: [...settings.profiles, { id, name: clean, color: nextColor(settings) }], active: id };\n}\n\nexport function renameProfile(settings: ProfileSettings, id: string, name: string): ProfileSettings {\n const clean = cleanName(name);\n if (!clean) return settings;\n return { ...settings, profiles: settings.profiles.map(p => (p.id === id ? { ...p, name: clean } : p)) };\n}\n\nexport function recolorProfile(settings: ProfileSettings, id: string, color: string): ProfileSettings {\n if (!validColor(color)) return settings;\n return { ...settings, profiles: settings.profiles.map(p => (p.id === id ? { ...p, color: color.toLowerCase() } : p)) };\n}\n\n/** The default profile cannot be removed; removing the active one falls back to default. */\nexport function removeProfile(settings: ProfileSettings, id: string): ProfileSettings {\n if (id === DEFAULT_PROFILE) return settings;\n const profiles = settings.profiles.filter(p => p.id !== id);\n return { profiles, active: settings.active === id ? DEFAULT_PROFILE : settings.active };\n}\n\nexport const setActiveProfile = (settings: ProfileSettings, id: string): ProfileSettings =>\n settings.profiles.some(p => p.id === id) ? { ...settings, active: id } : settings;\n\n/** The profile a tab really runs in: unknown or missing ids (old layouts) use the default. */\nexport function tabProfile(settings: ProfileSettings, id: string | undefined): Profile {\n return settings.profiles.find(p => p.id === id) ?? settings.profiles.find(p => p.id === DEFAULT_PROFILE) ?? defaultProfiles().profiles[0]!;\n}\n"]}
@@ -0,0 +1,35 @@
1
+ /** Mirrors `UpdateStatus` in waybetter-host `update.rs` (serde tag "state"). */
2
+ export type UpdateStatus = {
3
+ state: 'idle' | 'checking' | 'upToDate';
4
+ } | {
5
+ state: 'downloading' | 'installing';
6
+ version: string;
7
+ } | {
8
+ state: 'ready';
9
+ version: string;
10
+ notes: string;
11
+ } | {
12
+ state: 'error';
13
+ message: string;
14
+ } | {
15
+ state: 'problem';
16
+ downloadUrl: string;
17
+ };
18
+ export type UpdateChip = {
19
+ kind: 'restart';
20
+ label: string;
21
+ title: string;
22
+ } | {
23
+ kind: 'installing';
24
+ label: string;
25
+ title: string;
26
+ } | {
27
+ kind: 'problem';
28
+ label: string;
29
+ title: string;
30
+ href: string;
31
+ };
32
+ /** Accepts only well-formed status payloads; anything else is treated as idle. */
33
+ export declare function parseUpdateStatus(value: unknown): UpdateStatus;
34
+ /** Unobtrusive: nothing while checking/downloading/erroring; a chip only when action is useful. */
35
+ export declare function chipFor(status: UpdateStatus, product: string): UpdateChip | null;
@@ -0,0 +1,32 @@
1
+ // Pure model for the titlebar update chip (no Tauri imports, so it is unit-tested under node).
2
+ const STATES = new Set(['idle', 'checking', 'upToDate', 'downloading', 'installing', 'ready', 'error', 'problem']);
3
+ /** Accepts only well-formed status payloads; anything else is treated as idle. */
4
+ export function parseUpdateStatus(value) {
5
+ if (!value || typeof value !== 'object')
6
+ return { state: 'idle' };
7
+ const v = value;
8
+ if (typeof v.state !== 'string' || !STATES.has(v.state))
9
+ return { state: 'idle' };
10
+ const str = (key) => (typeof v[key] === 'string' ? v[key] : '');
11
+ switch (v.state) {
12
+ case 'ready': return { state: 'ready', version: str('version'), notes: str('notes') };
13
+ case 'downloading':
14
+ case 'installing': return { state: v.state, version: str('version') };
15
+ case 'error': return { state: 'error', message: str('message') };
16
+ case 'problem': {
17
+ const href = str('downloadUrl');
18
+ return href.startsWith('https://') ? { state: 'problem', downloadUrl: href } : { state: 'idle' };
19
+ }
20
+ default: return { state: v.state };
21
+ }
22
+ }
23
+ /** Unobtrusive: nothing while checking/downloading/erroring; a chip only when action is useful. */
24
+ export function chipFor(status, product) {
25
+ switch (status.state) {
26
+ case 'ready': return { kind: 'restart', label: 'Update ready · restart', title: `Restart to update to ${product} ${status.version}. Tabs are restored.` };
27
+ case 'installing': return { kind: 'installing', label: 'Updating…', title: `Installing ${product} ${status.version}` };
28
+ case 'problem': return { kind: 'problem', label: 'Update problem', title: 'The last update did not start correctly. Download the current version.', href: status.downloadUrl };
29
+ default: return null;
30
+ }
31
+ }
32
+ //# sourceMappingURL=updateModel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateModel.js","sourceRoot":"","sources":["../../../shell/src/updateModel.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAe/F,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AAEnH,kFAAkF;AAClF,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAClE,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAClF,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,GAAG,CAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpF,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;QAChB,KAAK,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACtF,KAAK,aAAa,CAAC;QAAC,KAAK,YAAY,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1F,KAAK,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QACjE,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC;YAChC,OAAO,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QACnG,CAAC;QACD,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAyC,EAAE,CAAC;IACzE,CAAC;AACH,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,OAAO,CAAC,MAAoB,EAAE,OAAe;IAC3D,QAAQ,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,KAAK,OAAO,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,wBAAwB,EAAE,KAAK,EAAE,wBAAwB,OAAO,IAAI,MAAM,CAAC,OAAO,sBAAsB,EAAE,CAAC;QAC1J,KAAK,YAAY,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,cAAc,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QACvH,KAAK,SAAS,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,wEAAwE,EAAE,IAAI,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;QAC/K,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC;IACvB,CAAC;AACH,CAAC","sourcesContent":["// Pure model for the titlebar update chip (no Tauri imports, so it is unit-tested under node).\n\n/** Mirrors `UpdateStatus` in waybetter-host `update.rs` (serde tag \"state\"). */\nexport type UpdateStatus =\n | { state: 'idle' | 'checking' | 'upToDate' }\n | { state: 'downloading' | 'installing'; version: string }\n | { state: 'ready'; version: string; notes: string }\n | { state: 'error'; message: string }\n | { state: 'problem'; downloadUrl: string };\n\nexport type UpdateChip =\n | { kind: 'restart'; label: string; title: string }\n | { kind: 'installing'; label: string; title: string }\n | { kind: 'problem'; label: string; title: string; href: string };\n\nconst STATES = new Set(['idle', 'checking', 'upToDate', 'downloading', 'installing', 'ready', 'error', 'problem']);\n\n/** Accepts only well-formed status payloads; anything else is treated as idle. */\nexport function parseUpdateStatus(value: unknown): UpdateStatus {\n if (!value || typeof value !== 'object') return { state: 'idle' };\n const v = value as Record<string, unknown>;\n if (typeof v.state !== 'string' || !STATES.has(v.state)) return { state: 'idle' };\n const str = (key: string) => (typeof v[key] === 'string' ? (v[key] as string) : '');\n switch (v.state) {\n case 'ready': return { state: 'ready', version: str('version'), notes: str('notes') };\n case 'downloading': case 'installing': return { state: v.state, version: str('version') };\n case 'error': return { state: 'error', message: str('message') };\n case 'problem': {\n const href = str('downloadUrl');\n return href.startsWith('https://') ? { state: 'problem', downloadUrl: href } : { state: 'idle' };\n }\n default: return { state: v.state as 'idle' | 'checking' | 'upToDate' };\n }\n}\n\n/** Unobtrusive: nothing while checking/downloading/erroring; a chip only when action is useful. */\nexport function chipFor(status: UpdateStatus, product: string): UpdateChip | null {\n switch (status.state) {\n case 'ready': return { kind: 'restart', label: 'Update ready · restart', title: `Restart to update to ${product} ${status.version}. Tabs are restored.` };\n case 'installing': return { kind: 'installing', label: 'Updating…', title: `Installing ${product} ${status.version}` };\n case 'problem': return { kind: 'problem', label: 'Update problem', title: 'The last update did not start correctly. Download the current version.', href: status.downloadUrl };\n default: return null;\n }\n}\n"]}
@@ -0,0 +1,24 @@
1
+ export type ResizeDirection = 'North' | 'South' | 'East' | 'West' | 'NorthEast' | 'NorthWest' | 'SouthEast' | 'SouthWest';
2
+ /** The subset of Tauri's `Window` the shell uses. */
3
+ export type NativeWindow = {
4
+ minimize(): Promise<void>;
5
+ toggleMaximize(): Promise<void>;
6
+ close(): Promise<void>;
7
+ startResizeDragging(direction: ResizeDirection): Promise<void>;
8
+ isMaximized(): Promise<boolean>;
9
+ onResized(handler: () => void): Promise<() => void>;
10
+ };
11
+ export type Invoke = (command: string, args?: Record<string, unknown>) => Promise<unknown>;
12
+ export declare const isTauri: boolean;
13
+ /** macOS keeps a native (overlay) frame with traffic lights; Windows is frameless with our own caption buttons. */
14
+ export declare const os: 'macos' | 'windows';
15
+ export declare const frameless: boolean;
16
+ export declare function createWindowControls(invoke: Invoke, currentWindow: () => NativeWindow): {
17
+ minimize: () => Promise<void>;
18
+ toggleMaximize: () => Promise<void>;
19
+ close: () => Promise<void>;
20
+ startResize: (direction: ResizeDirection) => Promise<void>;
21
+ onMaximizedChange: (cb: (maximized: boolean) => void) => Promise<() => void>;
22
+ /** The native system menu at a screen position (host `system_menu`; Windows only). */
23
+ systemMenu: (x: number, y: number) => Promise<unknown>;
24
+ };
@@ -0,0 +1,22 @@
1
+ // Native window calls for the custom titlebar, resize grips and caption buttons. The Tauri API is
2
+ // passed in (the app owns its `@tauri-apps/api` version), so this module has no dependencies.
3
+ export const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
4
+ /** macOS keeps a native (overlay) frame with traffic lights; Windows is frameless with our own caption buttons. */
5
+ export const os = typeof navigator !== 'undefined' && /Mac/i.test(navigator.userAgent) ? 'macos' : 'windows';
6
+ export const frameless = isTauri && os !== 'macos';
7
+ export function createWindowControls(invoke, currentWindow) {
8
+ return {
9
+ minimize: () => currentWindow().minimize().catch(() => { }),
10
+ toggleMaximize: () => currentWindow().toggleMaximize().catch(() => { }),
11
+ close: () => currentWindow().close().catch(() => { }),
12
+ startResize: (direction) => currentWindow().startResizeDragging(direction).catch(() => { }),
13
+ onMaximizedChange: (cb) => {
14
+ const read = () => { currentWindow().isMaximized().then(cb).catch(() => { }); };
15
+ read();
16
+ return currentWindow().onResized(read);
17
+ },
18
+ /** The native system menu at a screen position (host `system_menu`; Windows only). */
19
+ systemMenu: (x, y) => invoke('window_system_menu', { x, y }).catch(() => { }),
20
+ };
21
+ }
22
+ //# sourceMappingURL=windowControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windowControls.js","sourceRoot":"","sources":["../../../shell/src/windowControls.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,8FAA8F;AAe9F,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,MAAM,KAAK,WAAW,IAAI,qBAAqB,IAAI,MAAM,CAAC;AACxF,mHAAmH;AACnH,MAAM,CAAC,MAAM,EAAE,GAAwB,OAAO,SAAS,KAAK,WAAW,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AAClI,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,IAAI,EAAE,KAAK,OAAO,CAAC;AAEnD,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,aAAiC;IACpF,OAAO;QACL,QAAQ,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;QAC1D,cAAc,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,cAAc,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;QACtE,KAAK,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;QACpD,WAAW,EAAE,CAAC,SAA0B,EAAE,EAAE,CAAC,aAAa,EAAE,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;QAC3G,iBAAiB,EAAE,CAAC,EAAgC,EAAE,EAAE;YACtD,MAAM,IAAI,GAAG,GAAG,EAAE,GAAG,aAAa,EAAE,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/E,IAAI,EAAE,CAAC;YACP,OAAO,aAAa,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QACD,sFAAsF;QACtF,UAAU,EAAE,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,oBAAoB,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;KAC7F,CAAC;AACJ,CAAC","sourcesContent":["// Native window calls for the custom titlebar, resize grips and caption buttons. The Tauri API is\n// passed in (the app owns its `@tauri-apps/api` version), so this module has no dependencies.\n\nexport type ResizeDirection = 'North' | 'South' | 'East' | 'West' | 'NorthEast' | 'NorthWest' | 'SouthEast' | 'SouthWest';\n\n/** The subset of Tauri's `Window` the shell uses. */\nexport type NativeWindow = {\n minimize(): Promise<void>;\n toggleMaximize(): Promise<void>;\n close(): Promise<void>;\n startResizeDragging(direction: ResizeDirection): Promise<void>;\n isMaximized(): Promise<boolean>;\n onResized(handler: () => void): Promise<() => void>;\n};\nexport type Invoke = (command: string, args?: Record<string, unknown>) => Promise<unknown>;\n\nexport const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;\n/** macOS keeps a native (overlay) frame with traffic lights; Windows is frameless with our own caption buttons. */\nexport const os: 'macos' | 'windows' = typeof navigator !== 'undefined' && /Mac/i.test(navigator.userAgent) ? 'macos' : 'windows';\nexport const frameless = isTauri && os !== 'macos';\n\nexport function createWindowControls(invoke: Invoke, currentWindow: () => NativeWindow) {\n return {\n minimize: () => currentWindow().minimize().catch(() => {}),\n toggleMaximize: () => currentWindow().toggleMaximize().catch(() => {}),\n close: () => currentWindow().close().catch(() => {}),\n startResize: (direction: ResizeDirection) => currentWindow().startResizeDragging(direction).catch(() => {}),\n onMaximizedChange: (cb: (maximized: boolean) => void) => {\n const read = () => { currentWindow().isMaximized().then(cb).catch(() => {}); };\n read();\n return currentWindow().onResized(read);\n },\n /** The native system menu at a screen position (host `system_menu`; Windows only). */\n systemMenu: (x: number, y: number) => invoke('window_system_menu', { x, y }).catch(() => {}),\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,112 @@
1
+ {
2
+ "name": "waybetter",
3
+ "version": "0.1.0",
4
+ "license": "GPL-3.0-or-later",
5
+ "type": "module",
6
+ "description": "wayBetter: the framework for apps that extend the apps you already use in place (websites and web-built desktop apps). Page runtime (DOM/request hubs, plugins, theme color model, link routing, account reporting) and shell models (address bar, panes, profiles, palette, updates). No dependencies.",
7
+ "keywords": [
8
+ "waybetter",
9
+ "tauri",
10
+ "electron",
11
+ "betterdiscord",
12
+ "plugins",
13
+ "theming",
14
+ "dom",
15
+ "desktop"
16
+ ],
17
+ "sideEffects": false,
18
+ "exports": {
19
+ "./runtime/accountReport": {
20
+ "types": "./dist/runtime/accountReport.d.ts",
21
+ "import": "./dist/runtime/accountReport.js"
22
+ },
23
+ "./runtime/domHub": {
24
+ "types": "./dist/runtime/domHub.d.ts",
25
+ "import": "./dist/runtime/domHub.js"
26
+ },
27
+ "./runtime/linkRouting": {
28
+ "types": "./dist/runtime/linkRouting.d.ts",
29
+ "import": "./dist/runtime/linkRouting.js"
30
+ },
31
+ "./runtime/netHub": {
32
+ "types": "./dist/runtime/netHub.d.ts",
33
+ "import": "./dist/runtime/netHub.js"
34
+ },
35
+ "./runtime/paneNav": {
36
+ "types": "./dist/runtime/paneNav.d.ts",
37
+ "import": "./dist/runtime/paneNav.js"
38
+ },
39
+ "./runtime/pluginApi": {
40
+ "types": "./dist/runtime/pluginApi.d.ts",
41
+ "import": "./dist/runtime/pluginApi.js"
42
+ },
43
+ "./runtime/site": {
44
+ "types": "./dist/runtime/site.d.ts",
45
+ "import": "./dist/runtime/site.js"
46
+ },
47
+ "./runtime/themeTokens": {
48
+ "types": "./dist/runtime/themeTokens.d.ts",
49
+ "import": "./dist/runtime/themeTokens.js"
50
+ },
51
+ "./runtime/*": {
52
+ "types": "./dist/runtime/*.d.ts",
53
+ "import": "./dist/runtime/*.js"
54
+ },
55
+ "./shell/addressModel": {
56
+ "types": "./dist/shell/addressModel.d.ts",
57
+ "import": "./dist/shell/addressModel.js"
58
+ },
59
+ "./shell/chrome": {
60
+ "types": "./dist/shell/chrome.d.ts",
61
+ "import": "./dist/shell/chrome.js"
62
+ },
63
+ "./shell/linkDrop": {
64
+ "types": "./dist/shell/linkDrop.d.ts",
65
+ "import": "./dist/shell/linkDrop.js"
66
+ },
67
+ "./shell/palette": {
68
+ "types": "./dist/shell/palette.d.ts",
69
+ "import": "./dist/shell/palette.js"
70
+ },
71
+ "./shell/paneModel": {
72
+ "types": "./dist/shell/paneModel.d.ts",
73
+ "import": "./dist/shell/paneModel.js"
74
+ },
75
+ "./shell/profileModel": {
76
+ "types": "./dist/shell/profileModel.d.ts",
77
+ "import": "./dist/shell/profileModel.js"
78
+ },
79
+ "./shell/updateModel": {
80
+ "types": "./dist/shell/updateModel.d.ts",
81
+ "import": "./dist/shell/updateModel.js"
82
+ },
83
+ "./shell/windowControls": {
84
+ "types": "./dist/shell/windowControls.d.ts",
85
+ "import": "./dist/shell/windowControls.js"
86
+ },
87
+ "./shell/*": {
88
+ "types": "./dist/shell/*.d.ts",
89
+ "import": "./dist/shell/*.js"
90
+ }
91
+ },
92
+ "files": [
93
+ "dist",
94
+ "README.md",
95
+ "LICENSE"
96
+ ],
97
+ "engines": {
98
+ "node": ">=18.0.0"
99
+ },
100
+ "publishConfig": {
101
+ "access": "public"
102
+ },
103
+ "scripts": {
104
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p ../runtime/tsconfig.build.json --outDir dist/runtime --inlineSources --declarationMap false && tsc -p ../shell/tsconfig.build.json --outDir dist/shell --inlineSources --declarationMap false",
105
+ "test": "cd ../.. && node --test packages/runtime/tests/*.test.ts packages/shell/tests/*.test.ts",
106
+ "prepublishOnly": "npm run build && npm test"
107
+ },
108
+ "devDependencies": {
109
+ "@types/node": "^24.0.0",
110
+ "typescript": "^5.9.0"
111
+ }
112
+ }