@jabworks/condux 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jabworks/condux",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "OpenCode plugin for the condux workflow toolkit — bundles the condux skills and injects the specialist agents (coder, explorer, planner, researcher) plus an opt-in plan-review listener.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -9,7 +9,7 @@
9
9
  Card, Textarea, Popover, Separator) — adapted as vanilla CSS, no React. -->
10
10
  <style>
11
11
  /* tokens:core:start */
12
- /* Shared colour core — do not hand-edit; edit scripts/tokens/core.css. */
12
+ /* Shared design core — do not hand-edit; edit scripts/tokens/core.css. */
13
13
  :root {
14
14
  --background: #111110;
15
15
  --card: #191918;
@@ -41,11 +41,49 @@
41
41
  --info-border: #104d87;
42
42
  --outline: rgba(237, 237, 236, 0.08);
43
43
  --subtle: #686560;
44
- --radius: 0.25rem;
44
+ --shadow-sm: 0 1px 2px rgba(6, 6, 5, 0.5);
45
+ --shadow-md: 0 4px 14px rgba(6, 6, 5, 0.55);
46
+ --shadow-lg: 0 20px 60px rgba(6, 6, 5, 0.62);
45
47
  --mono: "Geist Mono", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Code", monospace;
48
+ --sans: "Geist", "Inter Tight", ui-sans-serif, system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
49
+ --text-2xs: 0.6875rem;
50
+ --text-xs: 0.75rem;
51
+ --text-sm: 0.8125rem;
52
+ --text-base: 0.875rem;
53
+ --text-md: 1rem;
54
+ --text-lg: 1.125rem;
55
+ --text-xl: 1.375rem;
56
+ --text-2xl: 1.75rem;
57
+ --text-3xl: 2.5rem;
58
+ --text-4xl: 3.5rem;
59
+ --leading-tight: 1.15;
60
+ --leading-snug: 1.35;
61
+ --leading-normal: 1.55;
62
+ --leading-relaxed: 1.7;
63
+ --tracking-tight: -0.022em;
64
+ --tracking-normal: 0em;
65
+ --tracking-wide: 0.06em;
66
+ --space-1: 0.25rem;
67
+ --space-2: 0.5rem;
68
+ --space-3: 0.75rem;
69
+ --space-4: 1rem;
70
+ --space-5: 1.5rem;
71
+ --space-6: 2rem;
72
+ --space-8: 3rem;
73
+ --space-12: 4rem;
74
+ --radius-sm: 0.1875rem;
75
+ --radius: 0.25rem;
76
+ --radius-md: 0.5rem;
77
+ --radius-lg: 0.75rem;
78
+ --radius-full: 9999px;
79
+ --dur-fast: 120ms;
80
+ --dur: 200ms;
81
+ --dur-slow: 320ms;
82
+ --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
83
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
46
84
  }
47
85
  @media (prefers-color-scheme: light) {
48
- :root {
86
+ :root:not([data-theme="dark"]) {
49
87
  --background: #f6f5ef;
50
88
  --card: #fcfbf6;
51
89
  --card-foreground: #36302a;
@@ -76,34 +114,381 @@
76
114
  --info-border: #bed7ef;
77
115
  --outline: rgba(54, 48, 42, 0.08);
78
116
  --subtle: #a8a294;
117
+ --shadow-sm: 0 1px 2px rgba(54, 48, 42, 0.1);
118
+ --shadow-md: 0 4px 14px rgba(54, 48, 42, 0.13);
119
+ --shadow-lg: 0 20px 60px rgba(54, 48, 42, 0.18);
79
120
  }
80
121
  }
122
+ :root[data-theme="light"] {
123
+ --background: #f6f5ef;
124
+ --card: #fcfbf6;
125
+ --card-foreground: #36302a;
126
+ --popover: #fcfbf6;
127
+ --foreground: #36302a;
128
+ --muted: #f0efe3;
129
+ --muted-foreground: #726c5e;
130
+ --border: #ddd6c6;
131
+ --input: #ddd6c6;
132
+ --primary: #967e60;
133
+ --primary-foreground: #ffffff;
134
+ --primary-hover: #897254;
135
+ --primary-muted: #efece0;
136
+ --primary-text: #6f5e44;
137
+ --ring: #967e60;
138
+ --accent: #eae7da;
139
+ --success: #3f7a34;
140
+ --success-muted: #e6f5e2;
141
+ --success-border: #c2e1b3;
142
+ --warning: #93761d;
143
+ --warning-muted: #fbf4c4;
144
+ --warning-border: #ebd87f;
145
+ --destructive: #bf4f3b;
146
+ --destructive-muted: #fbe8e2;
147
+ --destructive-border: #f0c9ba;
148
+ --info: #3f6fa6;
149
+ --info-muted: #e6f1fc;
150
+ --info-border: #bed7ef;
151
+ --outline: rgba(54, 48, 42, 0.08);
152
+ --subtle: #a8a294;
153
+ --shadow-sm: 0 1px 2px rgba(54, 48, 42, 0.1);
154
+ --shadow-md: 0 4px 14px rgba(54, 48, 42, 0.13);
155
+ --shadow-lg: 0 20px 60px rgba(54, 48, 42, 0.18);
156
+ }
81
157
  /* tokens:core:end */
82
158
 
83
- /* plan-review extensions — the annotation highlight and a second radius
84
- step. Outside the markers on purpose: anything inside is replaced. */
159
+ /* kit:css:start */
160
+ /* Shared state layer — do not hand-edit; edit scripts/tokens/kit.css. */
161
+
162
+ /* :where() keeps these at zero specificity, so a surface overrides them by
163
+ writing a plain rule rather than escalating into an !important contest. */
164
+ :where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
165
+ outline: 2px solid var(--ring);
166
+ outline-offset: 2px;
167
+ border-radius: var(--radius-sm);
168
+ }
169
+
170
+ /* Mouse users should not see the ring; keyboard users must. */
171
+ :where(a, button, input, select, textarea, summary, [tabindex]):focus:not(:focus-visible) {
172
+ outline: none;
173
+ }
174
+
175
+ .kit-skip {
176
+ position: absolute;
177
+ left: var(--space-2);
178
+ top: -4rem;
179
+ z-index: 999;
180
+ padding: var(--space-2) var(--space-3);
181
+ background: var(--primary);
182
+ color: var(--primary-foreground);
183
+ border-radius: var(--radius);
184
+ text-decoration: none;
185
+ transition: top var(--dur) var(--ease-out);
186
+ }
187
+
188
+ .kit-skip:focus {
189
+ top: var(--space-2);
190
+ }
191
+
192
+ /* Empty, loading and error states. Namespaced kit- because three of the four
193
+ surfaces already own an .empty or .error class of their own. */
194
+ .kit-empty {
195
+ border: 1px dashed var(--border);
196
+ border-radius: var(--radius-md);
197
+ padding: var(--space-8) var(--space-4);
198
+ text-align: center;
199
+ color: var(--muted-foreground);
200
+ }
201
+
202
+ .kit-empty__title {
203
+ margin: 0 0 var(--space-1);
204
+ font-size: var(--text-md);
205
+ color: var(--foreground);
206
+ }
207
+
208
+ .kit-empty__body {
209
+ margin: 0 auto;
210
+ max-width: 44ch;
211
+ font-size: var(--text-sm);
212
+ text-wrap: pretty;
213
+ }
214
+
215
+ .kit-error {
216
+ display: flex;
217
+ gap: var(--space-2);
218
+ align-items: baseline;
219
+ padding: var(--space-2) var(--space-3);
220
+ background: var(--destructive-muted);
221
+ border: 1px solid var(--destructive-border);
222
+ border-radius: var(--radius);
223
+ color: var(--destructive);
224
+ font-size: var(--text-sm);
225
+ }
226
+
227
+ /* Shaped like the content it stands in for — never a spinner. Width is the
228
+ caller's job, so one rule serves a title, a row and a paragraph. */
229
+ .kit-skel {
230
+ height: 0.75rem;
231
+ border-radius: var(--radius-sm);
232
+ background: linear-gradient(90deg, var(--muted) 0%, var(--accent) 50%, var(--muted) 100%);
233
+ background-size: 200% 100%;
234
+ animation: kit-shimmer 1.4s var(--ease-out) infinite;
235
+ }
236
+
237
+ @keyframes kit-shimmer {
238
+ 100% {
239
+ background-position: -200% 0;
240
+ }
241
+ }
242
+
243
+ /* Keyboard help overlay, populated by kit.js from the live binding registry. */
244
+ .kit-help {
245
+ position: fixed;
246
+ inset: 0;
247
+ z-index: 998;
248
+ display: grid;
249
+ place-items: center;
250
+ padding: var(--space-4);
251
+ background: color-mix(in oklch, var(--background) 78%, transparent);
252
+ }
253
+
254
+ .kit-help[hidden] {
255
+ display: none;
256
+ }
257
+
258
+ .kit-help__panel {
259
+ width: min(34rem, 100%);
260
+ max-height: 80dvh;
261
+ overflow-y: auto;
262
+ padding: var(--space-5);
263
+ background: var(--card);
264
+ color: var(--card-foreground);
265
+ border: 1px solid var(--border);
266
+ border-radius: var(--radius-md);
267
+ box-shadow: var(--shadow-lg);
268
+ }
269
+
270
+ .kit-help__title {
271
+ margin: 0 0 var(--space-4);
272
+ font-family: var(--mono);
273
+ font-size: var(--text-2xs);
274
+ letter-spacing: var(--tracking-wide);
275
+ text-transform: uppercase;
276
+ color: var(--subtle);
277
+ }
278
+
279
+ .kit-help__row {
280
+ display: flex;
281
+ justify-content: space-between;
282
+ gap: var(--space-4);
283
+ padding: var(--space-1) 0;
284
+ font-size: var(--text-sm);
285
+ }
286
+
287
+ .kit-help__keys {
288
+ flex: none;
289
+ display: flex;
290
+ gap: var(--space-1);
291
+ }
292
+
293
+ .kit-kbd {
294
+ min-width: 1.4rem;
295
+ padding: 0.1rem 0.4rem;
296
+ text-align: center;
297
+ font-family: var(--mono);
298
+ font-size: var(--text-2xs);
299
+ color: var(--foreground);
300
+ background: var(--popover);
301
+ border: 1px solid var(--border);
302
+ border-bottom-width: 2px;
303
+ border-radius: var(--radius-sm);
304
+ }
305
+
306
+ .kit-theme {
307
+ display: inline-flex;
308
+ gap: 1px;
309
+ }
310
+
311
+ .kit-theme button {
312
+ font: inherit;
313
+ font-size: var(--text-2xs);
314
+ padding: var(--space-1) var(--space-2);
315
+ color: var(--muted-foreground);
316
+ background: var(--card);
317
+ border: 1px solid var(--border);
318
+ cursor: pointer;
319
+ transition:
320
+ color var(--dur-fast) var(--ease-out),
321
+ background-color var(--dur-fast) var(--ease-out),
322
+ border-color var(--dur-fast) var(--ease-out);
323
+ }
324
+
325
+ .kit-theme button:first-child {
326
+ border-radius: var(--radius) 0 0 var(--radius);
327
+ }
328
+
329
+ .kit-theme button:last-child {
330
+ border-radius: 0 var(--radius) var(--radius) 0;
331
+ }
332
+
333
+ .kit-theme button:hover {
334
+ color: var(--foreground);
335
+ border-color: var(--primary);
336
+ }
337
+
338
+ .kit-theme button[aria-pressed="true"] {
339
+ color: var(--primary-foreground);
340
+ background: var(--primary);
341
+ border-color: var(--primary);
342
+ }
343
+
344
+ /* A keyboard layer nobody can discover is a keyboard layer nobody uses. kit.js
345
+ places this next to every theme group, so the ? overlay has a visible way in
346
+ for anyone who was never told the shortcut exists. */
347
+ /* kit.js wraps the theme group and the help button in this. The theme group
348
+ keeps growing where its host stretched it (the guide's rail), and stays
349
+ content-sized where the host did not (the board's action row). */
350
+ .kit-controls {
351
+ display: flex;
352
+ align-items: center;
353
+ }
354
+
355
+ .kit-controls > .kit-theme {
356
+ flex: 1 1 auto;
357
+ }
358
+
359
+ .kit-helpbtn {
360
+ margin-left: var(--space-2);
361
+ width: 1.65rem;
362
+ height: 1.65rem;
363
+ flex: none;
364
+ font: inherit;
365
+ font-family: var(--mono);
366
+ font-size: var(--text-2xs);
367
+ line-height: 1;
368
+ color: var(--muted-foreground);
369
+ background: var(--card);
370
+ border: 1px solid var(--border);
371
+ border-radius: var(--radius-full);
372
+ cursor: pointer;
373
+ transition:
374
+ color var(--dur-fast) var(--ease-out),
375
+ border-color var(--dur-fast) var(--ease-out);
376
+ }
377
+
378
+ .kit-helpbtn:hover {
379
+ color: var(--foreground);
380
+ border-color: var(--primary);
381
+ }
382
+
383
+ .kit-copied {
384
+ color: var(--success);
385
+ }
386
+
387
+ @media (prefers-reduced-motion: reduce) {
388
+ *,
389
+ *::before,
390
+ *::after {
391
+ animation-duration: 0.01ms !important;
392
+ animation-iteration-count: 1 !important;
393
+ transition-duration: 0.01ms !important;
394
+ scroll-behavior: auto !important;
395
+ }
396
+ }
397
+
398
+ /* Browsers do not print background colours by default, so a dark-themed page
399
+ prints light text onto white and reads as blank. Repointing the tokens is
400
+ what makes Ctrl-P work at all; hiding chrome is the surface's opt-in. */
401
+ @media print {
402
+ :root {
403
+ --background: #ffffff;
404
+ --card: #ffffff;
405
+ --popover: #ffffff;
406
+ --muted: #f4f4f2;
407
+ --accent: #f4f4f2;
408
+ --foreground: #000000;
409
+ --card-foreground: #000000;
410
+ --muted-foreground: #444444;
411
+ --subtle: #666666;
412
+ --border: #999999;
413
+ --input: #999999;
414
+ --shadow-sm: none;
415
+ --shadow-md: none;
416
+ --shadow-lg: none;
417
+ }
418
+
419
+ body {
420
+ background: #ffffff;
421
+ color: #000000;
422
+ }
423
+
424
+ .kit-skip,
425
+ .kit-help,
426
+ .kit-helpbtn,
427
+ .kit-theme,
428
+ [data-kit-chrome] {
429
+ display: none !important;
430
+ }
431
+
432
+ a[href^="http"]::after {
433
+ content: " (" attr(href) ")";
434
+ font-size: 90%;
435
+ color: #555555;
436
+ }
437
+
438
+ .kit-skel {
439
+ animation: none;
440
+ }
441
+ }
442
+ /* kit:css:end */
443
+
444
+ /* plan-review extensions — the annotation highlight. Outside the markers
445
+ on purpose: anything inside is replaced.
446
+ --radius-md was defined here at 0.25rem, shadowing the core's 0.5rem
447
+ by source order. The core value is adopted as-is; spots that wanted
448
+ the smaller 0.25rem step now say so explicitly via --radius, which
449
+ already carries that value and is already this file's convention
450
+ (.nav .tabs, .popover, .chip, code, pre, …). */
85
451
  :root {
86
452
  --hl: rgba(255, 202, 22, 0.30); --hl-active: rgba(255, 202, 22, 0.55);
87
- --radius-md: 0.25rem;
88
453
  }
89
454
  @media (prefers-color-scheme: light) {
90
455
  :root { --hl: rgba(234, 179, 8, 0.42); --hl-active: rgba(202, 138, 4, 0.62); }
91
456
  }
92
457
  * { box-sizing: border-box; }
93
458
  html, body { height: 100%; }
94
- body { margin: 0; background: var(--background); color: var(--foreground); font: 13px/1.6 var(--mono); font-variant-numeric: tabular-nums; display: flex; flex-direction: column; height: 100vh; }
95
- .app { display: grid; grid-template-columns: 248px minmax(0, 1fr) 372px; flex: 1 1 auto; min-height: 0; }
459
+ /* .kit-error sets display:flex unconditionally; an author rule beats the
460
+ UA [hidden] default regardless of specificity, so the hidden attribute
461
+ needs its own author rule to still win. */
462
+ .kit-error[hidden] { display: none; }
463
+ /* Page shell: content row fills, the decision bar pins to the bottom.
464
+ .app below is the second axis (3 columns) — together they are the
465
+ one legitimately 2-D layout in this file; everything else here is a
466
+ one-dimensional row or column and stays flex. */
467
+ body { margin: 0; background: var(--background); color: var(--foreground); font-family: var(--sans); font-size: var(--text-sm); line-height: var(--leading-normal); font-variant-numeric: tabular-nums; display: grid; grid-template-rows: 1fr auto; height: 100dvh; }
468
+ .app { display: grid; grid-template-columns: 248px minmax(0, 1fr) 372px; min-height: 0; }
469
+ @media (max-width: 1100px) {
470
+ .app { grid-template-columns: 208px minmax(0, 1fr) 320px; }
471
+ }
472
+ @media (max-width: 860px) {
473
+ .app { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; max-height: none; }
474
+ .nav { max-height: 38vh; border-right: none; border-bottom: 1px solid var(--border); }
475
+ .chat { max-height: 34vh; border-left: none; border-top: 1px solid var(--border); }
476
+ }
477
+ @media (max-width: 640px) {
478
+ .main { padding: var(--space-5) var(--space-4) 100px; }
479
+ .popover { width: calc(100vw - 24px); }
480
+ }
96
481
  ::-webkit-scrollbar { width: 9px; height: 9px; }
97
482
  ::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 9999px; border: 2px solid transparent; background-clip: padding-box; }
98
483
  ::-webkit-scrollbar-track { background: transparent; }
99
484
 
100
485
  /* ---- Terminus: Button ---- */
101
486
  .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;
102
- font: 500 13px var(--mono); border-radius: var(--radius-md); padding: 8px 14px; border: 1px solid transparent;
487
+ font: 500 var(--text-sm) var(--sans); border-radius: var(--radius); padding: 8px 14px; border: 1px solid transparent;
103
488
  background: transparent; color: var(--foreground); cursor: pointer; transition: background-color .12s, border-color .12s, color .12s; }
104
489
  .btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--background), 0 0 0 4px color-mix(in oklch, var(--ring) 45%, transparent); }
105
490
  .btn:disabled { pointer-events: none; opacity: .4; }
106
- .btn-sm { padding: 6px 10px; font-size: 12px; }
491
+ .btn-sm { padding: 6px 10px; font-size: var(--text-xs); }
107
492
  .btn-block { width: 100%; }
108
493
  .btn-solid { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); }
109
494
  .btn-solid:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
@@ -119,40 +504,41 @@
119
504
  .btn-danger:hover { background: color-mix(in oklch, var(--destructive-muted) 70%, var(--destructive)); }
120
505
 
121
506
  /* ---- Terminus: Badge ---- */
122
- .badge { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--radius-md); border: 1px solid transparent;
123
- font: 500 11px var(--mono); padding: 1px 6px; min-width: 18px; justify-content: center; }
507
+ .badge { display: inline-flex; align-items: center; gap: 4px; border-radius: var(--radius); border: 1px solid transparent;
508
+ font: 500 var(--text-2xs) var(--mono); padding: 1px 6px; min-width: 18px; justify-content: center; }
124
509
  .badge-warning { background: var(--warning-muted); color: var(--warning); border-color: var(--warning-border); }
125
510
  .badge-soft { background: var(--primary-muted); color: var(--primary-text); }
126
511
 
127
512
  /* Left: navigation — Contents (docs + headings) | Files (paths touched) */
128
513
  .nav { border-right: 1px solid var(--border); background: var(--card); overflow-y: auto; padding: 18px 14px 40px; }
514
+ .nav .kit-theme { margin: 0 0 12px; }
129
515
  .nav .tabs { display: flex; margin: 0 0 12px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
130
- .nav .tab { flex: 1; font: 500 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; padding: 6px 4px;
516
+ .nav .tab { flex: 1; font: 500 var(--text-2xs) var(--sans); letter-spacing: .08em; text-transform: uppercase; padding: 6px 4px;
131
517
  background: transparent; border: none; border-right: 1px solid var(--border); color: var(--subtle); cursor: pointer; }
132
518
  .nav .tab:last-child { border-right: none; }
133
519
  .nav .tab:hover { color: var(--foreground); }
134
520
  .nav .tab.active { background: var(--accent); color: var(--foreground); }
135
521
  .nav a.doc { font-weight: 600; color: var(--foreground); }
136
522
  .nav a.doc.active { border-left-color: var(--primary); background: var(--accent); }
137
- .nav a.file .t { font-size: 12px; }
138
- .nav .none { color: var(--subtle); font-size: 12px; padding: 4px 6px; }
523
+ .nav a.file .t { font-family: var(--mono); font-size: var(--text-xs); }
524
+ .nav .none { color: var(--subtle); font-size: var(--text-xs); padding: 4px 6px; }
139
525
  code.mention-flash { outline: 2px solid var(--warning); outline-offset: 1px; }
140
- .nav .label, .chat .label { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin: 0 0 12px; padding: 0 6px; }
526
+ .nav .label, .chat .label { font-size: var(--text-2xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--subtle); margin: 0 0 12px; padding: 0 6px; }
141
527
  .nav a { display: flex; align-items: center; gap: 6px; color: var(--muted-foreground); text-decoration: none; padding: 4px 6px; border-radius: 4px; border-left: 2px solid transparent; }
142
528
  .nav a span.t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
143
529
  .nav a:hover { background: var(--accent); color: var(--foreground); }
144
530
  .nav a.active { color: var(--foreground); border-left-color: var(--primary); background: var(--accent); }
145
- .nav a.lvl2 { padding-left: 16px; } .nav a.lvl3 { padding-left: 28px; font-size: 12px; color: var(--subtle); }
531
+ .nav a.lvl2 { padding-left: 16px; } .nav a.lvl3 { padding-left: 28px; font-size: var(--text-xs); color: var(--subtle); }
146
532
 
147
533
  /* Directory mode: doc-site tree — folder rows fold, filter narrows, the
148
534
  active doc alone carries its headings. All of it DIRMODE-gated. */
149
535
  .nav .tree-filter { display: none; margin: 0 0 10px; }
150
- .nav .tree-filter input { width: 100%; font: inherit; font-size: 13px; color: var(--foreground); background: var(--background);
536
+ .nav .tree-filter input { width: 100%; font: inherit; font-size: var(--text-sm); color: var(--foreground); background: var(--background);
151
537
  border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 8px; outline: none; }
152
538
  .nav .tree-filter input:focus { border-color: var(--ring); }
153
539
  .nav a.folder { font-weight: 600; color: var(--muted-foreground); }
154
- .nav a.folder .arrow { flex: none; width: 12px; color: var(--subtle); font-size: 10px; }
155
- .crumbs { display: none; max-width: 760px; margin: 0 auto 18px; font: 12px var(--mono); color: var(--subtle);
540
+ .nav a.folder .arrow { flex: none; width: 12px; color: var(--subtle); font-size: var(--text-2xs); }
541
+ .crumbs { display: none; max-width: 760px; margin: 0 auto 18px; font: var(--text-xs) var(--mono); color: var(--subtle);
156
542
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
157
543
  .crumbs .seg { color: var(--muted-foreground); }
158
544
  .crumbs a.seg { color: var(--muted-foreground); text-decoration: none; cursor: pointer; }
@@ -162,7 +548,7 @@
162
548
  .pnav { display: none; max-width: 760px; margin: 32px auto 0; padding-top: 14px; border-top: 1px solid var(--border);
163
549
  justify-content: space-between; gap: 12px; }
164
550
  .pnav a { display: inline-flex; align-items: center; gap: 6px; max-width: 48%; color: var(--muted-foreground);
165
- text-decoration: none; font-size: 13px; padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius);
551
+ text-decoration: none; font-size: var(--text-sm); padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius);
166
552
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
167
553
  .pnav a:hover { background: var(--accent); color: var(--foreground); }
168
554
  .pnav .spacer { flex: 1; }
@@ -173,20 +559,20 @@
173
559
  linear-gradient(to right, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px),
174
560
  linear-gradient(to bottom, color-mix(in oklab, var(--foreground) 3%, transparent) 1px, transparent 1px);
175
561
  background-size: 32px 32px; }
176
- .main .hint { color: var(--subtle); margin: 0 auto 24px; font-size: 12px; max-width: 760px; }
177
- .doc { max-width: 760px; margin: 0 auto; }
562
+ .main .hint { color: var(--subtle); margin: 0 auto 24px; font-size: var(--text-xs); max-width: 760px; }
563
+ .doc { max-width: 760px; margin: 0 auto; line-height: var(--leading-relaxed); }
178
564
 
179
565
  /* Revision diff — on a revised plan, show what changed instead of a silent swap. */
180
566
  .revbar { display: none; align-items: center; gap: 10px; max-width: 760px; margin: 0 auto 20px;
181
567
  padding: 9px 12px; border: 1px solid var(--warning-border); background: var(--warning-muted); border-radius: var(--radius); }
182
568
  .revbar.show { display: flex; }
183
- .revbar .rb-text { color: var(--warning); font: 600 12px var(--mono); }
184
- .revbar .rb-stat { color: var(--muted-foreground); font-size: 12px; }
569
+ .revbar .rb-text { color: var(--warning); font: 600 var(--text-xs) var(--sans); }
570
+ .revbar .rb-stat { color: var(--muted-foreground); font-size: var(--text-xs); }
185
571
  .revbar .rb-actions { margin-left: auto; display: flex; gap: 6px; }
186
572
  .diffwrap { display: none; max-width: 760px; margin: 0 auto; }
187
573
  .diffwrap.show { display: block; }
188
574
  .diffview { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius);
189
- padding: 8px 0; overflow-x: auto; font: 12px/1.55 var(--mono); }
575
+ padding: 8px 0; overflow-x: auto; font: var(--text-xs)/var(--leading-normal) var(--mono); }
190
576
  .diffline { display: flex; white-space: pre; padding: 0 12px; }
191
577
  .diffline .g { width: 14px; flex: none; color: var(--subtle); user-select: none; }
192
578
  .diffline.ctx { color: var(--muted-foreground); }
@@ -195,20 +581,20 @@
195
581
  .diffline.del { background: var(--destructive-muted); color: var(--destructive); }
196
582
  .diffline.del .g { color: var(--destructive); }
197
583
  .diffline.fold { justify-content: center; color: var(--subtle); font-style: italic; padding: 3px 12px; }
198
- h1, h2, h3, h4, h5, h6 { line-height: 1.3; margin: 1.5em 0 0.6em; scroll-margin-top: 24px; }
199
- h1 { font-size: 21px; } h2 { font-size: 16px; } h3 { font-size: 14px; color: var(--muted-foreground); }
200
- h4 { font-size: 13px; } h5, h6 { font-size: 12px; color: var(--muted-foreground); }
584
+ h1, h2, h3, h4, h5, h6 { line-height: var(--leading-snug); margin: 1.5em 0 0.6em; scroll-margin-top: 24px; }
585
+ h1 { font-size: var(--text-xl); } h2 { font-size: var(--text-md); } h3 { font-size: var(--text-base); color: var(--muted-foreground); }
586
+ h4 { font-size: var(--text-sm); } h5, h6 { font-size: var(--text-xs); color: var(--muted-foreground); }
201
587
  p { margin: 0.6em 0; }
202
588
  ul, ol { margin: 0.6em 0; padding-left: 22px; } li { margin: 0.25em 0; }
203
589
  li.task { list-style: none; margin-left: -18px; }
204
590
  del { color: var(--subtle); }
205
591
  a { color: var(--info); }
206
- code { background: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; font-size: 12px; }
207
- pre { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; overflow-x: auto; }
592
+ code { background: var(--muted); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; font-family: var(--mono); font-size: var(--text-xs); }
593
+ pre { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; overflow-x: auto; font-family: var(--mono); }
208
594
  pre code { background: none; border: none; padding: 0; }
209
595
  blockquote { margin: 0.6em 0; padding: 2px 14px; color: var(--muted-foreground); border-left: 3px solid var(--primary); }
210
596
  hr { border: none; border-top: 1px solid var(--border); margin: 1.6em 0; }
211
- table { border-collapse: collapse; margin: 0.9em 0; width: 100%; display: block; overflow-x: auto; font-size: 13px; }
597
+ table { border-collapse: collapse; margin: 0.9em 0; width: 100%; display: block; overflow-x: auto; font-size: var(--text-sm); }
212
598
  th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; vertical-align: top; }
213
599
  th { background: var(--muted); font-weight: 600; }
214
600
  tbody tr:nth-child(even) { background: color-mix(in oklch, var(--muted) 40%, transparent); }
@@ -224,18 +610,18 @@
224
610
  .popover:not(.expanded) .chip:last-child { border-bottom-right-radius: var(--radius); }
225
611
  /* Floating annotation toolbar — a menubar of category items over an input. */
226
612
  .popover { position: absolute; z-index: 31; display: none; width: 340px; background: var(--popover); border: 1px solid var(--border);
227
- border-radius: var(--radius); box-shadow: 0 12px 32px rgba(0,0,0,0.45); padding: 0; }
228
- .popover .q { color: var(--muted-foreground); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
613
+ border-radius: var(--radius); box-shadow: var(--shadow-lg); padding: 0; }
614
+ .popover .q { color: var(--muted-foreground); font-size: var(--text-2xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
229
615
  padding: 6px 10px; margin: 0; background: var(--muted); border-bottom: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0; }
230
616
  /* Menubar strip: category items, evenly split, divider-separated */
231
617
  .popover .cats { display: flex; margin: 0; border-bottom: 1px solid var(--border); }
232
- .chip { flex: 1; font: 500 11px var(--mono); padding: 6px 8px; text-align: center; border: none; border-right: 1px solid var(--border);
618
+ .chip { flex: 1; font: 500 var(--text-2xs) var(--sans); padding: 6px 8px; text-align: center; border: none; border-right: 1px solid var(--border);
233
619
  background: transparent; color: var(--muted-foreground); cursor: pointer; transition: background-color .12s, color .12s; }
234
620
  .chip:last-child { border-right: none; }
235
621
  .chip:hover { background: var(--accent); color: var(--foreground); }
236
622
  .chip.active { background: var(--primary-muted); color: var(--primary-text); }
237
623
  .popover .row { display: flex; justify-content: flex-end; gap: 8px; align-items: center; padding: 6px 8px; border-top: 1px solid var(--border); }
238
- .popover .hintk { color: var(--subtle); font-size: 11px; margin-right: auto; }
624
+ .popover .hintk { color: var(--subtle); font-size: var(--text-2xs); margin-right: auto; }
239
625
  /* Caret pointing at the selection (down when the toolbar sits above it) */
240
626
  .popover::after { content: ''; position: absolute; left: 18px; width: 9px; height: 9px; background: var(--popover);
241
627
  border: 1px solid var(--border); transform: rotate(45deg); display: none; }
@@ -243,8 +629,8 @@
243
629
  .popover.below::after { display: block; top: -5px; background: var(--muted); border-bottom: none; border-right: none; }
244
630
 
245
631
  /* ---- Terminus: Textarea ---- */
246
- .ta { width: 100%; resize: vertical; font: 13px/1.5 var(--mono); color: var(--foreground); background: transparent;
247
- border: 1px solid var(--input); border-radius: var(--radius-md); padding: 8px 10px; }
632
+ .ta { width: 100%; resize: vertical; font: var(--text-sm)/var(--leading-normal) var(--sans); color: var(--foreground); background: transparent;
633
+ border: 1px solid var(--input); border-radius: var(--radius); padding: 8px 10px; }
248
634
  .ta::placeholder { color: var(--subtle); }
249
635
  .ta:focus-visible { outline: none; border-color: var(--ring); box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 28%, transparent); }
250
636
  .popover .ta { min-height: 132px; border: none; border-radius: 0; padding: 8px 10px; } .popover .ta:focus-visible { outline: none; box-shadow: none; }
@@ -256,26 +642,26 @@
256
642
  .thread { flex: 1; overflow-y: auto; padding: 6px 14px; display: flex; flex-direction: column; gap: 8px; }
257
643
  .thread .empty { color: var(--subtle); padding: 8px 4px; }
258
644
  /* Terminus: Card */
259
- .msg { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--card); box-shadow: 0 1px 2px rgba(0,0,0,0.16); cursor: default; }
645
+ .msg { border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; background: var(--card); box-shadow: var(--shadow-sm); cursor: default; }
260
646
  .msg.note { border-color: var(--warning-border); cursor: pointer; }
261
647
  .msg.note.active { box-shadow: 0 0 0 2px var(--warning); }
262
648
  .msg .meta { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; align-items: center; }
263
- .msg .who { color: var(--muted-foreground); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; } .msg.note .who { color: var(--warning); }
264
- .msg .del { cursor: pointer; border: none; background: none; color: var(--subtle); font-size: 11px; padding: 0 2px; } .msg .del:hover { color: var(--destructive); }
265
- .msg .quote { color: var(--muted-foreground); font-size: 11px; border-left: 2px solid var(--warning); padding: 1px 8px; margin-bottom: 5px; max-height: 42px; overflow: hidden; }
649
+ .msg .who { color: var(--muted-foreground); font-size: var(--text-2xs); font-weight: 600; text-transform: uppercase; letter-spacing: .07em; } .msg.note .who { color: var(--warning); }
650
+ .msg .del { cursor: pointer; border: none; background: none; color: var(--subtle); font-size: var(--text-2xs); padding: 0 2px; } .msg .del:hover { color: var(--destructive); }
651
+ .msg .quote { color: var(--muted-foreground); font-size: var(--text-2xs); border-left: 2px solid var(--warning); padding: 1px 8px; margin-bottom: 5px; max-height: 42px; overflow: hidden; }
266
652
  .msg .body { white-space: pre-wrap; word-break: break-word; color: var(--card-foreground); }
267
653
 
268
654
  /* Terminus: Separator + composer */
269
655
  .composer { border-top: 1px solid var(--border); padding: 12px 14px; }
270
656
  .actions { padding: 0 14px 14px; }
271
- .count { color: var(--subtle); font-size: 11px; }
657
+ .count { color: var(--subtle); font-family: var(--mono); font-size: var(--text-2xs); }
272
658
 
273
659
  /* Bottom decision bar — decisions read as the end of the review. Reject sits
274
660
  isolated on the left; the two "continue" actions group on the right. The
275
661
  summary in the middle says what rides along with the decision. */
276
662
  .dbar { flex: none; display: flex; align-items: center; border-top: 1px solid var(--border); background: var(--card); padding: 10px 16px; min-height: 58px; }
277
663
  .dbar-live { display: flex; flex: 1; align-items: center; gap: 12px; }
278
- .dbar-summary { flex: 1; text-align: center; color: var(--subtle); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
664
+ .dbar-summary { flex: 1; text-align: center; color: var(--subtle); font-size: var(--text-xs); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
279
665
  .dbar-group { display: flex; gap: 8px; }
280
666
 
281
667
  /* Post-submit confirmation — replaces the decision bar content once a decision
@@ -285,12 +671,12 @@
285
671
  .sent.approve { border-color: var(--success-border); background: var(--success-muted); }
286
672
  .sent.revise { border-color: var(--warning-border); background: var(--warning-muted); }
287
673
  .sent.deny { border-color: var(--destructive-border); background: var(--destructive-muted); }
288
- .sent .h { display: flex; align-items: center; gap: 8px; font: 600 13px var(--mono); }
674
+ .sent .h { display: flex; align-items: center; gap: 8px; font: 600 var(--text-sm) var(--sans); }
289
675
  .sent.approve .h { color: var(--success); }
290
676
  .sent.revise .h { color: var(--warning); }
291
677
  .sent.deny .h { color: var(--destructive); }
292
678
  .sent .h { flex: none; }
293
- .sent .sub { color: var(--muted-foreground); font-size: 12px; line-height: 1.5; }
679
+ .sent .sub { color: var(--muted-foreground); font-size: var(--text-xs); line-height: var(--leading-normal); }
294
680
  .sent .ico { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; }
295
681
  .sent .again { margin-top: 10px; }
296
682
  .dot { width: 7px; height: 7px; border-radius: 9999px; background: currentColor; animation: pulse 1.3s ease-in-out infinite; }
@@ -299,21 +685,32 @@
299
685
  </head>
300
686
  <body>
301
687
  <div class="app">
302
- <aside class="nav">
688
+ <aside class="nav" data-kit-chrome>
689
+ <div class="kit-theme">
690
+ <button type="button" data-kit-theme="light">Light</button>
691
+ <button type="button" data-kit-theme="dark">Dark</button>
692
+ <button type="button" data-kit-theme="system">Auto</button>
693
+ </div>
303
694
  <div class="tabs">
304
695
  <button type="button" class="tab active" id="tab-contents">Contents</button>
305
696
  <button type="button" class="tab" id="tab-files">Files</button>
306
697
  </div>
307
698
  <div id="pane-contents">
308
- <div class="tree-filter" id="tree-filter"><input type="search" id="tree-q" placeholder="Filter docs…" aria-label="Filter documents"></div>
309
- <nav id="toc"></nav>
699
+ <div class="tree-filter" id="tree-filter"><input type="search" id="tree-q" data-kit-filter placeholder="Filter docs…" aria-label="Filter documents"></div>
700
+ <nav id="toc">
701
+ <div class="kit-skel" style="width:70%;height:0.85rem;margin:0 6px 10px"></div>
702
+ <div class="kit-skel" style="width:55%;height:0.85rem;margin:0 6px 10px"></div>
703
+ <div class="kit-skel" style="width:80%;height:0.85rem;margin:0 6px 10px"></div>
704
+ <div class="kit-skel" style="width:45%;height:0.85rem;margin:0 6px 10px"></div>
705
+ </nav>
310
706
  </div>
311
707
  <div id="pane-files" style="display:none"><nav id="filelist"></nav></div>
312
708
  </aside>
313
709
 
314
710
  <main class="main">
315
711
  <p class="hint">Select any text to comment on it. Or chat with the agent on the right. Then decide.</p>
316
- <div class="revbar" id="revbar">
712
+ <div class="kit-error" id="api-error" hidden><span class="msg-text"></span></div>
713
+ <div class="revbar" id="revbar" data-kit-chrome>
317
714
  <span class="rb-text">Plan revised</span>
318
715
  <span class="rb-stat" id="rb-stat"></span>
319
716
  <span class="rb-actions">
@@ -322,10 +719,15 @@
322
719
  </span>
323
720
  </div>
324
721
  <div class="crumbs" id="crumbs"></div>
325
- <div class="doc" id="plan">Loading plan…</div>
722
+ <div class="doc" id="plan">
723
+ <div class="kit-skel" style="width:60%;height:1.4rem;margin-bottom:1em"></div>
724
+ <div class="kit-skel" style="width:100%;margin-bottom:0.6em"></div>
725
+ <div class="kit-skel" style="width:96%;margin-bottom:0.6em"></div>
726
+ <div class="kit-skel" style="width:88%;margin-bottom:0.6em"></div>
727
+ </div>
326
728
  <div class="pnav" id="pnav"></div>
327
729
  <div class="diffwrap" id="diffwrap"><div class="diffview" id="diffview"></div></div>
328
- <div class="popover" id="popover">
730
+ <div class="popover" id="popover" data-kit-chrome>
329
731
  <div class="q" id="pop-quote"></div>
330
732
  <div class="cats" id="pop-cats">
331
733
  <button type="button" class="chip active" data-cat="Comment">Comment</button>
@@ -341,7 +743,7 @@
341
743
  </div>
342
744
  </main>
343
745
 
344
- <aside class="chat">
746
+ <aside class="chat" data-kit-chrome>
345
747
  <p class="label">Review <span class="count" id="count"></span></p>
346
748
  <div class="thread" id="thread"><div class="empty">No notes yet. Select text to comment, or send a message.</div></div>
347
749
  <div class="composer">
@@ -355,7 +757,7 @@
355
757
 
356
758
  <!-- Decisions live in a fixed bottom bar — the rail above is purely
357
759
  conversational (notes + messages). Reject is isolated on the left. -->
358
- <footer class="dbar">
760
+ <footer class="dbar" data-kit-chrome>
359
761
  <div class="dbar-live" id="dbar-live">
360
762
  <button class="btn btn-danger" id="btn-reject" data-decision="Reject">Reject</button>
361
763
  <span class="dbar-summary" id="dbar-summary"></span>
@@ -370,6 +772,373 @@
370
772
  </div>
371
773
  </footer>
372
774
 
775
+ <script>
776
+ /* kit:js:start */
777
+ /* Shared behaviour layer — do not hand-edit; edit scripts/tokens/kit.js. */
778
+
779
+ /* Deliberately free of backticks and interpolation openers. No target is a JS
780
+ template literal since the board-shell extraction, so this is insurance
781
+ rather than a requirement — but it is the difference between this file being
782
+ safe to inline anywhere and being safe only where someone remembered to
783
+ check. Regexes and escapes are avoided for the same reason. */
784
+
785
+ (function () {
786
+ 'use strict';
787
+
788
+ var THEME_KEY = 'jabworks-theme';
789
+ var THEME_MODES = ['light', 'dark', 'system'];
790
+ var root = document.documentElement;
791
+
792
+ /* localStorage throws on an opaque origin, which is what a file:// artifact
793
+ often gets. Degrade to session-only theming; never break the page. */
794
+ function readStored(key) {
795
+ try {
796
+ return window.localStorage.getItem(key);
797
+ } catch (e) {
798
+ return null;
799
+ }
800
+ }
801
+
802
+ function writeStored(key, value) {
803
+ try {
804
+ window.localStorage.setItem(key, value);
805
+ } catch (e) {
806
+ /* no-op */
807
+ }
808
+ }
809
+
810
+ function applyTheme(mode) {
811
+ if (mode === 'light' || mode === 'dark') root.setAttribute('data-theme', mode);
812
+ else root.removeAttribute('data-theme');
813
+
814
+ writeStored(THEME_KEY, mode);
815
+
816
+ var buttons = document.querySelectorAll('[data-kit-theme]');
817
+
818
+ for (var i = 0; i < buttons.length; i++) {
819
+ buttons[i].setAttribute('aria-pressed', String(buttons[i].getAttribute('data-kit-theme') === mode));
820
+ }
821
+ }
822
+
823
+ function currentTheme() {
824
+ return root.getAttribute('data-theme') || 'system';
825
+ }
826
+
827
+ function cycleTheme() {
828
+ applyTheme(THEME_MODES[(THEME_MODES.indexOf(currentTheme()) + 1) % THEME_MODES.length]);
829
+ }
830
+
831
+ function items() {
832
+ return Array.prototype.slice.call(document.querySelectorAll('[data-kit-item]'));
833
+ }
834
+
835
+ /* document.activeElement is null in a detached or not-yet-painted document,
836
+ and Element.closest is absent on the document node itself. */
837
+ function activeWithin(selector) {
838
+ var el = document.activeElement;
839
+
840
+ return el && el.closest ? el.closest(selector) : null;
841
+ }
842
+
843
+ function moveItem(step) {
844
+ var all = items();
845
+
846
+ if (all.length === 0) return;
847
+
848
+ var index = all.indexOf(activeWithin('[data-kit-item]'));
849
+ var next = all[Math.min(Math.max(index + step, 0), all.length - 1)] || all[0];
850
+
851
+ next.setAttribute('tabindex', '-1');
852
+ next.focus();
853
+ next.scrollIntoView({ block: 'nearest' });
854
+
855
+ /* Reflect the focused item in the URL so a board or a review can be linked
856
+ to a specific row. replaceState, not a hash assignment: assigning to
857
+ location.hash pushes a history entry per keypress. */
858
+ if (next.id && window.history && window.history.replaceState) {
859
+ window.history.replaceState(null, '', '#' + next.id);
860
+ }
861
+ }
862
+
863
+ function reveal(el) {
864
+ if (el && el.focus) {
865
+ el.focus();
866
+ if (el.select) el.select();
867
+
868
+ return true;
869
+ }
870
+
871
+ return false;
872
+ }
873
+
874
+ /* A surface whose filter field ships hidden cannot hang the hook on the field
875
+ itself — a hidden element does not take focus, so the binding would be a
876
+ silent no-op. Those surfaces put the hook on the control that REVEALS the
877
+ field instead; activate it, then focus what it revealed via aria-controls.
878
+ Without this the ? overlay would advertise "Focus filter" for a key that
879
+ only moves focus to a button, and a generated overlay that misdescribes its
880
+ own bindings is worse than no overlay. */
881
+ function focusFilter() {
882
+ var hook = document.querySelector('[data-kit-filter]');
883
+
884
+ if (!hook) return false;
885
+
886
+ if (hook.tagName === 'BUTTON') {
887
+ if (hook.getAttribute('aria-expanded') === 'false' && hook.click) hook.click();
888
+
889
+ if (reveal(document.getElementById(hook.getAttribute('aria-controls') || ''))) return true;
890
+ }
891
+
892
+ return reveal(hook);
893
+ }
894
+
895
+ function flashCopied(el) {
896
+ el.classList.add('kit-copied');
897
+ window.setTimeout(function () {
898
+ el.classList.remove('kit-copied');
899
+ }, 900);
900
+ }
901
+
902
+ function copyFrom(el) {
903
+ if (!el) return;
904
+
905
+ var text = el.getAttribute('data-kit-copy') || el.textContent || '';
906
+
907
+ if (!text || !navigator.clipboard || !navigator.clipboard.writeText) return;
908
+
909
+ navigator.clipboard.writeText(text.trim()).then(
910
+ function () {
911
+ flashCopied(el);
912
+ },
913
+ function () {
914
+ /* Clipboard is permission-gated; a refusal is not an error worth
915
+ interrupting the page for. */
916
+ },
917
+ );
918
+ }
919
+
920
+ function sections() {
921
+ return Array.prototype.slice.call(document.querySelectorAll('[data-kit-section]'));
922
+ }
923
+
924
+ function jumpToSection(n) {
925
+ var target = sections()[n - 1];
926
+
927
+ if (target) target.scrollIntoView({ block: 'start' });
928
+ }
929
+
930
+ /* One registry drives both the key handler and the help overlay, so the
931
+ documented shortcuts cannot drift from the implemented ones. */
932
+ var BINDINGS = [
933
+ { keys: ['j'], label: 'Next item', when: function () { return items().length > 0; }, run: function () { moveItem(1); } },
934
+ { keys: ['k'], label: 'Previous item', when: function () { return items().length > 0; }, run: function () { moveItem(-1); } },
935
+ {
936
+ keys: ['/'],
937
+ label: 'Focus filter',
938
+ when: function () { return Boolean(document.querySelector('[data-kit-filter]')); },
939
+ run: focusFilter,
940
+ },
941
+ {
942
+ keys: ['y'],
943
+ label: 'Copy id or path',
944
+ when: function () { return Boolean(document.querySelector('[data-kit-copy]')); },
945
+ run: function () {
946
+ copyFrom(activeWithin('[data-kit-copy]'));
947
+ },
948
+ },
949
+ { keys: ['t'], label: 'Cycle theme', when: function () { return true; }, run: cycleTheme },
950
+ {
951
+ keys: ['g', '1-9'],
952
+ label: 'Jump to section',
953
+ when: function () { return sections().length > 0; },
954
+ run: null,
955
+ },
956
+ { keys: ['?'], label: 'Show shortcuts', when: function () { return true; }, run: null },
957
+ { keys: ['Esc'], label: 'Dismiss', when: function () { return true; }, run: null },
958
+ ];
959
+
960
+ var overlay = null;
961
+
962
+ function buildOverlay() {
963
+ var host = document.createElement('div');
964
+
965
+ host.className = 'kit-help';
966
+ host.hidden = true;
967
+ host.setAttribute('role', 'dialog');
968
+ host.setAttribute('aria-modal', 'true');
969
+ host.setAttribute('aria-label', 'Keyboard shortcuts');
970
+
971
+ var panel = document.createElement('div');
972
+
973
+ panel.className = 'kit-help__panel';
974
+
975
+ var title = document.createElement('p');
976
+
977
+ title.className = 'kit-help__title';
978
+ title.textContent = 'Keyboard shortcuts';
979
+ panel.appendChild(title);
980
+
981
+ for (var i = 0; i < BINDINGS.length; i++) {
982
+ if (!BINDINGS[i].when()) continue;
983
+
984
+ var row = document.createElement('div');
985
+
986
+ row.className = 'kit-help__row';
987
+
988
+ var label = document.createElement('span');
989
+
990
+ label.textContent = BINDINGS[i].label;
991
+ row.appendChild(label);
992
+
993
+ var keys = document.createElement('span');
994
+
995
+ keys.className = 'kit-help__keys';
996
+
997
+ for (var k = 0; k < BINDINGS[i].keys.length; k++) {
998
+ var kbd = document.createElement('kbd');
999
+
1000
+ kbd.className = 'kit-kbd';
1001
+ kbd.textContent = BINDINGS[i].keys[k];
1002
+ keys.appendChild(kbd);
1003
+ }
1004
+
1005
+ row.appendChild(keys);
1006
+ panel.appendChild(row);
1007
+ }
1008
+
1009
+ host.appendChild(panel);
1010
+ host.addEventListener('click', function (event) {
1011
+ if (event.target === host) toggleOverlay(false);
1012
+ });
1013
+ document.body.appendChild(host);
1014
+
1015
+ return host;
1016
+ }
1017
+
1018
+ function toggleOverlay(show) {
1019
+ if (!overlay) overlay = buildOverlay();
1020
+
1021
+ overlay.hidden = !show;
1022
+ }
1023
+
1024
+ function isTypingTarget(el) {
1025
+ if (!el) return false;
1026
+
1027
+ var tag = el.tagName;
1028
+
1029
+ return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable === true;
1030
+ }
1031
+
1032
+ var pendingJump = false;
1033
+
1034
+ function onKeydown(event) {
1035
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
1036
+
1037
+ if (event.key === 'Escape') {
1038
+ toggleOverlay(false);
1039
+ if (isTypingTarget(event.target) && event.target.blur) event.target.blur();
1040
+
1041
+ return;
1042
+ }
1043
+
1044
+ /* A filter box must be typeable: every other binding is inert while the
1045
+ caret is in a field. Escape above is the deliberate exception. */
1046
+ if (isTypingTarget(event.target)) return;
1047
+
1048
+ if (pendingJump) {
1049
+ pendingJump = false;
1050
+
1051
+ if (event.key >= '1' && event.key <= '9') {
1052
+ jumpToSection(Number(event.key));
1053
+ event.preventDefault();
1054
+
1055
+ return;
1056
+ }
1057
+ }
1058
+
1059
+ if (event.key === 'g' && sections().length > 0) {
1060
+ pendingJump = true;
1061
+ window.setTimeout(function () {
1062
+ pendingJump = false;
1063
+ }, 1200);
1064
+
1065
+ return;
1066
+ }
1067
+
1068
+ if (event.key === '?') {
1069
+ toggleOverlay(overlay ? overlay.hidden : true);
1070
+ event.preventDefault();
1071
+
1072
+ return;
1073
+ }
1074
+
1075
+ for (var i = 0; i < BINDINGS.length; i++) {
1076
+ if (BINDINGS[i].run && BINDINGS[i].keys.indexOf(event.key) !== -1 && BINDINGS[i].when()) {
1077
+ BINDINGS[i].run();
1078
+ event.preventDefault();
1079
+
1080
+ return;
1081
+ }
1082
+ }
1083
+ }
1084
+
1085
+ function init() {
1086
+ applyTheme(readStored(THEME_KEY) || 'system');
1087
+
1088
+ var buttons = document.querySelectorAll('[data-kit-theme]');
1089
+
1090
+ for (var i = 0; i < buttons.length; i++) {
1091
+ buttons[i].addEventListener('click', function (event) {
1092
+ applyTheme(event.currentTarget.getAttribute('data-kit-theme'));
1093
+ });
1094
+ }
1095
+
1096
+ /* Give the ? overlay a visible way in. It rides alongside the theme group
1097
+ because every surface already has one, and because a floating badge is
1098
+ chrome each surface would then have to design around. */
1099
+ var groups = document.querySelectorAll('.kit-theme');
1100
+
1101
+ for (var g = 0; g < groups.length; g++) {
1102
+ if (!groups[g].parentNode) continue;
1103
+
1104
+ var help = document.createElement('button');
1105
+
1106
+ help.type = 'button';
1107
+ help.className = 'kit-helpbtn';
1108
+ help.textContent = '?';
1109
+ help.title = 'Keyboard shortcuts (?)';
1110
+ help.setAttribute('aria-label', 'Keyboard shortcuts');
1111
+ help.addEventListener('click', function () {
1112
+ toggleOverlay(true);
1113
+ });
1114
+
1115
+ /* Wrap rather than insert as a sibling: a surface may host the theme group
1116
+ in a flex row (the board) or in block flow (the guide's rail), and in
1117
+ block flow a bare sibling drops onto its own line. The wrapper makes the
1118
+ pairing hold in either. */
1119
+ var controls = document.createElement('div');
1120
+
1121
+ controls.className = 'kit-controls';
1122
+ groups[g].parentNode.insertBefore(controls, groups[g]);
1123
+ controls.appendChild(groups[g]);
1124
+ controls.appendChild(help);
1125
+ }
1126
+
1127
+ document.addEventListener('click', function (event) {
1128
+ var target = event.target.closest ? event.target.closest('[data-kit-copy]') : null;
1129
+
1130
+ if (target) copyFrom(target);
1131
+ });
1132
+
1133
+ document.addEventListener('keydown', onKeydown);
1134
+ }
1135
+
1136
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1137
+ else init();
1138
+ })();
1139
+ /* kit:js:end */
1140
+ </script>
1141
+
373
1142
  <script>
374
1143
  // ---- Self-contained Markdown renderer (no external deps) ----
375
1144
  function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
@@ -395,13 +1164,26 @@
395
1164
  if(/^#{1,6}\s/.test(line)){const lvl=line.match(/^#+/)[0].length;const text=line.replace(/^#+\s/,'');out.push({html:'<h'+lvl+'>'+inline(text)+'</h'+lvl+'>',heading:{level:lvl,text:text}});i++;continue;}
396
1165
  if(/^---+\s*$/.test(line)){out.push({html:'<hr>',heading:null});i++;continue;}
397
1166
  if(/^\s*>/.test(line)){const buf=[];while(i<lines.length&&/^\s*>/.test(lines[i]))buf.push(lines[i++].replace(/^\s*>\s?/,''));out.push({html:'<blockquote>'+inline(buf.join(' '))+'</blockquote>',heading:null});continue;}
398
- if(/^\s*([-*+]|\d+\.)\s/.test(line)){const ordered=/^\s*\d+\./.test(line);const buf=[];while(i<lines.length&&/^\s*([-*+]|\d+\.)\s/.test(lines[i])){let it=lines[i].replace(/^\s*([-*+]|\d+\.)\s/,'');const cb=it.match(/^\[([ xX])\]\s+/);if(cb){it=it.slice(cb[0].length);buf.push('<li class="task">'+(/[xX]/.test(cb[1])?'☑':'☐')+' '+inline(it)+'</li>');}else{buf.push('<li>'+inline(it)+'</li>');}i++;}out.push({html:'<'+(ordered?'ol':'ul')+'>'+buf.join('')+'</'+(ordered?'ol':'ul')+'>',heading:null});continue;}
1167
+ // Lazy continuation: a soft-wrapped bullet leaves its follow-on lines with no
1168
+ // marker. Advancing only on marker lines ended the list there and rendered the
1169
+ // remainder as a left-margin paragraph — every wrapped bullet in every spec.
1170
+ // The exclusion set is deliberately the paragraph loop's below, not a new one.
1171
+ if(/^\s*([-*+]|\d+\.)\s/.test(line)){const ordered=/^\s*\d+\./.test(line);const buf=[];while(i<lines.length&&/^\s*([-*+]|\d+\.)\s/.test(lines[i])){let it=lines[i].replace(/^\s*([-*+]|\d+\.)\s/,'');i++;while(i<lines.length&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|\s*>|\s*([-*+]|\d+\.)\s|---+\s*$)/.test(lines[i])&&!(lines[i].indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1]))){it+=' '+lines[i].trim();i++;}const cb=it.match(/^\[([ xX])\]\s+/);if(cb){it=it.slice(cb[0].length);buf.push('<li class="task">'+(/[xX]/.test(cb[1])?'☑':'☐')+' '+inline(it)+'</li>');}else{buf.push('<li>'+inline(it)+'</li>');}}out.push({html:'<'+(ordered?'ol':'ul')+'>'+buf.join('')+'</'+(ordered?'ol':'ul')+'>',heading:null});continue;}
399
1172
  if(line.indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1])){const header=tcells(line);const aligns=tcells(lines[i+1]).map(function(c){const l=/^:/.test(c),r=/:$/.test(c);return l&&r?'center':r?'right':l?'left':'';});i+=2;const rows=[];while(i<lines.length&&lines[i].indexOf('|')>=0&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|---+\s*$)/.test(lines[i])){rows.push(tcells(lines[i]));i++;}let h='<table><thead><tr>';header.forEach(function(c,ci){h+='<th'+(aligns[ci]?' style="text-align:'+aligns[ci]+'"':'')+'>'+inline(c)+'</th>';});h+='</tr></thead><tbody>';rows.forEach(function(cs){h+='<tr>';header.forEach(function(_,ci){h+='<td'+(aligns[ci]?' style="text-align:'+aligns[ci]+'"':'')+'>'+inline(cs[ci]!==undefined?cs[ci]:'')+'</td>';});h+='</tr>';});h+='</tbody></table>';out.push({html:h,heading:null});continue;}
400
1173
  const buf=[];while(i<lines.length&&!/^\s*$/.test(lines[i])&&!/^(#{1,6}\s|```|\s*>|\s*([-*+]|\d+\.)\s|---+\s*$)/.test(lines[i])&&!(lines[i].indexOf('|')>=0&&i+1<lines.length&&isDelim(lines[i+1])))buf.push(lines[i++]);
401
1174
  out.push({html:'<p>'+inline(buf.join(' '))+'</p>',heading:null});}
402
1175
  return out;
403
1176
  }
404
1177
 
1178
+ // ---- API error banner (kit-error) ----
1179
+ function showApiError(msg){
1180
+ const el=document.getElementById('api-error'); if(!el) return;
1181
+ el.querySelector('.msg-text').textContent=msg; el.hidden=false;
1182
+ }
1183
+ function hideApiError(){
1184
+ const el=document.getElementById('api-error'); if(el) el.hidden=true;
1185
+ }
1186
+
405
1187
  // ---- State ----
406
1188
  const annotations = []; let nextId = 1;
407
1189
  const planEl=document.getElementById('plan');
@@ -482,6 +1264,7 @@
482
1264
  node.docs.forEach(function(doc){
483
1265
  const m=matches(doc); if(!m.show) return;
484
1266
  const d=document.createElement('a'); d.href='#'; d.className='doc'+(doc===activeDoc?' active':''); d.dataset.doc=doc; d.title=doc;
1267
+ d.setAttribute('data-kit-item','');
485
1268
  indent(d,depth);
486
1269
  const t=document.createElement('span'); t.className='t'; t.textContent=doc.slice(pathStr?pathStr.length+1:0); d.appendChild(t);
487
1270
  const bd=document.createElement('span'); bd.className='badge badge-warning'; bd.style.display='none'; bd.dataset.fordoc=doc; d.appendChild(bd);
@@ -718,7 +1501,7 @@
718
1501
  'Request Revisions':{cls:'revise', ico:'',wait:true, title:'Revisions requested',sub:'Your notes are on their way. The agent will revise the plan; the updated version reloads here when it’s ready.'},
719
1502
  'Reject': {cls:'deny', ico:'✕',wait:false,title:'Rejected',sub:'Sent to the agent — it will not implement this. It steps back to reconsider whether the feature should be built at all.'},
720
1503
  };
721
- let submitting=false;
1504
+ let submitting=false, decisionSent=false;
722
1505
  function clearAllNotes(){
723
1506
  annotations.slice().forEach(function(a){ if(a.kind==='note') unhighlight(a.id); });
724
1507
  annotations.length=0; nextId=1; paint();
@@ -740,10 +1523,18 @@
740
1523
  async function submit(decision){
741
1524
  if(submitting) return; submitting=true;
742
1525
  document.querySelectorAll('.dbar button').forEach(function(b){b.disabled=true;});
743
- try{ await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:annotations})}); }
744
- catch(e){}
745
- clearAllNotes(); // notes clear the moment the decision is sent
746
- showSent(decision);
1526
+ try{
1527
+ const r=await fetch('/api/feedback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({decision:decision,thread:annotations})});
1528
+ if(!r.ok) throw new Error('status '+r.status);
1529
+ hideApiError();
1530
+ decisionSent=true; // a terminal decision makes the server exit —
1531
+ // the SSE drop that follows is expected, not a fault
1532
+ clearAllNotes(); // notes clear the moment the decision is sent
1533
+ showSent(decision);
1534
+ }catch(e){
1535
+ showApiError('Could not send your decision — the review server may be unreachable. Nothing was sent; try again.');
1536
+ document.querySelectorAll('.dbar button').forEach(function(b){b.disabled=false;});
1537
+ }
747
1538
  submitting=false;
748
1539
  }
749
1540
  document.getElementById('send').onclick=sendChat;
@@ -850,10 +1641,18 @@
850
1641
  tabC.onclick=function(){setTab(false);}; tabF.onclick=function(){setTab(true);};
851
1642
 
852
1643
  // ---- Load + live reload ----
853
- function fetchDoc(doc){ return fetch(DIRMODE?'/api/plan?doc='+encodeURIComponent(doc):'/api/plan').then(function(r){return r.text();}); }
1644
+ // null means "fetch failed" (error banner already shown) and is kept
1645
+ // distinct from '' so a transient failure can never masquerade as a
1646
+ // real empty document and trip the revised-doc diff below.
1647
+ function fetchDoc(doc){
1648
+ return fetch(DIRMODE?'/api/plan?doc='+encodeURIComponent(doc):'/api/plan')
1649
+ .then(function(r){ if(!r.ok) throw new Error('status '+r.status); hideApiError(); return r.text(); })
1650
+ .catch(function(e){ showApiError('Could not load "'+doc+'" — the review server may be unreachable.'); return null; });
1651
+ }
854
1652
  async function reloadDoc(doc){
855
1653
  if(!docState[doc]) return;
856
1654
  const md=await fetchDoc(doc);
1655
+ if(md===null) return; // load failed; state untouched, banner already shown
857
1656
  const st=docState[doc];
858
1657
  if(md===st.md) return; // spurious watch event, no content change
859
1658
  const prev=st.md, revised=prev!==null;
@@ -869,8 +1668,22 @@
869
1668
  }
870
1669
  async function boot(){
871
1670
  let meta={dir:false,docs:['plan']};
872
- try{ meta=await (await fetch('/api/docs')).json(); }catch(e){}
1671
+ try{
1672
+ const r=await fetch('/api/docs');
1673
+ if(!r.ok) throw new Error('status '+r.status);
1674
+ meta=await r.json();
1675
+ hideApiError();
1676
+ }catch(e){ showApiError('Could not reach the review server. Retry once it is back up.'); }
873
1677
  DIRMODE=!!meta.dir; docs=meta.docs||[];
1678
+ // A directory with no markdown at all — nothing to render, nothing to
1679
+ // load per-doc. Show the empty state and stop before the doc loop.
1680
+ if(DIRMODE&&!docs.length){
1681
+ tocEl.innerHTML='';
1682
+ planEl.innerHTML='<div class="kit-empty"><p class="kit-empty__title">No documents to review</p>'
1683
+ +'<p class="kit-empty__body">This directory has no markdown files.</p></div>';
1684
+ paint();
1685
+ return;
1686
+ }
874
1687
  // Accept-or-fix reviews hide the plan-only "Reject / don't build it"
875
1688
  // verdict: directory (spec) review always — a spec documents work already
876
1689
  // decided — and single-file review when the server was started with
@@ -889,22 +1702,31 @@
889
1702
  };
890
1703
  qEl.onkeydown=function(e){ if(e.key==='Escape'){ qEl.value=''; qEl.oninput(); qEl.blur(); } };
891
1704
  }
892
- planEl.innerHTML='';
1705
+ // Panes are built off-DOM and only swapped in once every doc has
1706
+ // resolved, so the .kit-skel placeholders already in #plan stay
1707
+ // visible for the whole fetch instead of being wiped to blank first.
1708
+ const frag=document.createDocumentFragment();
893
1709
  docs.forEach(function(d,i){
894
1710
  const pane=document.createElement('div'); pane.className='docpane'; pane.style.display='none';
895
- planEl.appendChild(pane);
1711
+ frag.appendChild(pane);
896
1712
  docState[d]={md:null,pane:pane,scroll:0,key:'d'+i,headings:[]};
897
1713
  });
898
- for(let i=0;i<docs.length;i++){ const d=docs[i]; renderDocPane(d,await fetchDoc(d)); }
1714
+ for(let i=0;i<docs.length;i++){ const d=docs[i]; const md=await fetchDoc(d); if(md!==null) renderDocPane(d,md); }
1715
+ planEl.innerHTML=''; planEl.appendChild(frag);
899
1716
  if(!DIRMODE||!docs.length) buildToc(); // DIRMODE: switchDoc below builds the tree once, seeded
900
1717
  paint();
901
1718
  if(docs.length) switchDoc(docs[0]);
902
1719
  buildFilesIndex();
903
1720
  }
904
- try{const es=new EventSource('/api/events');es.onmessage=function(ev){
905
- const m=String((ev&&ev.data)||''); const doc=m.indexOf('change:')===0?m.slice(7):docs[0];
906
- reloadDoc(doc);
907
- };}catch(e){}
1721
+ try{
1722
+ const es=new EventSource('/api/events');
1723
+ es.onmessage=function(ev){
1724
+ hideApiError();
1725
+ const m=String((ev&&ev.data)||''); const doc=m.indexOf('change:')===0?m.slice(7):docs[0];
1726
+ reloadDoc(doc);
1727
+ };
1728
+ es.onerror=function(){ if(!decisionSent) showApiError('Live connection to the review server was lost — revised docs will not auto-reload. Your notes are safe.'); };
1729
+ }catch(e){ showApiError('Live reload is unavailable in this browser.'); }
908
1730
  boot();
909
1731
  </script>
910
1732
  </body>