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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// The color foundation every theme is built on. The site's own palette (one per site color mode) is
|
|
2
|
+
// the reference coordinate system: every neutral the site paints sits somewhere on its axis from page
|
|
3
|
+
// background to text, and a theme says where its own background, surfaces, border, secondary text and
|
|
4
|
+
// text sit on that same axis. Any site neutral (listed or not, hover tints included) lands at the same
|
|
5
|
+
// position on the theme's curve, interpolated in OKLab, so nothing keeps the site's gray and nothing
|
|
6
|
+
// collapses onto the wrong role. The site's accent family follows the theme accent with the same
|
|
7
|
+
// lightness offset. Colors that mean something (likes, errors, badges) are left alone.
|
|
8
|
+
//
|
|
9
|
+
// Pure: runs in the page runtime, the settings previews and the tests. The site adapter supplies the
|
|
10
|
+
// source palettes (`SourceAxis`, one per site mode); e.g. X's Default/Dim/Lights out.
|
|
11
|
+
/** `#rrggbb` → [r, g, b] (local, so themeSpec.ts can import this module without a cycle). */
|
|
12
|
+
function hexToRgb(hex) {
|
|
13
|
+
const n = parseInt(hex.slice(1, 7), 16);
|
|
14
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
15
|
+
}
|
|
16
|
+
const toLinear = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
|
17
|
+
const toGamma = (v) => Math.round(255 * Math.min(1, Math.max(0, v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055)));
|
|
18
|
+
export function rgbToOklab([r, g, b]) {
|
|
19
|
+
const [lr, lg, lb] = [toLinear(r), toLinear(g), toLinear(b)];
|
|
20
|
+
const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
|
|
21
|
+
const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
|
|
22
|
+
const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
|
|
23
|
+
return [
|
|
24
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
25
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
26
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
export function oklabToRgb([L, A, B]) {
|
|
30
|
+
const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;
|
|
31
|
+
const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;
|
|
32
|
+
const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;
|
|
33
|
+
return [
|
|
34
|
+
toGamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
35
|
+
toGamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
36
|
+
toGamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
const keyRgb = (key) => key.split(',').map(Number);
|
|
40
|
+
const lerp = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
41
|
+
const chroma = (c) => Math.hypot(c[1], c[2]);
|
|
42
|
+
const hue = (c) => (Math.atan2(c[2], c[1]) * 180) / Math.PI;
|
|
43
|
+
/** Anything whose chroma is below this is a neutral (the site's grays carry a faint tint). */
|
|
44
|
+
const NEUTRAL_CHROMA = 0.05;
|
|
45
|
+
/** How far past the site's own background/text a neutral may sit and still be on the axis (pure white
|
|
46
|
+
* text in Lights out is slightly past the site's 231,233,234; black in Default is far past and is a shadow). */
|
|
47
|
+
const AXIS_SLACK = 0.12;
|
|
48
|
+
const PRIORITY = {
|
|
49
|
+
background: ['bg', 'surface', 'elevated', 'border', 'muted', 'text'],
|
|
50
|
+
border: ['bg', 'border', 'elevated', 'surface', 'muted', 'text'],
|
|
51
|
+
// Text pins only text roles: a theme may draw borders brighter than its secondary text (High
|
|
52
|
+
// Contrast), and a grey the site uses for text must not jump to that border.
|
|
53
|
+
text: ['bg', 'text', 'muted'],
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The model for mapping the site's palette (`source`, one site mode) onto a theme's axis colors for one usage.
|
|
57
|
+
* Every color the site lists for a role is pinned to the theme's color for that role (so a surface the site uses
|
|
58
|
+
* stays exactly the theme's surface), and everything the site paints between them is interpolated.
|
|
59
|
+
*/
|
|
60
|
+
export function buildColorModel(source, target, usage = 'background') {
|
|
61
|
+
if (!target.bg || !target.text)
|
|
62
|
+
return null;
|
|
63
|
+
const sourceBg = rgbToOklab(keyRgb(source.bg[0]));
|
|
64
|
+
const sourceText = rgbToOklab(keyRgb(source.text[0]));
|
|
65
|
+
const span = sourceText[0] - sourceBg[0];
|
|
66
|
+
const position = (c) => (c[0] - sourceBg[0]) / span;
|
|
67
|
+
const anchors = [
|
|
68
|
+
{ t: 0, color: rgbToOklab(hexToRgb(target.bg)) },
|
|
69
|
+
{ t: 1, color: rgbToOklab(hexToRgb(target.text)) },
|
|
70
|
+
];
|
|
71
|
+
const claimed = new Set();
|
|
72
|
+
const exact = new Map();
|
|
73
|
+
for (const role of PRIORITY[usage]) {
|
|
74
|
+
const color = target[role];
|
|
75
|
+
for (const key of source[role]) {
|
|
76
|
+
if (claimed.has(key))
|
|
77
|
+
continue;
|
|
78
|
+
claimed.add(key);
|
|
79
|
+
if (!color)
|
|
80
|
+
continue;
|
|
81
|
+
exact.set(key, hexToRgb(color));
|
|
82
|
+
const t = position(rgbToOklab(keyRgb(key)));
|
|
83
|
+
if (t <= 0.002 || t >= 0.998 || anchors.some(anchor => Math.abs(anchor.t - t) < 0.002))
|
|
84
|
+
continue;
|
|
85
|
+
anchors.push({ t, color: rgbToOklab(hexToRgb(color)) });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Text and borders in the site's blue stay exactly the theme accent (legibility is checked on it);
|
|
89
|
+
// accent backgrounds keep the site's lightness offsets (hover, pressed) through projectColor.
|
|
90
|
+
if (target.accent && usage !== 'background')
|
|
91
|
+
for (const key of source.accent)
|
|
92
|
+
if (!exact.has(key))
|
|
93
|
+
exact.set(key, hexToRgb(target.accent));
|
|
94
|
+
anchors.sort((a, b) => a.t - b.t);
|
|
95
|
+
return {
|
|
96
|
+
sourceBg, sourceText, anchors, exact,
|
|
97
|
+
sourceAccent: rgbToOklab(keyRgb(source.accent[0])),
|
|
98
|
+
accent: target.accent ? rgbToOklab(hexToRgb(target.accent)) : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Where a neutral sits on the site's axis (0 = the site background, 1 = the site text), or null when it is off the axis. */
|
|
102
|
+
export function axisPosition(model, rgb) {
|
|
103
|
+
const c = rgbToOklab(rgb);
|
|
104
|
+
if (chroma(c) >= NEUTRAL_CHROMA)
|
|
105
|
+
return null;
|
|
106
|
+
const t = (c[0] - model.sourceBg[0]) / (model.sourceText[0] - model.sourceBg[0]);
|
|
107
|
+
if (t < -AXIS_SLACK || t > 1 + AXIS_SLACK)
|
|
108
|
+
return null;
|
|
109
|
+
return Math.min(1, Math.max(0, t));
|
|
110
|
+
}
|
|
111
|
+
function onCurve(model, t) {
|
|
112
|
+
const { anchors } = model;
|
|
113
|
+
for (let i = 1; i < anchors.length; i++) {
|
|
114
|
+
const a = anchors[i - 1], b = anchors[i];
|
|
115
|
+
if (t <= b.t)
|
|
116
|
+
return lerp(a.color, b.color, (t - a.t) / (b.t - a.t || 1));
|
|
117
|
+
}
|
|
118
|
+
return anchors[anchors.length - 1].color;
|
|
119
|
+
}
|
|
120
|
+
/** The site's accent family: same hue family as the site's accent, clearly chromatic. */
|
|
121
|
+
function isAccentFamily(model, c) {
|
|
122
|
+
if (chroma(c) < 0.07)
|
|
123
|
+
return false;
|
|
124
|
+
const delta = Math.abs(((hue(c) - hue(model.sourceAccent) + 540) % 360) - 180);
|
|
125
|
+
return delta < 22;
|
|
126
|
+
}
|
|
127
|
+
/** The theme color for one of the site's colors, or null to leave it as the site painted it. */
|
|
128
|
+
export function projectColor(model, rgb) {
|
|
129
|
+
const listed = model.exact.get(rgb.join(','));
|
|
130
|
+
if (listed)
|
|
131
|
+
return listed;
|
|
132
|
+
const c = rgbToOklab(rgb);
|
|
133
|
+
const t = axisPosition(model, rgb);
|
|
134
|
+
if (t !== null)
|
|
135
|
+
return oklabToRgb(onCurve(model, t));
|
|
136
|
+
if (model.accent && isAccentFamily(model, c)) {
|
|
137
|
+
// Keep the site's lightness offset (hover is darker, tints lighter) on the theme's accent.
|
|
138
|
+
const shifted = [Math.min(1, Math.max(0, model.accent[0] + (c[0] - model.sourceAccent[0]))), model.accent[1], model.accent[2]];
|
|
139
|
+
return oklabToRgb(shifted);
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The base structure every theme is built on: where a raised surface, a hover/elevated surface, a
|
|
145
|
+
* border and secondary text sit between a theme's background (0) and its text (1), in OKLab
|
|
146
|
+
* lightness. Taken from the built-in themes (their median), so a theme that only names its
|
|
147
|
+
* background, text and accent gets the same proportions as the rest.
|
|
148
|
+
*/
|
|
149
|
+
export const BASE_STEPS = {
|
|
150
|
+
dark: { surface: 0.04, elevated: 0.09, border: 0.17, muted: 0.7 },
|
|
151
|
+
light: { surface: 0.025, elevated: 0.06, border: 0.13, muted: 0.7 },
|
|
152
|
+
};
|
|
153
|
+
const toHex = (rgb) => `#${rgb.map(v => v.toString(16).padStart(2, '0')).join('')}`;
|
|
154
|
+
/** Fills in the roles a theme did not name from its background, text and the base structure. */
|
|
155
|
+
export function deriveColors(seed) {
|
|
156
|
+
const bg = rgbToOklab(hexToRgb(seed.bg));
|
|
157
|
+
const text = rgbToOklab(hexToRgb(seed.text));
|
|
158
|
+
const steps = BASE_STEPS[bg[0] < text[0] ? 'dark' : 'light'];
|
|
159
|
+
const at = (t) => toHex(oklabToRgb(lerp(bg, text, t)));
|
|
160
|
+
return {
|
|
161
|
+
...seed,
|
|
162
|
+
surface: seed.surface ?? at(steps.surface),
|
|
163
|
+
elevated: seed.elevated ?? at(steps.elevated),
|
|
164
|
+
border: seed.border ?? at(steps.border),
|
|
165
|
+
muted: seed.muted ?? at(steps.muted),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Where a theme's roles sit on its own background→text axis (0 = bg, 1 = text). */
|
|
169
|
+
export function rolePositions(colors) {
|
|
170
|
+
const L = (hex) => rgbToOklab(hexToRgb(hex))[0];
|
|
171
|
+
const span = L(colors.text) - L(colors.bg);
|
|
172
|
+
const pos = (hex) => (L(hex) - L(colors.bg)) / span;
|
|
173
|
+
return { surface: pos(colors.surface), elevated: pos(colors.elevated), border: pos(colors.border), muted: pos(colors.muted) };
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=themeTokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"themeTokens.js","sourceRoot":"","sources":["../../../runtime/src/themeTokens.ts"],"names":[],"mappings":"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","sourcesContent":["// The color foundation every theme is built on. The site's own palette (one per site color mode) is\n// the reference coordinate system: every neutral the site paints sits somewhere on its axis from page\n// background to text, and a theme says where its own background, surfaces, border, secondary text and\n// text sit on that same axis. Any site neutral (listed or not, hover tints included) lands at the same\n// position on the theme's curve, interpolated in OKLab, so nothing keeps the site's gray and nothing\n// collapses onto the wrong role. The site's accent family follows the theme accent with the same\n// lightness offset. Colors that mean something (likes, errors, badges) are left alone.\n//\n// Pure: runs in the page runtime, the settings previews and the tests. The site adapter supplies the\n// source palettes (`SourceAxis`, one per site mode); e.g. X's Default/Dim/Lights out.\n\n/** `#rrggbb` → [r, g, b] (local, so themeSpec.ts can import this module without a cycle). */\nfunction hexToRgb(hex: string): Rgb {\n const n = parseInt(hex.slice(1, 7), 16);\n return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\nexport type Oklab = [number, number, number];\ntype Rgb = [number, number, number];\nexport type AxisRole = 'bg' | 'surface' | 'elevated' | 'border' | 'muted' | 'text';\nexport type SourceAxis = Record<AxisRole | 'accent', string[]>;\nexport type TargetAxis = Partial<Record<AxisRole | 'accent', string>>;\n\nconst toLinear = (v: number) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };\nconst toGamma = (v: number) => Math.round(255 * Math.min(1, Math.max(0, v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055)));\n\nexport function rgbToOklab([r, g, b]: Rgb): Oklab {\n const [lr, lg, lb] = [toLinear(r), toLinear(g), toLinear(b)];\n const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);\n const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);\n const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);\n return [\n 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,\n 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,\n 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,\n ];\n}\n\nexport function oklabToRgb([L, A, B]: Oklab): Rgb {\n const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;\n const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;\n const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;\n return [\n toGamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),\n toGamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),\n toGamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),\n ];\n}\n\nconst keyRgb = (key: string) => key.split(',').map(Number) as Rgb;\nconst lerp = (a: Oklab, b: Oklab, t: number): Oklab => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\nconst chroma = (c: Oklab) => Math.hypot(c[1], c[2]);\nconst hue = (c: Oklab) => (Math.atan2(c[2], c[1]) * 180) / Math.PI;\n\n/** Anything whose chroma is below this is a neutral (the site's grays carry a faint tint). */\nconst NEUTRAL_CHROMA = 0.05;\n/** How far past the site's own background/text a neutral may sit and still be on the axis (pure white\n * text in Lights out is slightly past the site's 231,233,234; black in Default is far past and is a shadow). */\nconst AXIS_SLACK = 0.12;\n\nexport type ColorModel = {\n sourceBg: Oklab;\n sourceText: Oklab;\n /** Sorted by `t`: the site's position on its bg→text axis and the theme color placed there. */\n anchors: Array<{ t: number; color: Oklab }>;\n /** The site's listed role colors (`r,g,b`) and the theme color they become, exactly. */\n exact: Map<string, Rgb>;\n sourceAccent: Oklab;\n accent: Oklab | null;\n};\n\n/**\n * How a color is used decides which site role wins where the site reuses one value for two roles (Default's\n * 239,243,244 is both a raised surface and a border): backgrounds read it as a surface, borders as\n * a border, text as text.\n */\nexport type Usage = 'background' | 'border' | 'text';\nconst PRIORITY: Record<Usage, AxisRole[]> = {\n background: ['bg', 'surface', 'elevated', 'border', 'muted', 'text'],\n border: ['bg', 'border', 'elevated', 'surface', 'muted', 'text'],\n // Text pins only text roles: a theme may draw borders brighter than its secondary text (High\n // Contrast), and a grey the site uses for text must not jump to that border.\n text: ['bg', 'text', 'muted'],\n};\n\n/**\n * The model for mapping the site's palette (`source`, one site mode) onto a theme's axis colors for one usage.\n * Every color the site lists for a role is pinned to the theme's color for that role (so a surface the site uses\n * stays exactly the theme's surface), and everything the site paints between them is interpolated.\n */\nexport function buildColorModel(source: SourceAxis, target: TargetAxis, usage: Usage = 'background'): ColorModel | null {\n if (!target.bg || !target.text) return null;\n const sourceBg = rgbToOklab(keyRgb(source.bg[0]!));\n const sourceText = rgbToOklab(keyRgb(source.text[0]!));\n const span = sourceText[0] - sourceBg[0];\n const position = (c: Oklab) => (c[0] - sourceBg[0]) / span;\n const anchors: ColorModel['anchors'] = [\n { t: 0, color: rgbToOklab(hexToRgb(target.bg)) },\n { t: 1, color: rgbToOklab(hexToRgb(target.text)) },\n ];\n const claimed = new Set<string>();\n const exact = new Map<string, Rgb>();\n for (const role of PRIORITY[usage]) {\n const color = target[role];\n for (const key of source[role]) {\n if (claimed.has(key)) continue;\n claimed.add(key);\n if (!color) continue;\n exact.set(key, hexToRgb(color));\n const t = position(rgbToOklab(keyRgb(key)));\n if (t <= 0.002 || t >= 0.998 || anchors.some(anchor => Math.abs(anchor.t - t) < 0.002)) continue;\n anchors.push({ t, color: rgbToOklab(hexToRgb(color)) });\n }\n }\n // Text and borders in the site's blue stay exactly the theme accent (legibility is checked on it);\n // accent backgrounds keep the site's lightness offsets (hover, pressed) through projectColor.\n if (target.accent && usage !== 'background') for (const key of source.accent) if (!exact.has(key)) exact.set(key, hexToRgb(target.accent));\n anchors.sort((a, b) => a.t - b.t);\n return {\n sourceBg, sourceText, anchors, exact,\n sourceAccent: rgbToOklab(keyRgb(source.accent[0]!)),\n accent: target.accent ? rgbToOklab(hexToRgb(target.accent)) : null,\n };\n}\n\n/** Where a neutral sits on the site's axis (0 = the site background, 1 = the site text), or null when it is off the axis. */\nexport function axisPosition(model: ColorModel, rgb: Rgb): number | null {\n const c = rgbToOklab(rgb);\n if (chroma(c) >= NEUTRAL_CHROMA) return null;\n const t = (c[0] - model.sourceBg[0]) / (model.sourceText[0] - model.sourceBg[0]);\n if (t < -AXIS_SLACK || t > 1 + AXIS_SLACK) return null;\n return Math.min(1, Math.max(0, t));\n}\n\nfunction onCurve(model: ColorModel, t: number): Oklab {\n const { anchors } = model;\n for (let i = 1; i < anchors.length; i++) {\n const a = anchors[i - 1]!, b = anchors[i]!;\n if (t <= b.t) return lerp(a.color, b.color, (t - a.t) / (b.t - a.t || 1));\n }\n return anchors[anchors.length - 1]!.color;\n}\n\n/** The site's accent family: same hue family as the site's accent, clearly chromatic. */\nfunction isAccentFamily(model: ColorModel, c: Oklab): boolean {\n if (chroma(c) < 0.07) return false;\n const delta = Math.abs(((hue(c) - hue(model.sourceAccent) + 540) % 360) - 180);\n return delta < 22;\n}\n\n/** The theme color for one of the site's colors, or null to leave it as the site painted it. */\nexport function projectColor(model: ColorModel, rgb: Rgb): Rgb | null {\n const listed = model.exact.get(rgb.join(','));\n if (listed) return listed;\n const c = rgbToOklab(rgb);\n const t = axisPosition(model, rgb);\n if (t !== null) return oklabToRgb(onCurve(model, t));\n if (model.accent && isAccentFamily(model, c)) {\n // Keep the site's lightness offset (hover is darker, tints lighter) on the theme's accent.\n const shifted: Oklab = [Math.min(1, Math.max(0, model.accent[0] + (c[0] - model.sourceAccent[0]))), model.accent[1], model.accent[2]];\n return oklabToRgb(shifted);\n }\n return null;\n}\n\n/**\n * The base structure every theme is built on: where a raised surface, a hover/elevated surface, a\n * border and secondary text sit between a theme's background (0) and its text (1), in OKLab\n * lightness. Taken from the built-in themes (their median), so a theme that only names its\n * background, text and accent gets the same proportions as the rest.\n */\nexport const BASE_STEPS: Record<'dark' | 'light', Record<'surface' | 'elevated' | 'border' | 'muted', number>> = {\n dark: { surface: 0.04, elevated: 0.09, border: 0.17, muted: 0.7 },\n light: { surface: 0.025, elevated: 0.06, border: 0.13, muted: 0.7 },\n};\n\nconst toHex = (rgb: Rgb) => `#${rgb.map(v => v.toString(16).padStart(2, '0')).join('')}`;\n\n/** Fills in the roles a theme did not name from its background, text and the base structure. */\nexport function deriveColors<T extends { bg: string; text: string; accent: string; surface?: string; elevated?: string; border?: string; muted?: string }>(seed: T):\n T & Record<'surface' | 'elevated' | 'border' | 'muted', string> {\n const bg = rgbToOklab(hexToRgb(seed.bg));\n const text = rgbToOklab(hexToRgb(seed.text));\n const steps = BASE_STEPS[bg[0] < text[0] ? 'dark' : 'light'];\n const at = (t: number) => toHex(oklabToRgb(lerp(bg, text, t)));\n return {\n ...seed,\n surface: seed.surface ?? at(steps.surface),\n elevated: seed.elevated ?? at(steps.elevated),\n border: seed.border ?? at(steps.border),\n muted: seed.muted ?? at(steps.muted),\n };\n}\n\n/** Where a theme's roles sit on its own background→text axis (0 = bg, 1 = text). */\nexport function rolePositions(colors: { bg: string; text: string } & Record<'surface' | 'elevated' | 'border' | 'muted', string>) {\n const L = (hex: string) => rgbToOklab(hexToRgb(hex))[0];\n const span = L(colors.text) - L(colors.bg);\n const pos = (hex: string) => (L(hex) - L(colors.bg)) / span;\n return { surface: pos(colors.surface), elevated: pos(colors.elevated), border: pos(colors.border), muted: pos(colors.muted) };\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type Segment = {
|
|
2
|
+
label: string;
|
|
3
|
+
href: string;
|
|
4
|
+
mono?: boolean;
|
|
5
|
+
quoted?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type AddressModel<Kind extends string = string> = {
|
|
8
|
+
site: {
|
|
9
|
+
kind: Kind | 'host';
|
|
10
|
+
label: string;
|
|
11
|
+
href: string;
|
|
12
|
+
};
|
|
13
|
+
segments: Segment[];
|
|
14
|
+
insecure: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type AddressSite<Kind extends string = string> = {
|
|
17
|
+
/** `site.kind` for the adapter's own pages, e.g. 'x'. */
|
|
18
|
+
kind: Kind;
|
|
19
|
+
/** Hostnames (lowercase) that are the site. */
|
|
20
|
+
hosts: ReadonlySet<string>;
|
|
21
|
+
/** The site chip's label. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Where the site chip links: its home page on this origin. */
|
|
24
|
+
home(origin: string): string;
|
|
25
|
+
/** Named chips for one of the site's URLs, or null to show no chips (an internal page). */
|
|
26
|
+
segments(url: URL): Segment[] | null;
|
|
27
|
+
};
|
|
28
|
+
export declare function truncate(value: string, max: number): string;
|
|
29
|
+
/** Long numeric ids shown by their last 6 characters. */
|
|
30
|
+
export declare function last6(id: string): string;
|
|
31
|
+
export declare function titleCase(part: string): string;
|
|
32
|
+
/** Generic chips: one per path segment, then `?…` for a query. */
|
|
33
|
+
export declare function hostSegments(url: URL): Segment[];
|
|
34
|
+
export declare function parseAddress<Kind extends string>(url: string, site?: AddressSite<Kind>): AddressModel<Kind> | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Pure, unit-testable model for the address bar's breadcrumb chips.
|
|
2
|
+
// No React, no Tauri: parseAddress(url, site) -> AddressModel | null. The site adapter turns its
|
|
3
|
+
// own URLs into named chips; any other site gets host + path chips.
|
|
4
|
+
export function truncate(value, max) {
|
|
5
|
+
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
6
|
+
}
|
|
7
|
+
/** Long numeric ids shown by their last 6 characters. */
|
|
8
|
+
export function last6(id) {
|
|
9
|
+
return `…${id.slice(-6)}`;
|
|
10
|
+
}
|
|
11
|
+
export function titleCase(part) {
|
|
12
|
+
return part
|
|
13
|
+
.split(/[-_]/)
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
16
|
+
.join(' ');
|
|
17
|
+
}
|
|
18
|
+
/** Generic chips: one per path segment, then `?…` for a query. */
|
|
19
|
+
export function hostSegments(url) {
|
|
20
|
+
const origin = `${url.protocol}//${url.hostname}`;
|
|
21
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
22
|
+
const segments = [];
|
|
23
|
+
parts.forEach((part, index) => {
|
|
24
|
+
const href = `${origin}/${parts.slice(0, index + 1).join('/')}`;
|
|
25
|
+
segments.push({ label: truncate(decodeURIComponent(part), 24), href });
|
|
26
|
+
});
|
|
27
|
+
if (url.search) {
|
|
28
|
+
segments.push({ label: '?…', href: `${origin}${url.pathname}${url.search}` });
|
|
29
|
+
}
|
|
30
|
+
return segments;
|
|
31
|
+
}
|
|
32
|
+
export function parseAddress(url, site) {
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = new URL(url);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const insecure = parsed.protocol === 'http:';
|
|
44
|
+
const hostname = parsed.hostname.replace(/^www\./, '');
|
|
45
|
+
if (site && site.hosts.has(parsed.hostname.toLowerCase())) {
|
|
46
|
+
const origin = `${parsed.protocol}//${parsed.hostname}`;
|
|
47
|
+
const segments = site.segments(parsed);
|
|
48
|
+
if (segments === null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const last = segments[segments.length - 1];
|
|
52
|
+
if (last)
|
|
53
|
+
last.current = true;
|
|
54
|
+
return {
|
|
55
|
+
site: { kind: site.kind, label: site.label, href: site.home(origin) },
|
|
56
|
+
segments,
|
|
57
|
+
insecure,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
site: { kind: 'host', label: hostname, href: `${parsed.protocol}//${parsed.hostname}` },
|
|
62
|
+
segments: hostSegments(parsed),
|
|
63
|
+
insecure,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=addressModel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addressModel.js","sourceRoot":"","sources":["../../../shell/src/addressModel.ts"],"names":[],"mappings":"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","sourcesContent":["// Pure, unit-testable model for the address bar's breadcrumb chips.\n// No React, no Tauri: parseAddress(url, site) -> AddressModel | null. The site adapter turns its\n// own URLs into named chips; any other site gets host + path chips.\n\nexport type Segment = { label: string; href: string; mono?: boolean; quoted?: boolean };\nexport type AddressModel<Kind extends string = string> = {\n site: { kind: Kind | 'host'; label: string; href: string };\n segments: Segment[];\n insecure: boolean;\n};\n\nexport type AddressSite<Kind extends string = string> = {\n /** `site.kind` for the adapter's own pages, e.g. 'x'. */\n kind: Kind;\n /** Hostnames (lowercase) that are the site. */\n hosts: ReadonlySet<string>;\n /** The site chip's label. */\n label: string;\n /** Where the site chip links: its home page on this origin. */\n home(origin: string): string;\n /** Named chips for one of the site's URLs, or null to show no chips (an internal page). */\n segments(url: URL): Segment[] | null;\n};\n\nexport function truncate(value: string, max: number): string {\n return value.length > max ? `${value.slice(0, max - 1)}…` : value;\n}\n\n/** Long numeric ids shown by their last 6 characters. */\nexport function last6(id: string): string {\n return `…${id.slice(-6)}`;\n}\n\nexport function titleCase(part: string): string {\n return part\n .split(/[-_]/)\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n/** Generic chips: one per path segment, then `?…` for a query. */\nexport function hostSegments(url: URL): Segment[] {\n const origin = `${url.protocol}//${url.hostname}`;\n const parts = url.pathname.split('/').filter(Boolean);\n const segments: Segment[] = [];\n parts.forEach((part, index) => {\n const href = `${origin}/${parts.slice(0, index + 1).join('/')}`;\n segments.push({ label: truncate(decodeURIComponent(part), 24), href });\n });\n if (url.search) {\n segments.push({ label: '?…', href: `${origin}${url.pathname}${url.search}` });\n }\n return segments;\n}\n\nexport function parseAddress<Kind extends string>(url: string, site?: AddressSite<Kind>): AddressModel<Kind> | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return null;\n }\n const insecure = parsed.protocol === 'http:';\n const hostname = parsed.hostname.replace(/^www\\./, '');\n\n if (site && site.hosts.has(parsed.hostname.toLowerCase())) {\n const origin = `${parsed.protocol}//${parsed.hostname}`;\n const segments = site.segments(parsed);\n if (segments === null) {\n return null;\n }\n const last = segments[segments.length - 1];\n if (last) (last as Segment & { current?: boolean }).current = true;\n return {\n site: { kind: site.kind, label: site.label, href: site.home(origin) },\n segments,\n insecure,\n };\n }\n\n return {\n site: { kind: 'host', label: hostname, href: `${parsed.protocol}//${parsed.hostname}` },\n segments: hostSegments(parsed),\n insecure,\n };\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Layout constants for the custom window chrome (titlebar + toolbar), in logical px. TITLEBAR_H and
|
|
2
|
+
// RESIZE_BORDER must match `policy::TITLEBAR_H` / `policy::RESIZE_BORDER` in waybetter-host.
|
|
3
|
+
export const TITLEBAR_H = 44;
|
|
4
|
+
export const TOOLBAR_H = 50;
|
|
5
|
+
export const RESIZE_BORDER = 5;
|
|
6
|
+
//# sourceMappingURL=chrome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome.js","sourceRoot":"","sources":["../../../shell/src/chrome.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,6FAA6F;AAC7F,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,CAAC;AAC7B,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAC5B,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC","sourcesContent":["// Layout constants for the custom window chrome (titlebar + toolbar), in logical px. TITLEBAR_H and\n// RESIZE_BORDER must match `policy::TITLEBAR_H` / `policy::RESIZE_BORDER` in waybetter-host.\nexport const TITLEBAR_H = 44;\nexport const TOOLBAR_H = 50;\nexport const RESIZE_BORDER = 5;\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const MAX_LINK_LENGTH = 4096;
|
|
2
|
+
/** Only plain http(s) links without credentials open as tabs (same rule as the host's `tabs::parse_web_url`). */
|
|
3
|
+
export declare function webUrl(value: string | null | undefined): string | null;
|
|
4
|
+
/**
|
|
5
|
+
* The link carried by a drop: `text/uri-list` (first non-comment line), then `text/x-moz-url`,
|
|
6
|
+
* then the first line of `text/plain`; else the URL the source tab reported on dragstart.
|
|
7
|
+
*/
|
|
8
|
+
export declare function dropUrl(getData: (type: string) => string, reported?: string | null): string | null;
|
|
9
|
+
/** Drag types that may carry a link. File drags are left to the page (e.g. a composer). */
|
|
10
|
+
export declare function carriesLink(types: readonly string[]): boolean;
|
|
11
|
+
export type DropPosition = 'top' | 'bottom' | 'left' | 'right' | 'center';
|
|
12
|
+
export type DropDirection = 'left' | 'right' | 'above' | 'below';
|
|
13
|
+
export type Placement = {
|
|
14
|
+
referenceGroup: string;
|
|
15
|
+
direction: 'within' | DropDirection;
|
|
16
|
+
index?: number;
|
|
17
|
+
} | {
|
|
18
|
+
direction: DropDirection;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Where a dropped link opens. `groupId`: the group under the pointer (missing for the dock's outer
|
|
22
|
+
* edges). `tabIndex`: the tab dropped on, if any. `undefined`: the default (active group, or a new
|
|
23
|
+
* group in an empty dock).
|
|
24
|
+
*/
|
|
25
|
+
export declare function placementFor(target: {
|
|
26
|
+
position: DropPosition;
|
|
27
|
+
groupId?: string;
|
|
28
|
+
tabIndex?: number;
|
|
29
|
+
}): Placement | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Pure helpers for dropping a dragged link into the dock as a new tab or split pane.
|
|
2
|
+
// No React, no Tauri, no dockview runtime: the Shell maps dockview drop events through these.
|
|
3
|
+
export const MAX_LINK_LENGTH = 4096;
|
|
4
|
+
/** Only plain http(s) links without credentials open as tabs (same rule as the host's `tabs::parse_web_url`). */
|
|
5
|
+
export function webUrl(value) {
|
|
6
|
+
const text = value?.trim();
|
|
7
|
+
if (!text || text.length > MAX_LINK_LENGTH)
|
|
8
|
+
return null;
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(text);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password)
|
|
17
|
+
return null;
|
|
18
|
+
return url.href;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The link carried by a drop: `text/uri-list` (first non-comment line), then `text/x-moz-url`,
|
|
22
|
+
* then the first line of `text/plain`; else the URL the source tab reported on dragstart.
|
|
23
|
+
*/
|
|
24
|
+
export function dropUrl(getData, reported) {
|
|
25
|
+
const read = (type) => { try {
|
|
26
|
+
return getData(type) || '';
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return '';
|
|
30
|
+
} };
|
|
31
|
+
const firstLine = (text, skipComments) => text.split(/\r?\n/).map(line => line.trim()).find(line => line && !(skipComments && line.startsWith('#'))) ?? '';
|
|
32
|
+
return webUrl(firstLine(read('text/uri-list'), true))
|
|
33
|
+
?? webUrl(firstLine(read('text/x-moz-url'), false))
|
|
34
|
+
?? webUrl(firstLine(read('text/plain'), false))
|
|
35
|
+
?? webUrl(reported);
|
|
36
|
+
}
|
|
37
|
+
/** Drag types that may carry a link. File drags are left to the page (e.g. a composer). */
|
|
38
|
+
export function carriesLink(types) {
|
|
39
|
+
if (types.includes('Files'))
|
|
40
|
+
return false;
|
|
41
|
+
return types.includes('text/uri-list') || types.includes('text/x-moz-url');
|
|
42
|
+
}
|
|
43
|
+
const DIRECTIONS = { top: 'above', bottom: 'below', left: 'left', right: 'right' };
|
|
44
|
+
/**
|
|
45
|
+
* Where a dropped link opens. `groupId`: the group under the pointer (missing for the dock's outer
|
|
46
|
+
* edges). `tabIndex`: the tab dropped on, if any. `undefined`: the default (active group, or a new
|
|
47
|
+
* group in an empty dock).
|
|
48
|
+
*/
|
|
49
|
+
export function placementFor(target) {
|
|
50
|
+
const { position, groupId, tabIndex } = target;
|
|
51
|
+
if (!groupId)
|
|
52
|
+
return position === 'center' ? undefined : { direction: DIRECTIONS[position] };
|
|
53
|
+
if (position === 'center')
|
|
54
|
+
return tabIndex === undefined ? { referenceGroup: groupId, direction: 'within' } : { referenceGroup: groupId, direction: 'within', index: tabIndex };
|
|
55
|
+
return { referenceGroup: groupId, direction: DIRECTIONS[position] };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=linkDrop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkDrop.js","sourceRoot":"","sources":["../../../shell/src/linkDrop.ts"],"names":[],"mappings":"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","sourcesContent":["// Pure helpers for dropping a dragged link into the dock as a new tab or split pane.\n// No React, no Tauri, no dockview runtime: the Shell maps dockview drop events through these.\n\nexport const MAX_LINK_LENGTH = 4096;\n\n/** Only plain http(s) links without credentials open as tabs (same rule as the host's `tabs::parse_web_url`). */\nexport function webUrl(value: string | null | undefined): string | null {\n const text = value?.trim();\n if (!text || text.length > MAX_LINK_LENGTH) return null;\n let url: URL;\n try { url = new URL(text); } catch { return null; }\n if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password) return null;\n return url.href;\n}\n\n/**\n * The link carried by a drop: `text/uri-list` (first non-comment line), then `text/x-moz-url`,\n * then the first line of `text/plain`; else the URL the source tab reported on dragstart.\n */\nexport function dropUrl(getData: (type: string) => string, reported?: string | null): string | null {\n const read = (type: string) => { try { return getData(type) || ''; } catch { return ''; } };\n const firstLine = (text: string, skipComments: boolean) =>\n text.split(/\\r?\\n/).map(line => line.trim()).find(line => line && !(skipComments && line.startsWith('#'))) ?? '';\n return webUrl(firstLine(read('text/uri-list'), true))\n ?? webUrl(firstLine(read('text/x-moz-url'), false))\n ?? webUrl(firstLine(read('text/plain'), false))\n ?? webUrl(reported);\n}\n\n/** Drag types that may carry a link. File drags are left to the page (e.g. a composer). */\nexport function carriesLink(types: readonly string[]): boolean {\n if (types.includes('Files')) return false;\n return types.includes('text/uri-list') || types.includes('text/x-moz-url');\n}\n\nexport type DropPosition = 'top' | 'bottom' | 'left' | 'right' | 'center';\nexport type DropDirection = 'left' | 'right' | 'above' | 'below';\nexport type Placement =\n | { referenceGroup: string; direction: 'within' | DropDirection; index?: number }\n | { direction: DropDirection };\n\nconst DIRECTIONS: Record<Exclude<DropPosition, 'center'>, DropDirection> = { top: 'above', bottom: 'below', left: 'left', right: 'right' };\n\n/**\n * Where a dropped link opens. `groupId`: the group under the pointer (missing for the dock's outer\n * edges). `tabIndex`: the tab dropped on, if any. `undefined`: the default (active group, or a new\n * group in an empty dock).\n */\nexport function placementFor(target: { position: DropPosition; groupId?: string; tabIndex?: number }): Placement | undefined {\n const { position, groupId, tabIndex } = target;\n if (!groupId) return position === 'center' ? undefined : { direction: DIRECTIONS[position] };\n if (position === 'center') return tabIndex === undefined ? { referenceGroup: groupId, direction: 'within' } : { referenceGroup: groupId, direction: 'within', index: tabIndex };\n return { referenceGroup: groupId, direction: DIRECTIONS[position] };\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type RankedItem = {
|
|
2
|
+
label: string;
|
|
3
|
+
keywords?: string;
|
|
4
|
+
group: string;
|
|
5
|
+
};
|
|
6
|
+
/** Subsequence match, case-insensitive. 0 = no match; prefix/word-start/consecutive hits score higher. */
|
|
7
|
+
export declare function fuzzyScore(target: string, query: string): number;
|
|
8
|
+
/** An item's score: its label, or half of its keywords/group match. */
|
|
9
|
+
export declare function scoreItem(item: RankedItem, query: string): number;
|
|
10
|
+
/** Matching items, best first; equal scores keep their original order. `skip` items never match. */
|
|
11
|
+
export declare function rankItems<T extends RankedItem>(items: readonly T[], query: string, skip?: (item: T) => boolean): T[];
|
|
12
|
+
/** Wraps selection movement within the list. */
|
|
13
|
+
export declare const moveSelection: (index: number, step: number, length: number) => number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Command palette core: fuzzy scoring, ranking and selection movement. The app builds its own
|
|
2
|
+
// items (tabs, profiles, settings sections, site destinations); every item is navigation only.
|
|
3
|
+
/** Subsequence match, case-insensitive. 0 = no match; prefix/word-start/consecutive hits score higher. */
|
|
4
|
+
export function fuzzyScore(target, query) {
|
|
5
|
+
const t = target.toLowerCase();
|
|
6
|
+
const q = query.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
7
|
+
if (!q)
|
|
8
|
+
return 1;
|
|
9
|
+
if (!t)
|
|
10
|
+
return 0;
|
|
11
|
+
if (t.startsWith(q))
|
|
12
|
+
return 1000 - t.length;
|
|
13
|
+
const at = t.indexOf(q);
|
|
14
|
+
if (at >= 0)
|
|
15
|
+
return 600 - at - t.length / 10 + (/[\s/_-]/.test(t[at - 1] ?? ' ') ? 100 : 0);
|
|
16
|
+
let score = 0;
|
|
17
|
+
let ti = 0;
|
|
18
|
+
let previous = -2;
|
|
19
|
+
for (const char of q) {
|
|
20
|
+
if (char === ' ')
|
|
21
|
+
continue;
|
|
22
|
+
const found = t.indexOf(char, ti);
|
|
23
|
+
if (found < 0)
|
|
24
|
+
return 0;
|
|
25
|
+
score += 10;
|
|
26
|
+
if (found === previous + 1)
|
|
27
|
+
score += 15;
|
|
28
|
+
if (found === 0 || /[\s/_-]/.test(t[found - 1]))
|
|
29
|
+
score += 20;
|
|
30
|
+
previous = found;
|
|
31
|
+
ti = found + 1;
|
|
32
|
+
}
|
|
33
|
+
return Math.max(1, score - t.length / 10);
|
|
34
|
+
}
|
|
35
|
+
/** An item's score: its label, or half of its keywords/group match. */
|
|
36
|
+
export function scoreItem(item, query) {
|
|
37
|
+
const label = fuzzyScore(item.label, query);
|
|
38
|
+
const extra = Math.max(fuzzyScore(item.keywords ?? '', query), fuzzyScore(item.group, query)) * 0.5;
|
|
39
|
+
return Math.max(label, extra);
|
|
40
|
+
}
|
|
41
|
+
/** Matching items, best first; equal scores keep their original order. `skip` items never match. */
|
|
42
|
+
export function rankItems(items, query, skip = () => false) {
|
|
43
|
+
return items
|
|
44
|
+
.map((item, index) => ({ item, index, score: skip(item) ? 0 : scoreItem(item, query) }))
|
|
45
|
+
.filter(entry => entry.score > 0)
|
|
46
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
47
|
+
.map(entry => entry.item);
|
|
48
|
+
}
|
|
49
|
+
/** Wraps selection movement within the list. */
|
|
50
|
+
export const moveSelection = (index, step, length) => length === 0 ? 0 : (index + step + length) % length;
|
|
51
|
+
//# sourceMappingURL=palette.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"palette.js","sourceRoot":"","sources":["../../../shell/src/palette.ts"],"names":[],"mappings":"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","sourcesContent":["// Command palette core: fuzzy scoring, ranking and selection movement. The app builds its own\n// items (tabs, profiles, settings sections, site destinations); every item is navigation only.\n\nexport type RankedItem = { label: string; keywords?: string; group: string };\n\n/** Subsequence match, case-insensitive. 0 = no match; prefix/word-start/consecutive hits score higher. */\nexport function fuzzyScore(target: string, query: string): number {\n const t = target.toLowerCase();\n const q = query.toLowerCase().replace(/\\s+/g, ' ').trim();\n if (!q) return 1;\n if (!t) return 0;\n if (t.startsWith(q)) return 1000 - t.length;\n const at = t.indexOf(q);\n if (at >= 0) return 600 - at - t.length / 10 + (/[\\s/_-]/.test(t[at - 1] ?? ' ') ? 100 : 0);\n let score = 0;\n let ti = 0;\n let previous = -2;\n for (const char of q) {\n if (char === ' ') continue;\n const found = t.indexOf(char, ti);\n if (found < 0) return 0;\n score += 10;\n if (found === previous + 1) score += 15;\n if (found === 0 || /[\\s/_-]/.test(t[found - 1]!)) score += 20;\n previous = found;\n ti = found + 1;\n }\n return Math.max(1, score - t.length / 10);\n}\n\n/** An item's score: its label, or half of its keywords/group match. */\nexport function scoreItem(item: RankedItem, query: string): number {\n const label = fuzzyScore(item.label, query);\n const extra = Math.max(fuzzyScore(item.keywords ?? '', query), fuzzyScore(item.group, query)) * 0.5;\n return Math.max(label, extra);\n}\n\n/** Matching items, best first; equal scores keep their original order. `skip` items never match. */\nexport function rankItems<T extends RankedItem>(items: readonly T[], query: string, skip: (item: T) => boolean = () => false): T[] {\n return items\n .map((item, index) => ({ item, index, score: skip(item) ? 0 : scoreItem(item, query) }))\n .filter(entry => entry.score > 0)\n .sort((a, b) => b.score - a.score || a.index - b.index)\n .map(entry => entry.item);\n}\n\n/** Wraps selection movement within the list. */\nexport const moveSelection = (index: number, step: number, length: number) =>\n length === 0 ? 0 : (index + step + length) % length;\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PaneBox = {
|
|
2
|
+
id: string;
|
|
3
|
+
url: string;
|
|
4
|
+
profile: string;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
visible: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** `besideOf`: the source group ids whose "beside" links this group's tabs were opened for. */
|
|
10
|
+
export type GroupBox = {
|
|
11
|
+
id: string;
|
|
12
|
+
left: number;
|
|
13
|
+
top: number;
|
|
14
|
+
right: number;
|
|
15
|
+
bottom: number;
|
|
16
|
+
besideOf?: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Where a link opened "beside" a pane goes: the side pane this source opened before (so repeated
|
|
20
|
+
* links reuse one), when it is still to its right and shares some of its height; else null to split
|
|
21
|
+
* a new one. A group the user opened themselves (another feed, another account) is never taken over.
|
|
22
|
+
*/
|
|
23
|
+
export declare function besideGroup(source: GroupBox, groups: readonly GroupBox[]): string | null;
|
|
24
|
+
/** Ids of visible site panes whose navigation duplicates the leftmost site pane of the same account. */
|
|
25
|
+
export declare function redundantNavPanes(panes: readonly PaneBox[], isSiteUrl: (url: string) => boolean): Set<string>;
|
|
26
|
+
/**
|
|
27
|
+
* The `navRedundant` flag per tab for `tabs_sync`. Only visible panes are judged (a shell overlay
|
|
28
|
+
* hides them all and so changes nothing); a hidden tab keeps its last value (`memory`) so it shows
|
|
29
|
+
* up again without a flicker. Tabs that are gone are forgotten.
|
|
30
|
+
*/
|
|
31
|
+
export declare function withNavRedundant<T extends PaneBox>(boxes: readonly T[], isSiteUrl: (url: string) => boolean, memory: Map<string, boolean>): Array<T & {
|
|
32
|
+
navRedundant: boolean;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Pane geometry rules for the dock: where a link opened "beside" a pane goes, and which site panes
|
|
2
|
+
// repeat a navigation rail already on screen. Two site panes of the same account side by side (or
|
|
3
|
+
// stacked) both draw the site's rail; only the leftmost (then topmost) one needs it. Panes of
|
|
4
|
+
// different accounts keep theirs: the rail's account button is the only in-page sign of which
|
|
5
|
+
// account a pane is.
|
|
6
|
+
/**
|
|
7
|
+
* Where a link opened "beside" a pane goes: the side pane this source opened before (so repeated
|
|
8
|
+
* links reuse one), when it is still to its right and shares some of its height; else null to split
|
|
9
|
+
* a new one. A group the user opened themselves (another feed, another account) is never taken over.
|
|
10
|
+
*/
|
|
11
|
+
export function besideGroup(source, groups) {
|
|
12
|
+
let best = null;
|
|
13
|
+
for (const group of groups) {
|
|
14
|
+
if (group.id === source.id || !group.besideOf?.includes(source.id))
|
|
15
|
+
continue;
|
|
16
|
+
const overlap = Math.min(group.bottom, source.bottom) - Math.max(group.top, source.top);
|
|
17
|
+
if (group.left < source.right - 4 || overlap <= 0)
|
|
18
|
+
continue;
|
|
19
|
+
if (!best || group.left < best.left || (group.left === best.left && group.top < best.top))
|
|
20
|
+
best = group;
|
|
21
|
+
}
|
|
22
|
+
return best?.id ?? null;
|
|
23
|
+
}
|
|
24
|
+
/** Ids of visible site panes whose navigation duplicates the leftmost site pane of the same account. */
|
|
25
|
+
export function redundantNavPanes(panes, isSiteUrl) {
|
|
26
|
+
const lead = new Map();
|
|
27
|
+
const redundant = new Set();
|
|
28
|
+
const ordered = panes
|
|
29
|
+
.filter(pane => pane.visible && isSiteUrl(pane.url))
|
|
30
|
+
.sort((a, b) => a.x - b.x || a.y - b.y || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
31
|
+
for (const pane of ordered) {
|
|
32
|
+
if (lead.has(pane.profile))
|
|
33
|
+
redundant.add(pane.id);
|
|
34
|
+
else
|
|
35
|
+
lead.set(pane.profile, pane);
|
|
36
|
+
}
|
|
37
|
+
return redundant;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The `navRedundant` flag per tab for `tabs_sync`. Only visible panes are judged (a shell overlay
|
|
41
|
+
* hides them all and so changes nothing); a hidden tab keeps its last value (`memory`) so it shows
|
|
42
|
+
* up again without a flicker. Tabs that are gone are forgotten.
|
|
43
|
+
*/
|
|
44
|
+
export function withNavRedundant(boxes, isSiteUrl, memory) {
|
|
45
|
+
const redundant = redundantNavPanes(boxes, isSiteUrl);
|
|
46
|
+
const items = boxes.map(box => {
|
|
47
|
+
if (box.visible)
|
|
48
|
+
memory.set(box.id, redundant.has(box.id));
|
|
49
|
+
return { ...box, navRedundant: memory.get(box.id) ?? false };
|
|
50
|
+
});
|
|
51
|
+
for (const id of memory.keys())
|
|
52
|
+
if (!boxes.some(box => box.id === id))
|
|
53
|
+
memory.delete(id);
|
|
54
|
+
return items;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=paneModel.js.map
|