@north-light/crouter 0.3.198 → 0.3.200

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 (316) 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__/helpers/harness.js +17 -55
  99. package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
  100. package/dist/core/__tests__/human-deliver.test.js +31 -26
  101. package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
  102. package/dist/core/__tests__/plugin-page-components.test.js +157 -0
  103. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +4 -5
  104. package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
  105. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
  106. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
  107. package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
  108. package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
  109. package/dist/core/__tests__/serial/worktree.test.js +24 -198
  110. package/dist/core/__tests__/session-cycles.test.js +22 -0
  111. package/dist/core/__tests__/stop-guard.test.js +4 -4
  112. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
  113. package/dist/core/bash-jobs.d.ts +6 -0
  114. package/dist/core/bash-jobs.js +10 -0
  115. package/dist/core/canvas/__tests__/attention.test.js +8 -5
  116. package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
  117. package/dist/core/canvas/attention.js +8 -6
  118. package/dist/core/canvas/browse/model.d.ts +8 -2
  119. package/dist/core/canvas/browse/model.js +11 -4
  120. package/dist/core/canvas/canvas.d.ts +7 -0
  121. package/dist/core/canvas/canvas.js +37 -0
  122. package/dist/core/canvas/crons.d.ts +9 -0
  123. package/dist/core/canvas/crons.js +51 -0
  124. package/dist/core/canvas/node-order.d.ts +22 -1
  125. package/dist/core/canvas/node-order.js +26 -1
  126. package/dist/core/canvas/render-source.d.ts +5 -0
  127. package/dist/core/canvas/render-source.js +1 -0
  128. package/dist/core/canvas/types.d.ts +9 -3
  129. package/dist/core/command-plugins/bundle.d.ts +6 -1
  130. package/dist/core/command-plugins/bundle.js +26 -13
  131. package/dist/core/command.js +2 -1
  132. package/dist/core/config.d.ts +47 -1
  133. package/dist/core/config.js +137 -4
  134. package/dist/core/help.js +1 -1
  135. package/dist/core/human/__tests__/page-catalog.test.js +4 -0
  136. package/dist/core/human/__tests__/page-tickets.test.js +65 -52
  137. package/dist/core/human/__tests__/page.test.js +56 -98
  138. package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
  139. package/dist/core/human/answer-text.js +3 -3
  140. package/dist/core/human/answer.d.ts +6 -4
  141. package/dist/core/human/answer.js +4 -10
  142. package/dist/core/human/component-docs.d.ts +11 -7
  143. package/dist/core/human/component-docs.js +361 -88
  144. package/dist/core/human/convention.d.ts +17 -3
  145. package/dist/core/human/convention.js +36 -5
  146. package/dist/core/human/feedback-companion.d.ts +20 -0
  147. package/dist/core/human/feedback-companion.js +98 -0
  148. package/dist/core/human/feedback.d.ts +65 -0
  149. package/dist/core/human/feedback.js +116 -0
  150. package/dist/core/human/page-catalog.d.ts +40 -5
  151. package/dist/core/human/page-catalog.js +102 -30
  152. package/dist/core/human/page-errors.d.ts +4 -0
  153. package/dist/core/human/page-errors.js +7 -0
  154. package/dist/core/human/page-eval.d.ts +20 -0
  155. package/dist/core/human/page-eval.js +123 -0
  156. package/dist/core/human/page-schema.d.ts +23 -191
  157. package/dist/core/human/page-schema.js +60 -89
  158. package/dist/core/human/page-synth.d.ts +1 -1
  159. package/dist/core/human/page-synth.js +15 -18
  160. package/dist/core/human/page.d.ts +12 -27
  161. package/dist/core/human/page.js +142 -474
  162. package/dist/core/human/root.d.ts +6 -2
  163. package/dist/core/human/root.js +12 -4
  164. package/dist/core/human/scan.d.ts +3 -4
  165. package/dist/core/human/scan.js +16 -15
  166. package/dist/core/human/summary.d.ts +1 -2
  167. package/dist/core/human/summary.js +2 -2
  168. package/dist/core/human/tickets.d.ts +20 -14
  169. package/dist/core/human/tickets.js +91 -31
  170. package/dist/core/human/types.d.ts +10 -2
  171. package/dist/core/preview-registry.js +8 -10
  172. package/dist/core/profiles/default-binding.d.ts +4 -0
  173. package/dist/core/profiles/default-binding.js +24 -0
  174. package/dist/core/profiles/deletion-reservation.d.ts +8 -0
  175. package/dist/core/profiles/deletion-reservation.js +38 -0
  176. package/dist/core/profiles/manifest.d.ts +3 -0
  177. package/dist/core/profiles/manifest.js +12 -0
  178. package/dist/core/profiles/select.js +2 -2
  179. package/dist/core/review/realize.js +3 -3
  180. package/dist/core/review/store.d.ts +6 -0
  181. package/dist/core/review/store.js +31 -0
  182. package/dist/core/runtime/bearings.d.ts +0 -4
  183. package/dist/core/runtime/bearings.js +4 -62
  184. package/dist/core/runtime/bin-contributions.d.ts +44 -0
  185. package/dist/core/runtime/bin-contributions.js +186 -0
  186. package/dist/core/runtime/broker/client-registry.d.ts +4 -0
  187. package/dist/core/runtime/broker/client-registry.js +13 -1
  188. package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
  189. package/dist/core/runtime/broker/extension-abort.js +4 -2
  190. package/dist/core/runtime/broker/fault-retry.js +4 -0
  191. package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
  192. package/dist/core/runtime/broker/frame-dispatch.js +5 -6
  193. package/dist/core/runtime/broker/read-ops.d.ts +1 -1
  194. package/dist/core/runtime/broker/read-ops.js +2 -2
  195. package/dist/core/runtime/broker/rebind.d.ts +1 -0
  196. package/dist/core/runtime/broker/rebind.js +6 -1
  197. package/dist/core/runtime/broker-extension-render.js +19 -0
  198. package/dist/core/runtime/broker-protocol.d.ts +14 -0
  199. package/dist/core/runtime/broker.d.ts +1 -1
  200. package/dist/core/runtime/broker.js +17 -6
  201. package/dist/core/runtime/front-door-env.d.ts +5 -0
  202. package/dist/core/runtime/front-door-env.js +16 -0
  203. package/dist/core/runtime/front-door.d.ts +1 -5
  204. package/dist/core/runtime/front-door.js +2 -5
  205. package/dist/core/runtime/host.d.ts +1 -1
  206. package/dist/core/runtime/invocation.d.ts +6 -0
  207. package/dist/core/runtime/invocation.js +10 -0
  208. package/dist/core/runtime/kickoff.js +34 -0
  209. package/dist/core/runtime/launch.d.ts +2 -6
  210. package/dist/core/runtime/node-read.js +4 -1
  211. package/dist/core/runtime/nodes.js +2 -0
  212. package/dist/core/runtime/promote.d.ts +1 -0
  213. package/dist/core/runtime/promote.js +3 -1
  214. package/dist/core/runtime/session-cycles.d.ts +6 -0
  215. package/dist/core/runtime/session-cycles.js +15 -1
  216. package/dist/core/runtime/session-visibility.d.ts +17 -8
  217. package/dist/core/runtime/session-visibility.js +19 -10
  218. package/dist/core/runtime/spawn-env.d.ts +9 -1
  219. package/dist/core/runtime/spawn-env.js +62 -2
  220. package/dist/core/runtime/stop-guard.d.ts +2 -2
  221. package/dist/core/runtime/stop-guard.js +2 -2
  222. package/dist/core/runtime/stop-signals.d.ts +1 -1
  223. package/dist/core/runtime/stop-signals.js +2 -2
  224. package/dist/core/runtime/tmux-bindings.js +1 -1
  225. package/dist/core/session-model/session-state.d.ts +4 -3
  226. package/dist/core/session-model/session-state.js +14 -1
  227. package/dist/core/user-settings.d.ts +21 -2
  228. package/dist/core/user-settings.js +32 -11
  229. package/dist/core/worktree.d.ts +15 -45
  230. package/dist/core/worktree.js +59 -261
  231. package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
  232. package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
  233. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  234. package/dist/daemon/api/handlers/broker-ops.js +9 -5
  235. package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
  236. package/dist/daemon/api/handlers/feedback-comments.js +207 -0
  237. package/dist/daemon/api/handlers/human.js +24 -20
  238. package/dist/daemon/api/handlers/inbox.d.ts +10 -0
  239. package/dist/daemon/api/handlers/inbox.js +39 -115
  240. package/dist/daemon/api/handlers/profiles.js +11 -29
  241. package/dist/daemon/api/map.d.ts +6 -1
  242. package/dist/daemon/api/map.js +23 -0
  243. package/dist/daemon/api/server.js +3 -1
  244. package/dist/daemon/companion-retire.d.ts +2 -0
  245. package/dist/daemon/companion-retire.js +33 -0
  246. package/dist/daemon/cron-run.js +14 -16
  247. package/dist/daemon/human/finish.d.ts +12 -7
  248. package/dist/daemon/human/finish.js +101 -38
  249. package/dist/daemon/human/sweep.js +5 -1
  250. package/dist/daemon/profile-delete.d.ts +7 -0
  251. package/dist/daemon/profile-delete.js +306 -0
  252. package/dist/daemon/reconcilers/broker-supervision.js +3 -1
  253. package/dist/daemon/review/deliver.js +3 -3
  254. package/dist/daemon/review/finish.d.ts +0 -2
  255. package/dist/daemon/review/finish.js +1 -29
  256. package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
  257. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
  258. package/dist/pi-extensions/canvas-bash-valve.js +19 -5
  259. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  260. package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
  261. package/dist/pi-extensions/canvas-review-boundary.js +18 -9
  262. package/dist/pi-extensions/canvas-stophook.js +4 -4
  263. package/dist/shared/generated-context.js +2 -2
  264. package/dist/types.d.ts +44 -2
  265. package/dist/types.js +7 -2
  266. package/package.json +6 -5
  267. package/runtime.lock.json +5 -33
  268. package/dist/builtin-memory/init.md +0 -38
  269. package/dist/builtin-memory/plan.md +0 -19
  270. package/dist/builtin-memory/spec.md +0 -19
  271. package/dist/commands/human/doc.d.ts +0 -2
  272. package/dist/commands/human/doc.js +0 -91
  273. package/dist/commands/human/html.js +0 -81
  274. package/dist/commands/human/inbox.d.ts +0 -2
  275. package/dist/core/human/__tests__/html-markdown.test.js +0 -52
  276. package/dist/core/human/__tests__/page-render.test.js +0 -66
  277. package/dist/core/human/html-markdown.d.ts +0 -3
  278. package/dist/core/human/html-markdown.js +0 -288
  279. package/dist/core/human/page-render.d.ts +0 -4
  280. package/dist/core/human/page-render.js +0 -105
  281. package/dist/pages/bundle.css +0 -1
  282. package/dist/pages/bundle.js +0 -1778
  283. package/dist/pages/comments.d.ts +0 -72
  284. package/dist/pages/comments.js +0 -176
  285. package/dist/pages/controls.d.ts +0 -98
  286. package/dist/pages/controls.js +0 -261
  287. package/dist/pages/elements/cards.d.ts +0 -28
  288. package/dist/pages/elements/cards.js +0 -915
  289. package/dist/pages/elements/chart.d.ts +0 -23
  290. package/dist/pages/elements/chart.js +0 -664
  291. package/dist/pages/elements/options.d.ts +0 -30
  292. package/dist/pages/elements/options.js +0 -742
  293. package/dist/pages/elements/pages.d.ts +0 -30
  294. package/dist/pages/elements/pages.js +0 -377
  295. package/dist/pages/elements/slot.d.ts +0 -26
  296. package/dist/pages/elements/slot.js +0 -112
  297. package/dist/pages/elements/table.d.ts +0 -36
  298. package/dist/pages/elements/table.js +0 -1043
  299. package/dist/pages/elements/text.d.ts +0 -31
  300. package/dist/pages/elements/text.js +0 -1175
  301. package/dist/pages/entry.d.ts +0 -8
  302. package/dist/pages/entry.js +0 -8
  303. package/dist/pages/host.d.ts +0 -134
  304. package/dist/pages/host.js +0 -165
  305. package/dist/pages/readonly.d.ts +0 -24
  306. package/dist/pages/readonly.js +0 -31
  307. package/dist/pages/register.d.ts +0 -2
  308. package/dist/pages/register.js +0 -3
  309. package/dist/pages/responses.d.ts +0 -37
  310. package/dist/pages/responses.js +0 -56
  311. package/dist/pages/slot-config.d.ts +0 -36
  312. package/dist/pages/slot-config.js +0 -50
  313. package/dist/pages/types.d.ts +0 -98
  314. package/dist/pages/types.js +0 -23
  315. /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
  316. /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
@@ -1,1043 +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
- * ## Styling
20
- *
21
- * Every styling value below is read through a `--crtr-table-*` component token, so a host
22
- * restyles this element without touching crtr — custom properties inherit through the shadow
23
- * boundary, which is the whole mechanism. The parts a person can name each own a scope:
24
- * `container`, `caption`, `header`, `column-toggle`, `row`, `cell`, `selection` (the row
25
- * control and the header's select-all, deliberately one thing), `comment-trigger`,
26
- * `comment-editor`, `comment-input`, `comment-button` and its `comment-cancel` /
27
- * `comment-save` paint, `notice`, `reason`, `empty`. Where a shared recipe draws the value the
28
- * chain is `--crtr-table-<part>-<knob>` over the family's `--crtr-control-*` and then the kit
29
- * default; every default stays inline at the rule that spends it, and no token is declared
30
- * anywhere, which is what keeps the kit's own look when a host sets nothing.
31
- *
32
- * Layout and behaviour stay literal: `display`, positioning, the shrink-to-content column
33
- * widths, text alignment, and the trigger's reveal-on-hover `opacity` are anatomy rather than
34
- * paint, so they are not knobs.
35
- */
36
- import { controlStyle, focusRingStyle, writingBoxStyle } from '../controls.js';
37
- import { registerElement } from '../register.js';
38
- import { readSlotConfig, readSlotId } from '../slot-config.js';
39
- import { respond, savedIds, savedResponse } from '../host.js';
40
- import { adoptComments, anchorLabel, canRecordComments, findComment, setComment } from '../comments.js';
41
- import { applyReadOnly, isReadOnly } from '../readonly.js';
42
- import { registerSlotResponse } from '../responses.js';
43
- /** The gap between a comment trigger and the editor that opens off it. */
44
- const EDITOR_GAP = 4;
45
- /**
46
- * The motion the table's control-shaped things wear. A column toggle, a comment trigger and
47
- * the editor's two buttons are all controls, so their timing chains through the family's
48
- * `--crtr-control-transition` before reaching the kit default; a row is not a control, so its
49
- * tint animation carries an element token alone.
50
- */
51
- const TOGGLE_MOTION = 'var(--crtr-table-column-toggle-transition, var(--crtr-control-transition, 120ms ease))';
52
- const ROW_MOTION = 'var(--crtr-table-row-transition, 120ms ease)';
53
- const TRIGGER_MOTION = 'var(--crtr-table-comment-trigger-transition, var(--crtr-control-transition, 120ms ease))';
54
- const COMMENT_BUTTON_MOTION = 'var(--crtr-table-comment-button-transition, var(--crtr-control-transition, 120ms ease))';
55
- 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';
56
- const STYLE = `
57
- :host {
58
- display: block;
59
- }
60
-
61
- /* The positioning context for the comment editor, and the ONLY one. The editor cannot live
62
- inside the container: that box clips to its own rounded corners, so a popover anchored to
63
- a row was cut off at the table's edge instead of floating over it. The frame has no paint
64
- and no clipping, so the editor overlays whatever the table sits on. */
65
- [part="frame"] {
66
- position: relative;
67
- }
68
-
69
- /* Flat: the table sits in the flow of the page. A shadow in this bundle means floating —
70
- the comment editor, and nothing else here. */
71
- [part="container"] {
72
- background: var(--crtr-table-container-fill, var(--crtr-color-background));
73
- border: var(--crtr-table-container-border-width, 1px)
74
- var(--crtr-table-container-border-style, solid)
75
- var(--crtr-table-container-border-color, var(--crtr-color-border));
76
- border-radius: var(--crtr-table-container-radius, var(--crtr-radius-medium));
77
- overflow: hidden;
78
- }
79
-
80
- /* Inside the border, not outside it: the container is full-bleed, so an outset ring is
81
- clipped at the edges of the frame the host gives the page and draws only three sides.
82
- That is why this is NOT the shared control focus family — it is a focusable region rather
83
- than a control, and it says so with its own inset outline. */
84
- [part="container"]:focus-visible {
85
- outline: var(--crtr-table-container-focus-outline, 2px solid var(--crtr-color-focus));
86
- outline-offset: var(--crtr-table-container-focus-outline-offset, -2px);
87
- }
88
-
89
- /* Typography here is the element's host typography, not a second set of knobs: font stays
90
- literal inherit so --crtr-table-font-family, --crtr-table-font-size and
91
- --crtr-table-line-height reach the table, and the colour reads the host's own
92
- --crtr-table-color, whose fallback is the colour this rule used to hard-code. */
93
- [part="table"] {
94
- width: 100%;
95
- border-collapse: collapse;
96
- font: inherit;
97
- color: var(--crtr-table-color, var(--crtr-color-text));
98
- }
99
-
100
- [part="caption"] {
101
- caption-side: top;
102
- padding: var(--crtr-table-caption-padding, var(--crtr-space-3) var(--crtr-space-3) var(--crtr-space-2));
103
- text-align: left;
104
- color: var(--crtr-table-caption-text-color, var(--crtr-color-text-muted));
105
- }
106
-
107
- th {
108
- padding: var(--crtr-table-header-padding, var(--crtr-space-2) var(--crtr-space-3));
109
- text-align: left;
110
- font-weight: var(--crtr-table-header-font-weight, 600);
111
- color: var(--crtr-table-header-text-color, var(--crtr-color-text-muted));
112
- /* A tinted header reads as the table's own chrome; the column-selected tint still wins,
113
- on specificity (0-1-0 against this 0-0-1). */
114
- background: var(--crtr-table-header-fill, var(--crtr-color-surface));
115
- border-bottom: var(--crtr-table-header-border-width, 1px)
116
- var(--crtr-table-header-border-style, solid)
117
- var(--crtr-table-header-border-color, var(--crtr-color-border));
118
- white-space: nowrap;
119
- }
120
-
121
- th[data-align="right"] {
122
- text-align: right;
123
- }
124
-
125
- [part="header-cell-inner"] {
126
- display: inline-flex;
127
- align-items: center;
128
- gap: var(--crtr-table-header-gap, var(--crtr-space-1));
129
- }
130
-
131
- th[data-align="right"] [part="header-cell-inner"] {
132
- flex-direction: row-reverse;
133
- }
134
-
135
- [part="column-toggle"] {
136
- font: var(--crtr-table-column-toggle-font, inherit);
137
- font-weight: var(--crtr-table-column-toggle-font-weight, 600);
138
- color: var(--crtr-table-column-toggle-text-color, inherit);
139
- background: var(--crtr-table-column-toggle-fill, transparent);
140
- border: var(--crtr-table-column-toggle-border-width, 1px)
141
- var(--crtr-table-column-toggle-border-style, solid)
142
- var(--crtr-table-column-toggle-border-color, transparent);
143
- border-radius: var(--crtr-table-column-toggle-radius, var(--crtr-radius-small));
144
- padding: var(--crtr-table-column-toggle-padding, var(--crtr-space-1) var(--crtr-space-2));
145
- /* The toggle is pulled back out of its own padding so the header label sits where an
146
- unselectable one does; a host that repads the toggle repays this too. */
147
- margin: var(--crtr-table-column-toggle-margin, calc(var(--crtr-space-1) * -1) calc(var(--crtr-space-2) * -1));
148
- cursor: pointer;
149
- transition: color ${TOGGLE_MOTION}, background-color ${TOGGLE_MOTION}, border-color ${TOGGLE_MOTION};
150
- }
151
-
152
- [part="column-toggle"]:hover {
153
- background: var(--crtr-table-column-toggle-hover-fill, var(--crtr-color-surface-hover));
154
- }
155
-
156
- [part="column-toggle"][aria-pressed="true"] {
157
- color: var(--crtr-table-column-toggle-selected-text-color, var(--crtr-color-text));
158
- background: var(--crtr-table-column-toggle-selected-fill,
159
- color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
160
- border-color: var(--crtr-table-column-toggle-selected-border-color,
161
- color-mix(in srgb, var(--crtr-color-accent) 45%, transparent));
162
- }
163
-
164
- [part="column-toggle"][aria-pressed="true"]:hover {
165
- background: var(--crtr-table-column-toggle-selected-hover-fill,
166
- color-mix(in srgb, var(--crtr-color-accent) 14%, transparent));
167
- }
168
-
169
- ${focusRingStyle({ scope: 'table-column-toggle' }, '[part="column-toggle"]')}
170
- ${focusRingStyle({ scope: 'table-comment-trigger' }, '[part="comment-trigger"]')}
171
-
172
- [part="row"] {
173
- border-bottom: var(--crtr-table-row-border-width, 1px)
174
- var(--crtr-table-row-border-style, solid)
175
- var(--crtr-table-row-border-color, color-mix(in srgb, var(--crtr-color-border) 60%, transparent));
176
- transition: background-color ${ROW_MOTION};
177
- }
178
-
179
- [part="row"]:last-child {
180
- border-bottom: var(--crtr-table-row-last-border, none);
181
- }
182
-
183
- [part="row"]:hover,
184
- [part="row"][data-active="true"] {
185
- background: var(--crtr-table-row-hover-fill, var(--crtr-color-surface-hover));
186
- }
187
-
188
- [part="row"][data-state="selected"] {
189
- background: var(--crtr-table-row-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
190
- }
191
-
192
- [part="row"][data-state="selected"]:hover,
193
- [part="row"][data-state="selected"][data-active="true"] {
194
- background: var(--crtr-table-row-selected-hover-fill,
195
- color-mix(in srgb, var(--crtr-color-accent) 14%, transparent));
196
- }
197
-
198
- /* Inset for the same reason the container's is: a row runs the full width of the table. */
199
- [part="row"]:focus-visible {
200
- outline: var(--crtr-table-row-focus-outline, 2px solid var(--crtr-color-focus));
201
- outline-offset: var(--crtr-table-row-focus-outline-offset, -2px);
202
- }
203
-
204
- td {
205
- padding: var(--crtr-table-cell-padding, var(--crtr-space-2) var(--crtr-space-3));
206
- vertical-align: middle;
207
- }
208
-
209
- td[data-align="right"] {
210
- text-align: right;
211
- font-variant-numeric: var(--crtr-table-cell-numeric-font-variant, tabular-nums);
212
- }
213
-
214
- td[data-mono="true"] {
215
- font-family: var(--crtr-table-cell-mono-font-family, var(--crtr-font-mono));
216
- font-size: var(--crtr-table-cell-mono-font-size, 0.8125em);
217
- }
218
-
219
- [data-column-selected="true"] {
220
- background: var(--crtr-table-column-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 6%, transparent));
221
- }
222
-
223
- [part="select-cell"],
224
- [part="select-head"] {
225
- width: 1px;
226
- padding-right: var(--crtr-table-select-cell-padding-right, 0);
227
- }
228
-
229
- [part="comment-cell"],
230
- [part="comment-head"] {
231
- width: 1px;
232
- text-align: right;
233
- padding-left: var(--crtr-table-comment-cell-padding-left, 0);
234
- }
235
-
236
- /* One scope over both selectors: the header's select-all IS the row control, drawn once in
237
- the head, and they are meant to be indistinguishable. */
238
- ${controlStyle({ scope: 'table-selection' }, '[part="row-control"]', '[part="select-all"]')}
239
-
240
- /* A page that can no longer take an answer keeps its selection legible and drops every
241
- affordance that says it could still be changed. */
242
- [part="column-toggle"]:disabled {
243
- cursor: default;
244
- }
245
-
246
- [part="column-toggle"]:disabled:hover {
247
- color: var(--crtr-table-column-toggle-disabled-text-color, var(--crtr-color-text-muted));
248
- background: var(--crtr-table-column-toggle-disabled-fill, none);
249
- }
250
-
251
- /* One size knob for a square button, and one for the glyph in it — width and height are one
252
- visual decision each. The opacity is the reveal-on-hover mechanic, which is behaviour. */
253
- [part="comment-trigger"] {
254
- display: inline-flex;
255
- align-items: center;
256
- justify-content: center;
257
- width: var(--crtr-table-comment-trigger-size, 1.5em);
258
- height: var(--crtr-table-comment-trigger-size, 1.5em);
259
- padding: var(--crtr-table-comment-trigger-padding, 0);
260
- color: var(--crtr-table-comment-trigger-text-color, var(--crtr-color-text-muted));
261
- background: var(--crtr-table-comment-trigger-fill, transparent);
262
- border: var(--crtr-table-comment-trigger-border-width, 0)
263
- var(--crtr-table-comment-trigger-border-style, none)
264
- var(--crtr-table-comment-trigger-border-color, currentColor);
265
- border-radius: var(--crtr-table-comment-trigger-radius, var(--crtr-radius-small));
266
- cursor: pointer;
267
- opacity: 0;
268
- transition: opacity ${TRIGGER_MOTION}, color ${TRIGGER_MOTION}, background-color ${TRIGGER_MOTION};
269
- }
270
-
271
- [part="comment-trigger"] svg {
272
- width: var(--crtr-table-comment-trigger-icon-size, 0.875em);
273
- height: var(--crtr-table-comment-trigger-icon-size, 0.875em);
274
- }
275
-
276
- /* The path geometry stays the icon's anatomy; its paint is component styling. Attribute
277
- selectors preserve the filled-comment state that syncState writes while allowing both
278
- appearances to inherit document-root overrides through this shadow root. */
279
- [part="comment-trigger"] path {
280
- stroke: var(--crtr-table-comment-trigger-icon-stroke, currentColor);
281
- stroke-width: var(--crtr-table-comment-trigger-icon-stroke-width, 1.25);
282
- stroke-linejoin: var(--crtr-table-comment-trigger-icon-stroke-linejoin, round);
283
- }
284
-
285
- [part="comment-trigger"] path[fill="none"] {
286
- fill: var(--crtr-table-comment-trigger-icon-fill, none);
287
- }
288
-
289
- [part="comment-trigger"] path[fill="currentColor"] {
290
- fill: var(--crtr-table-comment-trigger-commented-icon-fill, currentColor);
291
- }
292
-
293
- [part="comment-trigger"][data-has-comment="true"] {
294
- color: var(--crtr-table-comment-trigger-commented-text-color, var(--crtr-color-accent));
295
- opacity: 1;
296
- }
297
-
298
- [part="comment-trigger"]:hover {
299
- background: var(--crtr-table-comment-trigger-hover-fill, var(--crtr-color-surface-hover));
300
- }
301
-
302
- tr:hover [part="comment-trigger"],
303
- tr:focus-within [part="comment-trigger"],
304
- tr[data-active="true"] [part="comment-trigger"],
305
- [part="comment-trigger"]:focus-visible,
306
- [part="comment-trigger"][data-open="true"] {
307
- opacity: 1;
308
- }
309
-
310
- /* A floating panel over the page, not a box in the table's layout: the background token
311
- rather than the surface tint, so it reads as lifted off whatever it covers. */
312
- [part="comment-editor"] {
313
- position: absolute;
314
- z-index: 5;
315
- box-sizing: border-box;
316
- width: var(--crtr-table-comment-editor-width, min(20em, 100%));
317
- display: grid;
318
- gap: var(--crtr-table-comment-editor-gap, var(--crtr-space-2));
319
- padding: var(--crtr-table-comment-editor-padding, var(--crtr-space-3));
320
- background: var(--crtr-table-comment-editor-fill, var(--crtr-color-background));
321
- border: var(--crtr-table-comment-editor-border-width, 1px)
322
- var(--crtr-table-comment-editor-border-style, solid)
323
- var(--crtr-table-comment-editor-border-color, var(--crtr-color-border));
324
- border-radius: var(--crtr-table-comment-editor-radius, var(--crtr-radius-medium));
325
- box-shadow: var(--crtr-table-comment-editor-shadow, var(--crtr-shadow));
326
- }
327
-
328
- [part="comment-editor"][hidden] {
329
- display: none;
330
- }
331
-
332
- /* The editor's own prose, in the page's voice. It was set in caps mono, which shouted a
333
- row id at someone who had just clicked that row. */
334
- [part="comment-target"] {
335
- margin: var(--crtr-table-comment-target-margin, 0);
336
- font-size: var(--crtr-table-comment-target-font-size, 0.875em);
337
- color: var(--crtr-table-comment-target-text-color, var(--crtr-color-text-muted));
338
- }
339
-
340
- ${writingBoxStyle({ scope: 'table-comment-input' }, '[part="comment-input"]')}
341
- [part="comment-input"] {
342
- min-height: var(--crtr-table-comment-input-min-height, 4.5em);
343
- resize: vertical;
344
- }
345
-
346
- [part="comment-actions"] {
347
- display: flex;
348
- align-items: center;
349
- justify-content: space-between;
350
- gap: var(--crtr-table-comment-actions-gap, var(--crtr-space-2));
351
- }
352
-
353
- [part="comment-hint"] {
354
- font-size: var(--crtr-table-comment-hint-font-size, 0.875em);
355
- color: var(--crtr-table-comment-hint-text-color, var(--crtr-color-text-muted));
356
- }
357
-
358
- /* The chrome both editor buttons share reads one scope — a person sees one pair of buttons —
359
- and each button's own paint carries its own. */
360
- [part="comment-cancel"],
361
- [part="comment-save"] {
362
- font: var(--crtr-table-comment-button-font, inherit);
363
- font-size: var(--crtr-table-comment-button-font-size, 0.875em);
364
- padding: var(--crtr-table-comment-button-padding, var(--crtr-space-1) var(--crtr-space-2));
365
- border-radius: var(--crtr-table-comment-button-radius, var(--crtr-radius-small));
366
- border: var(--crtr-table-comment-button-border-width, 1px)
367
- var(--crtr-table-comment-button-border-style, solid)
368
- var(--crtr-table-comment-button-border-color, var(--crtr-color-border));
369
- cursor: pointer;
370
- transition: color ${COMMENT_BUTTON_MOTION}, background-color ${COMMENT_BUTTON_MOTION},
371
- border-color ${COMMENT_BUTTON_MOTION};
372
- }
373
-
374
- [part="comment-cancel"] {
375
- color: var(--crtr-table-comment-cancel-text-color, var(--crtr-color-text-muted));
376
- background: var(--crtr-table-comment-cancel-fill, transparent);
377
- border-color: var(--crtr-table-comment-cancel-border-color,
378
- var(--crtr-table-comment-button-border-color, transparent));
379
- }
380
-
381
- [part="comment-cancel"]:hover {
382
- color: var(--crtr-table-comment-cancel-hover-text-color, var(--crtr-color-text));
383
- background: var(--crtr-table-comment-cancel-hover-fill, var(--crtr-color-surface-hover));
384
- }
385
-
386
- [part="comment-save"] {
387
- color: var(--crtr-table-comment-save-text-color, var(--crtr-color-accent-contrast));
388
- background: var(--crtr-table-comment-save-fill, var(--crtr-color-accent));
389
- border-color: var(--crtr-table-comment-save-border-color,
390
- var(--crtr-table-comment-button-border-color, var(--crtr-color-accent)));
391
- }
392
-
393
- /* Mixing toward the text colour darkens on a light token set and lightens on a dark one —
394
- a brightness filter moves the wrong way on one of them. */
395
- [part="comment-save"]:hover {
396
- background: var(--crtr-table-comment-save-hover-fill,
397
- color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
398
- border-color: var(--crtr-table-comment-save-hover-border-color,
399
- color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
400
- }
401
-
402
- ${focusRingStyle({ scope: 'table-comment-cancel' }, '[part="comment-cancel"]')}
403
- ${focusRingStyle({ scope: 'table-comment-save' }, '[part="comment-save"]')}
404
-
405
- /* One scope over both: a loading line and an unavailable notice are the same box, saying
406
- the table has nothing to show yet or ever. */
407
- [part="status"],
408
- [part="unavailable"] {
409
- padding: var(--crtr-table-notice-padding, var(--crtr-space-3));
410
- color: var(--crtr-table-notice-text-color, var(--crtr-color-text-muted));
411
- background: var(--crtr-table-notice-fill, var(--crtr-color-surface));
412
- border: var(--crtr-table-notice-border-width, 1px)
413
- var(--crtr-table-notice-border-style, solid)
414
- var(--crtr-table-notice-border-color, var(--crtr-color-border));
415
- border-radius: var(--crtr-table-notice-radius, var(--crtr-radius-medium));
416
- }
417
-
418
- [part="reason"] {
419
- color: var(--crtr-table-reason-text-color, var(--crtr-color-text));
420
- font-family: var(--crtr-table-reason-font-family, var(--crtr-font-mono));
421
- }
422
-
423
- [part="empty"] {
424
- padding: var(--crtr-table-empty-padding, var(--crtr-space-3));
425
- color: var(--crtr-table-empty-text-color, var(--crtr-color-text-muted));
426
- text-align: center;
427
- }
428
- `;
429
- /** A cell value the schema allows, as the string a table shows. */
430
- function cellText(value) {
431
- if (value === undefined || value === null)
432
- return '';
433
- return typeof value === 'string' ? value : String(value);
434
- }
435
- function isPlainObject(value) {
436
- return typeof value === 'object' && value !== null && !Array.isArray(value);
437
- }
438
- function commentIcon() {
439
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
440
- svg.setAttribute('viewBox', '0 0 16 16');
441
- svg.setAttribute('aria-hidden', 'true');
442
- svg.setAttribute('focusable', 'false');
443
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
444
- path.setAttribute('d', COMMENT_BUBBLE);
445
- path.setAttribute('fill', 'none');
446
- path.setAttribute('stroke', 'currentColor');
447
- path.setAttribute('stroke-width', '1.25');
448
- path.setAttribute('stroke-linejoin', 'round');
449
- svg.append(path);
450
- return svg;
451
- }
452
- function isTextEntry(target) {
453
- if (!(target instanceof HTMLElement))
454
- return false;
455
- return target.closest('input[type="text"], textarea, [contenteditable="true"]') !== null;
456
- }
457
- class CrtrTableElement extends HTMLElement {
458
- /** The host marks a resolved or canceled page `inert`; that is this element's read-only signal. */
459
- static observedAttributes = ['inert'];
460
- #root;
461
- #slotId;
462
- #config;
463
- #rows = [];
464
- #selectedRowIds = [];
465
- #selectedColumnIds = [];
466
- #comments = [];
467
- #activeRowId = null;
468
- #editorAnchor = null;
469
- #editorTrigger;
470
- #container;
471
- /** The editor's positioning context — the unclipped wrapper around the container. */
472
- #frame;
473
- #editor;
474
- #editorTarget;
475
- #editorInput;
476
- #editorHint;
477
- #selectAll;
478
- #rowParts = new Map();
479
- #columnParts = new Map();
480
- #wholeComment;
481
- constructor() {
482
- super();
483
- this.#root = this.attachShadow({ mode: 'open' });
484
- }
485
- connectedCallback() {
486
- const read = readSlotConfig(this);
487
- if (read.status !== 'ok') {
488
- this.#renderUnavailable(read.reason);
489
- return;
490
- }
491
- const config = read.config;
492
- if (!Array.isArray(config.columns) || config.columns.length === 0) {
493
- this.#renderUnavailable('table config has no columns');
494
- return;
495
- }
496
- this.#config = config;
497
- this.#slotId = readSlotId(this);
498
- // Only a selectable table bears a response — the host rejects a map naming a display-only
499
- // slot — and the modes are known here before rendering.
500
- if (this.#slotId !== undefined && (this.#rowSelect !== 'none' || this.#columnSelect !== 'none')) {
501
- registerSlotResponse(this, this.#slotId, () => this.#response());
502
- }
503
- this.#rows = config.rows ?? [];
504
- this.#hydrate();
505
- this.#renderTable();
506
- }
507
- attributeChangedCallback() {
508
- if (this.isConnected)
509
- this.#syncReadOnly();
510
- }
511
- /**
512
- * Repainted after every state sync, because a rebuild replaces the controls. Column
513
- * toggles are buttons rather than inputs, so they are named here; the mutation paths
514
- * refuse anyway, and this is what makes the refusal visible.
515
- */
516
- #syncReadOnly() {
517
- const readOnly = isReadOnly(this);
518
- applyReadOnly(this.#root, readOnly);
519
- for (const parts of this.#columnParts.values()) {
520
- if (parts.toggle !== undefined)
521
- parts.toggle.disabled = readOnly;
522
- }
523
- // A page can go read-only while the composer is open — the host sets `inert` on a ticket
524
- // that resolves under it. A box that can no longer save is closed rather than left there
525
- // taking words nothing will keep.
526
- if (readOnly && this.#editorAnchor !== null)
527
- this.#closeEditor();
528
- }
529
- /**
530
- * Seed local state from the answer the host is already holding, so a reopened page shows
531
- * the rows and columns that were picked. It runs once rows are known because a saved id is
532
- * only honoured while it still names something on
533
- * screen. The select modes are re-imposed for the same reason.
534
- */
535
- #hydrate() {
536
- if (this.#slotId === undefined)
537
- return;
538
- const saved = savedResponse(this.#slotId);
539
- if (saved === undefined)
540
- return;
541
- const rows = this.#orderRows(savedIds(saved.selectedRowIds));
542
- const columns = this.#orderColumns(savedIds(saved.selectedColumnIds));
543
- this.#selectedRowIds = this.#rowSelect === 'none' ? [] : this.#rowSelect === 'single' ? rows.slice(0, 1) : rows;
544
- this.#selectedColumnIds =
545
- this.#columnSelect === 'none' ? [] : this.#columnSelect === 'single' ? columns.slice(0, 1) : columns;
546
- const rowIds = new Set(this.#rows.map((row) => row.id));
547
- const columnIds = new Set((this.#config?.columns ?? []).map((column) => column.id));
548
- this.#comments = adoptComments(saved.comments).filter((comment) => {
549
- if (comment.anchor.kind === 'row')
550
- return rowIds.has(comment.anchor.rowId);
551
- if (comment.anchor.kind === 'column')
552
- return columnIds.has(comment.anchor.columnId);
553
- return true;
554
- });
555
- }
556
- // ── the response ───────────────────────────────────────────────────────────────────────
557
- get #rowSelect() {
558
- return this.#config?.rowSelect ?? 'none';
559
- }
560
- get #columnSelect() {
561
- return this.#config?.columnSelect ?? 'none';
562
- }
563
- #response() {
564
- return {
565
- selectedRowIds: [...this.#selectedRowIds],
566
- selectedColumnIds: [...this.#selectedColumnIds],
567
- comments: [...this.#comments],
568
- };
569
- }
570
- /** Every user change hands the host this slot's complete current response, never a delta. */
571
- #commit() {
572
- this.#syncState();
573
- if (this.#slotId !== undefined)
574
- respond(this.#slotId, this.#response());
575
- }
576
- /** Selection stays in the order the agent authored, not the order the user clicked. */
577
- #orderRows(ids) {
578
- return this.#rows.map((row) => row.id).filter((id) => ids.includes(id));
579
- }
580
- #orderColumns(ids) {
581
- return (this.#config?.columns ?? []).map((column) => column.id).filter((id) => ids.includes(id));
582
- }
583
- #setRowSelected(rowId, selected) {
584
- const mode = this.#rowSelect;
585
- if (mode === 'none' || isReadOnly(this))
586
- return;
587
- if (mode === 'single') {
588
- this.#selectedRowIds = selected ? [rowId] : [];
589
- }
590
- else {
591
- this.#selectedRowIds = this.#orderRows(selected ? [...this.#selectedRowIds, rowId] : this.#selectedRowIds.filter((id) => id !== rowId));
592
- }
593
- this.#commit();
594
- }
595
- #toggleRow(rowId) {
596
- if (this.#rowSelect === 'single') {
597
- this.#setRowSelected(rowId, true);
598
- return;
599
- }
600
- this.#setRowSelected(rowId, !this.#selectedRowIds.includes(rowId));
601
- }
602
- #setColumnSelected(columnId, selected) {
603
- const mode = this.#columnSelect;
604
- if (mode === 'none' || isReadOnly(this))
605
- return;
606
- if (mode === 'single') {
607
- this.#selectedColumnIds = selected ? [columnId] : [];
608
- }
609
- else {
610
- this.#selectedColumnIds = this.#orderColumns(selected ? [...this.#selectedColumnIds, columnId] : this.#selectedColumnIds.filter((id) => id !== columnId));
611
- }
612
- this.#commit();
613
- }
614
- #toggleColumn(columnId) {
615
- this.#setColumnSelected(columnId, !this.#selectedColumnIds.includes(columnId));
616
- }
617
- #setAllRows(selected) {
618
- if (this.#rowSelect !== 'multi' || isReadOnly(this))
619
- return;
620
- this.#selectedRowIds = selected ? this.#rows.map((row) => row.id) : [];
621
- this.#commit();
622
- }
623
- // ── rendering ──────────────────────────────────────────────────────────────────────────
624
- #reset() {
625
- this.#root.replaceChildren();
626
- this.#rowParts.clear();
627
- this.#columnParts.clear();
628
- this.#container = undefined;
629
- this.#frame = undefined;
630
- this.#editor = undefined;
631
- this.#editorTrigger = undefined;
632
- this.#editorAnchor = null;
633
- this.#selectAll = undefined;
634
- this.#wholeComment = undefined;
635
- const style = document.createElement('style');
636
- style.textContent = STYLE;
637
- this.#root.append(style);
638
- }
639
- #renderUnavailable(reason) {
640
- this.#reset();
641
- const box = document.createElement('div');
642
- box.part.add('unavailable');
643
- box.setAttribute('role', 'status');
644
- const detail = document.createElement('span');
645
- detail.part.add('reason');
646
- detail.textContent = reason;
647
- box.append('Table unavailable: ', detail);
648
- this.#root.append(box);
649
- }
650
- #renderTable() {
651
- const config = this.#config;
652
- if (config === undefined)
653
- return;
654
- this.#reset();
655
- const container = document.createElement('div');
656
- container.part.add('container');
657
- container.tabIndex = 0;
658
- container.addEventListener('keydown', (event) => this.#onKeyDown(event));
659
- container.addEventListener('focus', () => {
660
- if (this.#activeRowId === null && this.#rows.length > 0) {
661
- this.#activeRowId = this.#rows[0].id;
662
- this.#syncState();
663
- }
664
- });
665
- this.#container = container;
666
- const table = document.createElement('table');
667
- table.part.add('table');
668
- if (config.label !== undefined && config.label !== '') {
669
- const caption = document.createElement('caption');
670
- caption.part.add('caption');
671
- caption.textContent = config.label;
672
- table.append(caption);
673
- }
674
- table.append(this.#buildHead(config), this.#buildBody(config));
675
- container.append(table);
676
- if (this.#rows.length === 0) {
677
- const empty = document.createElement('div');
678
- empty.part.add('empty');
679
- empty.textContent = 'No rows.';
680
- container.append(empty);
681
- }
682
- const frame = document.createElement('div');
683
- frame.part.add('frame');
684
- frame.append(container);
685
- if (this.#commentable)
686
- frame.append(this.#buildEditor());
687
- this.#frame = frame;
688
- this.#root.append(frame);
689
- this.#syncState();
690
- }
691
- #buildHead(config) {
692
- const head = document.createElement('thead');
693
- const tr = document.createElement('tr');
694
- tr.part.add('header-row');
695
- if (this.#rowSelect !== 'none') {
696
- const th = document.createElement('th');
697
- th.part.add('select-head');
698
- th.setAttribute('scope', 'col');
699
- if (this.#rowSelect === 'multi') {
700
- const all = document.createElement('input');
701
- all.type = 'checkbox';
702
- all.part.add('select-all');
703
- all.setAttribute('aria-label', 'Select every row');
704
- all.addEventListener('change', () => this.#setAllRows(all.checked));
705
- this.#selectAll = all;
706
- th.append(all);
707
- }
708
- tr.append(th);
709
- }
710
- for (const column of config.columns) {
711
- const th = document.createElement('th');
712
- th.part.add('column-header');
713
- th.setAttribute('scope', 'col');
714
- th.dataset.columnHeader = column.id;
715
- if (column.align === 'right')
716
- th.dataset.align = 'right';
717
- const inner = document.createElement('span');
718
- inner.part.add('header-cell-inner');
719
- let toggle;
720
- if (this.#columnSelect === 'none') {
721
- const label = document.createElement('span');
722
- label.textContent = column.label;
723
- inner.append(label);
724
- }
725
- else {
726
- toggle = document.createElement('button');
727
- toggle.type = 'button';
728
- toggle.part.add('column-toggle');
729
- toggle.dataset.columnToggle = column.id;
730
- toggle.setAttribute('aria-pressed', 'false');
731
- toggle.setAttribute('aria-label', `Select the ${column.label} column`);
732
- toggle.textContent = column.label;
733
- toggle.addEventListener('click', () => this.#toggleColumn(column.id));
734
- inner.append(toggle);
735
- }
736
- const comment = this.#commentable
737
- ? this.#buildCommentTrigger({ kind: 'column', columnId: column.id }, `the ${column.label} column`)
738
- : undefined;
739
- if (comment !== undefined)
740
- inner.append(comment);
741
- th.append(inner);
742
- tr.append(th);
743
- this.#columnParts.set(column.id, { toggle, ...(comment === undefined ? {} : { comment }), cells: [], head: th });
744
- }
745
- // The trailing column exists to hold the whole-table trigger; without comments there is
746
- // nothing to put in it, and an empty column would just be a stripe of dead width.
747
- if (this.#commentable) {
748
- const trailing = document.createElement('th');
749
- trailing.part.add('comment-head');
750
- trailing.setAttribute('scope', 'col');
751
- this.#wholeComment = this.#buildCommentTrigger({ kind: 'whole' }, 'this table');
752
- trailing.append(this.#wholeComment);
753
- tr.append(trailing);
754
- }
755
- head.append(tr);
756
- return head;
757
- }
758
- #buildBody(config) {
759
- const body = document.createElement('tbody');
760
- for (const row of this.#rows) {
761
- const tr = document.createElement('tr');
762
- tr.part.add('row');
763
- tr.dataset.rowId = row.id;
764
- tr.tabIndex = -1;
765
- tr.addEventListener('mouseenter', () => this.#setActiveRow(row.id));
766
- tr.addEventListener('focusin', () => this.#setActiveRow(row.id));
767
- let control;
768
- if (this.#rowSelect !== 'none') {
769
- const td = document.createElement('td');
770
- td.part.add('select-cell');
771
- const input = document.createElement('input');
772
- input.type = this.#rowSelect === 'single' ? 'radio' : 'checkbox';
773
- input.part.add('row-control');
774
- if (this.#rowSelect === 'single')
775
- input.name = `${this.#slotId ?? 'crtr-table'}-row`;
776
- input.value = row.id;
777
- input.setAttribute('aria-label', `Select row ${row.id}`);
778
- input.addEventListener('change', () => this.#setRowSelected(row.id, input.checked));
779
- control = input;
780
- td.append(input);
781
- tr.append(td);
782
- }
783
- for (const column of config.columns) {
784
- const td = document.createElement('td');
785
- td.part.add('cell');
786
- td.dataset.columnId = column.id;
787
- if (column.align === 'right')
788
- td.dataset.align = 'right';
789
- if (column.mono === true)
790
- td.dataset.mono = 'true';
791
- td.textContent = cellText(row.cells[column.id]);
792
- tr.append(td);
793
- this.#columnParts.get(column.id)?.cells.push(td);
794
- }
795
- let comment;
796
- if (this.#commentable) {
797
- const commentCell = document.createElement('td');
798
- commentCell.part.add('comment-cell');
799
- comment = this.#buildCommentTrigger({ kind: 'row', rowId: row.id }, `row ${row.id}`);
800
- commentCell.append(comment);
801
- tr.append(commentCell);
802
- }
803
- body.append(tr);
804
- this.#rowParts.set(row.id, { tr, ...(control === undefined ? {} : { control }), ...(comment === undefined ? {} : { comment }) });
805
- }
806
- return body;
807
- }
808
- /** Whether this table can keep a comment — see `canRecordComments`. */
809
- get #commentable() {
810
- return canRecordComments(this.#slotId);
811
- }
812
- #buildCommentTrigger(anchor, target) {
813
- const button = document.createElement('button');
814
- button.type = 'button';
815
- button.part.add('comment-trigger');
816
- button.setAttribute('aria-label', `Comment on ${target}`);
817
- button.dataset.commentTarget = target;
818
- button.append(commentIcon());
819
- button.addEventListener('click', () => this.#openEditor(anchor, button));
820
- return button;
821
- }
822
- #buildEditor() {
823
- const editor = document.createElement('div');
824
- editor.part.add('comment-editor');
825
- editor.hidden = true;
826
- editor.setAttribute('role', 'dialog');
827
- editor.setAttribute('aria-label', 'Comment');
828
- const target = document.createElement('p');
829
- target.part.add('comment-target');
830
- editor.append(target);
831
- const input = document.createElement('textarea');
832
- input.part.add('comment-input');
833
- input.placeholder = 'What should the agent know about this?';
834
- input.addEventListener('keydown', (event) => {
835
- if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
836
- event.preventDefault();
837
- this.#saveComment();
838
- }
839
- else if (event.key === 'Escape') {
840
- event.preventDefault();
841
- this.#closeEditor();
842
- }
843
- });
844
- editor.append(input);
845
- const actions = document.createElement('div');
846
- actions.part.add('comment-actions');
847
- const hint = document.createElement('span');
848
- hint.part.add('comment-hint');
849
- actions.append(hint);
850
- const buttons = document.createElement('span');
851
- const cancel = document.createElement('button');
852
- cancel.type = 'button';
853
- cancel.part.add('comment-cancel');
854
- cancel.textContent = 'Cancel';
855
- cancel.addEventListener('click', () => this.#closeEditor());
856
- const save = document.createElement('button');
857
- save.type = 'button';
858
- save.part.add('comment-save');
859
- save.textContent = 'Save';
860
- save.addEventListener('click', () => this.#saveComment());
861
- buttons.append(cancel, ' ', save);
862
- actions.append(buttons);
863
- editor.append(actions);
864
- this.#editor = editor;
865
- this.#editorTarget = target;
866
- this.#editorInput = input;
867
- this.#editorHint = hint;
868
- return editor;
869
- }
870
- // ── comments ───────────────────────────────────────────────────────────────────────────
871
- #openEditor(anchor, trigger) {
872
- const editor = this.#editor;
873
- const frame = this.#frame;
874
- // A composer opens only where its comment can land: never on a read-only page, whose
875
- // textareas are already `readOnly`, so the box would take focus and refuse every keystroke.
876
- if (editor === undefined || frame === undefined || !this.#commentable || isReadOnly(this))
877
- return;
878
- this.#editorAnchor = anchor;
879
- this.#editorTrigger = trigger;
880
- const existing = findComment(this.#comments, anchor);
881
- // The box is seeded from the saved comment every time it opens, so an abandoned edit
882
- // never leaks into the next one.
883
- if (this.#editorInput !== undefined)
884
- this.#editorInput.value = existing?.text ?? '';
885
- if (this.#editorTarget !== undefined) {
886
- this.#editorTarget.textContent = `Comment on ${trigger.dataset.commentTarget ?? anchorLabel(anchor)}`;
887
- }
888
- if (this.#editorHint !== undefined)
889
- this.#editorHint.textContent = existing === undefined ? '⌘↵ to save' : 'empty to delete';
890
- const bounds = frame.getBoundingClientRect();
891
- const rect = trigger.getBoundingClientRect();
892
- editor.hidden = false;
893
- editor.style.right = `${Math.max(0, bounds.right - rect.right)}px`;
894
- // Below the trigger by default; above it when the viewport has no room below and does
895
- // have room above. Measured after unhiding, because a hidden panel has no height.
896
- editor.style.top = '0px';
897
- const height = editor.getBoundingClientRect().height;
898
- const below = rect.bottom + EDITOR_GAP + height <= window.innerHeight;
899
- const above = rect.top - EDITOR_GAP - height >= 0;
900
- editor.style.top =
901
- !below && above
902
- ? `${rect.top - EDITOR_GAP - height - bounds.top}px`
903
- : `${rect.bottom + EDITOR_GAP - bounds.top}px`;
904
- this.#syncState();
905
- this.#editorInput?.focus();
906
- }
907
- /**
908
- * Closing hands focus back to the trigger that opened the editor. Without it the keyboard
909
- * grammar dies on save: the focused Save button is inside the panel being hidden, so focus
910
- * falls out of the element entirely and j/k/x/c stop arriving.
911
- */
912
- #closeEditor() {
913
- const trigger = this.#editorTrigger;
914
- this.#editorAnchor = null;
915
- this.#editorTrigger = undefined;
916
- if (this.#editor !== undefined)
917
- this.#editor.hidden = true;
918
- this.#syncState();
919
- trigger?.focus();
920
- }
921
- #saveComment() {
922
- const anchor = this.#editorAnchor;
923
- if (anchor === null || isReadOnly(this))
924
- return;
925
- this.#comments = setComment(this.#comments, anchor, this.#editorInput?.value ?? '');
926
- this.#closeEditor();
927
- this.#commit();
928
- }
929
- // ── interaction ────────────────────────────────────────────────────────────────────────
930
- #setActiveRow(rowId) {
931
- if (this.#activeRowId === rowId)
932
- return;
933
- this.#activeRowId = rowId;
934
- this.#syncState();
935
- }
936
- #focusRow(rowId) {
937
- const parts = this.#rowParts.get(rowId);
938
- (parts?.control ?? parts?.tr)?.focus();
939
- }
940
- /**
941
- * The kit's classic list grammar: j/k move (wrapping) and carry DOM focus with them, x
942
- * selects, c comments. A key pressed on a column toggle acts on that column instead, since
943
- * the header IS the column. Modified keys and anything typed in the comment box pass through.
944
- */
945
- #onKeyDown(event) {
946
- if (event.metaKey || event.ctrlKey || event.altKey)
947
- return;
948
- const target = event.composedPath()[0] ?? event.target;
949
- if (isTextEntry(target))
950
- return;
951
- const key = event.key.toLowerCase();
952
- if (key !== 'j' && key !== 'k' && key !== 'x' && key !== 'c')
953
- return;
954
- // Anything focused inside a header cell — the toggle or that column's comment trigger —
955
- // acts on the column, because the header IS the column.
956
- const columnId = target instanceof HTMLElement ? target.closest('[data-column-header]')?.dataset.columnHeader : undefined;
957
- if (columnId !== undefined && (key === 'x' || key === 'c')) {
958
- event.preventDefault();
959
- if (key === 'x')
960
- this.#toggleColumn(columnId);
961
- else {
962
- const comment = this.#columnParts.get(columnId)?.comment;
963
- if (comment !== undefined)
964
- this.#openEditor({ kind: 'column', columnId }, comment);
965
- }
966
- return;
967
- }
968
- const ids = this.#rows.map((row) => row.id);
969
- if (ids.length === 0)
970
- return;
971
- const currentId = this.#activeRowId ?? this.#selectedRowIds[this.#selectedRowIds.length - 1] ?? ids[0];
972
- if (key === 'j' || key === 'k') {
973
- event.preventDefault();
974
- const index = Math.max(0, ids.indexOf(currentId));
975
- const nextId = ids[(index + (key === 'j' ? 1 : -1) + ids.length) % ids.length];
976
- this.#setActiveRow(nextId);
977
- this.#focusRow(nextId);
978
- return;
979
- }
980
- event.preventDefault();
981
- if (key === 'x') {
982
- this.#toggleRow(currentId);
983
- return;
984
- }
985
- const comment = this.#rowParts.get(currentId)?.comment;
986
- if (comment !== undefined)
987
- this.#openEditor({ kind: 'row', rowId: currentId }, comment);
988
- }
989
- // ── state → DOM ────────────────────────────────────────────────────────────────────────
990
- #syncCommentTrigger(button, anchor) {
991
- const comment = findComment(this.#comments, anchor);
992
- const target = button.dataset.commentTarget ?? anchorLabel(anchor);
993
- button.dataset.hasComment = comment === undefined ? 'false' : 'true';
994
- button.setAttribute('aria-label', comment === undefined ? `Comment on ${target}` : `Edit comment on ${target}`);
995
- button.title = comment?.text ?? '';
996
- const path = button.querySelector('path');
997
- path?.setAttribute('fill', comment === undefined ? 'none' : 'currentColor');
998
- button.dataset.open = this.#isEditorAnchor(anchor) ? 'true' : 'false';
999
- }
1000
- #isEditorAnchor(anchor) {
1001
- const current = this.#editorAnchor;
1002
- if (current === null || current.kind !== anchor.kind)
1003
- return false;
1004
- if (current.kind === 'row' && anchor.kind === 'row')
1005
- return current.rowId === anchor.rowId;
1006
- if (current.kind === 'column' && anchor.kind === 'column')
1007
- return current.columnId === anchor.columnId;
1008
- return current.kind === 'whole';
1009
- }
1010
- #syncState() {
1011
- for (const [rowId, parts] of this.#rowParts) {
1012
- const selected = this.#selectedRowIds.includes(rowId);
1013
- if (selected)
1014
- parts.tr.dataset.state = 'selected';
1015
- else
1016
- delete parts.tr.dataset.state;
1017
- parts.tr.dataset.active = this.#activeRowId === rowId ? 'true' : 'false';
1018
- parts.tr.setAttribute('aria-selected', selected ? 'true' : 'false');
1019
- if (parts.control !== undefined)
1020
- parts.control.checked = selected;
1021
- if (parts.comment !== undefined)
1022
- this.#syncCommentTrigger(parts.comment, { kind: 'row', rowId });
1023
- }
1024
- for (const [columnId, parts] of this.#columnParts) {
1025
- const selected = this.#selectedColumnIds.includes(columnId);
1026
- parts.toggle?.setAttribute('aria-pressed', selected ? 'true' : 'false');
1027
- parts.head.dataset.columnSelected = selected ? 'true' : 'false';
1028
- for (const cell of parts.cells)
1029
- cell.dataset.columnSelected = selected ? 'true' : 'false';
1030
- if (parts.comment !== undefined)
1031
- this.#syncCommentTrigger(parts.comment, { kind: 'column', columnId });
1032
- }
1033
- if (this.#wholeComment !== undefined)
1034
- this.#syncCommentTrigger(this.#wholeComment, { kind: 'whole' });
1035
- if (this.#selectAll !== undefined) {
1036
- const all = this.#rows.length > 0 && this.#selectedRowIds.length === this.#rows.length;
1037
- this.#selectAll.checked = all;
1038
- this.#selectAll.indeterminate = this.#selectedRowIds.length > 0 && !all;
1039
- }
1040
- this.#syncReadOnly();
1041
- }
1042
- }
1043
- registerElement('crtr-table', CrtrTableElement);