@north-light/crouter 0.3.198 → 0.3.199

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 (314) hide show
  1. package/dist/api/client.d.ts +18 -18
  2. package/dist/api/client.js +29 -37
  3. package/dist/api/dto/broker-ops.d.ts +3 -1
  4. package/dist/api/dto/broker.d.ts +13 -2
  5. package/dist/api/dto/broker.js +4 -4
  6. package/dist/api/dto/human.d.ts +6 -5
  7. package/dist/api/dto/inbox.d.ts +81 -34
  8. package/dist/api/dto/nodes.d.ts +8 -2
  9. package/dist/api/dto/profiles.d.ts +23 -0
  10. package/dist/api/dto/profiles.js +2 -2
  11. package/dist/api/dto/worktree.d.ts +3 -5
  12. package/dist/api/routes.d.ts +3 -5
  13. package/dist/api/routes.js +4 -6
  14. package/dist/builtin-memory/00-runtime-base.md +6 -6
  15. package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
  16. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
  17. package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
  18. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
  19. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
  20. package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
  21. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
  22. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
  23. package/dist/builtin-memory/insights/capture.md +2 -2
  24. package/dist/builtin-memory/insights/init.md +3 -3
  25. package/dist/builtin-memory/internal/INDEX.md +1 -1
  26. package/dist/builtin-memory/internal/memory-loading.md +1 -1
  27. package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
  28. package/dist/builtin-memory/internal/plugins.md +59 -2
  29. package/dist/builtin-memory/internal/storage-tiers.md +3 -1
  30. package/dist/builtin-memory/spec/roadmap.md +2 -2
  31. package/dist/builtin-memory/testing.md +1 -1
  32. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  33. package/dist/clients/attach/render/chat-view.d.ts +26 -1
  34. package/dist/clients/attach/render/chat-view.js +61 -11
  35. package/dist/clients/attach/render/page-block.d.ts +34 -0
  36. package/dist/clients/attach/render/page-block.js +226 -0
  37. package/dist/clients/attach/session/whip.d.ts +2 -0
  38. package/dist/clients/attach/session/whip.js +2 -0
  39. package/dist/clients/attach/viewer.js +1419 -993
  40. package/dist/clients/conversation/projection.js +18 -3
  41. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  42. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
  43. package/dist/clients/inbox/controller.js +10 -16
  44. package/dist/clients/inbox/page-adapter.d.ts +1 -3
  45. package/dist/clients/inbox/page-adapter.js +3 -4
  46. package/dist/clients/inbox/resolve.d.ts +1 -1
  47. package/dist/clients/inbox/resolve.js +3 -3
  48. package/dist/clients/inbox/review/document-surface.js +3 -1
  49. package/dist/clients/inbox/review/state.d.ts +2 -1
  50. package/dist/clients/inbox/review/state.js +5 -16
  51. package/dist/clients/inbox/tui/input.js +37 -19
  52. package/dist/clients/inbox/tui/panel.js +5 -31
  53. package/dist/clients/inbox/tui/render.d.ts +11 -0
  54. package/dist/clients/inbox/tui/render.js +71 -27
  55. package/dist/clients/inbox/tui/slots.d.ts +3 -1
  56. package/dist/clients/inbox/tui/slots.js +16 -12
  57. package/dist/clients/inbox/tui/types.d.ts +2 -7
  58. package/dist/clients/inbox/tui.js +2 -2
  59. package/dist/commands/__tests__/human.test.js +71 -114
  60. package/dist/commands/__tests__/node-message.test.js +14 -0
  61. package/dist/commands/attention.js +4 -4
  62. package/dist/commands/canvas-snapshot.js +1 -1
  63. package/dist/commands/canvas.js +1 -1
  64. package/dist/commands/dashboard.js +1 -1
  65. package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
  66. package/dist/commands/human/components.js +77 -0
  67. package/dist/commands/human/feedback.d.ts +2 -0
  68. package/dist/commands/human/feedback.js +108 -0
  69. package/dist/commands/human/prompts.d.ts +13 -5
  70. package/dist/commands/human/prompts.js +160 -97
  71. package/dist/commands/human/queue.js +57 -88
  72. package/dist/commands/human/shared.d.ts +2 -12
  73. package/dist/commands/human/shared.js +17 -11
  74. package/dist/commands/human.js +29 -21
  75. package/dist/commands/memory/origin.js +2 -2
  76. package/dist/commands/node/bash.js +4 -4
  77. package/dist/commands/node/inspect.js +3 -3
  78. package/dist/commands/node/lifecycle.js +1 -1
  79. package/dist/commands/node/message.js +2 -2
  80. package/dist/commands/node-worktree.js +8 -8
  81. package/dist/commands/pkg/browse.js +1 -1
  82. package/dist/commands/pkg/plugin-manage.js +129 -10
  83. package/dist/commands/profile/delete.js +41 -15
  84. package/dist/commands/profile.js +1 -1
  85. package/dist/commands/push.js +1 -1
  86. package/dist/commands/surface/node/focus.js +29 -14
  87. package/dist/commands/surface-edit.js +3 -3
  88. package/dist/commands/surface-inbox.d.ts +2 -0
  89. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  90. package/dist/commands/surface.js +2 -1
  91. package/dist/commands/sys/doctor.js +60 -3
  92. package/dist/commands/sys/feedback.js +79 -25
  93. package/dist/commands/sys/settings.js +1 -1
  94. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  95. package/dist/core/__tests__/extension-abort.test.js +5 -2
  96. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  97. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  98. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  99. package/dist/core/__tests__/human-deliver.test.js +31 -26
  100. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  101. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  102. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  103. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  104. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  105. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  106. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  107. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  108. package/dist/core/__tests__/session-cycles.test.js +22 -0
  109. package/dist/core/__tests__/stop-guard.test.js +4 -4
  110. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  111. package/dist/core/bash-jobs.d.ts +6 -0
  112. package/dist/core/bash-jobs.js +10 -0
  113. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  114. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  115. package/dist/core/canvas/attention.js +8 -6
  116. package/dist/core/canvas/browse/model.d.ts +8 -2
  117. package/dist/core/canvas/browse/model.js +11 -4
  118. package/dist/core/canvas/canvas.d.ts +7 -0
  119. package/dist/core/canvas/canvas.js +37 -0
  120. package/dist/core/canvas/crons.d.ts +9 -0
  121. package/dist/core/canvas/crons.js +51 -0
  122. package/dist/core/canvas/node-order.d.ts +22 -1
  123. package/dist/core/canvas/node-order.js +26 -1
  124. package/dist/core/canvas/render-source.d.ts +5 -0
  125. package/dist/core/canvas/render-source.js +1 -0
  126. package/dist/core/canvas/types.d.ts +9 -3
  127. package/dist/core/command-plugins/bundle.d.ts +6 -1
  128. package/dist/core/command-plugins/bundle.js +26 -13
  129. package/dist/core/command.js +2 -1
  130. package/dist/core/config.d.ts +47 -1
  131. package/dist/core/config.js +137 -4
  132. package/dist/core/help.js +1 -1
  133. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  134. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  135. package/dist/core/human/__tests__/page.test.js +56 -98
  136. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  137. package/dist/core/human/answer-text.js +3 -3
  138. package/dist/core/human/answer.d.ts +6 -4
  139. package/dist/core/human/answer.js +4 -10
  140. package/dist/core/human/component-docs.d.ts +11 -7
  141. package/dist/core/human/component-docs.js +361 -88
  142. package/dist/core/human/convention.d.ts +17 -3
  143. package/dist/core/human/convention.js +36 -5
  144. package/dist/core/human/feedback-companion.d.ts +20 -0
  145. package/dist/core/human/feedback-companion.js +98 -0
  146. package/dist/core/human/feedback.d.ts +65 -0
  147. package/dist/core/human/feedback.js +116 -0
  148. package/dist/core/human/page-catalog.d.ts +40 -5
  149. package/dist/core/human/page-catalog.js +102 -30
  150. package/dist/core/human/page-errors.d.ts +4 -0
  151. package/dist/core/human/page-errors.js +7 -0
  152. package/dist/core/human/page-eval.d.ts +20 -0
  153. package/dist/core/human/page-eval.js +123 -0
  154. package/dist/core/human/page-schema.d.ts +23 -191
  155. package/dist/core/human/page-schema.js +60 -89
  156. package/dist/core/human/page-synth.d.ts +1 -1
  157. package/dist/core/human/page-synth.js +15 -18
  158. package/dist/core/human/page.d.ts +12 -27
  159. package/dist/core/human/page.js +142 -474
  160. package/dist/core/human/root.d.ts +6 -2
  161. package/dist/core/human/root.js +12 -4
  162. package/dist/core/human/scan.d.ts +3 -4
  163. package/dist/core/human/scan.js +16 -15
  164. package/dist/core/human/summary.d.ts +1 -2
  165. package/dist/core/human/summary.js +2 -2
  166. package/dist/core/human/tickets.d.ts +20 -14
  167. package/dist/core/human/tickets.js +91 -31
  168. package/dist/core/human/types.d.ts +10 -2
  169. package/dist/core/preview-registry.js +8 -10
  170. package/dist/core/profiles/default-binding.d.ts +4 -0
  171. package/dist/core/profiles/default-binding.js +24 -0
  172. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  173. package/dist/core/profiles/deletion-reservation.js +38 -0
  174. package/dist/core/profiles/manifest.d.ts +3 -0
  175. package/dist/core/profiles/manifest.js +12 -0
  176. package/dist/core/profiles/select.js +2 -2
  177. package/dist/core/review/realize.js +3 -3
  178. package/dist/core/review/store.d.ts +6 -0
  179. package/dist/core/review/store.js +31 -0
  180. package/dist/core/runtime/bearings.d.ts +0 -4
  181. package/dist/core/runtime/bearings.js +4 -62
  182. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  183. package/dist/core/runtime/bin-contributions.js +186 -0
  184. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  185. package/dist/core/runtime/broker/client-registry.js +13 -1
  186. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  187. package/dist/core/runtime/broker/extension-abort.js +4 -2
  188. package/dist/core/runtime/broker/fault-retry.js +4 -0
  189. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  190. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  191. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  192. package/dist/core/runtime/broker/read-ops.js +2 -2
  193. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  194. package/dist/core/runtime/broker/rebind.js +6 -1
  195. package/dist/core/runtime/broker-extension-render.js +19 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  197. package/dist/core/runtime/broker.d.ts +1 -1
  198. package/dist/core/runtime/broker.js +17 -6
  199. package/dist/core/runtime/front-door-env.d.ts +5 -0
  200. package/dist/core/runtime/front-door-env.js +16 -0
  201. package/dist/core/runtime/front-door.d.ts +1 -5
  202. package/dist/core/runtime/front-door.js +2 -5
  203. package/dist/core/runtime/host.d.ts +1 -1
  204. package/dist/core/runtime/invocation.d.ts +6 -0
  205. package/dist/core/runtime/invocation.js +10 -0
  206. package/dist/core/runtime/kickoff.js +34 -0
  207. package/dist/core/runtime/launch.d.ts +2 -6
  208. package/dist/core/runtime/node-read.js +4 -1
  209. package/dist/core/runtime/nodes.js +2 -0
  210. package/dist/core/runtime/promote.d.ts +1 -0
  211. package/dist/core/runtime/promote.js +3 -1
  212. package/dist/core/runtime/session-cycles.d.ts +6 -0
  213. package/dist/core/runtime/session-cycles.js +15 -1
  214. package/dist/core/runtime/session-visibility.d.ts +17 -8
  215. package/dist/core/runtime/session-visibility.js +19 -10
  216. package/dist/core/runtime/spawn-env.d.ts +9 -1
  217. package/dist/core/runtime/spawn-env.js +62 -2
  218. package/dist/core/runtime/stop-guard.d.ts +2 -2
  219. package/dist/core/runtime/stop-guard.js +2 -2
  220. package/dist/core/runtime/stop-signals.d.ts +1 -1
  221. package/dist/core/runtime/stop-signals.js +2 -2
  222. package/dist/core/runtime/tmux-bindings.js +1 -1
  223. package/dist/core/session-model/session-state.d.ts +4 -3
  224. package/dist/core/session-model/session-state.js +14 -1
  225. package/dist/core/user-settings.d.ts +21 -2
  226. package/dist/core/user-settings.js +32 -11
  227. package/dist/core/worktree.d.ts +15 -45
  228. package/dist/core/worktree.js +59 -261
  229. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  230. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  231. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  232. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  233. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  234. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  235. package/dist/daemon/api/handlers/human.js +24 -20
  236. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  237. package/dist/daemon/api/handlers/inbox.js +39 -115
  238. package/dist/daemon/api/handlers/profiles.js +11 -29
  239. package/dist/daemon/api/map.d.ts +6 -1
  240. package/dist/daemon/api/map.js +23 -0
  241. package/dist/daemon/api/server.js +3 -1
  242. package/dist/daemon/companion-retire.d.ts +2 -0
  243. package/dist/daemon/companion-retire.js +33 -0
  244. package/dist/daemon/cron-run.js +14 -16
  245. package/dist/daemon/human/finish.d.ts +12 -7
  246. package/dist/daemon/human/finish.js +101 -38
  247. package/dist/daemon/human/sweep.js +5 -1
  248. package/dist/daemon/profile-delete.d.ts +7 -0
  249. package/dist/daemon/profile-delete.js +306 -0
  250. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  251. package/dist/daemon/review/deliver.js +3 -3
  252. package/dist/daemon/review/finish.d.ts +0 -2
  253. package/dist/daemon/review/finish.js +1 -29
  254. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  255. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  256. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  257. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  258. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  259. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  260. package/dist/pi-extensions/canvas-stophook.js +4 -4
  261. package/dist/shared/generated-context.js +2 -2
  262. package/dist/types.d.ts +44 -2
  263. package/dist/types.js +7 -2
  264. package/package.json +4 -4
  265. package/runtime.lock.json +5 -33
  266. package/dist/builtin-memory/init.md +0 -38
  267. package/dist/builtin-memory/plan.md +0 -19
  268. package/dist/builtin-memory/spec.md +0 -19
  269. package/dist/commands/human/doc.d.ts +0 -2
  270. package/dist/commands/human/doc.js +0 -91
  271. package/dist/commands/human/html.js +0 -81
  272. package/dist/commands/human/inbox.d.ts +0 -2
  273. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  274. package/dist/core/human/__tests__/page-render.test.js +0 -66
  275. package/dist/core/human/html-markdown.d.ts +0 -3
  276. package/dist/core/human/html-markdown.js +0 -288
  277. package/dist/core/human/page-render.d.ts +0 -4
  278. package/dist/core/human/page-render.js +0 -105
  279. package/dist/pages/bundle.css +0 -1
  280. package/dist/pages/bundle.js +0 -1778
  281. package/dist/pages/comments.d.ts +0 -72
  282. package/dist/pages/comments.js +0 -176
  283. package/dist/pages/controls.d.ts +0 -98
  284. package/dist/pages/controls.js +0 -261
  285. package/dist/pages/elements/cards.d.ts +0 -28
  286. package/dist/pages/elements/cards.js +0 -915
  287. package/dist/pages/elements/chart.d.ts +0 -23
  288. package/dist/pages/elements/chart.js +0 -664
  289. package/dist/pages/elements/options.d.ts +0 -30
  290. package/dist/pages/elements/options.js +0 -742
  291. package/dist/pages/elements/pages.d.ts +0 -30
  292. package/dist/pages/elements/pages.js +0 -377
  293. package/dist/pages/elements/slot.d.ts +0 -26
  294. package/dist/pages/elements/slot.js +0 -112
  295. package/dist/pages/elements/table.d.ts +0 -36
  296. package/dist/pages/elements/table.js +0 -1043
  297. package/dist/pages/elements/text.d.ts +0 -31
  298. package/dist/pages/elements/text.js +0 -1175
  299. package/dist/pages/entry.d.ts +0 -8
  300. package/dist/pages/entry.js +0 -8
  301. package/dist/pages/host.d.ts +0 -134
  302. package/dist/pages/host.js +0 -165
  303. package/dist/pages/readonly.d.ts +0 -24
  304. package/dist/pages/readonly.js +0 -31
  305. package/dist/pages/register.d.ts +0 -2
  306. package/dist/pages/register.js +0 -3
  307. package/dist/pages/responses.d.ts +0 -37
  308. package/dist/pages/responses.js +0 -56
  309. package/dist/pages/slot-config.d.ts +0 -36
  310. package/dist/pages/slot-config.js +0 -50
  311. package/dist/pages/types.d.ts +0 -98
  312. package/dist/pages/types.js +0 -23
  313. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  314. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,915 +0,0 @@
1
- /**
2
- * `<crtr-cards>` — the choice `options` makes, when each candidate has enough body to
3
- * deserve its own surface: a meeting slot, a vendor, a search result.
4
- *
5
- * This is a port of the agent interaction kit's `cards` component, not a new design. The
6
- * selection treatment is the kit's selectable-surface recipe — a hairline surface that
7
- * answers the pointer and goes accent-toned once chosen — carrying the kit's selection
8
- * control and the kit's comment affordance. Navigation is the kit's classic list grammar
9
- * (`j`/`k` move, `x` selects, `c` comments), so a card grid, an option list and a table
10
- * are all driven the same way.
11
- *
12
- * Data arrives inline as `cards`. The element never touches the network: a page source file
13
- * is read and embedded once at submit, before this element renders.
14
- *
15
- * ## Every value here is a `--crtr-cards-*` token
16
- *
17
- * The card surface, its heading, its tag, the comment affordance and the three notice states
18
- * are all painted through `var(--crtr-cards-<part>-<knob>, <kit default>)`, so a host restyles
19
- * this element by setting properties on the document root — custom properties inherit into
20
- * shadow roots, which is the whole mechanism. The shared control, writing-box and focus
21
- * recipes carry their own element layer through the `{ scope }` each call passes, so the
22
- * card's radio reads `--crtr-cards-control-*` over the family's `--crtr-control-box-*`.
23
- *
24
- * Defaults stay inline at the rule that spends them and no `--crtr-cards-*` token is ever
25
- * declared: an unset token is what carries the kit default down the chain, which is why an
26
- * unstyled page renders exactly as it did before any of this existed.
27
- */
28
- import { adoptComments, canRecordComments, findComment, setComment } from '../comments.js';
29
- import { controlStyle, focusRingStyle, writingBoxStyle } from '../controls.js';
30
- import { respond, savedIds, savedResponse } from '../host.js';
31
- import { applyReadOnly, isReadOnly } from '../readonly.js';
32
- import { registerElement } from '../register.js';
33
- import { registerSlotResponse } from '../responses.js';
34
- import { readSlotConfig, readSlotId } from '../slot-config.js';
35
- const STYLE = `
36
- :host {
37
- display: block;
38
- /* Column count answers the element's own width, not the viewport: a slot can sit in
39
- a narrow rail on a wide screen. */
40
- container-type: inline-size;
41
- }
42
-
43
- [part~="container"] {
44
- display: flex;
45
- flex-direction: column;
46
- gap: var(--crtr-cards-container-gap, var(--crtr-space-1));
47
- }
48
-
49
- /* The question reads as the agent's own prose above its control, matching the table's
50
- caption rather than shouting over the cards it introduces. The container's gap is a
51
- hairline, so the breathing room under the prompt is the prompt's own. */
52
- [part~="label"] {
53
- margin: 0 0 var(--crtr-cards-label-gap, var(--crtr-space-2));
54
- color: var(--crtr-cards-label-text-color, var(--crtr-color-text-muted));
55
- }
56
-
57
- /* The column count is anatomy rather than paint — a container query cannot read a custom
58
- property, so the breakpoint below stays a literal. */
59
- [part~="grid"] {
60
- display: grid;
61
- grid-template-columns: minmax(0, 1fr);
62
- gap: var(--crtr-cards-grid-gap, var(--crtr-space-3));
63
- }
64
-
65
- @container (min-width: 30rem) {
66
- [part~="grid"] {
67
- grid-template-columns: repeat(2, minmax(0, 1fr));
68
- }
69
-
70
- [part~="grid"][data-columns="3"] {
71
- grid-template-columns: repeat(3, minmax(0, 1fr));
72
- }
73
- }
74
-
75
- [part~="card"] {
76
- display: flex;
77
- flex-direction: column;
78
- gap: var(--crtr-cards-card-gap, var(--crtr-space-2));
79
- padding: var(--crtr-cards-card-padding, var(--crtr-space-3));
80
- color: var(--crtr-cards-card-text-color, var(--crtr-color-text));
81
- background: var(--crtr-cards-card-fill, var(--crtr-color-surface));
82
- border: var(--crtr-cards-card-border-width, 1px)
83
- var(--crtr-cards-card-border-style, solid)
84
- var(--crtr-cards-card-border-color, color-mix(in srgb, var(--crtr-color-border) 60%, transparent));
85
- border-radius: var(--crtr-cards-card-radius, var(--crtr-radius-medium));
86
- /* The whole card is the target, not just its heading — see the click handler. */
87
- cursor: pointer;
88
- transition: background-color var(--crtr-cards-card-transition, 120ms ease),
89
- border-color var(--crtr-cards-card-transition, 120ms ease);
90
- }
91
-
92
- [part~="card"]:has([part~="control"]:disabled) {
93
- cursor: default;
94
- }
95
-
96
- [part~="card"]:hover,
97
- [part~="card"][part~="active"] {
98
- background: var(--crtr-cards-card-hover-fill, var(--crtr-color-surface-hover));
99
- border-color: var(--crtr-cards-card-hover-border-color, var(--crtr-color-border));
100
- }
101
-
102
- [part~="card"][part~="selected"] {
103
- background: var(--crtr-cards-card-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
104
- border-color: var(--crtr-cards-card-selected-border-color, color-mix(in srgb, var(--crtr-color-accent) 45%, transparent));
105
- }
106
-
107
- [part~="card"][part~="selected"]:hover,
108
- [part~="card"][part~="selected"][part~="active"] {
109
- background: var(--crtr-cards-card-selected-hover-fill, color-mix(in srgb, var(--crtr-color-accent) 14%, transparent));
110
- }
111
-
112
- [part~="card-header"] {
113
- display: flex;
114
- align-items: flex-start;
115
- gap: var(--crtr-cards-card-header-gap, var(--crtr-space-2));
116
- }
117
-
118
- /* The label carries the click target, so the whole heading toggles the native control
119
- the way the kit's Label does — and the comment button stays its own control. */
120
- [part~="card-label"] {
121
- display: flex;
122
- flex: 1;
123
- align-items: flex-start;
124
- gap: var(--crtr-cards-card-label-gap, var(--crtr-space-2));
125
- min-width: 0;
126
- font: var(--crtr-cards-card-label-font, inherit);
127
- cursor: pointer;
128
- }
129
-
130
- ${controlStyle({ scope: 'cards-control' }, '[part~="control"]')}
131
-
132
- /* Centred on the card's title line, which sets its own tighter line-height. The box size
133
- is read back through --control-box, the local the recipe above resolves it into, so a
134
- host that resizes the control keeps it centred without touching this offset. */
135
- [part~="control"] {
136
- margin-top: var(--crtr-cards-control-offset, calc((1.35em - var(--control-box)) / 2));
137
- }
138
-
139
- [part~="card-heading"] {
140
- display: grid;
141
- flex: 1;
142
- gap: var(--crtr-cards-card-heading-gap, calc(var(--crtr-space-1) / 2));
143
- min-width: 0;
144
- }
145
-
146
- [part~="card-title"] {
147
- font-weight: var(--crtr-cards-card-title-font-weight, 500);
148
- line-height: var(--crtr-cards-card-title-line-height, 1.35);
149
- }
150
-
151
- [part~="card-subtitle"] {
152
- color: var(--crtr-cards-card-subtitle-text-color, var(--crtr-color-text-muted));
153
- font-size: var(--crtr-cards-card-subtitle-font-size, 0.875em);
154
- }
155
-
156
- [part~="card-body"] {
157
- margin: var(--crtr-cards-card-body-margin, 0);
158
- color: var(--crtr-cards-card-body-text-color, var(--crtr-color-text-muted));
159
- font-size: var(--crtr-cards-card-body-font-size, 0.875em);
160
- line-height: var(--crtr-cards-card-body-line-height, 1.6);
161
- }
162
-
163
- [part~="tag"] {
164
- align-self: flex-start;
165
- padding: var(--crtr-cards-tag-padding, 0 var(--crtr-space-1));
166
- color: var(--crtr-cards-tag-text-color, var(--crtr-color-text-muted));
167
- font-size: var(--crtr-cards-tag-font-size, 0.75em);
168
- line-height: var(--crtr-cards-tag-line-height, 1.7);
169
- border: var(--crtr-cards-tag-border-width, 1px)
170
- var(--crtr-cards-tag-border-style, solid)
171
- var(--crtr-cards-tag-border-color, var(--crtr-color-border));
172
- border-radius: var(--crtr-cards-tag-radius, var(--crtr-radius-small));
173
- }
174
-
175
- [part~="card"][part~="selected"] [part~="tag"] {
176
- color: var(--crtr-cards-tag-selected-text-color, var(--crtr-color-accent));
177
- background: var(--crtr-cards-tag-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
178
- border-color: var(--crtr-cards-tag-selected-border-color, color-mix(in srgb, var(--crtr-color-accent) 45%, transparent));
179
- }
180
-
181
- /* The affordance is quiet until you reach for it, and stays visible once a note lives
182
- there — that presence is the whole "there is a comment here" signal. */
183
- [part~="comment-button"] {
184
- flex: none;
185
- display: flex;
186
- align-items: center;
187
- justify-content: center;
188
- /* One knob: the affordance is a square, so its width and height are the same value. */
189
- width: var(--crtr-cards-comment-button-size, 1.5em);
190
- height: var(--crtr-cards-comment-button-size, 1.5em);
191
- padding: var(--crtr-cards-comment-button-padding, 0);
192
- color: var(--crtr-cards-comment-button-text-color, var(--crtr-color-text-muted));
193
- background: var(--crtr-cards-comment-button-fill, none);
194
- border: var(--crtr-cards-comment-button-border-width, 1px)
195
- var(--crtr-cards-comment-button-border-style, solid)
196
- var(--crtr-cards-comment-button-border-color, transparent);
197
- border-radius: var(--crtr-cards-comment-button-radius, var(--crtr-radius-small));
198
- cursor: pointer;
199
- opacity: var(--crtr-cards-comment-button-resting-opacity, 0);
200
- transition: opacity var(--crtr-cards-comment-button-transition, var(--crtr-control-transition, 120ms ease)),
201
- color var(--crtr-cards-comment-button-transition, var(--crtr-control-transition, 120ms ease)),
202
- background-color var(--crtr-cards-comment-button-transition, var(--crtr-control-transition, 120ms ease));
203
- }
204
-
205
- [part~="comment-button"] svg {
206
- width: var(--crtr-cards-comment-button-icon-size, 0.875em);
207
- height: var(--crtr-cards-comment-button-icon-size, 0.875em);
208
- fill: var(--crtr-cards-comment-button-icon-fill, none);
209
- stroke: var(--crtr-cards-comment-button-icon-stroke-color, currentColor);
210
- stroke-width: var(--crtr-cards-comment-button-icon-stroke-width, 2);
211
- stroke-linecap: var(--crtr-cards-comment-button-icon-line-cap, round);
212
- stroke-linejoin: var(--crtr-cards-comment-button-icon-line-join, round);
213
- /* The glyph is never the event target: a repaint that swaps it mid-press would
214
- otherwise leave mousedown and mouseup on different nodes and eat the click. */
215
- pointer-events: none;
216
- }
217
-
218
- [part~="comment-button"]:hover {
219
- color: var(--crtr-cards-comment-button-hover-text-color, var(--crtr-color-text));
220
- background: var(--crtr-cards-comment-button-hover-fill, var(--crtr-color-surface-hover));
221
- }
222
-
223
- [part~="comment-button"][part~="has-comment"] {
224
- color: var(--crtr-cards-comment-button-noted-text-color, var(--crtr-color-accent));
225
- }
226
-
227
- ${focusRingStyle({ scope: 'cards-comment-button' }, '[part~="comment-button"]')}
228
- [part~="card"]:hover [part~="comment-button"],
229
- [part~="card"]:focus-within [part~="comment-button"],
230
- [part~="comment-button"]:focus-visible,
231
- [part~="comment-button"][part~="has-comment"] {
232
- opacity: var(--crtr-cards-comment-button-revealed-opacity, 1);
233
- }
234
-
235
- [part~="comment-editor"] {
236
- display: grid;
237
- gap: var(--crtr-cards-comment-editor-gap, var(--crtr-space-2));
238
- width: 100%;
239
- padding: var(--crtr-cards-comment-editor-padding, var(--crtr-space-2));
240
- text-align: var(--crtr-cards-comment-editor-text-align, left);
241
- background: var(--crtr-cards-comment-editor-fill, var(--crtr-color-background));
242
- border: var(--crtr-cards-comment-editor-border-width, 1px)
243
- var(--crtr-cards-comment-editor-border-style, solid)
244
- var(--crtr-cards-comment-editor-border-color, var(--crtr-color-border));
245
- border-radius: var(--crtr-cards-comment-editor-radius, var(--crtr-radius-small));
246
- cursor: auto;
247
- }
248
-
249
- /* The editor's own prose, in the page's voice — not a caps-mono field label shouting a
250
- card id at someone who just clicked that card. */
251
- [part~="comment-subject"] {
252
- color: var(--crtr-cards-comment-subject-text-color, var(--crtr-color-text-muted));
253
- font-size: var(--crtr-cards-comment-subject-font-size, 0.875em);
254
- }
255
-
256
- [part~="comment-hint"] {
257
- color: var(--crtr-cards-comment-hint-text-color, var(--crtr-color-text-muted));
258
- font-size: var(--crtr-cards-comment-hint-font-size, 0.875em);
259
- }
260
-
261
- ${writingBoxStyle({ scope: 'cards-comment-input' }, '[part~="comment-input"]')}
262
- [part~="comment-input"] {
263
- min-height: var(--crtr-cards-comment-input-min-height, 4.5em);
264
- resize: vertical;
265
- }
266
-
267
- [part~="comment-actions"] {
268
- display: flex;
269
- align-items: center;
270
- justify-content: space-between;
271
- gap: var(--crtr-cards-comment-actions-gap, var(--crtr-space-2));
272
- }
273
-
274
- [part~="comment-buttons"] {
275
- display: flex;
276
- gap: var(--crtr-cards-comment-buttons-gap, var(--crtr-space-1));
277
- }
278
-
279
- [part~="comment-cancel"] {
280
- padding: var(--crtr-cards-comment-cancel-padding, var(--crtr-space-1) var(--crtr-space-2));
281
- color: var(--crtr-cards-comment-cancel-text-color, var(--crtr-color-text-muted));
282
- font: var(--crtr-cards-comment-cancel-font, inherit);
283
- font-size: var(--crtr-cards-comment-cancel-font-size, 0.875em);
284
- background: var(--crtr-cards-comment-cancel-fill, none);
285
- border: var(--crtr-cards-comment-cancel-border-width, 1px)
286
- var(--crtr-cards-comment-cancel-border-style, solid)
287
- var(--crtr-cards-comment-cancel-border-color, transparent);
288
- border-radius: var(--crtr-cards-comment-cancel-radius, var(--crtr-radius-small));
289
- cursor: pointer;
290
- transition: color var(--crtr-cards-comment-cancel-transition, var(--crtr-control-transition, 120ms ease)),
291
- background-color var(--crtr-cards-comment-cancel-transition, var(--crtr-control-transition, 120ms ease)),
292
- border-color var(--crtr-cards-comment-cancel-transition, var(--crtr-control-transition, 120ms ease));
293
- }
294
-
295
- [part~="comment-cancel"]:hover {
296
- color: var(--crtr-cards-comment-cancel-hover-text-color, var(--crtr-color-text));
297
- background: var(--crtr-cards-comment-cancel-hover-fill, var(--crtr-color-surface-hover));
298
- }
299
-
300
- [part~="comment-save"] {
301
- padding: var(--crtr-cards-comment-save-padding, var(--crtr-space-1) var(--crtr-space-2));
302
- color: var(--crtr-cards-comment-save-text-color, var(--crtr-color-accent-contrast));
303
- font: var(--crtr-cards-comment-save-font, inherit);
304
- font-size: var(--crtr-cards-comment-save-font-size, 0.875em);
305
- background: var(--crtr-cards-comment-save-fill, var(--crtr-color-accent));
306
- border: var(--crtr-cards-comment-save-border-width, 1px)
307
- var(--crtr-cards-comment-save-border-style, solid)
308
- var(--crtr-cards-comment-save-border-color, var(--crtr-color-accent));
309
- border-radius: var(--crtr-cards-comment-save-radius, var(--crtr-radius-small));
310
- cursor: pointer;
311
- transition: color var(--crtr-cards-comment-save-transition, var(--crtr-control-transition, 120ms ease)),
312
- background-color var(--crtr-cards-comment-save-transition, var(--crtr-control-transition, 120ms ease)),
313
- border-color var(--crtr-cards-comment-save-transition, var(--crtr-control-transition, 120ms ease));
314
- }
315
-
316
- /* Mixing toward the text colour darkens on a light token set and lightens on a dark one —
317
- a brightness filter moves the wrong way on one of them. */
318
- [part~="comment-save"]:hover {
319
- background: var(--crtr-cards-comment-save-hover-fill, color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
320
- border-color: var(--crtr-cards-comment-save-hover-border-color, color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
321
- }
322
-
323
- ${focusRingStyle({ scope: 'cards-comment-cancel' }, '[part~="comment-cancel"]')}
324
- ${focusRingStyle({ scope: 'cards-comment-save' }, '[part~="comment-save"]')}
325
- /* These sit in the flow of the page rather than above it, so they carry no elevation —
326
- a shadow in this bundle means floating (a popover, an editor). Each state keeps its own
327
- scope even though their kit defaults match: one scope never names two different parts. */
328
- [part~="unavailable"] {
329
- padding: var(--crtr-cards-unavailable-padding, var(--crtr-space-3));
330
- color: var(--crtr-cards-unavailable-text-color, var(--crtr-color-text-muted));
331
- background: var(--crtr-cards-unavailable-fill, var(--crtr-color-surface));
332
- border: var(--crtr-cards-unavailable-border-width, 1px)
333
- var(--crtr-cards-unavailable-border-style, solid)
334
- var(--crtr-cards-unavailable-border-color, var(--crtr-color-border));
335
- border-radius: var(--crtr-cards-unavailable-radius, var(--crtr-radius-medium));
336
- }
337
-
338
- [part~="empty"] {
339
- padding: var(--crtr-cards-empty-padding, var(--crtr-space-3));
340
- color: var(--crtr-cards-empty-text-color, var(--crtr-color-text-muted));
341
- background: var(--crtr-cards-empty-fill, var(--crtr-color-surface));
342
- border: var(--crtr-cards-empty-border-width, 1px)
343
- var(--crtr-cards-empty-border-style, solid)
344
- var(--crtr-cards-empty-border-color, var(--crtr-color-border));
345
- border-radius: var(--crtr-cards-empty-radius, var(--crtr-radius-medium));
346
- }
347
-
348
- [part~="loading"] {
349
- padding: var(--crtr-cards-loading-padding, var(--crtr-space-3));
350
- color: var(--crtr-cards-loading-text-color, var(--crtr-color-text-muted));
351
- background: var(--crtr-cards-loading-fill, var(--crtr-color-surface));
352
- border: var(--crtr-cards-loading-border-width, 1px)
353
- var(--crtr-cards-loading-border-style, solid)
354
- var(--crtr-cards-loading-border-color, var(--crtr-color-border));
355
- border-radius: var(--crtr-cards-loading-radius, var(--crtr-radius-medium));
356
- }
357
-
358
- [part~="kind"] {
359
- color: var(--crtr-cards-kind-text-color, var(--crtr-color-text));
360
- font-family: var(--crtr-cards-kind-font-family, var(--crtr-font-mono));
361
- }
362
-
363
- [part~="reason"] {
364
- display: block;
365
- margin-top: var(--crtr-cards-reason-gap, var(--crtr-space-1));
366
- font-size: var(--crtr-cards-reason-font-size, 0.875em);
367
- }
368
- `;
369
- /**
370
- * A card is a plain `{ id, title }` record; everything else is optional prose. This mirrors
371
- * `cardSchema`, which is strict — an unknown key means the data is not a card, so a typo
372
- * surfaces as an unavailable state rather than a silently dropped field.
373
- */
374
- const CARD_KEYS = new Set(['id', 'title', 'subtitle', 'body', 'tag']);
375
- function normalizeCard(raw) {
376
- if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
377
- return null;
378
- const value = raw;
379
- if (typeof value.id !== 'string' || value.id === '')
380
- return null;
381
- if (typeof value.title !== 'string' || value.title === '')
382
- return null;
383
- for (const key of ['subtitle', 'body', 'tag']) {
384
- if (value[key] !== undefined && typeof value[key] !== 'string')
385
- return null;
386
- }
387
- for (const key of Object.keys(value)) {
388
- if (!CARD_KEYS.has(key))
389
- return null;
390
- }
391
- return {
392
- id: value.id,
393
- title: value.title,
394
- subtitle: value.subtitle,
395
- body: value.body,
396
- tag: value.tag,
397
- };
398
- }
399
- /** A card list, or `null` when the data is not one. Ids must be unique — selection keys off them. */
400
- function normalizeCards(raw) {
401
- if (!Array.isArray(raw))
402
- return null;
403
- const cards = [];
404
- const seen = new Set();
405
- for (const entry of raw) {
406
- const card = normalizeCard(entry);
407
- if (card === null || seen.has(card.id))
408
- return null;
409
- seen.add(card.id);
410
- cards.push(card);
411
- }
412
- return cards;
413
- }
414
- const CONFIG_KEYS = new Set(['label', 'cards', 'mode', 'columns']);
415
- /** Ids are scoped to this element's shadow root, so one constant is unique everywhere. */
416
- const LABEL_ID = 'prompt-label';
417
- /** Mirrors strict `cardsConfigSchema`, so this element accepts exactly what the agent may send. */
418
- function normalizeConfig(raw) {
419
- for (const key of Object.keys(raw)) {
420
- if (!CONFIG_KEYS.has(key))
421
- return { status: 'invalid', reason: `unknown config property "${key}"` };
422
- }
423
- const label = raw.label;
424
- if (label !== undefined && typeof label !== 'string')
425
- return { status: 'invalid', reason: 'label must be a string' };
426
- const mode = raw.mode;
427
- if (mode !== 'single' && mode !== 'multi')
428
- return { status: 'invalid', reason: 'mode must be "single" or "multi"' };
429
- const columns = raw.columns;
430
- if (columns !== undefined && columns !== 2 && columns !== 3) {
431
- return { status: 'invalid', reason: 'columns must be 2 or 3' };
432
- }
433
- let cards;
434
- if (raw.cards !== undefined) {
435
- const parsed = normalizeCards(raw.cards);
436
- if (parsed === null)
437
- return { status: 'invalid', reason: 'cards must be a list of uniquely identified cards' };
438
- cards = parsed;
439
- }
440
- return {
441
- status: 'ok',
442
- config: { label: label, cards, mode, columns },
443
- };
444
- }
445
- /**
446
- * Typing in the comment box is ordinary writing — the list grammar keeps out of it. A card's
447
- * own radio or checkbox is not prose, so `j`/`k`/`x`/`c` still reach the grid from there.
448
- */
449
- function isTextEntry(target) {
450
- if (target instanceof HTMLTextAreaElement)
451
- return true;
452
- if (target instanceof HTMLElement && target.isContentEditable)
453
- return true;
454
- if (!(target instanceof HTMLInputElement))
455
- return false;
456
- return target.type !== 'checkbox' && target.type !== 'radio';
457
- }
458
- const SVG_NS = 'http://www.w3.org/2000/svg';
459
- /** The comment glyph: a speech square, gaining a line of text once a note lives there. */
460
- function commentIcon(hasComment) {
461
- const svg = document.createElementNS(SVG_NS, 'svg');
462
- svg.setAttribute('viewBox', '0 0 24 24');
463
- svg.setAttribute('aria-hidden', 'true');
464
- const bubble = document.createElementNS(SVG_NS, 'path');
465
- bubble.setAttribute('d', 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z');
466
- svg.append(bubble);
467
- const marks = hasComment
468
- ? [
469
- ['7', '9', '17', '9'],
470
- ['7', '13', '13', '13'],
471
- ]
472
- : [
473
- ['12', '7', '12', '13'],
474
- ['9', '10', '15', '10'],
475
- ];
476
- for (const [x1, y1, x2, y2] of marks) {
477
- const line = document.createElementNS(SVG_NS, 'line');
478
- line.setAttribute('x1', x1 ?? '0');
479
- line.setAttribute('y1', y1 ?? '0');
480
- line.setAttribute('x2', x2 ?? '0');
481
- line.setAttribute('y2', y2 ?? '0');
482
- svg.append(line);
483
- }
484
- return svg;
485
- }
486
- /** The prompt above the grid, or nothing when the agent asked its question elsewhere. */
487
- function buildLabel(text) {
488
- if (text === undefined || text === '')
489
- return null;
490
- const label = document.createElement('p');
491
- label.part.add('label');
492
- label.id = LABEL_ID;
493
- label.textContent = text;
494
- return label;
495
- }
496
- /** Radio groups are keyed by name, so every element on a page needs its own. */
497
- let groupCounter = 0;
498
- class CrtrCardsElement extends HTMLElement {
499
- /** The host marks a resolved or canceled page `inert`; that is this element's read-only signal. */
500
- static observedAttributes = ['inert'];
501
- #container = document.createElement('div');
502
- #group;
503
- #config = null;
504
- /** The rendered prompt, kept across repaints because every render replaces the container. */
505
- #label = null;
506
- #cards = [];
507
- #slotId;
508
- // Response state. Authoritative for rendering; `respond()` only asks the host to save it.
509
- #selected = [];
510
- #comments = [];
511
- #activeId = null;
512
- #nodes = new Map();
513
- #open = null;
514
- #editor = null;
515
- constructor() {
516
- super();
517
- groupCounter += 1;
518
- this.#group = `crtr-cards-${groupCounter}`;
519
- const root = this.attachShadow({ mode: 'open' });
520
- const style = document.createElement('style');
521
- style.textContent = STYLE;
522
- this.#container.part.add('container');
523
- this.#container.addEventListener('keydown', (event) => this.#onKeyDown(event));
524
- root.append(style, this.#container);
525
- }
526
- connectedCallback() {
527
- const read = readSlotConfig(this);
528
- if (read.status !== 'ok') {
529
- this.#renderUnavailable(read.reason);
530
- return;
531
- }
532
- const normalized = normalizeConfig(read.config);
533
- if (normalized.status !== 'ok') {
534
- this.#renderUnavailable(normalized.reason);
535
- return;
536
- }
537
- this.#config = normalized.config;
538
- this.#slotId = readSlotId(this);
539
- // Registered before render: the pager reports every response-bearing slot
540
- // at submit, whether or not the person ever selected a card.
541
- if (this.#slotId !== undefined)
542
- registerSlotResponse(this, this.#slotId, () => this.#response());
543
- this.#label = buildLabel(normalized.config.label);
544
- this.#cards = normalized.config.cards ?? [];
545
- this.#hydrate();
546
- this.#build();
547
- }
548
- attributeChangedCallback() {
549
- if (this.isConnected)
550
- this.#syncReadOnly();
551
- }
552
- /** Repainted after every build, because a rebuild replaces the controls. */
553
- #syncReadOnly() {
554
- const readOnly = isReadOnly(this);
555
- applyReadOnly(this.#container, readOnly);
556
- if (readOnly && this.#open !== null)
557
- this.#closeEditor(false);
558
- }
559
- /**
560
- * Seed local state from the answer the host is already holding, so a reopened page shows
561
- * the cards that were picked. `#build` is what drops ids the current data no longer has,
562
- * so this only has to re-impose single-select.
563
- */
564
- #hydrate() {
565
- if (this.#slotId === undefined)
566
- return;
567
- const saved = savedResponse(this.#slotId);
568
- if (saved === undefined)
569
- return;
570
- const ids = this.#cards.map((card) => card.id).filter((id) => savedIds(saved.selectedCardIds).includes(id));
571
- this.#selected = this.#config?.mode === 'single' ? ids.slice(0, 1) : ids;
572
- this.#comments = adoptComments(saved.comments);
573
- }
574
- // ---------------------------------------------------------------- rendering
575
- #renderUnavailable(reason) {
576
- this.#nodes.clear();
577
- this.#open = null;
578
- this.#editor = null;
579
- const box = document.createElement('div');
580
- box.part.add('unavailable');
581
- box.setAttribute('role', 'status');
582
- const kind = document.createElement('span');
583
- kind.part.add('kind');
584
- kind.textContent = 'cards';
585
- const detail = document.createElement('span');
586
- detail.part.add('reason');
587
- detail.textContent = reason;
588
- box.append('Component unavailable: ', kind, detail);
589
- this.#setContent(box);
590
- }
591
- #renderStatus(part, text) {
592
- const box = document.createElement('div');
593
- box.part.add(part);
594
- box.setAttribute('role', 'status');
595
- box.textContent = text;
596
- this.#setContent(box);
597
- }
598
- /** Every repaint replaces the container's children, so the prompt is re-laid each time. */
599
- #setContent(content) {
600
- if (this.#label === null)
601
- this.#container.replaceChildren(content);
602
- else
603
- this.#container.replaceChildren(this.#label, content);
604
- }
605
- /** Builds the grid once per data load; every later change repaints in place. */
606
- #build() {
607
- const config = this.#config;
608
- if (config === null)
609
- return;
610
- this.#nodes.clear();
611
- this.#open = null;
612
- this.#editor = null;
613
- // A selection or comment carried over from a previous data load can only name a card
614
- // that is still here — true even when the new load has no cards at all.
615
- const ids = new Set(this.#cards.map((card) => card.id));
616
- this.#selected = this.#selected.filter((id) => ids.has(id));
617
- this.#comments = this.#comments.filter((comment) => comment.anchor.kind !== 'card' || ids.has(comment.anchor.cardId));
618
- if (this.#cards.length === 0) {
619
- this.#renderStatus('empty', 'No cards to choose from.');
620
- return;
621
- }
622
- const grid = document.createElement('div');
623
- grid.part.add('grid');
624
- if (config.columns === 3)
625
- grid.dataset.columns = '3';
626
- if (this.#label !== null) {
627
- // A named group of choices: single-select is a radio group, multi a plain group.
628
- grid.setAttribute('role', config.mode === 'single' ? 'radiogroup' : 'group');
629
- grid.setAttribute('aria-labelledby', LABEL_ID);
630
- }
631
- grid.append(...this.#cards.map((card) => this.#buildCard(card, config.mode, this.#group)));
632
- this.#setContent(grid);
633
- this.#paint();
634
- }
635
- #buildCard(card, mode, group) {
636
- const root = document.createElement('div');
637
- root.part.add('card');
638
- root.dataset.cardId = card.id;
639
- const header = document.createElement('div');
640
- header.part.add('card-header');
641
- // The surface is presentational; the real radio / checkbox is the control, so a
642
- // native radio group's arrow keys, Enter and Space all behave as they always do.
643
- const label = document.createElement('label');
644
- label.part.add('card-label');
645
- const control = document.createElement('input');
646
- control.part.add('control');
647
- control.type = mode === 'single' ? 'radio' : 'checkbox';
648
- control.name = group;
649
- control.value = card.id;
650
- control.addEventListener('change', () => this.#toggle(card.id, control.checked));
651
- control.addEventListener('focus', () => this.#setActive(card.id, false));
652
- const heading = document.createElement('span');
653
- heading.part.add('card-heading');
654
- const title = document.createElement('span');
655
- title.part.add('card-title');
656
- title.textContent = card.title;
657
- heading.append(title);
658
- if (card.subtitle !== undefined && card.subtitle !== '') {
659
- const subtitle = document.createElement('span');
660
- subtitle.part.add('card-subtitle');
661
- subtitle.textContent = card.subtitle;
662
- heading.append(subtitle);
663
- }
664
- let commentButton;
665
- if (canRecordComments(this.#slotId)) {
666
- commentButton = document.createElement('button');
667
- commentButton.type = 'button';
668
- commentButton.part.add('comment-button');
669
- commentButton.addEventListener('click', (event) => {
670
- event.stopPropagation();
671
- this.#setActive(card.id, false);
672
- this.#toggleEditor({ kind: 'card', cardId: card.id }, card.title, root, null);
673
- });
674
- }
675
- label.append(control, heading);
676
- header.append(label);
677
- if (commentButton !== undefined)
678
- header.append(commentButton);
679
- root.append(header);
680
- if (card.body !== undefined && card.body !== '') {
681
- const body = document.createElement('p');
682
- body.part.add('card-body');
683
- body.textContent = card.body;
684
- root.append(body);
685
- }
686
- let tag = null;
687
- if (card.tag !== undefined && card.tag !== '') {
688
- tag = document.createElement('span');
689
- tag.part.add('tag');
690
- tag.textContent = card.tag;
691
- root.append(tag);
692
- }
693
- root.addEventListener('mouseenter', () => this.#setActive(card.id, false));
694
- root.addEventListener('mouseleave', () => {
695
- if (this.#activeId === card.id)
696
- this.#setActive(null, false);
697
- });
698
- root.addEventListener('focusin', () => this.#setActive(card.id, false));
699
- // The whole card is the choice, not just its heading: a click on the body, the tag, or
700
- // the padding around them picks the card. A click that went through the label is left
701
- // alone — the native control has already toggled from it, and toggling again here would
702
- // undo the user's own click. The comment button and the editor stop propagation, so a
703
- // press on either never reaches this. A press that ends a text selection is someone
704
- // reading the card, not choosing it.
705
- root.addEventListener('click', (event) => {
706
- if (control.disabled)
707
- return;
708
- if (event.composedPath().includes(label))
709
- return;
710
- if (document.getSelection()?.isCollapsed === false)
711
- return;
712
- this.#setActive(card.id, false);
713
- this.#toggle(card.id, !this.#selected.includes(card.id));
714
- });
715
- this.#nodes.set(card.id, {
716
- root,
717
- control,
718
- ...(commentButton === undefined ? {} : { commentButton }),
719
- tag,
720
- });
721
- return root;
722
- }
723
- /** Repaints selection, activity and comment presence without rebuilding the grid. */
724
- #paint() {
725
- const mode = this.#config?.mode ?? 'multi';
726
- const tabbableId = mode === 'single' ? (this.#selected[0] ?? this.#cards[0]?.id) : undefined;
727
- for (const card of this.#cards) {
728
- const node = this.#nodes.get(card.id);
729
- if (node === undefined)
730
- continue;
731
- const selected = this.#selected.includes(card.id);
732
- node.root.part.toggle('selected', selected);
733
- node.root.part.toggle('active', this.#activeId === card.id);
734
- node.control.checked = selected;
735
- // A radio group holds one tab stop; an unselected group still needs an entry point.
736
- node.control.tabIndex = mode === 'single' && card.id !== tabbableId ? -1 : 0;
737
- const comment = findComment(this.#comments, { kind: 'card', cardId: card.id });
738
- if (node.commentButton !== undefined)
739
- this.#paintCommentButton(node.commentButton, comment, card.title);
740
- }
741
- this.#syncReadOnly();
742
- }
743
- #paintCommentButton(button, comment, target) {
744
- const has = comment !== undefined;
745
- const glyphChanged = button.part.contains('has-comment') !== has;
746
- button.part.toggle('has-comment', has);
747
- button.setAttribute('aria-label', has ? `Edit comment on ${target}` : `Comment on ${target}`);
748
- // The saved note reads on hover, so a comment is legible without opening its editor.
749
- if (has)
750
- button.title = comment.text;
751
- else
752
- button.removeAttribute('title');
753
- // Paint runs on every hover and focus change; only a real change touches the DOM.
754
- if (glyphChanged || button.childElementCount === 0)
755
- button.replaceChildren(commentIcon(has));
756
- }
757
- // ------------------------------------------------------------- interaction
758
- #setActive(id, focus) {
759
- if (this.#activeId === id && !focus)
760
- return;
761
- this.#activeId = id;
762
- this.#paint();
763
- if (focus && id !== null)
764
- this.#nodes.get(id)?.control.focus();
765
- }
766
- #toggle(cardId, selected) {
767
- if (this.#config === null || isReadOnly(this))
768
- return;
769
- if (this.#config.mode === 'single') {
770
- // Radio semantics: choosing a card replaces the answer, and re-choosing keeps it.
771
- this.#selected = [cardId];
772
- }
773
- else {
774
- const next = selected
775
- ? [...this.#selected, cardId]
776
- : this.#selected.filter((id) => id !== cardId);
777
- // Selection reads back in the order the agent authored the cards, not click order.
778
- this.#selected = this.#cards.map((card) => card.id).filter((id) => next.includes(id));
779
- }
780
- this.#paint();
781
- this.#emit();
782
- }
783
- #onKeyDown(event) {
784
- if (event.metaKey || event.ctrlKey || event.altKey)
785
- return;
786
- if (isTextEntry(event.target))
787
- return;
788
- const ids = this.#cards.map((card) => card.id);
789
- if (ids.length === 0)
790
- return;
791
- const key = event.key.toLowerCase();
792
- const currentId = this.#activeId ?? this.#selected[this.#selected.length - 1] ?? ids[0] ?? '';
793
- if (key === 'j' || key === 'k') {
794
- event.preventDefault();
795
- const index = Math.max(0, ids.indexOf(currentId));
796
- const step = key === 'j' ? 1 : -1;
797
- const nextId = ids[(index + step + ids.length) % ids.length];
798
- if (nextId !== undefined)
799
- this.#setActive(nextId, true);
800
- return;
801
- }
802
- if (key === 'x') {
803
- event.preventDefault();
804
- this.#toggle(currentId, this.#config?.mode === 'single' ? true : !this.#selected.includes(currentId));
805
- return;
806
- }
807
- if (key === 'c') {
808
- event.preventDefault();
809
- const card = this.#cards.find((entry) => entry.id === currentId);
810
- const node = this.#nodes.get(currentId);
811
- if (card === undefined || node?.commentButton === undefined)
812
- return;
813
- this.#setActive(currentId, false);
814
- this.#toggleEditor({ kind: 'card', cardId: card.id }, card.title, node.root, null);
815
- }
816
- // Enter, Space and the arrow keys are left to the native radio / checkbox, exactly as
817
- // the kit leaves them — this handler only adds the list grammar on top.
818
- }
819
- // ----------------------------------------------------------------- comments
820
- /** Opens the editor for `anchor`, or closes it when it is already the open one. */
821
- #toggleEditor(anchor, target, mount, after) {
822
- if (mount === null || !canRecordComments(this.#slotId) || isReadOnly(this))
823
- return;
824
- const openAnchor = this.#open?.anchor;
825
- const sameAnchor = openAnchor !== undefined &&
826
- openAnchor.kind === anchor.kind &&
827
- (anchor.kind !== 'card' || (openAnchor.kind === 'card' && openAnchor.cardId === anchor.cardId));
828
- this.#closeEditor(false);
829
- if (sameAnchor)
830
- return;
831
- this.#openEditor({ anchor, target, mount, after });
832
- }
833
- #openEditor(open) {
834
- const editor = document.createElement('div');
835
- editor.part.add('comment-editor');
836
- // The editor lives inside the card it comments on; a click in it is not a card click.
837
- editor.addEventListener('click', (event) => event.stopPropagation());
838
- const subject = document.createElement('p');
839
- subject.part.add('comment-subject');
840
- subject.textContent = `Comment on ${open.target}`;
841
- const input = document.createElement('textarea');
842
- input.part.add('comment-input');
843
- input.placeholder = 'What should the agent know about this?';
844
- // Seeded from the saved comment every time it opens, so an abandoned edit never leaks.
845
- input.value = findComment(this.#comments, open.anchor)?.text ?? '';
846
- const actions = document.createElement('div');
847
- actions.part.add('comment-actions');
848
- const hint = document.createElement('span');
849
- hint.part.add('comment-hint');
850
- hint.textContent = findComment(this.#comments, open.anchor) !== undefined ? 'empty to delete' : '⌘↵ to save';
851
- const buttons = document.createElement('div');
852
- buttons.part.add('comment-buttons');
853
- const cancel = document.createElement('button');
854
- cancel.type = 'button';
855
- cancel.part.add('comment-cancel');
856
- cancel.textContent = 'Cancel';
857
- cancel.addEventListener('click', () => this.#closeEditor(true));
858
- const save = document.createElement('button');
859
- save.type = 'button';
860
- save.part.add('comment-save');
861
- save.textContent = 'Save';
862
- save.addEventListener('click', () => this.#commitComment(input.value));
863
- buttons.append(cancel, save);
864
- actions.append(hint, buttons);
865
- input.addEventListener('keydown', (event) => {
866
- if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
867
- event.preventDefault();
868
- this.#commitComment(input.value);
869
- return;
870
- }
871
- if (event.key === 'Escape') {
872
- event.preventDefault();
873
- this.#closeEditor(true);
874
- }
875
- });
876
- editor.append(subject, input, actions);
877
- if (open.after !== null)
878
- open.after.after(editor);
879
- else
880
- open.mount.append(editor);
881
- this.#open = open;
882
- this.#editor = editor;
883
- input.focus();
884
- }
885
- #closeEditor(restoreFocus) {
886
- const open = this.#open;
887
- this.#editor?.remove();
888
- this.#editor = null;
889
- this.#open = null;
890
- if (!restoreFocus || open === null)
891
- return;
892
- if (open.anchor.kind === 'card')
893
- this.#nodes.get(open.anchor.cardId)?.control.focus();
894
- }
895
- #commitComment(text) {
896
- const open = this.#open;
897
- if (open === null || isReadOnly(this))
898
- return;
899
- this.#comments = setComment(this.#comments, open.anchor, text);
900
- this.#closeEditor(true);
901
- this.#paint();
902
- this.#emit();
903
- }
904
- // ----------------------------------------------------------------- response
905
- #response() {
906
- return { selectedCardIds: [...this.#selected], comments: this.#comments.map((comment) => ({ ...comment })) };
907
- }
908
- /** Every user change hands the host this slot's complete current response, never a delta. */
909
- #emit() {
910
- if (this.#slotId === undefined)
911
- return;
912
- respond(this.#slotId, this.#response());
913
- }
914
- }
915
- registerElement('crtr-cards', CrtrCardsElement);