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.
- package/CHANGELOG.md +73 -0
- package/LICENSE +661 -0
- package/README.md +386 -0
- package/THIRD_PARTY_NOTICES.md +56 -0
- package/bin/beadcyte.mjs +60 -0
- package/package.json +77 -0
- package/src/changelog-cli.mjs +105 -0
- package/src/changelog.mjs +196 -0
- package/src/cli.mjs +577 -0
- package/src/estimator.mjs +314 -0
- package/src/format.mjs +22 -0
- package/src/history-walk.mjs +170 -0
- package/src/index.mjs +5 -0
- package/src/mutate.mjs +193 -0
- package/src/projects.mjs +120 -0
- package/src/provenance.mjs +75 -0
- package/src/review-hours.mjs +117 -0
- package/src/roster-path.mjs +24 -0
- package/src/scheduler.mjs +424 -0
- package/src/serve.mjs +411 -0
- package/src/server-state.mjs +105 -0
- package/src/ship.mjs +178 -0
- package/src/stage-waits.mjs +69 -0
- package/src/start.mjs +111 -0
- package/src/stop.mjs +66 -0
- package/src/velocity.mjs +209 -0
- package/src/web/App.vue +691 -0
- package/src/web/app.css +54 -0
- package/src/web/assets/favicon.svg +12 -0
- package/src/web/avatar.ts +53 -0
- package/src/web/bead-detail.ts +240 -0
- package/src/web/changelog-view.ts +41 -0
- package/src/web/components/BeadDrawer.vue +1883 -0
- package/src/web/components/BeadSubGraph.vue +326 -0
- package/src/web/components/BeadSubGraphOverlay.vue +192 -0
- package/src/web/components/BeadTooltip.vue +516 -0
- package/src/web/components/BeadcyteMark.vue +64 -0
- package/src/web/components/BeadsGantt.vue +2125 -0
- package/src/web/components/BeadsGrid.vue +468 -0
- package/src/web/components/BeadsIncytes.vue +567 -0
- package/src/web/components/BeadsMine.vue +325 -0
- package/src/web/components/BeadsTable.vue +335 -0
- package/src/web/components/ChangelogOverlay.vue +198 -0
- package/src/web/components/ContextMenu.vue +386 -0
- package/src/web/components/ControlsPanel.vue +476 -0
- package/src/web/components/CostTrend.vue +206 -0
- package/src/web/components/FilterPopover.vue +245 -0
- package/src/web/components/GroupProgress.vue +274 -0
- package/src/web/components/LoadMeter.vue +144 -0
- package/src/web/components/MineRow.vue +28 -0
- package/src/web/components/OptionsMenu.vue +825 -0
- package/src/web/components/PriorityChip.vue +105 -0
- package/src/web/components/ScoreStrip.vue +131 -0
- package/src/web/components/SearchPalette.vue +210 -0
- package/src/web/components/ShipTrend.vue +510 -0
- package/src/web/components/ShortcutsOverlay.vue +164 -0
- package/src/web/components/Term.vue +177 -0
- package/src/web/components/Toast.vue +50 -0
- package/src/web/components/TriageMeters.vue +426 -0
- package/src/web/components/TypeChip.vue +96 -0
- package/src/web/components/Walkthrough.vue +209 -0
- package/src/web/components/WhatIfPanel.vue +206 -0
- package/src/web/components/WipBullets.vue +191 -0
- package/src/web/components/filter-option.ts +9 -0
- package/src/web/composables/url-codec.ts +136 -0
- package/src/web/composables/useBeadTooltip.ts +148 -0
- package/src/web/composables/useKeyboard.ts +97 -0
- package/src/web/composables/useLiveRefresh.ts +69 -0
- package/src/web/composables/useTheme.ts +125 -0
- package/src/web/composables/useUrlState.ts +208 -0
- package/src/web/controls-scope.ts +83 -0
- package/src/web/cost.ts +251 -0
- package/src/web/dep-headings.ts +62 -0
- package/src/web/economics.ts +440 -0
- package/src/web/env.d.ts +85 -0
- package/src/web/frontier.ts +208 -0
- package/src/web/gantt-viewport.ts +99 -0
- package/src/web/highlights.ts +124 -0
- package/src/web/index.html +46 -0
- package/src/web/insights.ts +107 -0
- package/src/web/keybindings.ts +200 -0
- package/src/web/load-meter.ts +72 -0
- package/src/web/main.ts +20 -0
- package/src/web/markdown.ts +14 -0
- package/src/web/mine.ts +137 -0
- package/src/web/mutations.ts +21 -0
- package/src/web/person.ts +102 -0
- package/src/web/projects-text.ts +15 -0
- package/src/web/projects.ts +188 -0
- package/src/web/refresh.ts +47 -0
- package/src/web/search.ts +50 -0
- package/src/web/shortcuts.ts +113 -0
- package/src/web/status-filter.ts +48 -0
- package/src/web/store.ts +1378 -0
- package/src/web/style-audit.mjs +346 -0
- package/src/web/styles-alt.css +111 -0
- package/src/web/styles-ported.css +270 -0
- package/src/web/subgraph.ts +362 -0
- package/src/web/table.ts +201 -0
- package/src/web/theme.ts +88 -0
- package/src/web/tokens.css +168 -0
- package/src/web/triage.ts +914 -0
- package/src/web/view-model.ts +717 -0
- package/src/web/walkthrough.ts +133 -0
- package/src/web/watchlist.ts +47 -0
- package/src/web/whatif.ts +291 -0
- package/src/web/window.ts +73 -0
- 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
|
+
}
|