create-pmos 0.124.0 → 0.126.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/scripts/nine-stages.js +81 -0
  6. package/payload/scripts/workflow-state.js +2 -0
  7. package/payload/state/discoveries.md +1 -1
  8. package/payload/state/log.md +1 -1
  9. package/workspace/c3-one-home-test.js +131 -0
  10. package/workspace/fixtures/initiatives-api/list.json +171 -3
  11. package/workspace/fixtures/initiatives-api/roadmap.json +228 -4
  12. package/workspace/fixtures/initiatives-api/sheet.json +57 -1
  13. package/workspace/fixtures/one-home-baseline.json +83 -0
  14. package/workspace/guide/briefing-cards.md +40 -0
  15. package/workspace/guide/index.md +1 -0
  16. package/workspace/guide/layout.md +2 -0
  17. package/workspace/lib/engine/nine-stages.js +81 -0
  18. package/workspace/lib/engine/workflow-state.js +2 -0
  19. package/workspace/lib/facts-async.js +51 -0
  20. package/workspace/lib/item-steps.js +52 -0
  21. package/workspace/lib/jira-status.js +8 -3
  22. package/workspace/lib/jobs-to-be-done.js +52 -0
  23. package/workspace/lib/needs-overdue.js +41 -0
  24. package/workspace/lib/outbox-store.js +87 -0
  25. package/workspace/lib/pm-items.js +115 -0
  26. package/workspace/lib/route-modules.js +21 -0
  27. package/workspace/lib/routes-briefing.js +109 -0
  28. package/workspace/lib/routes-c3-init.js +53 -0
  29. package/workspace/lib/routes-c3-life.js +119 -0
  30. package/workspace/lib/routes-c3-strategy.js +169 -0
  31. package/workspace/lib/routes-c3-timeline.js +191 -0
  32. package/workspace/lib/routes-calendar-import.js +60 -0
  33. package/workspace/lib/routes-calendar-views.js +61 -0
  34. package/workspace/lib/routes-doc-health.js +79 -0
  35. package/workspace/lib/routes-doc-sheet.js +169 -0
  36. package/workspace/lib/routes-forecast.js +85 -0
  37. package/workspace/lib/routes-hooks.js +121 -0
  38. package/workspace/lib/routes-initiatives.js +3 -0
  39. package/workspace/lib/routes-intake.js +24 -0
  40. package/workspace/lib/routes-outbox.js +97 -0
  41. package/workspace/lib/routes-pm-items.js +216 -0
  42. package/workspace/lib/routes-records.js +61 -0
  43. package/workspace/lib/routes-releases.js +129 -0
  44. package/workspace/lib/routes-report-facts.js +84 -0
  45. package/workspace/lib/routes-report-families.js +32 -0
  46. package/workspace/lib/routes-roadmap-ops.js +141 -0
  47. package/workspace/lib/routes-runs.js +119 -0
  48. package/workspace/lib/routes-signals.js +50 -0
  49. package/workspace/lib/run-answers.js +69 -0
  50. package/workspace/package-lock.json +2 -2
  51. package/workspace/package.json +1 -1
  52. package/workspace/public/c3.html +26 -0
  53. package/workspace/public/pm/briefing-deck.js +413 -0
  54. package/workspace/public/pm/briefing-model.js +211 -0
  55. package/workspace/public/pm/c3-breakdown.js +240 -0
  56. package/workspace/public/pm/c3-brief.js +278 -0
  57. package/workspace/public/pm/c3-cal-strategy.js +528 -0
  58. package/workspace/public/pm/c3-case.js +185 -0
  59. package/workspace/public/pm/c3-daypart.js +176 -0
  60. package/workspace/public/pm/c3-demo-brief-ix.js +350 -0
  61. package/workspace/public/pm/c3-demo-cal-ix.js +74 -0
  62. package/workspace/public/pm/c3-demo-cal2.js +66 -0
  63. package/workspace/public/pm/c3-demo-cal3.js +173 -0
  64. package/workspace/public/pm/c3-demo-data.js +560 -0
  65. package/workspace/public/pm/c3-demo-init.js +373 -0
  66. package/workspace/public/pm/c3-demo-life.js +77 -0
  67. package/workspace/public/pm/c3-demo-misc2.js +46 -0
  68. package/workspace/public/pm/c3-demo-plan2.js +30 -0
  69. package/workspace/public/pm/c3-demo-rep-ix.js +126 -0
  70. package/workspace/public/pm/c3-demo-rep2.js +92 -0
  71. package/workspace/public/pm/c3-demo-term.js +153 -0
  72. package/workspace/public/pm/c3-demo-time.js +49 -0
  73. package/workspace/public/pm/c3-demo-work-ix.js +80 -0
  74. package/workspace/public/pm/c3-demo-work2.js +13 -0
  75. package/workspace/public/pm/c3-demo.js +113 -0
  76. package/workspace/public/pm/c3-doc-sheet.js +314 -0
  77. package/workspace/public/pm/c3-dock.js +180 -0
  78. package/workspace/public/pm/c3-docs.js +290 -0
  79. package/workspace/public/pm/c3-expand.js +96 -0
  80. package/workspace/public/pm/c3-init-extras.js +170 -0
  81. package/workspace/public/pm/c3-lifecycle-panels.js +476 -0
  82. package/workspace/public/pm/c3-misc-panels.js +89 -0
  83. package/workspace/public/pm/c3-plan-panels.js +163 -0
  84. package/workspace/public/pm/c3-plan.js +192 -0
  85. package/workspace/public/pm/c3-roadmap.js +249 -0
  86. package/workspace/public/pm/c3-sheet.js +107 -0
  87. package/workspace/public/pm/c3-shell.js +674 -0
  88. package/workspace/public/pm/c3-signals.js +196 -0
  89. package/workspace/public/pm/c3-strategy-panels.js +45 -0
  90. package/workspace/public/pm/c3-swipe.js +230 -0
  91. package/workspace/public/pm/c3-terminal-data.js +26 -0
  92. package/workspace/public/pm/c3-terminal-edge.js +91 -0
  93. package/workspace/public/pm/c3-terminal-icons.js +41 -0
  94. package/workspace/public/pm/c3-terminal-results.js +141 -0
  95. package/workspace/public/pm/c3-terminal.js +52 -0
  96. package/workspace/public/pm/c3-tickets.js +163 -0
  97. package/workspace/public/pm/c3-timeline.js +201 -0
  98. package/workspace/public/pm/c3-toast.js +16 -0
  99. package/workspace/public/pm/calendar-views.js +1014 -0
  100. package/workspace/public/pm/console3-lift.md +220 -0
  101. package/workspace/public/pm/console3.css +1547 -0
  102. package/workspace/public/pm/dest-initiatives.js +16 -3
  103. package/workspace/public/pm/dest-mywork.js +8 -1
  104. package/workspace/public/pm/dest-reports.js +47 -1
  105. package/workspace/public/pm/dest-today.js +55 -1
  106. package/workspace/public/pm/doc-health-panel.js +110 -0
  107. package/workspace/public/pm/doc-rail.js +9 -3
  108. package/workspace/public/pm/lifecycle-lens-model.js +228 -0
  109. package/workspace/public/pm/lifecycle-lens.js +261 -0
  110. package/workspace/public/pm/new-initiative-sheet.js +116 -21
  111. package/workspace/public/pm/outbox-panel.js +125 -0
  112. package/workspace/public/pm/pm-work-view.js +895 -0
  113. package/workspace/public/pm/releases-view.js +248 -0
  114. package/workspace/public/pm/report-families-view.js +503 -0
  115. package/workspace/public/pm/run-card.js +244 -0
  116. package/workspace/public/pm/sessions-panel.js +149 -0
  117. package/workspace/public/pm/shell-bell.js +162 -0
  118. package/workspace/public/pm/shell-model.js +83 -0
  119. package/workspace/public/pm/shell-phone.js +124 -0
  120. package/workspace/public/pm/shell-readaloud.js +53 -0
  121. package/workspace/public/pm/shell-recording.js +110 -0
  122. package/workspace/public/pm/shell-ticker.js +90 -0
  123. package/workspace/public/pm/slide-confirm.js +163 -0
  124. package/workspace/public/pm/timeline-forecast-view.js +164 -0
  125. package/workspace/public/pm/today-day.js +8 -2
  126. package/workspace/public/pm/tokens.css +1 -0
  127. package/workspace/server.js +5 -2
  128. package/workspace/test-files.json +21 -3
  129. package/workspace/test-lists/browser-suites-green.json +5 -0
@@ -0,0 +1,895 @@
1
+ /* pm/pm-work-view.js — the Work view of My work, built on the console3 mockup's own markup and CSS (initiative
2
+ * planning-flows-and-agent-sessions). The mockup (prototype/pmos-console-handoff/mockups/console3.html) is the UI: the rows
3
+ * (.nrow with .rv, .ki, .nb, .tt, .chips, .xp), the lens bar (.calbar, .seg), the add line (.c3add, .chips2), the case file
4
+ * (.case, .c3jf, .c3steps), the jobs card (.card.sect) and the swipe gesture are the mockup's; the styles are pm/console3.css
5
+ * (see pm/console3-lift.md). This file keeps the real data calls and draws what they return, nothing else.
6
+ *
7
+ * mountPmWork(host, ctx) draws once per host; disposePmWork(host) removes it.
8
+ * Routes (lib/routes-pm-items.js). Every write is a page press through pmPost:
9
+ * GET /api/pm-items POST /api/pm-items/{add,edit,remove,undo,done,reopen,park,unpark,move,steps/add,steps/who,steps/plan}
10
+ * POST /api/pm-jobs/{add,edit,delete}
11
+ * A row opens its CASE FILE in place (c3-expand.js + c3-case.js: the job sentence, evidence, options with a recommendation, a slide to
12
+ * confirm); the editable fields sit inside it behind "Edit details". A row swipes (c3-swipe.js) and takes keys: right = the right-hand
13
+ * action of its list (done, in Today, ...), left = the left-hand one (later, tomorrow, ...); e and n do the same, j and k move, and the
14
+ * arrows do the swipes. Every action offers Undo through the reverse route (reopen, unpark, move back, edit back, undo). Anything that
15
+ * would send something to a person is an outward option: its slide is drawn disabled ("no channel connected"); nothing is sent.
16
+ * In demo mode (/c3.html?demo=1, window.__c3demo) the answers of the writes are laid over the fixtures in memory (`applyOv`); the
17
+ * agent run of a row with a suggested move is a mock that only walks the steps and shows a labelled draft.
18
+ * Drawn from the items: the lens ring (done of total). The deck is ctx.plan, a node the page builds. The
19
+ * microphone is drawn disabled.
20
+ *
21
+ * The helpers c3ic, c3ring, ensureC3Css and c3Phone are shared with briefing-deck.js and calendar-views.js. */
22
+ import { el } from './dom.js';
23
+ import { pmGet, pmPost } from './http.js';
24
+ import { renderCase } from './c3-case.js';
25
+ import { makeXp, attachExpand } from './c3-expand.js';
26
+ import { attachSwipe } from './c3-swipe.js';
27
+ import { toast } from './c3-toast.js';
28
+
29
+ /* ── shared with the Briefing and Calendar views: the stylesheet, the icon sprite, the ring, the phone class ───────── */
30
+
31
+ const SVG_NS = 'http://www.w3.org/2000/svg';
32
+ /* The mockup's icon sprite (console3.html lines 970 to 1026, ids renamed c3i-*, plus the c3 layer's edit and trash). */
33
+ const SPRITE = '<symbol id="c3i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol><symbol id="c3i-moon" viewBox="0 0 24 24"><path d="M20 14a8 8 0 11-10-10 6 6 0 0010 10z"/></symbol><symbol id="c3i-cal" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M4 10h16M8 3v4M16 3v4"/></symbol><symbol id="c3i-inbox" viewBox="0 0 24 24"><path d="M4 13l2-8h12l2 8M4 13v6h16v-6M4 13h5l1 2h4l1-2h5"/></symbol><symbol id="c3i-todo" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 12l3 3 5-6"/></symbol><symbol id="c3i-init" viewBox="0 0 24 24"><path d="M12 3l9 5-9 5-9-5 9-5zM3 13l9 5 9-5"/></symbol><symbol id="c3i-report" viewBox="0 0 24 24"><path d="M5 20V10M12 20V4M19 20v-7"/></symbol><symbol id="c3i-notes" viewBox="0 0 24 24"><path d="M6 3h9l4 4v14H6zM14 3v5h5M9 13h7M9 17h5"/></symbol><symbol id="c3i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 4-6 8-6s8 2 8 6"/></symbol><symbol id="c3i-doc" viewBox="0 0 24 24"><path d="M6 3h9l4 4v14H6zM14 3v5h5"/></symbol><symbol id="c3i-sheet" viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M4 10h16M4 15h16M10 4v16"/></symbol><symbol id="c3i-img" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="M21 16l-5-5-9 9"/></symbol><symbol id="c3i-pdf" viewBox="0 0 24 24"><path d="M6 3h9l4 4v14H6zM14 3v5h5M9 15h6M9 18h4"/></symbol><symbol id="c3i-term" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3M13 15h4"/></symbol><symbol id="c3i-branch" viewBox="0 0 24 24"><circle cx="6" cy="5" r="2"/><circle cx="6" cy="19" r="2"/><circle cx="18" cy="9" r="2"/><path d="M6 7v10M18 11c0 4-6 3-12 6"/></symbol><symbol id="c3i-repo" viewBox="0 0 24 24"><path d="M5 4h11a3 3 0 013 3v13H8a3 3 0 01-3-3zM5 17a3 3 0 013-3h11"/></symbol><symbol id="c3i-flag" viewBox="0 0 24 24"><path d="M6 21V4M6 5h11l-2 4 2 4H6"/></symbol><symbol id="c3i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol><symbol id="c3i-alert" viewBox="0 0 24 24"><path d="M12 3l10 18H2zM12 10v5M12 18v.5"/></symbol><symbol id="c3i-check" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></symbol><symbol id="c3i-x" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></symbol><symbol id="c3i-eye" viewBox="0 0 24 24"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></symbol><symbol id="c3i-undo" viewBox="0 0 24 24"><path d="M4 12a8 8 0 108-8M4 4v6h6"/></symbol><symbol id="c3i-out" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></symbol><symbol id="c3i-shield" viewBox="0 0 24 24"><path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/></symbol><symbol id="c3i-mic" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0014 0M12 18v3"/></symbol><symbol id="c3i-clip" viewBox="0 0 24 24"><rect x="6" y="4" width="12" height="17" rx="2"/><path d="M9 4h6v3H9zM9 12h6M9 16h4"/></symbol><symbol id="c3i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5"/></symbol><symbol id="c3i-phone" viewBox="0 0 24 24"><rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/></symbol><symbol id="c3i-cloud" viewBox="0 0 24 24"><path d="M7 18a4 4 0 010-8 5 5 0 019.6-1A4.5 4.5 0 0117 18z"/></symbol><symbol id="c3i-bolt" viewBox="0 0 24 24"><path d="M13 2L4 14h7l-1 8 9-12h-7z"/></symbol><symbol id="c3i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol><symbol id="c3i-scale" viewBox="0 0 24 24"><path d="M12 3v18M5 7h14M5 7l-3 7a3 3 0 006 0zM19 7l-3 7a3 3 0 006 0z"/></symbol><symbol id="c3i-reply" viewBox="0 0 24 24"><path d="M9 7L4 12l5 5M4 12h10a6 6 0 016 6"/></symbol><symbol id="c3i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 11v6M12 7.5v.5"/></symbol><symbol id="c3i-wait" viewBox="0 0 24 24"><path d="M7 3h10M7 21h10M8 3c0 5 8 5 8 9s-8 4-8 9"/></symbol><symbol id="c3i-up" viewBox="0 0 24 24"><path d="M7 11v9H4v-9zM7 11l4-8c2 0 3 1 3 3v3h5a2 2 0 012 2l-1 6a2 2 0 01-2 2H7"/></symbol><symbol id="c3i-cols" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16M15 4v16"/></symbol><symbol id="c3i-fold" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M15 4v16"/></symbol><symbol id="c3i-spark" viewBox="0 0 24 24"><path d="M12 3l2 5 5 2-5 2-2 5-2-5-5-2 5-2z"/></symbol><symbol id="c3i-target" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1"/></symbol><symbol id="c3i-filter" viewBox="0 0 24 24"><path d="M3 5h18l-7 8v6l-4-2v-4z"/></symbol><symbol id="c3i-mail" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></symbol><symbol id="c3i-lock" viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 018 0v3"/></symbol><symbol id="c3i-play" viewBox="0 0 24 24"><path d="M7 4l13 8-13 8z"/></symbol><symbol id="c3i-stop" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="1"/></symbol><symbol id="i-pause" viewBox="0 0 24 24"><path d="M8 5v14M16 5v14"/></symbol><symbol id="c3i-prev" viewBox="0 0 24 24"><path d="M15 6l-6 6 6 6"/></symbol><symbol id="c3i-next" viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></symbol><symbol id="c3i-tree" viewBox="0 0 24 24"><path d="M5 4h7v4H5zM13 10h7v4h-7zM13 17h7v4h-7zM8 8v11h5M8 12h5"/></symbol><symbol id="c3i-org" viewBox="0 0 24 24"><path d="M9 3h6v5H9zM3 16h6v5H3zM15 16h6v5h-6zM12 8v4M6 16v-4h12v4"/></symbol><symbol id="c3i-drop" viewBox="0 0 24 24"><path d="M12 3c4 5 6 8 6 11a6 6 0 01-12 0c0-3 2-6 6-11z"/></symbol><symbol id="c3i-more" viewBox="0 0 24 24"><path d="M5 12h.01M12 12h.01M19 12h.01" stroke-width="3"/></symbol><symbol id="c3i-zin" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5M8 11h6M11 8v6"/></symbol><symbol id="c3i-zout" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5M8 11h6"/></symbol><symbol id="c3i-bell" viewBox="0 0 24 24"><path d="M6 17v-6a6 6 0 0112 0v6l2 2H4zM10 21h4"/></symbol><symbol id="c3i-vol" viewBox="0 0 24 24"><path d="M4 9v6h4l5 4V5L8 9zM16.5 8.5a5 5 0 010 7M19 6a8.5 8.5 0 010 12"/></symbol><symbol id="c3i-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol><symbol id="c3i-edit" viewBox="0 0 24 24"><path d="M4 20h4L19 9l-4-4L4 16v4zM13.5 6.5l4 4"/></symbol><symbol id="c3i-trash" viewBox="0 0 24 24"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></symbol>';
34
+
35
+ /** Link console3.css once, from this module's own folder (the same way primitives.js links components.css). */
36
+ export function ensureC3Css() {
37
+ if (typeof document === 'undefined' || document.querySelector('link[data-c3]')) return;
38
+ const l = document.createElement('link');
39
+ l.rel = 'stylesheet';
40
+ l.href = new URL('./console3.css', import.meta.url).href;
41
+ l.setAttribute('data-c3', '');
42
+ document.head.appendChild(l);
43
+ if (!document.getElementById('c3-sprite') && document.body) {
44
+ const d = document.createElement('div');
45
+ d.id = 'c3-sprite';
46
+ d.setAttribute('aria-hidden', 'true');
47
+ d.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden';
48
+ d.innerHTML = '<svg width="0" height="0" aria-hidden="true"><defs>' + SPRITE + '</defs></svg>';
49
+ document.body.appendChild(d);
50
+ }
51
+ }
52
+
53
+ /** The mockup's ic(): <svg class="ic sm"><use href="#c3i-name"/></svg>. */
54
+ export function c3ic(name, cls) {
55
+ const s = document.createElementNS(SVG_NS, 'svg');
56
+ s.setAttribute('class', 'ic' + (cls ? ' ' + cls : ''));
57
+ s.setAttribute('aria-hidden', 'true');
58
+ const u = document.createElementNS(SVG_NS, 'use');
59
+ u.setAttribute('href', '#c3i-' + name);
60
+ s.appendChild(u);
61
+ return s;
62
+ }
63
+
64
+ /** The mockup's ring(done, total, size), built as DOM. A label replaces the "done/total" text in the middle. */
65
+ export function c3ring(done, total, size, label) {
66
+ const long = String(label || done + '/' + total).length > 5; // "322/322" does not fit a 44px ring: grow it and use the smaller size
67
+ if (long) size = Math.max(size, 64);
68
+ const r = size / 2 - 5; const cc = 2 * Math.PI * r; const p = total ? done / total : 0; const c = size / 2;
69
+ const s = document.createElementNS(SVG_NS, 'svg');
70
+ const set = (n, o) => { for (const k in o) n.setAttribute(k, o[k]); return n; };
71
+ set(s, { width: size, height: size, viewBox: `0 0 ${size} ${size}`, role: 'img', 'aria-label': label ? String(label) : `${done} of ${total}` });
72
+ s.appendChild(set(document.createElementNS(SVG_NS, 'circle'), { cx: c, cy: c, r, fill: 'none', stroke: 'var(--muted)', 'stroke-width': 6 }));
73
+ s.appendChild(set(document.createElementNS(SVG_NS, 'circle'), { cx: c, cy: c, r, fill: 'none', class: 'rg', stroke: 'var(--acc)', 'stroke-width': 6, 'stroke-linecap': 'round', 'stroke-dasharray': `${cc * p} ${cc}`, transform: `rotate(-90 ${c} ${c})` }));
74
+ const t = set(document.createElementNS(SVG_NS, 'text'), { x: c, y: c + 4, 'text-anchor': 'middle', class: 't', style: 'font-size:' + (long ? '10px' : 'var(--fs-2)') });
75
+ t.textContent = label ? String(label) : `${done}/${total}`;
76
+ s.appendChild(t);
77
+ return s;
78
+ }
79
+
80
+ /** The mockup's `.ph` phone class, driven by one media query so the view and a real phone behave the same. Returns the stop function. */
81
+ export function c3Phone(root) {
82
+ if (typeof matchMedia !== 'function') return () => {};
83
+ const q = matchMedia('(max-width:640px)');
84
+ const on = () => root.classList.toggle('ph', q.matches);
85
+ on();
86
+ if (q.addEventListener) q.addEventListener('change', on); else q.addListener(on);
87
+ return () => { if (q.removeEventListener) q.removeEventListener('change', on); else q.removeListener(on); };
88
+ }
89
+
90
+ /** The mockup's ib(): an icon button. */
91
+ export function c3ib(label, icon, attrs, cls) {
92
+ return el('button', { type: 'button', class: 'ib ' + (cls || ''), title: label, 'aria-label': label, ...(attrs || {}) }, c3ic(icon));
93
+ }
94
+
95
+ /* ── copy and constants ──────────────────────────────────────────────────────────────────────────────────────────── */
96
+
97
+ const API = '/api/pm-items';
98
+ const JOBS = '/api/pm-jobs';
99
+ const LENSES = [['triage', 'Triage'], ['today', 'Today'], ['week', 'Week'], ['waiting', 'Waiting'], ['later', 'Later']];
100
+ const WLI = { triage: 'inbox', today: 'sun', week: 'cal', waiting: 'wait', later: 'moon' }; // console3.html line 3207
101
+ const WHO = ['you', 'agent-drafts', 'agent-does'];
102
+ const OUTWARD = /\b(send|sends|sending|email|emails|message|messages|tell|post|reply to)\b/i; // mirrors lib/item-steps.js
103
+ const NEED = 'Needs the backend: no endpoint for this yet.';
104
+
105
+ export const COPY = Object.freeze({
106
+ addPlaceholder: 'Add work: type or speak',
107
+ addLabel: 'Add work', addBtn: 'Add', hint: 'Try: Send Dana the new date by Fri !1',
108
+ addsTo: 'adds to', noJob: 'no job yet', waitingOn: (w) => 'waiting on ' + w,
109
+ empty: 'Nothing here.', noJobGroup: 'No job yet', jobs: 'Jobs to be done', noJobs: 'No jobs yet.',
110
+ addJob: 'Add a job', addJobPlaceholder: 'when I plan a release, I want honest dates, so Dana can trust them',
111
+ groupJob: 'Group by job to be done', groupTime: 'Group by time', loading: 'Reading…',
112
+ micOff: 'Speak is off on this line: on-device dictation runs in Notes, in the main app. Type the item here.',
113
+ jobsNote: 'The count is every item in the job, across all lists. Tap a job to edit or delete it.',
114
+ legend: ['open', 'done', 'migrated', 'scheduled'], readError: 'Your items could not be read.', saveError: 'Nothing was saved.',
115
+ removed: 'Removed.', undo: 'Undo', restored: 'Restored.', tooLate: 'Too late to undo.', drop: 'Drop it',
116
+ reassigned: (n) => (n === 1 ? '1 item now has no job.' : `${n} items now have no job.`),
117
+ progress: (d, t) => `${d} of ${t} done`,
118
+ jobLine: (j) => `When ${j.when}, I want ${j.want}, so ${j.so}.`,
119
+ lens: {
120
+ triage: (n) => `${n} to triage`, today: (n) => `${n} committed today`, week: (n) => `${n} land in the next 7 days`,
121
+ waiting: (n) => `${n} waiting on others`, later: (n) => `${n} for later`,
122
+ },
123
+ lensSub: {
124
+ triage: 'No date and nobody to wait on. Give each one a date, or park it.', today: 'Due today or overdue.',
125
+ week: 'Due in the next 7 days.', waiting: 'Someone else must act first.', later: 'Dated further out.',
126
+ },
127
+ done: 'Done', park: 'Park', doneToast: 'Marked done.', parkToast: 'Parked in Later.', steps: 'Steps', noSteps: 'No steps yet.', addStep: 'Add a step',
128
+ who: { you: 'You', 'agent-drafts': 'Agent drafts', 'agent-does': 'Agent does' },
129
+ whoOutward: 'Sends to a person: the agent drafts, you slide to send.', options: 'Options', plan: 'Plan the steps',
130
+ });
131
+ const insts = new WeakMap();
132
+
133
+ /* The toast is the shared one (c3-toast.js): the text and, for an action that can be taken back, an Undo button. One at a time. */
134
+ const say = toast;
135
+
136
+ /* ── the demo overlay ────────────────────────────────────────────────────────────────────────────────────────────── *
137
+ * In /c3.html?demo=1 every write is answered { ok: true } and the fixtures never change, so the page keeps what the PM did in
138
+ * memory (`inst.ov`) and lays it over each read. Real mode has no overlay: the server is the truth and every press re-reads. */
139
+ const DEMO = () => typeof window !== 'undefined' && !!window.__c3demo;
140
+ const newOv = () => ({ done: new Set(), gone: new Set(), lens: new Map(), edits: new Map(), added: [], steps: new Map(), who: new Map(), jobsAdded: [], jobEdits: new Map(), jobsGone: new Map(), seq: 0 });
141
+ const LENS_IDS = LENSES.map((l) => l[0]);
142
+
143
+ function applyOv(base, ov) {
144
+ const d = JSON.parse(JSON.stringify(base));
145
+ const home = {};
146
+ LENS_IDS.forEach((l) => (d.lists[l] || []).forEach((i) => { home[i.id] = l; }));
147
+ const added = ov.added.map((a) => { home[a.item.id] = a.lens; return a.item; }).reverse();
148
+ const jobOfItem = {};
149
+ let items = [...added, ...(d.items || [])].filter((i) => !ov.gone.has(i.id));
150
+ const wasDone = new Set((d.items || []).filter((i) => i.done).map((i) => i.id));
151
+ items = items.map((i) => {
152
+ const m = { ...i, ...(ov.edits.get(i.id) || {}) };
153
+ m.done = !!(i.done || ov.done.has(i.id));
154
+ m.steps = [...(i.steps || []), ...(ov.steps.get(i.id) || [])].map((s) => (ov.who.has(s.id) ? { ...s, who: ov.who.get(s.id) } : s));
155
+ jobOfItem[m.id] = m.job;
156
+ return m;
157
+ });
158
+ const lists = {}; LENS_IDS.forEach((l) => { lists[l] = []; });
159
+ items.forEach((i) => { const l = ov.lens.get(i.id) || home[i.id] || 'triage'; (lists[l] || lists.triage).push(i); });
160
+ /* The mockup's Today (console3.html wItems, line 3215) is the work set for today AND the inbox items due today: an item due today shows in both lenses. */
161
+ const dueToday = (i) => String(i.kind || '').toLowerCase() !== 'fyi' && ((i.due && i.due === d.today) || /^(\d\d:\d\d|today)$/.test(i.time || ''));
162
+ lists.triage.filter((i) => !i.done && dueToday(i) && !lists.today.includes(i)).forEach((i) => lists.today.push(i));
163
+ d.items = items; d.lists = lists;
164
+ if (d.noise) d.noise = d.noise.filter((n) => !ov.done.has(n.id || 'noise-' + String(n.title).toLowerCase().replace(/[^a-z0-9]+/g, '-')));
165
+ const jobs = [...(d.jobs || []), ...ov.jobsAdded].filter((j) => !ov.jobsGone.has(j.id)).map((j) => ({ ...j, ...(ov.jobEdits.get(j.id) || {}) }));
166
+ jobs.forEach((j) => {
167
+ j.done = (j.done || 0) + [...ov.done].filter((id) => !wasDone.has(id) && jobOfItem[id] === j.id).length;
168
+ j.total = (j.total || 0) + ov.added.filter((a) => a.item.job === j.id).length;
169
+ });
170
+ d.jobs = jobs;
171
+ return d;
172
+ }
173
+
174
+ /** Record in the overlay what a demo press would have changed. `body` and `r` are the press and its answer. */
175
+ function applyLocal(inst, path, body, r) {
176
+ const ov = inst.ov; const id = body && body.id;
177
+ const today = (inst.base && inst.base.today) || iso(new Date());
178
+ const cur = id && ((inst.data && inst.data.items) || []).find((i) => i.id === id);
179
+ switch (path.replace(API, '').replace(JOBS, '/job')) {
180
+ case '/done': ov.done.add(id); break;
181
+ case '/reopen': ov.done.delete(id); break;
182
+ case '/park': ov.lens.set(id, 'later'); break;
183
+ case '/unpark': ov.lens.delete(id); break;
184
+ case '/move': ov.lens.set(id, body.list); break;
185
+ case '/edit': {
186
+ ov.edits.set(id, { ...(ov.edits.get(id) || {}), ...body.fields });
187
+ if (cur && ('due' in body.fields || 'waiting_on' in body.fields)) ov.lens.set(id, lensOf({ ...cur, ...body.fields }, today));
188
+ break;
189
+ }
190
+ case '/add': {
191
+ const p = previewParse(body.line, today);
192
+ const item = { ...(r.item || {}), id: (r.item && r.item.id) || 'item-demo-add' + (++ov.seq), title: p.title || String(body.line), due: p.due, priority: p.priority, waiting_on: p.waiting_on, job: null, steps: [], created_at: today + 'T05:00:00.000Z' };
193
+ ov.added.push({ item, lens: lensOf(p, today) });
194
+ r.item = item;
195
+ break;
196
+ }
197
+ case '/remove': ov.gone.add(id); break;
198
+ case '/undo': ov.gone.delete(id); break;
199
+ case '/steps/add': { const l = ov.steps.get(id) || []; l.push({ id: 'step-demo' + (++ov.seq), text: body.text, who: 'you', done: false }); ov.steps.set(id, l); break; }
200
+ case '/steps/who': ov.who.set(body.step_id, body.who); break;
201
+ case '/steps/plan': {
202
+ const t = (cur && cur.title) || 'the item';
203
+ const l = ov.steps.get(id) || [];
204
+ [['Read the item and what it links to', 'agent-does'], ['Draft the first pass or the message', 'agent-drafts'], ['Check it and decide', 'you']].forEach(([text, who]) => l.push({ id: 'step-demo' + (++ov.seq), text, who, done: false }));
205
+ ov.steps.set(id, l);
206
+ break;
207
+ }
208
+ case '/job/add': {
209
+ const m = (re) => { const x = String(body.line || '').match(re); return x ? x[1].trim().replace(/[,;]$/, '') : ''; };
210
+ const job = { id: 'job-demo-new' + (++ov.seq), when: m(/\bwhen\s+(.+?)(?=,?\s*I want\b|,?\s*so\b|$)/i), want: m(/\bI want\s+(.+?)(?=,?\s*so\b|$)/i), so: m(/\bso\s+(.+)$/i), done: 0, total: 0, ...(body.name ? { mock_name: body.name } : {}) };
211
+ ov.jobsAdded.push(job);
212
+ break;
213
+ }
214
+ case '/job/edit': ov.jobEdits.set(id, { ...(ov.jobEdits.get(id) || {}), ...body.fields }); break;
215
+ case '/job/delete': {
216
+ ov.jobsGone.set(id, true);
217
+ ((inst.data && inst.data.items) || []).filter((i) => i.job === id).forEach((i) => ov.edits.set(i.id, { ...(ov.edits.get(i.id) || {}), job: body.reassign_to || null }));
218
+ r.reassigned = ((inst.data && inst.data.items) || []).filter((i) => i.job === id).length;
219
+ break;
220
+ }
221
+ default: break;
222
+ }
223
+ }
224
+
225
+ export function mountPmWork(host, ctx) {
226
+ let inst = insts.get(host);
227
+ if (inst) return;
228
+ if (ctx && ctx.FIXTURE) return;
229
+ ensureC3Css();
230
+ inst = { host, ctx: ctx || {}, root: el('div', { class: 'c3 calwrap', 'data-pm-work': '' }), data: null, base: null, ov: newOv(), lens: 'triage', group: false,
231
+ opens: new Set(), picks: new Map(), editing: new Set(), planning: new Set(), runs: new Map(), trays: new Set(), cases: new Set(), timers: new Set(), hidden: new Set(), jobNew: null, jobForm: false };
232
+ inst.body = el('div', { class: 'calbody pmw-body' });
233
+ inst.bar = el('div', { class: 'calbar' });
234
+ inst.addBox = addLine(inst);
235
+ inst.root.append(inst.addBox, inst.bar, inst.body);
236
+ inst.stopPhone = c3Phone(inst.root);
237
+ insts.set(host, inst);
238
+ host.appendChild(inst.root);
239
+ load(inst);
240
+ }
241
+
242
+ export function disposePmWork(host) {
243
+ const inst = insts.get(host);
244
+ if (!inst) return;
245
+ if (inst.stopPhone) inst.stopPhone();
246
+ disposeCases(inst);
247
+ inst.timers.forEach((t) => clearTimeout(t));
248
+ inst.root.remove();
249
+ insts.delete(host);
250
+ }
251
+
252
+ function disposeCases(inst) { inst.cases.forEach((c) => { try { c.c3case.dispose(); } catch { /* already gone */ } }); inst.cases.clear(); }
253
+ const later = (inst, fn, ms) => { const t = setTimeout(() => { inst.timers.delete(t); if (insts.get(inst.host) === inst) fn(); }, ms); inst.timers.add(t); };
254
+
255
+ /* ── reads and presses ───────────────────────────────────────────────────────────────────────────────────────────── */
256
+
257
+ async function load(inst) {
258
+ const r = await pmGet(API);
259
+ inst.base = r && r.ok ? r : null;
260
+ inst.data = inst.base && DEMO() ? applyOv(inst.base, inst.ov) : inst.base;
261
+ if (!inst.data) say({ text: (r && r.error) || COPY.readError, tone: 'error' });
262
+ if (insts.get(inst.host) === inst) draw(inst);
263
+ }
264
+
265
+ async function press(inst, path, body, { quiet } = {}) {
266
+ const r = await pmPost(path, body);
267
+ if (!r || !r.ok) { say({ text: (r && r.error) || COPY.saveError, tone: 'error' }); return null; }
268
+ if (DEMO()) applyLocal(inst, path, body, r);
269
+ if (!quiet) await load(inst);
270
+ return r;
271
+ }
272
+
273
+ /* One action on an item, as the mockup does it: the row leaves, a toast says what happened and offers Undo. `spec` =
274
+ * { route, body, text, undo: async (r) => boolean, leave } and every route is an existing page-press route. */
275
+ async function act(inst, rowEl, spec) {
276
+ const r = await press(inst, spec.route.startsWith('/api/') ? spec.route : API + spec.route, spec.body, { quiet: true });
277
+ if (!r) return false;
278
+ const leaves = spec.leave !== false;
279
+ if (rowEl && leaves) rowEl.classList.add('leave'); // the mockup's .leave: fade and slide out (console3.html line 76)
280
+ say({
281
+ text: spec.text, timeout: r.undo_ms || 6000, actionLabel: spec.undo ? COPY.undo : undefined,
282
+ onAction: spec.undo ? async () => { const u = await spec.undo(r); if (u) say({ text: COPY.restored, timeout: 2500 }); await load(inst); } : undefined,
283
+ });
284
+ later(inst, () => load(inst), rowEl && leaves ? 230 : 0);
285
+ return true;
286
+ }
287
+
288
+ const undoBy = (inst, route, body) => async () => !!(await press(inst, API + route, body, { quiet: true }));
289
+
290
+ const doneAct = (inst, item, row, text) => act(inst, row, { route: '/done', body: { id: item.id }, text: text || COPY.doneToast, undo: undoBy(inst, '/reopen', { id: item.id }) });
291
+ const parkAct = (inst, item, row, text) => act(inst, row, { route: '/park', body: { id: item.id }, text: text || COPY.parkToast, undo: undoBy(inst, '/unpark', { id: item.id }) });
292
+ const moveAct = (inst, item, row, list, text) => {
293
+ const back = lensOfItem(inst, item.id);
294
+ return act(inst, row, { route: '/move', body: { id: item.id, list }, text, undo: undoBy(inst, '/move', { id: item.id, list: back }) });
295
+ };
296
+ const dueAct = (inst, item, row, due, text) => act(inst, row, { route: '/edit', body: { id: item.id, fields: { due } }, text, undo: undoBy(inst, '/edit', { id: item.id, fields: { due: item.due || null } }) });
297
+ const dropAct = (inst, item, row) => act(inst, row, { route: '/remove', body: { id: item.id }, text: COPY.removed, undo: undoBy(inst, '/undo', { id: item.id }) });
298
+
299
+ function lensOfItem(inst, id) {
300
+ for (const l of LENS_IDS) if ((lists(inst)[l] || []).some((i) => i.id === id)) return l;
301
+ return inst.lens;
302
+ }
303
+
304
+ async function cycleWho(inst, item, step) {
305
+ const outward = OUTWARD.test(step.text);
306
+ const next = WHO[(WHO.indexOf(step.who) + 1) % WHO.length];
307
+ // Outward steps stay agent-drafts; the server enforces it too.
308
+ if (outward) { say({ text: COPY.whoOutward, timeout: 3000 }); if (step.who === 'agent-drafts') return; }
309
+ await press(inst, API + '/steps/who', { step_id: step.id, who: outward ? 'agent-drafts' : next });
310
+ }
311
+
312
+ function planSteps(inst, item) {
313
+ inst.planning.add(item.id); inst.opens.add(item.id); draw(inst);
314
+ later(inst, async () => { await press(inst, API + '/steps/plan', { id: item.id }, { quiet: true }); inst.planning.delete(item.id); await load(inst); }, 800);
315
+ }
316
+
317
+ /* ── the add line: a client preview of the server parser (lib/pm-items.js parseLine), so the chips match what is saved ── */
318
+
319
+ const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
320
+ const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
321
+ const iso = (d) => d.toISOString().slice(0, 10);
322
+ const addIso = (s, n) => { const d = new Date(s + 'T00:00:00Z'); d.setUTCDate(d.getUTCDate() + n); return iso(d); };
323
+ const nextMonday = (s) => { const d = new Date(s + 'T00:00:00Z'); return addIso(s, ((8 - d.getUTCDay()) % 7) || 7); };
324
+ const dayLabel = (s) => { const d = new Date(s + 'T00:00:00Z'); return DAYS[d.getUTCDay()].slice(0, 1).toUpperCase() + DAYS[d.getUTCDay()].slice(1, 3) + ' ' + d.getUTCDate() + ' ' + MON[d.getUTCMonth()]; };
325
+
326
+ function previewParse(text, todayIso) {
327
+ let rest = String(text || '').trim();
328
+ const out = { title: '', due: null, priority: null, waiting_on: null };
329
+ const p = rest.match(/\bP([1-3])\b/i) || rest.match(/(?:^|\s)!([1-3])(?=\s|$)/); // "P1" or the shorthand "!1" (the server parser reads both)
330
+ if (p) { out.priority = 'P' + p[1]; rest = rest.replace(p[0], ' '); }
331
+ const w = rest.match(/\bwaiting on\s+([^,;]+?)(?=\s+(?:by|due|P[1-3])\b|$)/i);
332
+ if (w) { out.waiting_on = w[1].trim(); rest = rest.replace(w[0], ' '); }
333
+ const t = new Date(todayIso + 'T00:00:00Z');
334
+ const di = rest.match(/\b(?:due|by)\s+(\d{4}-\d{2}-\d{2})\b/i);
335
+ const dw = rest.match(/\b(?:due|by)\s+(today|tomorrow|sun(?:day)?|mon(?:day)?|tue(?:s(?:day)?)?|wed(?:nesday)?|thu(?:r(?:s(?:day)?)?)?|fri(?:day)?|sat(?:urday)?)\b/i);
336
+ if (di) { out.due = di[1]; rest = rest.replace(di[0], ' '); }
337
+ else if (dw) {
338
+ const k = dw[1].toLowerCase();
339
+ const d = new Date(t.getTime());
340
+ if (k === 'tomorrow') d.setUTCDate(d.getUTCDate() + 1);
341
+ else if (k !== 'today') d.setUTCDate(d.getUTCDate() + ((DAYS.findIndex((w) => w.startsWith(k.slice(0, 3))) - d.getUTCDay() + 7) % 7));
342
+ out.due = iso(d);
343
+ rest = rest.replace(dw[0], ' ');
344
+ }
345
+ out.title = rest.replace(/\s+/g, ' ').replace(/[,;]\s*$/, '').trim();
346
+ return out;
347
+ }
348
+
349
+ function lensOf(o, todayIso) {
350
+ if (o.waiting_on) return 'waiting';
351
+ if (!o.due) return 'triage';
352
+ if (o.due <= todayIso) return 'today';
353
+ const w = new Date(todayIso + 'T00:00:00Z'); w.setUTCDate(w.getUTCDate() + 7);
354
+ return o.due <= iso(w) ? 'week' : 'later';
355
+ }
356
+
357
+ const labelOf = (id) => (LENSES.find((l) => l[0] === id) || [])[1] || id;
358
+ const small = 'font-size:var(--fs-2)';
359
+
360
+ /* The mockup's c3prev(): "adds to", a .tag, then .chip2 / .fl / .st chips (console3.html line 4573). */
361
+ function previewChips(inst, text) {
362
+ const today = (inst.data && inst.data.today) || iso(new Date());
363
+ if (!text.trim()) return [el('span', { class: 'dim mono', style: small }, COPY.hint)];
364
+ const o = previewParse(text, today);
365
+ if (!o.title) return [el('span', { class: 'dim mono', style: small }, 'The line needs a title.')];
366
+ const out = [el('span', { class: 'dim mono', style: small }, COPY.addsTo), el('span', { class: 'tag' }, labelOf(lensOf(o, today)))];
367
+ if (o.due) out.push(el('span', { class: 'chip2' }, c3ic('cal', 'sm'), o.due));
368
+ if (o.priority) out.push(el('span', { class: 'fl ' + o.priority.toLowerCase() }, o.priority));
369
+ if (o.waiting_on) out.push(el('span', { class: 'chip2' }, c3ic('wait', 'sm'), COPY.waitingOn(o.waiting_on)));
370
+ out.push(el('span', { class: 'st warn' }, c3ic('target', 'sm'), COPY.noJob));
371
+ return out;
372
+ }
373
+
374
+ /* The mockup's c3addLine(): .c3add > .form (input, add button) + .chips2 with the live chips. The microphone is drawn disabled (see addLine). */
375
+ function addLine(inst) {
376
+ const input = el('input', { type: 'text', placeholder: COPY.addPlaceholder, 'aria-label': COPY.addLabel, autocomplete: 'off', 'data-pm-add': API + '/add' });
377
+ const chips = el('div', { class: 'chips2', 'data-pm-chips': '' }, previewChips(inst, ''));
378
+ inst.chips = chips; inst.input = input;
379
+ const send = async () => {
380
+ const line = input.value.trim();
381
+ if (!line) return;
382
+ const before = inst.data && inst.data.today;
383
+ const o = previewParse(line, before || iso(new Date()));
384
+ const r = await press(inst, API + '/add', { line }, { quiet: true });
385
+ if (!r) return;
386
+ input.value = ''; chips.replaceChildren(...previewChips(inst, ''));
387
+ inst.lens = lensOf(o, before || iso(new Date())); if (inst.ctx.onLens) inst.ctx.onLens(inst.lens); inst.group = false;
388
+ if (r.item) inst.opens.add(r.item.id);
389
+ say({ text: 'Added to ' + labelOf(inst.lens), timeout: r.undo_ms || 6000, actionLabel: COPY.undo, onAction: async () => { if (r.item) await press(inst, API + '/remove', { id: r.item.id }, { quiet: true }); await load(inst); } });
390
+ await load(inst);
391
+ if (DEMO() && r.item) later(inst, () => planSteps(inst, r.item), 900); // the mockup's agent plans the steps a moment after the add; real mode waits for the PM to press "Plan the steps"
392
+ };
393
+ input.addEventListener('input', () => chips.replaceChildren(...previewChips(inst, input.value)));
394
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) send(); });
395
+ const add = c3ib(COPY.addBtn, 'plus', { 'data-pm-add-send': '', onClick: send }, 'pri');
396
+ // The mockup's microphone (c3addLine). Disabled: the on-device dictation (whisper.cpp) belongs to a Notes document and needs the
397
+ // server; nothing on this page may record, and the browser recogniser is not on-device.
398
+ const mic = c3ib(COPY.micOff, 'mic', { 'data-off': '', 'data-pm-mic': '', onclick: () => say({ text: 'The microphone needs the app. Nothing was recorded: type the item here.', timeout: 4200 }) });
399
+ return el('div', { class: 'c3add' }, el('div', { class: 'form' }, input, mic, add), chips);
400
+ }
401
+
402
+ /* ── rows and the case file ──────────────────────────────────────────────────────────────────────────────────────── */
403
+
404
+ const jobById = (inst, id) => (inst.data.jobs || []).find((j) => j.id === id);
405
+ const lists = (inst) => inst.data.lists || {};
406
+ const shown = (inst, id) => (lists(inst)[id] || []).filter((i) => !inst.hidden.has(i.id) && !i.done);
407
+
408
+ /* The mockup's .c3jf: a label (.dim.mono) and an input on one line (console3.html line 4651). */
409
+ function field(label, input) { return el('label', null, el('span', { class: 'dim mono' }, label), input); }
410
+
411
+ function editField(inst, item, label, shortLabel, kind, key, value) {
412
+ const input = el(kind === 'select' ? 'select' : 'input', { 'aria-label': label, 'data-pm-edit': item.id + ':' + key, ...(kind === 'date' ? { type: 'date' } : kind === 'text' ? { type: 'text' } : {}) });
413
+ if (kind === 'select') {
414
+ const opts = key === 'job'
415
+ ? [['', COPY.noJobGroup], ...(inst.data.jobs || []).map((j) => [j.id, j.want])]
416
+ : [['', 'None'], ['P1', 'P1'], ['P2', 'P2'], ['P3', 'P3']];
417
+ opts.forEach(([v, t]) => input.appendChild(el('option', { value: v }, t)));
418
+ }
419
+ input.value = value || '';
420
+ input.addEventListener('change', async () => {
421
+ const v = input.value.trim();
422
+ const old = item[key] === undefined ? null : item[key];
423
+ const r = await press(inst, API + '/edit', { id: item.id, fields: { [key]: v === '' && key !== 'title' ? null : v } });
424
+ if (r) say({ text: 'Saved', timeout: 5000, actionLabel: COPY.undo, onAction: async () => { await press(inst, API + '/edit', { id: item.id, fields: { [key]: old } }); } });
425
+ });
426
+ return field(shortLabel, input);
427
+ }
428
+
429
+ /* The mockup's c3stepsHtml(): .c3steps > .c3sh + ul > li (.c3sg, .c3stt, .chips2 with the who pill) + an add input (line 4595). */
430
+ function stepsBlock(inst, item) {
431
+ if (inst.planning.has(item.id)) return el('div', { class: 'fold a', 'data-pm-planning': item.id, role: 'status' }, c3ic('spark', 'sm'), 'The agent is reading the item and planning the steps');
432
+ const list = item.steps || [];
433
+ if (!list.length) return el('div', { class: 'chips2' }, el('button', { type: 'button', class: 'chip2 lk', 'data-pm-plan': item.id, onclick: () => planSteps(inst, item) }, c3ic('spark', 'sm'), COPY.plan));
434
+ const input = el('input', { type: 'text', placeholder: COPY.addStep, 'aria-label': COPY.addStep, 'data-pm-step-add': item.id });
435
+ input.addEventListener('keydown', async (e) => {
436
+ if (e.key !== 'Enter' || !input.value.trim()) return;
437
+ if (await press(inst, API + '/steps/add', { id: item.id, text: input.value.trim() })) input.value = '';
438
+ });
439
+ const rows = list.map((st) => {
440
+ const outward = OUTWARD.test(st.text);
441
+ const who = el('button', { type: 'button', class: 'pill', 'data-who': st.who, 'data-pm-who': st.id, title: outward ? COPY.whoOutward : 'Who does this step. Tap to change.' }, COPY.who[st.who] || st.who);
442
+ who.addEventListener('click', () => cycleWho(inst, item, st));
443
+ return el('li', { class: st.done ? 'ok' : outward ? 'warn' : '', 'data-pm-step': st.id },
444
+ el('span', { class: 'c3sg' }, c3ic(st.done ? 'check' : st.who === 'you' ? 'user' : 'spark', 'sm')),
445
+ el('span', { class: 'c3stt' + (st.done ? ' dn' : '') }, st.text, outward && !st.done ? [' ', el('span', { class: 'tag' }, 'needs your slide')] : null),
446
+ el('span', { class: 'chips2' }, who));
447
+ });
448
+ const dn = list.filter((s) => s.done).length;
449
+ return el('div', { class: 'c3steps', 'data-pm-steps': item.id },
450
+ el('div', { class: 'c3sh' }, el('h4', null, c3ic('tree', 'sm'), COPY.steps), el('span', { class: 'dim mono' }, COPY.progress(dn, list.length))),
451
+ el('ul', null, rows),
452
+ el('div', { class: 'form' }, input));
453
+ }
454
+
455
+ /* ── the case model of one item (console3.html genTask line 3216, agDecor line 5126) ─────────────────────────────────────────── */
456
+
457
+ const OUT_REASON = 'No channel connected, so this stays a draft. Nothing is sent.';
458
+ const fx = (...a) => a;
459
+
460
+ /** The options of an item by its list. Each: { id, l, rec, fx, outward, run(inst, item, row) } where run does the work through a press route. */
461
+ function lensOptions(inst, item, lens) {
462
+ const mock = item.mock_row === true; const q = mock && item.mins <= 2; const who = item.waiting_on || 'them';
463
+ const today = inst.data.today; const mon = nextMonday(today);
464
+ const o = (id, l, rec, f, run, extra) => ({ id, l, rec: !!rec, fx: f, run, ...(extra || {}) });
465
+ const nowO = (rec) => o('now', q ? 'Do it now: the agent drafts, you send' : (mock ? 'Do it now' : 'Mark it done'), rec, q ? [fx('bolt', 'Under 2 minutes', 'ok')] : [fx('clock', mock && item.mins ? 'Takes ' + item.mins + ' min' : 'Closes the item', mock ? 'warn' : 'ok')], (r) => doneAct(inst, item, r, q ? 'Done. Anything outward stays a draft for you to send.' : 'Done.'));
466
+ const todayO = (rec) => o('today', mock ? (q ? 'Put it in Today' : 'Put it in Today and block time') : 'Put it in Today', rec, mock && !q ? [fx('check', 'Gets a slot', 'ok')] : [fx('check', 'Moves to your Today list', 'ok')], (r) => moveAct(inst, item, r, 'today', 'In Today.'));
467
+ const schedO = () => o('sched', 'Schedule for ' + dayLabel(mon), 0, [fx('cal', 'Out of today', 'ok')], (r) => dueAct(inst, item, r, mon, 'Scheduled for ' + dayLabel(mon) + '.'));
468
+ const dropO = () => o('drop', 'Drop it', 0, [fx('x', 'Gone, with Undo', 'warn')], (r) => dropAct(inst, item, r));
469
+ if (lens === 'waiting') {
470
+ return { conf: 80, cta: 'Slide to nudge', sit: 'Waiting on ' + who + '. ' + (item.why || 'No reply yet.'), opts: [
471
+ o('nudge', 'Nudge ' + who + ' now (drafted)', 1, [fx('out', 'Goes to Review first', 'ok')], null, { outward: true }),
472
+ o('esc', 'Escalate to their lead', 0, [fx('alert', 'Stronger signal', 'warn')], null, { outward: true }),
473
+ o('wait', 'Keep waiting', 0, [fx('clock', 'Ages another day', 'warn')], () => say({ text: 'Still waiting.', timeout: 2500 })),
474
+ ] };
475
+ }
476
+ if (lens === 'later') return { conf: 66, cta: 'Slide to promote', sit: 'Parked for later. Nothing is due, so it only matters if it has a date.', opts: [
477
+ o('promo', 'Promote to this week', 1, [fx('check', 'Gets a date', 'ok')], (r) => moveAct(inst, item, r, 'week', 'Moved to Week.')), dropO()] };
478
+ if (lens === 'week') return { conf: 70, cta: 'Slide to keep the date', opts: [
479
+ o('today', 'Bring it into Today', 0, [fx('clock', 'Earlier than planned', 'warn')], (r) => moveAct(inst, item, r, 'today', 'In Today.')),
480
+ o('keep', 'Keep the date', 1, [fx('check', 'On plan', 'ok')], () => say({ text: 'Kept.', timeout: 2500 })),
481
+ o('later', 'Move to Later', 0, [fx('moon', 'No date')], (r) => moveAct(inst, item, r, 'later', 'Moved to Later.'))] };
482
+ if (lens === 'today') return { conf: 82, cta: 'Slide to finish', opts: [nowO(1),
483
+ o('mig', 'Migrate to tomorrow', 0, [fx('moon', 'Back tomorrow 09:00', 'warn')], (r) => dueAct(inst, item, r, addIso(today, 1), 'Migrated to tomorrow.')), schedO()] };
484
+ return { conf: q ? 90 : 78, cta: q ? 'Slide to do it now' : 'Slide to commit', opts: q ? [nowO(1), todayO(0), schedO(), dropO()] : [todayO(1), nowO(0), schedO(), dropO()] };
485
+ }
486
+
487
+ /** { model, run } for an item: the model goes to renderCase(), `run(id, row)` does what the picked option says. */
488
+ function itemCase(inst, item, lens) {
489
+ if (item.noise) { // the mockup's genTask() for an FYI item (line 3232): the agent read it and found nothing for the PM
490
+ const o = (id, l, rec, f, run) => ({ id, l, rec: !!rec, fx: f, run });
491
+ const L = [o('arch', 'Archive it', 1, [fx('check', 'Keeps the inbox clean', 'ok')], (r) => doneAct(inst, item, r, 'Archived.')), o('keep', 'Keep it', 0, [fx('moon', 'Stays in the inbox')], () => say({ text: 'Kept.', timeout: 2500 }))];
492
+ const model = { id: 'task:' + item.id, sit: 'Noise. Nothing in it needs you, and the agent has already read it.', chg: [['clock', 'About under 2 min', 'effort']], conf: 92, cta: 'Slide to archive', unsure: '', ign: '', outboxReason: OUT_REASON,
493
+ opts: L.map((x) => ({ id: x.id, l: x.l, rec: x.rec, fx: x.fx, outward: false })), why: '' };
494
+ const run = {}; L.forEach((x) => { run[x.id] = x.run; });
495
+ return { model, run };
496
+ }
497
+ const ix = item.ix || {}; const mock = item.mock_row === true;
498
+ const j = item.job && jobById(inst, item.job);
499
+ const L = lensOptions(inst, item, lens);
500
+ const jm = j && (j.mock_name || j.want);
501
+ const evidence = ix.chg ? ix.chg.slice() : (() => {
502
+ const c = [];
503
+ if (ix.why) c.push(['info', ix.why[0], ix.why[1] || '']);
504
+ else if (item.why) c.push(['info', item.why, item.src || '']);
505
+ if (j) c.push(['target', 'Job: ' + jm, 'JTBD']);
506
+ if (mock && item.mins) c.push(['clock', 'About ' + (item.mins <= 2 ? 'under 2' : item.mins) + ' min', 'effort']);
507
+ if (item.due) c.push(['cal', 'Due ' + item.due, '']);
508
+ if (item.priority && !mock) c.push(['flag', 'Priority ' + item.priority, '']);
509
+ if (item.waiting_on) c.push(['wait', 'Waiting on ' + item.waiting_on, '']);
510
+ return c.slice(0, 3);
511
+ })();
512
+ const model = {
513
+ id: 'task:' + item.id, sit: ix.sit || (j ? COPY.jobLine(j) : (item.why || (mock ? item.title : 'No job to be done yet. File it under one below, or leave it.'))),
514
+ chg: evidence, conf: ix.conf || L.conf, cta: L.cta, unsure: ix.unsure || '', ign: ix.ign || '', outboxReason: OUT_REASON,
515
+ opts: L.opts.map((x) => ({ id: x.id, l: x.l, rec: x.rec, fx: x.fx, outward: !!x.outward })), why: '',
516
+ };
517
+ const run = {}; L.opts.forEach((x) => { if (x.run) run[x.id] = x.run; });
518
+ /* the agent's suggested move (mock rows): a recommended first option, and the run's result once it has run */
519
+ const rs = inst.runs.get(item.id);
520
+ if (mock && item.agent_move && ix.move) {
521
+ if (rs && rs.phase === 'work') { model.opts = []; model.move = ''; }
522
+ else if (rs && rs.phase === 'done') {
523
+ const res = (ix[rs.intent] && ix[rs.intent].h ? ix[rs.intent] : (ix.res || {}));
524
+ model.move = ''; model.quote = res.draft || ''; model.sit = res.h || model.sit;
525
+ model.chg = (res.claims || []).map(([label, text]) => ({ label, text }));
526
+ model.conf = 80; model.unsure = ''; model.ign = res.draft ? 'It stays a draft and the item waits.' : '';
527
+ const keep = { id: 'keep', l: res.draft ? 'Keep it as a draft' : 'Keep the result and decide later', rec: 0, fx: [fx('check', 'Nothing leaves the repo', 'ok')] };
528
+ model.opts = res.draft
529
+ ? [{ id: 'send', l: 'Send exactly this text to ' + res.out, rec: 1, outward: true, fx: [fx('lock', 'Outward: bound to this exact text', 'warn'), fx('out', 'Goes to ' + res.out, 'warn')] }, keep]
530
+ : [{ id: 'accept', l: 'Accept the result', rec: 1, fx: [fx('check', 'Recorded as your acceptance', 'ok')] }, keep];
531
+ model.cta = res.draft ? 'Slide to send' : 'Slide to accept';
532
+ run.accept = (r) => doneAct(inst, item, r, 'Accepted. Recorded as your press.');
533
+ run.keep = () => say({ text: 'Kept.', timeout: 2500 });
534
+ } else {
535
+ model.move = ix.move + ' \u00b7 swipe right, or slide below';
536
+ model.opts = [{ id: 'agmv', l: 'Let the agent act: ' + ix.move, rec: 1, fx: [fx('spark', 'The suggested move, taken by an agent', 'ok'), fx('shield', 'Inside the repo; outward only as a draft', 'ok')] }, ...model.opts.map((x) => ({ ...x, rec: 0 }))];
537
+ model.cta = 'Slide to let the agent act';
538
+ run.agmv = () => { say({ text: 'The agent is working on it. Anything outward stays a draft for your slide.', timeout: 4000 }); startRun(inst, item, 'res'); }; // the toast is the case's slide; a swipe starts the run without one, as the mockup does
539
+ }
540
+ }
541
+ return { model, run };
542
+ }
543
+
544
+ /* The agent run (mock): nothing runs. A strip walks the steps of the move, then the result is a labelled draft for the PM to read. */
545
+ function startRun(inst, item, intent) {
546
+ if (inst.runs.get(item.id) && inst.runs.get(item.id).phase === 'work') { say({ text: 'A session is already working on this.', timeout: 2500 }); return; }
547
+ const ix = item.ix || {}; const steps = (ix.steps && ix.steps.length ? ix.steps : ['Reading the item']).slice();
548
+ const st = { phase: 'work', i: 0, steps, intent };
549
+ inst.runs.set(item.id, st); inst.trays.delete(item.id); inst.opens.add(item.id);
550
+ draw(inst);
551
+ const tick = () => {
552
+ if (inst.runs.get(item.id) !== st) return;
553
+ st.i += 1;
554
+ if (st.i >= steps.length) { st.phase = 'done'; draw(inst); return; }
555
+ draw(inst); later(inst, tick, 800);
556
+ };
557
+ later(inst, tick, 800);
558
+ }
559
+
560
+
561
+ /** The case file element of an item: c3-case.js plus what is Work's own (the steps, the agent tray, the edit fields). */
562
+ function caseFile(inst, item, lens) {
563
+ const { model, run } = itemCase(inst, item, lens);
564
+ const root = renderCase(model, {
565
+ picked: inst.picks.get(item.id), outbox: false,
566
+ onPick: (id) => inst.picks.set(item.id, id),
567
+ onConfirm: ({ option }) => {
568
+ const row = inst.root.querySelector('[data-pm-item="' + item.id + '"]');
569
+ const fn = run[option.id];
570
+ if (fn) fn(row);
571
+ },
572
+ });
573
+ root.setAttribute('data-pm-case', item.id);
574
+ if (!item.mock_row) { // real items carry no confidence figure: never show a made-up percentage
575
+ root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
576
+ root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
577
+ }
578
+ const extras = [];
579
+ const rs = inst.runs.get(item.id);
580
+ const agent = item.mock_row && item.agent_move && (item.ix || {}).move;
581
+ if (rs && rs.phase === 'work') {
582
+ extras.push(el('div', { class: 'c3ag', 'data-ag': item.id }, el('div', { class: 'fold a', role: 'status' }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), rs.steps[Math.min(rs.i, rs.steps.length - 1)] + ' · ' + (Math.min(rs.i, rs.steps.length - 1) + 1) + ' of ' + rs.steps.length,
583
+ el('button', { type: 'button', class: 'chip2 lk', 'data-pm-agent-stop': item.id, onclick: () => { inst.runs.delete(item.id); say({ text: 'Stopped. Nothing was kept.', timeout: 2500 }); draw(inst); } }, c3ic('stop', 'sm'), 'stop'))));
584
+ } else if (rs && rs.phase === 'done') {
585
+ extras.push(el('div', { class: 'chips2' }, el('button', { type: 'button', class: 'chip2 lk', 'data-pm-agent-discard': item.id, onclick: () => { inst.runs.delete(item.id); inst.picks.delete(item.id); draw(inst); } }, c3ic('undo', 'sm'), 'discard the run')));
586
+ }
587
+ if (inst.trays.has(item.id) && agent && !rs) {
588
+ const ch = (label, intent) => el('button', { type: 'button', class: 'pill', 'data-pm-agent-chip': intent, onclick: () => { inst.trays.delete(item.id); startRun(inst, item, intent); } }, label);
589
+ const input = el('input', { type: 'text', placeholder: 'Reply to the agent…', 'aria-label': 'Reply to the agent', autocomplete: 'off', 'data-pm-agent-reply': item.id });
590
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && input.value.trim()) { inst.trays.delete(item.id); startRun(inst, item, 'res'); } });
591
+ extras.push(el('div', { class: 'chips2', 'data-pm-tray': item.id }, ch('Ask for evidence', 'ev'), ch('Show other options', 'res'), el('span', { class: 'form', style: 'flex:1;min-width:160px' }, input),
592
+ c3ib('Speak is off on this line: nothing on this page records. Type the reply here.', 'mic', { 'data-off': '', onclick: () => say({ text: 'The microphone needs the app. Nothing was recorded: type the reply here.', timeout: 4200 }) }, 'sm'),
593
+ c3ib('Close the reply', 'x', { onClick: () => { inst.trays.delete(item.id); draw(inst); } }, 'sm')));
594
+ }
595
+ const showSteps = !(rs && rs.phase) && (inst.planning.has(item.id) || (item.steps || []).length || (!agent && (!item.mock_row || item.src === 'You')));
596
+ if (showSteps) extras.push(stepsBlock(inst, item));
597
+ const anchor = root.querySelector('.c3ag') || root.querySelector('.sit');
598
+ if (anchor) anchor.after(...extras); else root.prepend(...extras);
599
+ if (item.noise) return root; // the mockup's noise case has no edit form
600
+ /* the editable fields stay in the case file behind an Edit button (title, due, priority, waiting on, job) */
601
+ const form = el('div', { class: 'c3jf', 'data-pm-edit-form': item.id, hidden: !inst.editing.has(item.id) },
602
+ editField(inst, item, 'Title', 'Title', 'text', 'title', item.title),
603
+ editField(inst, item, 'Due', 'Due', 'date', 'due', item.due),
604
+ editField(inst, item, 'Priority', 'Priority', 'select', 'priority', item.priority),
605
+ editField(inst, item, 'Waiting on', 'Waiting', 'text', 'waiting_on', item.waiting_on),
606
+ editField(inst, item, 'Job', 'Job', 'select', 'job', item.job));
607
+ const edit = el('button', { type: 'button', class: 'chip2 lk', 'aria-expanded': String(inst.editing.has(item.id)), 'data-pm-edit-toggle': item.id,
608
+ onclick: () => { const on = !inst.editing.has(item.id); if (on) inst.editing.add(item.id); else inst.editing.delete(item.id); form.hidden = !on; edit.setAttribute('aria-expanded', String(on)); } }, c3ic('edit', 'sm'), 'Edit details');
609
+ const ft = root.querySelector('.cft');
610
+ if (ft) ft.append(edit); else root.append(el('div', { class: 'cft' }, edit));
611
+ root.append(form);
612
+ return root;
613
+ }
614
+
615
+ /* ── swipe: the mockup's workSwipe (console3.html line 3250) and the agent's swapped glyphs (line 5136) ─────────────────────── */
616
+
617
+ function swipeSpec(inst, item, lens) {
618
+ if (item.noise) { const a = { label: 'Archive', icon: 'check', fn: (r) => doneAct(inst, item, r, 'Archived.') }; return { right: a, left: { ...a, icon: 'moon' } }; } // the mockup archives a noise row either way
619
+ const mock = item.mock_row === true; const today = inst.data.today;
620
+ if (mock && item.agent_move && (item.ix || {}).move) {
621
+ return {
622
+ right: { label: 'Agent acts', icon: 'spark', fn: () => startRun(inst, item, 'res') },
623
+ left: { label: 'Reply', icon: 'reply', fn: () => { if (inst.runs.get(item.id) && inst.runs.get(item.id).phase === 'work') { say({ text: 'A session is already working on this.', timeout: 2500 }); return; } inst.trays.add(item.id); inst.opens.add(item.id); draw(inst); later(inst, () => { const i = inst.root.querySelector('[data-pm-agent-reply="' + item.id + '"]'); if (i) i.focus(); }, 30); } },
624
+ };
625
+ }
626
+ const q = mock && item.mins <= 2;
627
+ if (lens === 'triage') return {
628
+ right: q ? { label: 'Done', icon: 'check', fn: (r) => doneAct(inst, item, r, 'Done.') } : { label: 'In Today', icon: 'sun', fn: (r) => moveAct(inst, item, r, 'today', 'In Today.') },
629
+ left: { label: 'Later', icon: 'moon', fn: (r) => parkAct(inst, item, r, 'Moved to Later.') } };
630
+ if (lens === 'today') return { right: { label: 'Done', icon: 'check', fn: (r) => doneAct(inst, item, r, 'Done.') }, left: { label: 'Tomorrow', icon: 'moon', fn: (r) => dueAct(inst, item, r, addIso(today, 1), 'Migrated to tomorrow.') } };
631
+ if (lens === 'waiting') return {
632
+ right: { label: 'Nudge', icon: 'out', fn: () => say({ text: 'No channel connected: a nudge to ' + (item.waiting_on || 'them') + ' is not sent.', timeout: 4000 }) },
633
+ left: { label: 'Keep waiting', icon: 'wait', fn: () => say({ text: 'Still waiting.', timeout: 2500 }) } };
634
+ if (lens === 'week') return { right: { label: 'In Today', icon: 'sun', fn: (r) => moveAct(inst, item, r, 'today', 'In Today.') }, left: { label: 'Later', icon: 'moon', fn: (r) => parkAct(inst, item, r, 'Moved to Later.') } };
635
+ return { right: { label: 'To Week', icon: 'cal', fn: (r) => moveAct(inst, item, r, 'week', 'Promoted to Week.') }, left: { label: 'Drop', icon: 'trash', fn: (r) => dropAct(inst, item, r) } };
636
+ }
637
+
638
+ /* Open or close a case file in place with c3-expand.js, so the mockup's .xp height transition (grid-template-rows) plays. */
639
+ function expandRow(inst, row, key, build, onClose) {
640
+ const xp = makeXp();
641
+ const xi = xp.firstChild;
642
+ attachExpand(row, xp, {
643
+ key, initial: inst.opens.has(key),
644
+ onToggle: (on) => {
645
+ if (on) {
646
+ inst.opens.add(key);
647
+ const c = build(); inst.cases.add(c); xi.replaceChildren(c);
648
+ } else {
649
+ inst.opens.delete(key);
650
+ later(inst, () => { if (!xp.classList.contains('open')) { xi.querySelectorAll('.case').forEach((c) => { if (c.c3case) { c.c3case.dispose(); inst.cases.delete(c); } }); xi.replaceChildren(); } }, 340);
651
+ if (onClose) onClose();
652
+ }
653
+ },
654
+ });
655
+ return xp;
656
+ }
657
+
658
+ const KIND_ICON = { reply: 'reply', decide: 'scale', review: 'eye', do: 'check' }; // the mockup's KIC
659
+ /* The mockup's time-left bar (.fuse) with its text, for an item that carries `left` ({ text, pct, tone }). */
660
+ const fuseBar = (l) => { const c = l.tone === 'warn' ? 'var(--warning)' : 'var(--acc)'; return el('div', { class: 'chips2', style: 'margin-top:6px;gap:8px' }, el('div', { class: 'fuse' }, el('i', { style: 'width:' + Math.round(l.pct * 100) + '%;background:' + c })), el('span', { class: 'mono', style: 'font-size:var(--fs-1);color:' + c }, l.text)); };
661
+
662
+ /* The mockup's workRow(): li.nrow.wrow > .rv.l, .rv.r (made by the swipe), .ki, .nb (.tt, .chips, .xp > .xi), and the .sg glyphs (line 3258). */
663
+ function itemRow(inst, item, lens) {
664
+ const j = item.job && jobById(inst, item.job);
665
+ const chips = [];
666
+ /* An item with `mock_row` (the /c3.html demo mode's mockup rows) is drawn as the mockup draws a work row: agent move, effort, time, job, source, initiative. */
667
+ const mock = item.mock_row === true;
668
+ const rs = inst.runs.get(item.id);
669
+ const agent = mock && item.agent_move && (item.ix || {}).move;
670
+ if (rs && rs.phase === 'work') chips.push(el('span', { class: 'st c3strip', 'data-sxs': item.id }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), rs.steps[Math.min(rs.i, rs.steps.length - 1)] + ' · ' + (Math.min(rs.i, rs.steps.length - 1) + 1) + ' of ' + rs.steps.length));
671
+ else if (rs && rs.phase === 'done') chips.push(el('span', { class: 'st ok' }, c3ic('check', 'sm'), 'agent: result ready'));
672
+ else if (inst.planning.has(item.id)) chips.push(el('span', { class: 'st' }, el('span', { class: 'c3spin' }, c3ic('spark', 'sm')), 'planning the steps'));
673
+ if (mock && item.agent_move && !(rs && rs.phase === 'done')) chips.push(el('span', { class: 'chip2', style: 'gap:4px', title: 'Swipe right to let the agent act' }, c3ic('spark', 'sm'), 'agent move'));
674
+ if (mock && item.mins) chips.push(el('span', { class: 'chip2', title: 'Effort' }, c3ic('clock', 'sm'), item.mins <= 2 ? 'under 2 min' : item.mins + ' min'));
675
+ if (mock && item.time) chips.push(el('span', { class: 'chip2' }, c3ic('cal', 'sm'), item.time));
676
+ if (item.due && !mock) chips.push(el('span', { class: 'chip2' }, c3ic('cal', 'sm'), item.due));
677
+ if (item.priority && !mock) chips.push(el('span', { class: 'fl ' + item.priority.toLowerCase() }, item.priority));
678
+ if (item.waiting_on) chips.push(el('span', { class: 'chip2' }, c3ic('wait', 'sm'), COPY.waitingOn(item.waiting_on)));
679
+ if ((item.steps || []).length) { const dn = item.steps.filter((s) => s.done).length; const ag = item.steps.filter((s) => s.who !== 'you' && !s.done).length; chips.push(el('span', { class: 'chip2', title: 'Steps' }, c3ic('tree', 'sm'), dn + ' of ' + item.steps.length + ' steps' + (ag ? ' · agent can do ' + ag : ''))); }
680
+ if (j) chips.push(el('span', { class: 'chip2 jc', title: j.want }, c3ic('target', 'sm'), mock && j.mock_name ? j.mock_name : j.want));
681
+ if (mock && item.src) chips.push(el('span', { class: 'tag' }, item.src));
682
+ if (mock && item.init) chips.push(el('span', { class: 'chip2' }, c3ic('init', 'sm'), item.init.length > 16 ? item.init.slice(0, 15) + '…' : item.init));
683
+ const title = el('span', { class: 'tt', 'data-pm-title': item.id }, item.title);
684
+ const spec = swipeSpec(inst, item, lens);
685
+ const recL = itemCase(inst, item, lens).model.opts.find((o) => o.rec);
686
+ const recSide = agent ? 'r' : (recL && /^(drop|arch|later|mig|wait|keep)/.test(recL.id) ? 'l' : 'r');
687
+ const row = el('li', { class: 'nrow wrow', 'data-row': '', 'data-id': item.id, 'data-w': '1', 'data-pm-item': item.id, 'data-lens': lens },
688
+ el('span', { class: 'ki' + (item.priority === 'P1' ? ' hot' : ''), title: item.priority || item.kind || '', style: mock && item.mins <= 2 && item.kind !== 'fyi' ? 'background:var(--accw);color:var(--acc)' : null },
689
+ c3ic(mock ? (item.mins <= 2 && item.kind !== 'fyi' ? 'bolt' : (KIND_ICON[item.kind] || (item.kind === 'fyi' ? 'info' : 'check'))) : item.priority === 'P1' ? 'alert' : 'todo')),
690
+ el('div', { class: 'nb' }, title, mock && item.left ? fuseBar(item.left) : null, chips.length ? el('div', { class: 'chips' }, chips) : null),
691
+ el('span', { class: 'sg', title: agent ? 'Drag right: the agent acts. Drag left: reply.' : 'Drag right: ' + spec.right.label + '. Drag left: ' + spec.left.label + '.' },
692
+ el('span', { class: 'g l' + (recSide === 'l' ? ' y' : '') }, c3ic(spec.left.icon)), el('span', { class: 'g r' + (recSide === 'r' ? ' y' : '') }, c3ic(spec.right.icon))));
693
+ const xp = expandRow(inst, row, item.id, () => caseFile(inst, item, lens));
694
+ row.querySelector('.nb').append(xp);
695
+ row.__item = item;
696
+ attachSwipe(row, {
697
+ onRight: () => spec.right.fn(row), onLeft: () => spec.left.fn(row),
698
+ labelRight: spec.right.label, labelLeft: spec.left.label, iconRight: spec.right.icon, iconLeft: spec.left.icon,
699
+ });
700
+ row.addEventListener('keydown', (e) => {
701
+ if (e.target !== row || e.metaKey || e.ctrlKey || e.altKey) return;
702
+ /* the mockup's e and n keys act on the Briefing's rows only (its key handler tests S.tab === 'brief'); on a Work row they do nothing. j / k move. */
703
+ if (e.key === 'j' || e.key === 'k') {
704
+ e.preventDefault();
705
+ const all = [...inst.root.querySelectorAll('.nrow.wrow[data-pm-item]')]; const at = all.indexOf(row);
706
+ const nx = all[at + (e.key === 'j' ? 1 : -1)]; if (nx) nx.focus();
707
+ }
708
+ });
709
+ return row;
710
+ }
711
+
712
+ /* ── jobs ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
713
+
714
+ function jobEditField(inst, job, key) {
715
+ const lab = key === 'mock_name' ? 'Job' : key === 'when' ? 'When' : key === 'want' ? 'I want' : 'So';
716
+ const input = el('input', { type: 'text', 'aria-label': lab, 'data-pm-job-edit': job.id + ':' + key, autocomplete: 'off' });
717
+ input.value = job[key] || '';
718
+ input.addEventListener('change', async () => {
719
+ const v = input.value.trim(); if (!v) { input.value = job[key] || ''; return; }
720
+ const old = job[key] || '';
721
+ const r = await press(inst, JOBS + '/edit', { id: job.id, fields: { [key]: v } });
722
+ if (r) say({ text: 'Saved', timeout: 7000, actionLabel: COPY.undo, onAction: async () => { await press(inst, JOBS + '/edit', { id: job.id, fields: { [key]: old } }); } });
723
+ });
724
+ return field(lab, input);
725
+ }
726
+
727
+ /* The mockup's c3jall(): "All N items, across every list", grouped by list (line 4657). */
728
+ function jobAll(inst, job) {
729
+ const fx2 = DEMO() && window.__c3work2 && window.__c3work2.jobAll && window.__c3work2.jobAll[job.id]; // demo: the mockup's own items of the job, grouped by list
730
+ if (fx2) {
731
+ const n = fx2.reduce((a, g) => a + g[1].length, 0);
732
+ return el('div', { class: 'c3all', 'data-pm-job-all': job.id }, el('div', { class: 'hd dim mono' }, 'All ' + n + ' items, across every list'),
733
+ ...fx2.map(([tag, titles, dn]) => el('div', null, el('span', { class: 'tag' }, tag), el('span', null, ...titles.map((t) => el('span', { class: dn ? 'dn' : '' }, t))))));
734
+ }
735
+ const its = (inst.data.items || []).filter((i) => i.job === job.id);
736
+ if (!its.length) return el('div', { class: 'dim mono', style: small }, 'No item uses this job yet.');
737
+ const G = {};
738
+ its.forEach((i) => { const l = i.done ? 'done' : lensOfItem(inst, i.id); (G[l] = G[l] || []).push(i); });
739
+ return el('div', { class: 'c3all', 'data-pm-job-all': job.id }, el('div', { class: 'hd dim mono' }, 'All ' + its.length + ' items, across every list'),
740
+ ...[...LENS_IDS, 'done'].filter((l) => G[l]).map((l) => el('div', null, el('span', { class: 'tag' }, l === 'done' ? 'Done' : labelOf(l)), el('span', null, ...G[l].map((i) => el('span', { class: i.done ? 'dn' : '' }, i.title))))));
741
+ }
742
+
743
+ /* The job's case file (mockup C3.cases.job, line 4660): the words, the count, Keep / Delete and move / Delete and leave, slide to confirm. */
744
+ function jobCase(inst, j) {
745
+ const its = (inst.data.items || []).filter((i) => i.job === j.id);
746
+ const done = its.filter((i) => i.done).length;
747
+ const mk = !!j.mock_name; // demo: the mockup's own numbers, wording and Recommended badge
748
+ const others = (inst.data.jobs || []).filter((q) => q.id !== j.id); const near = others[0];
749
+ const nm = (q) => q.mock_name || q.want;
750
+ const del = (reassign) => async () => { const r = await press(inst, JOBS + '/delete', { id: j.id, ...(reassign ? { reassign_to: reassign } : {}) }); if (r) say({ text: COPY.reassigned(r.reassigned || 0), timeout: 4000 }); };
751
+ const model = { id: 'job:' + j.id, sit: COPY.jobLine(j), conf: 90, cta: mk ? 'Slide to delete' : 'Slide to keep it',
752
+ chg: [['target', mk ? (j.done + ' of ' + j.total + ' done') : (done + ' of ' + its.length + ' done'), 'count across every list', 'ok'], ['info', 'Change the words below. They save at once and you can undo for 7 seconds.', '']],
753
+ opts: [{ id: 'keep', l: 'Keep it', rec: 1, fx: [fx('check', 'Nothing changes', 'ok')] },
754
+ { id: 'mv', l: near ? 'Delete it and move its ' + (mk ? j.total : its.length) + ' item' + ((mk ? j.total : its.length) === 1 ? '' : 's') + ' to "' + nm(near) + '"' : 'Delete it', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('target', (mk ? j.total : its.length) + ' items change job', 'warn')] },
755
+ { id: 'nj', l: 'Delete it and leave its items without a job', rec: 0, fx: [fx('trash', 'The job is gone', 'warn'), fx('alert', mk ? 'The agent will suggest a job again' : 'The items have no job', '')] }] };
756
+ const run = { keep: () => say({ text: 'Kept.', timeout: 2500 }), mv: del(near && near.id), nj: del(null) };
757
+ const root = renderCase(model, { onConfirm: ({ option }) => { if (run[option.id]) run[option.id](); } });
758
+ root.setAttribute('data-pm-case', j.id);
759
+ if (!mk) { // a real job carries no confidence figure: never show a made-up percentage
760
+ root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
761
+ root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
762
+ }
763
+ const sit = root.querySelector('.sit');
764
+ sit.after(el('div', { class: 'c3jf' }, mk ? jobEditField(inst, j, 'mock_name') : null, jobEditField(inst, j, 'when'), jobEditField(inst, j, 'want'), jobEditField(inst, j, 'so')), jobAll(inst, j));
765
+ return root;
766
+ }
767
+
768
+ /* The "+ New job" draft (mockup c3jdraft, line 4678): one line in, the agent-style draft of the four fields out, then "Slide to create". */
769
+ function jobNewCase(inst) {
770
+ const x = inst.jobNew.line || 'keep the week calm';
771
+ const d = inst.jobNew.draft;
772
+ const inputs = ['when', 'want', 'so'].map((k) => el('input', { type: 'text', 'aria-label': k === 'when' ? 'When' : k === 'want' ? 'I want' : 'So', 'data-pm-job-new': k, value: d[k], autocomplete: 'off' }));
773
+ const val = (k) => inputs[['when', 'want', 'so'].indexOf(k)].value.trim();
774
+ const demo = DEMO(); // demo: the mockup's draft also names the job (its "Job" field); the server has no name field
775
+ const nameIn = demo ? el('input', { type: 'text', 'aria-label': 'Job', 'data-pm-job-new': 'name', value: x.charAt(0).toUpperCase() + x.slice(1).replace(/\.$/, ''), autocomplete: 'off' }) : null;
776
+ const model = { id: 'job:new', sit: 'The agent drafted this from your line. Change anything, then create it. It has no items yet.', conf: 70, cta: 'Slide to create',
777
+ chg: [['spark', 'Drafted by the agent', 'assumption', 'warn']],
778
+ opts: [{ id: 'mk', l: 'Create the job', rec: 1, fx: [fx('target', 'Items can now be filed under it', 'ok')] }, { id: 'no', l: 'Not now', rec: 0, fx: [] }] };
779
+ const root = renderCase(model, { onConfirm: async ({ option }) => {
780
+ if (option.id === 'no') { inst.jobNew = null; say({ text: 'Dropped the draft.', timeout: 2500 }); draw(inst); return; }
781
+ const line = 'when ' + val('when') + ', I want ' + val('want') + ', so ' + val('so');
782
+ const r = await press(inst, JOBS + '/add', nameIn ? { line, name: nameIn.value.trim() } : { line });
783
+ if (r) { inst.jobNew = null; say({ text: 'Job created. Add work to it by saying which job it serves.', timeout: 3000 }); await load(inst); }
784
+ } });
785
+ if (!demo) {
786
+ root.querySelectorAll('.opt .rk').forEach((n) => { n.replaceChildren(c3ic('spark', 'sm'), 'Suggested'); n.removeAttribute('title'); });
787
+ root.querySelectorAll('.opt .conf').forEach((n) => n.remove());
788
+ }
789
+ root.querySelector('.sit').after(el('div', { class: 'c3jf' }, nameIn ? field('Job', nameIn) : null, ...['when', 'want', 'so'].map((k, i) => field(k === 'when' ? 'When' : k === 'want' ? 'I want' : 'So', inputs[i]))));
790
+ inst.cases.add(root);
791
+ return root;
792
+ }
793
+
794
+ /* The mockup's jobsCard() (line 4667): section.card.sect, .sh2 with the title and the + at the right, a list of .nrow with a .pbar and a case file. */
795
+ function jobsCard(inst) {
796
+ const js = inst.data.jobs || [];
797
+ const rows = js.map((j) => {
798
+ const pct = j.total ? Math.round((j.done / j.total) * 100) : 0;
799
+ const row = el('li', { class: 'nrow', 'data-row': '', 'data-pm-job': j.id },
800
+ el('span', { class: 'ki' }, c3ic('target')),
801
+ el('div', { class: 'nb' }, el('span', { class: 'tt', title: COPY.jobLine(j) }, j.mock_name || j.want.charAt(0).toUpperCase() + j.want.slice(1)),
802
+ el('div', { class: 'chips' }, el('div', { class: 'pbar', style: 'width:90px' }, el('i', { style: 'width:' + pct + '%' })),
803
+ el('span', { class: 'mono dim', style: small }, COPY.progress(j.done, j.total)))));
804
+ row.querySelector('.nb').append(expandRow(inst, row, j.id, () => jobCase(inst, j)));
805
+ return row;
806
+ });
807
+ const lines = [];
808
+ if (inst.jobNew) {
809
+ if (inst.jobNew.draft) lines.push(el('li', { class: 'c3jnew' }, jobNewCase(inst)));
810
+ else {
811
+ const input = el('input', { type: 'text', placeholder: 'What are you trying to get done? One line', 'aria-label': 'New job', autocomplete: 'off', 'data-pm-add': JOBS + '/add' });
812
+ const go = () => { const x = (input.value || '').trim() || 'keep the week calm'; inst.jobNew = { line: x, draft: { when: 'it comes up', want: 'a clear way to ' + x.toLowerCase().replace(/^to /, '').replace(/\.$/, ''), so: 'the outcome is not left to chance' } }; draw(inst); later(inst, () => { const i = inst.root.querySelector('[data-pm-job-new="name"], [data-pm-job-new="when"]'); if (i) i.focus(); }, 30); };
813
+ input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.isComposing) go(); });
814
+ lines.push(el('li', { class: 'c3jnew' }, el('div', { class: 'form' }, input, c3ib('Draft it', 'spark', { 'data-pm-job-draft': '', onClick: go }, 'pri'), c3ib('Cancel', 'x', { 'data-pm-job-cancel': '', onClick: () => { inst.jobNew = null; draw(inst); } }, 'sm'))));
815
+ }
816
+ }
817
+ const plus = c3ib('New job', 'plus', { 'aria-expanded': String(!!inst.jobNew), 'data-pm-job-add-toggle': '', onClick: () => { inst.jobNew = inst.jobNew ? null : {}; draw(inst); later(inst, () => { const i = inst.root.querySelector('[aria-label="New job"]'); if (i) i.focus(); }, 30); } }, 'sm');
818
+ return el('section', { class: 'card sect', 'data-pm-jobs': '' },
819
+ el('div', { class: 'sh2' }, el('h2', null, c3ic('target', 'sm'), COPY.jobs), el('span', { class: 'r' }, plus)), // no count: the mockup's title has none
820
+ js.length || lines.length ? el('ul', { class: 'list', role: 'list' }, ...lines, ...rows) : el('div', { class: 'dim mono', style: small }, COPY.noJobs),
821
+ el('div', { class: 'dim', style: small }, COPY.jobsNote));
822
+ }
823
+
824
+ /* ── the page ────────────────────────────────────────────────────────────────────────────────────────────────────── */
825
+
826
+ /* The mockup's workHead() calbar: the .seg lens bar with count badges, and the group toggle as an .ib (line 3281). */
827
+ function lensBar(inst) {
828
+ const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Work lens', 'data-pm-lenses': '' },
829
+ LENSES.map(([id, label]) => {
830
+ const n = inst.data ? shown(inst, id).length : 0;
831
+ const b = el('button', { type: 'button', 'aria-pressed': String(inst.lens === id), 'data-lens': id }, c3ic(WLI[id], 'sm'), label, n ? el('span', { class: 'bdg g' }, String(n)) : null);
832
+ b.addEventListener('click', () => { inst.lens = id; if (inst.ctx.onLens) inst.ctx.onLens(id); draw(inst); }); // onLens: the page's deck follows the lens (demo)
833
+ return b;
834
+ }));
835
+ const grp = c3ib(inst.group ? COPY.groupTime : COPY.groupJob, 'target', { 'aria-pressed': String(inst.group), 'data-pm-group': '' }, 'sm');
836
+ grp.addEventListener('click', () => { inst.group = !inst.group; draw(inst); });
837
+ return [seg, el('div', { class: 'chips2' }, grp)];
838
+ }
839
+
840
+ /* The ring at the right of the lens header: done of total, over the items in the lens (the lists keep done items; the rows hide them). */
841
+ function lensRing(inst) {
842
+ const all = lists(inst)[inst.lens] || [];
843
+ return c3ring(all.filter((i) => i.done).length, all.length, 44);
844
+ }
845
+
846
+ /* A .card.sect with an .sh2 title row and a ul.list of rows (line 3262). */
847
+ function sect(head, rows) { return el('section', { class: 'card sect' }, head, rows); }
848
+
849
+ const FOCUS_ATTRS = ['data-pm-edit', 'data-pm-step-add', 'data-pm-job-edit', 'data-pm-agent-reply', 'data-pm-job-new', 'aria-label'];
850
+
851
+ function draw(inst) {
852
+ /* keep the focus: a field the PM is typing in, or the row they were on, is found again after the redraw */
853
+ const a = document.activeElement; let keep = null;
854
+ if (a && inst.root.contains(a) && a !== document.body) {
855
+ const at = FOCUS_ATTRS.slice(0, 5).find((k) => a.hasAttribute && a.hasAttribute(k));
856
+ if (at) keep = { sel: '[' + at + '="' + a.getAttribute(at) + '"]' };
857
+ else if (a.matches && a.matches('.nrow[data-pm-item]')) keep = { sel: '[data-pm-item="' + a.getAttribute('data-pm-item') + '"]' };
858
+ }
859
+ disposeCases(inst);
860
+ inst.bar.replaceChildren(...lensBar(inst));
861
+ const d = inst.data;
862
+ if (!d) { inst.body.replaceChildren(el('p', { class: 'dim mono', style: small }, COPY.loading)); return; }
863
+ const list = shown(inst, inst.lens);
864
+ const lh = el('div', { class: 'lh' },
865
+ el('span', { class: 'ki' }, c3ic(WLI[inst.lens])),
866
+ el('div', { style: 'min-width:0' }, el('b', null, (d.lens_text && d.lens_text[inst.lens] ? d.lens_text[inst.lens][0] : COPY.lens[inst.lens](list.length))), el('div', { class: 'dim' }, (d.lens_text && d.lens_text[inst.lens] ? d.lens_text[inst.lens][1] : COPY.lensSub[inst.lens]))),
867
+ lensRing(inst));
868
+ const empty = el('li', { class: 'fold' }, c3ic('check', 'sm'), COPY.empty);
869
+ let main;
870
+ if (inst.group) {
871
+ const groups = (d.jobs || []).map((j) => [j, list.filter((i) => i.job === j.id)])
872
+ .concat([[null, list.filter((i) => !i.job || !jobById(inst, i.job))]]).filter((g) => g[1].length);
873
+ main = groups.length ? groups.map(([j, its]) => {
874
+ const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic('target', 'sm'), j ? (j.mock_name || j.want) : COPY.noJobGroup),
875
+ j ? el('span', { class: 'chips2 dim mono', style: small }, COPY.progress(j.done, j.total)) : null),
876
+ el('ul', { class: 'list', role: 'list' }, its.map((i) => itemRow(inst, i, inst.lens))));
877
+ sec.setAttribute('data-pm-group-card', j ? j.id : '');
878
+ return sec;
879
+ }) : [sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(WLI[inst.lens], 'sm'), labelOf(inst.lens))), el('ul', { class: 'list' }, empty))];
880
+ } else {
881
+ const sec = sect(el('div', { class: 'sh2' }, el('h2', null, c3ic(WLI[inst.lens], 'sm'), labelOf(inst.lens), ' ', el('span', { class: 'bdg g' }, String(list.length))),
882
+ el('span', { class: 'fl2', 'data-pm-legend': '' }, COPY.legend.map((w, k) => el('span', null, ['• ', '× ', '> ', '< '][k] + w)))),
883
+ el('ul', { class: 'list', role: 'list' }, list.length ? list.map((i) => itemRow(inst, i, inst.lens)) : empty));
884
+ sec.setAttribute('data-pm-list', inst.lens);
885
+ main = [sec];
886
+ }
887
+ if (inst.lens === 'triage' && Array.isArray(d.noise) && d.noise.length) { // optional (demo): the mockup's "Noise: kept out of your list". Each row is a work row whose case says why the agent filtered it out (console3.html line 3270)
888
+ const nz = d.noise.map((n, i) => ({ id: n.id || 'noise-' + (n.title || i).toLowerCase().replace(/[^a-z0-9]+/g, '-'), title: n.title, src: n.src, kind: 'fyi', mins: 0, mock_row: true, noise: true, steps: [] }));
889
+ main.push(el('section', { class: 'card sect', 'data-pm-noise': '' }, el('div', { class: 'sh2' }, el('h2', null, c3ic('info', 'sm'), 'Noise ', el('span', { class: 'bdg g' }, String(nz.length))), el('span', { class: 'dim', style: small }, 'kept out of your list')),
890
+ el('ul', { class: 'list', role: 'list' }, nz.map((n) => itemRow(inst, n, 'triage')))));
891
+ }
892
+ /* The mockup's vWork() body: .calbody > .lh + .brief.sbb > .bcols > .bcol.l (the list) and .bcol.r (the jobs card). */
893
+ inst.body.replaceChildren(lh, el('div', { class: 'brief sbb' }, el('div', { class: 'bcols' }, el('div', { class: 'bcol l' }, inst.ctx.plan || null, ...main), el('div', { class: 'bcol r' }, jobsCard(inst)))));
894
+ if (keep) { const n = inst.root.querySelector(keep.sel); if (n && n.focus) { try { n.focus({ preventScroll: true }); } catch { /* not focusable */ } } }
895
+ }