beadcyte 0.4.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 (108) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +661 -0
  3. package/README.md +386 -0
  4. package/THIRD_PARTY_NOTICES.md +56 -0
  5. package/bin/beadcyte.mjs +60 -0
  6. package/package.json +77 -0
  7. package/src/changelog-cli.mjs +105 -0
  8. package/src/changelog.mjs +196 -0
  9. package/src/cli.mjs +577 -0
  10. package/src/estimator.mjs +314 -0
  11. package/src/format.mjs +22 -0
  12. package/src/history-walk.mjs +170 -0
  13. package/src/index.mjs +5 -0
  14. package/src/mutate.mjs +193 -0
  15. package/src/projects.mjs +120 -0
  16. package/src/provenance.mjs +75 -0
  17. package/src/review-hours.mjs +117 -0
  18. package/src/roster-path.mjs +24 -0
  19. package/src/scheduler.mjs +424 -0
  20. package/src/serve.mjs +411 -0
  21. package/src/server-state.mjs +105 -0
  22. package/src/ship.mjs +178 -0
  23. package/src/stage-waits.mjs +69 -0
  24. package/src/start.mjs +111 -0
  25. package/src/stop.mjs +66 -0
  26. package/src/velocity.mjs +209 -0
  27. package/src/web/App.vue +691 -0
  28. package/src/web/app.css +54 -0
  29. package/src/web/assets/favicon.svg +12 -0
  30. package/src/web/avatar.ts +53 -0
  31. package/src/web/bead-detail.ts +240 -0
  32. package/src/web/changelog-view.ts +41 -0
  33. package/src/web/components/BeadDrawer.vue +1883 -0
  34. package/src/web/components/BeadSubGraph.vue +326 -0
  35. package/src/web/components/BeadSubGraphOverlay.vue +192 -0
  36. package/src/web/components/BeadTooltip.vue +516 -0
  37. package/src/web/components/BeadcyteMark.vue +64 -0
  38. package/src/web/components/BeadsGantt.vue +2125 -0
  39. package/src/web/components/BeadsGrid.vue +468 -0
  40. package/src/web/components/BeadsIncytes.vue +567 -0
  41. package/src/web/components/BeadsMine.vue +325 -0
  42. package/src/web/components/BeadsTable.vue +335 -0
  43. package/src/web/components/ChangelogOverlay.vue +198 -0
  44. package/src/web/components/ContextMenu.vue +386 -0
  45. package/src/web/components/ControlsPanel.vue +476 -0
  46. package/src/web/components/CostTrend.vue +206 -0
  47. package/src/web/components/FilterPopover.vue +245 -0
  48. package/src/web/components/GroupProgress.vue +274 -0
  49. package/src/web/components/LoadMeter.vue +144 -0
  50. package/src/web/components/MineRow.vue +28 -0
  51. package/src/web/components/OptionsMenu.vue +825 -0
  52. package/src/web/components/PriorityChip.vue +105 -0
  53. package/src/web/components/ScoreStrip.vue +131 -0
  54. package/src/web/components/SearchPalette.vue +210 -0
  55. package/src/web/components/ShipTrend.vue +510 -0
  56. package/src/web/components/ShortcutsOverlay.vue +164 -0
  57. package/src/web/components/Term.vue +177 -0
  58. package/src/web/components/Toast.vue +50 -0
  59. package/src/web/components/TriageMeters.vue +426 -0
  60. package/src/web/components/TypeChip.vue +96 -0
  61. package/src/web/components/Walkthrough.vue +209 -0
  62. package/src/web/components/WhatIfPanel.vue +206 -0
  63. package/src/web/components/WipBullets.vue +191 -0
  64. package/src/web/components/filter-option.ts +9 -0
  65. package/src/web/composables/url-codec.ts +136 -0
  66. package/src/web/composables/useBeadTooltip.ts +148 -0
  67. package/src/web/composables/useKeyboard.ts +97 -0
  68. package/src/web/composables/useLiveRefresh.ts +69 -0
  69. package/src/web/composables/useTheme.ts +125 -0
  70. package/src/web/composables/useUrlState.ts +208 -0
  71. package/src/web/controls-scope.ts +83 -0
  72. package/src/web/cost.ts +251 -0
  73. package/src/web/dep-headings.ts +62 -0
  74. package/src/web/economics.ts +440 -0
  75. package/src/web/env.d.ts +85 -0
  76. package/src/web/frontier.ts +208 -0
  77. package/src/web/gantt-viewport.ts +99 -0
  78. package/src/web/highlights.ts +124 -0
  79. package/src/web/index.html +46 -0
  80. package/src/web/insights.ts +107 -0
  81. package/src/web/keybindings.ts +200 -0
  82. package/src/web/load-meter.ts +72 -0
  83. package/src/web/main.ts +20 -0
  84. package/src/web/markdown.ts +14 -0
  85. package/src/web/mine.ts +137 -0
  86. package/src/web/mutations.ts +21 -0
  87. package/src/web/person.ts +102 -0
  88. package/src/web/projects-text.ts +15 -0
  89. package/src/web/projects.ts +188 -0
  90. package/src/web/refresh.ts +47 -0
  91. package/src/web/search.ts +50 -0
  92. package/src/web/shortcuts.ts +113 -0
  93. package/src/web/status-filter.ts +48 -0
  94. package/src/web/store.ts +1378 -0
  95. package/src/web/style-audit.mjs +346 -0
  96. package/src/web/styles-alt.css +111 -0
  97. package/src/web/styles-ported.css +270 -0
  98. package/src/web/subgraph.ts +362 -0
  99. package/src/web/table.ts +201 -0
  100. package/src/web/theme.ts +88 -0
  101. package/src/web/tokens.css +168 -0
  102. package/src/web/triage.ts +914 -0
  103. package/src/web/view-model.ts +717 -0
  104. package/src/web/walkthrough.ts +133 -0
  105. package/src/web/watchlist.ts +47 -0
  106. package/src/web/whatif.ts +291 -0
  107. package/src/web/window.ts +73 -0
  108. package/src/web/wip.ts +83 -0
@@ -0,0 +1,346 @@
1
+ // style-audit.mjs — resolves the token layer without a browser, so the
2
+ // distinctness of encoding colours can be asserted in tests.
3
+ //
4
+ // This exists because a named style can override encoding colours, and a
5
+ // style that quietly lands two states on the same colour is a data bug, not
6
+ // a cosmetic one. The terminal style did exactly that: its green accent
7
+ // pulled `future` to within 0.029 OKLab of `shipped`, making closed and open
8
+ // bars indistinguishable. A test is the only thing that catches that class of
9
+ // mistake before a human squints at a chart.
10
+ //
11
+ // The parser is deliberately narrow. It understands only the shapes our own
12
+ // token files use — `light-dark(a, b)`, `oklch(L% C H)` and one level of
13
+ // `var(--x)` — and throws on anything else rather than guessing, so a token
14
+ // written in a form the audit can't check fails loudly instead of silently
15
+ // escaping the guard.
16
+
17
+ import { readFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+
20
+ const DEG = Math.PI / 180;
21
+
22
+ /** Selector blocks, in source order, so later ones override earlier ones. */
23
+ function parseBlocks(css) {
24
+ const blocks = [];
25
+ // Strip comments first; they contain braces and colons.
26
+ const clean = css.replace(/\/\*[\s\S]*?\*\//g, "");
27
+ const re = /([^{}]+)\{([^{}]*)\}/g;
28
+ let m;
29
+ while ((m = re.exec(clean))) {
30
+ const selector = m[1].trim();
31
+ const decls = {};
32
+ for (const line of m[2].split(";")) {
33
+ const i = line.indexOf(":");
34
+ if (i < 0) continue;
35
+ const prop = line.slice(0, i).trim();
36
+ if (!prop.startsWith("--")) continue;
37
+ decls[prop] = line.slice(i + 1).trim();
38
+ }
39
+ if (Object.keys(decls).length) blocks.push({ selector, decls });
40
+ }
41
+ return blocks;
42
+ }
43
+
44
+ /**
45
+ * Flatten the blocks that apply to a given style, in cascade order: bare
46
+ * `:root` first, then the style's own block.
47
+ */
48
+ function declarationsFor(blocks, style) {
49
+ const out = {};
50
+ for (const b of blocks) {
51
+ // Selector LISTS, not exact strings: the token blocks are shared with
52
+ // the options menu's style previews ( ":root, [data-preview=default]" ),
53
+ // and an exact-match check silently stopped collecting the base tokens
54
+ // the moment that hook was added.
55
+ const parts = b.selector.split(",").map((x) => x.trim());
56
+ const isBase = parts.includes(":root");
57
+ const isStyle =
58
+ style !== "default" &&
59
+ parts.some((x) => x.includes(`data-style="${style}"`));
60
+ if (isBase || isStyle) Object.assign(out, b.decls);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function splitTopLevel(s) {
66
+ const parts = [];
67
+ let depth = 0;
68
+ let cur = "";
69
+ for (const ch of s) {
70
+ if (ch === "(") depth++;
71
+ if (ch === ")") depth--;
72
+ if (ch === "," && depth === 0) {
73
+ parts.push(cur.trim());
74
+ cur = "";
75
+ } else cur += ch;
76
+ }
77
+ if (cur.trim()) parts.push(cur.trim());
78
+ return parts;
79
+ }
80
+
81
+ /** Resolve a declaration to {L, C, H}. Throws on anything unrecognised. */
82
+ function resolve(value, decls, mode, seen = new Set()) {
83
+ const v = value.trim();
84
+
85
+ const varMatch = v.match(/^var\(\s*(--[a-z0-9-]+)\s*\)$/i);
86
+ if (varMatch) {
87
+ const name = varMatch[1];
88
+ if (seen.has(name)) throw new Error(`var cycle at ${name}`);
89
+ if (!(name in decls)) throw new Error(`unknown token ${name}`);
90
+ return resolve(decls[name], decls, mode, new Set([...seen, name]));
91
+ }
92
+
93
+ if (v.startsWith("light-dark(")) {
94
+ const inner = v.slice("light-dark(".length, v.lastIndexOf(")"));
95
+ const [light, dark] = splitTopLevel(inner);
96
+ return resolve(mode === "light" ? light : dark, decls, mode, seen);
97
+ }
98
+
99
+ const ok = v.match(
100
+ /^oklch\(\s*([\d.]+)%\s+([\d.]+)\s+([\d.]+)\s*\)$/i,
101
+ );
102
+ if (ok) {
103
+ return { L: Number(ok[1]) / 100, C: Number(ok[2]), H: Number(ok[3]) };
104
+ }
105
+
106
+ throw new Error(`style-audit cannot resolve: ${v}`);
107
+ }
108
+
109
+ /**
110
+ * Perceptual distance in OKLab. LCH is polar, so hue differences at low
111
+ * chroma barely matter perceptually — converting to Lab before measuring is
112
+ * what makes "two greys with different hues" correctly read as close.
113
+ */
114
+ export function distance(a, b) {
115
+ const ax = a.C * Math.cos(a.H * DEG);
116
+ const ay = a.C * Math.sin(a.H * DEG);
117
+ const bx = b.C * Math.cos(b.H * DEG);
118
+ const by = b.C * Math.sin(b.H * DEG);
119
+ return Math.hypot(a.L - b.L, ax - bx, ay - by);
120
+ }
121
+
122
+ /** OKLCH -> linear sRGB -> gamma-encoded sRGB, for compositing. */
123
+ function oklchToSrgb({ L, C, H }) {
124
+ const a = C * Math.cos(H * DEG);
125
+ const b = C * Math.sin(H * DEG);
126
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
127
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
128
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
129
+ const l = l_ ** 3, m = m_ ** 3, s = s_ ** 3;
130
+ const lin = [
131
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
132
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
133
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
134
+ ];
135
+ return lin.map((v) => {
136
+ const c = v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(Math.max(v, 0), 1 / 2.4) - 0.055;
137
+ return Math.min(1, Math.max(0, c));
138
+ });
139
+ }
140
+
141
+ function srgbToOklab([r, g, b]) {
142
+ const lin = [r, g, b].map((c) =>
143
+ c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4,
144
+ );
145
+ const [R, G, B] = lin;
146
+ const l = Math.cbrt(0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B);
147
+ const m = Math.cbrt(0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B);
148
+ const s = Math.cbrt(0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B);
149
+ return {
150
+ L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
151
+ a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
152
+ b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * What the eye actually receives: the token painted at its fill-opacity over
158
+ * the plot background, composited in sRGB the way the browser does it.
159
+ *
160
+ * Comparing raw tokens is not enough. Bar kinds are drawn from 0.35 to 0.95
161
+ * opacity over a background the style also recolours, so two kinds can sit
162
+ * far apart as tokens and converge once blended — which is exactly how
163
+ * "closed and open are both almost the same green" survived a fix that only
164
+ * separated the tokens.
165
+ */
166
+ export function composite(fg, bg, alpha) {
167
+ const f = oklchToSrgb(fg);
168
+ const b = oklchToSrgb(bg);
169
+ return srgbToOklab(f.map((c, i) => c * alpha + b[i] * (1 - alpha)));
170
+ }
171
+
172
+ /** Distance between two already-Oklab colours. */
173
+ export function labDistance(a, b) {
174
+ return Math.hypot(a.L - b.L, a.a - b.a, a.b - b.b);
175
+ }
176
+
177
+ // ── colour-vision deficiency ──────────────────────────────────────────────
178
+ //
179
+ // Machado, Oliveira & Fernandes (2009), severity 1.0: the three dichromacies
180
+ // as linear-sRGB matrices. The audit's OKLab floor measures what full colour
181
+ // vision receives; these measure what a dichromat does, and a pair told apart
182
+ // by hue alone at one lightness passes the first and collapses under the
183
+ // second — which is exactly how the default palette shipped with in-progress
184
+ // and future 0.3 apart for a deuteranope (bp-67g.26).
185
+ const CVD_MATRICES = {
186
+ protan: [0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882, -0.048116, 1.051998],
187
+ deutan: [0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182, 0.04294, 0.968881],
188
+ tritan: [1.255528, -0.076749, -0.178779, -0.078411, 0.930809, 0.147602, 0.004733, 0.691367, 0.3039],
189
+ };
190
+
191
+ export const CVD_KINDS = Object.freeze(Object.keys(CVD_MATRICES));
192
+
193
+ const srgbToLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
194
+ const linearToSrgb = (v) =>
195
+ Math.min(1, Math.max(0, v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(Math.max(v, 0), 1 / 2.4) - 0.055));
196
+
197
+ /** What a dichromat of the given kind sees for an sRGB colour, as sRGB. */
198
+ export function simulateCvd(srgb, kind) {
199
+ const m = CVD_MATRICES[kind];
200
+ if (!m) throw new Error(`unknown CVD kind ${kind}`);
201
+ const lin = srgb.map(srgbToLinear);
202
+ return [0, 1, 2].map((i) => linearToSrgb(m[i * 3] * lin[0] + m[i * 3 + 1] * lin[1] + m[i * 3 + 2] * lin[2]));
203
+ }
204
+
205
+ /** A token painted at alpha over a background, as the sRGB the eye receives. */
206
+ export function paintedSrgb(fg, bg, alpha) {
207
+ const f = oklchToSrgb(fg);
208
+ const b = oklchToSrgb(bg);
209
+ return f.map((c, i) => c * alpha + b[i] * (1 - alpha));
210
+ }
211
+
212
+ /** Separation of two sRGB colours under a simulated deficiency: deltaE in OKLab x100. */
213
+ export function cvdDistanceSrgb(a, b, kind) {
214
+ return labDistance(srgbToOklab(simulateCvd(a, kind)), srgbToOklab(simulateCvd(b, kind))) * 100;
215
+ }
216
+
217
+ /** The same for two OKLCH tokens. */
218
+ export function cvdDistance(a, b, kind) {
219
+ return cvdDistanceSrgb(oklchToSrgb(a), oklchToSrgb(b), kind);
220
+ }
221
+
222
+ /**
223
+ * Reads the .bar.kind-* rules straight out of the Gantt component, so the
224
+ * audit measures the opacities the chart actually uses. A hand-copied table
225
+ * would silently drift from the stylesheet, and a guard that tests stale
226
+ * numbers is worse than no guard.
227
+ */
228
+ export function readBarKinds(vuePath) {
229
+ const src = readFileSync(vuePath, "utf8");
230
+ const re =
231
+ /\.bar\.kind-([A-Za-z]+)\s*\{[^}]*?fill:\s*var\((--[a-z0-9-]+)\)[^}]*?\}/g;
232
+ const kinds = [];
233
+ let m;
234
+ while ((m = re.exec(src))) {
235
+ const [block, name, token] = [m[0], m[1], m[2]];
236
+ const alphaMatch = block.match(/fill-opacity:\s*([\d.]+)/);
237
+ // Bars inherit .bar's own fill-opacity when their rule omits one.
238
+ const base = src.match(/\.beads-gantt \.bar\s*\{[^}]*?fill-opacity:\s*([\d.]+)/);
239
+ kinds.push({
240
+ name,
241
+ token,
242
+ alpha: Number(alphaMatch ? alphaMatch[1] : base ? base[1] : 1),
243
+ // A kind drawn through the hatch mask is told apart by TEXTURE, not
244
+ // hue (bp-6cg's wait segments): the painted-distance checks compare
245
+ // only unhatched kinds, and a separate check holds hatched kinds to
246
+ // the texture contract.
247
+ hatched: /mask:\s*url\(#beads-gantt-synthetic-mask\)/.test(block),
248
+ });
249
+ }
250
+ if (!kinds.length) throw new Error("no .bar.kind-* rules found — parser stale");
251
+ return kinds;
252
+ }
253
+
254
+ /**
255
+ * How a synthetic-assignee bar is drawn, read from the component the way
256
+ * readBarKinds is: the `.bar.synthetic` rule's declarations, plus the hatch
257
+ * geometry and bar height the template's mask is built from.
258
+ *
259
+ * Two things could quietly defeat the hatch (bp-67g.49). A `fill` or
260
+ * `fill-opacity` on `.bar.synthetic` would make synthetic a colour state the
261
+ * painted-kinds check never sees, because readBarKinds reads only the
262
+ * `.bar.kind-*` rules. And stripes finer than the bar is tall stop being
263
+ * stripes: at a pitch the eye cannot resolve, a hatched bar is a paler bar —
264
+ * the third opacity level the texture exists to avoid.
265
+ */
266
+ export function readSyntheticTreatment(vuePath) {
267
+ const src = readFileSync(vuePath, "utf8");
268
+ const rule = src.match(/\.beads-gantt \.bar\.synthetic\s*\{([^}]*)\}/);
269
+ if (!rule) throw new Error("no .bar.synthetic rule found — parser stale");
270
+ const declarations = {};
271
+ for (const line of rule[1].split(";")) {
272
+ const i = line.indexOf(":");
273
+ if (i < 0) continue;
274
+ declarations[line.slice(0, i).trim()] = line.slice(i + 1).trim();
275
+ }
276
+ const num = (re, what) => {
277
+ const m = src.match(re);
278
+ if (!m) throw new Error(`${what} not found in ${vuePath} — parser stale`);
279
+ return Number(m[1]);
280
+ };
281
+ const rowH = num(/const ROW_H = (\d+);/, "ROW_H");
282
+ const barInset = num(/const BAR_H = ROW_H - (\d+);/, "BAR_H");
283
+ return {
284
+ declarations,
285
+ pitch: num(/const HATCH_PITCH = (\d+(?:\.\d+)?);/, "HATCH_PITCH"),
286
+ stripe: num(/const HATCH_STRIPE = (\d+(?:\.\d+)?);/, "HATCH_STRIPE"),
287
+ barHeight: rowH - barInset,
288
+ };
289
+ }
290
+
291
+ /**
292
+ * The stylesheets the app actually loads, in load order, read out of main.ts
293
+ * rather than listed by hand. A style declared in a file the audit does not
294
+ * read is invisible to every check in the suite while the suite stays green;
295
+ * deriving the list from the import graph closes that hole (bp-jak).
296
+ */
297
+ export function readStyleSheets(mainTsPath) {
298
+ const src = readFileSync(mainTsPath, "utf8");
299
+ const dir = dirname(mainTsPath);
300
+ const re = /^import\s+"(\.\/[^"]+\.css)";/gm;
301
+ const out = [];
302
+ let m;
303
+ while ((m = re.exec(src))) out.push(join(dir, m[1]));
304
+ if (!out.length) throw new Error(`no css imports found in ${mainTsPath} — parser stale`);
305
+ return out;
306
+ }
307
+
308
+ /**
309
+ * The colour token each variant of a chip component paints its text with,
310
+ * read from the component the way readBarKinds reads bar fills: every rule
311
+ * whose selector matches `selectorRe` and declares `color: var(--c-…)`.
312
+ * Returns [{ variant, token }] in source order, so an audit group can be
313
+ * built from what the chip renders rather than from a list that drifts.
314
+ */
315
+ export function readChipInks(vuePath, selectorRe) {
316
+ const src = readFileSync(vuePath, "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
317
+ const out = [];
318
+ const re = new RegExp(`(${selectorRe.source})\\s*\\{([^}]*)\\}`, "g");
319
+ let m;
320
+ while ((m = re.exec(src))) {
321
+ const color = m[m.length - 1].match(/(?:^|;)\s*color:\s*var\((--[a-z0-9-]+)\)/);
322
+ if (color) out.push({ variant: m[1].trim(), token: color[1] });
323
+ }
324
+ if (!out.length) throw new Error(`no chip ink rules matched in ${vuePath} — parser stale`);
325
+ return out;
326
+ }
327
+
328
+ export function loadTokens(paths) {
329
+ const blocks = paths.flatMap((p) => parseBlocks(readFileSync(p, "utf8")));
330
+ return {
331
+ /** Resolved value of one token under a style + mode. */
332
+ get(token, style, mode) {
333
+ const decls = declarationsFor(blocks, style);
334
+ if (!(token in decls)) throw new Error(`unknown token ${token}`);
335
+ return resolve(decls[token], decls, mode);
336
+ },
337
+ styles() {
338
+ const found = new Set(["default"]);
339
+ for (const b of blocks) {
340
+ const m = b.selector.match(/data-style="([a-z0-9-]+)"/i);
341
+ if (m) found.add(m[1]);
342
+ }
343
+ return [...found];
344
+ },
345
+ };
346
+ }
@@ -0,0 +1,111 @@
1
+ /* styles-alt.css — alternate palette families.
2
+ *
3
+ * Loaded AFTER tokens.css so `:root[data-style=...]` reliably beats bare
4
+ * `:root` — a pseudo-class plus an attribute out-specifies a pseudo-class
5
+ * alone, but only wins the tie if it comes later.
6
+ *
7
+ * Each family redefines the same tokens every component already reads, so a
8
+ * whole-app reskin costs no per-component changes. Each supplies its own
9
+ * light-dark() pair, so every family respects the light/dark toggle
10
+ * independently rather than being dark-only.
11
+ *
12
+ * These two are beadcyte's own alternatives; the ports of other editors'
13
+ * palettes live in styles-ported.css. They also prove the layer is real: if
14
+ * the switcher only ever showed the default, nothing would catch it being
15
+ * accidentally coupled to the default's values.
16
+ *
17
+ * A style MAY restyle the chart's encoding colours, but only by declaring
18
+ * the whole set deliberately — never by side effect. That distinction is the
19
+ * whole lesson of the terminal bug: it overrode --c-accent alone, which
20
+ * `future` happened to alias, pulling future to within 0.029 OKLab of
21
+ * shipped and making closed and open bars indistinguishable.
22
+ *
23
+ * style-audit.test.mjs enforces it: every pair of colours inside an encoding
24
+ * group must stay at least MIN_DISTINCT apart in every style and mode. A
25
+ * style that collapses two states fails the suite instead of shipping.
26
+ *
27
+ * Each block also matches [data-preview="<name>"], so the options menu can
28
+ * render a swatch in a family's colours while a different family is active.
29
+ * Custom properties inherit, so declaring them on the swatch container is
30
+ * enough — and sharing the selector means a preview can never drift from
31
+ * the style it claims to show.
32
+ */
33
+
34
+ :root[data-style="synthwave"],
35
+ [data-preview="synthwave"] {
36
+ /* Encoding set, declared in full. Neon hues, still mutually separated:
37
+ teal done, violet in flight, hot pink ahead, muted mauve for closed
38
+ without a ship, and a red today rule that reads as a rule and not as
39
+ another bar. */
40
+ --c-shipped: light-dark(oklch(54.6% 0.094 190.0), oklch(79.9% 0.138 190.0));
41
+ --c-in-progress: light-dark(oklch(44.6% 0.200 275.0), oklch(70.0% 0.158 275.0));
42
+ --c-future: light-dark(oklch(61.2% 0.240 343.0), oklch(61.7% 0.250 343.0));
43
+ --c-backlog: light-dark(oklch(61.2% 0.240 343.0), oklch(61.7% 0.250 343.0));
44
+ --c-closed-noship: light-dark(oklch(37.5% 0.045 310.0), oklch(70.9% 0.045 310.0));
45
+ /* Yellow: distinct from teal shipped, violet review and pink future,
46
+ and reads as a live cursor rather than an error. */
47
+ --c-today: light-dark(oklch(62.0% 0.128 95.0), oklch(84.9% 0.174 95.0));
48
+
49
+ --c-bg: light-dark(oklch(96% 0.020 305), oklch(15.5% 0.062 296));
50
+ --c-panel: light-dark(oklch(92.5% 0.034 305), oklch(20.5% 0.070 300));
51
+ --c-panel-2: light-dark(oklch(88.5% 0.046 305), oklch(25.0% 0.082 298));
52
+ --c-border: light-dark(oklch(83% 0.055 305), oklch(33.5% 0.105 306));
53
+ --c-border-strong: light-dark(oklch(70% 0.085 305), oklch(45.0% 0.140 306));
54
+ --c-text: light-dark(oklch(28% 0.090 305), oklch(95% 0.031 304));
55
+ --c-dim: light-dark(oklch(48% 0.070 305), oklch(72% 0.110 311));
56
+ --c-dim-bright: light-dark(oklch(38% 0.080 305), oklch(85% 0.070 308));
57
+
58
+ /* Economics phases, in the family's own hues. Still mutually separated —
59
+ style-audit.test.mjs checks these per style, same as the bar kinds. */
60
+ --c-phase-worked: light-dark(oklch(46.5% 0.082 190.0), oklch(78.4% 0.136 190.0));
61
+ --c-phase-review: light-dark(oklch(54.2% 0.200 275.0), oklch(69.9% 0.158 275.0));
62
+ --c-phase-waiting: light-dark(oklch(66.0% 0.240 343.0), oklch(68.9% 0.250 343.0));
63
+ --c-phase-other: light-dark(oklch(74.0% 0.030 310.0), oklch(45.0% 0.060 310.0));
64
+
65
+ --c-accent: light-dark(oklch(52% 0.240 343), oklch(68.4% 0.270 343));
66
+ --c-epic: light-dark(oklch(55% 0.220 320), oklch(72% 0.230 320));
67
+ }
68
+
69
+ :root[data-style="terminal"],
70
+ [data-preview="terminal"] {
71
+ /* Encoding set, declared in full. A phosphor palette, but green alone
72
+ cannot carry five states, so the hues fan out: green done, amber in
73
+ flight, cyan ahead, dim green-grey for closed without a ship, red rule
74
+ for today. */
75
+ --c-shipped: light-dark(oklch(36.0% 0.118 145.0), oklch(89.5% 0.190 145.0));
76
+ --c-in-progress: light-dark(oklch(60.0% 0.124 80.0), oklch(78.3% 0.150 80.0));
77
+ --c-future: light-dark(oklch(43.4% 0.074 200.0), oklch(76.0% 0.120 200.0));
78
+ --c-backlog: light-dark(oklch(43.4% 0.074 200.0), oklch(76.0% 0.120 200.0));
79
+ --c-closed-noship: light-dark(oklch(57.9% 0.030 150.0), oklch(63.8% 0.035 150.0));
80
+ /* Magenta: the one hue a phosphor palette of green, amber and cyan
81
+ leaves free, so the now-rule never reads as another bar. */
82
+ --c-today: light-dark(oklch(52.0% 0.228 340.0), oklch(66.5% 0.260 340.0));
83
+
84
+ --c-bg: light-dark(oklch(97% 0.012 135), oklch(14% 0.028 150));
85
+ --c-panel: light-dark(oklch(94% 0.020 135), oklch(18% 0.036 150));
86
+ --c-panel-2: light-dark(oklch(90% 0.028 135), oklch(22% 0.044 150));
87
+ --c-border: light-dark(oklch(84% 0.040 135), oklch(30% 0.058 150));
88
+ --c-border-strong: light-dark(oklch(72% 0.060 135), oklch(42% 0.080 150));
89
+ --c-text: light-dark(oklch(30% 0.090 145), oklch(90% 0.130 145));
90
+ --c-dim: light-dark(oklch(50% 0.080 145), oklch(65% 0.100 145));
91
+ --c-dim-bright: light-dark(oklch(40% 0.090 145), oklch(78% 0.120 145));
92
+
93
+ /* Economics phases in phosphor. Waiting is ~95% of a typical bar, so it
94
+ carries the family's identity: dim idle green, not the amber the default
95
+ uses — otherwise the whole bar looks unthemed however the two thin
96
+ segments beside it are coloured. Worked is the bright active green and
97
+ review the amber accent a real terminal would use for attention. */
98
+ --c-phase-waiting: light-dark(oklch(66.0% 0.110 150.0), oklch(52.1% 0.110 150.0));
99
+ --c-phase-worked: light-dark(oklch(48.0% 0.152 145.0), oklch(88.0% 0.200 145.0));
100
+ --c-phase-review: light-dark(oklch(62.1% 0.130 80.0), oklch(79.9% 0.150 80.0));
101
+ --c-phase-other: light-dark(oklch(74.0% 0.020 150.0), oklch(40.0% 0.040 150.0));
102
+
103
+ --c-accent: light-dark(oklch(48% 0.150 145), oklch(78% 0.180 145));
104
+ /* Cyan-teal, held clear of the dim inks it shares the label column and the
105
+ type chips with; the previous teal sat under the audit floor against
106
+ them in both modes (bp-jak). */
107
+ --c-epic: light-dark(oklch(35.0% 0.060 200.0), oklch(80.5% 0.140 182.0));
108
+
109
+ --font-body: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
110
+ monospace;
111
+ }