@tenonhq/dovetail-dashboard 0.0.17 → 0.0.20
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/package.json +7 -2
- package/public/app.js +28 -0
- package/public/claude-plans.css +121 -0
- package/public/claude-plans.html +36 -1
- package/public/claude-plans.js +209 -18
- package/public/index.html +13 -0
- package/public/styles.css +28 -0
- package/public/tokens.css +61 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenonhq/dovetail-dashboard",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Update Set Dashboard for Dovetail",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"scripts": {
|
|
@@ -18,10 +18,15 @@
|
|
|
18
18
|
"tough-cookie": "^4.1.3"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=22"
|
|
22
22
|
},
|
|
23
23
|
"license": "ISC",
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/TenonHQ/Dovetail.git",
|
|
30
|
+
"directory": "packages/dashboard"
|
|
26
31
|
}
|
|
27
32
|
}
|
package/public/app.js
CHANGED
|
@@ -1046,3 +1046,31 @@ document.getElementById("task-search").addEventListener("input", function (e) {
|
|
|
1046
1046
|
|
|
1047
1047
|
// Render task toggles (My Tasks)
|
|
1048
1048
|
renderTaskToggles();
|
|
1049
|
+
|
|
1050
|
+
// --- Theme toggle ---
|
|
1051
|
+
|
|
1052
|
+
function currentTheme() {
|
|
1053
|
+
return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function applyTheme(theme) {
|
|
1057
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
1058
|
+
try {
|
|
1059
|
+
localStorage.setItem("cp-theme", theme);
|
|
1060
|
+
} catch (e) {}
|
|
1061
|
+
var btn = document.getElementById("cp-theme-toggle");
|
|
1062
|
+
if (btn) {
|
|
1063
|
+
btn.textContent = theme === "dark" ? "☀︎" : "☾";
|
|
1064
|
+
btn.title = theme === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
(function setupTheme() {
|
|
1069
|
+
applyTheme(currentTheme());
|
|
1070
|
+
var btn = document.getElementById("cp-theme-toggle");
|
|
1071
|
+
if (btn) {
|
|
1072
|
+
btn.addEventListener("click", function () {
|
|
1073
|
+
applyTheme(currentTheme() === "dark" ? "light" : "dark");
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
})();
|
package/public/claude-plans.css
CHANGED
|
@@ -82,6 +82,70 @@
|
|
|
82
82
|
font-size: 12px;
|
|
83
83
|
color: var(--brand);
|
|
84
84
|
}
|
|
85
|
+
.cp-rail-no-match {
|
|
86
|
+
padding: 20px 16px;
|
|
87
|
+
color: var(--fg-muted);
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
line-height: 1.55;
|
|
90
|
+
}
|
|
91
|
+
.cp-rail-no-match-q {
|
|
92
|
+
font-family: var(--font-mono);
|
|
93
|
+
color: var(--fg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── Search box ─────────────────────────────────────────────────────────── */
|
|
97
|
+
.cp-search {
|
|
98
|
+
position: relative;
|
|
99
|
+
padding: 10px 12px;
|
|
100
|
+
border-bottom: 1px solid var(--border);
|
|
101
|
+
background: var(--bg-paper);
|
|
102
|
+
}
|
|
103
|
+
.cp-search-input {
|
|
104
|
+
width: 100%;
|
|
105
|
+
box-sizing: border-box;
|
|
106
|
+
padding: 7px 28px 7px 10px;
|
|
107
|
+
font-size: 13px;
|
|
108
|
+
font-family: inherit;
|
|
109
|
+
color: var(--fg);
|
|
110
|
+
background: var(--bg);
|
|
111
|
+
border: 1px solid var(--border);
|
|
112
|
+
border-radius: var(--radius-sm);
|
|
113
|
+
outline: none;
|
|
114
|
+
transition: border-color var(--dur-1) var(--ease-out),
|
|
115
|
+
box-shadow var(--dur-1) var(--ease-out);
|
|
116
|
+
}
|
|
117
|
+
.cp-search-input::placeholder { color: var(--fg-muted); }
|
|
118
|
+
.cp-search-input:focus {
|
|
119
|
+
border-color: var(--accent);
|
|
120
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent);
|
|
121
|
+
}
|
|
122
|
+
/* Hide the native clear "x" on WebKit since we render our own button. */
|
|
123
|
+
.cp-search-input::-webkit-search-cancel-button { -webkit-appearance: none; }
|
|
124
|
+
|
|
125
|
+
.cp-search-clear {
|
|
126
|
+
position: absolute;
|
|
127
|
+
right: 16px;
|
|
128
|
+
top: 50%;
|
|
129
|
+
transform: translateY(-50%);
|
|
130
|
+
width: 20px;
|
|
131
|
+
height: 20px;
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
background: transparent;
|
|
136
|
+
border: 0;
|
|
137
|
+
color: var(--fg-muted);
|
|
138
|
+
font-size: 16px;
|
|
139
|
+
line-height: 1;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
border-radius: var(--radius-xs);
|
|
142
|
+
padding: 0;
|
|
143
|
+
}
|
|
144
|
+
.cp-search-clear:hover {
|
|
145
|
+
background: var(--bg-muted);
|
|
146
|
+
color: var(--fg);
|
|
147
|
+
}
|
|
148
|
+
|
|
85
149
|
.cp-list { list-style: none; }
|
|
86
150
|
.cp-list-item {
|
|
87
151
|
padding: 12px 16px;
|
|
@@ -266,6 +330,25 @@
|
|
|
266
330
|
border-radius: var(--radius-md);
|
|
267
331
|
white-space: pre-wrap;
|
|
268
332
|
}
|
|
333
|
+
.cp-mermaid-fallback {
|
|
334
|
+
background: var(--bg-paper);
|
|
335
|
+
border: 1px solid var(--border);
|
|
336
|
+
border-radius: var(--radius-md);
|
|
337
|
+
padding: 12px;
|
|
338
|
+
}
|
|
339
|
+
.cp-mermaid-fallback-note {
|
|
340
|
+
color: var(--error-700);
|
|
341
|
+
font-size: 12px;
|
|
342
|
+
margin-bottom: 8px;
|
|
343
|
+
}
|
|
344
|
+
.cp-mermaid-fallback pre {
|
|
345
|
+
margin: 0;
|
|
346
|
+
overflow: auto;
|
|
347
|
+
font-family: var(--font-mono);
|
|
348
|
+
font-size: 12px;
|
|
349
|
+
line-height: 1.5;
|
|
350
|
+
white-space: pre;
|
|
351
|
+
}
|
|
269
352
|
|
|
270
353
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
271
354
|
cp-c-* Structured Component Library
|
|
@@ -961,3 +1044,41 @@
|
|
|
961
1044
|
opacity: 1;
|
|
962
1045
|
transform: translateY(0);
|
|
963
1046
|
}
|
|
1047
|
+
|
|
1048
|
+
/* ════════════════════════════════════════════════════════════════════════════
|
|
1049
|
+
Dark theme overrides.
|
|
1050
|
+
The page already flows through the semantic tokens, so only the semantic
|
|
1051
|
+
color chips that hardcode light pastels need explicit dark variants here.
|
|
1052
|
+
The .cp-theme-toggle button itself is styled in styles.css (shared).
|
|
1053
|
+
════════════════════════════════════════════════════════════════════════════ */
|
|
1054
|
+
[data-theme="dark"] .cp-status-DRAFT { background: #322d0c; color: #e8d860; border-color: #f4dc0033; }
|
|
1055
|
+
[data-theme="dark"] .cp-status-APPROVED { background: #14301c; color: #8fe39d; border-color: #62dc4433; }
|
|
1056
|
+
|
|
1057
|
+
[data-theme="dark"] .cp-c-badge-success { background: #14301c; color: #8fe39d; border-color: #62dc4433; }
|
|
1058
|
+
[data-theme="dark"] .cp-c-badge-warning { background: #322d0c; color: #e8d860; border-color: #f4dc0033; }
|
|
1059
|
+
[data-theme="dark"] .cp-c-badge-danger { background: #371616; color: #f08f8f; border-color: #f4393933; }
|
|
1060
|
+
[data-theme="dark"] .cp-c-badge-info { background: #0e2c3a; color: #8fd9f5; border-color: #76d6ff33; }
|
|
1061
|
+
|
|
1062
|
+
[data-theme="dark"] .cp-c-callout-info { background: #0e2c3a55; border-color: #76d6ff33; }
|
|
1063
|
+
[data-theme="dark"] .cp-c-callout-success { background: #14301c55; border-color: #62dc4433; }
|
|
1064
|
+
[data-theme="dark"] .cp-c-callout-warning { background: #322d0c55; border-color: #f4dc0033; }
|
|
1065
|
+
[data-theme="dark"] .cp-c-callout-danger { background: #37161655; border-color: #f4393933; }
|
|
1066
|
+
[data-theme="dark"] .cp-c-callout-info .cp-c-callout-icon { color: #8fd9f5; }
|
|
1067
|
+
[data-theme="dark"] .cp-c-callout-success .cp-c-callout-icon { color: #8fe39d; }
|
|
1068
|
+
[data-theme="dark"] .cp-c-callout-warning .cp-c-callout-icon { color: #e8d860; }
|
|
1069
|
+
[data-theme="dark"] .cp-c-callout-danger .cp-c-callout-icon { color: #f08f8f; }
|
|
1070
|
+
|
|
1071
|
+
[data-theme="dark"] .cp-mermaid-error {
|
|
1072
|
+
background: #37161655;
|
|
1073
|
+
border-color: #f4393933;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
[data-theme="dark"] .cp-pr-badge {
|
|
1077
|
+
background: #0e2c3a;
|
|
1078
|
+
color: #8fd9f5;
|
|
1079
|
+
border-color: #76d6ff33;
|
|
1080
|
+
}
|
|
1081
|
+
[data-theme="dark"] .cp-pr-badge:hover {
|
|
1082
|
+
background: #14384a;
|
|
1083
|
+
color: #b6e8ff;
|
|
1084
|
+
}
|
package/public/claude-plans.html
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Dovetail — Claude Plans</title>
|
|
7
|
+
<script>
|
|
8
|
+
/* Apply the saved theme before first paint to avoid a flash of light. */
|
|
9
|
+
(function () {
|
|
10
|
+
try {
|
|
11
|
+
var t = localStorage.getItem("cp-theme");
|
|
12
|
+
if (t === "dark" || t === "light") {
|
|
13
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
14
|
+
}
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
})();
|
|
17
|
+
</script>
|
|
7
18
|
<link rel="stylesheet" href="tokens.css" />
|
|
8
19
|
<link rel="stylesheet" href="styles.css" />
|
|
9
20
|
<link rel="stylesheet" href="claude-plans.css" />
|
|
@@ -19,6 +30,8 @@
|
|
|
19
30
|
</div>
|
|
20
31
|
<div class="header-right">
|
|
21
32
|
<a class="cp-nav-link" href="/">← Update Sets</a>
|
|
33
|
+
<button class="cp-theme-toggle" id="cp-theme-toggle" type="button"
|
|
34
|
+
aria-label="Toggle dark mode" title="Toggle dark mode"></button>
|
|
22
35
|
<span class="cp-storage" id="cp-storage" title="storage root"></span>
|
|
23
36
|
</div>
|
|
24
37
|
</header>
|
|
@@ -29,10 +42,32 @@
|
|
|
29
42
|
<span class="cp-rail-title">Plans</span>
|
|
30
43
|
<span class="cp-count" id="cp-count">0</span>
|
|
31
44
|
</div>
|
|
45
|
+
<div class="cp-search">
|
|
46
|
+
<input
|
|
47
|
+
type="search"
|
|
48
|
+
class="cp-search-input"
|
|
49
|
+
id="cp-search-input"
|
|
50
|
+
placeholder="Search plans… (press / to focus)"
|
|
51
|
+
aria-label="Search plans by title, slug, status, or content"
|
|
52
|
+
autocomplete="off"
|
|
53
|
+
spellcheck="false"
|
|
54
|
+
/>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
class="cp-search-clear"
|
|
58
|
+
id="cp-search-clear"
|
|
59
|
+
aria-label="Clear search"
|
|
60
|
+
title="Clear (Esc)"
|
|
61
|
+
hidden
|
|
62
|
+
>×</button>
|
|
63
|
+
</div>
|
|
32
64
|
<div class="cp-rail-empty" id="cp-rail-empty">
|
|
33
65
|
Waiting for plans. Run <code>push_plan</code> from a Claude Code
|
|
34
66
|
session to populate this list.
|
|
35
67
|
</div>
|
|
68
|
+
<div class="cp-rail-no-match" id="cp-rail-no-match" hidden>
|
|
69
|
+
No plans match <span class="cp-rail-no-match-q" id="cp-rail-no-match-q"></span>.
|
|
70
|
+
</div>
|
|
36
71
|
<ul class="cp-list" id="cp-list"></ul>
|
|
37
72
|
</aside>
|
|
38
73
|
|
|
@@ -60,7 +95,7 @@
|
|
|
60
95
|
|
|
61
96
|
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
|
|
62
97
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.5/dist/purify.min.js"></script>
|
|
63
|
-
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.
|
|
98
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.6/dist/mermaid.min.js"></script>
|
|
64
99
|
<script src="claude-plans.js"></script>
|
|
65
100
|
</body>
|
|
66
101
|
</html>
|
package/public/claude-plans.js
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
/* /claude-plans page logic.
|
|
2
2
|
* - Fetches initial state from REST.
|
|
3
3
|
* - Subscribes to /api/claude-plans/stream (SSE) for live updates.
|
|
4
|
-
* - Renders markdown via marked + DOMPurify, Mermaid via mermaid.
|
|
4
|
+
* - Renders markdown via marked + DOMPurify, Mermaid via mermaid.parse()/render()
|
|
5
|
+
* with a graceful raw-source fallback when a diagram fails to parse.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
(function () {
|
|
8
9
|
"use strict";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
function currentTheme() {
|
|
12
|
+
return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
|
12
13
|
}
|
|
14
|
+
|
|
15
|
+
function initMermaid() {
|
|
16
|
+
if (window.mermaid && typeof window.mermaid.initialize === "function") {
|
|
17
|
+
window.mermaid.initialize({
|
|
18
|
+
startOnLoad: false,
|
|
19
|
+
theme: currentTheme() === "dark" ? "dark" : "default",
|
|
20
|
+
securityLevel: "strict",
|
|
21
|
+
// Never inject mermaid's "Syntax error in text" bomb SVG into the page.
|
|
22
|
+
// Bad sources are handled by renderMermaid's graceful fallback instead.
|
|
23
|
+
suppressErrorRendering: true
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
initMermaid();
|
|
28
|
+
|
|
13
29
|
if (window.marked && typeof window.marked.setOptions === "function") {
|
|
14
30
|
window.marked.setOptions({ breaks: true, gfm: true });
|
|
15
31
|
}
|
|
@@ -18,7 +34,8 @@
|
|
|
18
34
|
plans: new Map(), // slug -> plan
|
|
19
35
|
artifacts: new Map(), // slug -> Map<artifactSlug, artifact>
|
|
20
36
|
selectedSlug: null,
|
|
21
|
-
activeTab: "plan"
|
|
37
|
+
activeTab: "plan",
|
|
38
|
+
query: "" // lowercased search query; "" means show all
|
|
22
39
|
};
|
|
23
40
|
|
|
24
41
|
var els = {
|
|
@@ -26,6 +43,10 @@
|
|
|
26
43
|
list: document.getElementById("cp-list"),
|
|
27
44
|
count: document.getElementById("cp-count"),
|
|
28
45
|
railEmpty: document.getElementById("cp-rail-empty"),
|
|
46
|
+
railNoMatch: document.getElementById("cp-rail-no-match"),
|
|
47
|
+
railNoMatchQ: document.getElementById("cp-rail-no-match-q"),
|
|
48
|
+
searchInput: document.getElementById("cp-search-input"),
|
|
49
|
+
searchClear: document.getElementById("cp-search-clear"),
|
|
29
50
|
detailEmpty: document.getElementById("cp-detail-empty"),
|
|
30
51
|
detailBody: document.getElementById("cp-detail-body"),
|
|
31
52
|
detailTitle: document.getElementById("cp-detail-title"),
|
|
@@ -37,12 +58,46 @@
|
|
|
37
58
|
tabs: document.querySelectorAll(".cp-tab")
|
|
38
59
|
};
|
|
39
60
|
|
|
40
|
-
|
|
61
|
+
// True when plan matches q across title, slug, status, plan markdown/html,
|
|
62
|
+
// pr title, and every artifact title + content. Lets you find a plan by any
|
|
63
|
+
// snippet that appears anywhere in or attached to it.
|
|
64
|
+
function planMatchesQuery(plan, q) {
|
|
65
|
+
if (!q) return true;
|
|
66
|
+
var haystacks = [
|
|
67
|
+
plan.title,
|
|
68
|
+
plan.slug,
|
|
69
|
+
plan.status,
|
|
70
|
+
plan.pr_title,
|
|
71
|
+
plan.content_md,
|
|
72
|
+
plan.content_html
|
|
73
|
+
];
|
|
74
|
+
var bucket = state.artifacts.get(plan.slug);
|
|
75
|
+
if (bucket) {
|
|
76
|
+
bucket.forEach(function (a) {
|
|
77
|
+
haystacks.push(a.title);
|
|
78
|
+
haystacks.push(a.content);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
for (var i = 0; i < haystacks.length; i++) {
|
|
82
|
+
var h = haystacks[i];
|
|
83
|
+
if (h && String(h).toLowerCase().indexOf(q) !== -1) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function allPlansSorted() {
|
|
41
89
|
return Array.from(state.plans.values()).sort(function (a, b) {
|
|
42
90
|
return (b.updated_at || "").localeCompare(a.updated_at || "");
|
|
43
91
|
});
|
|
44
92
|
}
|
|
45
93
|
|
|
94
|
+
function sortedPlans() {
|
|
95
|
+
var q = state.query;
|
|
96
|
+
var all = allPlansSorted();
|
|
97
|
+
if (!q) return all;
|
|
98
|
+
return all.filter(function (p) { return planMatchesQuery(p, q); });
|
|
99
|
+
}
|
|
100
|
+
|
|
46
101
|
function sortedArtifacts(slug) {
|
|
47
102
|
var map = state.artifacts.get(slug);
|
|
48
103
|
if (!map) return [];
|
|
@@ -87,27 +142,62 @@
|
|
|
87
142
|
target.innerHTML = window.DOMPurify.sanitize(html);
|
|
88
143
|
}
|
|
89
144
|
|
|
145
|
+
// Strip a wrapping markdown code fence and normalize whitespace/line endings.
|
|
146
|
+
// LLM-authored sources often arrive fenced (```mermaid … ```) or with CRLF/BOM,
|
|
147
|
+
// which the parser rejects on line 1.
|
|
148
|
+
function preprocessMermaid(src) {
|
|
149
|
+
var s = src == null ? "" : String(src);
|
|
150
|
+
s = s.replace(/\r\n/g, "\n").replace(/^\uFEFF/, "");
|
|
151
|
+
s = s.replace(/^\s*```[^\n]*\n/, "").replace(/\n```\s*$/, "");
|
|
152
|
+
return s.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Graceful fallback: show the raw source instead of mermaid's "Syntax error"
|
|
156
|
+
// bomb so one bad diagram never poisons the page.
|
|
157
|
+
function renderMermaidFallback(source, target) {
|
|
158
|
+
target.classList.remove("cp-mermaid");
|
|
159
|
+
target.classList.add("cp-mermaid-fallback");
|
|
160
|
+
target.textContent = "";
|
|
161
|
+
var note = document.createElement("div");
|
|
162
|
+
note.className = "cp-mermaid-fallback-note";
|
|
163
|
+
note.textContent = "⚠ Diagram failed to render — showing source";
|
|
164
|
+
var pre = document.createElement("pre");
|
|
165
|
+
var code = document.createElement("code");
|
|
166
|
+
code.textContent = source;
|
|
167
|
+
pre.appendChild(code);
|
|
168
|
+
target.appendChild(note);
|
|
169
|
+
target.appendChild(pre);
|
|
170
|
+
}
|
|
171
|
+
|
|
90
172
|
function renderMermaid(source, target) {
|
|
173
|
+
target.classList.remove("cp-mermaid-fallback", "cp-mermaid-error");
|
|
91
174
|
target.classList.add("cp-mermaid");
|
|
92
175
|
target.textContent = "";
|
|
93
176
|
if (!window.mermaid || typeof window.mermaid.render !== "function") {
|
|
94
|
-
target
|
|
177
|
+
renderMermaidFallback(source, target);
|
|
95
178
|
return;
|
|
96
179
|
}
|
|
180
|
+
var clean = preprocessMermaid(source);
|
|
97
181
|
var id = "mmd-" + Math.random().toString(36).slice(2, 10);
|
|
98
|
-
|
|
99
|
-
window.mermaid.render(id,
|
|
182
|
+
var doRender = function () {
|
|
183
|
+
window.mermaid.render(id, clean).then(
|
|
100
184
|
function (out) { target.innerHTML = out.svg; },
|
|
101
|
-
function (
|
|
102
|
-
target.classList.remove("cp-mermaid");
|
|
103
|
-
target.classList.add("cp-mermaid-error");
|
|
104
|
-
target.textContent = "mermaid error: " + (err && err.message ? err.message : String(err));
|
|
105
|
-
}
|
|
185
|
+
function () { renderMermaidFallback(source, target); }
|
|
106
186
|
);
|
|
187
|
+
};
|
|
188
|
+
try {
|
|
189
|
+
// Validate before rendering. suppressErrors makes parse resolve false
|
|
190
|
+
// (instead of throwing) on bad source, so we can fall back cleanly.
|
|
191
|
+
if (typeof window.mermaid.parse === "function") {
|
|
192
|
+
window.mermaid.parse(clean, { suppressErrors: true }).then(
|
|
193
|
+
function (ok) { if (ok) { doRender(); } else { renderMermaidFallback(source, target); } },
|
|
194
|
+
function () { renderMermaidFallback(source, target); }
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
doRender();
|
|
198
|
+
}
|
|
107
199
|
} catch (err) {
|
|
108
|
-
target
|
|
109
|
-
target.classList.add("cp-mermaid-error");
|
|
110
|
-
target.textContent = "mermaid error: " + err.message;
|
|
200
|
+
renderMermaidFallback(source, target);
|
|
111
201
|
}
|
|
112
202
|
}
|
|
113
203
|
|
|
@@ -191,13 +281,25 @@
|
|
|
191
281
|
|
|
192
282
|
function renderRail() {
|
|
193
283
|
var plans = sortedPlans();
|
|
194
|
-
|
|
284
|
+
var totalPlans = state.plans.size;
|
|
285
|
+
var isFiltering = !!state.query;
|
|
286
|
+
els.count.textContent = isFiltering
|
|
287
|
+
? plans.length + " / " + totalPlans
|
|
288
|
+
: String(totalPlans);
|
|
195
289
|
if (plans.length === 0) {
|
|
196
|
-
els.railEmpty.style.display = "block";
|
|
197
290
|
els.list.innerHTML = "";
|
|
291
|
+
if (isFiltering && totalPlans > 0) {
|
|
292
|
+
els.railEmpty.style.display = "none";
|
|
293
|
+
els.railNoMatch.hidden = false;
|
|
294
|
+
if (els.railNoMatchQ) els.railNoMatchQ.textContent = '"' + state.query + '"';
|
|
295
|
+
} else {
|
|
296
|
+
els.railEmpty.style.display = "block";
|
|
297
|
+
els.railNoMatch.hidden = true;
|
|
298
|
+
}
|
|
198
299
|
return;
|
|
199
300
|
}
|
|
200
301
|
els.railEmpty.style.display = "none";
|
|
302
|
+
els.railNoMatch.hidden = true;
|
|
201
303
|
els.list.innerHTML = "";
|
|
202
304
|
plans.forEach(function (plan) {
|
|
203
305
|
var artifactCount = (state.artifacts.get(plan.slug) || new Map()).size;
|
|
@@ -377,6 +479,69 @@
|
|
|
377
479
|
btn.addEventListener("click", function () { setActiveTab(btn.dataset.tab); });
|
|
378
480
|
});
|
|
379
481
|
|
|
482
|
+
/* ─── Search ───────────────────────────────────────────────────────────────
|
|
483
|
+
* Filter the rail by title/slug/status/content (plan + artifacts).
|
|
484
|
+
* 120ms debounce keeps typing snappy even with hundreds of plans.
|
|
485
|
+
*
|
|
486
|
+
* Selection rule: when filtering narrows the rail, only auto-select a new
|
|
487
|
+
* plan if the currently selected one is filtered out — picking the first
|
|
488
|
+
* visible match. Don't yank the user off a plan they're already reading.
|
|
489
|
+
*/
|
|
490
|
+
var searchDebounce = null;
|
|
491
|
+
function applyQuery(raw) {
|
|
492
|
+
var q = (raw == null ? "" : String(raw)).trim().toLowerCase();
|
|
493
|
+
state.query = q;
|
|
494
|
+
if (els.searchClear) els.searchClear.hidden = q.length === 0;
|
|
495
|
+
renderRail();
|
|
496
|
+
if (q) {
|
|
497
|
+
var visible = sortedPlans();
|
|
498
|
+
var stillVisible = state.selectedSlug &&
|
|
499
|
+
visible.some(function (p) { return p.slug === state.selectedSlug; });
|
|
500
|
+
if (!stillVisible && visible.length > 0) {
|
|
501
|
+
selectPlan(visible[0].slug);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function setupSearch() {
|
|
507
|
+
if (els.searchInput) {
|
|
508
|
+
els.searchInput.addEventListener("input", function (e) {
|
|
509
|
+
var v = e.target.value;
|
|
510
|
+
if (searchDebounce) clearTimeout(searchDebounce);
|
|
511
|
+
searchDebounce = setTimeout(function () { applyQuery(v); }, 120);
|
|
512
|
+
});
|
|
513
|
+
els.searchInput.addEventListener("keydown", function (e) {
|
|
514
|
+
if (e.key === "Escape") {
|
|
515
|
+
e.preventDefault();
|
|
516
|
+
els.searchInput.value = "";
|
|
517
|
+
applyQuery("");
|
|
518
|
+
els.searchInput.blur();
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
if (els.searchClear) {
|
|
523
|
+
els.searchClear.addEventListener("click", function () {
|
|
524
|
+
if (els.searchInput) els.searchInput.value = "";
|
|
525
|
+
applyQuery("");
|
|
526
|
+
if (els.searchInput) els.searchInput.focus();
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
// Press "/" anywhere (except in another input) to focus the search box.
|
|
530
|
+
document.addEventListener("keydown", function (e) {
|
|
531
|
+
if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) return;
|
|
532
|
+
var t = e.target;
|
|
533
|
+
var tag = t && t.tagName;
|
|
534
|
+
var isEditable = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" ||
|
|
535
|
+
(t && t.isContentEditable);
|
|
536
|
+
if (isEditable) return;
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
if (els.searchInput) {
|
|
539
|
+
els.searchInput.focus();
|
|
540
|
+
els.searchInput.select();
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
380
545
|
function upsertPlan(plan) {
|
|
381
546
|
state.plans.set(plan.slug, plan);
|
|
382
547
|
renderRail();
|
|
@@ -472,7 +637,33 @@
|
|
|
472
637
|
es.onerror = function () { /* EventSource auto-reconnects */ };
|
|
473
638
|
}
|
|
474
639
|
|
|
640
|
+
/* ─── Theme toggle ─────────────────────────────────────────────────────────── */
|
|
641
|
+
|
|
642
|
+
function applyTheme(theme) {
|
|
643
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
644
|
+
try { localStorage.setItem("cp-theme", theme); } catch (_) {}
|
|
645
|
+
var btn = document.getElementById("cp-theme-toggle");
|
|
646
|
+
if (btn) {
|
|
647
|
+
btn.textContent = theme === "dark" ? "☀︎" : "☾";
|
|
648
|
+
btn.title = theme === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
|
649
|
+
}
|
|
650
|
+
initMermaid();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function setupTheme() {
|
|
654
|
+
applyTheme(currentTheme());
|
|
655
|
+
var btn = document.getElementById("cp-theme-toggle");
|
|
656
|
+
if (btn) {
|
|
657
|
+
btn.addEventListener("click", function () {
|
|
658
|
+
applyTheme(currentTheme() === "dark" ? "light" : "dark");
|
|
659
|
+
if (state.selectedSlug) renderDetail();
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
475
664
|
document.addEventListener("DOMContentLoaded", function () {
|
|
665
|
+
setupTheme();
|
|
666
|
+
setupSearch();
|
|
476
667
|
setActiveTab("plan");
|
|
477
668
|
loadInitial().then(startStream);
|
|
478
669
|
});
|
package/public/index.html
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Dovetail — Update Sets</title>
|
|
7
|
+
<script>
|
|
8
|
+
/* Apply the saved theme before first paint to avoid a flash of light. */
|
|
9
|
+
(function () {
|
|
10
|
+
try {
|
|
11
|
+
var t = localStorage.getItem("cp-theme");
|
|
12
|
+
if (t === "dark" || t === "light") {
|
|
13
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
14
|
+
}
|
|
15
|
+
} catch (e) {}
|
|
16
|
+
})();
|
|
17
|
+
</script>
|
|
7
18
|
<link rel="stylesheet" href="tokens.css">
|
|
8
19
|
<link rel="stylesheet" href="styles.css">
|
|
9
20
|
</head>
|
|
@@ -20,6 +31,8 @@
|
|
|
20
31
|
<div class="header-right">
|
|
21
32
|
<button class="btn-refresh" id="refresh-btn" title="Refresh scopes and update sets">Refresh</button>
|
|
22
33
|
<a class="btn-refresh" href="/claude-plans" title="Claude plans and diagrams pushed via MCP">Claude Plans</a>
|
|
34
|
+
<button class="cp-theme-toggle" id="cp-theme-toggle" type="button"
|
|
35
|
+
aria-label="Toggle dark mode" title="Toggle dark mode"></button>
|
|
23
36
|
<div class="active-task-chip" id="active-task-chip" style="display:none;"></div>
|
|
24
37
|
<div class="instance-badge" id="instance-badge">Loading...</div>
|
|
25
38
|
</div>
|
package/public/styles.css
CHANGED
|
@@ -887,3 +887,31 @@ header h1 span {
|
|
|
887
887
|
.recent-edit-dismiss:hover {
|
|
888
888
|
color: var(--danger);
|
|
889
889
|
}
|
|
890
|
+
|
|
891
|
+
/* ── Theme toggle button (header) — shared by dashboard + Claude Plans ──── */
|
|
892
|
+
.cp-theme-toggle {
|
|
893
|
+
display: inline-flex;
|
|
894
|
+
align-items: center;
|
|
895
|
+
justify-content: center;
|
|
896
|
+
width: 32px;
|
|
897
|
+
height: 32px;
|
|
898
|
+
padding: 0;
|
|
899
|
+
font-size: 14px;
|
|
900
|
+
line-height: 1;
|
|
901
|
+
background: var(--surface-raised);
|
|
902
|
+
color: var(--fg-muted);
|
|
903
|
+
border: 1px solid var(--border-strong);
|
|
904
|
+
border-radius: var(--radius-md);
|
|
905
|
+
cursor: pointer;
|
|
906
|
+
transition: background var(--dur-1) var(--ease-out),
|
|
907
|
+
color var(--dur-1) var(--ease-out);
|
|
908
|
+
}
|
|
909
|
+
.cp-theme-toggle:hover {
|
|
910
|
+
background: var(--bg-subtle);
|
|
911
|
+
color: var(--fg);
|
|
912
|
+
}
|
|
913
|
+
.cp-theme-toggle:focus-visible {
|
|
914
|
+
outline: none;
|
|
915
|
+
border-color: var(--border-focus);
|
|
916
|
+
box-shadow: 0 0 0 3px rgba(50, 181, 127, 0.18);
|
|
917
|
+
}
|
package/public/tokens.css
CHANGED
|
@@ -160,3 +160,64 @@
|
|
|
160
160
|
--av-pink-bg: #fb61fe; --av-pink-fg: #fbf8f3;
|
|
161
161
|
--av-earthy-bg: #dcd4c0; --av-earthy-fg: #987e59;
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/* ============================================================================
|
|
165
|
+
Dark theme — activated by <html data-theme="dark">.
|
|
166
|
+
Used by the Claude Plans page; safe to adopt dashboard-wide later.
|
|
167
|
+
Overrides the semantic aliases only — the raw color scales above are shared.
|
|
168
|
+
Brand emerald and neon accent are preserved; grounds invert to dark woodgrain.
|
|
169
|
+
============================================================================ */
|
|
170
|
+
[data-theme="dark"] {
|
|
171
|
+
--bg: #0d1410;
|
|
172
|
+
--bg-subtle: #121c16;
|
|
173
|
+
--bg-muted: #18241c;
|
|
174
|
+
--bg-paper: #121c16;
|
|
175
|
+
--bg-oak: #20302554;
|
|
176
|
+
--bg-deep: #060b08;
|
|
177
|
+
--bg-overlay: rgba(0, 0, 0, 0.66);
|
|
178
|
+
|
|
179
|
+
--surface: #121c16;
|
|
180
|
+
--surface-raised: #18241c;
|
|
181
|
+
|
|
182
|
+
--fg: #e9efe9;
|
|
183
|
+
--fg-muted: #8da095;
|
|
184
|
+
--fg-subtle: #5f7167;
|
|
185
|
+
--fg-on-dark: #e9efe9;
|
|
186
|
+
--fg-on-neon: var(--emerald-700);
|
|
187
|
+
|
|
188
|
+
--accent: var(--neon-300);
|
|
189
|
+
--accent-hover: var(--neon-500);
|
|
190
|
+
--accent-press: var(--neon-700);
|
|
191
|
+
--accent-fg: #0d1410;
|
|
192
|
+
|
|
193
|
+
--brand: var(--emerald-50);
|
|
194
|
+
--brand-deep: #e9efe9;
|
|
195
|
+
|
|
196
|
+
--success: var(--success-300);
|
|
197
|
+
--warning: var(--warning-300);
|
|
198
|
+
--danger: var(--error-300);
|
|
199
|
+
--danger-hover: var(--error-500);
|
|
200
|
+
--info: var(--blue-300);
|
|
201
|
+
|
|
202
|
+
--border: #26342b;
|
|
203
|
+
--border-strong: #344538;
|
|
204
|
+
--border-subtle: #1d2922;
|
|
205
|
+
--border-focus: var(--neon-500);
|
|
206
|
+
|
|
207
|
+
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
208
|
+
--shadow-2: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
209
|
+
--shadow-3: 0 10px 34px rgba(0, 0, 0, 0.6);
|
|
210
|
+
--shadow-pop: 0 12px 40px rgba(0, 0, 0, 0.7);
|
|
211
|
+
|
|
212
|
+
/* ===== Tag color pairs — dark grounds, light ink ===================== */
|
|
213
|
+
--tag-green-bg: #14301c; --tag-green-fg: #8fe39d;
|
|
214
|
+
--tag-blue-bg: #182040; --tag-blue-fg: #aab4ff;
|
|
215
|
+
--tag-cyan-bg: #0e2c3a; --tag-cyan-fg: #8fd9f5;
|
|
216
|
+
--tag-sage-bg: #1d2922; --tag-sage-fg: #b6c6bc;
|
|
217
|
+
--tag-warm-bg: #2c2417; --tag-warm-fg: #d8c4a2;
|
|
218
|
+
--tag-yellow-bg: #322d0c; --tag-yellow-fg: #e8d860;
|
|
219
|
+
--tag-purple-bg: #2a1633; --tag-purple-fg: #e29bf0;
|
|
220
|
+
--tag-orange-bg: #331c0c; --tag-orange-fg: #f0a878;
|
|
221
|
+
--tag-red-bg: #371616; --tag-red-fg: #f08f8f;
|
|
222
|
+
--tag-teal-bg: #103027; --tag-teal-fg: #6fd1ad;
|
|
223
|
+
}
|