@try-works/dsh-recursive-mode 0.1.11 → 0.1.13

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/lib/client.js CHANGED
@@ -206,6 +206,34 @@ window.__ModuleLoader__.load({
206
206
  subagents
207
207
  };
208
208
  }
209
+ /** Human pill label per kind (neutral renders an empty dash). */
210
+ const PILL_LABELS = {
211
+ locked: "locked",
212
+ "in-progress": "in progress",
213
+ paused: "paused",
214
+ blocked: "blocked",
215
+ tampered: "tampered",
216
+ advisory: "advisory",
217
+ neutral: "—"
218
+ };
219
+ /**
220
+ * Solid-pill kind for a whole run card. Priority: tamper > fully-locked >
221
+ * run-state blocked > run-state paused > still in-progress.
222
+ */
223
+ function cardPill(card) {
224
+ const facts = cardFacts(card);
225
+ if (facts.tampered) return "tampered";
226
+ if (facts.lockValidity === "locked") return "locked";
227
+ if (facts.state === "blocked") return "blocked";
228
+ if (facts.state === "paused") return "paused";
229
+ return "in-progress";
230
+ }
231
+ /** Solid-pill kind for a phase row status string (LOCKED / DRAFT / '—'). */
232
+ function phaseStatusPill(status) {
233
+ if (status === "LOCKED") return "locked";
234
+ if (status === "" || status === "—") return "neutral";
235
+ return "in-progress";
236
+ }
209
237
  /**
210
238
  * Expand a card's phase chain into full inspector rows: the always-shown
211
239
  * '03.5' slot (§11.10 mandatory deviation) plus 01.5 when present, and
@@ -242,104 +270,60 @@ window.__ModuleLoader__.load({
242
270
  return rows;
243
271
  }
244
272
  //#endregion
273
+ //#region src/client/theme.ts
274
+ /**
275
+ * Board/Inspector theme state (run 16 / Paper). Default LIGHT, dark via a
276
+ * header toggle that flips data-theme on the root .rec-board / .rec-inspector.
277
+ * Persisted to localStorage under 'dsh-recursive-theme' (first visit = light).
278
+ */
279
+ const BOARD_THEME_STORAGE_KEY = "dsh-recursive-theme";
280
+ function readInitialTheme() {
281
+ if (typeof localStorage === "undefined") return "light";
282
+ try {
283
+ return localStorage.getItem("dsh-recursive-theme") === "dark" ? "dark" : "light";
284
+ } catch {
285
+ return "light";
286
+ }
287
+ }
288
+ /**
289
+ * Hoisted at the TOP of Board and Inspector (never after an early return — run
290
+ * 0.1.10 invariant). Defaults light, reads a persisted dark on init, and writes
291
+ * the chosen theme back to localStorage.
292
+ */
293
+ function useBoardTheme() {
294
+ const [theme, setTheme] = (0, react.useState)(readInitialTheme);
295
+ const toggle = (0, react.useCallback)(() => setTheme((t) => t === "light" ? "dark" : "light"), []);
296
+ (0, react.useEffect)(() => {
297
+ try {
298
+ localStorage.setItem(BOARD_THEME_STORAGE_KEY, theme);
299
+ } catch {}
300
+ }, [theme]);
301
+ return {
302
+ theme,
303
+ toggle
304
+ };
305
+ }
306
+ /** The shared header toggle button (moon in light, sun in dark). */
307
+ function ThemeToggle({ theme, toggle }) {
308
+ return (0, react.createElement)("button", {
309
+ type: "button",
310
+ className: "rec-theme-toggle",
311
+ onClick: toggle,
312
+ title: theme === "light" ? "Switch to dark theme" : "Switch to light theme",
313
+ "aria-label": "Toggle theme"
314
+ }, theme === "light" ? "☾" : "☀");
315
+ }
316
+ //#endregion
245
317
  //#region src/client/board.tsx
246
318
  /**
247
- * Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
248
- * host-route projection. Pure read-only renderer no filesystem, no run-file
249
- * scraping. Cards are grouped by worktree root (the projection's outer key) and
250
- * mapped to their current-phase lane via derive.columnForRun. Renders NOTHING
251
- * when the snapshot is null (no recursive root / not loaded) or the current
252
- * session is not on the recursive preset (the gate lives in slots.ts — here an
253
- * empty projection with a null root is a no-op, not an "empty board").
254
- *
255
- * Run 08 R3: a card click opens the drill-down inspector via onOpenInspector
256
- * (the overlay swaps board <-> inspector in slots.ts). Read-only still: the
257
- * callback only selects a run for display; no mutation.
319
+ * Board overlay (SP2 R1 + run 08 R3 + run 15/16): kanban over the live
320
+ * host-route projection, class-based grid board with the Paper theme.
321
+ * Pure read-only renderer no filesystem, no run-file scraping.
258
322
  *
259
- * Run 12 (LIVE BUG): shell.overlay is a click-through, unstyled frame layer
260
- * without inline styles the mounted board was INVISIBLE (no position/size/z)
261
- * and CLICK-THROUGH (no pointer-events opt-in). Everything is styled inline
262
- * (no CSS pipeline in the CJS client bundle): full-cover fixed overlay, opaque
263
- * backdrop, pointerEvents auto, and a visible close button.
323
+ * Run 16 (Paper): data-theme (default light), header theme toggle, workspace
324
+ * path + count chip, solid status pills. useBoardTheme is hoisted ABOVE the
325
+ * early returns (0.1.10 hook invariant).
264
326
  */
265
- /** Full-cover overlay style — the shell.overlay seat is click-through + unstyled by default (run 12). */
266
- const overlayStyle = {
267
- position: "fixed",
268
- inset: 0,
269
- background: "rgba(15, 17, 21, 0.92)",
270
- color: "#e6e6e6",
271
- zIndex: 9999,
272
- overflowY: "auto",
273
- padding: "24px 32px",
274
- pointerEvents: "auto",
275
- fontFamily: "system-ui, sans-serif"
276
- };
277
- const laneStyle = { marginBottom: 24 };
278
- const laneTitleStyle = {
279
- fontSize: 14,
280
- textTransform: "uppercase",
281
- letterSpacing: "0.08em",
282
- marginBottom: 8,
283
- color: "#aaa"
284
- };
285
- const cardStyle = {
286
- display: "inline-block",
287
- width: 280,
288
- margin: "0 12px 12px 0",
289
- padding: 12,
290
- background: "#1f2329",
291
- borderRadius: 8,
292
- border: "1px solid #333",
293
- cursor: "pointer",
294
- verticalAlign: "top"
295
- };
296
- const cardHeadStyle = {
297
- display: "flex",
298
- justifyContent: "space-between",
299
- marginBottom: 8
300
- };
301
- const badgeStyle$1 = {
302
- fontSize: 11,
303
- padding: "2px 6px",
304
- borderRadius: 4,
305
- background: "#333"
306
- };
307
- const badgeTamperStyle = { background: "#7a1f1f" };
308
- const progressStyle = {
309
- height: 6,
310
- background: "#333",
311
- borderRadius: 3,
312
- overflow: "hidden"
313
- };
314
- const progressFillStyle = {
315
- background: "#4c9aff",
316
- height: "100%"
317
- };
318
- const metaStyle = {
319
- marginTop: 8,
320
- fontSize: 12,
321
- color: "#aaa",
322
- display: "flex",
323
- justifyContent: "space-between"
324
- };
325
- const emptyStyle = {
326
- padding: 40,
327
- textAlign: "center",
328
- color: "#888"
329
- };
330
- const closeStyle$1 = {
331
- position: "absolute",
332
- top: 12,
333
- right: 16,
334
- background: "#1f2329",
335
- color: "#e6e6e6",
336
- border: "1px solid #444",
337
- borderRadius: 6,
338
- padding: "4px 12px",
339
- cursor: "pointer",
340
- fontSize: 16,
341
- zIndex: 1
342
- };
343
327
  /** Flatten the worktree-grouped projection into a stable run list. */
344
328
  function listRuns(projection) {
345
329
  if (projection === void 0) return [];
@@ -347,220 +331,116 @@ window.__ModuleLoader__.load({
347
331
  for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
348
332
  return runs;
349
333
  }
334
+ function closeButton$1(onClose) {
335
+ if (onClose === void 0) return null;
336
+ return (0, react.createElement)("button", {
337
+ type: "button",
338
+ className: "rec-close",
339
+ onClick: onClose,
340
+ title: "Close",
341
+ "aria-label": "Close"
342
+ }, "×");
343
+ }
344
+ function solidPill$1(kind) {
345
+ return (0, react.createElement)("span", {
346
+ className: "rec-pill",
347
+ "data-pill": kind
348
+ }, PILL_LABELS[kind]);
349
+ }
350
350
  function Board({ snapshot, onOpenInspector, onClose }) {
351
+ const { theme, toggle } = useBoardTheme();
351
352
  if (snapshot === null || snapshot.root === null) return null;
352
353
  const runs = listRuns(snapshot.projection);
353
- const close = onClose !== void 0 ? (0, react.createElement)("button", {
354
- className: "rec-close",
355
- style: closeStyle$1,
356
- onClick: onClose,
357
- title: "Close"
358
- }, "×") : null;
359
- if (runs.length === 0) return (0, react.createElement)("div", {
360
- className: "rec-board",
361
- "data-empty": true,
362
- style: overlayStyle
363
- }, close, (0, react.createElement)("p", {
364
- className: "rec-board-empty",
365
- style: emptyStyle
366
- }, "No recursive runs in this workspace yet."));
354
+ const workspacePath = snapshot.root;
367
355
  const open = (run) => () => onOpenInspector?.({
368
356
  worktreeRoot: run.worktreeRoot,
369
357
  runId: run.runId
370
358
  });
359
+ if (runs.length === 0) return (0, react.createElement)("div", {
360
+ className: "rec-board",
361
+ "data-theme": theme,
362
+ "data-empty": true
363
+ }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, workspacePath), (0, react.createElement)("span", { className: "rec-board-count" }, "0 runs"), (0, react.createElement)(ThemeToggle, {
364
+ theme,
365
+ toggle
366
+ }), closeButton$1(onClose)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
371
367
  return (0, react.createElement)("div", {
372
368
  className: "rec-board",
373
- style: overlayStyle
374
- }, close, KANBAN_LANES.map((lane) => {
369
+ "data-theme": theme
370
+ }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, workspacePath), (0, react.createElement)("span", { className: "rec-board-count" }, runs.length + " runs"), (0, react.createElement)(ThemeToggle, {
371
+ theme,
372
+ toggle
373
+ }), closeButton$1(onClose)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
375
374
  const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
376
375
  return (0, react.createElement)("section", {
377
376
  key: lane.id,
378
- className: "rec-lane",
379
- style: laneStyle
380
- }, (0, react.createElement)("h2", {
381
- className: "rec-lane-title",
382
- style: laneTitleStyle
383
- }, lane.label), laneRuns.map((run) => {
377
+ className: "rec-column"
378
+ }, (0, react.createElement)("header", { className: "rec-column-header" }, (0, react.createElement)("span", {
379
+ className: "rec-status-dot",
380
+ "data-status": laneRuns.length > 0 ? laneRuns[laneRuns.length - 1].state : "new",
381
+ "aria-hidden": true
382
+ }), (0, react.createElement)("h3", { className: "rec-column-title" }, lane.label), (0, react.createElement)("span", { className: "rec-column-count" }, String(laneRuns.length))), (0, react.createElement)("div", { className: "rec-cards" }, laneRuns.map((run) => {
384
383
  const facts = cardFacts(run);
384
+ const pill = cardPill(run);
385
385
  return (0, react.createElement)("article", {
386
386
  key: run.worktreeRoot + "\0" + run.runId,
387
387
  className: "rec-card",
388
- style: cardStyle,
388
+ "data-state": run.state,
389
389
  onClick: open(run)
390
- }, (0, react.createElement)("div", {
391
- className: "rec-card-head",
392
- style: cardHeadStyle
393
- }, (0, react.createElement)("strong", {}, run.runId), facts.tampered && (0, react.createElement)("span", {
394
- className: "rec-badge rec-badge-tamper",
395
- style: {
396
- ...badgeStyle$1,
397
- ...badgeTamperStyle
398
- },
399
- title: "tampered"
400
- }, "!")), (0, react.createElement)("div", {
401
- className: "rec-card-progress",
402
- style: progressStyle
403
- }, (0, react.createElement)("div", {
390
+ }, (0, react.createElement)("div", { className: "rec-card-title" }, run.runId), (0, react.createElement)("div", { className: "rec-card-progress" }, (0, react.createElement)("div", {
404
391
  className: "rec-card-progress-fill",
405
- style: {
406
- ...progressFillStyle,
407
- width: Math.round(facts.progress * 100) + "%"
408
- }
409
- })), (0, react.createElement)("div", {
410
- className: "rec-card-meta",
411
- style: metaStyle
412
- }, (0, react.createElement)("span", {}, facts.currentPhase ?? "no phases"), (0, react.createElement)("span", {
413
- className: "rec-badge",
414
- style: badgeStyle$1
415
- }, facts.state)));
416
- }));
417
- }));
392
+ style: { width: Math.round(facts.progress * 100) + "%" }
393
+ })), (0, react.createElement)("div", { className: "rec-card-meta" }, (0, react.createElement)("span", { className: "rec-card-phase" }, facts.currentPhase ?? "no phases"), solidPill$1(pill)));
394
+ }), laneRuns.length === 0 && (0, react.createElement)("div", { className: "rec-column-empty" }, "")));
395
+ })));
418
396
  }
419
397
  //#endregion
420
398
  //#region src/client/inspector.tsx
421
399
  /**
422
- * Drill-down inspector (SP2 R1 + run 12): the details-seat swap rendering one
423
- * run's phase chain (with the always-shown 3.5 row + full 6/7/8 rows), lock
424
- * badges, and tamper/gate facts all from the live host-route snapshot, never
425
- * disk.
426
- *
427
- * Run 12 (LIVE BUG): same as the board — shell.overlay is click-through and
428
- * unstyled, so the inspector mounts INVISIBLE + CLICK-THROUGH without inline
429
- * styles. Full-cover fixed overlay + pointerEvents auto + a back button and a
430
- * close button.
400
+ * Drill-down inspector (SP2 R1 + run 15/16): centered modal-style detail panel
401
+ * (Paper .detail shape) with back + close, data-theme (default light), theme
402
+ * toggle, and solid status pills. All from the live host-route snapshot.
403
+ * useBoardTheme is hoisted ABOVE the not-found early return.
431
404
  */
432
- const headStyle = {
433
- display: "flex",
434
- alignItems: "center",
435
- gap: 12,
436
- marginBottom: 16,
437
- borderBottom: "1px solid #333",
438
- paddingBottom: 12
439
- };
440
- const backStyle = {
441
- background: "#1f2329",
442
- color: "#e6e6e6",
443
- border: "1px solid #444",
444
- borderRadius: 6,
445
- padding: "6px 12px",
446
- cursor: "pointer"
447
- };
448
- const closeStyle = {
449
- background: "#1f2329",
450
- color: "#e6e6e6",
451
- border: "1px solid #444",
452
- borderRadius: 6,
453
- padding: "6px 12px",
454
- cursor: "pointer",
455
- marginLeft: "auto"
456
- };
457
- const sectionStyle = { marginBottom: 16 };
458
- const rowStyle = {
459
- display: "flex",
460
- gap: 12,
461
- alignItems: "center",
462
- padding: "6px 0",
463
- borderBottom: "1px solid #222",
464
- fontSize: 13
465
- };
466
- const phaseIdStyle = {
467
- minWidth: 220,
468
- fontFamily: "monospace",
469
- color: "#4c9aff"
470
- };
471
- const phaseNameStyle = {
472
- flex: 1,
473
- color: "#ccc"
474
- };
475
- const badgeStyle = {
476
- fontSize: 11,
477
- padding: "2px 6px",
478
- borderRadius: 4,
479
- background: "#333"
480
- };
481
- const lockhashStyle = {
482
- fontFamily: "monospace",
483
- fontSize: 11,
484
- color: "#888"
485
- };
486
- function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
487
- const close = onClose !== void 0 ? (0, react.createElement)("button", {
405
+ function closeButton(onClose) {
406
+ if (onClose === void 0) return null;
407
+ return (0, react.createElement)("button", {
408
+ type: "button",
488
409
  className: "rec-close",
489
- style: closeStyle,
490
410
  onClick: onClose,
491
- title: "Close"
492
- }, "×") : null;
493
- const card = snapshot?.projection?.[worktreeRoot]?.[runId];
494
- if (card === void 0) return (0, react.createElement)("div", {
411
+ title: "Close",
412
+ "aria-label": "Close"
413
+ }, "×");
414
+ }
415
+ function solidPill(kind) {
416
+ return (0, react.createElement)("span", {
417
+ className: "rec-pill",
418
+ "data-pill": kind
419
+ }, PILL_LABELS[kind]);
420
+ }
421
+ function detailShell(runId, onBackToToolDetails, onClose, headerExtra, body, theme, toggle) {
422
+ return (0, react.createElement)("div", {
495
423
  className: "rec-inspector",
496
- style: overlayStyle
497
- }, (0, react.createElement)("div", {
498
- className: "rec-inspector-head",
499
- style: headStyle
500
- }, (0, react.createElement)("button", {
501
- onClick: onBackToToolDetails,
424
+ "data-theme": theme
425
+ }, (0, react.createElement)("div", { className: "rec-detail" }, (0, react.createElement)("div", { className: "rec-detail-header" }, (0, react.createElement)("button", {
426
+ type: "button",
502
427
  className: "rec-back",
503
- style: backStyle
504
- }, "← back to tool details"), close), (0, react.createElement)("p", {}, "Run not found: " + runId));
428
+ onClick: onBackToToolDetails
429
+ }, "← Back"), (0, react.createElement)("h2", { className: "rec-detail-title" }, runId), headerExtra, (0, react.createElement)(ThemeToggle, {
430
+ theme,
431
+ toggle
432
+ }), closeButton(onClose)), (0, react.createElement)("div", { className: "rec-detail-body" }, body)));
433
+ }
434
+ function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
435
+ const { theme, toggle } = useBoardTheme();
436
+ const card = snapshot?.projection?.[worktreeRoot]?.[runId];
437
+ if (card === void 0) return detailShell(runId, onBackToToolDetails, onClose, null, (0, react.createElement)("p", { className: "rec-detail-text" }, "Run not found: " + runId), theme, toggle);
505
438
  const facts = cardFacts(card);
506
439
  const rows = expandPhaseRows(card);
507
- return (0, react.createElement)("div", {
508
- className: "rec-inspector",
509
- style: overlayStyle
510
- }, (0, react.createElement)("div", {
511
- className: "rec-inspector-head",
512
- style: headStyle
513
- }, (0, react.createElement)("button", {
514
- onClick: onBackToToolDetails,
515
- className: "rec-back",
516
- style: backStyle
517
- }, "← back to tool details"), (0, react.createElement)("h2", { style: {
518
- margin: 0,
519
- fontSize: 18
520
- } }, runId), (0, react.createElement)("span", {
521
- className: "rec-badge",
522
- style: badgeStyle
523
- }, facts.state), facts.tampered && (0, react.createElement)("span", {
524
- className: "rec-badge rec-badge-tamper",
525
- style: {
526
- ...badgeStyle,
527
- background: "#7a1f1f"
528
- }
529
- }, "tampered"), close), (0, react.createElement)("section", {
530
- className: "rec-inspector-section",
531
- style: sectionStyle
532
- }, (0, react.createElement)("h3", { style: {
533
- fontSize: 13,
534
- textTransform: "uppercase",
535
- letterSpacing: "0.08em",
536
- color: "#aaa",
537
- marginBottom: 8
538
- } }, "Phases"), rows.map((row) => (0, react.createElement)("div", {
440
+ return detailShell(runId, onBackToToolDetails, onClose, solidPill(cardPill(card)), (0, react.createElement)("section", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
539
441
  key: row.phase,
540
- className: "rec-phase-row",
541
- style: rowStyle
542
- }, (0, react.createElement)("span", {
543
- className: "rec-phase-id",
544
- style: phaseIdStyle
545
- }, row.phase), (0, react.createElement)("span", {
546
- className: "rec-phase-name",
547
- style: phaseNameStyle
548
- }, row.fileName ?? "—"), (0, react.createElement)("span", {
549
- className: "rec-badge",
550
- style: badgeStyle
551
- }, row.status), row.lockHash !== void 0 && (0, react.createElement)("code", {
552
- className: "rec-lockhash",
553
- style: lockhashStyle
554
- }, "#" + row.lockHash.slice(0, 8))))), facts.gateBlocked && (0, react.createElement)("section", {
555
- className: "rec-inspector-section",
556
- style: sectionStyle
557
- }, (0, react.createElement)("h3", { style: {
558
- fontSize: 13,
559
- textTransform: "uppercase",
560
- letterSpacing: "0.08em",
561
- color: "#aaa",
562
- marginBottom: 8
563
- } }, "Gate"), (0, react.createElement)("p", {}, "Blocked (" + (facts.gateKind ?? "") + ")")));
442
+ className: "rec-phase-row"
443
+ }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), solidPill(phaseStatusPill(row.status)), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8)))), facts.gateBlocked && (0, react.createElement)("div", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", { className: "rec-detail-text" }, "Blocked (" + (facts.gateKind ?? "") + ")"))), theme, toggle);
564
444
  }
565
445
  //#endregion
566
446
  //#region src/client/strip.tsx
@@ -766,6 +646,571 @@ window.__ModuleLoader__.load({
766
646
  return (0, react.useSyncExternalStore)(store.subscribe, store.get, store.get);
767
647
  }
768
648
  //#endregion
649
+ //#region src/client/styles.ts
650
+ /**
651
+ * One-shot board/inspector stylesheet injection (run 15 + run 16 / Paper).
652
+ * The client bundle is a plain CJS closure with NO CSS pipeline (tsdown), so we
653
+ * inject a single <style data-dsh-recursive> element into document.head.
654
+ *
655
+ * Run 16 (Paper): the design is ported 1:1 — DEFAULT LIGHT theme, dark via a
656
+ * data-theme attribute on the .rec-board / .rec-inspector root. Raw --rm3-*
657
+ * tokens live on the scoped roots; semantic --board-* aliases remap under
658
+ * [data-theme='dark']. EVERY component rule consumes ONLY --board-* aliases
659
+ * (never --rm3-* directly). Idempotent injection; disposer removes the element.
660
+ */
661
+ const BOARD_CSS = `
662
+ /* ===== dsh-recursive Paper theme (run 16) ===== */
663
+
664
+ /* Raw tokens: light (default) + dark + shared, scoped to the board/inspector. */
665
+ .rec-board, .rec-inspector {
666
+ /* light */
667
+ --rm3-light-background: #FFFFFF;
668
+ --rm3-light-foreground: #111111;
669
+ --rm3-light-card: #FFFFFF;
670
+ --rm3-light-muted: #FAFAFA;
671
+ --rm3-light-muted-foreground: #666666;
672
+ --rm3-light-accent: #F5F5F5;
673
+ --rm3-light-border: #EAEAEA;
674
+ --rm3-light-success: #27A644;
675
+ --rm3-light-warning: #B67A11;
676
+ --rm3-light-error: #D84F6A;
677
+ --rm3-light-advisory: #9664E8;
678
+ --rm3-light-info: #3F87F5;
679
+ --rm3-light-destructive: #B4261A;
680
+ /* dark */
681
+ --rm3-background: #0A0A0A;
682
+ --rm3-foreground: #EDEDED;
683
+ --rm3-card: #0F0F0F;
684
+ --rm3-muted: #141414;
685
+ --rm3-muted-foreground: #9A9A9A;
686
+ --rm3-accent: #1A1A1A;
687
+ --rm3-border: #1F1F1F;
688
+ --rm3-success: #27A644;
689
+ --rm3-warning: #D9A441;
690
+ --rm3-error: #E06C89;
691
+ --rm3-advisory: #B479FF;
692
+ --rm3-info: #6EA8FF;
693
+ /* shared */
694
+ --rm3-font-sans: 'Geist', ui-sans-serif, system-ui, sans-serif;
695
+ --rm3-font-mono: 'Geist Mono', ui-monospace, Menlo, monospace;
696
+ --rm3-text-xs: 12px;
697
+ --rm3-text-sm: 14px;
698
+ --rm3-text-md: 16px;
699
+ --rm3-text-lg: 20px;
700
+ --rm3-text-xl: 28px;
701
+ --rm3-font-weight-regular: 400;
702
+ --rm3-font-weight-medium: 500;
703
+ --rm3-font-weight-semibold: 600;
704
+ --rm3-radius-sm: 5px;
705
+ --rm3-radius-md: 6px;
706
+ --rm3-radius-lg: 8px;
707
+ --rm3-radius-xl: 11px;
708
+ --rm3-space-8: 8px;
709
+ --rm3-space-12: 12px;
710
+ --rm3-space-16: 16px;
711
+ --rm3-space-24: 24px;
712
+ --rm3-space-32: 32px;
713
+ --rm3-tracking-tight: -0.02em;
714
+ --rm3-tracking-mono: 0.02em;
715
+
716
+ /* Semantic aliases (LIGHT default) */
717
+ --board-bg: var(--rm3-light-background);
718
+ --board-fg: var(--rm3-light-foreground);
719
+ --board-card: var(--rm3-light-card);
720
+ --board-muted: var(--rm3-light-muted);
721
+ --board-muted-fg: var(--rm3-light-muted-foreground);
722
+ --board-accent: var(--rm3-light-accent);
723
+ --board-border: var(--rm3-light-border);
724
+ --board-success: var(--rm3-light-success);
725
+ --board-warning: var(--rm3-light-warning);
726
+ --board-error: var(--rm3-light-error);
727
+ --board-advisory: var(--rm3-light-advisory);
728
+ --board-info: var(--rm3-light-info);
729
+ --board-destructive: var(--rm3-light-destructive);
730
+ --board-font-sans: var(--rm3-font-sans);
731
+ --board-font-mono: var(--rm3-font-mono);
732
+ --board-text-xs: var(--rm3-text-xs);
733
+ --board-text-sm: var(--rm3-text-sm);
734
+ --board-text-md: var(--rm3-text-md);
735
+ --board-text-lg: var(--rm3-text-lg);
736
+ --board-text-xl: var(--rm3-text-xl);
737
+ --board-fw-regular: var(--rm3-font-weight-regular);
738
+ --board-fw-medium: var(--rm3-font-weight-medium);
739
+ --board-fw-semibold: var(--rm3-font-weight-semibold);
740
+ --board-radius-sm: var(--rm3-radius-sm);
741
+ --board-radius-md: var(--rm3-radius-md);
742
+ --board-radius-lg: var(--rm3-radius-lg);
743
+ --board-radius-xl: var(--rm3-radius-xl);
744
+ --board-space-8: var(--rm3-space-8);
745
+ --board-space-12: var(--rm3-space-12);
746
+ --board-space-16: var(--rm3-space-16);
747
+ --board-space-24: var(--rm3-space-24);
748
+ --board-space-32: var(--rm3-space-32);
749
+ --board-tracking-tight: var(--rm3-tracking-tight);
750
+ --board-tracking-mono: var(--rm3-tracking-mono);
751
+ }
752
+
753
+ /* Semantic aliases remapped for DARK. */
754
+ .rec-board[data-theme='dark'], .rec-inspector[data-theme='dark'] {
755
+ --board-bg: var(--rm3-background);
756
+ --board-fg: var(--rm3-foreground);
757
+ --board-card: var(--rm3-card);
758
+ --board-muted: var(--rm3-muted);
759
+ --board-muted-fg: var(--rm3-muted-foreground);
760
+ --board-accent: var(--rm3-accent);
761
+ --board-border: var(--rm3-border);
762
+ --board-success: var(--rm3-success);
763
+ --board-warning: var(--rm3-warning);
764
+ --board-error: var(--rm3-error);
765
+ --board-advisory: var(--rm3-advisory);
766
+ --board-info: var(--rm3-info);
767
+ --board-destructive: var(--rm3-error);
768
+ }
769
+
770
+ /* ===== Board ===== */
771
+ .rec-board {
772
+ position: fixed;
773
+ inset: 0;
774
+ z-index: 9999;
775
+ display: flex;
776
+ flex-direction: column;
777
+ box-sizing: border-box;
778
+ padding: var(--board-space-16);
779
+ gap: var(--board-space-12);
780
+ background: var(--board-bg);
781
+ color: var(--board-fg);
782
+ font-family: var(--board-font-sans);
783
+ pointer-events: auto;
784
+ overflow: hidden;
785
+ }
786
+
787
+ .rec-board-header {
788
+ display: flex;
789
+ align-items: center;
790
+ gap: var(--board-space-12);
791
+ flex: none;
792
+ }
793
+
794
+ .rec-board-title {
795
+ margin: 0;
796
+ font-size: 22px;
797
+ font-weight: var(--board-fw-semibold);
798
+ letter-spacing: var(--board-tracking-tight);
799
+ color: var(--board-fg);
800
+ white-space: nowrap;
801
+ }
802
+
803
+ .rec-board-path {
804
+ flex: none;
805
+ font-family: var(--board-font-mono);
806
+ font-size: var(--board-text-xs);
807
+ letter-spacing: var(--board-tracking-mono);
808
+ color: var(--board-muted-fg);
809
+ overflow: hidden;
810
+ text-overflow: ellipsis;
811
+ white-space: nowrap;
812
+ max-width: 40%;
813
+ }
814
+
815
+ .rec-board-count {
816
+ flex: none;
817
+ font-size: var(--board-text-xs);
818
+ font-weight: var(--board-fw-medium);
819
+ color: var(--board-muted-fg);
820
+ background: var(--board-muted);
821
+ border-radius: 9999px;
822
+ padding: 4px 10px;
823
+ }
824
+
825
+ .rec-board-empty {
826
+ margin: 0;
827
+ padding: var(--board-space-32);
828
+ text-align: center;
829
+ color: var(--board-muted-fg);
830
+ }
831
+
832
+ .rec-close {
833
+ display: inline-flex;
834
+ align-items: center;
835
+ justify-content: center;
836
+ width: 28px;
837
+ height: 28px;
838
+ padding: 0;
839
+ margin-left: auto;
840
+ flex: none;
841
+ background: transparent;
842
+ border: 1px solid var(--board-border);
843
+ border-radius: var(--board-radius-md);
844
+ color: var(--board-muted-fg);
845
+ cursor: pointer;
846
+ font-size: var(--board-text-md);
847
+ line-height: 1;
848
+ }
849
+
850
+ .rec-close:hover {
851
+ background: var(--board-accent);
852
+ color: var(--board-fg);
853
+ }
854
+
855
+ .rec-theme-toggle {
856
+ display: inline-flex;
857
+ align-items: center;
858
+ justify-content: center;
859
+ width: 28px;
860
+ height: 28px;
861
+ padding: 0;
862
+ flex: none;
863
+ background: transparent;
864
+ border: 1px solid var(--board-border);
865
+ border-radius: var(--board-radius-md);
866
+ color: var(--board-muted-fg);
867
+ cursor: pointer;
868
+ font-size: var(--board-text-md);
869
+ line-height: 1;
870
+ }
871
+
872
+ .rec-theme-toggle:hover {
873
+ background: var(--board-accent);
874
+ color: var(--board-fg);
875
+ }
876
+
877
+ .rec-columns {
878
+ display: grid;
879
+ grid-auto-flow: column;
880
+ grid-auto-columns: minmax(220px, 1fr);
881
+ gap: var(--board-space-12);
882
+ flex: 1;
883
+ min-height: 0;
884
+ overflow-x: auto;
885
+ overflow-y: hidden;
886
+ overscroll-behavior-inline: contain;
887
+ padding-bottom: 6px;
888
+ scrollbar-color: var(--board-border) var(--board-accent);
889
+ scrollbar-width: thin;
890
+ }
891
+
892
+ .rec-columns::-webkit-scrollbar { height: 10px; }
893
+ .rec-columns::-webkit-scrollbar-track { background: var(--board-accent); border-radius: 999px; }
894
+ .rec-columns::-webkit-scrollbar-thumb { background: var(--board-border); border-radius: 999px; }
895
+
896
+ .rec-column {
897
+ display: flex;
898
+ flex-direction: column;
899
+ min-height: 0;
900
+ background: var(--board-muted);
901
+ border: 1px solid var(--board-border);
902
+ border-radius: var(--board-radius-xl);
903
+ overflow: hidden;
904
+ }
905
+
906
+ .rec-column-header {
907
+ display: flex;
908
+ align-items: center;
909
+ gap: 6px;
910
+ padding: var(--board-space-12);
911
+ flex: none;
912
+ }
913
+
914
+ .rec-column-title {
915
+ margin: 0;
916
+ flex: 1;
917
+ font-size: 13px;
918
+ font-weight: var(--board-fw-semibold);
919
+ color: var(--board-fg);
920
+ overflow: hidden;
921
+ text-overflow: ellipsis;
922
+ white-space: nowrap;
923
+ }
924
+
925
+ .rec-column-count {
926
+ flex: none;
927
+ font-size: var(--board-text-xs);
928
+ font-weight: var(--board-fw-medium);
929
+ color: var(--board-muted-fg);
930
+ background: var(--board-accent);
931
+ border-radius: 9999px;
932
+ padding: 1px 8px;
933
+ }
934
+
935
+ .rec-status-dot {
936
+ width: 6px;
937
+ height: 6px;
938
+ border-radius: 50%;
939
+ flex: none;
940
+ }
941
+
942
+ .rec-status-dot[data-status='new'] { background: var(--board-muted-fg); }
943
+ .rec-status-dot[data-status='active'] { background: var(--board-info); }
944
+ .rec-status-dot[data-status='paused'] { background: var(--board-warning); }
945
+ .rec-status-dot[data-status='blocked'] { background: var(--board-error); }
946
+ .rec-status-dot[data-status='complete'] { background: var(--board-success); }
947
+
948
+ .rec-cards {
949
+ display: flex;
950
+ flex-direction: column;
951
+ gap: var(--board-space-8);
952
+ padding: 10px;
953
+ overflow-y: auto;
954
+ flex: 1;
955
+ min-height: 0;
956
+ }
957
+
958
+ .rec-column-empty {
959
+ padding: var(--board-space-24) var(--board-space-8);
960
+ text-align: center;
961
+ font-size: var(--board-text-xs);
962
+ color: var(--board-muted-fg);
963
+ }
964
+
965
+ .rec-card {
966
+ display: flex;
967
+ flex-direction: column;
968
+ gap: var(--board-space-8);
969
+ padding: var(--board-space-12) 14px;
970
+ text-align: left;
971
+ background: var(--board-card);
972
+ border: 1px solid var(--board-border);
973
+ border-radius: var(--board-radius-lg);
974
+ cursor: pointer;
975
+ color: var(--board-fg);
976
+ font-family: inherit;
977
+ transition: box-shadow 120ms ease, border-color 120ms ease, transform 120ms ease;
978
+ }
979
+
980
+ .rec-card:hover {
981
+ border-color: var(--board-info);
982
+ transform: translateY(-1px);
983
+ }
984
+
985
+ .rec-card:active {
986
+ transform: translateY(0);
987
+ }
988
+
989
+ .rec-card-title {
990
+ font-size: 13px;
991
+ font-weight: var(--board-fw-semibold);
992
+ line-height: 1.35;
993
+ overflow: hidden;
994
+ display: -webkit-box;
995
+ -webkit-line-clamp: 2;
996
+ -webkit-box-orient: vertical;
997
+ }
998
+
999
+ .rec-card-progress {
1000
+ height: 6px;
1001
+ background: var(--board-accent);
1002
+ border-radius: 3px;
1003
+ overflow: hidden;
1004
+ }
1005
+
1006
+ .rec-card-progress-fill {
1007
+ height: 100%;
1008
+ background: var(--board-info);
1009
+ }
1010
+
1011
+ .rec-card-meta {
1012
+ display: flex;
1013
+ align-items: center;
1014
+ gap: var(--board-space-8);
1015
+ }
1016
+
1017
+ .rec-card-phase {
1018
+ flex: 1;
1019
+ font-family: var(--board-font-mono);
1020
+ font-size: var(--board-text-xs);
1021
+ letter-spacing: var(--board-tracking-mono);
1022
+ color: var(--board-muted-fg);
1023
+ overflow: hidden;
1024
+ text-overflow: ellipsis;
1025
+ white-space: nowrap;
1026
+ }
1027
+
1028
+ /* ===== Solid status pill (Paper StatusPill) ===== */
1029
+ .rec-pill {
1030
+ flex: none;
1031
+ border-radius: 9999px;
1032
+ padding: 4px 10px;
1033
+ font-size: var(--board-text-xs);
1034
+ font-weight: var(--board-fw-medium);
1035
+ line-height: 1;
1036
+ border: none;
1037
+ background: var(--board-muted-fg);
1038
+ color: var(--board-bg);
1039
+ }
1040
+
1041
+ .rec-pill[data-pill='locked'] { background: var(--board-success); color: var(--board-bg); }
1042
+ .rec-pill[data-pill='in-progress'] { background: var(--board-info); color: var(--board-bg); }
1043
+ .rec-pill[data-pill='paused'] { background: var(--board-warning); color: var(--board-bg); }
1044
+ .rec-pill[data-pill='blocked'] { background: var(--board-error); color: var(--board-bg); }
1045
+ .rec-pill[data-pill='tampered'] { background: var(--board-error); color: var(--board-bg); }
1046
+ .rec-pill[data-pill='advisory'] { background: var(--board-advisory); color: var(--board-bg); }
1047
+ .rec-pill[data-pill='neutral'] { background: var(--board-muted-fg); color: var(--board-bg); }
1048
+
1049
+ /* ===== Inspector: centered modal detail ===== */
1050
+ .rec-inspector {
1051
+ position: fixed;
1052
+ inset: 0;
1053
+ z-index: 9999;
1054
+ display: flex;
1055
+ align-items: center;
1056
+ justify-content: center;
1057
+ background: rgba(0, 0, 0, 0.4);
1058
+ pointer-events: auto;
1059
+ }
1060
+
1061
+ .rec-detail {
1062
+ display: flex;
1063
+ flex-direction: column;
1064
+ width: min(640px, calc(100vw - 48px));
1065
+ max-height: calc(100vh - 80px);
1066
+ background: var(--board-bg);
1067
+ border: 1px solid var(--board-border);
1068
+ border-radius: 14px;
1069
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.24);
1070
+ color: var(--board-fg);
1071
+ font-family: var(--board-font-sans);
1072
+ overflow: hidden;
1073
+ }
1074
+
1075
+ .rec-detail-header {
1076
+ display: flex;
1077
+ align-items: center;
1078
+ gap: var(--board-space-12);
1079
+ padding: var(--board-space-16) var(--board-space-16);
1080
+ border-bottom: 1px solid var(--board-border);
1081
+ flex: none;
1082
+ }
1083
+
1084
+ .rec-detail-title {
1085
+ margin: 0;
1086
+ flex: 1;
1087
+ font-size: 15px;
1088
+ font-weight: var(--board-fw-semibold);
1089
+ letter-spacing: var(--board-tracking-tight);
1090
+ overflow-wrap: anywhere;
1091
+ }
1092
+
1093
+ .rec-back {
1094
+ display: inline-flex;
1095
+ align-items: center;
1096
+ gap: 4px;
1097
+ padding: 5px var(--board-space-12);
1098
+ font-size: var(--board-text-xs);
1099
+ color: var(--board-fg);
1100
+ background: transparent;
1101
+ border: 1px solid var(--board-border);
1102
+ border-radius: var(--board-radius-lg);
1103
+ cursor: pointer;
1104
+ white-space: nowrap;
1105
+ }
1106
+
1107
+ .rec-back:hover {
1108
+ background: var(--board-accent);
1109
+ }
1110
+
1111
+ .rec-detail-body {
1112
+ padding: var(--board-space-16);
1113
+ overflow-y: auto;
1114
+ display: flex;
1115
+ flex-direction: column;
1116
+ gap: var(--board-space-16);
1117
+ flex: 1;
1118
+ }
1119
+
1120
+ .rec-detail-section {
1121
+ display: flex;
1122
+ flex-direction: column;
1123
+ gap: 6px;
1124
+ }
1125
+
1126
+ .rec-detail-section h3 {
1127
+ margin: 0;
1128
+ font-size: var(--board-text-xs);
1129
+ font-weight: var(--board-fw-semibold);
1130
+ color: var(--board-muted-fg);
1131
+ }
1132
+
1133
+ .rec-detail-text {
1134
+ margin: 0;
1135
+ font-size: var(--board-text-sm);
1136
+ color: var(--board-muted-fg);
1137
+ }
1138
+
1139
+ .rec-phase-row {
1140
+ display: flex;
1141
+ align-items: center;
1142
+ gap: var(--board-space-12);
1143
+ padding: var(--board-space-8) var(--board-space-12);
1144
+ border: 1px solid var(--board-border);
1145
+ border-radius: var(--board-radius-lg);
1146
+ font-size: 13px;
1147
+ }
1148
+
1149
+ .rec-phase-id {
1150
+ min-width: 48px;
1151
+ font-family: var(--board-font-mono);
1152
+ font-size: var(--board-text-xs);
1153
+ letter-spacing: var(--board-tracking-mono);
1154
+ color: var(--board-info);
1155
+ }
1156
+
1157
+ .rec-phase-name {
1158
+ flex: 1;
1159
+ color: var(--board-fg);
1160
+ overflow: hidden;
1161
+ text-overflow: ellipsis;
1162
+ white-space: nowrap;
1163
+ }
1164
+
1165
+ .rec-lockhash {
1166
+ font-family: var(--board-font-mono);
1167
+ font-size: 11px;
1168
+ letter-spacing: var(--board-tracking-mono);
1169
+ color: var(--board-muted-fg);
1170
+ }
1171
+
1172
+ /* ===== Strip (session dock) — keeps the shell --dsw-* theme ===== */
1173
+ .rec-badge {
1174
+ flex: none;
1175
+ padding: 2px 10px;
1176
+ font-size: 12px;
1177
+ border-radius: 999px;
1178
+ border: 1px solid var(--dsw-alias-border-l2);
1179
+ color: var(--dsw-alias-label-secondary);
1180
+ }
1181
+ .rec-badge-tamper {
1182
+ color: var(--dsw-alias-state-error-primary);
1183
+ border-color: var(--dsw-alias-state-error-primary);
1184
+ }
1185
+ .rec-badge-gate {
1186
+ color: var(--dsw-alias-state-warn-primary);
1187
+ border-color: var(--dsw-alias-state-warn-primary);
1188
+ }
1189
+
1190
+ @media (prefers-reduced-motion: reduce) {
1191
+ .rec-card, .rec-back, .rec-close, .rec-theme-toggle { transition: none; }
1192
+ }
1193
+ `;
1194
+ /**
1195
+ * Inject the board/inspector stylesheet once. Idempotent: if a <style
1196
+ * data-dsh-recursive> already exists, returns a disposer that does nothing
1197
+ * (the first injector owns the element). Returns a disposer that removes it.
1198
+ */
1199
+ function injectBoardStyles() {
1200
+ if (typeof document === "undefined") return () => {};
1201
+ if (document.querySelector("style[data-dsh-recursive]") !== null) return () => {};
1202
+ const style = document.createElement("style");
1203
+ style.setAttribute("data-dsh-recursive", "");
1204
+ style.textContent = BOARD_CSS;
1205
+ document.head.appendChild(style);
1206
+ let disposed = false;
1207
+ return () => {
1208
+ if (disposed) return;
1209
+ disposed = true;
1210
+ style.remove();
1211
+ };
1212
+ }
1213
+ //#endregion
769
1214
  //#region src/client/slots.ts
770
1215
  /**
771
1216
  * Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
@@ -817,6 +1262,7 @@ window.__ModuleLoader__.load({
817
1262
  }
818
1263
  function registerSlots(ctx) {
819
1264
  const disposers = [];
1265
+ disposers.push(injectBoardStyles());
820
1266
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
821
1267
  name: "sidebar.footer.action",
822
1268
  id: "recursive",