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
package/src/web/table.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// table.ts — rows and sorting for the table view (bp-67g.44).
|
|
2
|
+
//
|
|
3
|
+
// The table is the surface for scanning and sorting rather than for seeing
|
|
4
|
+
// time: one row per bead with everything the app knows about it, every
|
|
5
|
+
// column sortable. Membership comes from the SAME filtered, windowed view
|
|
6
|
+
// model the grid draws — one definition of "what is in view" — with the
|
|
7
|
+
// Gantt's per-group truncation lifted, because dropping rows is a drawing
|
|
8
|
+
// affordance and a table that silently loses them is worse than a long one.
|
|
9
|
+
//
|
|
10
|
+
// The derived columns are read through the same functions the drawer and the
|
|
11
|
+
// tooltip use (buildBeadDetail for estimate and finish, the triage map for
|
|
12
|
+
// the score), so a number here is the number there.
|
|
13
|
+
//
|
|
14
|
+
// DEFAULT SORT (recorded decision): triage score, descending. The table's
|
|
15
|
+
// job is "what next", and the score is the app's one answer to that; bead id
|
|
16
|
+
// is more predictable but says nothing. Ties break on id ascending so the
|
|
17
|
+
// order is stable from load to load. Pure, so all of it is testable.
|
|
18
|
+
|
|
19
|
+
import type { ViewModel } from "./view-model";
|
|
20
|
+
import type { BeadDetail } from "./bead-detail";
|
|
21
|
+
import type { TriageResult } from "./triage";
|
|
22
|
+
|
|
23
|
+
export type TableSortKey =
|
|
24
|
+
| "id"
|
|
25
|
+
| "title"
|
|
26
|
+
| "type"
|
|
27
|
+
| "priority"
|
|
28
|
+
| "status"
|
|
29
|
+
| "assignee"
|
|
30
|
+
| "labels"
|
|
31
|
+
| "estimate"
|
|
32
|
+
| "finish"
|
|
33
|
+
| "triage"
|
|
34
|
+
| "group";
|
|
35
|
+
|
|
36
|
+
export type SortDir = "asc" | "desc";
|
|
37
|
+
|
|
38
|
+
export interface TableSort {
|
|
39
|
+
key: TableSortKey;
|
|
40
|
+
dir: SortDir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TableRow {
|
|
44
|
+
id: string;
|
|
45
|
+
title: string;
|
|
46
|
+
type: string;
|
|
47
|
+
priority: number;
|
|
48
|
+
status: string;
|
|
49
|
+
assignee: string | null;
|
|
50
|
+
/** The scheduler inferred the assignee; never to be shown as recorded fact. */
|
|
51
|
+
synthetic: boolean;
|
|
52
|
+
labels: string[];
|
|
53
|
+
estimateDays: number | null;
|
|
54
|
+
/** Days from today, for sorting; the date is for showing. */
|
|
55
|
+
finishDay: number | null;
|
|
56
|
+
finishDate: Date | null;
|
|
57
|
+
/** The finish is the scheduling horizon, a bound rather than a projection. */
|
|
58
|
+
beyondHorizon: boolean;
|
|
59
|
+
triage: number | null;
|
|
60
|
+
groupId: string;
|
|
61
|
+
group: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TableColumn {
|
|
65
|
+
key: TableSortKey;
|
|
66
|
+
label: string;
|
|
67
|
+
/** The direction a fresh click on this column sorts by. */
|
|
68
|
+
natural: SortDir;
|
|
69
|
+
numeric?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* In display order. Each column's natural direction is the one a reader
|
|
74
|
+
* wants first: P0 before P4, the soonest finish first, the highest score
|
|
75
|
+
* first, everything textual A to Z.
|
|
76
|
+
*/
|
|
77
|
+
export const COLUMNS: readonly TableColumn[] = [
|
|
78
|
+
{ key: "id", label: "id", natural: "asc" },
|
|
79
|
+
{ key: "title", label: "title", natural: "asc" },
|
|
80
|
+
{ key: "type", label: "type", natural: "asc" },
|
|
81
|
+
// Chips, not digits: left-aligned like its cells, though it sorts as a number.
|
|
82
|
+
{ key: "priority", label: "pri", natural: "asc" },
|
|
83
|
+
{ key: "status", label: "status", natural: "asc" },
|
|
84
|
+
{ key: "assignee", label: "assignee", natural: "asc" },
|
|
85
|
+
{ key: "labels", label: "labels", natural: "asc" },
|
|
86
|
+
{ key: "estimate", label: "estimate", natural: "asc", numeric: true },
|
|
87
|
+
{ key: "finish", label: "finish", natural: "asc", numeric: true },
|
|
88
|
+
{ key: "triage", label: "triage", natural: "desc", numeric: true },
|
|
89
|
+
{ key: "group", label: "group", natural: "asc" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
export const DEFAULT_SORT: TableSort = { key: "triage", dir: "desc" };
|
|
93
|
+
|
|
94
|
+
/** Lifecycle order for the status column; anything unknown sorts after, A to Z. */
|
|
95
|
+
const STATUS_ORDER = ["open", "pinned", "in_progress", "in_review", "blocked", "deferred", "closed", "tombstone"];
|
|
96
|
+
|
|
97
|
+
export interface TableSources {
|
|
98
|
+
detailFor: (id: string) => Pick<BeadDetail, "estimateDays" | "scheduled"> | null;
|
|
99
|
+
triage: ReadonlyMap<string, TriageResult>;
|
|
100
|
+
labelsFor: (id: string) => readonly string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One row per view-model row, in view-model order (sorting comes after). */
|
|
104
|
+
export function buildTableRows(vm: ViewModel, src: TableSources): TableRow[] {
|
|
105
|
+
const groupLabel = new Map(vm.groups.map((g) => [g.id, g.label] as const));
|
|
106
|
+
return vm.rows.map((r) => {
|
|
107
|
+
const d = src.detailFor(r.beadId);
|
|
108
|
+
const s = d?.scheduled ?? null;
|
|
109
|
+
return {
|
|
110
|
+
id: r.beadId,
|
|
111
|
+
title: r.title,
|
|
112
|
+
type: r.issueType,
|
|
113
|
+
priority: r.priority,
|
|
114
|
+
status: r.status,
|
|
115
|
+
assignee: r.assignee,
|
|
116
|
+
synthetic: r.syntheticAssignee,
|
|
117
|
+
labels: [...src.labelsFor(r.beadId)],
|
|
118
|
+
estimateDays: d?.estimateDays ?? null,
|
|
119
|
+
finishDay: s ? s.endDay : null,
|
|
120
|
+
finishDate: s ? s.endDate : null,
|
|
121
|
+
beyondHorizon: s?.beyondHorizon ?? false,
|
|
122
|
+
triage: src.triage.get(r.beadId)?.score ?? null,
|
|
123
|
+
groupId: r.groupId,
|
|
124
|
+
group: r.groupId.startsWith("__") ? "" : groupLabel.get(r.groupId) ?? r.groupId,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The sort a click produces: the same column flips, a new one starts natural. */
|
|
130
|
+
export function toggleSort(current: TableSort, key: TableSortKey): TableSort {
|
|
131
|
+
if (current.key === key) return { key, dir: current.dir === "asc" ? "desc" : "asc" };
|
|
132
|
+
return { key, dir: COLUMNS.find((c) => c.key === key)?.natural ?? "asc" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type Keyed = { v: number | string | null; row: TableRow };
|
|
136
|
+
|
|
137
|
+
function sortValue(row: TableRow, key: TableSortKey): number | string | null {
|
|
138
|
+
switch (key) {
|
|
139
|
+
case "id":
|
|
140
|
+
return row.id;
|
|
141
|
+
case "title":
|
|
142
|
+
return row.title.toLowerCase();
|
|
143
|
+
case "type":
|
|
144
|
+
return row.type;
|
|
145
|
+
case "priority":
|
|
146
|
+
return row.priority;
|
|
147
|
+
case "status": {
|
|
148
|
+
const i = STATUS_ORDER.indexOf(row.status);
|
|
149
|
+
return i >= 0 ? i : STATUS_ORDER.length;
|
|
150
|
+
}
|
|
151
|
+
case "assignee":
|
|
152
|
+
return row.assignee ? row.assignee.toLowerCase() : null;
|
|
153
|
+
case "labels":
|
|
154
|
+
return row.labels.length ? row.labels.join(",").toLowerCase() : null;
|
|
155
|
+
case "estimate":
|
|
156
|
+
return row.estimateDays;
|
|
157
|
+
case "finish":
|
|
158
|
+
return row.finishDay;
|
|
159
|
+
case "triage":
|
|
160
|
+
return row.triage;
|
|
161
|
+
case "group":
|
|
162
|
+
return row.group || null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Stable sort on one column. Rows with nothing in the column sort LAST in
|
|
168
|
+
* either direction — an unassigned bead is not "less than" alice on the way
|
|
169
|
+
* up and "more than" her on the way down — and ties break on id ascending
|
|
170
|
+
* so two loads of the same data agree.
|
|
171
|
+
*/
|
|
172
|
+
export function sortRows(rows: readonly TableRow[], sort: TableSort): TableRow[] {
|
|
173
|
+
const keyed: Keyed[] = rows.map((row) => ({ v: sortValue(row, sort.key), row }));
|
|
174
|
+
const sign = sort.dir === "asc" ? 1 : -1;
|
|
175
|
+
// Unknown statuses share one rank; fall through to the text so they still order.
|
|
176
|
+
const tieText = sort.key === "status";
|
|
177
|
+
keyed.sort((a, b) => {
|
|
178
|
+
if (a.v === null && b.v === null) return a.row.id.localeCompare(b.row.id);
|
|
179
|
+
if (a.v === null) return 1;
|
|
180
|
+
if (b.v === null) return -1;
|
|
181
|
+
let c = 0;
|
|
182
|
+
if (typeof a.v === "number" && typeof b.v === "number") c = a.v - b.v;
|
|
183
|
+
else c = String(a.v).localeCompare(String(b.v));
|
|
184
|
+
if (c === 0 && tieText) c = a.row.status.localeCompare(b.row.status);
|
|
185
|
+
if (c !== 0) return c * sign;
|
|
186
|
+
return a.row.id.localeCompare(b.row.id);
|
|
187
|
+
});
|
|
188
|
+
return keyed.map((k) => k.row);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** "sorted by triage, highest first" — for the footer. */
|
|
192
|
+
export function describeSort(sort: TableSort): string {
|
|
193
|
+
const col = COLUMNS.find((c) => c.key === sort.key);
|
|
194
|
+
const label = col?.label ?? sort.key;
|
|
195
|
+
// Priority's number runs the other way from its meaning: ascending is P0
|
|
196
|
+
// first, which is the HIGHEST priority. Say the rank, not the number.
|
|
197
|
+
if (sort.key === "priority") return `sorted by ${label}, ${sort.dir === "asc" ? "P0" : "P4"} first`;
|
|
198
|
+
if (sort.key === "finish") return `sorted by ${label}, ${sort.dir === "asc" ? "soonest" : "latest"} first`;
|
|
199
|
+
if (col?.numeric) return `sorted by ${label}, ${sort.dir === "asc" ? "lowest" : "highest"} first`;
|
|
200
|
+
return `sorted by ${label}, ${sort.dir === "asc" ? "A to Z" : "Z to A"}`;
|
|
201
|
+
}
|
package/src/web/theme.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// theme.ts — theme/style preference values and their storage format.
|
|
2
|
+
//
|
|
3
|
+
// Pure: no DOM, no localStorage, no Vue. composables/useTheme.ts owns the
|
|
4
|
+
// reactive wiring and the root-element attributes; this owns what counts as
|
|
5
|
+
// a valid preference, so the rules are testable.
|
|
6
|
+
|
|
7
|
+
/** 'system' follows the OS; the others force a mode via [data-theme]. */
|
|
8
|
+
export type ThemeMode = "system" | "light" | "dark";
|
|
9
|
+
|
|
10
|
+
/** Palette family. 'default' is the app's own dark-derived scheme; the rest
|
|
11
|
+
* live in styles-alt.css (ours) and styles-ported.css (ports + High Contrast). */
|
|
12
|
+
export type StyleName =
|
|
13
|
+
| "default"
|
|
14
|
+
| "high-contrast"
|
|
15
|
+
| "catppuccin"
|
|
16
|
+
| "dracula"
|
|
17
|
+
| "github"
|
|
18
|
+
| "gruvbox"
|
|
19
|
+
| "nord"
|
|
20
|
+
| "one"
|
|
21
|
+
| "solarized"
|
|
22
|
+
| "synthwave"
|
|
23
|
+
| "terminal"
|
|
24
|
+
| "tokyonight";
|
|
25
|
+
|
|
26
|
+
export const THEME_MODES: readonly ThemeMode[] = ["system", "light", "dark"];
|
|
27
|
+
/** Picker order: the default, then the accessibility choice, then the rest A–Z. */
|
|
28
|
+
export const STYLE_NAMES: readonly StyleName[] = [
|
|
29
|
+
"default",
|
|
30
|
+
"high-contrast",
|
|
31
|
+
"catppuccin",
|
|
32
|
+
"dracula",
|
|
33
|
+
"github",
|
|
34
|
+
"gruvbox",
|
|
35
|
+
"nord",
|
|
36
|
+
"one",
|
|
37
|
+
"solarized",
|
|
38
|
+
"synthwave",
|
|
39
|
+
"terminal",
|
|
40
|
+
"tokyonight",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** What the picker prints. Ids stay lowercase single tokens for the URL and storage. */
|
|
44
|
+
export const STYLE_LABELS: Readonly<Record<StyleName, string>> = {
|
|
45
|
+
default: "Default",
|
|
46
|
+
"high-contrast": "High Contrast",
|
|
47
|
+
catppuccin: "Catppuccin",
|
|
48
|
+
dracula: "Dracula",
|
|
49
|
+
github: "GitHub",
|
|
50
|
+
gruvbox: "Gruvbox",
|
|
51
|
+
nord: "Nord",
|
|
52
|
+
one: "One",
|
|
53
|
+
solarized: "Solarized",
|
|
54
|
+
synthwave: "Synthwave",
|
|
55
|
+
terminal: "Terminal",
|
|
56
|
+
tokyonight: "Tokyo Night",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const THEME_STORAGE_KEY = "beadcyte:theme";
|
|
60
|
+
export const STYLE_STORAGE_KEY = "beadcyte:style";
|
|
61
|
+
|
|
62
|
+
export function parseThemeMode(raw: string | null | undefined): ThemeMode {
|
|
63
|
+
return raw && (THEME_MODES as readonly string[]).includes(raw)
|
|
64
|
+
? (raw as ThemeMode)
|
|
65
|
+
: "system";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseStyleName(raw: string | null | undefined): StyleName {
|
|
69
|
+
return raw && (STYLE_NAMES as readonly string[]).includes(raw)
|
|
70
|
+
? (raw as StyleName)
|
|
71
|
+
: "default";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What the app is actually showing. 'system' has no answer of its own — it
|
|
76
|
+
* defers to the OS — so the caller supplies what the OS currently prefers.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveIsDark(mode: ThemeMode, prefersDark: boolean): boolean {
|
|
79
|
+
if (mode === "dark") return true;
|
|
80
|
+
if (mode === "light") return false;
|
|
81
|
+
return prefersDark;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Next mode in the cycle, for a single-button toggle. */
|
|
85
|
+
export function nextThemeMode(mode: ThemeMode): ThemeMode {
|
|
86
|
+
const i = THEME_MODES.indexOf(mode);
|
|
87
|
+
return THEME_MODES[(i + 1) % THEME_MODES.length];
|
|
88
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* tokens.css — the app's one colour/type/scalar layer.
|
|
2
|
+
*
|
|
3
|
+
* Loaded before app.css and every component's <style>, so anything here can
|
|
4
|
+
* be overridden downstream; loaded before styles-alt.css, so a named style
|
|
5
|
+
* can override it.
|
|
6
|
+
*
|
|
7
|
+
* Three things make this shorter than it looks:
|
|
8
|
+
*
|
|
9
|
+
* 1. `light-dark()` gives one declaration per token instead of the four-way
|
|
10
|
+
* :root / prefers-color-scheme / [data-theme=light] / [data-theme=dark]
|
|
11
|
+
* duplication. `color-scheme` below is what makes it resolve.
|
|
12
|
+
* 2. Colours are authored in OKLCH, so lightness tracks perceived
|
|
13
|
+
* brightness. That matters here because the light-mode values keep each
|
|
14
|
+
* dark hue and only move L — a shift that stays a consistent, comparable
|
|
15
|
+
* step across hues in OKLCH and does not in HSL.
|
|
16
|
+
* 3. Variants are derived once, generically, with relative colour syntax
|
|
17
|
+
* rather than hand-authored per token per theme.
|
|
18
|
+
*
|
|
19
|
+
* The dark values ARE the previous hardcoded palette, converted. Dark mode
|
|
20
|
+
* is meant to look unchanged; any visible difference in dark is a bug.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/* Makes light-dark() resolve, and themes native UI (scrollbars, form
|
|
24
|
+
controls) to match. Unset [data-theme] = follow the OS. */
|
|
25
|
+
:root {
|
|
26
|
+
color-scheme: light dark;
|
|
27
|
+
}
|
|
28
|
+
:root[data-theme="light"] {
|
|
29
|
+
color-scheme: light;
|
|
30
|
+
}
|
|
31
|
+
:root[data-theme="dark"] {
|
|
32
|
+
color-scheme: dark;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:root,
|
|
36
|
+
/* Lets the options menu preview the default family while another is
|
|
37
|
+
active — same shared-selector trick as styles-alt.css. */
|
|
38
|
+
[data-preview="default"] {
|
|
39
|
+
/* ── surfaces and ink ───────────────────────────────────────────────────
|
|
40
|
+
The light ladder is authored rather than derived: numerically inverting
|
|
41
|
+
the dark ladder lands on muddy mid-greys, where a light UI wants a
|
|
42
|
+
near-white ground carrying the same cool hue at much lower chroma. */
|
|
43
|
+
--c-bg: light-dark(oklch(98.5% 0.004 263.0), oklch(18.3% 0.031 263.4));
|
|
44
|
+
--c-panel: light-dark(oklch(96.3% 0.006 263.0), oklch(21.8% 0.036 262.5));
|
|
45
|
+
--c-panel-2: light-dark(oklch(93.5% 0.009 263.0), oklch(26.7% 0.048 264.7));
|
|
46
|
+
--c-border: light-dark(oklch(88.5% 0.012 263.0), oklch(28.8% 0.050 265.9));
|
|
47
|
+
--c-border-strong: light-dark(oklch(76.0% 0.022 263.0), oklch(38.6% 0.063 264.9));
|
|
48
|
+
--c-text: light-dark(oklch(24.0% 0.020 263.0), oklch(92.8% 0.006 264.5));
|
|
49
|
+
--c-dim: light-dark(oklch(48.0% 0.028 257.0), oklch(71.1% 0.035 256.8));
|
|
50
|
+
--c-dim-bright: light-dark(oklch(36.0% 0.024 253.0), oklch(86.9% 0.020 252.9));
|
|
51
|
+
/* ── data encoding ─────────────────────────────────────────────────────
|
|
52
|
+
These carry MEANING — bar kinds, the priority ramp, blocked/ready-now,
|
|
53
|
+
over-cap. Hue is preserved exactly across themes so green keeps meaning
|
|
54
|
+
shipped and red keeps meaning blocked; only lightness and chroma move,
|
|
55
|
+
because bright-on-dark and deep-on-light are what hold contrast on their
|
|
56
|
+
respective grounds.
|
|
57
|
+
|
|
58
|
+
A named style should think twice before overriding these: restyling the
|
|
59
|
+
chrome is a reskin, but restyling these is relabelling the data. */
|
|
60
|
+
--c-accent: light-dark(oklch(52.0% 0.183 293.5), oklch(70.9% 0.159 293.5));
|
|
61
|
+
--c-epic: light-dark(oklch(50.0% 0.203 305.5), oklch(72.2% 0.177 305.5));
|
|
62
|
+
/* Lightness is placed by search, not by taste (bp-67g.26). Hue stays; the L
|
|
63
|
+
of every colour-only encoding token — the bar kinds, the phases, danger —
|
|
64
|
+
is chosen so each pair clears 6 (aiming for 8) under simulated protan,
|
|
65
|
+
deutan and tritan vision as well as the 0.08 OKLab floor, both as tokens
|
|
66
|
+
and as bars painted over --c-panel. Re-run the placement instead of
|
|
67
|
+
nudging a value by hand: scripts/place-encoding-lightness.mjs. The
|
|
68
|
+
-bright and -stroke variants are text and outline tokens tuned for their
|
|
69
|
+
own contrast and do not track these fills. */
|
|
70
|
+
--c-shipped: light-dark(oklch(40.6% 0.109 151.7), oklch(83.1% 0.182 151.7));
|
|
71
|
+
--c-shipped-stroke: light-dark(oklch(38.0% 0.125 151.3), oklch(44.8% 0.108 151.3));
|
|
72
|
+
--c-shipped-bright: light-dark(oklch(60.0% 0.157 154.4), oklch(87.1% 0.136 154.4));
|
|
73
|
+
--c-in-progress: light-dark(oklch(60.5% 0.165 254.6), oklch(72.4% 0.143 254.6));
|
|
74
|
+
--c-in-progress-bright: light-dark(oklch(58.0% 0.110 251.8), oklch(80.9% 0.096 251.8));
|
|
75
|
+
--c-closed-noship: light-dark(oklch(49.4% 0.047 257.4), oklch(70.9% 0.041 257.4));
|
|
76
|
+
--c-today: light-dark(oklch(64.7% 0.239 25.3), oklch(63.8% 0.208 25.3));
|
|
77
|
+
--c-danger: light-dark(oklch(51.9% 0.191 22.2), oklch(71.0% 0.166 22.2));
|
|
78
|
+
--c-danger-bright: light-dark(oklch(60.0% 0.119 19.6), oklch(80.8% 0.103 19.6));
|
|
79
|
+
--c-warning: light-dark(oklch(62.0% 0.189 84.4), oklch(83.7% 0.164 84.4));
|
|
80
|
+
--c-warning-strong: light-dark(oklch(56.0% 0.183 55.9), oklch(75.8% 0.159 55.9));
|
|
81
|
+
--c-warning-bright: light-dark(oklch(66.0% 0.134 66.3), oklch(83.7% 0.117 66.3));
|
|
82
|
+
/* Bar kinds. These used to alias --c-accent, on the argument that "future"
|
|
83
|
+
isn't told apart from a sibling state by hue. That was wrong: it IS told
|
|
84
|
+
apart from `shipped` by hue, and a style whose accent lands near green
|
|
85
|
+
collapses the two. The terminal style did exactly that — shipped
|
|
86
|
+
oklch(0.8 0.182 151.7) against future oklch(0.78 0.18 145), 6.7 degrees
|
|
87
|
+
apart. So they carry their own hue, fixed like every other encoding
|
|
88
|
+
colour, and a style recolouring its accent can no longer reach them. */
|
|
89
|
+
--c-future: light-dark(oklch(38.8% 0.183 293.5), oklch(63.5% 0.159 293.5));
|
|
90
|
+
--c-backlog: light-dark(oklch(38.8% 0.183 293.5), oklch(63.5% 0.159 293.5));
|
|
91
|
+
|
|
92
|
+
/* Lifecycle phase fills for the economics breakdown. Separate from the
|
|
93
|
+
text-oriented semantics above because a fill and a label want different
|
|
94
|
+
values: light-mode --c-warning sits at L62 for text contrast, which as a
|
|
95
|
+
10px bar reads as muddy ochre rather than "waiting". Fixed across styles
|
|
96
|
+
like the rest of the encoding — including `other`, which previously
|
|
97
|
+
borrowed --c-border-strong and so drifted with the style while the three
|
|
98
|
+
beside it stayed put. */
|
|
99
|
+
--c-phase-worked: light-dark(oklch(52.6% 0.140 151.7), oklch(78.2% 0.175 151.7));
|
|
100
|
+
--c-phase-review: light-dark(oklch(59.7% 0.150 254.6), oklch(70.6% 0.143 254.6));
|
|
101
|
+
--c-phase-waiting: light-dark(oklch(74.0% 0.155 78.0), oklch(79.1% 0.162 84.4));
|
|
102
|
+
--c-phase-other: light-dark(oklch(74.0% 0.012 263.0), oklch(45.0% 0.063 264.9));
|
|
103
|
+
|
|
104
|
+
/* ── derived once, for whichever style is active ────────────────────────
|
|
105
|
+
Relative colour syntax against the RESOLVED token, so a named style
|
|
106
|
+
that changes --c-accent gets its own hover/dim/ring for free. "Toward
|
|
107
|
+
contrast" means darker on light and lighter on dark. */
|
|
108
|
+
--c-accent-hover: light-dark(
|
|
109
|
+
oklch(from var(--c-accent) calc(l - 0.08) c h),
|
|
110
|
+
oklch(from var(--c-accent) calc(l + 0.09) c h)
|
|
111
|
+
);
|
|
112
|
+
--c-accent-dim: oklch(from var(--c-accent) l c h / 15%);
|
|
113
|
+
--c-focus-ring: var(--c-accent);
|
|
114
|
+
|
|
115
|
+
/* Ink for content sitting ON --c-accent. --c-bg is already the correct
|
|
116
|
+
high-contrast opposite in both directions, so this needs no authoring. */
|
|
117
|
+
--c-accent-fg: var(--c-bg);
|
|
118
|
+
|
|
119
|
+
/* Accent washes, previously hardcoded as rgba() of the accent's literal
|
|
120
|
+
channels — which silently stopped matching the moment a style changed
|
|
121
|
+
the accent. Derived now, so they always track it. */
|
|
122
|
+
--c-accent-wash: oklch(from var(--c-accent) l c h / 8%);
|
|
123
|
+
--c-accent-wash-strong: oklch(from var(--c-accent) l c h / 15%);
|
|
124
|
+
--c-dim-wash: oklch(from var(--c-dim) l c h / 15%);
|
|
125
|
+
|
|
126
|
+
/* A subtle raise off the surface. Inverts direction by theme: lifting
|
|
127
|
+
means lighter on dark and darker on light. */
|
|
128
|
+
--c-lift: light-dark(rgba(0, 0, 0, 0.04), rgba(255, 255, 255, 0.04));
|
|
129
|
+
|
|
130
|
+
/* Scrim dims toward black in both directions, so one plain value. */
|
|
131
|
+
--c-scrim: rgba(0, 0, 0, 0.45);
|
|
132
|
+
|
|
133
|
+
/* Shadows are much weaker on a light ground: the same opacity that reads
|
|
134
|
+
as depth against near-black reads as dirt against near-white. */
|
|
135
|
+
--shadow-panel: light-dark(
|
|
136
|
+
0 8px 24px rgba(0, 0, 0, 0.12),
|
|
137
|
+
0 8px 24px rgba(0, 0, 0, 0.4)
|
|
138
|
+
);
|
|
139
|
+
--shadow-pop: light-dark(
|
|
140
|
+
0 12px 32px rgba(0, 0, 0, 0.16),
|
|
141
|
+
0 12px 32px rgba(0, 0, 0, 0.5)
|
|
142
|
+
);
|
|
143
|
+
--shadow-drawer: light-dark(
|
|
144
|
+
-20px 0 60px rgba(0, 0, 0, 0.18),
|
|
145
|
+
-20px 0 60px rgba(0, 0, 0, 0.5)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
/* ── dimming ───────────────────────────────────────────────────────────
|
|
149
|
+
Per-theme, not shared: an opacity is a step toward the background, and
|
|
150
|
+
the same fraction is a different perceptual step against near-white
|
|
151
|
+
than against near-black. Light mode needs to fade less far to read as
|
|
152
|
+
"receded" without vanishing. */
|
|
153
|
+
--o-inferred: light-dark(0.55, 0.62);
|
|
154
|
+
--o-dim-label: light-dark(0.45, 0.35);
|
|
155
|
+
--o-spent-edge: light-dark(0.45, 0.5);
|
|
156
|
+
|
|
157
|
+
/* ── type ──────────────────────────────────────────────────────────────
|
|
158
|
+
Tokenised so a named style can supply its own faces; the default style
|
|
159
|
+
keeps the stack the app already used. */
|
|
160
|
+
--font-body: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
161
|
+
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
|
162
|
+
monospace;
|
|
163
|
+
|
|
164
|
+
/* ── scalars ───────────────────────────────────────────────────────────── */
|
|
165
|
+
--radius: 4px;
|
|
166
|
+
--radius-sm: 3px;
|
|
167
|
+
--transition: 0.12s ease-out;
|
|
168
|
+
}
|