@north-light/crouter 0.3.196 → 0.3.198

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 (148) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/dto/inbox.d.ts +46 -0
  3. package/dist/builtin-memory/00-runtime-base.md +3 -3
  4. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  5. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  7. package/dist/clients/attach/render/chat-view.d.ts +3 -8
  8. package/dist/clients/attach/render/chat-view.js +3 -24
  9. package/dist/clients/attach/render/markdown-source.js +0 -6
  10. package/dist/clients/attach/render/tool-calls.js +16 -2
  11. package/dist/clients/attach/viewer.js +613 -602
  12. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
  13. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
  14. package/dist/clients/inbox/controller.js +4 -4
  15. package/dist/clients/inbox/page-adapter.d.ts +2 -1
  16. package/dist/clients/inbox/tui/render.js +46 -25
  17. package/dist/clients/inbox/tui/slots.d.ts +1 -3
  18. package/dist/clients/inbox/tui/slots.js +8 -37
  19. package/dist/clients/inbox/tui/types.d.ts +4 -3
  20. package/dist/commands/__tests__/human.test.js +160 -23
  21. package/dist/commands/canvas-browse.js +1 -1
  22. package/dist/commands/human/doc.js +21 -74
  23. package/dist/commands/human/html.d.ts +2 -0
  24. package/dist/commands/human/html.js +81 -0
  25. package/dist/commands/human/prompts.d.ts +5 -4
  26. package/dist/commands/human/prompts.js +90 -168
  27. package/dist/commands/human/shared.d.ts +5 -6
  28. package/dist/commands/human/shared.js +13 -61
  29. package/dist/commands/human.js +7 -2
  30. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  31. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  32. package/dist/core/__tests__/human-deliver.test.js +28 -37
  33. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  34. package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
  35. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
  36. package/dist/core/canvas/__tests__/attention.test.js +2 -2
  37. package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
  38. package/dist/core/canvas/browse/app.d.ts +1 -1
  39. package/dist/core/canvas/browse/app.js +21 -3
  40. package/dist/core/canvas/browse/render.d.ts +4 -0
  41. package/dist/core/canvas/browse/render.js +5 -1
  42. package/dist/core/canvas/canvas.d.ts +26 -0
  43. package/dist/core/canvas/canvas.js +55 -0
  44. package/dist/core/command.js +6 -11
  45. package/dist/core/config.js +3 -2
  46. package/dist/core/help.d.ts +3 -0
  47. package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
  48. package/dist/core/human/__tests__/html-markdown.test.js +52 -0
  49. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  50. package/dist/core/human/__tests__/page-catalog.test.js +16 -0
  51. package/dist/core/human/__tests__/page-render.test.js +40 -23
  52. package/dist/core/human/__tests__/page-tickets.test.js +48 -27
  53. package/dist/core/human/__tests__/page.test.js +48 -65
  54. package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
  55. package/dist/core/human/answer-text.d.ts +3 -0
  56. package/dist/core/human/answer-text.js +98 -0
  57. package/dist/core/human/answer.d.ts +51 -0
  58. package/dist/core/human/answer.js +125 -0
  59. package/dist/core/human/component-docs.d.ts +14 -0
  60. package/dist/core/human/component-docs.js +161 -0
  61. package/dist/core/human/html-markdown.d.ts +3 -0
  62. package/dist/core/human/html-markdown.js +288 -0
  63. package/dist/core/human/page-catalog.d.ts +19 -3
  64. package/dist/core/human/page-catalog.js +73 -16
  65. package/dist/core/human/page-render.d.ts +3 -2
  66. package/dist/core/human/page-render.js +19 -45
  67. package/dist/core/human/page-schema.d.ts +108 -8
  68. package/dist/core/human/page-schema.js +88 -31
  69. package/dist/core/human/page-synth.d.ts +2 -4
  70. package/dist/core/human/page-synth.js +23 -7
  71. package/dist/core/human/page.d.ts +14 -7
  72. package/dist/core/human/page.js +178 -90
  73. package/dist/core/human/review-schema.d.ts +1 -0
  74. package/dist/core/human/review-schema.js +1 -1
  75. package/dist/core/human/scan.d.ts +3 -2
  76. package/dist/core/human/scan.js +7 -8
  77. package/dist/core/human/summary.d.ts +4 -7
  78. package/dist/core/human/summary.js +4 -60
  79. package/dist/core/human/tickets.d.ts +10 -9
  80. package/dist/core/human/tickets.js +5 -3
  81. package/dist/core/human/types.d.ts +4 -0
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +1 -0
  84. package/dist/core/review/realize.js +9 -1
  85. package/dist/core/runtime/boot-root.js +6 -1
  86. package/dist/core/runtime/broker.js +8 -6
  87. package/dist/core/runtime/busy.d.ts +4 -3
  88. package/dist/core/runtime/busy.js +4 -3
  89. package/dist/core/runtime/spawn.js +4 -4
  90. package/dist/core/termrender/display.d.ts +7 -3
  91. package/dist/core/termrender/display.js +45 -9
  92. package/dist/core/termrender/termrender.d.ts +3 -12
  93. package/dist/core/termrender/termrender.js +6 -37
  94. package/dist/core/user-settings.d.ts +17 -2
  95. package/dist/core/user-settings.js +32 -11
  96. package/dist/daemon/api/handlers/human.js +1 -1
  97. package/dist/daemon/api/handlers/inbox.js +22 -7
  98. package/dist/daemon/cron-run.js +16 -2
  99. package/dist/daemon/crtrd.js +1 -1
  100. package/dist/daemon/fleet.d.ts +22 -10
  101. package/dist/daemon/fleet.js +43 -24
  102. package/dist/daemon/human/finish.d.ts +2 -1
  103. package/dist/daemon/human/finish.js +45 -28
  104. package/dist/daemon/human/sweep.js +4 -2
  105. package/dist/daemon/reconcilers/broker-supervision.js +11 -8
  106. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  107. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  108. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  109. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  110. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  111. package/dist/daemon/review/sweep.js +12 -1
  112. package/dist/pages/bundle.css +1 -1
  113. package/dist/pages/bundle.js +1008 -577
  114. package/dist/pages/comments.d.ts +29 -0
  115. package/dist/pages/comments.js +51 -0
  116. package/dist/pages/controls.d.ts +98 -0
  117. package/dist/pages/controls.js +261 -0
  118. package/dist/pages/elements/cards.d.ts +15 -4
  119. package/dist/pages/elements/cards.js +299 -186
  120. package/dist/pages/elements/chart.d.ts +22 -0
  121. package/dist/pages/elements/chart.js +107 -91
  122. package/dist/pages/elements/options.d.ts +9 -0
  123. package/dist/pages/elements/options.js +268 -155
  124. package/dist/pages/elements/pages.d.ts +6 -0
  125. package/dist/pages/elements/pages.js +60 -38
  126. package/dist/pages/elements/slot.d.ts +5 -0
  127. package/dist/pages/elements/slot.js +17 -10
  128. package/dist/pages/elements/table.d.ts +17 -0
  129. package/dist/pages/elements/table.js +336 -228
  130. package/dist/pages/elements/text.d.ts +9 -3
  131. package/dist/pages/elements/text.js +350 -143
  132. package/dist/pages/host.d.ts +0 -24
  133. package/dist/pages/host.js +0 -17
  134. package/dist/pages/readonly.d.ts +24 -0
  135. package/dist/pages/readonly.js +31 -0
  136. package/dist/pages/responses.d.ts +37 -0
  137. package/dist/pages/responses.js +56 -0
  138. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  139. package/dist/pi-extensions/canvas-bash-valve.js +12 -8
  140. package/dist/types.d.ts +19 -5
  141. package/dist/types.js +2 -1
  142. package/package.json +1 -1
  143. package/runtime.lock.json +2 -2
  144. package/scripts/install-runtime.mjs +19 -24
  145. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  146. package/dist/clients/attach/render/html-markdown.js +0 -206
  147. package/dist/core/human/markdown-html.d.ts +0 -8
  148. package/dist/core/human/markdown-html.js +0 -581
@@ -9,14 +9,28 @@
9
9
  * (`j`/`k` move, `x` selects, `c` comments), so a card grid, an option list and a table
10
10
  * are all driven the same way.
11
11
  *
12
- * Data arrives one of two ways: inline `cards`, or a `source` the host resolves through
13
- * `artifactData()`. The element never touches the network, and anything it cannot render —
14
- * a malformed config, an unreachable source, source data that is not a card list — becomes
15
- * a named unavailable state rather than an exception.
12
+ * Data arrives inline as `cards`. The element never touches the network: a page source file
13
+ * is read and embedded once at submit, before this element renders.
14
+ *
15
+ * ## Every value here is a `--crtr-cards-*` token
16
+ *
17
+ * The card surface, its heading, its tag, the comment affordance and the three notice states
18
+ * are all painted through `var(--crtr-cards-<part>-<knob>, <kit default>)`, so a host restyles
19
+ * this element by setting properties on the document root — custom properties inherit into
20
+ * shadow roots, which is the whole mechanism. The shared control, writing-box and focus
21
+ * recipes carry their own element layer through the `{ scope }` each call passes, so the
22
+ * card's radio reads `--crtr-cards-control-*` over the family's `--crtr-control-box-*`.
23
+ *
24
+ * Defaults stay inline at the rule that spends them and no `--crtr-cards-*` token is ever
25
+ * declared: an unset token is what carries the kit default down the chain, which is why an
26
+ * unstyled page renders exactly as it did before any of this existed.
16
27
  */
17
- import { adoptComments, findComment, setComment } from '../comments.js';
18
- import { artifactData, respond, savedIds, savedResponse } from '../host.js';
28
+ import { adoptComments, canRecordComments, findComment, setComment } from '../comments.js';
29
+ import { controlStyle, focusRingStyle, writingBoxStyle } from '../controls.js';
30
+ import { respond, savedIds, savedResponse } from '../host.js';
31
+ import { applyReadOnly, isReadOnly } from '../readonly.js';
19
32
  import { registerElement } from '../register.js';
33
+ import { registerSlotResponse } from '../responses.js';
20
34
  import { readSlotConfig, readSlotId } from '../slot-config.js';
21
35
  const STYLE = `
22
36
  :host {
@@ -29,13 +43,23 @@ const STYLE = `
29
43
  [part~="container"] {
30
44
  display: flex;
31
45
  flex-direction: column;
32
- gap: var(--crtr-space-1);
46
+ gap: var(--crtr-cards-container-gap, var(--crtr-space-1));
33
47
  }
34
48
 
49
+ /* The question reads as the agent's own prose above its control, matching the table's
50
+ caption rather than shouting over the cards it introduces. The container's gap is a
51
+ hairline, so the breathing room under the prompt is the prompt's own. */
52
+ [part~="label"] {
53
+ margin: 0 0 var(--crtr-cards-label-gap, var(--crtr-space-2));
54
+ color: var(--crtr-cards-label-text-color, var(--crtr-color-text-muted));
55
+ }
56
+
57
+ /* The column count is anatomy rather than paint — a container query cannot read a custom
58
+ property, so the breakpoint below stays a literal. */
35
59
  [part~="grid"] {
36
60
  display: grid;
37
61
  grid-template-columns: minmax(0, 1fr);
38
- gap: var(--crtr-space-3);
62
+ gap: var(--crtr-cards-grid-gap, var(--crtr-space-3));
39
63
  }
40
64
 
41
65
  @container (min-width: 30rem) {
@@ -51,35 +75,44 @@ const STYLE = `
51
75
  [part~="card"] {
52
76
  display: flex;
53
77
  flex-direction: column;
54
- gap: var(--crtr-space-2);
55
- padding: var(--crtr-space-3);
56
- color: var(--crtr-color-text);
57
- background: var(--crtr-color-surface);
58
- border: 1px solid color-mix(in srgb, var(--crtr-color-border) 60%, transparent);
59
- border-radius: var(--crtr-radius-medium);
60
- transition: background-color 120ms ease, border-color 120ms ease;
78
+ gap: var(--crtr-cards-card-gap, var(--crtr-space-2));
79
+ padding: var(--crtr-cards-card-padding, var(--crtr-space-3));
80
+ color: var(--crtr-cards-card-text-color, var(--crtr-color-text));
81
+ background: var(--crtr-cards-card-fill, var(--crtr-color-surface));
82
+ border: var(--crtr-cards-card-border-width, 1px)
83
+ var(--crtr-cards-card-border-style, solid)
84
+ var(--crtr-cards-card-border-color, color-mix(in srgb, var(--crtr-color-border) 60%, transparent));
85
+ border-radius: var(--crtr-cards-card-radius, var(--crtr-radius-medium));
86
+ /* The whole card is the target, not just its heading — see the click handler. */
87
+ cursor: pointer;
88
+ transition: background-color var(--crtr-cards-card-transition, 120ms ease),
89
+ border-color var(--crtr-cards-card-transition, 120ms ease);
90
+ }
91
+
92
+ [part~="card"]:has([part~="control"]:disabled) {
93
+ cursor: default;
61
94
  }
62
95
 
63
96
  [part~="card"]:hover,
64
97
  [part~="card"][part~="active"] {
65
- background: var(--crtr-color-surface-hover);
66
- border-color: var(--crtr-color-border);
98
+ background: var(--crtr-cards-card-hover-fill, var(--crtr-color-surface-hover));
99
+ border-color: var(--crtr-cards-card-hover-border-color, var(--crtr-color-border));
67
100
  }
68
101
 
69
102
  [part~="card"][part~="selected"] {
70
- background: color-mix(in srgb, var(--crtr-color-accent) 7%, transparent);
71
- border-color: color-mix(in srgb, var(--crtr-color-accent) 40%, transparent);
103
+ background: var(--crtr-cards-card-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
104
+ border-color: var(--crtr-cards-card-selected-border-color, color-mix(in srgb, var(--crtr-color-accent) 45%, transparent));
72
105
  }
73
106
 
74
107
  [part~="card"][part~="selected"]:hover,
75
108
  [part~="card"][part~="selected"][part~="active"] {
76
- background: color-mix(in srgb, var(--crtr-color-accent) 12%, transparent);
109
+ background: var(--crtr-cards-card-selected-hover-fill, color-mix(in srgb, var(--crtr-color-accent) 14%, transparent));
77
110
  }
78
111
 
79
112
  [part~="card-header"] {
80
113
  display: flex;
81
114
  align-items: flex-start;
82
- gap: var(--crtr-space-2);
115
+ gap: var(--crtr-cards-card-header-gap, var(--crtr-space-2));
83
116
  }
84
117
 
85
118
  /* The label carries the click target, so the whole heading toggles the native control
@@ -88,62 +121,61 @@ const STYLE = `
88
121
  display: flex;
89
122
  flex: 1;
90
123
  align-items: flex-start;
91
- gap: var(--crtr-space-2);
124
+ gap: var(--crtr-cards-card-label-gap, var(--crtr-space-2));
92
125
  min-width: 0;
93
- font: inherit;
126
+ font: var(--crtr-cards-card-label-font, inherit);
94
127
  cursor: pointer;
95
128
  }
96
129
 
97
- [part~="control"] {
98
- flex: none;
99
- margin: 0;
100
- margin-top: 0.2em;
101
- accent-color: var(--crtr-color-accent);
102
- }
130
+ ${controlStyle({ scope: 'cards-control' }, '[part~="control"]')}
103
131
 
104
- [part~="control"]:focus-visible {
105
- outline: 2px solid var(--crtr-color-focus);
106
- outline-offset: 2px;
132
+ /* Centred on the card's title line, which sets its own tighter line-height. The box size
133
+ is read back through --control-box, the local the recipe above resolves it into, so a
134
+ host that resizes the control keeps it centred without touching this offset. */
135
+ [part~="control"] {
136
+ margin-top: var(--crtr-cards-control-offset, calc((1.35em - var(--control-box)) / 2));
107
137
  }
108
138
 
109
139
  [part~="card-heading"] {
110
140
  display: grid;
111
141
  flex: 1;
112
- gap: calc(var(--crtr-space-1) / 2);
142
+ gap: var(--crtr-cards-card-heading-gap, calc(var(--crtr-space-1) / 2));
113
143
  min-width: 0;
114
144
  }
115
145
 
116
146
  [part~="card-title"] {
117
- font-weight: 500;
118
- line-height: 1.35;
147
+ font-weight: var(--crtr-cards-card-title-font-weight, 500);
148
+ line-height: var(--crtr-cards-card-title-line-height, 1.35);
119
149
  }
120
150
 
121
151
  [part~="card-subtitle"] {
122
- color: var(--crtr-color-text-muted);
123
- font-size: calc(var(--crtr-font-size) * 0.85);
152
+ color: var(--crtr-cards-card-subtitle-text-color, var(--crtr-color-text-muted));
153
+ font-size: var(--crtr-cards-card-subtitle-font-size, 0.875em);
124
154
  }
125
155
 
126
156
  [part~="card-body"] {
127
- margin: 0;
128
- color: var(--crtr-color-text-muted);
129
- font-size: calc(var(--crtr-font-size) * 0.85);
130
- line-height: 1.6;
157
+ margin: var(--crtr-cards-card-body-margin, 0);
158
+ color: var(--crtr-cards-card-body-text-color, var(--crtr-color-text-muted));
159
+ font-size: var(--crtr-cards-card-body-font-size, 0.875em);
160
+ line-height: var(--crtr-cards-card-body-line-height, 1.6);
131
161
  }
132
162
 
133
163
  [part~="tag"] {
134
164
  align-self: flex-start;
135
- padding: 0 var(--crtr-space-1);
136
- color: var(--crtr-color-text-muted);
137
- font-size: calc(var(--crtr-font-size) * 0.78);
138
- line-height: 1.7;
139
- border: 1px solid var(--crtr-color-border);
140
- border-radius: var(--crtr-radius-small);
165
+ padding: var(--crtr-cards-tag-padding, 0 var(--crtr-space-1));
166
+ color: var(--crtr-cards-tag-text-color, var(--crtr-color-text-muted));
167
+ font-size: var(--crtr-cards-tag-font-size, 0.75em);
168
+ line-height: var(--crtr-cards-tag-line-height, 1.7);
169
+ border: var(--crtr-cards-tag-border-width, 1px)
170
+ var(--crtr-cards-tag-border-style, solid)
171
+ var(--crtr-cards-tag-border-color, var(--crtr-color-border));
172
+ border-radius: var(--crtr-cards-tag-radius, var(--crtr-radius-small));
141
173
  }
142
174
 
143
175
  [part~="card"][part~="selected"] [part~="tag"] {
144
- color: var(--crtr-color-accent);
145
- background: color-mix(in srgb, var(--crtr-color-accent) 10%, transparent);
146
- border-color: color-mix(in srgb, var(--crtr-color-accent) 40%, transparent);
176
+ color: var(--crtr-cards-tag-selected-text-color, var(--crtr-color-accent));
177
+ background: var(--crtr-cards-tag-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
178
+ border-color: var(--crtr-cards-tag-selected-border-color, color-mix(in srgb, var(--crtr-color-accent) 45%, transparent));
147
179
  }
148
180
 
149
181
  /* The affordance is quiet until you reach for it, and stays visible once a note lives
@@ -153,139 +185,185 @@ const STYLE = `
153
185
  display: flex;
154
186
  align-items: center;
155
187
  justify-content: center;
156
- width: calc(var(--crtr-font-size) * 1.6);
157
- height: calc(var(--crtr-font-size) * 1.6);
158
- padding: 0;
159
- color: var(--crtr-color-text-muted);
160
- background: none;
161
- border: none;
162
- border-radius: var(--crtr-radius-small);
188
+ /* One knob: the affordance is a square, so its width and height are the same value. */
189
+ width: var(--crtr-cards-comment-button-size, 1.5em);
190
+ height: var(--crtr-cards-comment-button-size, 1.5em);
191
+ padding: var(--crtr-cards-comment-button-padding, 0);
192
+ color: var(--crtr-cards-comment-button-text-color, var(--crtr-color-text-muted));
193
+ background: var(--crtr-cards-comment-button-fill, none);
194
+ border: var(--crtr-cards-comment-button-border-width, 1px)
195
+ var(--crtr-cards-comment-button-border-style, solid)
196
+ var(--crtr-cards-comment-button-border-color, transparent);
197
+ border-radius: var(--crtr-cards-comment-button-radius, var(--crtr-radius-small));
163
198
  cursor: pointer;
164
- opacity: 0;
165
- transition: opacity 120ms ease, color 120ms ease, background-color 120ms ease;
199
+ opacity: var(--crtr-cards-comment-button-resting-opacity, 0);
200
+ transition: opacity var(--crtr-cards-comment-button-transition, var(--crtr-control-transition, 120ms ease)),
201
+ color var(--crtr-cards-comment-button-transition, var(--crtr-control-transition, 120ms ease)),
202
+ background-color var(--crtr-cards-comment-button-transition, var(--crtr-control-transition, 120ms ease));
166
203
  }
167
204
 
168
205
  [part~="comment-button"] svg {
169
- width: calc(var(--crtr-font-size) * 0.95);
170
- height: calc(var(--crtr-font-size) * 0.95);
206
+ width: var(--crtr-cards-comment-button-icon-size, 0.875em);
207
+ height: var(--crtr-cards-comment-button-icon-size, 0.875em);
208
+ fill: var(--crtr-cards-comment-button-icon-fill, none);
209
+ stroke: var(--crtr-cards-comment-button-icon-stroke-color, currentColor);
210
+ stroke-width: var(--crtr-cards-comment-button-icon-stroke-width, 2);
211
+ stroke-linecap: var(--crtr-cards-comment-button-icon-line-cap, round);
212
+ stroke-linejoin: var(--crtr-cards-comment-button-icon-line-join, round);
171
213
  /* The glyph is never the event target: a repaint that swaps it mid-press would
172
214
  otherwise leave mousedown and mouseup on different nodes and eat the click. */
173
215
  pointer-events: none;
174
216
  }
175
217
 
176
218
  [part~="comment-button"]:hover {
177
- color: var(--crtr-color-text);
178
- background: var(--crtr-color-surface-hover);
219
+ color: var(--crtr-cards-comment-button-hover-text-color, var(--crtr-color-text));
220
+ background: var(--crtr-cards-comment-button-hover-fill, var(--crtr-color-surface-hover));
179
221
  }
180
222
 
181
223
  [part~="comment-button"][part~="has-comment"] {
182
- color: var(--crtr-color-accent);
224
+ color: var(--crtr-cards-comment-button-noted-text-color, var(--crtr-color-accent));
183
225
  }
184
226
 
227
+ ${focusRingStyle({ scope: 'cards-comment-button' }, '[part~="comment-button"]')}
185
228
  [part~="card"]:hover [part~="comment-button"],
186
229
  [part~="card"]:focus-within [part~="comment-button"],
187
230
  [part~="comment-button"]:focus-visible,
188
231
  [part~="comment-button"][part~="has-comment"] {
189
- opacity: 1;
232
+ opacity: var(--crtr-cards-comment-button-revealed-opacity, 1);
190
233
  }
191
234
 
192
235
  [part~="comment-editor"] {
193
236
  display: grid;
194
- gap: var(--crtr-space-2);
237
+ gap: var(--crtr-cards-comment-editor-gap, var(--crtr-space-2));
195
238
  width: 100%;
196
- padding: var(--crtr-space-2);
197
- text-align: left;
198
- background: var(--crtr-color-background);
199
- border: 1px solid var(--crtr-color-border);
200
- border-radius: var(--crtr-radius-small);
239
+ padding: var(--crtr-cards-comment-editor-padding, var(--crtr-space-2));
240
+ text-align: var(--crtr-cards-comment-editor-text-align, left);
241
+ background: var(--crtr-cards-comment-editor-fill, var(--crtr-color-background));
242
+ border: var(--crtr-cards-comment-editor-border-width, 1px)
243
+ var(--crtr-cards-comment-editor-border-style, solid)
244
+ var(--crtr-cards-comment-editor-border-color, var(--crtr-color-border));
245
+ border-radius: var(--crtr-cards-comment-editor-radius, var(--crtr-radius-small));
201
246
  cursor: auto;
202
247
  }
203
248
 
204
- [part~="comment-subject"],
249
+ /* The editor's own prose, in the page's voice — not a caps-mono field label shouting a
250
+ card id at someone who just clicked that card. */
251
+ [part~="comment-subject"] {
252
+ color: var(--crtr-cards-comment-subject-text-color, var(--crtr-color-text-muted));
253
+ font-size: var(--crtr-cards-comment-subject-font-size, 0.875em);
254
+ }
255
+
205
256
  [part~="comment-hint"] {
206
- color: var(--crtr-color-text-muted);
207
- font-family: var(--crtr-font-mono);
208
- font-size: calc(var(--crtr-font-size) * 0.72);
209
- letter-spacing: 0.06em;
210
- text-transform: uppercase;
257
+ color: var(--crtr-cards-comment-hint-text-color, var(--crtr-color-text-muted));
258
+ font-size: var(--crtr-cards-comment-hint-font-size, 0.875em);
211
259
  }
212
260
 
261
+ ${writingBoxStyle({ scope: 'cards-comment-input' }, '[part~="comment-input"]')}
213
262
  [part~="comment-input"] {
214
- box-sizing: border-box;
215
- width: 100%;
216
- min-height: 4.5em;
217
- padding: var(--crtr-space-1) var(--crtr-space-2);
218
- color: var(--crtr-color-text);
219
- font: inherit;
220
- background: var(--crtr-color-background);
221
- border: 1px solid var(--crtr-color-border);
222
- border-radius: var(--crtr-radius-small);
263
+ min-height: var(--crtr-cards-comment-input-min-height, 4.5em);
223
264
  resize: vertical;
224
265
  }
225
266
 
226
- [part~="comment-input"]:focus-visible {
227
- outline: 2px solid var(--crtr-color-focus);
228
- outline-offset: 1px;
229
- }
230
-
231
267
  [part~="comment-actions"] {
232
268
  display: flex;
233
269
  align-items: center;
234
270
  justify-content: space-between;
235
- gap: var(--crtr-space-2);
271
+ gap: var(--crtr-cards-comment-actions-gap, var(--crtr-space-2));
236
272
  }
237
273
 
238
274
  [part~="comment-buttons"] {
239
275
  display: flex;
240
- gap: var(--crtr-space-1);
241
- }
242
-
243
- [part~="comment-cancel"],
244
- [part~="comment-save"] {
245
- padding: var(--crtr-space-1) var(--crtr-space-2);
246
- font: inherit;
247
- font-size: calc(var(--crtr-font-size) * 0.9);
248
- border-radius: var(--crtr-radius-small);
249
- cursor: pointer;
276
+ gap: var(--crtr-cards-comment-buttons-gap, var(--crtr-space-1));
250
277
  }
251
278
 
252
279
  [part~="comment-cancel"] {
253
- color: var(--crtr-color-text-muted);
254
- background: none;
255
- border: 1px solid transparent;
280
+ padding: var(--crtr-cards-comment-cancel-padding, var(--crtr-space-1) var(--crtr-space-2));
281
+ color: var(--crtr-cards-comment-cancel-text-color, var(--crtr-color-text-muted));
282
+ font: var(--crtr-cards-comment-cancel-font, inherit);
283
+ font-size: var(--crtr-cards-comment-cancel-font-size, 0.875em);
284
+ background: var(--crtr-cards-comment-cancel-fill, none);
285
+ border: var(--crtr-cards-comment-cancel-border-width, 1px)
286
+ var(--crtr-cards-comment-cancel-border-style, solid)
287
+ var(--crtr-cards-comment-cancel-border-color, transparent);
288
+ border-radius: var(--crtr-cards-comment-cancel-radius, var(--crtr-radius-small));
289
+ cursor: pointer;
290
+ transition: color var(--crtr-cards-comment-cancel-transition, var(--crtr-control-transition, 120ms ease)),
291
+ background-color var(--crtr-cards-comment-cancel-transition, var(--crtr-control-transition, 120ms ease)),
292
+ border-color var(--crtr-cards-comment-cancel-transition, var(--crtr-control-transition, 120ms ease));
256
293
  }
257
294
 
258
295
  [part~="comment-cancel"]:hover {
259
- color: var(--crtr-color-text);
260
- background: var(--crtr-color-surface-hover);
296
+ color: var(--crtr-cards-comment-cancel-hover-text-color, var(--crtr-color-text));
297
+ background: var(--crtr-cards-comment-cancel-hover-fill, var(--crtr-color-surface-hover));
261
298
  }
262
299
 
263
300
  [part~="comment-save"] {
264
- color: var(--crtr-color-accent-contrast);
265
- background: var(--crtr-color-accent);
266
- border: 1px solid var(--crtr-color-accent);
301
+ padding: var(--crtr-cards-comment-save-padding, var(--crtr-space-1) var(--crtr-space-2));
302
+ color: var(--crtr-cards-comment-save-text-color, var(--crtr-color-accent-contrast));
303
+ font: var(--crtr-cards-comment-save-font, inherit);
304
+ font-size: var(--crtr-cards-comment-save-font-size, 0.875em);
305
+ background: var(--crtr-cards-comment-save-fill, var(--crtr-color-accent));
306
+ border: var(--crtr-cards-comment-save-border-width, 1px)
307
+ var(--crtr-cards-comment-save-border-style, solid)
308
+ var(--crtr-cards-comment-save-border-color, var(--crtr-color-accent));
309
+ border-radius: var(--crtr-cards-comment-save-radius, var(--crtr-radius-small));
310
+ cursor: pointer;
311
+ transition: color var(--crtr-cards-comment-save-transition, var(--crtr-control-transition, 120ms ease)),
312
+ background-color var(--crtr-cards-comment-save-transition, var(--crtr-control-transition, 120ms ease)),
313
+ border-color var(--crtr-cards-comment-save-transition, var(--crtr-control-transition, 120ms ease));
314
+ }
315
+
316
+ /* Mixing toward the text colour darkens on a light token set and lightens on a dark one —
317
+ a brightness filter moves the wrong way on one of them. */
318
+ [part~="comment-save"]:hover {
319
+ background: var(--crtr-cards-comment-save-hover-fill, color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
320
+ border-color: var(--crtr-cards-comment-save-hover-border-color, color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
321
+ }
322
+
323
+ ${focusRingStyle({ scope: 'cards-comment-cancel' }, '[part~="comment-cancel"]')}
324
+ ${focusRingStyle({ scope: 'cards-comment-save' }, '[part~="comment-save"]')}
325
+ /* These sit in the flow of the page rather than above it, so they carry no elevation —
326
+ a shadow in this bundle means floating (a popover, an editor). Each state keeps its own
327
+ scope even though their kit defaults match: one scope never names two different parts. */
328
+ [part~="unavailable"] {
329
+ padding: var(--crtr-cards-unavailable-padding, var(--crtr-space-3));
330
+ color: var(--crtr-cards-unavailable-text-color, var(--crtr-color-text-muted));
331
+ background: var(--crtr-cards-unavailable-fill, var(--crtr-color-surface));
332
+ border: var(--crtr-cards-unavailable-border-width, 1px)
333
+ var(--crtr-cards-unavailable-border-style, solid)
334
+ var(--crtr-cards-unavailable-border-color, var(--crtr-color-border));
335
+ border-radius: var(--crtr-cards-unavailable-radius, var(--crtr-radius-medium));
336
+ }
337
+
338
+ [part~="empty"] {
339
+ padding: var(--crtr-cards-empty-padding, var(--crtr-space-3));
340
+ color: var(--crtr-cards-empty-text-color, var(--crtr-color-text-muted));
341
+ background: var(--crtr-cards-empty-fill, var(--crtr-color-surface));
342
+ border: var(--crtr-cards-empty-border-width, 1px)
343
+ var(--crtr-cards-empty-border-style, solid)
344
+ var(--crtr-cards-empty-border-color, var(--crtr-color-border));
345
+ border-radius: var(--crtr-cards-empty-radius, var(--crtr-radius-medium));
267
346
  }
268
347
 
269
- [part~="unavailable"],
270
- [part~="empty"],
271
348
  [part~="loading"] {
272
- padding: var(--crtr-space-3);
273
- color: var(--crtr-color-text-muted);
274
- background: var(--crtr-color-surface);
275
- border: 1px solid var(--crtr-color-border);
276
- border-radius: var(--crtr-radius-medium);
277
- box-shadow: var(--crtr-shadow);
349
+ padding: var(--crtr-cards-loading-padding, var(--crtr-space-3));
350
+ color: var(--crtr-cards-loading-text-color, var(--crtr-color-text-muted));
351
+ background: var(--crtr-cards-loading-fill, var(--crtr-color-surface));
352
+ border: var(--crtr-cards-loading-border-width, 1px)
353
+ var(--crtr-cards-loading-border-style, solid)
354
+ var(--crtr-cards-loading-border-color, var(--crtr-color-border));
355
+ border-radius: var(--crtr-cards-loading-radius, var(--crtr-radius-medium));
278
356
  }
279
357
 
280
358
  [part~="kind"] {
281
- color: var(--crtr-color-text);
282
- font-family: var(--crtr-font-mono);
359
+ color: var(--crtr-cards-kind-text-color, var(--crtr-color-text));
360
+ font-family: var(--crtr-cards-kind-font-family, var(--crtr-font-mono));
283
361
  }
284
362
 
285
363
  [part~="reason"] {
286
364
  display: block;
287
- margin-top: var(--crtr-space-1);
288
- font-size: calc(var(--crtr-font-size) * 0.85);
365
+ margin-top: var(--crtr-cards-reason-gap, var(--crtr-space-1));
366
+ font-size: var(--crtr-cards-reason-font-size, 0.875em);
289
367
  }
290
368
  `;
291
369
  /**
@@ -333,35 +411,36 @@ function normalizeCards(raw) {
333
411
  }
334
412
  return cards;
335
413
  }
336
- const CONFIG_KEYS = new Set(['cards', 'source', 'mode', 'columns']);
414
+ const CONFIG_KEYS = new Set(['label', 'cards', 'mode', 'columns']);
415
+ /** Ids are scoped to this element's shadow root, so one constant is unique everywhere. */
416
+ const LABEL_ID = 'prompt-label';
337
417
  /** Mirrors strict `cardsConfigSchema`, so this element accepts exactly what the agent may send. */
338
418
  function normalizeConfig(raw) {
339
419
  for (const key of Object.keys(raw)) {
340
420
  if (!CONFIG_KEYS.has(key))
341
421
  return { status: 'invalid', reason: `unknown config property "${key}"` };
342
422
  }
423
+ const label = raw.label;
424
+ if (label !== undefined && typeof label !== 'string')
425
+ return { status: 'invalid', reason: 'label must be a string' };
343
426
  const mode = raw.mode;
344
427
  if (mode !== 'single' && mode !== 'multi')
345
428
  return { status: 'invalid', reason: 'mode must be "single" or "multi"' };
346
- // The source string is opaque to this element — the host is the only thing that reads it.
347
- const source = raw.source;
348
- if (source !== undefined && typeof source !== 'string') {
349
- return { status: 'invalid', reason: 'source must be a string' };
350
- }
351
429
  const columns = raw.columns;
352
430
  if (columns !== undefined && columns !== 2 && columns !== 3) {
353
431
  return { status: 'invalid', reason: 'columns must be 2 or 3' };
354
432
  }
355
433
  let cards;
356
434
  if (raw.cards !== undefined) {
357
- if (source !== undefined)
358
- return { status: 'invalid', reason: 'cards and source are mutually exclusive' };
359
435
  const parsed = normalizeCards(raw.cards);
360
436
  if (parsed === null)
361
437
  return { status: 'invalid', reason: 'cards must be a list of uniquely identified cards' };
362
438
  cards = parsed;
363
439
  }
364
- return { status: 'ok', config: { cards, source: source, mode, columns } };
440
+ return {
441
+ status: 'ok',
442
+ config: { label: label, cards, mode, columns },
443
+ };
365
444
  }
366
445
  /**
367
446
  * Typing in the comment box is ordinary writing — the list grammar keeps out of it. A card's
@@ -381,11 +460,6 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
381
460
  function commentIcon(hasComment) {
382
461
  const svg = document.createElementNS(SVG_NS, 'svg');
383
462
  svg.setAttribute('viewBox', '0 0 24 24');
384
- svg.setAttribute('fill', 'none');
385
- svg.setAttribute('stroke', 'currentColor');
386
- svg.setAttribute('stroke-width', '2');
387
- svg.setAttribute('stroke-linecap', 'round');
388
- svg.setAttribute('stroke-linejoin', 'round');
389
463
  svg.setAttribute('aria-hidden', 'true');
390
464
  const bubble = document.createElementNS(SVG_NS, 'path');
391
465
  bubble.setAttribute('d', 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z');
@@ -409,12 +483,26 @@ function commentIcon(hasComment) {
409
483
  }
410
484
  return svg;
411
485
  }
486
+ /** The prompt above the grid, or nothing when the agent asked its question elsewhere. */
487
+ function buildLabel(text) {
488
+ if (text === undefined || text === '')
489
+ return null;
490
+ const label = document.createElement('p');
491
+ label.part.add('label');
492
+ label.id = LABEL_ID;
493
+ label.textContent = text;
494
+ return label;
495
+ }
412
496
  /** Radio groups are keyed by name, so every element on a page needs its own. */
413
497
  let groupCounter = 0;
414
498
  class CrtrCardsElement extends HTMLElement {
499
+ /** The host marks a resolved or canceled page `inert`; that is this element's read-only signal. */
500
+ static observedAttributes = ['inert'];
415
501
  #container = document.createElement('div');
416
502
  #group;
417
503
  #config = null;
504
+ /** The rendered prompt, kept across repaints because every render replaces the container. */
505
+ #label = null;
418
506
  #cards = [];
419
507
  #slotId;
420
508
  // Response state. Authoritative for rendering; `respond()` only asks the host to save it.
@@ -424,8 +512,6 @@ class CrtrCardsElement extends HTMLElement {
424
512
  #nodes = new Map();
425
513
  #open = null;
426
514
  #editor = null;
427
- /** Bumped on every (dis)connect so a late source read cannot paint a stale grid. */
428
- #generation = 0;
429
515
  constructor() {
430
516
  super();
431
517
  groupCounter += 1;
@@ -438,8 +524,6 @@ class CrtrCardsElement extends HTMLElement {
438
524
  root.append(style, this.#container);
439
525
  }
440
526
  connectedCallback() {
441
- this.#generation += 1;
442
- const generation = this.#generation;
443
527
  const read = readSlotConfig(this);
444
528
  if (read.status !== 'ok') {
445
529
  this.#renderUnavailable(read.reason);
@@ -452,37 +536,26 @@ class CrtrCardsElement extends HTMLElement {
452
536
  }
453
537
  this.#config = normalized.config;
454
538
  this.#slotId = readSlotId(this);
455
- const source = normalized.config.source;
456
- if (source === undefined) {
457
- this.#cards = normalized.config.cards ?? [];
458
- this.#hydrate();
459
- this.#build();
460
- return;
461
- }
462
- this.#renderStatus('loading', 'Loading cards…');
463
- void this.#resolveSource(source, generation);
464
- }
465
- disconnectedCallback() {
466
- this.#generation += 1;
467
- }
468
- /** The host resolves `source`; this element never touches the network. */
469
- async #resolveSource(source, generation) {
470
- const read = await artifactData(source);
471
- if (generation !== this.#generation)
472
- return;
473
- if (read.status !== 'ok') {
474
- this.#renderUnavailable(read.reason);
475
- return;
476
- }
477
- const cards = normalizeCards(read.data);
478
- if (cards === null) {
479
- this.#renderUnavailable(`source "${source}" is not a list of cards`);
480
- return;
481
- }
482
- this.#cards = cards;
539
+ // Registered before render: the pager reports every response-bearing slot
540
+ // at submit, whether or not the person ever selected a card.
541
+ if (this.#slotId !== undefined)
542
+ registerSlotResponse(this, this.#slotId, () => this.#response());
543
+ this.#label = buildLabel(normalized.config.label);
544
+ this.#cards = normalized.config.cards ?? [];
483
545
  this.#hydrate();
484
546
  this.#build();
485
547
  }
548
+ attributeChangedCallback() {
549
+ if (this.isConnected)
550
+ this.#syncReadOnly();
551
+ }
552
+ /** Repainted after every build, because a rebuild replaces the controls. */
553
+ #syncReadOnly() {
554
+ const readOnly = isReadOnly(this);
555
+ applyReadOnly(this.#container, readOnly);
556
+ if (readOnly && this.#open !== null)
557
+ this.#closeEditor(false);
558
+ }
486
559
  /**
487
560
  * Seed local state from the answer the host is already holding, so a reopened page shows
488
561
  * the cards that were picked. `#build` is what drops ids the current data no longer has,
@@ -513,14 +586,21 @@ class CrtrCardsElement extends HTMLElement {
513
586
  detail.part.add('reason');
514
587
  detail.textContent = reason;
515
588
  box.append('Component unavailable: ', kind, detail);
516
- this.#container.replaceChildren(box);
589
+ this.#setContent(box);
517
590
  }
518
591
  #renderStatus(part, text) {
519
592
  const box = document.createElement('div');
520
593
  box.part.add(part);
521
594
  box.setAttribute('role', 'status');
522
595
  box.textContent = text;
523
- this.#container.replaceChildren(box);
596
+ this.#setContent(box);
597
+ }
598
+ /** Every repaint replaces the container's children, so the prompt is re-laid each time. */
599
+ #setContent(content) {
600
+ if (this.#label === null)
601
+ this.#container.replaceChildren(content);
602
+ else
603
+ this.#container.replaceChildren(this.#label, content);
524
604
  }
525
605
  /** Builds the grid once per data load; every later change repaints in place. */
526
606
  #build() {
@@ -543,8 +623,13 @@ class CrtrCardsElement extends HTMLElement {
543
623
  grid.part.add('grid');
544
624
  if (config.columns === 3)
545
625
  grid.dataset.columns = '3';
626
+ if (this.#label !== null) {
627
+ // A named group of choices: single-select is a radio group, multi a plain group.
628
+ grid.setAttribute('role', config.mode === 'single' ? 'radiogroup' : 'group');
629
+ grid.setAttribute('aria-labelledby', LABEL_ID);
630
+ }
546
631
  grid.append(...this.#cards.map((card) => this.#buildCard(card, config.mode, this.#group)));
547
- this.#container.replaceChildren(grid);
632
+ this.#setContent(grid);
548
633
  this.#paint();
549
634
  }
550
635
  #buildCard(card, mode, group) {
@@ -576,16 +661,21 @@ class CrtrCardsElement extends HTMLElement {
576
661
  subtitle.textContent = card.subtitle;
577
662
  heading.append(subtitle);
578
663
  }
579
- const commentButton = document.createElement('button');
580
- commentButton.type = 'button';
581
- commentButton.part.add('comment-button');
582
- commentButton.addEventListener('click', (event) => {
583
- event.stopPropagation();
584
- this.#setActive(card.id, false);
585
- this.#toggleEditor({ kind: 'card', cardId: card.id }, card.title, root, null);
586
- });
664
+ let commentButton;
665
+ if (canRecordComments(this.#slotId)) {
666
+ commentButton = document.createElement('button');
667
+ commentButton.type = 'button';
668
+ commentButton.part.add('comment-button');
669
+ commentButton.addEventListener('click', (event) => {
670
+ event.stopPropagation();
671
+ this.#setActive(card.id, false);
672
+ this.#toggleEditor({ kind: 'card', cardId: card.id }, card.title, root, null);
673
+ });
674
+ }
587
675
  label.append(control, heading);
588
- header.append(label, commentButton);
676
+ header.append(label);
677
+ if (commentButton !== undefined)
678
+ header.append(commentButton);
589
679
  root.append(header);
590
680
  if (card.body !== undefined && card.body !== '') {
591
681
  const body = document.createElement('p');
@@ -606,7 +696,28 @@ class CrtrCardsElement extends HTMLElement {
606
696
  this.#setActive(null, false);
607
697
  });
608
698
  root.addEventListener('focusin', () => this.#setActive(card.id, false));
609
- this.#nodes.set(card.id, { root, control, commentButton, tag });
699
+ // The whole card is the choice, not just its heading: a click on the body, the tag, or
700
+ // the padding around them picks the card. A click that went through the label is left
701
+ // alone — the native control has already toggled from it, and toggling again here would
702
+ // undo the user's own click. The comment button and the editor stop propagation, so a
703
+ // press on either never reaches this. A press that ends a text selection is someone
704
+ // reading the card, not choosing it.
705
+ root.addEventListener('click', (event) => {
706
+ if (control.disabled)
707
+ return;
708
+ if (event.composedPath().includes(label))
709
+ return;
710
+ if (document.getSelection()?.isCollapsed === false)
711
+ return;
712
+ this.#setActive(card.id, false);
713
+ this.#toggle(card.id, !this.#selected.includes(card.id));
714
+ });
715
+ this.#nodes.set(card.id, {
716
+ root,
717
+ control,
718
+ ...(commentButton === undefined ? {} : { commentButton }),
719
+ tag,
720
+ });
610
721
  return root;
611
722
  }
612
723
  /** Repaints selection, activity and comment presence without rebuilding the grid. */
@@ -624,8 +735,10 @@ class CrtrCardsElement extends HTMLElement {
624
735
  // A radio group holds one tab stop; an unselected group still needs an entry point.
625
736
  node.control.tabIndex = mode === 'single' && card.id !== tabbableId ? -1 : 0;
626
737
  const comment = findComment(this.#comments, { kind: 'card', cardId: card.id });
627
- this.#paintCommentButton(node.commentButton, comment, card.title);
738
+ if (node.commentButton !== undefined)
739
+ this.#paintCommentButton(node.commentButton, comment, card.title);
628
740
  }
741
+ this.#syncReadOnly();
629
742
  }
630
743
  #paintCommentButton(button, comment, target) {
631
744
  const has = comment !== undefined;
@@ -651,7 +764,7 @@ class CrtrCardsElement extends HTMLElement {
651
764
  this.#nodes.get(id)?.control.focus();
652
765
  }
653
766
  #toggle(cardId, selected) {
654
- if (this.#config === null)
767
+ if (this.#config === null || isReadOnly(this))
655
768
  return;
656
769
  if (this.#config.mode === 'single') {
657
770
  // Radio semantics: choosing a card replaces the answer, and re-choosing keeps it.
@@ -695,7 +808,7 @@ class CrtrCardsElement extends HTMLElement {
695
808
  event.preventDefault();
696
809
  const card = this.#cards.find((entry) => entry.id === currentId);
697
810
  const node = this.#nodes.get(currentId);
698
- if (card === undefined || node === undefined)
811
+ if (card === undefined || node?.commentButton === undefined)
699
812
  return;
700
813
  this.#setActive(currentId, false);
701
814
  this.#toggleEditor({ kind: 'card', cardId: card.id }, card.title, node.root, null);
@@ -706,7 +819,7 @@ class CrtrCardsElement extends HTMLElement {
706
819
  // ----------------------------------------------------------------- comments
707
820
  /** Opens the editor for `anchor`, or closes it when it is already the open one. */
708
821
  #toggleEditor(anchor, target, mount, after) {
709
- if (mount === null)
822
+ if (mount === null || !canRecordComments(this.#slotId) || isReadOnly(this))
710
823
  return;
711
824
  const openAnchor = this.#open?.anchor;
712
825
  const sameAnchor = openAnchor !== undefined &&
@@ -781,7 +894,7 @@ class CrtrCardsElement extends HTMLElement {
781
894
  }
782
895
  #commitComment(text) {
783
896
  const open = this.#open;
784
- if (open === null)
897
+ if (open === null || isReadOnly(this))
785
898
  return;
786
899
  this.#comments = setComment(this.#comments, open.anchor, text);
787
900
  this.#closeEditor(true);