goalbuddy 0.3.2 → 0.3.5
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/README.md +28 -3
- package/RELEASE-0.3.5.md +324 -0
- package/goalbuddy/SKILL.md +8 -2
- package/goalbuddy/agents/goal_judge.toml +29 -17
- package/goalbuddy/agents/goal_scout.toml +34 -14
- package/goalbuddy/agents/goal_worker.toml +32 -15
- package/goalbuddy/extend/local-goal-board/README.md +8 -4
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/goal.md +3 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/notes/.gitkeep +1 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/state.yaml +60 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +3 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +1 -0
- package/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +52 -0
- package/goalbuddy/extend/local-goal-board/extension.yaml +6 -4
- package/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +940 -24
- package/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +389 -54
- package/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +420 -4
- package/goalbuddy/scripts/check-goal-state.mjs +116 -6
- package/goalbuddy/scripts/parallel-plan.mjs +191 -0
- package/goalbuddy/scripts/render-task-prompt.mjs +248 -0
- package/goalbuddy/templates/agents.md +2 -2
- package/goalbuddy/templates/state.yaml +8 -0
- package/internal/assets/goalbuddy-v0.3.5-release.png +0 -0
- package/internal/cli/goal-maker.mjs +64 -1
- package/package.json +3 -2
- package/plugins/goalbuddy/.claude-plugin/plugin.json +2 -2
- package/plugins/goalbuddy/.codex-plugin/plugin.json +4 -4
- package/plugins/goalbuddy/README.md +5 -3
- package/plugins/goalbuddy/agents/goal-judge.md +31 -16
- package/plugins/goalbuddy/agents/goal-scout.md +38 -13
- package/plugins/goalbuddy/agents/goal-worker.md +35 -14
- package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +8 -2
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_judge.toml +29 -17
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_scout.toml +34 -14
- package/plugins/goalbuddy/skills/goalbuddy/agents/goal_worker.toml +32 -15
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/README.md +8 -4
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/goal.md +3 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/notes/.gitkeep +1 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/state.yaml +60 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +3 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +1 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +52 -0
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +6 -4
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/lib/goal-board.mjs +940 -24
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/scripts/local-goal-board.mjs +389 -54
- package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/test/local-goal-board.test.mjs +420 -4
- package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +116 -6
- package/plugins/goalbuddy/skills/goalbuddy/scripts/parallel-plan.mjs +191 -0
- package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +248 -0
- package/plugins/goalbuddy/skills/goalbuddy/templates/agents.md +2 -2
- package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +8 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const VALID_STATUSES = new Set(["queued", "active", "blocked", "done"]);
|
|
@@ -26,7 +26,8 @@ export async function loadGoalBoard(goalDir) {
|
|
|
26
26
|
return normalizeGoalBoard(parseGoalStateText(text), root);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function createBoardPayload(goalDir) {
|
|
29
|
+
export function createBoardPayload(goalDir, options = {}) {
|
|
30
|
+
const includeSubgoals = options.includeSubgoals !== false;
|
|
30
31
|
const root = resolve(goalDir);
|
|
31
32
|
const statePath = join(root, "state.yaml");
|
|
32
33
|
if (!existsSync(statePath)) {
|
|
@@ -36,7 +37,9 @@ export function createBoardPayload(goalDir) {
|
|
|
36
37
|
const document = parseGoalStateText(readFileSync(statePath, "utf8"));
|
|
37
38
|
const board = normalizeGoalBoard(document, root);
|
|
38
39
|
const noteIndex = loadNotes(root);
|
|
39
|
-
const tasks = board.tasks
|
|
40
|
+
const tasks = board.tasks
|
|
41
|
+
.map((task) => attachTaskNote(task, noteIndex))
|
|
42
|
+
.map((task) => includeSubgoals ? attachTaskSubgoal(task, root) : task);
|
|
40
43
|
const columns = buildColumns(tasks);
|
|
41
44
|
const stateStat = statSync(statePath);
|
|
42
45
|
|
|
@@ -128,6 +131,7 @@ export function normalizeTask(task, index) {
|
|
|
128
131
|
allowedFiles: normalizeStringList(task.allowed_files),
|
|
129
132
|
verify: normalizeStringList(task.verify),
|
|
130
133
|
stopIf: normalizeStringList(task.stop_if),
|
|
134
|
+
subgoal: normalizeSubgoal(task.subgoal),
|
|
131
135
|
receipt: normalizeReceipt(task.receipt),
|
|
132
136
|
};
|
|
133
137
|
}
|
|
@@ -170,6 +174,42 @@ function attachTaskNote(task, noteIndex) {
|
|
|
170
174
|
};
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
function attachTaskSubgoal(task, goalDir) {
|
|
178
|
+
if (!task.subgoal) return task;
|
|
179
|
+
const childStatePath = resolve(goalDir, task.subgoal.path);
|
|
180
|
+
validateChildSubgoalPath(task, goalDir, childStatePath);
|
|
181
|
+
const childGoalDir = dirname(childStatePath);
|
|
182
|
+
if (!existsSync(childStatePath)) {
|
|
183
|
+
throw new GoalBoardError(`Missing sub-goal state for ${task.id}: ${task.subgoal.path}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
...task,
|
|
188
|
+
subgoal: {
|
|
189
|
+
...task.subgoal,
|
|
190
|
+
board: createBoardPayload(childGoalDir, { includeSubgoals: false }),
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateChildSubgoalPath(task, goalDir, childStatePath) {
|
|
196
|
+
if (task.subgoal.depth !== 1) {
|
|
197
|
+
throw new GoalBoardError(`Invalid sub-goal depth for ${task.id}: only depth 1 is supported.`);
|
|
198
|
+
}
|
|
199
|
+
const childRelativePath = relative(goalDir, childStatePath);
|
|
200
|
+
if (!isInsideRoot(childRelativePath)) {
|
|
201
|
+
throw new GoalBoardError(`Invalid sub-goal path for ${task.id}: ${task.subgoal.path} must stay inside the goal root.`);
|
|
202
|
+
}
|
|
203
|
+
const parts = childRelativePath.split(/[\\/]+/);
|
|
204
|
+
if (parts.length !== 3 || parts[0] !== "subgoals" || parts[2] !== "state.yaml") {
|
|
205
|
+
throw new GoalBoardError(`Invalid sub-goal path for ${task.id}: ${task.subgoal.path} must be subgoals/<slug>/state.yaml.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isInsideRoot(relativePath) {
|
|
210
|
+
return relativePath && relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
211
|
+
}
|
|
212
|
+
|
|
173
213
|
function loadNotes(goalDir) {
|
|
174
214
|
const notesDir = join(goalDir, "notes");
|
|
175
215
|
if (!existsSync(notesDir)) return {};
|
|
@@ -215,6 +255,19 @@ function normalizeReceipt(receipt) {
|
|
|
215
255
|
};
|
|
216
256
|
}
|
|
217
257
|
|
|
258
|
+
function normalizeSubgoal(subgoal) {
|
|
259
|
+
if (!subgoal || typeof subgoal !== "object" || Array.isArray(subgoal)) return null;
|
|
260
|
+
return {
|
|
261
|
+
status: cleanText(subgoal.status || ""),
|
|
262
|
+
path: cleanText(subgoal.path || ""),
|
|
263
|
+
owner: cleanText(subgoal.owner || ""),
|
|
264
|
+
createdFrom: cleanText(subgoal.created_from || ""),
|
|
265
|
+
depth: Number(subgoal.depth || 0),
|
|
266
|
+
rollupReceipt: cleanText(subgoal.rollup_receipt || ""),
|
|
267
|
+
board: null,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
218
271
|
function normalizeCommands(commands) {
|
|
219
272
|
if (!commands) return [];
|
|
220
273
|
if (!Array.isArray(commands)) return [cleanText(commands)].filter(Boolean).map((cmd) => ({ cmd, status: "" }));
|
|
@@ -228,8 +281,33 @@ function normalizeCommands(commands) {
|
|
|
228
281
|
}
|
|
229
282
|
|
|
230
283
|
function titleForTask(task) {
|
|
284
|
+
if (task.title) return compactTaskTitle(task.title);
|
|
231
285
|
const objective = cleanText(task.objective || "Untitled task");
|
|
232
|
-
return objective
|
|
286
|
+
return compactTaskTitle(objective);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function compactTaskTitle(value) {
|
|
290
|
+
const text = cleanText(value).replace(/\.$/, "");
|
|
291
|
+
const routeMatch = text.match(/^Implement\b.*?\s(\/[A-Za-z0-9_./:-]+)\s+(route|queue slice|slice)\b/i);
|
|
292
|
+
if (routeMatch) return truncateTitle(`Implement ${routeMatch[1]} ${routeMatch[2]}`);
|
|
293
|
+
|
|
294
|
+
const firstClause = text
|
|
295
|
+
.split(/(?<=[.!?])\s+|\s+(?:Use only|Add|Match|Render|Clearly label|Do not)\b/i)[0]
|
|
296
|
+
.replace(/\bas the next first-milestone slice\b/gi, "")
|
|
297
|
+
.replace(/\bblocker documentation\b/gi, "blocker docs")
|
|
298
|
+
.replace(/\benv\/setup notes\b/gi, "setup notes")
|
|
299
|
+
.replace(/\s+/g, " ")
|
|
300
|
+
.replace(/[.;:,]\s*$/, "")
|
|
301
|
+
.trim();
|
|
302
|
+
|
|
303
|
+
return truncateTitle(firstClause || text);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function truncateTitle(value, maxLength = 82) {
|
|
307
|
+
const text = cleanText(value).replace(/\.$/, "");
|
|
308
|
+
if (text.length <= maxLength) return text;
|
|
309
|
+
const shortened = text.slice(0, maxLength + 1).replace(/\s+\S*$/, "").trim();
|
|
310
|
+
return `${shortened || text.slice(0, maxLength).trim()}...`;
|
|
233
311
|
}
|
|
234
312
|
|
|
235
313
|
function columnForStatus(status) {
|
|
@@ -452,11 +530,75 @@ function boardHtml() {
|
|
|
452
530
|
</head>
|
|
453
531
|
<body>
|
|
454
532
|
<header class="topbar">
|
|
455
|
-
<div class="
|
|
456
|
-
<
|
|
457
|
-
|
|
533
|
+
<div class="topbar-primary">
|
|
534
|
+
<div class="brand" aria-label="Goal Buddy">
|
|
535
|
+
<img class="brand-mark" src="./goalbuddy-mark.png" alt="GoalBuddy">
|
|
536
|
+
<span class="brand-name">Goal Buddy</span>
|
|
537
|
+
<span class="live-dot" id="live-dot" aria-hidden="true"></span>
|
|
538
|
+
</div>
|
|
539
|
+
<nav class="board-switcher is-empty" aria-label="Local GoalBuddy boards">
|
|
540
|
+
<label for="board-switcher">Board</label>
|
|
541
|
+
<select id="board-switcher" aria-label="Switch local board"></select>
|
|
542
|
+
</nav>
|
|
543
|
+
</div>
|
|
544
|
+
<div class="header-tools">
|
|
545
|
+
<a class="github-stars" href="https://github.com/tolibear/goalbuddy" target="_blank" rel="noreferrer" aria-label="Open GoalBuddy on GitHub">
|
|
546
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 2.8 2.84 5.76 6.36.92-4.6 4.48 1.08 6.34L12 17.32 6.32 20.3l1.08-6.34-4.6-4.48 6.36-.92L12 2.8Z"></path></svg>
|
|
547
|
+
<span id="github-stars">Stars</span>
|
|
548
|
+
</a>
|
|
549
|
+
<div class="settings-wrap">
|
|
550
|
+
<button class="settings-button" id="settings-button" type="button" aria-expanded="false" aria-controls="settings-popover">
|
|
551
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
552
|
+
<path d="M12.2 2.75h-.4a1.6 1.6 0 0 0-1.58 1.36l-.18 1.18c-.46.16-.9.34-1.31.56l-1.02-.64a1.6 1.6 0 0 0-2.08.31l-.28.28a1.6 1.6 0 0 0-.31 2.08l.64 1.02c-.22.42-.4.86-.56 1.31l-1.18.18A1.6 1.6 0 0 0 2.58 12v.4A1.6 1.6 0 0 0 3.94 14l1.18.18c.16.46.34.9.56 1.31l-.64 1.02a1.6 1.6 0 0 0 .31 2.08l.28.28a1.6 1.6 0 0 0 2.08.31l1.02-.64c.42.22.86.4 1.31.56l.18 1.18a1.6 1.6 0 0 0 1.58 1.36h.4a1.6 1.6 0 0 0 1.58-1.36l.18-1.18c.46-.16.9-.34 1.31-.56l1.02.64a1.6 1.6 0 0 0 2.08-.31l.28-.28a1.6 1.6 0 0 0 .31-2.08l-.64-1.02c.22-.42.4-.86.56-1.31l1.18-.18a1.6 1.6 0 0 0 1.36-1.58V12a1.6 1.6 0 0 0-1.36-1.58l-1.18-.18a7.2 7.2 0 0 0-.56-1.31l.64-1.02a1.6 1.6 0 0 0-.31-2.08l-.28-.28a1.6 1.6 0 0 0-2.08-.31l-1.02.64c-.42-.22-.86-.4-1.31-.56l-.18-1.18a1.6 1.6 0 0 0-1.58-1.39Z"></path>
|
|
553
|
+
<circle cx="12" cy="12.2" r="3.15"></circle>
|
|
554
|
+
</svg>
|
|
555
|
+
<span class="visually-hidden" id="live-state">Connecting</span>
|
|
556
|
+
</button>
|
|
557
|
+
<section class="settings-popover" id="settings-popover" aria-label="Local board settings" hidden>
|
|
558
|
+
<div class="settings-heading">
|
|
559
|
+
<p class="eyebrow">Board settings</p>
|
|
560
|
+
<h2>Local preferences</h2>
|
|
561
|
+
</div>
|
|
562
|
+
<div class="setting-row">
|
|
563
|
+
<label for="setting-theme">Theme</label>
|
|
564
|
+
<select id="setting-theme" data-setting="theme">
|
|
565
|
+
<option value="system">System</option>
|
|
566
|
+
<option value="light">Light</option>
|
|
567
|
+
<option value="dark">Dark</option>
|
|
568
|
+
</select>
|
|
569
|
+
</div>
|
|
570
|
+
<div class="setting-row">
|
|
571
|
+
<label for="setting-density">Density</label>
|
|
572
|
+
<select id="setting-density" data-setting="density">
|
|
573
|
+
<option value="comfortable">Comfortable</option>
|
|
574
|
+
<option value="compact">Compact</option>
|
|
575
|
+
</select>
|
|
576
|
+
</div>
|
|
577
|
+
<div class="setting-row">
|
|
578
|
+
<label for="setting-completed">Completed</label>
|
|
579
|
+
<select id="setting-completed" data-setting="completedVisibility">
|
|
580
|
+
<option value="show">Show</option>
|
|
581
|
+
<option value="collapse">Collapse</option>
|
|
582
|
+
</select>
|
|
583
|
+
</div>
|
|
584
|
+
<div class="setting-row">
|
|
585
|
+
<label for="setting-board-open">Open boards</label>
|
|
586
|
+
<select id="setting-board-open" data-setting="boardOpenBehavior">
|
|
587
|
+
<option value="last">Last viewed</option>
|
|
588
|
+
<option value="newest">Newest active</option>
|
|
589
|
+
</select>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="setting-row">
|
|
592
|
+
<label for="setting-motion">Motion</label>
|
|
593
|
+
<select id="setting-motion" data-setting="motion">
|
|
594
|
+
<option value="system">System</option>
|
|
595
|
+
<option value="reduce">Reduce</option>
|
|
596
|
+
<option value="allow">Allow</option>
|
|
597
|
+
</select>
|
|
598
|
+
</div>
|
|
599
|
+
</section>
|
|
600
|
+
</div>
|
|
458
601
|
</div>
|
|
459
|
-
<div class="live-state" id="live-state">Connecting</div>
|
|
460
602
|
</header>
|
|
461
603
|
<main class="shell">
|
|
462
604
|
<section class="goal-header" aria-labelledby="goal-title">
|
|
@@ -508,9 +650,94 @@ function boardCss() {
|
|
|
508
650
|
--red-text: #9f2f2d;
|
|
509
651
|
--yellow-bg: #fbf3db;
|
|
510
652
|
--yellow-text: #956400;
|
|
653
|
+
--active-surface: #fbfdfe;
|
|
511
654
|
font-family: "SF Pro Display", "Geist Sans", "Helvetica Neue", Arial, sans-serif;
|
|
512
655
|
}
|
|
513
656
|
|
|
657
|
+
:root[data-theme="dark"] {
|
|
658
|
+
color-scheme: dark;
|
|
659
|
+
--canvas: #07101f;
|
|
660
|
+
--surface: #101a2d;
|
|
661
|
+
--surface-muted: #0c1525;
|
|
662
|
+
--ink: #f7f9fc;
|
|
663
|
+
--muted: #9aa7bf;
|
|
664
|
+
--line: #26334a;
|
|
665
|
+
--blue-bg: #173653;
|
|
666
|
+
--blue-text: #9ed8ff;
|
|
667
|
+
--green-bg: #143929;
|
|
668
|
+
--green-text: #a6e8bf;
|
|
669
|
+
--red-bg: #3a1d22;
|
|
670
|
+
--red-text: #ffb2b9;
|
|
671
|
+
--yellow-bg: #3a3014;
|
|
672
|
+
--yellow-text: #f6d878;
|
|
673
|
+
--active-surface: #0f2031;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
@media (prefers-color-scheme: dark) {
|
|
677
|
+
:root[data-theme="system"] {
|
|
678
|
+
color-scheme: dark;
|
|
679
|
+
--canvas: #07101f;
|
|
680
|
+
--surface: #101a2d;
|
|
681
|
+
--surface-muted: #0c1525;
|
|
682
|
+
--ink: #f7f9fc;
|
|
683
|
+
--muted: #9aa7bf;
|
|
684
|
+
--line: #26334a;
|
|
685
|
+
--blue-bg: #173653;
|
|
686
|
+
--blue-text: #9ed8ff;
|
|
687
|
+
--green-bg: #143929;
|
|
688
|
+
--green-text: #a6e8bf;
|
|
689
|
+
--red-bg: #3a1d22;
|
|
690
|
+
--red-text: #ffb2b9;
|
|
691
|
+
--yellow-bg: #3a3014;
|
|
692
|
+
--yellow-text: #f6d878;
|
|
693
|
+
--active-surface: #0f2031;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
:root[data-theme="system"] .topbar {
|
|
697
|
+
border-color: rgba(61, 76, 108, 0.86);
|
|
698
|
+
background: rgba(13, 23, 41, 0.84);
|
|
699
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
:root[data-theme="system"] .brand {
|
|
703
|
+
color: var(--ink);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
:root[data-theme="system"] .board-switcher select,
|
|
707
|
+
:root[data-theme="system"] .github-stars,
|
|
708
|
+
:root[data-theme="system"] .settings-button {
|
|
709
|
+
border-color: rgba(61, 76, 108, 0.9);
|
|
710
|
+
background: rgba(16, 26, 45, 0.78);
|
|
711
|
+
color: var(--ink);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
:root[data-theme="system"] .settings-popover {
|
|
715
|
+
border-color: rgba(61, 76, 108, 0.96);
|
|
716
|
+
background: rgba(16, 26, 45, 0.96);
|
|
717
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
:root[data-theme="system"] .setting-row select {
|
|
721
|
+
background: var(--surface);
|
|
722
|
+
color: var(--ink);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
:root[data-theme="system"] .goal-tranche,
|
|
726
|
+
:root[data-theme="system"] .task-title,
|
|
727
|
+
:root[data-theme="system"] .setting-row select {
|
|
728
|
+
color: var(--ink);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
:root[data-theme="system"] .task-card.is-active {
|
|
732
|
+
background: linear-gradient(var(--active-surface), var(--active-surface)) padding-box,
|
|
733
|
+
linear-gradient(110deg, #78d7ff, #6c63ff, #78f2b9, #78d7ff) border-box;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
:root[data-theme="system"] .task-card.is-active::after {
|
|
737
|
+
background: var(--active-surface);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
514
741
|
* { box-sizing: border-box; }
|
|
515
742
|
|
|
516
743
|
body {
|
|
@@ -526,18 +753,46 @@ textarea {
|
|
|
526
753
|
font: inherit;
|
|
527
754
|
}
|
|
528
755
|
|
|
756
|
+
a {
|
|
757
|
+
color: inherit;
|
|
758
|
+
text-decoration: none;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
select,
|
|
762
|
+
button {
|
|
763
|
+
font: inherit;
|
|
764
|
+
}
|
|
765
|
+
|
|
529
766
|
.topbar {
|
|
530
767
|
position: sticky;
|
|
531
|
-
top:
|
|
768
|
+
top: 16px;
|
|
532
769
|
z-index: 10;
|
|
533
770
|
display: flex;
|
|
534
771
|
align-items: center;
|
|
535
772
|
justify-content: space-between;
|
|
536
773
|
gap: 16px;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
774
|
+
width: min(1392px, calc(100% - 48px));
|
|
775
|
+
min-height: 64px;
|
|
776
|
+
margin: 0 auto;
|
|
777
|
+
padding: 10px 12px 10px 18px;
|
|
778
|
+
border: 1px solid rgba(219, 226, 240, 0.86);
|
|
779
|
+
border-radius: 999px;
|
|
780
|
+
background: rgba(255, 255, 255, 0.78);
|
|
781
|
+
box-shadow: 0 18px 48px rgba(30, 40, 72, 0.1);
|
|
782
|
+
backdrop-filter: blur(22px);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
:root[data-theme="dark"] .topbar {
|
|
786
|
+
border-color: rgba(61, 76, 108, 0.86);
|
|
787
|
+
background: rgba(13, 23, 41, 0.84);
|
|
788
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.topbar-primary {
|
|
792
|
+
display: inline-flex;
|
|
793
|
+
align-items: center;
|
|
794
|
+
gap: 24px;
|
|
795
|
+
min-width: 0;
|
|
541
796
|
}
|
|
542
797
|
|
|
543
798
|
.brand {
|
|
@@ -546,12 +801,18 @@ textarea {
|
|
|
546
801
|
gap: 10px;
|
|
547
802
|
color: #071236;
|
|
548
803
|
font-weight: 800;
|
|
804
|
+
min-width: fit-content;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
:root[data-theme="dark"] .brand {
|
|
808
|
+
color: var(--ink);
|
|
549
809
|
}
|
|
550
810
|
|
|
551
811
|
.brand-mark {
|
|
552
812
|
display: block;
|
|
553
|
-
width:
|
|
554
|
-
height:
|
|
813
|
+
width: 38px;
|
|
814
|
+
height: 38px;
|
|
815
|
+
filter: drop-shadow(0 8px 13px rgba(87, 76, 210, 0.18));
|
|
555
816
|
}
|
|
556
817
|
|
|
557
818
|
.brand-name {
|
|
@@ -559,7 +820,198 @@ textarea {
|
|
|
559
820
|
letter-spacing: 0;
|
|
560
821
|
}
|
|
561
822
|
|
|
562
|
-
.
|
|
823
|
+
.board-switcher {
|
|
824
|
+
display: flex;
|
|
825
|
+
align-items: center;
|
|
826
|
+
justify-content: flex-start;
|
|
827
|
+
gap: 8px;
|
|
828
|
+
min-width: 0;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.board-switcher label {
|
|
832
|
+
color: var(--muted);
|
|
833
|
+
font-size: 11px;
|
|
834
|
+
font-weight: 700;
|
|
835
|
+
letter-spacing: 0.08em;
|
|
836
|
+
text-transform: uppercase;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.board-switcher select {
|
|
840
|
+
width: min(280px, 100%);
|
|
841
|
+
min-width: 0;
|
|
842
|
+
min-height: 38px;
|
|
843
|
+
border: 1px solid rgba(219, 226, 240, 0.9);
|
|
844
|
+
border-radius: 999px;
|
|
845
|
+
padding: 0 34px 0 14px;
|
|
846
|
+
background: rgba(255, 255, 255, 0.72);
|
|
847
|
+
color: #2f3c59;
|
|
848
|
+
font-weight: 700;
|
|
849
|
+
font-size: 14px;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
:root[data-theme="dark"] .board-switcher select,
|
|
853
|
+
:root[data-theme="dark"] .github-stars,
|
|
854
|
+
:root[data-theme="dark"] .settings-button {
|
|
855
|
+
border-color: rgba(61, 76, 108, 0.9);
|
|
856
|
+
background: rgba(16, 26, 45, 0.78);
|
|
857
|
+
color: var(--ink);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.board-switcher.is-empty {
|
|
861
|
+
display: none;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.header-tools {
|
|
865
|
+
display: inline-flex;
|
|
866
|
+
align-items: center;
|
|
867
|
+
justify-content: flex-end;
|
|
868
|
+
gap: 10px;
|
|
869
|
+
min-width: fit-content;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.github-stars,
|
|
873
|
+
.settings-button {
|
|
874
|
+
display: inline-flex;
|
|
875
|
+
align-items: center;
|
|
876
|
+
justify-content: center;
|
|
877
|
+
min-height: 44px;
|
|
878
|
+
border: 1px solid rgba(219, 226, 240, 0.9);
|
|
879
|
+
border-radius: 999px;
|
|
880
|
+
background: rgba(255, 255, 255, 0.72);
|
|
881
|
+
color: #2f3c59;
|
|
882
|
+
font-weight: 800;
|
|
883
|
+
transition: transform 180ms ease, color 180ms ease, border-color 180ms ease, background 180ms ease;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.github-stars {
|
|
887
|
+
gap: 7px;
|
|
888
|
+
padding: 0 15px;
|
|
889
|
+
font-size: 14px;
|
|
890
|
+
white-space: nowrap;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.github-stars:hover,
|
|
894
|
+
.settings-button:hover {
|
|
895
|
+
transform: translateY(-2px);
|
|
896
|
+
color: #071236;
|
|
897
|
+
border-color: rgba(79, 70, 216, 0.26);
|
|
898
|
+
background: #fff;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.github-stars svg {
|
|
902
|
+
width: 16px;
|
|
903
|
+
height: 16px;
|
|
904
|
+
color: #4f46d8;
|
|
905
|
+
fill: currentColor;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.settings-wrap {
|
|
909
|
+
position: relative;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.settings-button {
|
|
913
|
+
position: relative;
|
|
914
|
+
gap: 8px;
|
|
915
|
+
width: 44px;
|
|
916
|
+
padding: 0;
|
|
917
|
+
cursor: pointer;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.settings-button svg {
|
|
921
|
+
width: 18px;
|
|
922
|
+
height: 18px;
|
|
923
|
+
fill: none;
|
|
924
|
+
stroke: currentColor;
|
|
925
|
+
stroke-width: 1.8;
|
|
926
|
+
stroke-linejoin: round;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.live-dot {
|
|
930
|
+
width: 8px;
|
|
931
|
+
height: 8px;
|
|
932
|
+
border: 2px solid #fff;
|
|
933
|
+
border-radius: 999px;
|
|
934
|
+
background: #1f9d69;
|
|
935
|
+
box-shadow: 0 0 0 4px rgba(31, 157, 105, 0.12);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
.live-dot.offline {
|
|
939
|
+
background: var(--yellow-text);
|
|
940
|
+
box-shadow: 0 0 0 4px rgba(149, 100, 0, 0.12);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.settings-popover {
|
|
944
|
+
position: absolute;
|
|
945
|
+
top: calc(100% + 10px);
|
|
946
|
+
right: 0;
|
|
947
|
+
width: min(320px, calc(100vw - 32px));
|
|
948
|
+
padding: 16px;
|
|
949
|
+
border: 1px solid rgba(219, 226, 240, 0.96);
|
|
950
|
+
border-radius: 18px;
|
|
951
|
+
background: rgba(255, 255, 255, 0.96);
|
|
952
|
+
box-shadow: 0 24px 64px rgba(30, 40, 72, 0.16);
|
|
953
|
+
backdrop-filter: blur(20px);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
:root[data-theme="dark"] .settings-popover {
|
|
957
|
+
border-color: rgba(61, 76, 108, 0.96);
|
|
958
|
+
background: rgba(16, 26, 45, 0.96);
|
|
959
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.settings-popover[hidden] {
|
|
963
|
+
display: none;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.settings-heading {
|
|
967
|
+
margin-bottom: 12px;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.settings-heading .eyebrow {
|
|
971
|
+
margin-bottom: 6px;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.settings-heading h2 {
|
|
975
|
+
margin: 0;
|
|
976
|
+
font-size: 20px;
|
|
977
|
+
letter-spacing: 0;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.setting-row {
|
|
981
|
+
display: grid;
|
|
982
|
+
gap: 6px;
|
|
983
|
+
margin-top: 12px;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.setting-row label {
|
|
987
|
+
color: var(--muted);
|
|
988
|
+
font-size: 12px;
|
|
989
|
+
font-weight: 800;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.setting-row select {
|
|
993
|
+
min-height: 38px;
|
|
994
|
+
border: 1px solid var(--line);
|
|
995
|
+
border-radius: 8px;
|
|
996
|
+
padding: 0 10px;
|
|
997
|
+
background: #fff;
|
|
998
|
+
color: #2f3437;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
:root[data-theme="dark"] .setting-row select {
|
|
1002
|
+
background: var(--surface);
|
|
1003
|
+
color: var(--ink);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.visually-hidden {
|
|
1007
|
+
position: absolute;
|
|
1008
|
+
width: 1px;
|
|
1009
|
+
height: 1px;
|
|
1010
|
+
overflow: hidden;
|
|
1011
|
+
clip: rect(0 0 0 0);
|
|
1012
|
+
white-space: nowrap;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
563
1015
|
.badge {
|
|
564
1016
|
display: inline-flex;
|
|
565
1017
|
align-items: center;
|
|
@@ -625,6 +1077,12 @@ h1 {
|
|
|
625
1077
|
line-height: 1.55;
|
|
626
1078
|
}
|
|
627
1079
|
|
|
1080
|
+
:root[data-theme="dark"] .goal-tranche,
|
|
1081
|
+
:root[data-theme="dark"] .task-title,
|
|
1082
|
+
:root[data-theme="dark"] .setting-row select {
|
|
1083
|
+
color: var(--ink);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
628
1086
|
.goal-meta {
|
|
629
1087
|
display: grid;
|
|
630
1088
|
grid-template-columns: repeat(3, minmax(94px, auto));
|
|
@@ -708,6 +1166,7 @@ h1 {
|
|
|
708
1166
|
}
|
|
709
1167
|
|
|
710
1168
|
.task-card {
|
|
1169
|
+
position: relative;
|
|
711
1170
|
width: 100%;
|
|
712
1171
|
min-height: 138px;
|
|
713
1172
|
display: flex;
|
|
@@ -720,10 +1179,16 @@ h1 {
|
|
|
720
1179
|
color: inherit;
|
|
721
1180
|
text-align: left;
|
|
722
1181
|
cursor: pointer;
|
|
1182
|
+
overflow: hidden;
|
|
723
1183
|
transition: transform 160ms ease, border-color 160ms ease;
|
|
724
1184
|
will-change: transform, opacity;
|
|
725
1185
|
}
|
|
726
1186
|
|
|
1187
|
+
.task-card > * {
|
|
1188
|
+
position: relative;
|
|
1189
|
+
z-index: 1;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
727
1192
|
.task-card:hover {
|
|
728
1193
|
border-color: #d1d0cc;
|
|
729
1194
|
transform: translateY(-1px);
|
|
@@ -736,10 +1201,80 @@ h1 {
|
|
|
736
1201
|
}
|
|
737
1202
|
|
|
738
1203
|
.task-card.is-active {
|
|
739
|
-
border-color:
|
|
1204
|
+
border-color: transparent;
|
|
1205
|
+
background: linear-gradient(#fbfdfe, #fbfdfe) padding-box,
|
|
1206
|
+
linear-gradient(110deg, #78d7ff, #4f46d8, #78f2b9, #78d7ff) border-box;
|
|
1207
|
+
box-shadow: 0 14px 38px rgba(31, 108, 159, 0.12);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.task-card.is-active::before {
|
|
1211
|
+
position: absolute;
|
|
1212
|
+
inset: -2px;
|
|
1213
|
+
z-index: 0;
|
|
1214
|
+
content: "";
|
|
1215
|
+
background: conic-gradient(from 0deg, transparent 0 58%, rgba(79, 70, 216, 0.28), rgba(120, 215, 255, 0.44), transparent 78% 100%);
|
|
1216
|
+
opacity: 0.86;
|
|
1217
|
+
animation: active-card-orbit 2.8s linear infinite;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.task-card.is-active::after {
|
|
1221
|
+
position: absolute;
|
|
1222
|
+
inset: 2px;
|
|
1223
|
+
z-index: 0;
|
|
1224
|
+
content: "";
|
|
1225
|
+
border-radius: 6px;
|
|
740
1226
|
background: #fbfdfe;
|
|
741
1227
|
}
|
|
742
1228
|
|
|
1229
|
+
:root[data-theme="dark"] .task-card.is-active {
|
|
1230
|
+
background: linear-gradient(var(--active-surface), var(--active-surface)) padding-box,
|
|
1231
|
+
linear-gradient(110deg, #78d7ff, #6c63ff, #78f2b9, #78d7ff) border-box;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
:root[data-theme="dark"] .task-card.is-active::after {
|
|
1235
|
+
background: var(--active-surface);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
:root[data-density="compact"] .shell {
|
|
1239
|
+
padding-top: 20px;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
:root[data-density="compact"] .board {
|
|
1243
|
+
gap: 12px;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
:root[data-density="compact"] .column-header {
|
|
1247
|
+
padding: 12px;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
:root[data-density="compact"] .card-list {
|
|
1251
|
+
gap: 8px;
|
|
1252
|
+
padding: 10px;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
:root[data-density="compact"] .task-card {
|
|
1256
|
+
min-height: 110px;
|
|
1257
|
+
gap: 9px;
|
|
1258
|
+
padding: 11px;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
:root[data-density="compact"] .task-title {
|
|
1262
|
+
font-size: 14px;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
:root[data-completed-visibility="collapse"] .column[data-column-id="completed"] .card-list {
|
|
1266
|
+
display: none;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
:root[data-completed-visibility="collapse"] .column[data-column-id="completed"] {
|
|
1270
|
+
max-height: 80px;
|
|
1271
|
+
overflow: hidden;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
@keyframes active-card-orbit {
|
|
1275
|
+
to { transform: rotate(360deg); }
|
|
1276
|
+
}
|
|
1277
|
+
|
|
743
1278
|
.task-card.is-moving {
|
|
744
1279
|
border-color: #c2b8ff;
|
|
745
1280
|
}
|
|
@@ -776,6 +1311,14 @@ h1 {
|
|
|
776
1311
|
.badge.status-done { background: var(--green-bg); color: var(--green-text); }
|
|
777
1312
|
.badge.status-blocked { background: var(--red-bg); color: var(--red-text); }
|
|
778
1313
|
.badge.role { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
1314
|
+
.badge.subgoal { background: #ece8ff; color: #5c43c6; }
|
|
1315
|
+
.badge.subgoal.status-blocked { background: var(--red-bg); color: var(--red-text); }
|
|
1316
|
+
.badge.subgoal.status-done { background: var(--green-bg); color: var(--green-text); }
|
|
1317
|
+
|
|
1318
|
+
:root[data-theme="dark"] .badge.subgoal {
|
|
1319
|
+
background: #263052;
|
|
1320
|
+
color: #c7d2ff;
|
|
1321
|
+
}
|
|
779
1322
|
|
|
780
1323
|
.empty {
|
|
781
1324
|
padding: 18px;
|
|
@@ -784,9 +1327,27 @@ h1 {
|
|
|
784
1327
|
}
|
|
785
1328
|
|
|
786
1329
|
@media (prefers-reduced-motion: reduce) {
|
|
1330
|
+
.github-stars,
|
|
1331
|
+
.settings-button,
|
|
787
1332
|
.task-card {
|
|
788
1333
|
transition: none;
|
|
789
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
.task-card.is-active::before {
|
|
1337
|
+
animation: none;
|
|
1338
|
+
opacity: 0.26;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
:root[data-motion="reduce"] .github-stars,
|
|
1343
|
+
:root[data-motion="reduce"] .settings-button,
|
|
1344
|
+
:root[data-motion="reduce"] .task-card {
|
|
1345
|
+
transition: none;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
:root[data-motion="reduce"] .task-card.is-active::before {
|
|
1349
|
+
animation: none;
|
|
1350
|
+
opacity: 0.26;
|
|
790
1351
|
}
|
|
791
1352
|
|
|
792
1353
|
.modal[hidden] {
|
|
@@ -811,7 +1372,7 @@ h1 {
|
|
|
811
1372
|
|
|
812
1373
|
.modal-panel {
|
|
813
1374
|
position: relative;
|
|
814
|
-
width: min(
|
|
1375
|
+
width: min(1080px, 100%);
|
|
815
1376
|
max-height: min(760px, calc(100vh - 48px));
|
|
816
1377
|
overflow: auto;
|
|
817
1378
|
border: 1px solid var(--line);
|
|
@@ -896,10 +1457,94 @@ h1 {
|
|
|
896
1457
|
.detail-section ul {
|
|
897
1458
|
margin: 0;
|
|
898
1459
|
padding-left: 18px;
|
|
899
|
-
color:
|
|
1460
|
+
color: var(--ink);
|
|
900
1461
|
line-height: 1.55;
|
|
901
1462
|
}
|
|
902
1463
|
|
|
1464
|
+
.detail-section li {
|
|
1465
|
+
color: var(--ink);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
.subgoal-section {
|
|
1469
|
+
border: 1px solid var(--line);
|
|
1470
|
+
border-radius: 8px;
|
|
1471
|
+
padding: 14px;
|
|
1472
|
+
background: var(--surface-muted);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
.subgoal-header {
|
|
1476
|
+
display: flex;
|
|
1477
|
+
align-items: start;
|
|
1478
|
+
justify-content: space-between;
|
|
1479
|
+
gap: 12px;
|
|
1480
|
+
margin-bottom: 12px;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
.subgoal-title {
|
|
1484
|
+
margin: 0 0 4px;
|
|
1485
|
+
font-size: 15px;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
.subgoal-meta {
|
|
1489
|
+
margin: 0;
|
|
1490
|
+
color: var(--muted);
|
|
1491
|
+
font-size: 12px;
|
|
1492
|
+
line-height: 1.45;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.subgoal-board {
|
|
1496
|
+
display: grid;
|
|
1497
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1498
|
+
gap: 10px;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
.subgoal-column {
|
|
1502
|
+
min-width: 0;
|
|
1503
|
+
border: 1px solid var(--line);
|
|
1504
|
+
border-radius: 8px;
|
|
1505
|
+
background: var(--surface);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.subgoal-column-header {
|
|
1509
|
+
display: flex;
|
|
1510
|
+
align-items: center;
|
|
1511
|
+
justify-content: space-between;
|
|
1512
|
+
gap: 8px;
|
|
1513
|
+
padding: 10px;
|
|
1514
|
+
border-bottom: 1px solid var(--line);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.subgoal-column-header h4 {
|
|
1518
|
+
margin: 0;
|
|
1519
|
+
font-size: 12px;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.subgoal-card-list {
|
|
1523
|
+
display: grid;
|
|
1524
|
+
gap: 8px;
|
|
1525
|
+
padding: 8px;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.subgoal-task-card {
|
|
1529
|
+
min-height: 74px;
|
|
1530
|
+
border: 1px solid var(--line);
|
|
1531
|
+
border-radius: 7px;
|
|
1532
|
+
padding: 9px;
|
|
1533
|
+
background: var(--surface);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
.subgoal-task-card.is-active {
|
|
1537
|
+
border-color: #8e9cff;
|
|
1538
|
+
background: var(--active-surface);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
.subgoal-task-title {
|
|
1542
|
+
margin: 6px 0 0;
|
|
1543
|
+
color: var(--ink);
|
|
1544
|
+
font-size: 12px;
|
|
1545
|
+
line-height: 1.35;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
903
1548
|
pre.note {
|
|
904
1549
|
overflow: auto;
|
|
905
1550
|
margin: 0;
|
|
@@ -907,7 +1552,7 @@ pre.note {
|
|
|
907
1552
|
border: 1px solid var(--line);
|
|
908
1553
|
border-radius: 8px;
|
|
909
1554
|
background: var(--canvas);
|
|
910
|
-
color:
|
|
1555
|
+
color: var(--ink);
|
|
911
1556
|
font-family: "Geist Mono", "SF Mono", monospace;
|
|
912
1557
|
font-size: 12px;
|
|
913
1558
|
line-height: 1.55;
|
|
@@ -926,10 +1571,27 @@ pre.note {
|
|
|
926
1571
|
.board {
|
|
927
1572
|
grid-template-columns: 1fr;
|
|
928
1573
|
}
|
|
1574
|
+
|
|
1575
|
+
.subgoal-board {
|
|
1576
|
+
grid-template-columns: 1fr;
|
|
1577
|
+
}
|
|
929
1578
|
}
|
|
930
1579
|
|
|
931
1580
|
@media (max-width: 640px) {
|
|
932
|
-
.topbar
|
|
1581
|
+
.topbar {
|
|
1582
|
+
align-items: flex-start;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
.topbar-primary {
|
|
1586
|
+
flex: 1;
|
|
1587
|
+
flex-wrap: wrap;
|
|
1588
|
+
gap: 10px 14px;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
.board-switcher select {
|
|
1592
|
+
width: 100%;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
933
1595
|
.shell {
|
|
934
1596
|
padding-left: 14px;
|
|
935
1597
|
padding-right: 14px;
|
|
@@ -949,22 +1611,69 @@ pre.note {
|
|
|
949
1611
|
function boardJs() {
|
|
950
1612
|
return `let currentBoard = null;
|
|
951
1613
|
let eventSource = null;
|
|
1614
|
+
let currentSettings = null;
|
|
952
1615
|
|
|
953
1616
|
const boardEl = document.getElementById("board");
|
|
954
1617
|
const liveStateEl = document.getElementById("live-state");
|
|
1618
|
+
const liveDotEl = document.getElementById("live-dot");
|
|
1619
|
+
const boardSwitcherEl = document.getElementById("board-switcher");
|
|
1620
|
+
const settingsButtonEl = document.getElementById("settings-button");
|
|
1621
|
+
const settingsPopoverEl = document.getElementById("settings-popover");
|
|
1622
|
+
const githubStarsEl = document.getElementById("github-stars");
|
|
955
1623
|
const modalEl = document.getElementById("task-modal");
|
|
956
1624
|
const modalTitleEl = document.getElementById("modal-title");
|
|
957
1625
|
const modalKickerEl = document.getElementById("modal-kicker");
|
|
958
1626
|
const modalBodyEl = document.getElementById("modal-body");
|
|
1627
|
+
const settingsStorageKey = "goalbuddy.localBoardSettings.v1";
|
|
1628
|
+
const settingsDefaults = {
|
|
1629
|
+
theme: "system",
|
|
1630
|
+
density: "comfortable",
|
|
1631
|
+
completedVisibility: "show",
|
|
1632
|
+
boardOpenBehavior: "last",
|
|
1633
|
+
motion: "system",
|
|
1634
|
+
lastBoardPath: "",
|
|
1635
|
+
};
|
|
1636
|
+
const settingsOptions = {
|
|
1637
|
+
theme: new Set(["system", "light", "dark"]),
|
|
1638
|
+
density: new Set(["comfortable", "compact"]),
|
|
1639
|
+
completedVisibility: new Set(["show", "collapse"]),
|
|
1640
|
+
boardOpenBehavior: new Set(["last", "newest"]),
|
|
1641
|
+
motion: new Set(["system", "reduce", "allow"]),
|
|
1642
|
+
};
|
|
959
1643
|
|
|
960
1644
|
document.addEventListener("click", (event) => {
|
|
961
1645
|
const card = event.target.closest("[data-task-id]");
|
|
962
1646
|
if (card) openTask(card.dataset.taskId);
|
|
963
1647
|
if (event.target.matches("[data-close-modal]")) closeModal();
|
|
1648
|
+
if (settingsPopoverEl.hidden) return;
|
|
1649
|
+
if (!event.target.closest(".settings-wrap")) closeSettings();
|
|
964
1650
|
});
|
|
965
1651
|
|
|
966
1652
|
document.addEventListener("keydown", (event) => {
|
|
967
|
-
if (event.key === "Escape")
|
|
1653
|
+
if (event.key === "Escape") {
|
|
1654
|
+
closeModal();
|
|
1655
|
+
closeSettings();
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
boardSwitcherEl.addEventListener("change", () => {
|
|
1660
|
+
if (boardSwitcherEl.value && boardSwitcherEl.value !== window.location.href) {
|
|
1661
|
+
window.location.href = boardSwitcherEl.value;
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
settingsButtonEl.addEventListener("click", () => {
|
|
1666
|
+
if (settingsPopoverEl.hidden) {
|
|
1667
|
+
openSettings();
|
|
1668
|
+
} else {
|
|
1669
|
+
closeSettings();
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
settingsPopoverEl.addEventListener("change", (event) => {
|
|
1674
|
+
const control = event.target.closest("[data-setting]");
|
|
1675
|
+
if (!control) return;
|
|
1676
|
+
saveSettings({ ...currentSettings, [control.dataset.setting]: control.value });
|
|
968
1677
|
});
|
|
969
1678
|
|
|
970
1679
|
async function loadBoard() {
|
|
@@ -973,6 +1682,122 @@ async function loadBoard() {
|
|
|
973
1682
|
renderBoard(await response.json());
|
|
974
1683
|
}
|
|
975
1684
|
|
|
1685
|
+
async function loadBoardSwitcher() {
|
|
1686
|
+
const response = await fetch("../api/boards", { cache: "no-store" });
|
|
1687
|
+
if (!response.ok) return;
|
|
1688
|
+
const payload = await response.json();
|
|
1689
|
+
renderBoardSwitcher(payload.boards || []);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
async function loadSettings() {
|
|
1693
|
+
try {
|
|
1694
|
+
const response = await fetch("../api/settings", { cache: "no-store" });
|
|
1695
|
+
if (!response.ok) throw new Error("Settings request failed");
|
|
1696
|
+
const payload = await response.json();
|
|
1697
|
+
currentSettings = normalizeSettings(payload.settings);
|
|
1698
|
+
window.localStorage?.setItem(settingsStorageKey, JSON.stringify(currentSettings));
|
|
1699
|
+
} catch {
|
|
1700
|
+
currentSettings = readStoredSettings();
|
|
1701
|
+
}
|
|
1702
|
+
applySettings(currentSettings);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
async function saveSettings(nextSettings) {
|
|
1706
|
+
currentSettings = normalizeSettings(nextSettings);
|
|
1707
|
+
window.localStorage?.setItem(settingsStorageKey, JSON.stringify(currentSettings));
|
|
1708
|
+
applySettings(currentSettings);
|
|
1709
|
+
try {
|
|
1710
|
+
const response = await fetch("../api/settings", {
|
|
1711
|
+
method: "PUT",
|
|
1712
|
+
headers: { "Content-Type": "application/json" },
|
|
1713
|
+
body: JSON.stringify({ settings: currentSettings }),
|
|
1714
|
+
});
|
|
1715
|
+
if (!response.ok) throw new Error("Settings save failed");
|
|
1716
|
+
const payload = await response.json();
|
|
1717
|
+
currentSettings = normalizeSettings(payload.settings);
|
|
1718
|
+
window.localStorage?.setItem(settingsStorageKey, JSON.stringify(currentSettings));
|
|
1719
|
+
applySettings(currentSettings);
|
|
1720
|
+
} catch {
|
|
1721
|
+
// Keep the localStorage fallback active when the local settings API is unavailable.
|
|
1722
|
+
}
|
|
1723
|
+
return currentSettings;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function readStoredSettings() {
|
|
1727
|
+
try {
|
|
1728
|
+
return normalizeSettings(JSON.parse(window.localStorage?.getItem(settingsStorageKey) || "{}"));
|
|
1729
|
+
} catch {
|
|
1730
|
+
return { ...settingsDefaults };
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function normalizeSettings(settings) {
|
|
1735
|
+
const normalized = { ...settingsDefaults };
|
|
1736
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return normalized;
|
|
1737
|
+
for (const [key, allowed] of Object.entries(settingsOptions)) {
|
|
1738
|
+
if (allowed.has(settings[key])) normalized[key] = settings[key];
|
|
1739
|
+
}
|
|
1740
|
+
if (typeof settings.lastBoardPath === "string" && /^\\/[a-z0-9][a-z0-9-]*\\/$/.test(settings.lastBoardPath)) {
|
|
1741
|
+
normalized.lastBoardPath = settings.lastBoardPath;
|
|
1742
|
+
}
|
|
1743
|
+
return normalized;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
function applySettings(settings) {
|
|
1747
|
+
const normalized = normalizeSettings(settings);
|
|
1748
|
+
document.documentElement.dataset.theme = normalized.theme;
|
|
1749
|
+
document.documentElement.dataset.density = normalized.density;
|
|
1750
|
+
document.documentElement.dataset.completedVisibility = normalized.completedVisibility;
|
|
1751
|
+
document.documentElement.dataset.boardOpenBehavior = normalized.boardOpenBehavior;
|
|
1752
|
+
document.documentElement.dataset.motion = normalized.motion;
|
|
1753
|
+
for (const control of settingsPopoverEl.querySelectorAll("[data-setting]")) {
|
|
1754
|
+
control.value = normalized[control.dataset.setting] || settingsDefaults[control.dataset.setting];
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function rememberCurrentBoard() {
|
|
1759
|
+
const boardPath = normalizePath(window.location.pathname);
|
|
1760
|
+
if (!/^\\/[a-z0-9][a-z0-9-]*\\/$/.test(boardPath)) return;
|
|
1761
|
+
const nextSettings = normalizeSettings({ ...currentSettings, lastBoardPath: boardPath });
|
|
1762
|
+
currentSettings = nextSettings;
|
|
1763
|
+
window.localStorage?.setItem(settingsStorageKey, JSON.stringify(nextSettings));
|
|
1764
|
+
fetch("../api/settings", {
|
|
1765
|
+
method: "PUT",
|
|
1766
|
+
headers: { "Content-Type": "application/json" },
|
|
1767
|
+
body: JSON.stringify({ settings: nextSettings }),
|
|
1768
|
+
}).catch(() => {});
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function openSettings() {
|
|
1772
|
+
settingsPopoverEl.hidden = false;
|
|
1773
|
+
settingsButtonEl.setAttribute("aria-expanded", "true");
|
|
1774
|
+
settingsPopoverEl.querySelector("[data-setting]")?.focus();
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function closeSettings() {
|
|
1778
|
+
settingsPopoverEl.hidden = true;
|
|
1779
|
+
settingsButtonEl.setAttribute("aria-expanded", "false");
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function formatStars(count) {
|
|
1783
|
+
if (count >= 1000) return \`\${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k\`;
|
|
1784
|
+
return String(count);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
async function loadGithubStars() {
|
|
1788
|
+
if (!githubStarsEl) return;
|
|
1789
|
+
try {
|
|
1790
|
+
const response = await fetch("https://api.github.com/repos/tolibear/goalbuddy", {
|
|
1791
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
1792
|
+
});
|
|
1793
|
+
if (!response.ok) throw new Error("GitHub API unavailable");
|
|
1794
|
+
const repo = await response.json();
|
|
1795
|
+
githubStarsEl.textContent = \`\${formatStars(repo.stargazers_count)} stars\`;
|
|
1796
|
+
} catch {
|
|
1797
|
+
githubStarsEl.textContent = "GitHub";
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
976
1801
|
function connectEvents() {
|
|
977
1802
|
eventSource = new EventSource("./events");
|
|
978
1803
|
eventSource.addEventListener("board", (event) => {
|
|
@@ -1007,6 +1832,20 @@ function renderBoard(board) {
|
|
|
1007
1832
|
}, delay);
|
|
1008
1833
|
}
|
|
1009
1834
|
|
|
1835
|
+
function renderBoardSwitcher(boards) {
|
|
1836
|
+
boardSwitcherEl.closest(".board-switcher").classList.toggle("is-empty", boards.length <= 1);
|
|
1837
|
+
const currentPath = normalizePath(window.location.pathname);
|
|
1838
|
+
const options = boards.map((board) => {
|
|
1839
|
+
const option = document.createElement("option");
|
|
1840
|
+
option.value = board.url;
|
|
1841
|
+
option.textContent = boardOptionLabel(board);
|
|
1842
|
+
const boardPath = normalizePath(new URL(board.url, window.location.href).pathname);
|
|
1843
|
+
if (boardPath === currentPath) option.selected = true;
|
|
1844
|
+
return option;
|
|
1845
|
+
});
|
|
1846
|
+
boardSwitcherEl.replaceChildren(...options);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1010
1849
|
function renderColumn(column) {
|
|
1011
1850
|
const section = el("section", "column");
|
|
1012
1851
|
section.dataset.columnId = column.id;
|
|
@@ -1037,6 +1876,7 @@ function renderCard(task) {
|
|
|
1037
1876
|
|
|
1038
1877
|
const footer = el("div", "card-footer");
|
|
1039
1878
|
footer.append(el("span", "badge role", task.assignee || task.type || "PM"));
|
|
1879
|
+
if (task.subgoal) footer.append(subgoalBadge(task.subgoal));
|
|
1040
1880
|
if (task.receipt?.present) footer.append(el("span", "badge status-done", "Receipt"));
|
|
1041
1881
|
|
|
1042
1882
|
button.append(topline, el("h3", "task-title", task.title), footer);
|
|
@@ -1156,6 +1996,7 @@ function renderTaskDetail(task) {
|
|
|
1156
1996
|
grid.append(item);
|
|
1157
1997
|
}
|
|
1158
1998
|
root.append(grid);
|
|
1999
|
+
if (task.subgoal) root.append(renderSubgoal(task.subgoal));
|
|
1159
2000
|
root.append(detailText("Objective", task.objective));
|
|
1160
2001
|
root.append(detailList("Inputs", task.inputs));
|
|
1161
2002
|
root.append(detailList("Constraints", task.constraints));
|
|
@@ -1176,6 +2017,61 @@ function renderTaskDetail(task) {
|
|
|
1176
2017
|
return root;
|
|
1177
2018
|
}
|
|
1178
2019
|
|
|
2020
|
+
function renderSubgoal(subgoal) {
|
|
2021
|
+
const section = el("section", "detail-section subgoal-section");
|
|
2022
|
+
const header = el("div", "subgoal-header");
|
|
2023
|
+
const titleWrap = el("div");
|
|
2024
|
+
const board = subgoal.board;
|
|
2025
|
+
titleWrap.append(
|
|
2026
|
+
el("h3", "subgoal-title", board?.goal?.title || "Sub-goal"),
|
|
2027
|
+
el("p", "subgoal-meta", [
|
|
2028
|
+
subgoal.path,
|
|
2029
|
+
subgoal.owner ? \`owner: \${subgoal.owner}\` : "",
|
|
2030
|
+
subgoal.depth ? \`depth: \${subgoal.depth}\` : "",
|
|
2031
|
+
].filter(Boolean).join(" · ")),
|
|
2032
|
+
);
|
|
2033
|
+
header.append(titleWrap, subgoalBadge(subgoal));
|
|
2034
|
+
section.append(header);
|
|
2035
|
+
|
|
2036
|
+
if (!board?.columns?.length) {
|
|
2037
|
+
section.append(el("p", "", "No child board payload."));
|
|
2038
|
+
return section;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const boardEl = el("div", "subgoal-board");
|
|
2042
|
+
for (const column of board.columns) {
|
|
2043
|
+
const columnEl = el("section", "subgoal-column");
|
|
2044
|
+
const columnHeader = el("header", "subgoal-column-header");
|
|
2045
|
+
columnHeader.append(el("h4", "", column.title), el("span", "column-count", String(column.tasks.length)));
|
|
2046
|
+
const list = el("div", "subgoal-card-list");
|
|
2047
|
+
if (column.tasks.length === 0) {
|
|
2048
|
+
list.append(el("p", "empty", "No cards"));
|
|
2049
|
+
} else {
|
|
2050
|
+
for (const task of column.tasks) list.append(renderSubgoalTask(task));
|
|
2051
|
+
}
|
|
2052
|
+
columnEl.append(columnHeader, list);
|
|
2053
|
+
boardEl.append(columnEl);
|
|
2054
|
+
}
|
|
2055
|
+
section.append(boardEl);
|
|
2056
|
+
|
|
2057
|
+
if (subgoal.rollupReceipt) {
|
|
2058
|
+
section.append(detailText("Roll-up Receipt", subgoal.rollupReceipt));
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
return section;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function renderSubgoalTask(task) {
|
|
2065
|
+
const card = el("article", \`subgoal-task-card \${task.active ? "is-active" : ""}\`);
|
|
2066
|
+
const topline = el("div", "card-topline");
|
|
2067
|
+
topline.append(el("span", "task-id", task.id), statusBadge(task.status));
|
|
2068
|
+
const footer = el("div", "card-footer");
|
|
2069
|
+
footer.append(el("span", "badge role", task.assignee || task.type || "PM"));
|
|
2070
|
+
if (task.receipt?.present) footer.append(el("span", "badge status-done", "Receipt"));
|
|
2071
|
+
card.append(topline, el("h4", "subgoal-task-title", task.title), footer);
|
|
2072
|
+
return card;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
1179
2075
|
function detailText(title, value) {
|
|
1180
2076
|
const section = el("section", "detail-section");
|
|
1181
2077
|
section.append(el("h3", "", title), el("p", "", value || "None"));
|
|
@@ -1200,9 +2096,24 @@ function statusBadge(status) {
|
|
|
1200
2096
|
return el("span", \`badge status-\${status}\`, label);
|
|
1201
2097
|
}
|
|
1202
2098
|
|
|
2099
|
+
function subgoalBadge(subgoal) {
|
|
2100
|
+
return el("span", \`badge subgoal status-\${subgoal.status}\`, \`Sub-goal \${subgoal.status || "linked"}\`);
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1203
2103
|
function setLiveState(text, live) {
|
|
1204
2104
|
liveStateEl.textContent = text;
|
|
1205
|
-
|
|
2105
|
+
liveDotEl.classList.toggle("offline", !live);
|
|
2106
|
+
settingsButtonEl.setAttribute("aria-label", \`Settings. Board status: \${text}\`);
|
|
2107
|
+
settingsButtonEl.title = \`Settings · \${text}\`;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
function normalizePath(pathname) {
|
|
2111
|
+
return pathname.endsWith("/") ? pathname : pathname + "/";
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function boardOptionLabel(board) {
|
|
2115
|
+
const title = board.title || board.slug || board.goalDir || "GoalBuddy board";
|
|
2116
|
+
return /[/\\\\]subgoals[/\\\\]/.test(board.goalDir || "") ? \`Child: \${title}\` : title;
|
|
1206
2117
|
}
|
|
1207
2118
|
|
|
1208
2119
|
function el(tag, className = "", text = "") {
|
|
@@ -1212,9 +2123,14 @@ function el(tag, className = "", text = "") {
|
|
|
1212
2123
|
return node;
|
|
1213
2124
|
}
|
|
1214
2125
|
|
|
1215
|
-
|
|
2126
|
+
loadSettings()
|
|
2127
|
+
.then(loadBoard)
|
|
1216
2128
|
.then(() => {
|
|
1217
2129
|
setLiveState("Live", true);
|
|
2130
|
+
rememberCurrentBoard();
|
|
2131
|
+
loadGithubStars();
|
|
2132
|
+
loadBoardSwitcher();
|
|
2133
|
+
window.setInterval(loadBoardSwitcher, 5000);
|
|
1218
2134
|
connectEvents();
|
|
1219
2135
|
})
|
|
1220
2136
|
.catch((error) => {
|