@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/bin/wheat.js +193 -0
  4. package/compiler/detect-sprints.js +319 -0
  5. package/compiler/generate-manifest.js +280 -0
  6. package/compiler/wheat-compiler.js +1229 -0
  7. package/lib/compiler.js +35 -0
  8. package/lib/connect.js +418 -0
  9. package/lib/disconnect.js +188 -0
  10. package/lib/guard.js +151 -0
  11. package/lib/index.js +14 -0
  12. package/lib/init.js +457 -0
  13. package/lib/install-prompt.js +186 -0
  14. package/lib/quickstart.js +276 -0
  15. package/lib/serve-mcp.js +509 -0
  16. package/lib/server.js +391 -0
  17. package/lib/stats.js +184 -0
  18. package/lib/status.js +135 -0
  19. package/lib/update.js +71 -0
  20. package/package.json +53 -0
  21. package/public/index.html +1798 -0
  22. package/templates/claude.md +122 -0
  23. package/templates/commands/blind-spot.md +47 -0
  24. package/templates/commands/brief.md +73 -0
  25. package/templates/commands/calibrate.md +39 -0
  26. package/templates/commands/challenge.md +72 -0
  27. package/templates/commands/connect.md +104 -0
  28. package/templates/commands/evaluate.md +80 -0
  29. package/templates/commands/feedback.md +60 -0
  30. package/templates/commands/handoff.md +53 -0
  31. package/templates/commands/init.md +68 -0
  32. package/templates/commands/merge.md +51 -0
  33. package/templates/commands/present.md +52 -0
  34. package/templates/commands/prototype.md +68 -0
  35. package/templates/commands/replay.md +61 -0
  36. package/templates/commands/research.md +73 -0
  37. package/templates/commands/resolve.md +42 -0
  38. package/templates/commands/status.md +56 -0
  39. package/templates/commands/witness.md +79 -0
  40. 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">&#x2715;</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">&#9654;</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
+ '&nbsp;<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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
1567
+ }
1568
+ function escAttr(s) {
1569
+ return esc(s).replace(/'/g, '&#39;');
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>