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

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
@@ -244,102 +244,14 @@ window.__ModuleLoader__.load({
244
244
  //#endregion
245
245
  //#region src/client/board.tsx
246
246
  /**
247
- * Board overlay (SP2 R1 + run 08 R3 + run 12): the kanban view over the live
247
+ * Board overlay (SP2 R1 + run 08 R3 + run 15): the kanban view over the live
248
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").
249
+ * scraping. Cards are grouped by worktree root and mapped to their current-phase
250
+ * lane via derive.columnForRun.
254
251
  *
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.
258
- *
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.
252
+ * Run 15 (task-board redesign): class-based structure + theme-token stylesheet
253
+ * injected once via styles.ts (no inline styles — unlocks hover/grid/theme).
264
254
  */
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
255
  /** Flatten the worktree-grouped projection into a stable run list. */
344
256
  function listRuns(projection) {
345
257
  if (projection === void 0) return [];
@@ -347,220 +259,105 @@ window.__ModuleLoader__.load({
347
259
  for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
348
260
  return runs;
349
261
  }
262
+ function closeButton$1(onClose) {
263
+ if (onClose === void 0) return null;
264
+ return (0, react.createElement)("button", {
265
+ type: "button",
266
+ className: "rec-close",
267
+ onClick: onClose,
268
+ title: "Close",
269
+ "aria-label": "Close"
270
+ }, "×");
271
+ }
350
272
  function Board({ snapshot, onOpenInspector, onClose }) {
351
273
  if (snapshot === null || snapshot.root === null) return null;
352
274
  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."));
367
275
  const open = (run) => () => onOpenInspector?.({
368
276
  worktreeRoot: run.worktreeRoot,
369
277
  runId: run.runId
370
278
  });
371
- return (0, react.createElement)("div", {
279
+ if (runs.length === 0) return (0, react.createElement)("div", {
372
280
  className: "rec-board",
373
- style: overlayStyle
374
- }, close, KANBAN_LANES.map((lane) => {
281
+ "data-empty": true
282
+ }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), closeButton$1(onClose)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
283
+ return (0, react.createElement)("div", { className: "rec-board" }, (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-count" }, String(runs.length)), closeButton$1(onClose)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
375
284
  const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
376
285
  return (0, react.createElement)("section", {
377
286
  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) => {
287
+ className: "rec-column"
288
+ }, (0, react.createElement)("header", { className: "rec-column-header" }, (0, react.createElement)("span", {
289
+ className: "rec-status-dot",
290
+ "data-status": laneRuns.length > 0 ? laneRuns[laneRuns.length - 1].state : "new",
291
+ "aria-hidden": true
292
+ }), (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
293
  const facts = cardFacts(run);
385
294
  return (0, react.createElement)("article", {
386
295
  key: run.worktreeRoot + "\0" + run.runId,
387
296
  className: "rec-card",
388
- style: cardStyle,
297
+ "data-state": run.state,
389
298
  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", {
299
+ }, (0, react.createElement)("div", { className: "rec-card-title" }, run.runId), (0, react.createElement)("div", { className: "rec-card-progress" }, (0, react.createElement)("div", {
300
+ className: "rec-card-progress-fill",
301
+ style: { width: Math.round(facts.progress * 100) + "%" }
302
+ })), (0, react.createElement)("div", { className: "rec-card-meta" }, (0, react.createElement)("span", { className: "rec-card-phase" }, facts.currentPhase ?? "no phases"), facts.tampered && (0, react.createElement)("span", {
394
303
  className: "rec-badge rec-badge-tamper",
395
- style: {
396
- ...badgeStyle$1,
397
- ...badgeTamperStyle
398
- },
304
+ "data-status": run.state,
399
305
  title: "tampered"
400
- }, "!")), (0, react.createElement)("div", {
401
- className: "rec-card-progress",
402
- style: progressStyle
403
- }, (0, react.createElement)("div", {
404
- 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", {
306
+ }, "!"), (0, react.createElement)("span", {
413
307
  className: "rec-badge",
414
- style: badgeStyle$1
415
- }, facts.state)));
416
- }));
417
- }));
308
+ "data-status": run.state
309
+ }, run.state)));
310
+ }), laneRuns.length === 0 && (0, react.createElement)("div", { className: "rec-column-empty" }, "—")));
311
+ })));
418
312
  }
419
313
  //#endregion
420
314
  //#region src/client/inspector.tsx
421
315
  /**
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.
316
+ * Drill-down inspector (SP2 R1 + run 15): centered modal-style detail panel
317
+ * (task-board .detail shape) rendering one run's phase chain, lock badges, and
318
+ * tamper/gate facts — all from the live host-route snapshot, never disk.
431
319
  */
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", {
320
+ function closeButton(onClose) {
321
+ if (onClose === void 0) return null;
322
+ return (0, react.createElement)("button", {
323
+ type: "button",
488
324
  className: "rec-close",
489
- style: closeStyle,
490
325
  onClick: onClose,
491
- title: "Close"
492
- }, "×") : null;
493
- const card = snapshot?.projection?.[worktreeRoot]?.[runId];
494
- if (card === void 0) return (0, react.createElement)("div", {
495
- 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,
326
+ title: "Close",
327
+ "aria-label": "Close"
328
+ }, "×");
329
+ }
330
+ function detailShell(runId, onBackToToolDetails, onClose, headerExtra, body) {
331
+ return (0, react.createElement)("div", { className: "rec-inspector" }, (0, react.createElement)("div", { className: "rec-detail" }, (0, react.createElement)("div", { className: "rec-detail-header" }, (0, react.createElement)("button", {
332
+ type: "button",
502
333
  className: "rec-back",
503
- style: backStyle
504
- }, "← back to tool details"), close), (0, react.createElement)("p", {}, "Run not found: " + runId));
334
+ onClick: onBackToToolDetails
335
+ }, "← Back"), (0, react.createElement)("h2", { className: "rec-detail-title" }, runId), headerExtra, closeButton(onClose)), (0, react.createElement)("div", { className: "rec-detail-body" }, body)));
336
+ }
337
+ function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
338
+ const card = snapshot?.projection?.[worktreeRoot]?.[runId];
339
+ if (card === void 0) return detailShell(runId, onBackToToolDetails, onClose, null, (0, react.createElement)("p", { className: "rec-detail-text" }, "Run not found: " + runId));
505
340
  const facts = cardFacts(card);
506
341
  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", {
342
+ const badge = (0, react.createElement)("span", {
521
343
  className: "rec-badge",
522
- style: badgeStyle
523
- }, facts.state), facts.tampered && (0, react.createElement)("span", {
344
+ "data-status": card.state
345
+ }, facts.state);
346
+ const tamper = facts.tampered ? (0, react.createElement)("span", {
524
347
  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", {
348
+ "data-status": card.state
349
+ }, "tampered") : null;
350
+ return detailShell(runId, onBackToToolDetails, onClose, (0, react.createElement)("span", { style: {
351
+ display: "flex",
352
+ alignItems: "center",
353
+ gap: 6
354
+ } }, badge, tamper), (0, react.createElement)("section", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
539
355
  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", {
356
+ className: "rec-phase-row"
357
+ }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), (0, react.createElement)("span", {
549
358
  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 ?? "") + ")")));
359
+ "data-status": row.status === "LOCKED" ? "complete" : "active"
360
+ }, 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 ?? "") + ")"))));
564
361
  }
565
362
  //#endregion
566
363
  //#region src/client/strip.tsx
@@ -766,6 +563,402 @@ window.__ModuleLoader__.load({
766
563
  return (0, react.useSyncExternalStore)(store.subscribe, store.get, store.get);
767
564
  }
768
565
  //#endregion
566
+ //#region src/client/styles.ts
567
+ /**
568
+ * One-shot board/inspector stylesheet injection (run 15). The client bundle is a
569
+ * plain CJS closure with NO CSS pipeline (tsdown has no CSS handling), so we
570
+ * inject a single <style data-dsh-recursive> element into document.head and
571
+ * style via class names + the shell's --dsw-* theme tokens. That unlocks hover,
572
+ * grid layout, and light/dark/skin theme following — impossible with inline
573
+ * styles. Idempotent: guarded by document.querySelector, so every registration
574
+ * reuses the one element; the returned disposer removes it.
575
+ */
576
+ const BOARD_CSS = `
577
+ /* dsh-recursive board + inspector (theme-token driven, .rec-* scoped) */
578
+ .rec-board {
579
+ position: fixed;
580
+ inset: 0;
581
+ z-index: 9999;
582
+ background: var(--dsw-alias-bg-base);
583
+ color: var(--dsw-alias-label-primary);
584
+ font-family: var(--dsw-font-family);
585
+ display: flex;
586
+ flex-direction: column;
587
+ box-sizing: border-box;
588
+ padding: 14px 16px 16px;
589
+ gap: 12px;
590
+ pointer-events: auto;
591
+ overflow: hidden;
592
+ }
593
+
594
+ .rec-board-header {
595
+ display: flex;
596
+ align-items: center;
597
+ gap: 10px;
598
+ flex: none;
599
+ }
600
+
601
+ .rec-board-title {
602
+ margin: 0;
603
+ font-size: 16px;
604
+ font-weight: 700;
605
+ color: var(--dsw-alias-label-primary);
606
+ white-space: nowrap;
607
+ }
608
+
609
+ .rec-board-count {
610
+ flex: none;
611
+ font-size: 12px;
612
+ color: var(--dsw-alias-label-tertiary);
613
+ background: var(--dsw-alias-interactive-bg-hover);
614
+ border-radius: 999px;
615
+ padding: 1px 8px;
616
+ }
617
+
618
+ .rec-board-empty {
619
+ margin: 0;
620
+ padding: 40px;
621
+ text-align: center;
622
+ color: var(--dsw-alias-label-tertiary);
623
+ }
624
+
625
+ .rec-close {
626
+ position: absolute;
627
+ top: 14px;
628
+ right: 16px;
629
+ z-index: 1;
630
+ display: inline-flex;
631
+ align-items: center;
632
+ justify-content: center;
633
+ width: 26px;
634
+ height: 26px;
635
+ padding: 0;
636
+ background: transparent;
637
+ border: 1px solid var(--dsw-alias-border-l2);
638
+ border-radius: 6px;
639
+ color: var(--dsw-alias-label-secondary);
640
+ cursor: pointer;
641
+ font-size: 15px;
642
+ line-height: 1;
643
+ }
644
+
645
+ .rec-close:hover {
646
+ background: var(--dsw-alias-interactive-bg-hover);
647
+ color: var(--dsw-alias-label-primary);
648
+ }
649
+
650
+ .rec-columns {
651
+ display: grid;
652
+ grid-auto-flow: column;
653
+ grid-auto-columns: minmax(240px, 1fr);
654
+ gap: 12px;
655
+ flex: 1;
656
+ min-height: 0;
657
+ overflow-x: auto;
658
+ overflow-y: hidden;
659
+ overscroll-behavior-inline: contain;
660
+ padding-bottom: 6px;
661
+ scrollbar-color: var(--dsw-alias-border-l3) var(--dsw-alias-interactive-bg-hover);
662
+ scrollbar-width: thin;
663
+ }
664
+
665
+ .rec-columns::-webkit-scrollbar { height: 10px; }
666
+ .rec-columns::-webkit-scrollbar-track { background: var(--dsw-alias-interactive-bg-hover); border-radius: 999px; }
667
+ .rec-columns::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l3); background-clip: content-box; border: 2px solid transparent; border-radius: 999px; }
668
+ .rec-columns::-webkit-scrollbar-thumb:hover { background: var(--dsw-alias-border-l3); background-clip: content-box; }
669
+
670
+ .rec-column {
671
+ display: flex;
672
+ flex-direction: column;
673
+ min-height: 0;
674
+ background: var(--dsw-alias-bg-layer-2);
675
+ border: 1px solid var(--dsw-alias-border-l1);
676
+ border-radius: 12px;
677
+ overflow: hidden;
678
+ }
679
+
680
+ .rec-column-header {
681
+ display: flex;
682
+ align-items: center;
683
+ gap: 6px;
684
+ padding: 10px 12px;
685
+ flex: none;
686
+ }
687
+
688
+ .rec-column-title {
689
+ margin: 0;
690
+ flex: 1;
691
+ font-size: 13px;
692
+ font-weight: 700;
693
+ color: var(--dsw-alias-label-primary);
694
+ overflow: hidden;
695
+ text-overflow: ellipsis;
696
+ white-space: nowrap;
697
+ }
698
+
699
+ .rec-column-count {
700
+ flex: none;
701
+ font-size: 12px;
702
+ color: var(--dsw-alias-label-tertiary);
703
+ background: var(--dsw-alias-interactive-bg-hover);
704
+ border-radius: 999px;
705
+ padding: 1px 8px;
706
+ }
707
+
708
+ .rec-status-dot {
709
+ width: 8px;
710
+ height: 8px;
711
+ border-radius: 50%;
712
+ flex: none;
713
+ }
714
+
715
+ .rec-status-dot[data-status='new'] { background: var(--dsw-alias-label-tertiary); }
716
+ .rec-status-dot[data-status='active'] { background: var(--dsw-alias-state-business-primary); }
717
+ .rec-status-dot[data-status='paused'] { background: var(--dsw-alias-state-warn-primary); }
718
+ .rec-status-dot[data-status='blocked'] { background: var(--dsw-alias-state-error-primary); }
719
+ .rec-status-dot[data-status='complete'] { background: var(--dsw-alias-state-success-primary); }
720
+
721
+ .rec-cards {
722
+ display: flex;
723
+ flex-direction: column;
724
+ gap: 8px;
725
+ padding: 2px 8px 10px;
726
+ overflow-y: auto;
727
+ flex: 1;
728
+ min-height: 0;
729
+ }
730
+
731
+ .rec-column-empty {
732
+ padding: 24px 8px;
733
+ text-align: center;
734
+ font-size: 12px;
735
+ color: var(--dsw-alias-label-tertiary);
736
+ }
737
+
738
+ .rec-card {
739
+ display: flex;
740
+ flex-direction: column;
741
+ gap: 6px;
742
+ padding: 10px 12px;
743
+ text-align: left;
744
+ background: var(--dsw-alias-bg-base);
745
+ border: 1px solid var(--dsw-alias-border-l2);
746
+ border-radius: 10px;
747
+ cursor: pointer;
748
+ color: var(--dsw-alias-label-primary);
749
+ font-family: inherit;
750
+ transition: box-shadow 120ms ease, border-color 120ms ease, transform 120ms ease;
751
+ }
752
+
753
+ .rec-card:hover {
754
+ box-shadow: var(--dsw-shadow-lv2);
755
+ border-color: var(--dsw-alias-border-l3);
756
+ transform: translateY(-1px);
757
+ }
758
+
759
+ .rec-card:active {
760
+ box-shadow: var(--dsw-shadow-lv1);
761
+ transform: translateY(0);
762
+ }
763
+
764
+ .rec-card[data-state='active'] { border-color: var(--dsw-alias-state-business-primary); }
765
+ .rec-card[data-state='paused'] { border-color: var(--dsw-alias-state-warn-primary); }
766
+ .rec-card[data-state='blocked'] { border-color: var(--dsw-alias-state-error-primary); }
767
+ .rec-card[data-state='complete'] { border-color: var(--dsw-alias-state-success-primary); }
768
+
769
+ .rec-card-title {
770
+ font-size: 13px;
771
+ font-weight: 600;
772
+ line-height: 1.35;
773
+ overflow: hidden;
774
+ display: -webkit-box;
775
+ -webkit-line-clamp: 2;
776
+ -webkit-box-orient: vertical;
777
+ }
778
+
779
+ .rec-card-progress {
780
+ height: 6px;
781
+ background: var(--dsw-alias-interactive-bg-hover);
782
+ border-radius: 3px;
783
+ overflow: hidden;
784
+ }
785
+
786
+ .rec-card-progress-fill {
787
+ height: 100%;
788
+ background: var(--dsw-alias-state-business-primary);
789
+ }
790
+
791
+ .rec-card-meta {
792
+ display: flex;
793
+ align-items: center;
794
+ gap: 8px;
795
+ font-size: 11px;
796
+ color: var(--dsw-alias-label-tertiary);
797
+ }
798
+
799
+ .rec-card-phase {
800
+ flex: 1;
801
+ overflow: hidden;
802
+ text-overflow: ellipsis;
803
+ white-space: nowrap;
804
+ }
805
+
806
+ .rec-badge {
807
+ flex: none;
808
+ padding: 2px 10px;
809
+ font-size: 12px;
810
+ border-radius: 999px;
811
+ border: 1px solid var(--dsw-alias-border-l2);
812
+ color: var(--dsw-alias-label-secondary);
813
+ }
814
+
815
+ .rec-badge[data-status='active'] { color: var(--dsw-alias-state-business-primary); border-color: var(--dsw-alias-state-business-primary); }
816
+ .rec-badge[data-status='paused'] { color: var(--dsw-alias-state-warn-primary); border-color: var(--dsw-alias-state-warn-primary); }
817
+ .rec-badge[data-status='blocked'] { color: var(--dsw-alias-state-error-primary); border-color: var(--dsw-alias-state-error-primary); }
818
+ .rec-badge[data-status='complete'] { color: var(--dsw-alias-state-success-primary); border-color: var(--dsw-alias-state-success-primary); }
819
+ .rec-badge[data-status='new'] { color: var(--dsw-alias-label-tertiary); }
820
+
821
+ .rec-badge-tamper {
822
+ color: var(--dsw-alias-state-error-primary);
823
+ border-color: var(--dsw-alias-state-error-primary);
824
+ }
825
+
826
+ /* inspector: centered modal-style detail */
827
+ .rec-inspector {
828
+ position: fixed;
829
+ inset: 0;
830
+ z-index: 9999;
831
+ display: flex;
832
+ align-items: center;
833
+ justify-content: center;
834
+ background: var(--dsw-alias-bg-mask-1);
835
+ pointer-events: auto;
836
+ }
837
+
838
+ .rec-detail {
839
+ display: flex;
840
+ flex-direction: column;
841
+ width: min(640px, calc(100vw - 48px));
842
+ max-height: calc(100vh - 80px);
843
+ background: var(--dsw-alias-bg-base);
844
+ border: 1px solid var(--dsw-alias-border-l2);
845
+ border-radius: 14px;
846
+ box-shadow: var(--dsw-shadow-lv3);
847
+ color: var(--dsw-alias-label-primary);
848
+ overflow: hidden;
849
+ }
850
+
851
+ .rec-detail-header {
852
+ display: flex;
853
+ align-items: center;
854
+ gap: 10px;
855
+ padding: 14px 18px;
856
+ border-bottom: 1px solid var(--dsw-alias-separator-primary);
857
+ flex: none;
858
+ }
859
+
860
+ .rec-detail-title {
861
+ margin: 0;
862
+ flex: 1;
863
+ font-size: 15px;
864
+ font-weight: 700;
865
+ overflow-wrap: anywhere;
866
+ }
867
+
868
+ .rec-back {
869
+ display: inline-flex;
870
+ align-items: center;
871
+ gap: 4px;
872
+ padding: 5px 12px;
873
+ font-size: 12px;
874
+ color: var(--dsw-alias-label-primary);
875
+ background: transparent;
876
+ border: 1px solid var(--dsw-alias-border-l2);
877
+ border-radius: 8px;
878
+ cursor: pointer;
879
+ white-space: nowrap;
880
+ }
881
+
882
+ .rec-back:hover {
883
+ background: var(--dsw-alias-interactive-bg-hover);
884
+ }
885
+
886
+ .rec-detail-body {
887
+ padding: 14px 18px;
888
+ overflow-y: auto;
889
+ display: flex;
890
+ flex-direction: column;
891
+ gap: 16px;
892
+ flex: 1;
893
+ }
894
+
895
+ .rec-detail-section {
896
+ display: flex;
897
+ flex-direction: column;
898
+ gap: 6px;
899
+ }
900
+
901
+ .rec-detail-section h3 {
902
+ margin: 0;
903
+ font-size: 12px;
904
+ font-weight: 700;
905
+ color: var(--dsw-alias-label-tertiary);
906
+ }
907
+
908
+ .rec-phase-row {
909
+ display: flex;
910
+ align-items: center;
911
+ gap: 10px;
912
+ padding: 8px 10px;
913
+ border: 1px solid var(--dsw-alias-border-l1);
914
+ border-radius: 8px;
915
+ font-size: 13px;
916
+ }
917
+
918
+ .rec-phase-id {
919
+ min-width: 48px;
920
+ font-family: var(--dsw-font-markdown-code-block-small);
921
+ color: var(--dsw-alias-state-business-primary);
922
+ }
923
+
924
+ .rec-phase-name {
925
+ flex: 1;
926
+ color: var(--dsw-alias-label-primary);
927
+ overflow: hidden;
928
+ text-overflow: ellipsis;
929
+ white-space: nowrap;
930
+ }
931
+
932
+ .rec-lockhash {
933
+ font-family: var(--dsw-font-markdown-code-block-small);
934
+ font-size: 11px;
935
+ color: var(--dsw-alias-label-tertiary);
936
+ }
937
+
938
+ @media (prefers-reduced-motion: reduce) {
939
+ .rec-card, .rec-back, .rec-close { transition: none; }
940
+ }
941
+ `;
942
+ /**
943
+ * Inject the board/inspector stylesheet once. Idempotent: if a <style
944
+ * data-dsh-recursive> already exists, returns a disposer that does nothing
945
+ * (the first injector owns the element). Returns a disposer that removes it.
946
+ */
947
+ function injectBoardStyles() {
948
+ if (typeof document === "undefined") return () => {};
949
+ if (document.querySelector("style[data-dsh-recursive]") !== null) return () => {};
950
+ const style = document.createElement("style");
951
+ style.setAttribute("data-dsh-recursive", "");
952
+ style.textContent = BOARD_CSS;
953
+ document.head.appendChild(style);
954
+ let disposed = false;
955
+ return () => {
956
+ if (disposed) return;
957
+ disposed = true;
958
+ style.remove();
959
+ };
960
+ }
961
+ //#endregion
769
962
  //#region src/client/slots.ts
770
963
  /**
771
964
  * Slot registrations (SP2 R1 + run 08 R1/R3/R4): the sidebar launcher, the
@@ -817,6 +1010,7 @@ window.__ModuleLoader__.load({
817
1010
  }
818
1011
  function registerSlots(ctx) {
819
1012
  const disposers = [];
1013
+ disposers.push(injectBoardStyles());
820
1014
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
821
1015
  name: "sidebar.footer.action",
822
1016
  id: "recursive",