@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.
- package/dist/api/client.d.ts +2 -2
- package/dist/api/dto/inbox.d.ts +46 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -3
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/render/chat-view.d.ts +3 -8
- package/dist/clients/attach/render/chat-view.js +3 -24
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/viewer.js +613 -602
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
- package/dist/clients/inbox/controller.js +4 -4
- package/dist/clients/inbox/page-adapter.d.ts +2 -1
- package/dist/clients/inbox/tui/render.js +46 -25
- package/dist/clients/inbox/tui/slots.d.ts +1 -3
- package/dist/clients/inbox/tui/slots.js +8 -37
- package/dist/clients/inbox/tui/types.d.ts +4 -3
- package/dist/commands/__tests__/human.test.js +160 -23
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/human/doc.js +21 -74
- package/dist/commands/human/html.d.ts +2 -0
- package/dist/commands/human/html.js +81 -0
- package/dist/commands/human/prompts.d.ts +5 -4
- package/dist/commands/human/prompts.js +90 -168
- package/dist/commands/human/shared.d.ts +5 -6
- package/dist/commands/human/shared.js +13 -61
- package/dist/commands/human.js +7 -2
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/human-deliver.test.js +28 -37
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
- package/dist/core/canvas/__tests__/attention.test.js +2 -2
- package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +26 -0
- package/dist/core/canvas/canvas.js +55 -0
- package/dist/core/command.js +6 -11
- package/dist/core/config.js +3 -2
- package/dist/core/help.d.ts +3 -0
- package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/html-markdown.test.js +52 -0
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +16 -0
- package/dist/core/human/__tests__/page-render.test.js +40 -23
- package/dist/core/human/__tests__/page-tickets.test.js +48 -27
- package/dist/core/human/__tests__/page.test.js +48 -65
- package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +51 -0
- package/dist/core/human/answer.js +125 -0
- package/dist/core/human/component-docs.d.ts +14 -0
- package/dist/core/human/component-docs.js +161 -0
- package/dist/core/human/html-markdown.d.ts +3 -0
- package/dist/core/human/html-markdown.js +288 -0
- package/dist/core/human/page-catalog.d.ts +19 -3
- package/dist/core/human/page-catalog.js +73 -16
- package/dist/core/human/page-render.d.ts +3 -2
- package/dist/core/human/page-render.js +19 -45
- package/dist/core/human/page-schema.d.ts +108 -8
- package/dist/core/human/page-schema.js +88 -31
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +23 -7
- package/dist/core/human/page.d.ts +14 -7
- package/dist/core/human/page.js +178 -90
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/scan.d.ts +3 -2
- package/dist/core/human/scan.js +7 -8
- package/dist/core/human/summary.d.ts +4 -7
- package/dist/core/human/summary.js +4 -60
- package/dist/core/human/tickets.d.ts +10 -9
- package/dist/core/human/tickets.js +5 -3
- package/dist/core/human/types.d.ts +4 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/review/realize.js +9 -1
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker.js +8 -6
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/termrender/display.d.ts +7 -3
- package/dist/core/termrender/display.js +45 -9
- package/dist/core/termrender/termrender.d.ts +3 -12
- package/dist/core/termrender/termrender.js +6 -37
- package/dist/core/user-settings.d.ts +17 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/inbox.js +22 -7
- package/dist/daemon/cron-run.js +16 -2
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +2 -1
- package/dist/daemon/human/finish.js +45 -28
- package/dist/daemon/human/sweep.js +4 -2
- package/dist/daemon/reconcilers/broker-supervision.js +11 -8
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pages/bundle.css +1 -1
- package/dist/pages/bundle.js +1008 -577
- package/dist/pages/comments.d.ts +29 -0
- package/dist/pages/comments.js +51 -0
- package/dist/pages/controls.d.ts +98 -0
- package/dist/pages/controls.js +261 -0
- package/dist/pages/elements/cards.d.ts +15 -4
- package/dist/pages/elements/cards.js +299 -186
- package/dist/pages/elements/chart.d.ts +22 -0
- package/dist/pages/elements/chart.js +107 -91
- package/dist/pages/elements/options.d.ts +9 -0
- package/dist/pages/elements/options.js +268 -155
- package/dist/pages/elements/pages.d.ts +6 -0
- package/dist/pages/elements/pages.js +60 -38
- package/dist/pages/elements/slot.d.ts +5 -0
- package/dist/pages/elements/slot.js +17 -10
- package/dist/pages/elements/table.d.ts +17 -0
- package/dist/pages/elements/table.js +336 -228
- package/dist/pages/elements/text.d.ts +9 -3
- package/dist/pages/elements/text.js +350 -143
- package/dist/pages/host.d.ts +0 -24
- package/dist/pages/host.js +0 -17
- package/dist/pages/readonly.d.ts +24 -0
- package/dist/pages/readonly.js +31 -0
- package/dist/pages/responses.d.ts +37 -0
- package/dist/pages/responses.js +56 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +12 -8
- package/dist/types.d.ts +19 -5
- package/dist/types.js +2 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/install-runtime.mjs +19 -24
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/core/human/markdown-html.d.ts +0 -8
- 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
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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 {
|
|
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:
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
|
71
|
-
border-color: color-mix(in srgb, var(--crtr-color-accent)
|
|
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)
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
138
|
-
line-height: 1.7;
|
|
139
|
-
border:
|
|
140
|
-
|
|
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)
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
border
|
|
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
|
|
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:
|
|
170
|
-
height:
|
|
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:
|
|
200
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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:
|
|
276
|
-
|
|
277
|
-
|
|
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:
|
|
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(['
|
|
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 {
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
this.#
|
|
459
|
-
|
|
460
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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);
|