@north-light/crouter 0.3.197 → 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 (339) hide show
  1. package/dist/api/client.d.ts +19 -19
  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 +126 -33
  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 +8 -8
  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 +3 -3
  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/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  33. package/dist/clients/attach/chrome/bash-jobs.js +3 -3
  34. package/dist/clients/attach/render/chat-view.d.ts +29 -9
  35. package/dist/clients/attach/render/chat-view.js +64 -35
  36. package/dist/clients/attach/render/markdown-source.js +0 -6
  37. package/dist/clients/attach/render/page-block.d.ts +34 -0
  38. package/dist/clients/attach/render/page-block.js +226 -0
  39. package/dist/clients/attach/render/tool-calls.js +16 -2
  40. package/dist/clients/attach/session/whip.d.ts +2 -0
  41. package/dist/clients/attach/session/whip.js +2 -0
  42. package/dist/clients/attach/viewer.js +1419 -982
  43. package/dist/clients/conversation/projection.js +18 -3
  44. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
  45. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -14
  46. package/dist/clients/inbox/controller.js +10 -16
  47. package/dist/clients/inbox/page-adapter.d.ts +3 -4
  48. package/dist/clients/inbox/page-adapter.js +3 -4
  49. package/dist/clients/inbox/resolve.d.ts +1 -1
  50. package/dist/clients/inbox/resolve.js +3 -3
  51. package/dist/clients/inbox/review/document-surface.js +3 -1
  52. package/dist/clients/inbox/review/state.d.ts +2 -1
  53. package/dist/clients/inbox/review/state.js +5 -16
  54. package/dist/clients/inbox/tui/input.js +37 -19
  55. package/dist/clients/inbox/tui/panel.js +5 -31
  56. package/dist/clients/inbox/tui/render.d.ts +11 -0
  57. package/dist/clients/inbox/tui/render.js +101 -36
  58. package/dist/clients/inbox/tui/slots.d.ts +4 -4
  59. package/dist/clients/inbox/tui/slots.js +19 -44
  60. package/dist/clients/inbox/tui/types.d.ts +6 -10
  61. package/dist/clients/inbox/tui.js +2 -2
  62. package/dist/commands/__tests__/human.test.js +118 -24
  63. package/dist/commands/__tests__/node-message.test.js +14 -0
  64. package/dist/commands/attention.js +4 -4
  65. package/dist/commands/canvas-browse.js +1 -1
  66. package/dist/commands/canvas-snapshot.js +1 -1
  67. package/dist/commands/canvas.js +1 -1
  68. package/dist/commands/dashboard.js +1 -1
  69. package/dist/commands/human/{doc.d.ts → components.d.ts} +1 -1
  70. package/dist/commands/human/components.js +77 -0
  71. package/dist/commands/human/feedback.d.ts +2 -0
  72. package/dist/commands/human/feedback.js +108 -0
  73. package/dist/commands/human/prompts.d.ts +17 -8
  74. package/dist/commands/human/prompts.js +217 -226
  75. package/dist/commands/human/queue.js +57 -88
  76. package/dist/commands/human/shared.d.ts +4 -15
  77. package/dist/commands/human/shared.js +19 -61
  78. package/dist/commands/human.js +32 -19
  79. package/dist/commands/memory/origin.js +2 -2
  80. package/dist/commands/node/bash.js +4 -4
  81. package/dist/commands/node/inspect.js +3 -3
  82. package/dist/commands/node/lifecycle.js +1 -1
  83. package/dist/commands/node/message.js +2 -2
  84. package/dist/commands/node-worktree.js +8 -8
  85. package/dist/commands/pkg/browse.js +1 -1
  86. package/dist/commands/pkg/plugin-manage.js +129 -10
  87. package/dist/commands/profile/delete.js +41 -15
  88. package/dist/commands/profile.js +1 -1
  89. package/dist/commands/push.js +1 -1
  90. package/dist/commands/surface/node/focus.js +29 -14
  91. package/dist/commands/surface-edit.js +3 -3
  92. package/dist/commands/surface-inbox.d.ts +2 -0
  93. package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
  94. package/dist/commands/surface.js +2 -1
  95. package/dist/commands/sys/doctor.js +60 -3
  96. package/dist/commands/sys/feedback.js +79 -25
  97. package/dist/commands/sys/settings.js +1 -1
  98. package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
  99. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  100. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  101. package/dist/core/__tests__/extension-abort.test.js +5 -2
  102. package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
  103. package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
  104. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  105. package/dist/core/__tests__/human-deliver.test.js +41 -45
  106. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  107. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  108. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  109. package/dist/core/__tests__/seam/dormancy-release.test.js +103 -4
  110. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  111. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -16
  112. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  113. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  114. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  115. package/dist/core/__tests__/session-cycles.test.js +22 -0
  116. package/dist/core/__tests__/stop-guard.test.js +4 -4
  117. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  118. package/dist/core/bash-jobs.d.ts +6 -0
  119. package/dist/core/bash-jobs.js +10 -0
  120. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  121. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  122. package/dist/core/canvas/attention.js +8 -6
  123. package/dist/core/canvas/browse/app.d.ts +1 -1
  124. package/dist/core/canvas/browse/app.js +21 -3
  125. package/dist/core/canvas/browse/model.d.ts +8 -2
  126. package/dist/core/canvas/browse/model.js +11 -4
  127. package/dist/core/canvas/browse/render.d.ts +4 -0
  128. package/dist/core/canvas/browse/render.js +5 -1
  129. package/dist/core/canvas/canvas.d.ts +33 -0
  130. package/dist/core/canvas/canvas.js +92 -0
  131. package/dist/core/canvas/crons.d.ts +9 -0
  132. package/dist/core/canvas/crons.js +51 -0
  133. package/dist/core/canvas/node-order.d.ts +22 -1
  134. package/dist/core/canvas/node-order.js +26 -1
  135. package/dist/core/canvas/render-source.d.ts +5 -0
  136. package/dist/core/canvas/render-source.js +1 -0
  137. package/dist/core/canvas/types.d.ts +9 -3
  138. package/dist/core/command-plugins/bundle.d.ts +6 -1
  139. package/dist/core/command-plugins/bundle.js +26 -13
  140. package/dist/core/command.js +8 -12
  141. package/dist/core/config.d.ts +47 -1
  142. package/dist/core/config.js +138 -4
  143. package/dist/core/help.d.ts +3 -0
  144. package/dist/core/help.js +1 -1
  145. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  146. package/dist/core/human/__tests__/page-catalog.test.js +20 -0
  147. package/dist/core/human/__tests__/page-tickets.test.js +68 -34
  148. package/dist/core/human/__tests__/page.test.js +60 -119
  149. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -16
  150. package/dist/core/human/answer-text.d.ts +3 -0
  151. package/dist/core/human/answer-text.js +98 -0
  152. package/dist/core/human/answer.d.ts +53 -0
  153. package/dist/core/human/answer.js +119 -0
  154. package/dist/core/human/component-docs.d.ts +18 -0
  155. package/dist/core/human/component-docs.js +434 -0
  156. package/dist/core/human/convention.d.ts +17 -3
  157. package/dist/core/human/convention.js +36 -5
  158. package/dist/core/human/feedback-companion.d.ts +20 -0
  159. package/dist/core/human/feedback-companion.js +98 -0
  160. package/dist/core/human/feedback.d.ts +65 -0
  161. package/dist/core/human/feedback.js +116 -0
  162. package/dist/core/human/page-catalog.d.ts +56 -5
  163. package/dist/core/human/page-catalog.js +151 -22
  164. package/dist/core/human/page-errors.d.ts +4 -0
  165. package/dist/core/human/page-errors.js +7 -0
  166. package/dist/core/human/page-eval.d.ts +20 -0
  167. package/dist/core/human/page-eval.js +123 -0
  168. package/dist/core/human/page-schema.d.ts +48 -116
  169. package/dist/core/human/page-schema.js +100 -72
  170. package/dist/core/human/page-synth.d.ts +2 -4
  171. package/dist/core/human/page-synth.js +20 -7
  172. package/dist/core/human/page.d.ts +13 -21
  173. package/dist/core/human/page.js +147 -391
  174. package/dist/core/human/review-schema.d.ts +1 -0
  175. package/dist/core/human/review-schema.js +1 -1
  176. package/dist/core/human/root.d.ts +6 -2
  177. package/dist/core/human/root.js +12 -4
  178. package/dist/core/human/scan.d.ts +3 -3
  179. package/dist/core/human/scan.js +21 -21
  180. package/dist/core/human/summary.d.ts +2 -6
  181. package/dist/core/human/summary.js +3 -59
  182. package/dist/core/human/tickets.d.ts +22 -15
  183. package/dist/core/human/tickets.js +90 -28
  184. package/dist/core/human/types.d.ts +14 -2
  185. package/dist/core/keybindings/catalog.d.ts +2 -2
  186. package/dist/core/keybindings/catalog.js +1 -0
  187. package/dist/core/preview-registry.js +8 -10
  188. package/dist/core/profiles/default-binding.d.ts +4 -0
  189. package/dist/core/profiles/default-binding.js +24 -0
  190. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  191. package/dist/core/profiles/deletion-reservation.js +38 -0
  192. package/dist/core/profiles/manifest.d.ts +3 -0
  193. package/dist/core/profiles/manifest.js +12 -0
  194. package/dist/core/profiles/select.js +2 -2
  195. package/dist/core/review/realize.js +12 -4
  196. package/dist/core/review/store.d.ts +6 -0
  197. package/dist/core/review/store.js +31 -0
  198. package/dist/core/runtime/bearings.d.ts +0 -4
  199. package/dist/core/runtime/bearings.js +4 -62
  200. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  201. package/dist/core/runtime/bin-contributions.js +186 -0
  202. package/dist/core/runtime/boot-root.js +6 -1
  203. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  204. package/dist/core/runtime/broker/client-registry.js +13 -1
  205. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  206. package/dist/core/runtime/broker/extension-abort.js +4 -2
  207. package/dist/core/runtime/broker/fault-retry.js +4 -0
  208. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  209. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  210. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  211. package/dist/core/runtime/broker/read-ops.js +2 -2
  212. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  213. package/dist/core/runtime/broker/rebind.js +6 -1
  214. package/dist/core/runtime/broker-extension-render.js +19 -0
  215. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  216. package/dist/core/runtime/broker.d.ts +1 -1
  217. package/dist/core/runtime/broker.js +25 -12
  218. package/dist/core/runtime/busy.d.ts +4 -3
  219. package/dist/core/runtime/busy.js +4 -3
  220. package/dist/core/runtime/front-door-env.d.ts +5 -0
  221. package/dist/core/runtime/front-door-env.js +16 -0
  222. package/dist/core/runtime/front-door.d.ts +1 -5
  223. package/dist/core/runtime/front-door.js +2 -5
  224. package/dist/core/runtime/host.d.ts +1 -1
  225. package/dist/core/runtime/invocation.d.ts +6 -0
  226. package/dist/core/runtime/invocation.js +10 -0
  227. package/dist/core/runtime/kickoff.js +34 -0
  228. package/dist/core/runtime/launch.d.ts +2 -6
  229. package/dist/core/runtime/node-read.js +4 -1
  230. package/dist/core/runtime/nodes.js +2 -0
  231. package/dist/core/runtime/promote.d.ts +1 -0
  232. package/dist/core/runtime/promote.js +3 -1
  233. package/dist/core/runtime/session-cycles.d.ts +6 -0
  234. package/dist/core/runtime/session-cycles.js +15 -1
  235. package/dist/core/runtime/session-visibility.d.ts +17 -8
  236. package/dist/core/runtime/session-visibility.js +19 -10
  237. package/dist/core/runtime/spawn-env.d.ts +9 -1
  238. package/dist/core/runtime/spawn-env.js +62 -2
  239. package/dist/core/runtime/spawn.js +4 -4
  240. package/dist/core/runtime/stop-guard.d.ts +2 -2
  241. package/dist/core/runtime/stop-guard.js +2 -2
  242. package/dist/core/runtime/stop-signals.d.ts +1 -1
  243. package/dist/core/runtime/stop-signals.js +2 -2
  244. package/dist/core/runtime/tmux-bindings.js +1 -1
  245. package/dist/core/session-model/session-state.d.ts +4 -3
  246. package/dist/core/session-model/session-state.js +14 -1
  247. package/dist/core/user-settings.d.ts +36 -2
  248. package/dist/core/user-settings.js +56 -14
  249. package/dist/core/worktree.d.ts +15 -45
  250. package/dist/core/worktree.js +59 -261
  251. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  252. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  253. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  254. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  255. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  256. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  257. package/dist/daemon/api/handlers/human.js +24 -20
  258. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  259. package/dist/daemon/api/handlers/inbox.js +48 -109
  260. package/dist/daemon/api/handlers/profiles.js +11 -29
  261. package/dist/daemon/api/map.d.ts +6 -1
  262. package/dist/daemon/api/map.js +23 -0
  263. package/dist/daemon/api/server.js +3 -1
  264. package/dist/daemon/companion-retire.d.ts +2 -0
  265. package/dist/daemon/companion-retire.js +33 -0
  266. package/dist/daemon/cron-run.js +29 -17
  267. package/dist/daemon/crtrd.js +1 -1
  268. package/dist/daemon/fleet.d.ts +22 -10
  269. package/dist/daemon/fleet.js +43 -24
  270. package/dist/daemon/human/finish.d.ts +12 -6
  271. package/dist/daemon/human/finish.js +134 -54
  272. package/dist/daemon/human/sweep.js +9 -3
  273. package/dist/daemon/profile-delete.d.ts +7 -0
  274. package/dist/daemon/profile-delete.js +306 -0
  275. package/dist/daemon/reconcilers/broker-supervision.js +14 -9
  276. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  277. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  278. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  279. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  280. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  281. package/dist/daemon/review/deliver.js +3 -3
  282. package/dist/daemon/review/finish.d.ts +0 -2
  283. package/dist/daemon/review/finish.js +1 -29
  284. package/dist/daemon/review/sweep.js +12 -1
  285. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  286. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  287. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  288. package/dist/pi-extensions/canvas-bash-valve.js +31 -13
  289. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  290. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  291. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  292. package/dist/pi-extensions/canvas-stophook.js +4 -4
  293. package/dist/shared/generated-context.js +2 -2
  294. package/dist/types.d.ts +61 -5
  295. package/dist/types.js +8 -2
  296. package/package.json +4 -4
  297. package/runtime.lock.json +5 -33
  298. package/scripts/install-runtime.mjs +19 -24
  299. package/dist/builtin-memory/init.md +0 -38
  300. package/dist/builtin-memory/plan.md +0 -19
  301. package/dist/builtin-memory/spec.md +0 -19
  302. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  303. package/dist/clients/attach/render/html-markdown.js +0 -206
  304. package/dist/commands/human/doc.js +0 -144
  305. package/dist/commands/human/inbox.d.ts +0 -2
  306. package/dist/core/human/__tests__/page-render.test.js +0 -49
  307. package/dist/core/human/markdown-html.d.ts +0 -8
  308. package/dist/core/human/markdown-html.js +0 -581
  309. package/dist/core/human/page-render.d.ts +0 -3
  310. package/dist/core/human/page-render.js +0 -131
  311. package/dist/pages/bundle.css +0 -1
  312. package/dist/pages/bundle.js +0 -1347
  313. package/dist/pages/comments.d.ts +0 -43
  314. package/dist/pages/comments.js +0 -125
  315. package/dist/pages/elements/cards.d.ts +0 -17
  316. package/dist/pages/elements/cards.js +0 -802
  317. package/dist/pages/elements/chart.js +0 -648
  318. package/dist/pages/elements/options.d.ts +0 -21
  319. package/dist/pages/elements/options.js +0 -629
  320. package/dist/pages/elements/pages.d.ts +0 -24
  321. package/dist/pages/elements/pages.js +0 -355
  322. package/dist/pages/elements/slot.d.ts +0 -21
  323. package/dist/pages/elements/slot.js +0 -105
  324. package/dist/pages/elements/table.d.ts +0 -19
  325. package/dist/pages/elements/table.js +0 -935
  326. package/dist/pages/elements/text.d.ts +0 -25
  327. package/dist/pages/elements/text.js +0 -968
  328. package/dist/pages/entry.d.ts +0 -8
  329. package/dist/pages/entry.js +0 -8
  330. package/dist/pages/host.d.ts +0 -158
  331. package/dist/pages/host.js +0 -182
  332. package/dist/pages/register.d.ts +0 -2
  333. package/dist/pages/register.js +0 -3
  334. package/dist/pages/slot-config.d.ts +0 -36
  335. package/dist/pages/slot-config.js +0 -50
  336. package/dist/pages/types.d.ts +0 -98
  337. package/dist/pages/types.js +0 -23
  338. /package/dist/{core/human/__tests__/page-render.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  339. /package/dist/{pages/elements/chart.d.ts → core/__tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,935 +0,0 @@
1
- /**
2
- * `<crtr-table>` — a dataset the agent wants the user to pick from.
3
- *
4
- * Rows and columns are independently selectable, single or multi, because the two questions
5
- * an agent asks of a table are "which of these records" and "which of these fields", and they
6
- * are not the same question. Row selection wears a real checkbox or radio in a leading cell;
7
- * column selection turns the header into a pressed toggle, since a column has no natural row
8
- * to hang a checkbox on and the header IS the column.
9
- *
10
- * Anchored comments use the same one-per-anchor algebra as every other element: a row anchor
11
- * in each row's trailing cell, a column anchor beside each header label, and a `whole` anchor
12
- * in the trailing header cell for the table itself. Triggers stay invisible until the row is
13
- * hovered or focused, or until a comment is actually there.
14
- *
15
- * Ported from the agent interaction kit's `agent-table.tsx` (northlight `3705fe9e2`), with the
16
- * kit's `classic-list-keys` grammar — j/k move, x selects, c comments — reimplemented here
17
- * over the shadow root.
18
- */
19
- import { registerElement } from '../register.js';
20
- import { readSlotConfig, readSlotId } from '../slot-config.js';
21
- import { artifactData, respond, savedIds, savedResponse } from '../host.js';
22
- import { adoptComments, anchorLabel, findComment, setComment } from '../comments.js';
23
- const COMMENT_BUBBLE = 'M3.25 3.5h9.5A1.75 1.75 0 0 1 14.5 5.25v5A1.75 1.75 0 0 1 12.75 12H7.4l-3.15 2.36A.5.5 0 0 1 3.5 14v-2H3.25A1.75 1.75 0 0 1 1.5 10.25v-5A1.75 1.75 0 0 1 3.25 3.5Z';
24
- const STYLE = `
25
- :host {
26
- display: block;
27
- }
28
-
29
- [part="container"] {
30
- position: relative;
31
- background: var(--crtr-color-background);
32
- border: 1px solid var(--crtr-color-border);
33
- border-radius: var(--crtr-radius-medium);
34
- box-shadow: var(--crtr-shadow);
35
- overflow: hidden;
36
- }
37
-
38
- [part="container"]:focus-visible {
39
- outline: 2px solid var(--crtr-color-focus);
40
- outline-offset: 2px;
41
- }
42
-
43
- [part="table"] {
44
- width: 100%;
45
- border-collapse: collapse;
46
- font: inherit;
47
- color: var(--crtr-color-text);
48
- }
49
-
50
- [part="caption"] {
51
- caption-side: top;
52
- padding: var(--crtr-space-3) var(--crtr-space-3) var(--crtr-space-2);
53
- text-align: left;
54
- color: var(--crtr-color-text-muted);
55
- }
56
-
57
- th {
58
- padding: var(--crtr-space-2) var(--crtr-space-3);
59
- text-align: left;
60
- font-weight: 600;
61
- color: var(--crtr-color-text-muted);
62
- border-bottom: 1px solid var(--crtr-color-border);
63
- white-space: nowrap;
64
- }
65
-
66
- th[data-align="right"] {
67
- text-align: right;
68
- }
69
-
70
- [part="header-cell-inner"] {
71
- display: inline-flex;
72
- align-items: center;
73
- gap: var(--crtr-space-1);
74
- }
75
-
76
- th[data-align="right"] [part="header-cell-inner"] {
77
- flex-direction: row-reverse;
78
- }
79
-
80
- [part="column-toggle"] {
81
- font: inherit;
82
- font-weight: 600;
83
- color: inherit;
84
- background: transparent;
85
- border: 1px solid transparent;
86
- border-radius: var(--crtr-radius-small);
87
- padding: var(--crtr-space-1) var(--crtr-space-2);
88
- margin: calc(var(--crtr-space-1) * -1) calc(var(--crtr-space-2) * -1);
89
- cursor: pointer;
90
- }
91
-
92
- [part="column-toggle"]:hover {
93
- background: var(--crtr-color-surface-hover);
94
- }
95
-
96
- [part="column-toggle"][aria-pressed="true"] {
97
- color: var(--crtr-color-text);
98
- background: color-mix(in srgb, var(--crtr-color-accent) 12%, transparent);
99
- border-color: color-mix(in srgb, var(--crtr-color-accent) 40%, transparent);
100
- }
101
-
102
- [part="column-toggle"]:focus-visible,
103
- [part="comment-trigger"]:focus-visible,
104
- [part="row-control"]:focus-visible,
105
- [part="select-all"]:focus-visible {
106
- outline: 2px solid var(--crtr-color-focus);
107
- outline-offset: 1px;
108
- }
109
-
110
- [part="row"] {
111
- border-bottom: 1px solid color-mix(in srgb, var(--crtr-color-border) 60%, transparent);
112
- }
113
-
114
- [part="row"]:last-child {
115
- border-bottom: none;
116
- }
117
-
118
- [part="row"]:hover,
119
- [part="row"][data-active="true"] {
120
- background: var(--crtr-color-surface-hover);
121
- }
122
-
123
- [part="row"][data-state="selected"],
124
- [part="row"][data-state="selected"]:hover {
125
- background: color-mix(in srgb, var(--crtr-color-accent) 8%, transparent);
126
- }
127
-
128
- [part="row"]:focus-visible {
129
- outline: 2px solid var(--crtr-color-focus);
130
- outline-offset: -2px;
131
- }
132
-
133
- td {
134
- padding: var(--crtr-space-2) var(--crtr-space-3);
135
- vertical-align: middle;
136
- }
137
-
138
- td[data-align="right"] {
139
- text-align: right;
140
- font-variant-numeric: tabular-nums;
141
- }
142
-
143
- td[data-mono="true"] {
144
- font-family: var(--crtr-font-mono);
145
- font-size: 0.8125em;
146
- }
147
-
148
- [data-column-selected="true"] {
149
- background: color-mix(in srgb, var(--crtr-color-accent) 6%, transparent);
150
- }
151
-
152
- [part="select-cell"],
153
- [part="select-head"] {
154
- width: 1px;
155
- padding-right: 0;
156
- }
157
-
158
- [part="comment-cell"],
159
- [part="comment-head"] {
160
- width: 1px;
161
- text-align: right;
162
- padding-left: 0;
163
- }
164
-
165
- [part="row-control"],
166
- [part="select-all"] {
167
- accent-color: var(--crtr-color-accent);
168
- margin: 0;
169
- cursor: pointer;
170
- }
171
-
172
- [part="comment-trigger"] {
173
- display: inline-flex;
174
- align-items: center;
175
- justify-content: center;
176
- width: 1.5rem;
177
- height: 1.5rem;
178
- padding: 0;
179
- color: var(--crtr-color-text-muted);
180
- background: transparent;
181
- border: none;
182
- border-radius: var(--crtr-radius-small);
183
- cursor: pointer;
184
- opacity: 0;
185
- transition: opacity 120ms ease;
186
- }
187
-
188
- [part="comment-trigger"] svg {
189
- width: 0.875rem;
190
- height: 0.875rem;
191
- }
192
-
193
- [part="comment-trigger"][data-has-comment="true"] {
194
- color: var(--crtr-color-accent);
195
- opacity: 1;
196
- }
197
-
198
- [part="comment-trigger"]:hover {
199
- background: var(--crtr-color-surface-hover);
200
- }
201
-
202
- tr:hover [part="comment-trigger"],
203
- tr:focus-within [part="comment-trigger"],
204
- tr[data-active="true"] [part="comment-trigger"],
205
- [part="comment-trigger"]:focus-visible,
206
- [part="comment-trigger"][data-open="true"] {
207
- opacity: 1;
208
- }
209
-
210
- [part="comment-editor"] {
211
- position: absolute;
212
- z-index: 2;
213
- width: min(20rem, calc(100% - var(--crtr-space-4)));
214
- display: grid;
215
- gap: var(--crtr-space-2);
216
- padding: var(--crtr-space-3);
217
- background: var(--crtr-color-surface);
218
- border: 1px solid var(--crtr-color-border);
219
- border-radius: var(--crtr-radius-medium);
220
- box-shadow: var(--crtr-shadow);
221
- }
222
-
223
- [part="comment-editor"][hidden] {
224
- display: none;
225
- }
226
-
227
- [part="comment-target"] {
228
- margin: 0;
229
- font-family: var(--crtr-font-mono);
230
- font-size: 0.75em;
231
- text-transform: uppercase;
232
- letter-spacing: 0.06em;
233
- color: var(--crtr-color-text-muted);
234
- }
235
-
236
- [part="comment-input"] {
237
- font: inherit;
238
- color: var(--crtr-color-text);
239
- background: var(--crtr-color-background);
240
- border: 1px solid var(--crtr-color-border);
241
- border-radius: var(--crtr-radius-small);
242
- padding: var(--crtr-space-2);
243
- min-height: 4rem;
244
- resize: vertical;
245
- }
246
-
247
- [part="comment-input"]:focus-visible {
248
- outline: 2px solid var(--crtr-color-focus);
249
- outline-offset: -1px;
250
- }
251
-
252
- [part="comment-actions"] {
253
- display: flex;
254
- align-items: center;
255
- justify-content: space-between;
256
- gap: var(--crtr-space-2);
257
- }
258
-
259
- [part="comment-hint"] {
260
- font-family: var(--crtr-font-mono);
261
- font-size: 0.75em;
262
- text-transform: uppercase;
263
- letter-spacing: 0.06em;
264
- color: var(--crtr-color-text-muted);
265
- }
266
-
267
- [part="comment-cancel"],
268
- [part="comment-save"] {
269
- font: inherit;
270
- font-size: 0.875em;
271
- padding: var(--crtr-space-1) var(--crtr-space-2);
272
- border-radius: var(--crtr-radius-small);
273
- border: 1px solid var(--crtr-color-border);
274
- cursor: pointer;
275
- }
276
-
277
- [part="comment-cancel"] {
278
- color: var(--crtr-color-text-muted);
279
- background: transparent;
280
- }
281
-
282
- [part="comment-save"] {
283
- color: var(--crtr-color-accent-contrast);
284
- background: var(--crtr-color-accent);
285
- border-color: var(--crtr-color-accent);
286
- }
287
-
288
- [part="comment-cancel"]:focus-visible,
289
- [part="comment-save"]:focus-visible {
290
- outline: 2px solid var(--crtr-color-focus);
291
- outline-offset: 1px;
292
- }
293
-
294
- [part="status"],
295
- [part="unavailable"] {
296
- padding: var(--crtr-space-3);
297
- color: var(--crtr-color-text-muted);
298
- background: var(--crtr-color-surface);
299
- border: 1px solid var(--crtr-color-border);
300
- border-radius: var(--crtr-radius-medium);
301
- box-shadow: var(--crtr-shadow);
302
- }
303
-
304
- [part="reason"] {
305
- color: var(--crtr-color-text);
306
- font-family: var(--crtr-font-mono);
307
- }
308
-
309
- [part="empty"] {
310
- padding: var(--crtr-space-3);
311
- color: var(--crtr-color-text-muted);
312
- text-align: center;
313
- }
314
- `;
315
- /** A cell value the schema allows, as the string a table shows. */
316
- function cellText(value) {
317
- if (value === undefined || value === null)
318
- return '';
319
- return typeof value === 'string' ? value : String(value);
320
- }
321
- function isPlainObject(value) {
322
- return typeof value === 'object' && value !== null && !Array.isArray(value);
323
- }
324
- /**
325
- * The rows inside whatever the host handed back for a `source`. Accepted shapes are the same
326
- * `{ id, cells }` rows an inline config carries, either as a bare array or under a `rows` key;
327
- * anything else is a named unavailable state rather than a guess at the author's intent.
328
- */
329
- function readSourceRows(data) {
330
- const raw = Array.isArray(data) ? data : isPlainObject(data) && Array.isArray(data.rows) ? data.rows : undefined;
331
- if (raw === undefined)
332
- return { status: 'invalid', reason: 'source data is not a list of rows' };
333
- const rows = [];
334
- for (let index = 0; index < raw.length; index += 1) {
335
- const row = raw[index];
336
- if (!isPlainObject(row) || typeof row.id !== 'string' || row.id === '' || !isPlainObject(row.cells)) {
337
- return { status: 'invalid', reason: `source row #${index} is not { id, cells }` };
338
- }
339
- const cells = {};
340
- for (const [key, value] of Object.entries(row.cells)) {
341
- if (value === undefined)
342
- continue;
343
- if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
344
- return { status: 'invalid', reason: `source row "${row.id}" cell "${key}" is not a string, number, boolean or null` };
345
- }
346
- cells[key] = value;
347
- }
348
- rows.push({ id: row.id, cells });
349
- }
350
- return { status: 'ok', rows };
351
- }
352
- function commentIcon() {
353
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
354
- svg.setAttribute('viewBox', '0 0 16 16');
355
- svg.setAttribute('aria-hidden', 'true');
356
- svg.setAttribute('focusable', 'false');
357
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
358
- path.setAttribute('d', COMMENT_BUBBLE);
359
- path.setAttribute('fill', 'none');
360
- path.setAttribute('stroke', 'currentColor');
361
- path.setAttribute('stroke-width', '1.25');
362
- path.setAttribute('stroke-linejoin', 'round');
363
- svg.append(path);
364
- return svg;
365
- }
366
- function isTextEntry(target) {
367
- if (!(target instanceof HTMLElement))
368
- return false;
369
- return target.closest('input[type="text"], textarea, [contenteditable="true"]') !== null;
370
- }
371
- class CrtrTableElement extends HTMLElement {
372
- #root;
373
- #slotId;
374
- #config;
375
- #rows = [];
376
- #selectedRowIds = [];
377
- #selectedColumnIds = [];
378
- #comments = [];
379
- #activeRowId = null;
380
- #editorAnchor = null;
381
- #editorTrigger;
382
- #connected = false;
383
- #container;
384
- #editor;
385
- #editorTarget;
386
- #editorInput;
387
- #editorHint;
388
- #selectAll;
389
- #rowParts = new Map();
390
- #columnParts = new Map();
391
- #wholeComment;
392
- constructor() {
393
- super();
394
- this.#root = this.attachShadow({ mode: 'open' });
395
- }
396
- connectedCallback() {
397
- this.#connected = true;
398
- const read = readSlotConfig(this);
399
- if (read.status !== 'ok') {
400
- this.#renderUnavailable(read.reason);
401
- return;
402
- }
403
- const config = read.config;
404
- if (!Array.isArray(config.columns) || config.columns.length === 0) {
405
- this.#renderUnavailable('table config has no columns');
406
- return;
407
- }
408
- if (config.rows !== undefined && config.source !== undefined) {
409
- this.#renderUnavailable('table rows and source are mutually exclusive');
410
- return;
411
- }
412
- this.#config = config;
413
- this.#slotId = readSlotId(this);
414
- if (config.source !== undefined) {
415
- this.#renderStatus('Loading table data…');
416
- void this.#loadSource(config.source);
417
- return;
418
- }
419
- if (!Array.isArray(config.rows)) {
420
- this.#renderUnavailable('table config has neither rows nor source');
421
- return;
422
- }
423
- this.#rows = config.rows;
424
- this.#hydrate();
425
- this.#renderTable();
426
- }
427
- disconnectedCallback() {
428
- this.#connected = false;
429
- }
430
- async #loadSource(source) {
431
- const result = await artifactData(source);
432
- if (!this.#connected)
433
- return;
434
- if (result.status !== 'ok') {
435
- this.#renderUnavailable(`table source "${source}" is ${result.status}: ${result.reason}`);
436
- return;
437
- }
438
- const rows = readSourceRows(result.data);
439
- if (rows.status !== 'ok') {
440
- this.#renderUnavailable(`table source "${source}": ${rows.reason}`);
441
- return;
442
- }
443
- this.#rows = rows.rows;
444
- this.#hydrate();
445
- this.#renderTable();
446
- }
447
- /**
448
- * Seed local state from the answer the host is already holding, so a reopened page shows
449
- * the rows and columns that were picked. It runs once the rows are known — after a `source`
450
- * load, not before — because a saved id is only honoured while it still names something on
451
- * screen. The select modes are re-imposed for the same reason.
452
- */
453
- #hydrate() {
454
- if (this.#slotId === undefined)
455
- return;
456
- const saved = savedResponse(this.#slotId);
457
- if (saved === undefined)
458
- return;
459
- const rows = this.#orderRows(savedIds(saved.selectedRowIds));
460
- const columns = this.#orderColumns(savedIds(saved.selectedColumnIds));
461
- this.#selectedRowIds = this.#rowSelect === 'none' ? [] : this.#rowSelect === 'single' ? rows.slice(0, 1) : rows;
462
- this.#selectedColumnIds =
463
- this.#columnSelect === 'none' ? [] : this.#columnSelect === 'single' ? columns.slice(0, 1) : columns;
464
- const rowIds = new Set(this.#rows.map((row) => row.id));
465
- const columnIds = new Set((this.#config?.columns ?? []).map((column) => column.id));
466
- this.#comments = adoptComments(saved.comments).filter((comment) => {
467
- if (comment.anchor.kind === 'row')
468
- return rowIds.has(comment.anchor.rowId);
469
- if (comment.anchor.kind === 'column')
470
- return columnIds.has(comment.anchor.columnId);
471
- return true;
472
- });
473
- }
474
- // ── the response ───────────────────────────────────────────────────────────────────────
475
- get #rowSelect() {
476
- return this.#config?.rowSelect ?? 'none';
477
- }
478
- get #columnSelect() {
479
- return this.#config?.columnSelect ?? 'none';
480
- }
481
- #response() {
482
- return {
483
- selectedRowIds: [...this.#selectedRowIds],
484
- selectedColumnIds: [...this.#selectedColumnIds],
485
- comments: [...this.#comments],
486
- };
487
- }
488
- /** Every user change hands the host this slot's complete current response, never a delta. */
489
- #commit() {
490
- this.#syncState();
491
- if (this.#slotId !== undefined)
492
- respond(this.#slotId, this.#response());
493
- }
494
- /** Selection stays in the order the agent authored, not the order the user clicked. */
495
- #orderRows(ids) {
496
- return this.#rows.map((row) => row.id).filter((id) => ids.includes(id));
497
- }
498
- #orderColumns(ids) {
499
- return (this.#config?.columns ?? []).map((column) => column.id).filter((id) => ids.includes(id));
500
- }
501
- #setRowSelected(rowId, selected) {
502
- const mode = this.#rowSelect;
503
- if (mode === 'none')
504
- return;
505
- if (mode === 'single') {
506
- this.#selectedRowIds = selected ? [rowId] : [];
507
- }
508
- else {
509
- this.#selectedRowIds = this.#orderRows(selected ? [...this.#selectedRowIds, rowId] : this.#selectedRowIds.filter((id) => id !== rowId));
510
- }
511
- this.#commit();
512
- }
513
- #toggleRow(rowId) {
514
- if (this.#rowSelect === 'single') {
515
- this.#setRowSelected(rowId, true);
516
- return;
517
- }
518
- this.#setRowSelected(rowId, !this.#selectedRowIds.includes(rowId));
519
- }
520
- #setColumnSelected(columnId, selected) {
521
- const mode = this.#columnSelect;
522
- if (mode === 'none')
523
- return;
524
- if (mode === 'single') {
525
- this.#selectedColumnIds = selected ? [columnId] : [];
526
- }
527
- else {
528
- this.#selectedColumnIds = this.#orderColumns(selected ? [...this.#selectedColumnIds, columnId] : this.#selectedColumnIds.filter((id) => id !== columnId));
529
- }
530
- this.#commit();
531
- }
532
- #toggleColumn(columnId) {
533
- this.#setColumnSelected(columnId, !this.#selectedColumnIds.includes(columnId));
534
- }
535
- #setAllRows(selected) {
536
- if (this.#rowSelect !== 'multi')
537
- return;
538
- this.#selectedRowIds = selected ? this.#rows.map((row) => row.id) : [];
539
- this.#commit();
540
- }
541
- // ── rendering ──────────────────────────────────────────────────────────────────────────
542
- #reset() {
543
- this.#root.replaceChildren();
544
- this.#rowParts.clear();
545
- this.#columnParts.clear();
546
- this.#container = undefined;
547
- this.#editor = undefined;
548
- this.#editorTrigger = undefined;
549
- this.#editorAnchor = null;
550
- this.#selectAll = undefined;
551
- this.#wholeComment = undefined;
552
- const style = document.createElement('style');
553
- style.textContent = STYLE;
554
- this.#root.append(style);
555
- }
556
- #renderUnavailable(reason) {
557
- this.#reset();
558
- const box = document.createElement('div');
559
- box.part.add('unavailable');
560
- box.setAttribute('role', 'status');
561
- const detail = document.createElement('span');
562
- detail.part.add('reason');
563
- detail.textContent = reason;
564
- box.append('Table unavailable: ', detail);
565
- this.#root.append(box);
566
- }
567
- #renderStatus(text) {
568
- this.#reset();
569
- const box = document.createElement('div');
570
- box.part.add('status');
571
- box.setAttribute('role', 'status');
572
- box.textContent = text;
573
- this.#root.append(box);
574
- }
575
- #renderTable() {
576
- const config = this.#config;
577
- if (config === undefined)
578
- return;
579
- this.#reset();
580
- const container = document.createElement('div');
581
- container.part.add('container');
582
- container.tabIndex = 0;
583
- container.addEventListener('keydown', (event) => this.#onKeyDown(event));
584
- container.addEventListener('focus', () => {
585
- if (this.#activeRowId === null && this.#rows.length > 0) {
586
- this.#activeRowId = this.#rows[0].id;
587
- this.#syncState();
588
- }
589
- });
590
- this.#container = container;
591
- const table = document.createElement('table');
592
- table.part.add('table');
593
- if (config.caption !== undefined && config.caption !== '') {
594
- const caption = document.createElement('caption');
595
- caption.part.add('caption');
596
- caption.textContent = config.caption;
597
- table.append(caption);
598
- }
599
- table.append(this.#buildHead(config), this.#buildBody(config));
600
- container.append(table);
601
- if (this.#rows.length === 0) {
602
- const empty = document.createElement('div');
603
- empty.part.add('empty');
604
- empty.textContent = 'No rows.';
605
- container.append(empty);
606
- }
607
- container.append(this.#buildEditor());
608
- this.#root.append(container);
609
- this.#syncState();
610
- }
611
- #buildHead(config) {
612
- const head = document.createElement('thead');
613
- const tr = document.createElement('tr');
614
- tr.part.add('header-row');
615
- if (this.#rowSelect !== 'none') {
616
- const th = document.createElement('th');
617
- th.part.add('select-head');
618
- th.setAttribute('scope', 'col');
619
- if (this.#rowSelect === 'multi') {
620
- const all = document.createElement('input');
621
- all.type = 'checkbox';
622
- all.part.add('select-all');
623
- all.setAttribute('aria-label', 'Select every row');
624
- all.addEventListener('change', () => this.#setAllRows(all.checked));
625
- this.#selectAll = all;
626
- th.append(all);
627
- }
628
- tr.append(th);
629
- }
630
- for (const column of config.columns) {
631
- const th = document.createElement('th');
632
- th.part.add('column-header');
633
- th.setAttribute('scope', 'col');
634
- th.dataset.columnHeader = column.id;
635
- if (column.align === 'right')
636
- th.dataset.align = 'right';
637
- const inner = document.createElement('span');
638
- inner.part.add('header-cell-inner');
639
- let toggle;
640
- if (this.#columnSelect === 'none') {
641
- const label = document.createElement('span');
642
- label.textContent = column.label;
643
- inner.append(label);
644
- }
645
- else {
646
- toggle = document.createElement('button');
647
- toggle.type = 'button';
648
- toggle.part.add('column-toggle');
649
- toggle.dataset.columnToggle = column.id;
650
- toggle.setAttribute('aria-pressed', 'false');
651
- toggle.setAttribute('aria-label', `Select the ${column.label} column`);
652
- toggle.textContent = column.label;
653
- toggle.addEventListener('click', () => this.#toggleColumn(column.id));
654
- inner.append(toggle);
655
- }
656
- const comment = this.#buildCommentTrigger({ kind: 'column', columnId: column.id }, `the ${column.label} column`);
657
- inner.append(comment);
658
- th.append(inner);
659
- tr.append(th);
660
- this.#columnParts.set(column.id, { toggle, comment, cells: [], head: th });
661
- }
662
- const trailing = document.createElement('th');
663
- trailing.part.add('comment-head');
664
- trailing.setAttribute('scope', 'col');
665
- this.#wholeComment = this.#buildCommentTrigger({ kind: 'whole' }, 'this table');
666
- trailing.append(this.#wholeComment);
667
- tr.append(trailing);
668
- head.append(tr);
669
- return head;
670
- }
671
- #buildBody(config) {
672
- const body = document.createElement('tbody');
673
- for (const row of this.#rows) {
674
- const tr = document.createElement('tr');
675
- tr.part.add('row');
676
- tr.dataset.rowId = row.id;
677
- tr.tabIndex = -1;
678
- tr.addEventListener('mouseenter', () => this.#setActiveRow(row.id));
679
- tr.addEventListener('focusin', () => this.#setActiveRow(row.id));
680
- let control;
681
- if (this.#rowSelect !== 'none') {
682
- const td = document.createElement('td');
683
- td.part.add('select-cell');
684
- const input = document.createElement('input');
685
- input.type = this.#rowSelect === 'single' ? 'radio' : 'checkbox';
686
- input.part.add('row-control');
687
- if (this.#rowSelect === 'single')
688
- input.name = `${this.#slotId ?? 'crtr-table'}-row`;
689
- input.value = row.id;
690
- input.setAttribute('aria-label', `Select row ${row.id}`);
691
- input.addEventListener('change', () => this.#setRowSelected(row.id, input.checked));
692
- control = input;
693
- td.append(input);
694
- tr.append(td);
695
- }
696
- for (const column of config.columns) {
697
- const td = document.createElement('td');
698
- td.part.add('cell');
699
- td.dataset.columnId = column.id;
700
- if (column.align === 'right')
701
- td.dataset.align = 'right';
702
- if (column.mono === true)
703
- td.dataset.mono = 'true';
704
- td.textContent = cellText(row.cells[column.id]);
705
- tr.append(td);
706
- this.#columnParts.get(column.id)?.cells.push(td);
707
- }
708
- const commentCell = document.createElement('td');
709
- commentCell.part.add('comment-cell');
710
- const comment = this.#buildCommentTrigger({ kind: 'row', rowId: row.id }, `row ${row.id}`);
711
- commentCell.append(comment);
712
- tr.append(commentCell);
713
- body.append(tr);
714
- this.#rowParts.set(row.id, { tr, ...(control === undefined ? {} : { control }), comment });
715
- }
716
- return body;
717
- }
718
- #buildCommentTrigger(anchor, target) {
719
- const button = document.createElement('button');
720
- button.type = 'button';
721
- button.part.add('comment-trigger');
722
- button.setAttribute('aria-label', `Comment on ${target}`);
723
- button.dataset.commentTarget = target;
724
- button.append(commentIcon());
725
- button.addEventListener('click', () => this.#openEditor(anchor, button));
726
- return button;
727
- }
728
- #buildEditor() {
729
- const editor = document.createElement('div');
730
- editor.part.add('comment-editor');
731
- editor.hidden = true;
732
- editor.setAttribute('role', 'dialog');
733
- editor.setAttribute('aria-label', 'Comment');
734
- const target = document.createElement('p');
735
- target.part.add('comment-target');
736
- editor.append(target);
737
- const input = document.createElement('textarea');
738
- input.part.add('comment-input');
739
- input.placeholder = 'What should the agent know about this?';
740
- input.addEventListener('keydown', (event) => {
741
- if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
742
- event.preventDefault();
743
- this.#saveComment();
744
- }
745
- else if (event.key === 'Escape') {
746
- event.preventDefault();
747
- this.#closeEditor();
748
- }
749
- });
750
- editor.append(input);
751
- const actions = document.createElement('div');
752
- actions.part.add('comment-actions');
753
- const hint = document.createElement('span');
754
- hint.part.add('comment-hint');
755
- actions.append(hint);
756
- const buttons = document.createElement('span');
757
- const cancel = document.createElement('button');
758
- cancel.type = 'button';
759
- cancel.part.add('comment-cancel');
760
- cancel.textContent = 'Cancel';
761
- cancel.addEventListener('click', () => this.#closeEditor());
762
- const save = document.createElement('button');
763
- save.type = 'button';
764
- save.part.add('comment-save');
765
- save.textContent = 'Save';
766
- save.addEventListener('click', () => this.#saveComment());
767
- buttons.append(cancel, ' ', save);
768
- actions.append(buttons);
769
- editor.append(actions);
770
- this.#editor = editor;
771
- this.#editorTarget = target;
772
- this.#editorInput = input;
773
- this.#editorHint = hint;
774
- return editor;
775
- }
776
- // ── comments ───────────────────────────────────────────────────────────────────────────
777
- #openEditor(anchor, trigger) {
778
- const editor = this.#editor;
779
- const container = this.#container;
780
- if (editor === undefined || container === undefined)
781
- return;
782
- this.#editorAnchor = anchor;
783
- this.#editorTrigger = trigger;
784
- const existing = findComment(this.#comments, anchor);
785
- // The box is seeded from the saved comment every time it opens, so an abandoned edit
786
- // never leaks into the next one.
787
- if (this.#editorInput !== undefined)
788
- this.#editorInput.value = existing?.text ?? '';
789
- if (this.#editorTarget !== undefined) {
790
- this.#editorTarget.textContent = `Comment on ${trigger.dataset.commentTarget ?? anchorLabel(anchor)}`;
791
- }
792
- if (this.#editorHint !== undefined)
793
- this.#editorHint.textContent = existing === undefined ? '⌘↵ to save' : 'empty to delete';
794
- const bounds = container.getBoundingClientRect();
795
- const rect = trigger.getBoundingClientRect();
796
- editor.hidden = false;
797
- editor.style.top = `${Math.max(0, rect.bottom - bounds.top + 4)}px`;
798
- editor.style.right = `${Math.max(0, bounds.right - rect.right)}px`;
799
- this.#syncState();
800
- this.#editorInput?.focus();
801
- }
802
- /**
803
- * Closing hands focus back to the trigger that opened the editor. Without it the keyboard
804
- * grammar dies on save: the focused Save button is inside the panel being hidden, so focus
805
- * falls out of the element entirely and j/k/x/c stop arriving.
806
- */
807
- #closeEditor() {
808
- const trigger = this.#editorTrigger;
809
- this.#editorAnchor = null;
810
- this.#editorTrigger = undefined;
811
- if (this.#editor !== undefined)
812
- this.#editor.hidden = true;
813
- this.#syncState();
814
- trigger?.focus();
815
- }
816
- #saveComment() {
817
- const anchor = this.#editorAnchor;
818
- if (anchor === null)
819
- return;
820
- this.#comments = setComment(this.#comments, anchor, this.#editorInput?.value ?? '');
821
- this.#closeEditor();
822
- this.#commit();
823
- }
824
- // ── interaction ────────────────────────────────────────────────────────────────────────
825
- #setActiveRow(rowId) {
826
- if (this.#activeRowId === rowId)
827
- return;
828
- this.#activeRowId = rowId;
829
- this.#syncState();
830
- }
831
- #focusRow(rowId) {
832
- const parts = this.#rowParts.get(rowId);
833
- (parts?.control ?? parts?.tr)?.focus();
834
- }
835
- /**
836
- * The kit's classic list grammar: j/k move (wrapping) and carry DOM focus with them, x
837
- * selects, c comments. A key pressed on a column toggle acts on that column instead, since
838
- * the header IS the column. Modified keys and anything typed in the comment box pass through.
839
- */
840
- #onKeyDown(event) {
841
- if (event.metaKey || event.ctrlKey || event.altKey)
842
- return;
843
- const target = event.composedPath()[0] ?? event.target;
844
- if (isTextEntry(target))
845
- return;
846
- const key = event.key.toLowerCase();
847
- if (key !== 'j' && key !== 'k' && key !== 'x' && key !== 'c')
848
- return;
849
- // Anything focused inside a header cell — the toggle or that column's comment trigger —
850
- // acts on the column, because the header IS the column.
851
- const columnId = target instanceof HTMLElement ? target.closest('[data-column-header]')?.dataset.columnHeader : undefined;
852
- if (columnId !== undefined && (key === 'x' || key === 'c')) {
853
- event.preventDefault();
854
- if (key === 'x')
855
- this.#toggleColumn(columnId);
856
- else {
857
- const parts = this.#columnParts.get(columnId);
858
- if (parts !== undefined)
859
- this.#openEditor({ kind: 'column', columnId }, parts.comment);
860
- }
861
- return;
862
- }
863
- const ids = this.#rows.map((row) => row.id);
864
- if (ids.length === 0)
865
- return;
866
- const currentId = this.#activeRowId ?? this.#selectedRowIds[this.#selectedRowIds.length - 1] ?? ids[0];
867
- if (key === 'j' || key === 'k') {
868
- event.preventDefault();
869
- const index = Math.max(0, ids.indexOf(currentId));
870
- const nextId = ids[(index + (key === 'j' ? 1 : -1) + ids.length) % ids.length];
871
- this.#setActiveRow(nextId);
872
- this.#focusRow(nextId);
873
- return;
874
- }
875
- event.preventDefault();
876
- if (key === 'x') {
877
- this.#toggleRow(currentId);
878
- return;
879
- }
880
- const parts = this.#rowParts.get(currentId);
881
- if (parts !== undefined)
882
- this.#openEditor({ kind: 'row', rowId: currentId }, parts.comment);
883
- }
884
- // ── state → DOM ────────────────────────────────────────────────────────────────────────
885
- #syncCommentTrigger(button, anchor) {
886
- const comment = findComment(this.#comments, anchor);
887
- const target = button.dataset.commentTarget ?? anchorLabel(anchor);
888
- button.dataset.hasComment = comment === undefined ? 'false' : 'true';
889
- button.setAttribute('aria-label', comment === undefined ? `Comment on ${target}` : `Edit comment on ${target}`);
890
- button.title = comment?.text ?? '';
891
- const path = button.querySelector('path');
892
- path?.setAttribute('fill', comment === undefined ? 'none' : 'currentColor');
893
- button.dataset.open = this.#isEditorAnchor(anchor) ? 'true' : 'false';
894
- }
895
- #isEditorAnchor(anchor) {
896
- const current = this.#editorAnchor;
897
- if (current === null || current.kind !== anchor.kind)
898
- return false;
899
- if (current.kind === 'row' && anchor.kind === 'row')
900
- return current.rowId === anchor.rowId;
901
- if (current.kind === 'column' && anchor.kind === 'column')
902
- return current.columnId === anchor.columnId;
903
- return current.kind === 'whole';
904
- }
905
- #syncState() {
906
- for (const [rowId, parts] of this.#rowParts) {
907
- const selected = this.#selectedRowIds.includes(rowId);
908
- if (selected)
909
- parts.tr.dataset.state = 'selected';
910
- else
911
- delete parts.tr.dataset.state;
912
- parts.tr.dataset.active = this.#activeRowId === rowId ? 'true' : 'false';
913
- parts.tr.setAttribute('aria-selected', selected ? 'true' : 'false');
914
- if (parts.control !== undefined)
915
- parts.control.checked = selected;
916
- this.#syncCommentTrigger(parts.comment, { kind: 'row', rowId });
917
- }
918
- for (const [columnId, parts] of this.#columnParts) {
919
- const selected = this.#selectedColumnIds.includes(columnId);
920
- parts.toggle?.setAttribute('aria-pressed', selected ? 'true' : 'false');
921
- parts.head.dataset.columnSelected = selected ? 'true' : 'false';
922
- for (const cell of parts.cells)
923
- cell.dataset.columnSelected = selected ? 'true' : 'false';
924
- this.#syncCommentTrigger(parts.comment, { kind: 'column', columnId });
925
- }
926
- if (this.#wholeComment !== undefined)
927
- this.#syncCommentTrigger(this.#wholeComment, { kind: 'whole' });
928
- if (this.#selectAll !== undefined) {
929
- const all = this.#rows.length > 0 && this.#selectedRowIds.length === this.#rows.length;
930
- this.#selectAll.checked = all;
931
- this.#selectAll.indeterminate = this.#selectedRowIds.length > 0 && !all;
932
- }
933
- }
934
- }
935
- registerElement('crtr-table', CrtrTableElement);