@grainulation/wheat 1.0.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/LICENSE +21 -0
- package/README.md +136 -0
- package/bin/wheat.js +193 -0
- package/compiler/detect-sprints.js +319 -0
- package/compiler/generate-manifest.js +280 -0
- package/compiler/wheat-compiler.js +1229 -0
- package/lib/compiler.js +35 -0
- package/lib/connect.js +418 -0
- package/lib/disconnect.js +188 -0
- package/lib/guard.js +151 -0
- package/lib/index.js +14 -0
- package/lib/init.js +457 -0
- package/lib/install-prompt.js +186 -0
- package/lib/quickstart.js +276 -0
- package/lib/serve-mcp.js +509 -0
- package/lib/server.js +391 -0
- package/lib/stats.js +184 -0
- package/lib/status.js +135 -0
- package/lib/update.js +71 -0
- package/package.json +53 -0
- package/public/index.html +1798 -0
- package/templates/claude.md +122 -0
- package/templates/commands/blind-spot.md +47 -0
- package/templates/commands/brief.md +73 -0
- package/templates/commands/calibrate.md +39 -0
- package/templates/commands/challenge.md +72 -0
- package/templates/commands/connect.md +104 -0
- package/templates/commands/evaluate.md +80 -0
- package/templates/commands/feedback.md +60 -0
- package/templates/commands/handoff.md +53 -0
- package/templates/commands/init.md +68 -0
- package/templates/commands/merge.md +51 -0
- package/templates/commands/present.md +52 -0
- package/templates/commands/prototype.md +68 -0
- package/templates/commands/replay.md +61 -0
- package/templates/commands/research.md +73 -0
- package/templates/commands/resolve.md +42 -0
- package/templates/commands/status.md +56 -0
- package/templates/commands/witness.md +79 -0
- package/templates/explainer.html +343 -0
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" dir="auto">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
7
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
8
|
+
<title>Wheat</title>
|
|
9
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect width='64' height='64' rx='14' fill='%230a0e1a'/><text x='32' y='34' text-anchor='middle' dominant-baseline='central' fill='%23fbbf24' font-family='-apple-system,system-ui,sans-serif' font-size='34' font-weight='800'>W</text></svg>">
|
|
10
|
+
<style>
|
|
11
|
+
/* ── Design tokens ── */
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #0a0e1a;
|
|
14
|
+
--bg2: #111827;
|
|
15
|
+
--bg3: #1e293b;
|
|
16
|
+
--bg4: #334155;
|
|
17
|
+
--fg: #e2e8f0;
|
|
18
|
+
--fg2: #94a3b8;
|
|
19
|
+
--fg3: #64748b;
|
|
20
|
+
--accent: #fbbf24;
|
|
21
|
+
--accent-light: #f59e0b;
|
|
22
|
+
--accent-dim: rgba(251, 191, 36, 0.10);
|
|
23
|
+
--accent-border: rgba(251, 191, 36, 0.25);
|
|
24
|
+
--green: #34d399;
|
|
25
|
+
--red: #f87171;
|
|
26
|
+
--blue: #60a5fa;
|
|
27
|
+
--purple: #a78bfa;
|
|
28
|
+
--orange: #fb923c;
|
|
29
|
+
--cyan: #22d3ee;
|
|
30
|
+
--yellow: #facc15;
|
|
31
|
+
--border: #1e293b;
|
|
32
|
+
--border-subtle: rgba(255,255,255,0.08);
|
|
33
|
+
--space-xs: 4px; --space-sm: 8px; --space-md: 12px; --space-lg: 16px; --space-xl: 24px; --space-2xl: 32px;
|
|
34
|
+
--radius: 8px;
|
|
35
|
+
--radius-sm: 4px;
|
|
36
|
+
--radius-lg: 12px;
|
|
37
|
+
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
|
|
38
|
+
--font-mono: 'SF Mono', 'Cascadia Code', 'JetBrains Mono', 'Fira Code', monospace;
|
|
39
|
+
--transition-fast: 0.1s ease;
|
|
40
|
+
--transition-base: 0.15s ease;
|
|
41
|
+
}
|
|
42
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
43
|
+
html, body { height: 100%; overflow: hidden; }
|
|
44
|
+
body {
|
|
45
|
+
font-family: var(--font-sans);
|
|
46
|
+
background: var(--bg);
|
|
47
|
+
color: var(--fg);
|
|
48
|
+
font-size: 13px;
|
|
49
|
+
line-height: 1.5;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ── Layout ── */
|
|
53
|
+
.app {
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-rows: auto auto 1fr auto 32px;
|
|
56
|
+
grid-template-columns: 240px 1fr 320px;
|
|
57
|
+
height: 100vh;
|
|
58
|
+
}
|
|
59
|
+
.toolbar { grid-column: 1 / -1; grid-row: 1; }
|
|
60
|
+
.phase-bar { grid-column: 1 / -1; grid-row: 2; }
|
|
61
|
+
.sidebar { grid-row: 3; grid-column: 1; }
|
|
62
|
+
.main { grid-row: 3; grid-column: 2; }
|
|
63
|
+
.detail { grid-row: 3 / 5; grid-column: 3; }
|
|
64
|
+
.compilation-bar { grid-column: 1 / 3; grid-row: 4; }
|
|
65
|
+
.statusbar { grid-column: 1 / -1; grid-row: 5; }
|
|
66
|
+
|
|
67
|
+
/* detail collapsed */
|
|
68
|
+
.app.detail-collapsed {
|
|
69
|
+
grid-template-columns: 240px 1fr 0;
|
|
70
|
+
}
|
|
71
|
+
.app.detail-collapsed .detail { display: none; }
|
|
72
|
+
.app.detail-collapsed .compilation-bar { grid-column: 1 / -1; }
|
|
73
|
+
|
|
74
|
+
/* ── Toolbar ── */
|
|
75
|
+
.toolbar {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: 10px;
|
|
79
|
+
padding: 4px 24px;
|
|
80
|
+
background: rgba(255,255,255,0.08); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
|
|
81
|
+
border-bottom: 1px solid var(--border);
|
|
82
|
+
z-index: 10;
|
|
83
|
+
}
|
|
84
|
+
.brand {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 6px;
|
|
88
|
+
font-weight: 700;
|
|
89
|
+
font-size: 15px;
|
|
90
|
+
color: var(--accent);
|
|
91
|
+
letter-spacing: 0.5px;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
}
|
|
94
|
+
.brand svg { flex-shrink: 0; }
|
|
95
|
+
.brand canvas { flex-shrink: 0; display: block; }
|
|
96
|
+
.sprint-select {
|
|
97
|
+
padding: 6px 28px 6px 12px;
|
|
98
|
+
border-radius: 6px;
|
|
99
|
+
background: #111827;
|
|
100
|
+
border: 1px solid #1e293b;
|
|
101
|
+
color: #e2e8f0;
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
font-family: var(--font-sans);
|
|
104
|
+
outline: none;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
max-width: 320px;
|
|
107
|
+
text-overflow: ellipsis;
|
|
108
|
+
-webkit-appearance: none;
|
|
109
|
+
appearance: none;
|
|
110
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2394a3b8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
111
|
+
background-repeat: no-repeat;
|
|
112
|
+
background-position: right 10px center;
|
|
113
|
+
}
|
|
114
|
+
.badge-phase { background: var(--accent-dim); color: var(--accent); }
|
|
115
|
+
.badge-neutral { background: var(--bg3); color: var(--fg2); }
|
|
116
|
+
.connection-dot {
|
|
117
|
+
width: 8px; height: 8px;
|
|
118
|
+
border-radius: 50%;
|
|
119
|
+
background: var(--green);
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
transition: background 0.3s;
|
|
122
|
+
}
|
|
123
|
+
.connection-dot.disconnected { background: var(--red); animation: pulse-red 2s infinite; }
|
|
124
|
+
@keyframes pulse-red { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
125
|
+
.btn-toolbar {
|
|
126
|
+
background: none; border: 1px solid var(--border);
|
|
127
|
+
color: var(--fg2); padding: 4px 10px;
|
|
128
|
+
border-radius: var(--radius); cursor: pointer;
|
|
129
|
+
font-family: var(--font-sans); font-size: 11px;
|
|
130
|
+
transition: border-color 0.15s, color 0.15s;
|
|
131
|
+
flex-shrink: 0;
|
|
132
|
+
}
|
|
133
|
+
.btn-toolbar:hover { border-color: var(--accent); color: var(--accent); }
|
|
134
|
+
.btn-toolbar:active { background: var(--accent-dim); }
|
|
135
|
+
.reconnect-banner {
|
|
136
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
|
|
137
|
+
padding: 8px 16px; background: #92400e; color: #fbbf24;
|
|
138
|
+
font-size: 12px; text-align: center; font-family: var(--font-sans);
|
|
139
|
+
transform: translateY(-100%); transition: transform 0.3s;
|
|
140
|
+
}
|
|
141
|
+
.reconnect-banner.visible { transform: translateY(0); }
|
|
142
|
+
.reconnect-banner button {
|
|
143
|
+
background: none; border: 1px solid #fbbf24; color: #fbbf24;
|
|
144
|
+
padding: 2px 10px; border-radius: 4px; cursor: pointer;
|
|
145
|
+
font-size: 11px; margin-inline-start: 8px; font-family: var(--font-sans);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── Phase progress bar ── */
|
|
149
|
+
.phase-bar {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: 0;
|
|
153
|
+
padding: 0 16px;
|
|
154
|
+
background: var(--bg2);
|
|
155
|
+
border-bottom: 1px solid var(--border);
|
|
156
|
+
height: 36px;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
}
|
|
159
|
+
.phase-step {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 6px;
|
|
163
|
+
padding: 0 16px;
|
|
164
|
+
font-size: 11px;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
color: var(--fg3);
|
|
167
|
+
position: relative;
|
|
168
|
+
height: 100%;
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
}
|
|
171
|
+
.phase-step::after {
|
|
172
|
+
content: '';
|
|
173
|
+
position: absolute;
|
|
174
|
+
right: -6px;
|
|
175
|
+
top: 50%;
|
|
176
|
+
transform: translateY(-50%) rotate(45deg);
|
|
177
|
+
width: 10px; height: 10px;
|
|
178
|
+
border-top: 1px solid var(--border);
|
|
179
|
+
border-right: 1px solid var(--border);
|
|
180
|
+
background: var(--bg2);
|
|
181
|
+
z-index: 1;
|
|
182
|
+
}
|
|
183
|
+
.phase-step:last-child::after { display: none; }
|
|
184
|
+
.phase-step.completed { color: var(--green); }
|
|
185
|
+
.phase-step.completed .phase-dot { background: var(--green); }
|
|
186
|
+
.phase-step.active { color: var(--accent); }
|
|
187
|
+
.phase-step.active .phase-dot { background: var(--accent); box-shadow: 0 0 8px rgba(251, 191, 36, 0.4); }
|
|
188
|
+
.phase-dot {
|
|
189
|
+
width: 6px; height: 6px;
|
|
190
|
+
border-radius: 50%;
|
|
191
|
+
background: var(--fg3);
|
|
192
|
+
flex-shrink: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ── Sidebar ── */
|
|
196
|
+
.sidebar {
|
|
197
|
+
background: var(--bg2);
|
|
198
|
+
border-inline-end: 1px solid var(--border);
|
|
199
|
+
overflow-y: auto;
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-direction: column;
|
|
202
|
+
}
|
|
203
|
+
.sidebar-section {
|
|
204
|
+
padding: 12px;
|
|
205
|
+
}
|
|
206
|
+
.sidebar-section + .sidebar-section {
|
|
207
|
+
border-top: 1px solid var(--border);
|
|
208
|
+
}
|
|
209
|
+
.sidebar-label {
|
|
210
|
+
font-size: 10px;
|
|
211
|
+
font-weight: 700;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
letter-spacing: 1px;
|
|
214
|
+
color: var(--fg3);
|
|
215
|
+
margin-bottom: 8px;
|
|
216
|
+
}
|
|
217
|
+
.topic-item {
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: 8px;
|
|
221
|
+
padding: 6px 10px;
|
|
222
|
+
border-radius: var(--radius);
|
|
223
|
+
cursor: pointer;
|
|
224
|
+
font-size: 12px;
|
|
225
|
+
color: var(--fg2);
|
|
226
|
+
transition: background 0.1s;
|
|
227
|
+
}
|
|
228
|
+
.topic-item:hover { background: var(--bg3); }
|
|
229
|
+
.topic-item.active { background: var(--accent-dim); color: var(--accent); }
|
|
230
|
+
.topic-item .count {
|
|
231
|
+
margin-inline-start: auto;
|
|
232
|
+
font-size: 10px;
|
|
233
|
+
color: var(--fg3);
|
|
234
|
+
background: var(--bg);
|
|
235
|
+
padding: 1px 6px;
|
|
236
|
+
border-radius: 8px;
|
|
237
|
+
font-family: var(--font-mono);
|
|
238
|
+
}
|
|
239
|
+
.topic-item.active .count { background: var(--accent-border); color: var(--accent); }
|
|
240
|
+
.topic-name {
|
|
241
|
+
white-space: nowrap;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
min-width: 0;
|
|
245
|
+
}
|
|
246
|
+
.evidence-dot {
|
|
247
|
+
width: 6px; height: 6px;
|
|
248
|
+
border-radius: 50%;
|
|
249
|
+
flex-shrink: 0;
|
|
250
|
+
}
|
|
251
|
+
.ev-stated { background: var(--fg3); }
|
|
252
|
+
.ev-web { background: var(--blue); }
|
|
253
|
+
.ev-documented { background: var(--accent-light); }
|
|
254
|
+
.ev-tested { background: var(--green); }
|
|
255
|
+
.ev-production { background: var(--purple); }
|
|
256
|
+
|
|
257
|
+
/* sidebar sprint info */
|
|
258
|
+
.sprint-info {
|
|
259
|
+
padding: 12px;
|
|
260
|
+
border-bottom: 1px solid var(--border);
|
|
261
|
+
}
|
|
262
|
+
.sprint-info-label {
|
|
263
|
+
font-size: 10px;
|
|
264
|
+
font-weight: 700;
|
|
265
|
+
text-transform: uppercase;
|
|
266
|
+
letter-spacing: 1px;
|
|
267
|
+
color: var(--fg3);
|
|
268
|
+
margin-bottom: 4px;
|
|
269
|
+
}
|
|
270
|
+
.sprint-info-question {
|
|
271
|
+
font-size: 12px;
|
|
272
|
+
color: var(--fg);
|
|
273
|
+
line-height: 1.5;
|
|
274
|
+
margin-bottom: 8px;
|
|
275
|
+
}
|
|
276
|
+
.sprint-stats {
|
|
277
|
+
display: flex;
|
|
278
|
+
gap: 12px;
|
|
279
|
+
font-size: 11px;
|
|
280
|
+
}
|
|
281
|
+
.sprint-stat-value {
|
|
282
|
+
font-weight: 700;
|
|
283
|
+
color: var(--accent);
|
|
284
|
+
font-family: var(--font-mono);
|
|
285
|
+
}
|
|
286
|
+
.sprint-stat-label {
|
|
287
|
+
color: var(--fg3);
|
|
288
|
+
margin-inline-start: 3px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ── Search bar ── */
|
|
292
|
+
.search-bar {
|
|
293
|
+
padding: 8px 12px;
|
|
294
|
+
border-bottom: 1px solid var(--border);
|
|
295
|
+
}
|
|
296
|
+
.search-input {
|
|
297
|
+
width: 100%;
|
|
298
|
+
background: var(--bg);
|
|
299
|
+
border: 1px solid var(--border);
|
|
300
|
+
border-radius: var(--radius);
|
|
301
|
+
padding: 6px 10px 6px 30px;
|
|
302
|
+
color: var(--fg);
|
|
303
|
+
font-family: var(--font-sans);
|
|
304
|
+
font-size: 12px;
|
|
305
|
+
outline: none;
|
|
306
|
+
transition: border-color 0.15s;
|
|
307
|
+
}
|
|
308
|
+
.search-input:focus { border-color: var(--accent); }
|
|
309
|
+
.search-input::placeholder { color: var(--fg3); }
|
|
310
|
+
.search-wrap {
|
|
311
|
+
position: relative;
|
|
312
|
+
}
|
|
313
|
+
.search-icon {
|
|
314
|
+
position: absolute;
|
|
315
|
+
left: 9px;
|
|
316
|
+
top: 50%;
|
|
317
|
+
transform: translateY(-50%);
|
|
318
|
+
color: var(--fg3);
|
|
319
|
+
font-size: 12px;
|
|
320
|
+
pointer-events: none;
|
|
321
|
+
}
|
|
322
|
+
.search-clear {
|
|
323
|
+
position: absolute;
|
|
324
|
+
right: 6px;
|
|
325
|
+
top: 50%;
|
|
326
|
+
transform: translateY(-50%);
|
|
327
|
+
background: none;
|
|
328
|
+
border: none;
|
|
329
|
+
color: var(--fg3);
|
|
330
|
+
cursor: pointer;
|
|
331
|
+
font-size: 14px;
|
|
332
|
+
line-height: 1;
|
|
333
|
+
display: none;
|
|
334
|
+
padding: 2px;
|
|
335
|
+
}
|
|
336
|
+
.search-clear.visible { display: block; }
|
|
337
|
+
.search-clear:hover { color: var(--fg); }
|
|
338
|
+
|
|
339
|
+
/* ── Filter bar ── */
|
|
340
|
+
.filter-bar {
|
|
341
|
+
display: flex;
|
|
342
|
+
gap: 6px;
|
|
343
|
+
padding: 8px 12px;
|
|
344
|
+
border-bottom: 1px solid var(--border);
|
|
345
|
+
flex-wrap: wrap;
|
|
346
|
+
align-items: center;
|
|
347
|
+
}
|
|
348
|
+
.filter-chip {
|
|
349
|
+
padding: 3px 8px;
|
|
350
|
+
border-radius: 10px;
|
|
351
|
+
font-size: 10px;
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
border: 1px solid var(--border);
|
|
354
|
+
background: none;
|
|
355
|
+
color: var(--fg2);
|
|
356
|
+
font-family: var(--font-sans);
|
|
357
|
+
transition: border-color 0.15s, color 0.15s;
|
|
358
|
+
}
|
|
359
|
+
.filter-chip:hover { border-color: var(--fg3); }
|
|
360
|
+
.filter-chip.active { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }
|
|
361
|
+
.filter-sep {
|
|
362
|
+
width: 1px;
|
|
363
|
+
height: 16px;
|
|
364
|
+
background: var(--border);
|
|
365
|
+
flex-shrink: 0;
|
|
366
|
+
}
|
|
367
|
+
.sort-select {
|
|
368
|
+
margin-inline-start: auto;
|
|
369
|
+
background: var(--bg);
|
|
370
|
+
border: 1px solid var(--border);
|
|
371
|
+
border-radius: var(--radius);
|
|
372
|
+
padding: 2px 6px;
|
|
373
|
+
color: var(--fg2);
|
|
374
|
+
font-family: var(--font-sans);
|
|
375
|
+
font-size: 10px;
|
|
376
|
+
cursor: pointer;
|
|
377
|
+
outline: none;
|
|
378
|
+
}
|
|
379
|
+
.sort-select:focus { border-color: var(--accent); }
|
|
380
|
+
|
|
381
|
+
/* ── Main (claims list) ── */
|
|
382
|
+
.main {
|
|
383
|
+
display: flex;
|
|
384
|
+
flex-direction: column;
|
|
385
|
+
overflow: hidden;
|
|
386
|
+
border-inline-end: 1px solid var(--border);
|
|
387
|
+
}
|
|
388
|
+
.claims-header {
|
|
389
|
+
display: grid;
|
|
390
|
+
grid-template-columns: 56px 90px 1fr 72px;
|
|
391
|
+
gap: 8px;
|
|
392
|
+
align-items: center;
|
|
393
|
+
padding: 6px 16px;
|
|
394
|
+
background: var(--bg2);
|
|
395
|
+
border-bottom: 1px solid var(--border);
|
|
396
|
+
font-size: 10px;
|
|
397
|
+
color: var(--fg3);
|
|
398
|
+
font-weight: 700;
|
|
399
|
+
text-transform: uppercase;
|
|
400
|
+
letter-spacing: 0.5px;
|
|
401
|
+
}
|
|
402
|
+
.claims-header span { cursor: pointer; user-select: none; }
|
|
403
|
+
.claims-header span:hover { color: var(--fg2); }
|
|
404
|
+
.claims-header span.sort-active { color: var(--accent); }
|
|
405
|
+
.claims-count {
|
|
406
|
+
font-size: 10px;
|
|
407
|
+
color: var(--fg3);
|
|
408
|
+
padding: 6px 16px 0;
|
|
409
|
+
font-weight: 600;
|
|
410
|
+
}
|
|
411
|
+
.claims-list {
|
|
412
|
+
flex: 1;
|
|
413
|
+
overflow-y: auto;
|
|
414
|
+
padding: 2px 0;
|
|
415
|
+
}
|
|
416
|
+
.claim-row {
|
|
417
|
+
display: grid;
|
|
418
|
+
grid-template-columns: 56px 90px 1fr 72px;
|
|
419
|
+
gap: 8px;
|
|
420
|
+
align-items: center;
|
|
421
|
+
padding: 8px 16px;
|
|
422
|
+
border-bottom: 1px solid rgba(30, 41, 59, 0.4);
|
|
423
|
+
cursor: pointer;
|
|
424
|
+
transition: background 0.1s;
|
|
425
|
+
}
|
|
426
|
+
.claim-row:hover { background: var(--bg2); }
|
|
427
|
+
.claim-row.selected {
|
|
428
|
+
background: var(--accent-dim);
|
|
429
|
+
border-inline-start: 2px solid var(--accent);
|
|
430
|
+
padding-inline-start: 14px;
|
|
431
|
+
}
|
|
432
|
+
.claim-row.conflicted {
|
|
433
|
+
border-inline-start: 2px solid var(--red);
|
|
434
|
+
padding-inline-start: 14px;
|
|
435
|
+
}
|
|
436
|
+
.claim-row.superseded { opacity: 0.4; }
|
|
437
|
+
.claim-id {
|
|
438
|
+
font-size: 11px;
|
|
439
|
+
font-weight: 700;
|
|
440
|
+
font-family: var(--font-mono);
|
|
441
|
+
color: var(--fg2);
|
|
442
|
+
}
|
|
443
|
+
.claim-type-badge {
|
|
444
|
+
font-size: 10px;
|
|
445
|
+
padding: 2px 6px;
|
|
446
|
+
border-radius: 4px;
|
|
447
|
+
font-weight: 600;
|
|
448
|
+
text-align: center;
|
|
449
|
+
white-space: nowrap;
|
|
450
|
+
}
|
|
451
|
+
.type-constraint { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
452
|
+
.type-factual { background: rgba(96, 165, 250, 0.15); color: var(--blue); }
|
|
453
|
+
.type-estimate { background: rgba(250, 204, 21, 0.15); color: var(--yellow); }
|
|
454
|
+
.type-risk { background: rgba(251, 146, 60, 0.15); color: var(--orange); }
|
|
455
|
+
.type-recommendation { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
456
|
+
.type-feedback { background: rgba(167, 139, 250, 0.15); color: var(--purple); }
|
|
457
|
+
.claim-content {
|
|
458
|
+
font-size: 12px;
|
|
459
|
+
color: var(--fg2);
|
|
460
|
+
white-space: nowrap;
|
|
461
|
+
overflow: hidden;
|
|
462
|
+
text-overflow: ellipsis;
|
|
463
|
+
min-width: 0;
|
|
464
|
+
}
|
|
465
|
+
.claim-evidence {
|
|
466
|
+
font-size: 10px;
|
|
467
|
+
text-align: end;
|
|
468
|
+
font-family: var(--font-mono);
|
|
469
|
+
}
|
|
470
|
+
.ev-text-stated { color: var(--fg3); }
|
|
471
|
+
.ev-text-web { color: var(--blue); }
|
|
472
|
+
.ev-text-documented { color: var(--accent-light); }
|
|
473
|
+
.ev-text-tested { color: var(--green); }
|
|
474
|
+
.ev-text-production { color: var(--purple); }
|
|
475
|
+
|
|
476
|
+
/* ── Detail pane ── */
|
|
477
|
+
.detail {
|
|
478
|
+
background: var(--bg2);
|
|
479
|
+
overflow-y: auto;
|
|
480
|
+
border-inline-start: 1px solid var(--border);
|
|
481
|
+
display: flex;
|
|
482
|
+
flex-direction: column;
|
|
483
|
+
}
|
|
484
|
+
.detail-empty {
|
|
485
|
+
display: flex;
|
|
486
|
+
flex-direction: column;
|
|
487
|
+
align-items: center;
|
|
488
|
+
justify-content: center;
|
|
489
|
+
height: 100%;
|
|
490
|
+
color: var(--fg3);
|
|
491
|
+
font-size: 13px;
|
|
492
|
+
text-align: center;
|
|
493
|
+
padding: 32px;
|
|
494
|
+
gap: 8px;
|
|
495
|
+
}
|
|
496
|
+
.detail-empty-hint {
|
|
497
|
+
font-size: 11px;
|
|
498
|
+
color: var(--fg3);
|
|
499
|
+
opacity: 0.6;
|
|
500
|
+
}
|
|
501
|
+
.detail-toolbar {
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
padding: 8px 12px;
|
|
505
|
+
border-bottom: 1px solid var(--border);
|
|
506
|
+
gap: 8px;
|
|
507
|
+
}
|
|
508
|
+
.detail-toolbar-title {
|
|
509
|
+
font-size: 11px;
|
|
510
|
+
font-weight: 700;
|
|
511
|
+
color: var(--fg3);
|
|
512
|
+
text-transform: uppercase;
|
|
513
|
+
letter-spacing: 0.5px;
|
|
514
|
+
}
|
|
515
|
+
.btn-collapse {
|
|
516
|
+
margin-inline-start: auto;
|
|
517
|
+
background: none;
|
|
518
|
+
border: none;
|
|
519
|
+
color: var(--fg3);
|
|
520
|
+
cursor: pointer;
|
|
521
|
+
font-size: 16px;
|
|
522
|
+
padding: 2px 6px;
|
|
523
|
+
border-radius: var(--radius);
|
|
524
|
+
}
|
|
525
|
+
.btn-collapse:hover { color: var(--fg); background: var(--bg3); }
|
|
526
|
+
.detail-header {
|
|
527
|
+
padding: 16px;
|
|
528
|
+
border-bottom: 1px solid var(--border);
|
|
529
|
+
}
|
|
530
|
+
.detail-id {
|
|
531
|
+
font-size: 18px;
|
|
532
|
+
font-weight: 700;
|
|
533
|
+
color: var(--accent);
|
|
534
|
+
font-family: var(--font-mono);
|
|
535
|
+
margin-bottom: 4px;
|
|
536
|
+
}
|
|
537
|
+
.detail-meta {
|
|
538
|
+
display: flex;
|
|
539
|
+
gap: 6px;
|
|
540
|
+
flex-wrap: wrap;
|
|
541
|
+
margin-top: 8px;
|
|
542
|
+
}
|
|
543
|
+
.detail-tag {
|
|
544
|
+
font-size: 10px;
|
|
545
|
+
padding: 2px 8px;
|
|
546
|
+
border-radius: 10px;
|
|
547
|
+
border: 1px solid var(--border);
|
|
548
|
+
color: var(--fg3);
|
|
549
|
+
}
|
|
550
|
+
.detail-body { padding: 16px; }
|
|
551
|
+
.detail-section { margin-bottom: 16px; }
|
|
552
|
+
.detail-section-title {
|
|
553
|
+
font-size: 10px;
|
|
554
|
+
font-weight: 700;
|
|
555
|
+
text-transform: uppercase;
|
|
556
|
+
letter-spacing: 1px;
|
|
557
|
+
color: var(--fg3);
|
|
558
|
+
margin-bottom: 6px;
|
|
559
|
+
}
|
|
560
|
+
.detail-content {
|
|
561
|
+
font-size: 13px;
|
|
562
|
+
color: var(--fg);
|
|
563
|
+
line-height: 1.7;
|
|
564
|
+
}
|
|
565
|
+
.detail-field {
|
|
566
|
+
display: flex;
|
|
567
|
+
gap: 8px;
|
|
568
|
+
font-size: 12px;
|
|
569
|
+
margin-bottom: 4px;
|
|
570
|
+
}
|
|
571
|
+
.detail-field-label { color: var(--fg3); min-width: 80px; flex-shrink: 0; }
|
|
572
|
+
.detail-field-value { color: var(--fg2); word-break: break-word; }
|
|
573
|
+
.related-claim {
|
|
574
|
+
display: inline-block;
|
|
575
|
+
padding: 2px 8px;
|
|
576
|
+
margin: 2px;
|
|
577
|
+
border-radius: 4px;
|
|
578
|
+
background: var(--bg3);
|
|
579
|
+
color: var(--accent);
|
|
580
|
+
font-size: 11px;
|
|
581
|
+
cursor: pointer;
|
|
582
|
+
font-family: var(--font-mono);
|
|
583
|
+
transition: background 0.1s;
|
|
584
|
+
}
|
|
585
|
+
.related-claim:hover { background: var(--accent-dim); }
|
|
586
|
+
|
|
587
|
+
/* ── Compilation bar ── */
|
|
588
|
+
.compilation-bar {
|
|
589
|
+
background: var(--bg2);
|
|
590
|
+
border-top: 1px solid var(--border);
|
|
591
|
+
display: flex;
|
|
592
|
+
flex-direction: column;
|
|
593
|
+
max-height: 160px;
|
|
594
|
+
overflow-y: auto;
|
|
595
|
+
}
|
|
596
|
+
.comp-header {
|
|
597
|
+
display: flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: 12px;
|
|
600
|
+
padding: 8px 16px;
|
|
601
|
+
font-size: 11px;
|
|
602
|
+
cursor: pointer;
|
|
603
|
+
user-select: none;
|
|
604
|
+
}
|
|
605
|
+
.comp-header:hover { background: var(--bg3); }
|
|
606
|
+
.comp-toggle {
|
|
607
|
+
color: var(--fg3);
|
|
608
|
+
font-size: 10px;
|
|
609
|
+
transition: transform 0.2s;
|
|
610
|
+
}
|
|
611
|
+
.comp-toggle.expanded { transform: rotate(90deg); }
|
|
612
|
+
.comp-status-text { font-weight: 600; }
|
|
613
|
+
.comp-status-ready { color: var(--green); }
|
|
614
|
+
.comp-status-blocked { color: var(--red); }
|
|
615
|
+
.comp-status-unknown { color: var(--fg3); }
|
|
616
|
+
.readiness-bar {
|
|
617
|
+
flex: 1;
|
|
618
|
+
height: 4px;
|
|
619
|
+
background: var(--bg);
|
|
620
|
+
border-radius: 2px;
|
|
621
|
+
overflow: hidden;
|
|
622
|
+
max-width: 200px;
|
|
623
|
+
}
|
|
624
|
+
.readiness-fill {
|
|
625
|
+
height: 100%;
|
|
626
|
+
border-radius: 2px;
|
|
627
|
+
transition: width 0.5s, background 0.3s;
|
|
628
|
+
}
|
|
629
|
+
.comp-body {
|
|
630
|
+
padding: 0 16px 8px;
|
|
631
|
+
display: none;
|
|
632
|
+
}
|
|
633
|
+
.comp-body.expanded { display: block; }
|
|
634
|
+
.comp-row {
|
|
635
|
+
display: flex;
|
|
636
|
+
align-items: center;
|
|
637
|
+
gap: 8px;
|
|
638
|
+
padding: 3px 0;
|
|
639
|
+
color: var(--fg2);
|
|
640
|
+
font-size: 11px;
|
|
641
|
+
}
|
|
642
|
+
.comp-label { color: var(--fg3); min-width: 80px; }
|
|
643
|
+
.comp-value { color: var(--fg); }
|
|
644
|
+
.comp-warning {
|
|
645
|
+
padding: 4px 8px;
|
|
646
|
+
margin-top: 4px;
|
|
647
|
+
background: rgba(251, 191, 36, 0.08);
|
|
648
|
+
border-inline-start: 2px solid var(--accent);
|
|
649
|
+
border-radius: 0 var(--radius) var(--radius) 0;
|
|
650
|
+
color: var(--fg2);
|
|
651
|
+
font-size: 11px;
|
|
652
|
+
}
|
|
653
|
+
.comp-conflict {
|
|
654
|
+
padding: 4px 8px;
|
|
655
|
+
margin-top: 4px;
|
|
656
|
+
background: rgba(248, 113, 113, 0.08);
|
|
657
|
+
border-inline-start: 2px solid var(--red);
|
|
658
|
+
border-radius: 0 var(--radius) var(--radius) 0;
|
|
659
|
+
color: var(--fg2);
|
|
660
|
+
font-size: 11px;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* ── Statusbar ── */
|
|
664
|
+
.statusbar {
|
|
665
|
+
display: flex;
|
|
666
|
+
align-items: center;
|
|
667
|
+
gap: 16px;
|
|
668
|
+
padding: 0 16px;
|
|
669
|
+
background: var(--bg2);
|
|
670
|
+
border-top: 1px solid var(--border);
|
|
671
|
+
font-size: 10px;
|
|
672
|
+
color: var(--fg3);
|
|
673
|
+
overflow: hidden;
|
|
674
|
+
white-space: nowrap;
|
|
675
|
+
min-width: 0;
|
|
676
|
+
}
|
|
677
|
+
.statusbar .hash { font-family: var(--font-mono); color: var(--fg3); }
|
|
678
|
+
.coverage-mini {
|
|
679
|
+
display: flex;
|
|
680
|
+
gap: 2px;
|
|
681
|
+
align-items: center;
|
|
682
|
+
overflow: hidden;
|
|
683
|
+
flex-shrink: 1;
|
|
684
|
+
min-width: 0;
|
|
685
|
+
}
|
|
686
|
+
.coverage-bar-mini {
|
|
687
|
+
width: 40px;
|
|
688
|
+
height: 4px;
|
|
689
|
+
background: var(--bg);
|
|
690
|
+
border-radius: 2px;
|
|
691
|
+
overflow: hidden;
|
|
692
|
+
}
|
|
693
|
+
.coverage-fill-mini {
|
|
694
|
+
height: 100%;
|
|
695
|
+
border-radius: 2px;
|
|
696
|
+
transition: width 0.3s;
|
|
697
|
+
}
|
|
698
|
+
.shortcut-hint {
|
|
699
|
+
font-size: 10px;
|
|
700
|
+
color: var(--fg3);
|
|
701
|
+
opacity: 0.5;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/* ── Scrollbar ── */
|
|
705
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
706
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
707
|
+
::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
|
708
|
+
::-webkit-scrollbar-thumb:hover { background: var(--fg3); }
|
|
709
|
+
|
|
710
|
+
/* ── A11y ── */
|
|
711
|
+
.skip-link {
|
|
712
|
+
position: absolute; top: -40px; inset-inline-start: 0;
|
|
713
|
+
background: var(--accent); color: #000; padding: 8px 16px;
|
|
714
|
+
z-index: 10000; font-size: 14px; font-weight: 600; transition: top 0.2s;
|
|
715
|
+
}
|
|
716
|
+
.skip-link:focus { top: 0; }
|
|
717
|
+
.sr-only {
|
|
718
|
+
position: absolute; width: 1px; height: 1px;
|
|
719
|
+
padding: 0; margin: -1px; overflow: hidden;
|
|
720
|
+
clip: rect(0,0,0,0); border: 0;
|
|
721
|
+
}
|
|
722
|
+
@media (prefers-reduced-motion: reduce) {
|
|
723
|
+
*, *::before, *::after {
|
|
724
|
+
animation-duration: 0.01ms !important;
|
|
725
|
+
transition-duration: 0.01ms !important;
|
|
726
|
+
scroll-behavior: auto !important;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/* ── Empty state ── */
|
|
731
|
+
.empty-state {
|
|
732
|
+
display: flex;
|
|
733
|
+
flex-direction: column;
|
|
734
|
+
align-items: center;
|
|
735
|
+
justify-content: center;
|
|
736
|
+
height: 100%;
|
|
737
|
+
color: var(--fg3);
|
|
738
|
+
gap: 12px;
|
|
739
|
+
padding: 32px;
|
|
740
|
+
text-align: center;
|
|
741
|
+
}
|
|
742
|
+
.empty-state-title {
|
|
743
|
+
font-size: 14px;
|
|
744
|
+
font-weight: 600;
|
|
745
|
+
color: var(--fg2);
|
|
746
|
+
}
|
|
747
|
+
.empty-state-hint {
|
|
748
|
+
font-size: 12px;
|
|
749
|
+
max-width: 300px;
|
|
750
|
+
line-height: 1.6;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* ── Mobile nav ── */
|
|
754
|
+
.mobile-nav {
|
|
755
|
+
display: none;
|
|
756
|
+
grid-column: 1 / -1;
|
|
757
|
+
background: var(--bg2);
|
|
758
|
+
border-bottom: 1px solid var(--border);
|
|
759
|
+
}
|
|
760
|
+
.mobile-nav-bar { display: flex; }
|
|
761
|
+
.mobile-tab {
|
|
762
|
+
flex: 1;
|
|
763
|
+
padding: 12px 0;
|
|
764
|
+
text-align: center;
|
|
765
|
+
font-size: 12px;
|
|
766
|
+
font-weight: 600;
|
|
767
|
+
color: var(--fg3);
|
|
768
|
+
background: none;
|
|
769
|
+
border: none;
|
|
770
|
+
border-bottom: 2px solid transparent;
|
|
771
|
+
cursor: pointer;
|
|
772
|
+
font-family: var(--font-sans);
|
|
773
|
+
}
|
|
774
|
+
.mobile-tab:hover { color: var(--fg2); }
|
|
775
|
+
.mobile-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
776
|
+
|
|
777
|
+
/* ── Responsive ── */
|
|
778
|
+
@media (max-width: 1024px) {
|
|
779
|
+
.app, .app.detail-collapsed {
|
|
780
|
+
grid-template-columns: 200px 1fr;
|
|
781
|
+
grid-template-rows: auto auto 1fr auto 32px;
|
|
782
|
+
}
|
|
783
|
+
.detail { display: none; }
|
|
784
|
+
.compilation-bar { grid-column: 1 / -1; }
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
@media (max-width: 768px) {
|
|
788
|
+
.app, .app.detail-collapsed {
|
|
789
|
+
grid-template-columns: 1fr;
|
|
790
|
+
grid-template-rows: 48px 36px auto 1fr auto 32px;
|
|
791
|
+
}
|
|
792
|
+
.toolbar { grid-column: 1; }
|
|
793
|
+
.mobile-nav { display: block; grid-row: 3; grid-column: 1; }
|
|
794
|
+
.phase-bar { grid-row: 2; grid-column: 1; }
|
|
795
|
+
.sidebar { grid-row: 4; grid-column: 1; display: none; border-inline-end: none; border-bottom: 1px solid var(--border); }
|
|
796
|
+
.main { grid-row: 4; grid-column: 1; border-inline-end: none; }
|
|
797
|
+
.detail { grid-row: 4; grid-column: 1; display: none; border-inline-start: none; }
|
|
798
|
+
.compilation-bar { grid-row: 5; grid-column: 1; }
|
|
799
|
+
.statusbar { grid-row: 6; }
|
|
800
|
+
.sidebar.mobile-visible { display: flex; max-height: none; }
|
|
801
|
+
.detail.mobile-visible { display: flex; }
|
|
802
|
+
.main.mobile-hidden { display: none; }
|
|
803
|
+
.content.mobile-hidden { display: none; }
|
|
804
|
+
.filter-bar { overflow-x: auto; flex-wrap: nowrap; -webkit-overflow-scrolling: touch; }
|
|
805
|
+
.sprint-select { max-width: 200px; }
|
|
806
|
+
.phase-bar { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
807
|
+
.claims-header { font-size: 9px; }
|
|
808
|
+
.claim-row { padding: 6px 12px; }
|
|
809
|
+
}
|
|
810
|
+
</style>
|
|
811
|
+
</head>
|
|
812
|
+
<body>
|
|
813
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
814
|
+
<div id="live-status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
|
|
815
|
+
<div class="reconnect-banner" id="reconnectBanner" role="status" aria-live="polite"></div>
|
|
816
|
+
|
|
817
|
+
<div class="app" id="app">
|
|
818
|
+
<!-- Toolbar -->
|
|
819
|
+
<header class="toolbar" role="banner">
|
|
820
|
+
<canvas id="grainLogo" width="256" height="256"></canvas>
|
|
821
|
+
<div style="flex:1"></div>
|
|
822
|
+
<select id="sprint-select" class="sprint-select" aria-label="Select sprint"></select>
|
|
823
|
+
<span id="connection-dot" class="connection-dot" title="SSE connection status"></span>
|
|
824
|
+
</header>
|
|
825
|
+
|
|
826
|
+
<!-- Phase progress -->
|
|
827
|
+
<div class="phase-bar" id="phase-bar" role="navigation" aria-label="Sprint phase progress"></div>
|
|
828
|
+
|
|
829
|
+
<!-- Mobile nav -->
|
|
830
|
+
<nav class="mobile-nav" id="mobile-nav" aria-label="Mobile navigation">
|
|
831
|
+
<div class="mobile-nav-bar">
|
|
832
|
+
<button class="mobile-tab active" data-panel="claims" onclick="switchMobilePanel('claims')">Claims</button>
|
|
833
|
+
<button class="mobile-tab" data-panel="topics" onclick="switchMobilePanel('topics')">Topics</button>
|
|
834
|
+
<button class="mobile-tab" data-panel="detail" onclick="switchMobilePanel('detail')">Detail</button>
|
|
835
|
+
</div>
|
|
836
|
+
</nav>
|
|
837
|
+
|
|
838
|
+
<!-- Sidebar -->
|
|
839
|
+
<aside class="sidebar" id="sidebar" role="complementary" aria-label="Sprint sidebar">
|
|
840
|
+
<div class="sprint-info" id="sprint-info"></div>
|
|
841
|
+
<div class="sidebar-section" id="topics-section">
|
|
842
|
+
<div class="sidebar-label">Topics</div>
|
|
843
|
+
<div id="topics-list"></div>
|
|
844
|
+
</div>
|
|
845
|
+
<div class="sidebar-section">
|
|
846
|
+
<div class="sidebar-label">Evidence legend</div>
|
|
847
|
+
<div style="padding: 0 2px;">
|
|
848
|
+
<div class="topic-item" style="cursor:default"><span class="evidence-dot ev-stated"></span><span>stated</span><span class="count" style="background:none;color:var(--fg3)">gray</span></div>
|
|
849
|
+
<div class="topic-item" style="cursor:default"><span class="evidence-dot ev-web"></span><span>web</span><span class="count" style="background:none;color:var(--blue)">blue</span></div>
|
|
850
|
+
<div class="topic-item" style="cursor:default"><span class="evidence-dot ev-documented"></span><span>documented</span><span class="count" style="background:none;color:var(--accent-light)">amber</span></div>
|
|
851
|
+
<div class="topic-item" style="cursor:default"><span class="evidence-dot ev-tested"></span><span>tested</span><span class="count" style="background:none;color:var(--green)">green</span></div>
|
|
852
|
+
<div class="topic-item" style="cursor:default"><span class="evidence-dot ev-production"></span><span>production</span><span class="count" style="background:none;color:var(--purple)">purple</span></div>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
<div class="sidebar-section">
|
|
856
|
+
<div class="sidebar-label">Sprints</div>
|
|
857
|
+
<div id="sprints-list"></div>
|
|
858
|
+
</div>
|
|
859
|
+
</aside>
|
|
860
|
+
|
|
861
|
+
<!-- Main: search + filters + claims list -->
|
|
862
|
+
<main class="main" id="main-content" aria-label="Claims workspace">
|
|
863
|
+
<div class="search-bar">
|
|
864
|
+
<div class="search-wrap">
|
|
865
|
+
<span class="search-icon" aria-hidden="true">/</span>
|
|
866
|
+
<input type="search" class="search-input" id="search-input"
|
|
867
|
+
placeholder="Search claims... (/ to focus)"
|
|
868
|
+
autocomplete="off" spellcheck="false"
|
|
869
|
+
aria-label="Search claims">
|
|
870
|
+
<button class="search-clear" id="search-clear" onclick="clearSearch()" aria-label="Clear search">x</button>
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
<div class="filter-bar" id="filter-bar"></div>
|
|
874
|
+
<div class="claims-header" id="claims-header">
|
|
875
|
+
<span data-sort="id" onclick="toggleSort('id')">ID</span>
|
|
876
|
+
<span data-sort="type" onclick="toggleSort('type')">Type</span>
|
|
877
|
+
<span data-sort="content" onclick="toggleSort('content')">Content</span>
|
|
878
|
+
<span data-sort="evidence" onclick="toggleSort('evidence')" style="text-align:end">Evidence</span>
|
|
879
|
+
</div>
|
|
880
|
+
<div id="visible-count" class="claims-count"></div>
|
|
881
|
+
<div class="claims-list" id="claims-list" role="list" aria-label="Claims list"></div>
|
|
882
|
+
</main>
|
|
883
|
+
|
|
884
|
+
<!-- Detail pane -->
|
|
885
|
+
<section class="detail" id="detail" aria-label="Claim detail">
|
|
886
|
+
<div class="detail-toolbar">
|
|
887
|
+
<span class="detail-toolbar-title">Detail</span>
|
|
888
|
+
<button class="btn-collapse" onclick="toggleDetail()" title="Collapse detail panel" aria-label="Collapse detail panel">✕</button>
|
|
889
|
+
</div>
|
|
890
|
+
<div class="detail-empty" id="detail-empty">
|
|
891
|
+
<span>Select a claim to view details</span>
|
|
892
|
+
<span class="detail-empty-hint">Click a row or use j/k to navigate, Enter to select</span>
|
|
893
|
+
</div>
|
|
894
|
+
<div id="detail-content" style="display:none"></div>
|
|
895
|
+
</section>
|
|
896
|
+
|
|
897
|
+
<!-- Compilation bar -->
|
|
898
|
+
<div class="compilation-bar" id="compilation-bar">
|
|
899
|
+
<div class="comp-header" id="comp-header" onclick="toggleCompilation()">
|
|
900
|
+
<span class="comp-toggle" id="comp-toggle">▶</span>
|
|
901
|
+
<span class="comp-status-text" id="comp-status-text">Compilation</span>
|
|
902
|
+
<div class="readiness-bar"><div class="readiness-fill" id="readiness-fill"></div></div>
|
|
903
|
+
<span id="comp-summary" style="color:var(--fg3);font-size:10px"></span>
|
|
904
|
+
</div>
|
|
905
|
+
<div class="comp-body" id="comp-body"></div>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<!-- Statusbar -->
|
|
909
|
+
<footer class="statusbar">
|
|
910
|
+
<span id="compiler-version"></span>
|
|
911
|
+
<span>hash: <span class="hash" id="claims-hash">--</span></span>
|
|
912
|
+
<span id="warning-count"></span>
|
|
913
|
+
<div style="flex:1"></div>
|
|
914
|
+
<div id="coverage-mini" class="coverage-mini"></div>
|
|
915
|
+
<span class="shortcut-hint">j/k navigate | Enter select | Esc clear | / search</span>
|
|
916
|
+
</footer>
|
|
917
|
+
</div>
|
|
918
|
+
|
|
919
|
+
<script>
|
|
920
|
+
// ── Constants ──
|
|
921
|
+
const PHASES = ['define', 'research', 'prototype', 'evaluate', 'compile'];
|
|
922
|
+
const EVIDENCE_ORDER = ['stated', 'web', 'documented', 'tested', 'production'];
|
|
923
|
+
const TYPES = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
|
|
924
|
+
const STATUSES = ['active', 'superseded', 'conflicted'];
|
|
925
|
+
|
|
926
|
+
// ── State ──
|
|
927
|
+
let state = { claims: [], compilation: null, sprints: [], activeSprint: null, meta: null };
|
|
928
|
+
let selectedClaimId = null;
|
|
929
|
+
let focusedIndex = -1;
|
|
930
|
+
let activeFilter = { topic: null, type: null, status: null };
|
|
931
|
+
let searchQuery = '';
|
|
932
|
+
let sortField = 'id';
|
|
933
|
+
let sortDir = 1; // 1 = asc, -1 = desc
|
|
934
|
+
let detailCollapsed = false;
|
|
935
|
+
let compExpanded = false;
|
|
936
|
+
|
|
937
|
+
// ── SSE ──
|
|
938
|
+
let es;
|
|
939
|
+
let retryCount = 0;
|
|
940
|
+
|
|
941
|
+
function showBanner(count) {
|
|
942
|
+
const b = document.getElementById('reconnectBanner');
|
|
943
|
+
if (count > 5) {
|
|
944
|
+
b.innerHTML = 'Connection lost. <button onclick="retryCount=0;connectSSE()">Retry now</button>';
|
|
945
|
+
} else if (count > 1) {
|
|
946
|
+
b.textContent = 'Reconnecting (attempt ' + count + ')...';
|
|
947
|
+
} else {
|
|
948
|
+
b.textContent = 'Reconnecting...';
|
|
949
|
+
}
|
|
950
|
+
b.classList.add('visible');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function hideBanner() {
|
|
954
|
+
document.getElementById('reconnectBanner').classList.remove('visible');
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function connectSSE() {
|
|
958
|
+
if (es) { try { es.close(); } catch {} }
|
|
959
|
+
es = new EventSource('/events');
|
|
960
|
+
es.onmessage = function(e) {
|
|
961
|
+
try {
|
|
962
|
+
var msg = JSON.parse(e.data);
|
|
963
|
+
if (msg.type === 'state') {
|
|
964
|
+
state = msg.data;
|
|
965
|
+
render();
|
|
966
|
+
var ls = document.getElementById('live-status');
|
|
967
|
+
if (ls) ls.textContent = 'Updated: ' + state.claims.length + ' claims loaded';
|
|
968
|
+
}
|
|
969
|
+
} catch (err) { /* ignore parse errors */ }
|
|
970
|
+
};
|
|
971
|
+
es.onopen = function() {
|
|
972
|
+
retryCount = 0;
|
|
973
|
+
document.getElementById('connection-dot').classList.remove('disconnected');
|
|
974
|
+
document.getElementById('connection-dot').title = 'Connected';
|
|
975
|
+
if (window._grainSetState) window._grainSetState('idle');
|
|
976
|
+
};
|
|
977
|
+
es.onerror = function() {
|
|
978
|
+
es.close();
|
|
979
|
+
document.getElementById('connection-dot').classList.add('disconnected');
|
|
980
|
+
document.getElementById('connection-dot').title = 'Disconnected - retrying...';
|
|
981
|
+
if (window._grainSetState) window._grainSetState('orbit');
|
|
982
|
+
var delay = Math.min(30000, 1000 * Math.pow(2, retryCount)) + Math.random() * 1000;
|
|
983
|
+
retryCount++;
|
|
984
|
+
setTimeout(connectSSE, delay);
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ── API ──
|
|
989
|
+
async function recompile() {
|
|
990
|
+
var btn = document.getElementById('btn-compile');
|
|
991
|
+
if (btn) { btn.textContent = '...'; btn.disabled = true; }
|
|
992
|
+
try {
|
|
993
|
+
var res = await fetch('/api/compile', { method: 'POST' });
|
|
994
|
+
if (res.ok) {
|
|
995
|
+
state = await res.json();
|
|
996
|
+
render();
|
|
997
|
+
}
|
|
998
|
+
} catch (err) {
|
|
999
|
+
console.error('compile failed:', err);
|
|
1000
|
+
}
|
|
1001
|
+
if (btn) { btn.textContent = 'compile'; btn.disabled = false; }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// ── Filtering + sorting ──
|
|
1005
|
+
function getFilteredClaims() {
|
|
1006
|
+
var claims = state.claims;
|
|
1007
|
+
if (activeFilter.topic) claims = claims.filter(function(c) { return c.topic === activeFilter.topic; });
|
|
1008
|
+
if (activeFilter.type) claims = claims.filter(function(c) { return c.type === activeFilter.type; });
|
|
1009
|
+
if (activeFilter.status) claims = claims.filter(function(c) { return c.status === activeFilter.status; });
|
|
1010
|
+
if (searchQuery) {
|
|
1011
|
+
var q = searchQuery.toLowerCase();
|
|
1012
|
+
claims = claims.filter(function(c) {
|
|
1013
|
+
return (c.id && c.id.toLowerCase().indexOf(q) !== -1) ||
|
|
1014
|
+
(c.content && c.content.toLowerCase().indexOf(q) !== -1) ||
|
|
1015
|
+
(c.topic && c.topic.toLowerCase().indexOf(q) !== -1) ||
|
|
1016
|
+
(c.type && c.type.toLowerCase().indexOf(q) !== -1);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
// Sort
|
|
1020
|
+
claims = claims.slice().sort(function(a, b) {
|
|
1021
|
+
var av, bv;
|
|
1022
|
+
if (sortField === 'evidence') {
|
|
1023
|
+
av = EVIDENCE_ORDER.indexOf(a.evidence);
|
|
1024
|
+
bv = EVIDENCE_ORDER.indexOf(b.evidence);
|
|
1025
|
+
} else if (sortField === 'type') {
|
|
1026
|
+
av = TYPES.indexOf(a.type);
|
|
1027
|
+
bv = TYPES.indexOf(b.type);
|
|
1028
|
+
} else if (sortField === 'content') {
|
|
1029
|
+
av = (a.content || '').toLowerCase();
|
|
1030
|
+
bv = (b.content || '').toLowerCase();
|
|
1031
|
+
return av < bv ? -sortDir : av > bv ? sortDir : 0;
|
|
1032
|
+
} else {
|
|
1033
|
+
// id sort — natural
|
|
1034
|
+
av = a.id || '';
|
|
1035
|
+
bv = b.id || '';
|
|
1036
|
+
return av < bv ? -sortDir : av > bv ? sortDir : 0;
|
|
1037
|
+
}
|
|
1038
|
+
return (av - bv) * sortDir;
|
|
1039
|
+
});
|
|
1040
|
+
return claims;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function toggleSort(field) {
|
|
1044
|
+
if (sortField === field) {
|
|
1045
|
+
sortDir *= -1;
|
|
1046
|
+
} else {
|
|
1047
|
+
sortField = field;
|
|
1048
|
+
sortDir = 1;
|
|
1049
|
+
}
|
|
1050
|
+
render();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// ── Render ──
|
|
1054
|
+
function render() {
|
|
1055
|
+
renderToolbar();
|
|
1056
|
+
renderPhaseBar();
|
|
1057
|
+
renderSprintInfo();
|
|
1058
|
+
renderTopics();
|
|
1059
|
+
renderSprints();
|
|
1060
|
+
renderFilters();
|
|
1061
|
+
renderClaims();
|
|
1062
|
+
renderDetail();
|
|
1063
|
+
renderCompilation();
|
|
1064
|
+
renderStatusbar();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function renderToolbar() {
|
|
1068
|
+
var sel = document.getElementById('sprint-select');
|
|
1069
|
+
var prevVal = sel.value;
|
|
1070
|
+
var opts = '';
|
|
1071
|
+
if (state.sprints && state.sprints.length > 0) {
|
|
1072
|
+
opts = '<option value="__all">All sprints (' + state.sprints.length + ')</option>';
|
|
1073
|
+
state.sprints.forEach(function(s) {
|
|
1074
|
+
var active = s.active_claims || s.activeClaims || 0;
|
|
1075
|
+
var total = s.claims_count || s.claimCount || 0;
|
|
1076
|
+
var label = (s.name || 'unnamed') + ' (' + active + '/' + total + ' claims, ' + (s.phase || '?') + ')';
|
|
1077
|
+
opts += '<option value="' + escAttr(s.name || '') + '">' + esc(label) + '</option>';
|
|
1078
|
+
});
|
|
1079
|
+
} else {
|
|
1080
|
+
var q = (state.meta && state.meta.question) || 'No sprint loaded';
|
|
1081
|
+
var active = state.claims.filter(function(c) { return c.status === 'active'; }).length;
|
|
1082
|
+
var label = active + '/' + state.claims.length + ' claims';
|
|
1083
|
+
if (state.meta && state.meta.phase) label += ' -- ' + state.meta.phase;
|
|
1084
|
+
if (state.compilation) label += ' -- ' + (state.compilation.status || 'unknown');
|
|
1085
|
+
opts = '<option value="" selected>' + esc(label) + '</option>';
|
|
1086
|
+
}
|
|
1087
|
+
sel.innerHTML = opts;
|
|
1088
|
+
if (prevVal) sel.value = prevVal;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function renderPhaseBar() {
|
|
1092
|
+
var currentPhase = (state.meta && state.meta.phase) || '';
|
|
1093
|
+
var currentIdx = PHASES.indexOf(currentPhase);
|
|
1094
|
+
var el = document.getElementById('phase-bar');
|
|
1095
|
+
el.innerHTML = PHASES.map(function(p, i) {
|
|
1096
|
+
var cls = 'phase-step';
|
|
1097
|
+
if (i < currentIdx) cls += ' completed';
|
|
1098
|
+
else if (i === currentIdx) cls += ' active';
|
|
1099
|
+
return '<div class="' + cls + '"><span class="phase-dot"></span>' + p + '</div>';
|
|
1100
|
+
}).join('');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function renderSprintInfo() {
|
|
1104
|
+
var el = document.getElementById('sprint-info');
|
|
1105
|
+
var q = (state.meta && state.meta.question) || '';
|
|
1106
|
+
var total = state.claims.length;
|
|
1107
|
+
var active = state.claims.filter(function(c) { return c.status === 'active'; }).length;
|
|
1108
|
+
var topics = new Set(state.claims.map(function(c) { return c.topic; }).filter(Boolean));
|
|
1109
|
+
|
|
1110
|
+
if (!q && total === 0) {
|
|
1111
|
+
el.innerHTML = '<div class="sprint-info-label">Sprint</div><div style="color:var(--fg3);font-size:12px">No sprint loaded. Start with wheat init.</div>';
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
el.innerHTML =
|
|
1116
|
+
'<div class="sprint-info-label">Sprint question</div>' +
|
|
1117
|
+
'<div class="sprint-info-question">' + esc(q) + '</div>' +
|
|
1118
|
+
'<div class="sprint-stats">' +
|
|
1119
|
+
'<span><span class="sprint-stat-value">' + active + '</span><span class="sprint-stat-label">active</span></span>' +
|
|
1120
|
+
'<span><span class="sprint-stat-value">' + total + '</span><span class="sprint-stat-label">total</span></span>' +
|
|
1121
|
+
'<span><span class="sprint-stat-value">' + topics.size + '</span><span class="sprint-stat-label">topics</span></span>' +
|
|
1122
|
+
'</div>';
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function renderTopics() {
|
|
1126
|
+
var coverage = (state.compilation && state.compilation.coverage) || {};
|
|
1127
|
+
// Also compute topics from claims if no compilation
|
|
1128
|
+
var topicCounts = {};
|
|
1129
|
+
var topicEvidence = {};
|
|
1130
|
+
state.claims.forEach(function(c) {
|
|
1131
|
+
if (!c.topic) return;
|
|
1132
|
+
topicCounts[c.topic] = (topicCounts[c.topic] || 0) + 1;
|
|
1133
|
+
var curMax = EVIDENCE_ORDER.indexOf(topicEvidence[c.topic] || 'stated');
|
|
1134
|
+
var thisEv = EVIDENCE_ORDER.indexOf(c.evidence || 'stated');
|
|
1135
|
+
if (thisEv > curMax) topicEvidence[c.topic] = c.evidence;
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// Merge with compilation coverage
|
|
1139
|
+
var topics = [];
|
|
1140
|
+
var seen = {};
|
|
1141
|
+
Object.keys(coverage).forEach(function(name) {
|
|
1142
|
+
topics.push({ name: name, claims: coverage[name].claims || 0, maxEvidence: coverage[name].max_evidence || 'stated' });
|
|
1143
|
+
seen[name] = true;
|
|
1144
|
+
});
|
|
1145
|
+
Object.keys(topicCounts).forEach(function(name) {
|
|
1146
|
+
if (!seen[name]) {
|
|
1147
|
+
topics.push({ name: name, claims: topicCounts[name], maxEvidence: topicEvidence[name] || 'stated' });
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
topics.sort(function(a, b) { return b.claims - a.claims; });
|
|
1151
|
+
|
|
1152
|
+
var el = document.getElementById('topics-list');
|
|
1153
|
+
if (topics.length === 0) {
|
|
1154
|
+
el.innerHTML = '<div style="padding:6px 10px;color:var(--fg3);font-size:11px">No topics yet</div>';
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
el.innerHTML = topics.map(function(t) {
|
|
1158
|
+
var isActive = activeFilter.topic === t.name ? ' active' : '';
|
|
1159
|
+
return '<div class="topic-item' + isActive + '" onclick="filterTopic(\'' + escAttr(t.name) + '\')" title="' + escAttr(t.name) + ': ' + t.claims + ' claims (' + t.maxEvidence + ')">' +
|
|
1160
|
+
'<span class="evidence-dot ev-' + t.maxEvidence + '"></span>' +
|
|
1161
|
+
'<span class="topic-name">' + esc(t.name) + '</span>' +
|
|
1162
|
+
'<span class="count">' + t.claims + '</span>' +
|
|
1163
|
+
'</div>';
|
|
1164
|
+
}).join('');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function renderSprints() {
|
|
1168
|
+
var el = document.getElementById('sprints-list');
|
|
1169
|
+
if (!state.sprints || !state.sprints.length) {
|
|
1170
|
+
el.innerHTML = '<div style="padding:6px 10px;color:var(--fg3);font-size:11px">No sprints detected</div>';
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
el.innerHTML = state.sprints.map(function(s) {
|
|
1174
|
+
var isActive = s.status === 'active';
|
|
1175
|
+
return '<div class="topic-item' + (isActive ? ' active' : '') + '" style="cursor:default">' +
|
|
1176
|
+
'<span class="evidence-dot" style="background:' + (isActive ? 'var(--green)' : 'var(--fg3)') + '"></span>' +
|
|
1177
|
+
'<span class="topic-name">' + esc(s.name || 'unnamed') + '</span>' +
|
|
1178
|
+
'<span class="count">' + (s.phase || '?') + '</span>' +
|
|
1179
|
+
'</div>';
|
|
1180
|
+
}).join('');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function renderFilters() {
|
|
1184
|
+
var el = document.getElementById('filter-bar');
|
|
1185
|
+
var html = '';
|
|
1186
|
+
|
|
1187
|
+
// Type chips
|
|
1188
|
+
TYPES.forEach(function(type) {
|
|
1189
|
+
var isActive = activeFilter.type === type ? ' active' : '';
|
|
1190
|
+
html += '<button class="filter-chip' + isActive + '" onclick="filterType(\'' + type + '\')">' + type + '</button>';
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
html += '<span class="filter-sep"></span>';
|
|
1194
|
+
|
|
1195
|
+
// Status chips
|
|
1196
|
+
STATUSES.forEach(function(status) {
|
|
1197
|
+
var isActive = activeFilter.status === status ? ' active' : '';
|
|
1198
|
+
html += '<button class="filter-chip' + isActive + '" onclick="filterStatus(\'' + status + '\')">' + status + '</button>';
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
// Clear filters button
|
|
1202
|
+
if (activeFilter.topic || activeFilter.type || activeFilter.status) {
|
|
1203
|
+
html += '<span class="filter-sep"></span>';
|
|
1204
|
+
html += '<button class="filter-chip" onclick="clearFilters()" style="color:var(--red);border-color:rgba(248,113,113,0.3)">clear</button>';
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
el.innerHTML = html;
|
|
1208
|
+
|
|
1209
|
+
// Update sort indicators in header
|
|
1210
|
+
var headers = document.querySelectorAll('.claims-header span[data-sort]');
|
|
1211
|
+
headers.forEach(function(h) {
|
|
1212
|
+
var field = h.getAttribute('data-sort');
|
|
1213
|
+
if (field === sortField) {
|
|
1214
|
+
h.className = 'sort-active';
|
|
1215
|
+
h.textContent = h.textContent.replace(/ [▲▼]$/, '') + (sortDir === 1 ? ' \u25B2' : ' \u25BC');
|
|
1216
|
+
} else {
|
|
1217
|
+
h.className = '';
|
|
1218
|
+
h.textContent = h.textContent.replace(/ [▲▼]$/, '');
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function renderClaims() {
|
|
1224
|
+
var claims = getFilteredClaims();
|
|
1225
|
+
document.getElementById('visible-count').textContent = claims.length + ' of ' + state.claims.length + ' shown';
|
|
1226
|
+
|
|
1227
|
+
var el = document.getElementById('claims-list');
|
|
1228
|
+
if (claims.length === 0) {
|
|
1229
|
+
if (state.claims.length === 0) {
|
|
1230
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-title">No claims yet</div><div class="empty-state-hint">Run wheat init to start a sprint, then use /research to grow claims.</div></div>';
|
|
1231
|
+
} else {
|
|
1232
|
+
el.innerHTML = '<div class="empty-state"><div class="empty-state-title">No matching claims</div><div class="empty-state-hint">Try adjusting your filters or search query.</div></div>';
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
el.innerHTML = claims.map(function(c, i) {
|
|
1238
|
+
var cls = 'claim-row';
|
|
1239
|
+
if (selectedClaimId === c.id) cls += ' selected';
|
|
1240
|
+
if (c.status === 'conflicted') cls += ' conflicted';
|
|
1241
|
+
if (c.status === 'superseded') cls += ' superseded';
|
|
1242
|
+
if (i === focusedIndex) cls += ' focused';
|
|
1243
|
+
return '<div class="' + cls + '" role="listitem" data-id="' + escAttr(c.id) + '" onclick="selectClaim(\'' + escAttr(c.id) + '\')" tabindex="-1">' +
|
|
1244
|
+
'<span class="claim-id">' + esc(c.id) + '</span>' +
|
|
1245
|
+
'<span class="claim-type-badge type-' + c.type + '">' + c.type + '</span>' +
|
|
1246
|
+
'<span class="claim-content" title="' + escAttr(c.content) + '">' + esc(c.content) + '</span>' +
|
|
1247
|
+
'<span class="claim-evidence ev-text-' + (c.evidence || 'stated') + '">' + (c.evidence || 'stated') + '</span>' +
|
|
1248
|
+
'</div>';
|
|
1249
|
+
}).join('');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function renderDetail() {
|
|
1253
|
+
var emptyEl = document.getElementById('detail-empty');
|
|
1254
|
+
var contentEl = document.getElementById('detail-content');
|
|
1255
|
+
if (!selectedClaimId) {
|
|
1256
|
+
emptyEl.style.display = '';
|
|
1257
|
+
contentEl.style.display = 'none';
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
var claim = state.claims.find(function(c) { return c.id === selectedClaimId; });
|
|
1261
|
+
if (!claim) {
|
|
1262
|
+
emptyEl.style.display = '';
|
|
1263
|
+
contentEl.style.display = 'none';
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
emptyEl.style.display = 'none';
|
|
1268
|
+
contentEl.style.display = '';
|
|
1269
|
+
|
|
1270
|
+
var conflicts = claim.conflicts_with || [];
|
|
1271
|
+
var tags = claim.tags || [];
|
|
1272
|
+
var resolvedBy = claim.resolved_by;
|
|
1273
|
+
var source = claim.source || {};
|
|
1274
|
+
|
|
1275
|
+
var html =
|
|
1276
|
+
'<div class="detail-header">' +
|
|
1277
|
+
'<div class="detail-id">' + esc(claim.id) + '</div>' +
|
|
1278
|
+
'<span class="claim-type-badge type-' + claim.type + '" style="font-size:12px">' + claim.type + '</span>' +
|
|
1279
|
+
' <span style="color:var(--fg3);font-size:12px">' + esc(claim.topic || '') + '</span>' +
|
|
1280
|
+
'<div class="detail-meta">' +
|
|
1281
|
+
'<span class="detail-tag ev-text-' + (claim.evidence || 'stated') + '">' + (claim.evidence || 'stated') + '</span>' +
|
|
1282
|
+
'<span class="detail-tag">' + (claim.status || 'active') + '</span>' +
|
|
1283
|
+
(claim.phase_added ? '<span class="detail-tag">' + claim.phase_added + '</span>' : '') +
|
|
1284
|
+
tags.map(function(t) { return '<span class="detail-tag">' + esc(t) + '</span>'; }).join('') +
|
|
1285
|
+
'</div>' +
|
|
1286
|
+
'</div>' +
|
|
1287
|
+
'<div class="detail-body">' +
|
|
1288
|
+
'<div class="detail-section">' +
|
|
1289
|
+
'<div class="detail-section-title">Content</div>' +
|
|
1290
|
+
'<div class="detail-content">' + esc(claim.content) + '</div>' +
|
|
1291
|
+
'</div>';
|
|
1292
|
+
|
|
1293
|
+
// Source section
|
|
1294
|
+
if (source.origin || source.artifact || source.witnessed_claim || source.challenged_claim || source.url) {
|
|
1295
|
+
html += '<div class="detail-section">' +
|
|
1296
|
+
'<div class="detail-section-title">Source</div>';
|
|
1297
|
+
if (source.origin) html += '<div class="detail-field"><span class="detail-field-label">origin</span><span class="detail-field-value">' + esc(source.origin) + '</span></div>';
|
|
1298
|
+
if (source.artifact) html += '<div class="detail-field"><span class="detail-field-label">artifact</span><span class="detail-field-value">' + esc(source.artifact) + '</span></div>';
|
|
1299
|
+
if (source.url) html += '<div class="detail-field"><span class="detail-field-label">url</span><span class="detail-field-value" style="word-break:break-all">' + esc(source.url) + '</span></div>';
|
|
1300
|
+
if (source.witnessed_claim) html += '<div class="detail-field"><span class="detail-field-label">witnesses</span><span class="related-claim" onclick="selectClaim(\'' + escAttr(source.witnessed_claim) + '\')">' + esc(source.witnessed_claim) + '</span></div>';
|
|
1301
|
+
if (source.challenged_claim) html += '<div class="detail-field"><span class="detail-field-label">challenges</span><span class="related-claim" onclick="selectClaim(\'' + escAttr(source.challenged_claim) + '\')">' + esc(source.challenged_claim) + '</span></div>';
|
|
1302
|
+
html += '</div>';
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Conflicts
|
|
1306
|
+
if (conflicts.length) {
|
|
1307
|
+
html += '<div class="detail-section">' +
|
|
1308
|
+
'<div class="detail-section-title">Conflicts with</div>' +
|
|
1309
|
+
conflicts.map(function(id) { return '<span class="related-claim" onclick="selectClaim(\'' + escAttr(id) + '\')">' + esc(id) + '</span>'; }).join(' ') +
|
|
1310
|
+
'</div>';
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Resolved by
|
|
1314
|
+
if (resolvedBy) {
|
|
1315
|
+
html += '<div class="detail-section">' +
|
|
1316
|
+
'<div class="detail-section-title">Resolved by</div>' +
|
|
1317
|
+
'<span class="related-claim" onclick="selectClaim(\'' + escAttr(resolvedBy) + '\')">' + esc(resolvedBy) + '</span>' +
|
|
1318
|
+
'</div>';
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Timestamp
|
|
1322
|
+
html += '<div class="detail-section">' +
|
|
1323
|
+
'<div class="detail-section-title">Timestamp</div>' +
|
|
1324
|
+
'<div class="detail-content" style="font-size:11px;color:var(--fg3)">' + esc(claim.timestamp || '--') + '</div>' +
|
|
1325
|
+
'</div>';
|
|
1326
|
+
|
|
1327
|
+
html += '</div>'; // close detail-body
|
|
1328
|
+
|
|
1329
|
+
contentEl.innerHTML = html;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function renderCompilation() {
|
|
1333
|
+
var comp = state.compilation;
|
|
1334
|
+
var statusText = document.getElementById('comp-status-text');
|
|
1335
|
+
var fill = document.getElementById('readiness-fill');
|
|
1336
|
+
var summary = document.getElementById('comp-summary');
|
|
1337
|
+
var body = document.getElementById('comp-body');
|
|
1338
|
+
|
|
1339
|
+
if (!comp) {
|
|
1340
|
+
statusText.textContent = 'No compilation data';
|
|
1341
|
+
statusText.className = 'comp-status-text comp-status-unknown';
|
|
1342
|
+
fill.style.width = '0%';
|
|
1343
|
+
fill.style.background = 'var(--fg3)';
|
|
1344
|
+
summary.textContent = '';
|
|
1345
|
+
body.innerHTML = '';
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
var status = comp.status || 'unknown';
|
|
1350
|
+
statusText.textContent = 'Compilation: ' + status;
|
|
1351
|
+
statusText.className = 'comp-status-text comp-status-' + (status === 'ready' ? 'ready' : status === 'blocked' ? 'blocked' : 'unknown');
|
|
1352
|
+
|
|
1353
|
+
// Readiness bar
|
|
1354
|
+
var warnings = comp.warnings || [];
|
|
1355
|
+
var conflicts = comp.conflict_graph || [];
|
|
1356
|
+
var totalClaims = comp.claim_count || state.claims.length;
|
|
1357
|
+
var activeClaims = state.claims.filter(function(c) { return c.status === 'active'; }).length;
|
|
1358
|
+
var readiness = totalClaims > 0 ? Math.round((activeClaims / totalClaims) * 100) : 0;
|
|
1359
|
+
if (status === 'ready') readiness = 100;
|
|
1360
|
+
if (conflicts.length > 0) readiness = Math.min(readiness, 60);
|
|
1361
|
+
fill.style.width = readiness + '%';
|
|
1362
|
+
fill.style.background = readiness >= 80 ? 'var(--green)' : readiness >= 50 ? 'var(--accent)' : 'var(--red)';
|
|
1363
|
+
|
|
1364
|
+
summary.textContent = warnings.length + ' warnings, ' + conflicts.length + ' conflicts';
|
|
1365
|
+
|
|
1366
|
+
// Body
|
|
1367
|
+
var html = '';
|
|
1368
|
+
var dtf = new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' });
|
|
1369
|
+
html += '<div class="comp-row"><span class="comp-label">Compiled</span><span class="comp-value">' + (comp.compiled_at ? dtf.format(new Date(comp.compiled_at)) : '--') + '</span></div>';
|
|
1370
|
+
html += '<div class="comp-row"><span class="comp-label">Claims</span><span class="comp-value">' + totalClaims + ' total, ' + activeClaims + ' active</span></div>';
|
|
1371
|
+
html += '<div class="comp-row"><span class="comp-label">Readiness</span><span class="comp-value">' + readiness + '%</span></div>';
|
|
1372
|
+
|
|
1373
|
+
if (warnings.length > 0) {
|
|
1374
|
+
html += '<div style="margin-top:8px;font-size:10px;color:var(--fg3);font-weight:700;text-transform:uppercase;letter-spacing:0.5px">Warnings (' + warnings.length + ')</div>';
|
|
1375
|
+
warnings.slice(0, 8).forEach(function(w) {
|
|
1376
|
+
var text = typeof w === 'string' ? w : (w.message || JSON.stringify(w));
|
|
1377
|
+
html += '<div class="comp-warning">' + esc(text) + '</div>';
|
|
1378
|
+
});
|
|
1379
|
+
if (warnings.length > 8) html += '<div style="color:var(--fg3);font-size:10px;margin-top:2px;padding-left:10px">...and ' + (warnings.length - 8) + ' more</div>';
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (conflicts.length > 0) {
|
|
1383
|
+
html += '<div style="margin-top:8px;font-size:10px;color:var(--fg3);font-weight:700;text-transform:uppercase;letter-spacing:0.5px">Conflicts (' + conflicts.length + ')</div>';
|
|
1384
|
+
conflicts.slice(0, 5).forEach(function(c) {
|
|
1385
|
+
var ids = Array.isArray(c) ? c.join(' vs ') : ((c.claims || []).join(' vs '));
|
|
1386
|
+
html += '<div class="comp-conflict">' + esc(ids) + '</div>';
|
|
1387
|
+
});
|
|
1388
|
+
if (conflicts.length > 5) html += '<div style="color:var(--fg3);font-size:10px;margin-top:2px;padding-left:10px">...and ' + (conflicts.length - 5) + ' more</div>';
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
body.innerHTML = html;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function renderStatusbar() {
|
|
1395
|
+
var comp = state.compilation;
|
|
1396
|
+
document.getElementById('claims-hash').textContent = (comp && comp.claims_hash) || '--';
|
|
1397
|
+
document.getElementById('compiler-version').textContent = comp ? 'wheat-compiler v' + comp.compiler_version : '';
|
|
1398
|
+
var warnings = (comp && comp.warnings) || [];
|
|
1399
|
+
document.getElementById('warning-count').textContent = warnings.length ? warnings.length + ' warnings' : '';
|
|
1400
|
+
|
|
1401
|
+
// Coverage mini bars
|
|
1402
|
+
var coverage = (comp && comp.coverage) || {};
|
|
1403
|
+
var evColors = { stated: 'var(--fg3)', web: 'var(--blue)', documented: 'var(--accent-light)', tested: 'var(--green)', production: 'var(--purple)' };
|
|
1404
|
+
var el = document.getElementById('coverage-mini');
|
|
1405
|
+
el.innerHTML = Object.entries(coverage).map(function(entry) {
|
|
1406
|
+
var name = entry[0], cov = entry[1];
|
|
1407
|
+
var pct = Math.min(100, (cov.claims / 8) * 100);
|
|
1408
|
+
var color = evColors[cov.max_evidence] || 'var(--fg3)';
|
|
1409
|
+
return '<div title="' + escAttr(name) + ': ' + cov.claims + ' claims (' + cov.max_evidence + ')" class="coverage-bar-mini"><div class="coverage-fill-mini" style="width:' + pct + '%;background:' + color + '"></div></div>';
|
|
1410
|
+
}).join('');
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// ── Interactions ──
|
|
1414
|
+
function selectClaim(id) {
|
|
1415
|
+
selectedClaimId = id;
|
|
1416
|
+
var claims = getFilteredClaims();
|
|
1417
|
+
focusedIndex = claims.findIndex(function(c) { return c.id === id; });
|
|
1418
|
+
renderClaims();
|
|
1419
|
+
renderDetail();
|
|
1420
|
+
|
|
1421
|
+
// On mobile, switch to detail panel
|
|
1422
|
+
if (window.innerWidth <= 768) {
|
|
1423
|
+
switchMobilePanel('detail');
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function filterTopic(topic) {
|
|
1428
|
+
activeFilter.topic = activeFilter.topic === topic ? null : topic;
|
|
1429
|
+
render();
|
|
1430
|
+
}
|
|
1431
|
+
function filterType(type) {
|
|
1432
|
+
activeFilter.type = activeFilter.type === type ? null : type;
|
|
1433
|
+
render();
|
|
1434
|
+
}
|
|
1435
|
+
function filterStatus(status) {
|
|
1436
|
+
activeFilter.status = activeFilter.status === status ? null : status;
|
|
1437
|
+
render();
|
|
1438
|
+
}
|
|
1439
|
+
function clearFilters() {
|
|
1440
|
+
activeFilter = { topic: null, type: null, status: null };
|
|
1441
|
+
render();
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function clearSearch() {
|
|
1445
|
+
searchQuery = '';
|
|
1446
|
+
document.getElementById('search-input').value = '';
|
|
1447
|
+
document.getElementById('search-clear').classList.remove('visible');
|
|
1448
|
+
render();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function toggleDetail() {
|
|
1452
|
+
detailCollapsed = !detailCollapsed;
|
|
1453
|
+
document.getElementById('app').classList.toggle('detail-collapsed', detailCollapsed);
|
|
1454
|
+
var dtBtn = document.getElementById('btn-toggle-detail');
|
|
1455
|
+
if (dtBtn) dtBtn.textContent = detailCollapsed ? 'detail +' : 'detail';
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function toggleCompilation() {
|
|
1459
|
+
compExpanded = !compExpanded;
|
|
1460
|
+
document.getElementById('comp-toggle').classList.toggle('expanded', compExpanded);
|
|
1461
|
+
document.getElementById('comp-body').classList.toggle('expanded', compExpanded);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// ── Mobile ──
|
|
1465
|
+
function switchMobilePanel(panel) {
|
|
1466
|
+
var sidebar = document.getElementById('sidebar');
|
|
1467
|
+
var main = document.getElementById('main-content');
|
|
1468
|
+
var detail = document.getElementById('detail');
|
|
1469
|
+
var tabs = document.querySelectorAll('.mobile-tab');
|
|
1470
|
+
|
|
1471
|
+
sidebar.classList.remove('mobile-visible');
|
|
1472
|
+
main.classList.remove('mobile-hidden');
|
|
1473
|
+
detail.classList.remove('mobile-visible');
|
|
1474
|
+
|
|
1475
|
+
tabs.forEach(function(t) { t.classList.toggle('active', t.dataset.panel === panel); });
|
|
1476
|
+
|
|
1477
|
+
if (panel === 'topics') {
|
|
1478
|
+
sidebar.classList.add('mobile-visible');
|
|
1479
|
+
main.classList.add('mobile-hidden');
|
|
1480
|
+
} else if (panel === 'detail') {
|
|
1481
|
+
detail.classList.add('mobile-visible');
|
|
1482
|
+
main.classList.add('mobile-hidden');
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// ── Search input ──
|
|
1487
|
+
document.getElementById('search-input').addEventListener('input', function(e) {
|
|
1488
|
+
searchQuery = e.target.value;
|
|
1489
|
+
document.getElementById('search-clear').classList.toggle('visible', searchQuery.length > 0);
|
|
1490
|
+
render();
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
// ── Keyboard ──
|
|
1494
|
+
document.addEventListener('keydown', function(e) {
|
|
1495
|
+
var target = e.target;
|
|
1496
|
+
var isInput = target.matches('input, textarea, select');
|
|
1497
|
+
|
|
1498
|
+
// "/" focuses search
|
|
1499
|
+
if (e.key === '/' && !isInput) {
|
|
1500
|
+
e.preventDefault();
|
|
1501
|
+
document.getElementById('search-input').focus();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Escape: blur search or deselect claim
|
|
1506
|
+
if (e.key === 'Escape') {
|
|
1507
|
+
if (isInput) {
|
|
1508
|
+
target.blur();
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
selectedClaimId = null;
|
|
1512
|
+
focusedIndex = -1;
|
|
1513
|
+
render();
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Don't handle navigation in inputs
|
|
1518
|
+
if (isInput) return;
|
|
1519
|
+
|
|
1520
|
+
var claims = getFilteredClaims();
|
|
1521
|
+
if (!claims.length) return;
|
|
1522
|
+
|
|
1523
|
+
if (e.key === 'j' || e.key === 'ArrowDown') {
|
|
1524
|
+
e.preventDefault();
|
|
1525
|
+
focusedIndex = Math.min(focusedIndex + 1, claims.length - 1);
|
|
1526
|
+
if (focusedIndex < 0) focusedIndex = 0;
|
|
1527
|
+
selectedClaimId = claims[focusedIndex].id;
|
|
1528
|
+
renderClaims();
|
|
1529
|
+
renderDetail();
|
|
1530
|
+
scrollClaimIntoView(selectedClaimId);
|
|
1531
|
+
}
|
|
1532
|
+
if (e.key === 'k' || e.key === 'ArrowUp') {
|
|
1533
|
+
e.preventDefault();
|
|
1534
|
+
focusedIndex = Math.max(focusedIndex - 1, 0);
|
|
1535
|
+
selectedClaimId = claims[focusedIndex].id;
|
|
1536
|
+
renderClaims();
|
|
1537
|
+
renderDetail();
|
|
1538
|
+
scrollClaimIntoView(selectedClaimId);
|
|
1539
|
+
}
|
|
1540
|
+
if (e.key === 'Enter' && selectedClaimId) {
|
|
1541
|
+
// Already selected, toggle detail open on mobile
|
|
1542
|
+
if (window.innerWidth <= 768) {
|
|
1543
|
+
switchMobilePanel('detail');
|
|
1544
|
+
} else if (detailCollapsed) {
|
|
1545
|
+
toggleDetail();
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
// 'c' to compile
|
|
1549
|
+
if (e.key === 'c' && !isInput) {
|
|
1550
|
+
recompile();
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
|
|
1554
|
+
function scrollClaimIntoView(id) {
|
|
1555
|
+
var rows = document.querySelectorAll('.claim-row');
|
|
1556
|
+
for (var i = 0; i < rows.length; i++) {
|
|
1557
|
+
if (rows[i].getAttribute('data-id') === id) {
|
|
1558
|
+
rows[i].scrollIntoView({ block: 'nearest' });
|
|
1559
|
+
break;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// ── Utility ──
|
|
1565
|
+
function esc(s) {
|
|
1566
|
+
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
1567
|
+
}
|
|
1568
|
+
function escAttr(s) {
|
|
1569
|
+
return esc(s).replace(/'/g, ''');
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// ── Sprint switching ──
|
|
1573
|
+
document.getElementById('sprint-select').addEventListener('change', function(e) {
|
|
1574
|
+
fetch('/api/switch', {
|
|
1575
|
+
method: 'POST',
|
|
1576
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1577
|
+
body: JSON.stringify({ sprint: e.target.value })
|
|
1578
|
+
})
|
|
1579
|
+
.then(function(r) { return r.json(); })
|
|
1580
|
+
.then(function(data) {
|
|
1581
|
+
state = data;
|
|
1582
|
+
render();
|
|
1583
|
+
})
|
|
1584
|
+
.catch(function(err) { console.error('switch failed:', err); });
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
// ── Init ──
|
|
1588
|
+
connectSSE();
|
|
1589
|
+
</script>
|
|
1590
|
+
<script>
|
|
1591
|
+
(function() {
|
|
1592
|
+
var LW = 0.025;
|
|
1593
|
+
var TOOL = { name: 'Wheat', letter: 'W', color: '#fbbf24' };
|
|
1594
|
+
var _c, _ctx, _s, _cx, _textStart, _restText, _font;
|
|
1595
|
+
var _state = 'drawon', _start = null, _raf, _pendingState = null;
|
|
1596
|
+
var _openPts = null, _closedPts = null;
|
|
1597
|
+
|
|
1598
|
+
function _lerp(a,b,t){ return {x:a.x+(b.x-a.x)*t, y:a.y+(b.y-a.y)*t}; }
|
|
1599
|
+
function _easeInOut(t){ return t<0.5 ? 2*t*t : 1-Math.pow(-2*t+2,2)/2; }
|
|
1600
|
+
|
|
1601
|
+
function _bracket(ctx, s, color, alpha) {
|
|
1602
|
+
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
1603
|
+
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
1604
|
+
if(alpha!==undefined) ctx.globalAlpha=alpha;
|
|
1605
|
+
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
1606
|
+
ctx.beginPath(); ctx.moveTo(cx,topY); ctx.lineTo(cx-fe,topY);
|
|
1607
|
+
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
1608
|
+
ctx.lineTo(cx,botY); ctx.stroke();
|
|
1609
|
+
if(alpha!==undefined) ctx.globalAlpha=1;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
function _drawBracket(ctx, s, color, progress) {
|
|
1613
|
+
var lw=s*LW, cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68;
|
|
1614
|
+
var topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
1615
|
+
ctx.strokeStyle=color; ctx.lineWidth=lw; ctx.lineCap='round'; ctx.lineJoin='round';
|
|
1616
|
+
var seg1=0.12, seg2=0.72;
|
|
1617
|
+
ctx.beginPath();
|
|
1618
|
+
if(progress<=seg1){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe*(progress/seg1),topY);}
|
|
1619
|
+
else if(progress<=seg1+seg2){ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);ctx.stroke();ctx.beginPath();
|
|
1620
|
+
var bt=(progress-seg1)/seg2;
|
|
1621
|
+
var p0={x:cx-fe,y:topY},p1={x:cx-gw*0.52,y:cy-gh*0.32},p2={x:cx-gw*0.52,y:cy+gh*0.24},p3={x:cx-fe,y:botY};
|
|
1622
|
+
var q1=_lerp(p0,p1,bt),q2=_lerp(p1,p2,bt),q3=_lerp(p2,p3,bt);
|
|
1623
|
+
var r1=_lerp(q1,q2,bt),r2=_lerp(q2,q3,bt),s1=_lerp(r1,r2,bt);
|
|
1624
|
+
ctx.moveTo(p0.x,p0.y);ctx.bezierCurveTo(q1.x,q1.y,r1.x,r1.y,s1.x,s1.y);}
|
|
1625
|
+
else{ctx.moveTo(cx,topY);ctx.lineTo(cx-fe,topY);
|
|
1626
|
+
ctx.bezierCurveTo(cx-gw*0.52,cy-gh*0.32, cx-gw*0.52,cy+gh*0.24, cx-fe,botY);
|
|
1627
|
+
ctx.lineTo((cx-fe)+fe*((progress-seg1-seg2)/(1-seg1-seg2)),botY);}
|
|
1628
|
+
ctx.stroke();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function _drawName(ctx, s, spellP, alpha) {
|
|
1632
|
+
var a = alpha !== undefined ? alpha : 1;
|
|
1633
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
1634
|
+
var cy = s/2 + s*0.02;
|
|
1635
|
+
ctx.globalAlpha = a; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
1636
|
+
ctx.fillText(TOOL.letter, _cx, cy);
|
|
1637
|
+
if(_restText.length > 0 && spellP > 0) {
|
|
1638
|
+
var n = _restText.length, num = Math.min(n, Math.ceil(spellP * n));
|
|
1639
|
+
var rawP = spellP * n, charP = num >= n ? 1 : rawP - Math.floor(rawP);
|
|
1640
|
+
var full = charP >= 1 ? num : num - 1;
|
|
1641
|
+
ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'left';
|
|
1642
|
+
if(full > 0) { ctx.globalAlpha = a; ctx.fillText(_restText.slice(0, full), _textStart, cy); }
|
|
1643
|
+
if(full < num) {
|
|
1644
|
+
var prevW = full > 0 ? ctx.measureText(_restText.slice(0, full)).width : 0;
|
|
1645
|
+
ctx.globalAlpha = a * (0.3 + 0.7 * charP);
|
|
1646
|
+
ctx.fillText(_restText[full], _textStart + prevW, cy);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
ctx.globalAlpha = 1;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function _getOpenPts(s) {
|
|
1653
|
+
if(_openPts && _openPts._s === s) return _openPts;
|
|
1654
|
+
var cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68, topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
1655
|
+
var pts=[];
|
|
1656
|
+
for(var t=0;t<=1;t+=0.05) pts.push({x:cx-fe*t,y:topY});
|
|
1657
|
+
var p0={x:cx-fe,y:topY},p1={x:cx-gw*0.52,y:cy-gh*0.32},p2={x:cx-gw*0.52,y:cy+gh*0.24},p3={x:cx-fe,y:botY};
|
|
1658
|
+
for(var t=0;t<=1;t+=0.02){var u=1-t;pts.push({x:u*u*u*p0.x+3*u*u*t*p1.x+3*u*t*t*p2.x+t*t*t*p3.x,y:u*u*u*p0.y+3*u*u*t*p1.y+3*u*t*t*p2.y+t*t*t*p3.y});}
|
|
1659
|
+
for(var t=0;t<=1;t+=0.05) pts.push({x:(cx-fe)+fe*t,y:botY});
|
|
1660
|
+
pts._s=s; _openPts=pts; return pts;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function _getClosedPts(s) {
|
|
1664
|
+
if(_closedPts && _closedPts._s === s) return _closedPts;
|
|
1665
|
+
var cx=_cx, cy=s/2, gw=s*0.72, gh=s*0.68, topY=cy-gh/2, botY=cy+gh/2, fe=gw*0.30;
|
|
1666
|
+
var pts=[];
|
|
1667
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:cx-fe*t,y:topY});
|
|
1668
|
+
var lp0={x:cx-fe,y:topY},lp1={x:cx-gw*0.52,y:cy-gh*0.32},lp2={x:cx-gw*0.52,y:cy+gh*0.24},lp3={x:cx-fe,y:botY};
|
|
1669
|
+
for(var t=0;t<=1;t+=0.02){var u=1-t;pts.push({x:u*u*u*lp0.x+3*u*u*t*lp1.x+3*u*t*t*lp2.x+t*t*t*lp3.x,y:u*u*u*lp0.y+3*u*u*t*lp1.y+3*u*t*t*lp2.y+t*t*t*lp3.y});}
|
|
1670
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx-fe)+2*fe*t,y:botY});
|
|
1671
|
+
var rp0={x:cx+fe,y:botY},rp1={x:cx+gw*0.52,y:cy+gh*0.24},rp2={x:cx+gw*0.52,y:cy-gh*0.32},rp3={x:cx+fe,y:topY};
|
|
1672
|
+
for(var t=0;t<=1;t+=0.02){var u=1-t;pts.push({x:u*u*u*rp0.x+3*u*u*t*rp1.x+3*u*t*t*rp2.x+t*t*t*rp3.x,y:u*u*u*rp0.y+3*u*u*t*rp1.y+3*u*t*t*rp2.y+t*t*t*rp3.y});}
|
|
1673
|
+
for(var t=0;t<=1;t+=0.03) pts.push({x:(cx+fe)-fe*t,y:topY});
|
|
1674
|
+
pts._s=s; _closedPts=pts; return pts;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function _frame(ts) {
|
|
1678
|
+
if(!_c) return;
|
|
1679
|
+
if(!_start) _start = ts;
|
|
1680
|
+
var e = ts - _start, ctx = _ctx, s = _s;
|
|
1681
|
+
ctx.clearRect(0, 0, _c.width, s);
|
|
1682
|
+
switch(_state) {
|
|
1683
|
+
case 'drawon':
|
|
1684
|
+
var bp = _easeInOut(Math.min(1, e / 1400));
|
|
1685
|
+
_drawBracket(ctx, s, TOOL.color, bp);
|
|
1686
|
+
var la = Math.max(0, Math.min(1, (e - 900) / 400));
|
|
1687
|
+
if(la > 0) {
|
|
1688
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
1689
|
+
ctx.globalAlpha = la; ctx.fillStyle = TOOL.color; ctx.textAlign = 'center';
|
|
1690
|
+
ctx.fillText(TOOL.letter, _cx, s/2 + s*0.02); ctx.globalAlpha = 1;
|
|
1691
|
+
}
|
|
1692
|
+
if(e > 1100 && _restText.length > 0) {
|
|
1693
|
+
var sp = Math.min(1, (e - 1100) / (120 * _restText.length));
|
|
1694
|
+
var n = _restText.length, num = Math.min(n, Math.ceil(sp * n));
|
|
1695
|
+
if(num > 0) {
|
|
1696
|
+
ctx.font = _font; ctx.textBaseline = 'middle';
|
|
1697
|
+
var cy = s/2 + s*0.02, rawP = sp * n;
|
|
1698
|
+
var charP = num >= n ? 1 : rawP - Math.floor(rawP);
|
|
1699
|
+
var full = charP >= 1 ? num : num - 1;
|
|
1700
|
+
ctx.fillStyle = '#e2e8f0'; ctx.textAlign = 'left';
|
|
1701
|
+
if(full > 0) ctx.fillText(_restText.slice(0, full), _textStart, cy);
|
|
1702
|
+
if(full < num) {
|
|
1703
|
+
var prevW = full > 0 ? ctx.measureText(_restText.slice(0, full)).width : 0;
|
|
1704
|
+
ctx.globalAlpha = 0.3 + 0.7 * charP;
|
|
1705
|
+
ctx.fillText(_restText[full], _textStart + prevW, cy); ctx.globalAlpha = 1;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if(e > 1100 + 120 * _restText.length + 300) { _state = _pendingState || 'idle'; _pendingState = null; _start = ts; }
|
|
1710
|
+
break;
|
|
1711
|
+
case 'idle':
|
|
1712
|
+
var breathe = 0.5 + 0.5 * (0.5 + 0.5 * Math.sin(e / 1200));
|
|
1713
|
+
_bracket(ctx, s, TOOL.color, breathe);
|
|
1714
|
+
var textBreath = 0.88 + 0.12 * (0.5 + 0.5 * Math.sin(e / 1800));
|
|
1715
|
+
_drawName(ctx, s, 1, textBreath);
|
|
1716
|
+
break;
|
|
1717
|
+
case 'shimmer':
|
|
1718
|
+
_bracket(ctx, s, TOOL.color, 0.2);
|
|
1719
|
+
var spts = _getOpenPts(s), sspeed = 1800;
|
|
1720
|
+
var spos = (e % sspeed) / sspeed;
|
|
1721
|
+
var sidx = Math.floor(spos * (spts.length - 1));
|
|
1722
|
+
var spt = spts[sidx];
|
|
1723
|
+
var sgrad = ctx.createRadialGradient(spt.x, spt.y, 0, spt.x, spt.y, s * 0.10);
|
|
1724
|
+
sgrad.addColorStop(0, TOOL.color + 'aa'); sgrad.addColorStop(1, 'transparent');
|
|
1725
|
+
ctx.fillStyle = sgrad; ctx.fillRect(0, 0, _c.width, s);
|
|
1726
|
+
var strailFrac = 0.18;
|
|
1727
|
+
var si0 = Math.max(0, Math.floor((spos - strailFrac) * (spts.length - 1)));
|
|
1728
|
+
ctx.strokeStyle = TOOL.color; ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
1729
|
+
ctx.globalAlpha = 0.85; ctx.beginPath(); ctx.moveTo(spts[si0].x, spts[si0].y);
|
|
1730
|
+
for(var si = si0 + 1; si <= sidx; si++) ctx.lineTo(spts[si].x, spts[si].y);
|
|
1731
|
+
ctx.stroke(); ctx.globalAlpha = 1;
|
|
1732
|
+
_drawName(ctx, s, 1, undefined);
|
|
1733
|
+
break;
|
|
1734
|
+
case 'orbit':
|
|
1735
|
+
_bracket(ctx, s, TOOL.color, 0.15);
|
|
1736
|
+
_drawName(ctx, s, 1, 0.4);
|
|
1737
|
+
var pts = _getOpenPts(s), speed = 1200, trailFrac = 0.28;
|
|
1738
|
+
var halfCycle = (e % speed) / speed;
|
|
1739
|
+
var cycle = (e % (speed * 2)) / (speed * 2);
|
|
1740
|
+
var pos = cycle < 0.5 ? halfCycle : 1 - halfCycle;
|
|
1741
|
+
var headIdx = Math.floor(pos * (pts.length - 1));
|
|
1742
|
+
var trailLen = Math.floor(trailFrac * pts.length);
|
|
1743
|
+
var dir = cycle < 0.5 ? 1 : -1;
|
|
1744
|
+
ctx.lineWidth = s * LW; ctx.lineCap = 'round';
|
|
1745
|
+
for(var i = 0; i < trailLen; i++) {
|
|
1746
|
+
var idx = headIdx - dir * (trailLen - i);
|
|
1747
|
+
if(idx < 0 || idx >= pts.length) continue;
|
|
1748
|
+
var nxt = idx + dir;
|
|
1749
|
+
if(nxt < 0 || nxt >= pts.length) continue;
|
|
1750
|
+
ctx.globalAlpha = (i / trailLen) * 0.7; ctx.strokeStyle = TOOL.color;
|
|
1751
|
+
ctx.beginPath(); ctx.moveTo(pts[idx].x, pts[idx].y); ctx.lineTo(pts[nxt].x, pts[nxt].y); ctx.stroke();
|
|
1752
|
+
}
|
|
1753
|
+
ctx.globalAlpha = 1;
|
|
1754
|
+
break;
|
|
1755
|
+
case 'dim':
|
|
1756
|
+
var dim = 0.1 + 0.08 * Math.sin(e / 2000);
|
|
1757
|
+
_bracket(ctx, s, TOOL.color, dim);
|
|
1758
|
+
_drawName(ctx, s, 1, 0.2);
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
_raf = requestAnimationFrame(_frame);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
_c = document.getElementById('grainLogo');
|
|
1765
|
+
if(_c) {
|
|
1766
|
+
_c.style.width = '0px';
|
|
1767
|
+
_s = 256;
|
|
1768
|
+
var targetFontPx = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
1769
|
+
var fontRatio = 0.38;
|
|
1770
|
+
var dh = 64;
|
|
1771
|
+
_c.height = _s; _c.width = 1024;
|
|
1772
|
+
_ctx = _c.getContext('2d');
|
|
1773
|
+
_cx = _s / 2;
|
|
1774
|
+
_restText = TOOL.name.slice(1);
|
|
1775
|
+
_font = '800 ' + (_s * fontRatio) + 'px -apple-system,"SF Pro Display","Helvetica Neue",Arial,sans-serif';
|
|
1776
|
+
_ctx.font = _font;
|
|
1777
|
+
var letterW = _ctx.measureText(TOOL.letter).width;
|
|
1778
|
+
var restW = _restText.length > 0 ? _ctx.measureText(_restText).width : 0;
|
|
1779
|
+
_textStart = _cx + letterW / 2 + _s * 0.02;
|
|
1780
|
+
var totalW = Math.ceil(_textStart + restW + _s * 0.12);
|
|
1781
|
+
_c.width = totalW;
|
|
1782
|
+
_ctx = _c.getContext('2d');
|
|
1783
|
+
_c.style.height = dh + 'px';
|
|
1784
|
+
_c.style.width = Math.round(totalW / _s * dh) + 'px';
|
|
1785
|
+
_state = 'drawon'; _start = null;
|
|
1786
|
+
_raf = requestAnimationFrame(_frame);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
window._grainSetState = function(state) {
|
|
1790
|
+
if(_state === state) return;
|
|
1791
|
+
if(_state === 'drawon') { _pendingState = state; return; }
|
|
1792
|
+
_state = state; _start = null;
|
|
1793
|
+
if(!_raf) _raf = requestAnimationFrame(_frame);
|
|
1794
|
+
};
|
|
1795
|
+
})();
|
|
1796
|
+
</script>
|
|
1797
|
+
</body>
|
|
1798
|
+
</html>
|