@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
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The three shapes in the catalog are all this one element:
|
|
6
6
|
*
|
|
7
|
-
* - a **draft** the user edits in place —
|
|
8
|
-
* - **read** text the user leaves anchored comments on, review-style —
|
|
9
|
-
* the comment affordance appears at the selection;
|
|
7
|
+
* - a **draft** the user edits in place — pre-filled and editable by default;
|
|
8
|
+
* - **read** text the user leaves anchored comments on, review-style — set `editable: false`,
|
|
9
|
+
* then select a range and the comment affordance appears at the selection;
|
|
10
10
|
* - an **empty input** — `editable` with no starting text, which opens already editing.
|
|
11
11
|
*
|
|
12
|
+
* `singleLine` is the fourth shape and the one that is not prose: one compact form field for a
|
|
13
|
+
* subject line, a name, a URL. It is always editing, so it drops the edit toggle; it has no
|
|
14
|
+
* paragraphs to anchor a range against, so it drops the comment affordance and shows only the
|
|
15
|
+
* comments a saved answer already carried. Enter is a form field's Enter — it types nothing,
|
|
16
|
+
* and the pager's `⌘`/`Ctrl` + `Enter` still submits.
|
|
17
|
+
*
|
|
12
18
|
* Text is always commentable; that is not a capability switch, and edit and comment coexist.
|
|
13
19
|
* Reading is the resting state, because that is where anchored commenting lives; the edit
|
|
14
20
|
* toggle swaps the block for a plain textarea. Comments survive the switch, but a range
|
|
@@ -22,15 +28,20 @@
|
|
|
22
28
|
* and `comment-affordance.tsx` @ `3705fe9e2`), framework-free and styled only through the
|
|
23
29
|
* bundle's `--crtr-*` tokens.
|
|
24
30
|
*/
|
|
31
|
+
import { applyReadOnly, isReadOnly } from '../readonly.js';
|
|
25
32
|
import { registerElement } from '../register.js';
|
|
26
33
|
import { readSlotConfig, readSlotId } from '../slot-config.js';
|
|
27
34
|
import { respond, savedResponse } from '../host.js';
|
|
28
|
-
import {
|
|
35
|
+
import { registerSlotResponse } from '../responses.js';
|
|
36
|
+
import { adoptComments, canRecordComments, findComment, quotePreview, setComment } from '../comments.js';
|
|
37
|
+
import { focusRingStyle, writingBoxStyle } from '../controls.js';
|
|
29
38
|
const WHOLE = { kind: 'whole' };
|
|
30
39
|
const WHOLE_TARGET = 'the whole passage';
|
|
31
40
|
const RANGE_TARGET = 'the selected text';
|
|
32
41
|
const POPOVER_GAP = 6;
|
|
33
42
|
const POPOVER_ID = 'comment-popover';
|
|
43
|
+
/** Ids are scoped to this element's shadow root, so one constant is unique everywhere. */
|
|
44
|
+
const LABEL_ID = 'prompt-label';
|
|
34
45
|
const STYLE = `
|
|
35
46
|
:host {
|
|
36
47
|
display: block;
|
|
@@ -39,7 +50,14 @@ const STYLE = `
|
|
|
39
50
|
[part="container"] {
|
|
40
51
|
position: relative;
|
|
41
52
|
display: grid;
|
|
42
|
-
gap: var(--crtr-space-2);
|
|
53
|
+
gap: var(--crtr-text-container-gap, var(--crtr-space-2));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* The prompt reads as the agent's own prose above the passage, matching the table's
|
|
57
|
+
caption rather than shouting over the text it introduces. */
|
|
58
|
+
[part="label"] {
|
|
59
|
+
margin: var(--crtr-text-label-margin, 0);
|
|
60
|
+
color: var(--crtr-text-label-text-color, var(--crtr-color-text-muted));
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
[part="toolbar"] {
|
|
@@ -47,134 +65,178 @@ const STYLE = `
|
|
|
47
65
|
flex-wrap: wrap;
|
|
48
66
|
align-items: center;
|
|
49
67
|
justify-content: flex-end;
|
|
50
|
-
gap: var(--crtr-space-2);
|
|
68
|
+
gap: var(--crtr-text-toolbar-gap, var(--crtr-space-2));
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
[part="edited"] {
|
|
54
|
-
padding: 0 var(--crtr-space-2);
|
|
55
|
-
color: var(--crtr-color-accent);
|
|
56
|
-
background: color-mix(in srgb, var(--crtr-color-accent) 14%, transparent);
|
|
57
|
-
border-radius: var(--crtr-radius-small);
|
|
58
|
-
font-family: var(--crtr-font-mono);
|
|
59
|
-
font-size: 0.
|
|
60
|
-
letter-spacing: 0.06em;
|
|
61
|
-
line-height: 1.7;
|
|
62
|
-
text-transform: uppercase;
|
|
72
|
+
padding: var(--crtr-text-edited-padding, 0 var(--crtr-space-2));
|
|
73
|
+
color: var(--crtr-text-edited-text-color, var(--crtr-color-accent));
|
|
74
|
+
background: var(--crtr-text-edited-fill, color-mix(in srgb, var(--crtr-color-accent) 14%, transparent));
|
|
75
|
+
border-radius: var(--crtr-text-edited-radius, var(--crtr-radius-small));
|
|
76
|
+
font-family: var(--crtr-text-edited-font-family, var(--crtr-font-mono));
|
|
77
|
+
font-size: var(--crtr-text-edited-font-size, 0.75em);
|
|
78
|
+
letter-spacing: var(--crtr-text-edited-letter-spacing, 0.06em);
|
|
79
|
+
line-height: var(--crtr-text-edited-line-height, 1.7);
|
|
80
|
+
text-transform: var(--crtr-text-edited-text-transform, uppercase);
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
button {
|
|
66
84
|
display: inline-flex;
|
|
67
85
|
align-items: center;
|
|
68
|
-
gap: var(--crtr-space-1);
|
|
69
|
-
max-width: 100
|
|
70
|
-
padding: var(--crtr-space-1) var(--crtr-space-2);
|
|
71
|
-
color: var(--crtr-color-text-muted);
|
|
72
|
-
font: inherit;
|
|
73
|
-
background: transparent;
|
|
74
|
-
border: 1px
|
|
75
|
-
|
|
86
|
+
gap: var(--crtr-text-button-gap, var(--crtr-space-1));
|
|
87
|
+
max-width: var(--crtr-text-button-max-width, 100%);
|
|
88
|
+
padding: var(--crtr-text-button-padding, var(--crtr-space-1) var(--crtr-space-2));
|
|
89
|
+
color: var(--crtr-text-button-text-color, var(--crtr-color-text-muted));
|
|
90
|
+
font: var(--crtr-text-button-font, inherit);
|
|
91
|
+
background: var(--crtr-text-button-fill, transparent);
|
|
92
|
+
border: var(--crtr-text-button-border-width, 1px)
|
|
93
|
+
var(--crtr-text-button-border-style, solid)
|
|
94
|
+
var(--crtr-text-button-border-color, transparent);
|
|
95
|
+
border-radius: var(--crtr-text-button-radius, var(--crtr-radius-small));
|
|
76
96
|
cursor: pointer;
|
|
97
|
+
/* One motion knob for all three properties, over the family's, because a button that
|
|
98
|
+
settles at a different speed than the field beside it is the drift controls.ts exists
|
|
99
|
+
to remove. */
|
|
100
|
+
transition: color var(--crtr-text-button-transition, var(--crtr-control-transition, 120ms ease)),
|
|
101
|
+
background-color var(--crtr-text-button-transition, var(--crtr-control-transition, 120ms ease)),
|
|
102
|
+
border-color var(--crtr-text-button-transition, var(--crtr-control-transition, 120ms ease));
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
button:hover {
|
|
80
|
-
color: var(--crtr-color-text);
|
|
81
|
-
background: var(--crtr-color-surface-hover);
|
|
106
|
+
color: var(--crtr-text-button-hover-text-color, var(--crtr-color-text));
|
|
107
|
+
background: var(--crtr-text-button-hover-fill, var(--crtr-color-surface-hover));
|
|
82
108
|
}
|
|
83
109
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
${focusRingStyle({ scope: 'text-whole-comment' }, '[part~="whole-comment"]')}
|
|
111
|
+
${focusRingStyle({ scope: 'text-edit-toggle' }, '[part="edit-toggle"]')}
|
|
112
|
+
${focusRingStyle({ scope: 'text-selection-comment' }, '[part~="selection-comment"]')}
|
|
113
|
+
${focusRingStyle({ scope: 'text-comment-chip' }, '[part~="comment-chip"]')}
|
|
114
|
+
${focusRingStyle({ scope: 'text-popover-save' }, '[part="popover-save"]')}
|
|
115
|
+
${focusRingStyle({ scope: 'text-popover-cancel' }, '[part="popover-cancel"]')}
|
|
116
|
+
button[data-state="on"] {
|
|
117
|
+
color: var(--crtr-text-button-on-text-color, var(--crtr-color-accent));
|
|
87
118
|
}
|
|
88
119
|
|
|
89
|
-
|
|
90
|
-
|
|
120
|
+
/* Editing a prefilled draft is a primary path, not an icon to discover. Keep the control
|
|
121
|
+
beside the text but give it the bordered, labelled shape that reads as an action. */
|
|
122
|
+
[part="edit-toggle"] {
|
|
123
|
+
color: var(--crtr-text-edit-toggle-text-color, var(--crtr-color-text));
|
|
124
|
+
background: var(--crtr-text-edit-toggle-fill, var(--crtr-color-background));
|
|
125
|
+
border-color: var(--crtr-text-edit-toggle-border-color, var(--crtr-color-border));
|
|
91
126
|
}
|
|
92
127
|
|
|
128
|
+
/* Reached for and engaged are one treatment here, so they are one knob: active. */
|
|
129
|
+
[part="edit-toggle"]:hover,
|
|
130
|
+
[part="edit-toggle"][data-state="on"] {
|
|
131
|
+
color: var(--crtr-text-edit-toggle-active-text-color, var(--crtr-color-accent));
|
|
132
|
+
border-color: var(--crtr-text-edit-toggle-active-border-color,
|
|
133
|
+
color-mix(in srgb, var(--crtr-color-accent) 45%, var(--crtr-color-border)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Width and height are one visual knob — the icon is square. */
|
|
93
137
|
svg {
|
|
94
|
-
width: 0.
|
|
95
|
-
height: 0.
|
|
138
|
+
width: var(--crtr-text-icon-size, 0.875em);
|
|
139
|
+
height: var(--crtr-text-icon-size, 0.875em);
|
|
96
140
|
flex: none;
|
|
97
141
|
}
|
|
98
142
|
|
|
143
|
+
/* Both writing boxes in one call, because in the kit they are one control at two heights:
|
|
144
|
+
input.tsx and textarea.tsx carry the same border, radius, padding, fill, elevation and
|
|
145
|
+
focus ring, and differ only in how tall they start. Writing them separately is what let
|
|
146
|
+
them drift into two visibly different fields — different padding, different radius,
|
|
147
|
+
different fill, and an inset outline on one against the kit's outset ring on the other.
|
|
148
|
+
|
|
149
|
+
Each selector still gets its own scope: the field and editor are distinct anatomy even
|
|
150
|
+
though this recipe keeps their default paint identical. */
|
|
151
|
+
${writingBoxStyle({ scope: 'text-input' }, '[part="input"]')}
|
|
152
|
+
${writingBoxStyle({ scope: 'text-editor' }, '[part="editor"]')}
|
|
153
|
+
/* The one thing the textarea does not share: it starts tall and may be dragged taller. */
|
|
99
154
|
[part="editor"] {
|
|
100
|
-
|
|
101
|
-
box-sizing: border-box;
|
|
102
|
-
width: 100%;
|
|
103
|
-
min-height: 10rem;
|
|
104
|
-
padding: var(--crtr-space-3) var(--crtr-space-4);
|
|
105
|
-
color: var(--crtr-color-text);
|
|
106
|
-
font: inherit;
|
|
107
|
-
line-height: var(--crtr-line-height);
|
|
108
|
-
background: var(--crtr-color-background);
|
|
109
|
-
border: 1px solid var(--crtr-color-border);
|
|
110
|
-
border-radius: var(--crtr-radius-medium);
|
|
155
|
+
min-height: var(--crtr-text-editor-min-height, 10em);
|
|
111
156
|
resize: vertical;
|
|
112
157
|
}
|
|
113
158
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
159
|
+
/* The kit ring is outset paint, so a writing box needs a three-pixel gutter inside its grid
|
|
160
|
+
track. Without it the box fills the track exactly and the ring is shaved off by a tight
|
|
161
|
+
host frame. Prose is the other face of this surface and keeps its own inset ring, so it
|
|
162
|
+
stays full-bleed. */
|
|
163
|
+
[part="surface"]:has([part="input"]),
|
|
164
|
+
[part="surface"]:has([part="editor"]) {
|
|
165
|
+
padding: var(--crtr-text-surface-padding, 0.1875rem);
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
[part="body"] {
|
|
121
169
|
position: relative;
|
|
122
|
-
padding: var(--crtr-space-3) var(--crtr-space-4);
|
|
123
|
-
background: var(--crtr-color-surface);
|
|
124
|
-
border:
|
|
125
|
-
|
|
170
|
+
padding: var(--crtr-text-body-padding, var(--crtr-space-3) var(--crtr-space-4));
|
|
171
|
+
background: var(--crtr-text-body-fill, var(--crtr-color-surface));
|
|
172
|
+
border: var(--crtr-text-body-border-width, 1px)
|
|
173
|
+
var(--crtr-text-body-border-style, solid)
|
|
174
|
+
var(--crtr-text-body-border-color, var(--crtr-color-border));
|
|
175
|
+
border-radius: var(--crtr-text-body-radius, var(--crtr-radius-medium));
|
|
126
176
|
}
|
|
127
177
|
|
|
178
|
+
/* The ring sits inside the border rather than outside it. The body is full-bleed — it runs
|
|
179
|
+
the whole width of the frame the host gives the page — so an outset ring has nowhere to be
|
|
180
|
+
drawn and gets clipped at the frame's edges, showing three sides of a rectangle. */
|
|
128
181
|
[part="body"]:focus-visible {
|
|
129
|
-
outline:
|
|
130
|
-
|
|
182
|
+
outline: var(--crtr-text-body-focus-ring-width, 2px)
|
|
183
|
+
var(--crtr-text-body-focus-ring-style, solid)
|
|
184
|
+
var(--crtr-text-body-focus-ring-color, var(--crtr-color-focus));
|
|
185
|
+
outline-offset: var(--crtr-text-body-focus-ring-offset, -2px);
|
|
131
186
|
}
|
|
132
187
|
|
|
133
188
|
[part="prose"] {
|
|
134
189
|
display: grid;
|
|
135
|
-
gap: var(--crtr-space-3);
|
|
190
|
+
gap: var(--crtr-text-prose-gap, var(--crtr-space-3));
|
|
136
191
|
}
|
|
137
192
|
|
|
138
193
|
[part="prose"] p {
|
|
139
|
-
margin: 0;
|
|
194
|
+
margin: var(--crtr-text-prose-paragraph-margin, 0);
|
|
195
|
+
/* The agent's own line breaks are the passage; preserving them is behavior. */
|
|
140
196
|
white-space: pre-wrap;
|
|
141
197
|
}
|
|
142
198
|
|
|
143
199
|
[part="placeholder"] {
|
|
144
|
-
color: var(--crtr-color-text-muted);
|
|
200
|
+
color: var(--crtr-text-placeholder-text-color, var(--crtr-color-text-muted));
|
|
145
201
|
}
|
|
146
202
|
|
|
147
203
|
[part~="commented"] {
|
|
148
204
|
position: relative;
|
|
149
|
-
border-radius: var(--crtr-radius-small);
|
|
150
|
-
background: color-mix(in srgb, var(--crtr-color-accent) 18%, transparent);
|
|
151
|
-
text-decoration: underline dotted;
|
|
152
|
-
text-decoration-color: var(--crtr-color-accent);
|
|
153
|
-
text-underline-offset:
|
|
205
|
+
border-radius: var(--crtr-text-commented-radius, var(--crtr-radius-small));
|
|
206
|
+
background: var(--crtr-text-commented-fill, color-mix(in srgb, var(--crtr-color-accent) 18%, transparent));
|
|
207
|
+
text-decoration: var(--crtr-text-commented-text-decoration, underline dotted);
|
|
208
|
+
text-decoration-color: var(--crtr-text-commented-text-decoration-color, var(--crtr-color-accent));
|
|
209
|
+
text-underline-offset: var(--crtr-text-commented-text-underline-offset, 0.25em);
|
|
154
210
|
cursor: help;
|
|
155
|
-
outline: none;
|
|
211
|
+
outline: var(--crtr-text-commented-outline, none);
|
|
156
212
|
}
|
|
157
213
|
|
|
158
214
|
[part~="commented"]:focus-visible {
|
|
159
|
-
outline:
|
|
160
|
-
|
|
215
|
+
outline: var(--crtr-text-commented-focus-ring-width, 2px)
|
|
216
|
+
var(--crtr-text-commented-focus-ring-style, solid)
|
|
217
|
+
var(--crtr-text-commented-focus-ring-color, var(--crtr-color-focus));
|
|
218
|
+
outline-offset: var(--crtr-text-commented-focus-ring-offset, 2px);
|
|
161
219
|
}
|
|
162
220
|
|
|
221
|
+
/* The staged delays here and on the hover rule below are the bubble's show/hide
|
|
222
|
+
choreography — behavior rather than a styling value, so they stay literal. */
|
|
163
223
|
[part="comment-bubble"] {
|
|
164
224
|
position: absolute;
|
|
165
|
-
bottom: calc(100% + var(--crtr-space-1));
|
|
225
|
+
bottom: calc(100% + var(--crtr-text-comment-bubble-offset, var(--crtr-space-1)));
|
|
166
226
|
left: 0;
|
|
167
227
|
z-index: 2;
|
|
168
228
|
display: grid;
|
|
169
|
-
gap: var(--crtr-space-1);
|
|
170
|
-
width: max-content;
|
|
171
|
-
max-width:
|
|
172
|
-
padding: var(--crtr-space-2) var(--crtr-space-3);
|
|
173
|
-
color: var(--crtr-color-text);
|
|
174
|
-
background: var(--crtr-color-background);
|
|
175
|
-
border:
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
gap: var(--crtr-text-comment-bubble-gap, var(--crtr-space-1));
|
|
230
|
+
width: var(--crtr-text-comment-bubble-width, max-content);
|
|
231
|
+
max-width: var(--crtr-text-comment-bubble-max-width, 18em);
|
|
232
|
+
padding: var(--crtr-text-comment-bubble-padding, var(--crtr-space-2) var(--crtr-space-3));
|
|
233
|
+
color: var(--crtr-text-comment-bubble-text-color, var(--crtr-color-text));
|
|
234
|
+
background: var(--crtr-text-comment-bubble-fill, var(--crtr-color-background));
|
|
235
|
+
border: var(--crtr-text-comment-bubble-border-width, 1px)
|
|
236
|
+
var(--crtr-text-comment-bubble-border-style, solid)
|
|
237
|
+
var(--crtr-text-comment-bubble-border-color, var(--crtr-color-border));
|
|
238
|
+
border-radius: var(--crtr-text-comment-bubble-radius, var(--crtr-radius-medium));
|
|
239
|
+
box-shadow: var(--crtr-text-comment-bubble-shadow, var(--crtr-shadow));
|
|
178
240
|
text-decoration: none;
|
|
179
241
|
white-space: normal;
|
|
180
242
|
user-select: none;
|
|
@@ -190,45 +252,46 @@ const STYLE = `
|
|
|
190
252
|
transition: opacity 120ms ease 220ms, visibility 0s linear 220ms;
|
|
191
253
|
}
|
|
192
254
|
|
|
255
|
+
/* The comment layer's micro-labels are the page's own prose, one step down and muted — not
|
|
256
|
+
caps-mono. Mono set in capitals reads as machine output, and what these say ("Comment on
|
|
257
|
+
this passage", "2 comments") is the interface talking to a person. */
|
|
193
258
|
[part="bubble-label"],
|
|
194
259
|
[part="comment-count"],
|
|
195
260
|
[part="popover-subject"],
|
|
196
261
|
[part="popover-hint"] {
|
|
197
|
-
color: var(--crtr-color-text-muted);
|
|
198
|
-
font-
|
|
199
|
-
font-size: 0.6875rem;
|
|
200
|
-
letter-spacing: 0.06em;
|
|
201
|
-
text-transform: uppercase;
|
|
262
|
+
color: var(--crtr-text-comment-label-text-color, var(--crtr-color-text-muted));
|
|
263
|
+
font-size: var(--crtr-text-comment-label-font-size, 0.875em);
|
|
202
264
|
}
|
|
203
265
|
|
|
204
266
|
[part="bubble-label"] {
|
|
205
|
-
color: var(--crtr-color-accent);
|
|
267
|
+
color: var(--crtr-text-bubble-label-text-color, var(--crtr-color-accent));
|
|
206
268
|
}
|
|
207
269
|
|
|
208
270
|
[part~="selection-comment"] {
|
|
209
271
|
position: absolute;
|
|
210
272
|
z-index: 3;
|
|
211
|
-
color: var(--crtr-color-accent);
|
|
212
|
-
background: var(--crtr-color-background);
|
|
213
|
-
border-color:
|
|
214
|
-
|
|
273
|
+
color: var(--crtr-text-selection-comment-text-color, var(--crtr-color-accent));
|
|
274
|
+
background: var(--crtr-text-selection-comment-fill, var(--crtr-color-background));
|
|
275
|
+
border-color: var(--crtr-text-selection-comment-border-color,
|
|
276
|
+
color-mix(in srgb, var(--crtr-color-accent) 40%, transparent));
|
|
277
|
+
box-shadow: var(--crtr-text-selection-comment-shadow, var(--crtr-shadow));
|
|
215
278
|
}
|
|
216
279
|
|
|
217
280
|
[part="comment-rail"] {
|
|
218
281
|
display: grid;
|
|
219
|
-
gap: var(--crtr-space-1);
|
|
282
|
+
gap: var(--crtr-text-comment-rail-gap, var(--crtr-space-1));
|
|
220
283
|
}
|
|
221
284
|
|
|
222
285
|
[part="comment-chips"] {
|
|
223
286
|
display: flex;
|
|
224
287
|
flex-wrap: wrap;
|
|
225
|
-
gap: var(--crtr-space-2);
|
|
288
|
+
gap: var(--crtr-text-comment-chips-gap, var(--crtr-space-2));
|
|
226
289
|
}
|
|
227
290
|
|
|
228
291
|
[part~="comment-chip"] {
|
|
229
|
-
color: var(--crtr-color-text);
|
|
230
|
-
border-color: var(--crtr-color-border);
|
|
231
|
-
text-align: left;
|
|
292
|
+
color: var(--crtr-text-comment-chip-text-color, var(--crtr-color-text));
|
|
293
|
+
border-color: var(--crtr-text-comment-chip-border-color, var(--crtr-color-border));
|
|
294
|
+
text-align: var(--crtr-text-comment-chip-text-align, left);
|
|
232
295
|
}
|
|
233
296
|
|
|
234
297
|
[part~="comment-chip"] > span {
|
|
@@ -242,17 +305,21 @@ const STYLE = `
|
|
|
242
305
|
z-index: 5;
|
|
243
306
|
box-sizing: border-box;
|
|
244
307
|
display: grid;
|
|
245
|
-
gap: var(--crtr-space-2);
|
|
246
|
-
width:
|
|
247
|
-
max-width: 100
|
|
248
|
-
padding: var(--crtr-space-3);
|
|
249
|
-
background: var(--crtr-color-background);
|
|
250
|
-
border:
|
|
251
|
-
|
|
252
|
-
|
|
308
|
+
gap: var(--crtr-text-popover-gap, var(--crtr-space-2));
|
|
309
|
+
width: var(--crtr-text-popover-width, 20em);
|
|
310
|
+
max-width: var(--crtr-text-popover-max-width, 100%);
|
|
311
|
+
padding: var(--crtr-text-popover-padding, var(--crtr-space-3));
|
|
312
|
+
background: var(--crtr-text-popover-fill, var(--crtr-color-background));
|
|
313
|
+
border: var(--crtr-text-popover-border-width, 1px)
|
|
314
|
+
var(--crtr-text-popover-border-style, solid)
|
|
315
|
+
var(--crtr-text-popover-border-color, var(--crtr-color-border));
|
|
316
|
+
border-radius: var(--crtr-text-popover-radius, var(--crtr-radius-medium));
|
|
317
|
+
box-shadow: var(--crtr-text-popover-shadow, var(--crtr-shadow));
|
|
253
318
|
}
|
|
254
319
|
|
|
255
320
|
[part="popover"][hidden],
|
|
321
|
+
[part="toolbar"][hidden],
|
|
322
|
+
[part~="whole-comment"][hidden],
|
|
256
323
|
[part="edited"][hidden],
|
|
257
324
|
[part="comment-rail"][hidden],
|
|
258
325
|
[part~="selection-comment"][hidden],
|
|
@@ -261,29 +328,30 @@ const STYLE = `
|
|
|
261
328
|
display: none;
|
|
262
329
|
}
|
|
263
330
|
|
|
264
|
-
[part="popover-subject"]
|
|
265
|
-
|
|
266
|
-
margin: 0;
|
|
331
|
+
[part="popover-subject"] {
|
|
332
|
+
margin: var(--crtr-text-popover-subject-margin, 0);
|
|
267
333
|
}
|
|
268
334
|
|
|
335
|
+
/* Smaller than the comment being written, because it is the context and not the subject.
|
|
336
|
+
The text arrives already elided to about two lines by quotePreview, so it wraps normally
|
|
337
|
+
rather than keeping the source's line breaks. */
|
|
269
338
|
[part="popover-quote"] {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
339
|
+
margin: var(--crtr-text-popover-quote-margin, 0);
|
|
340
|
+
padding: var(--crtr-text-popover-quote-padding, var(--crtr-space-1) var(--crtr-space-2));
|
|
341
|
+
color: var(--crtr-text-popover-quote-text-color, var(--crtr-color-text-muted));
|
|
342
|
+
font-size: var(--crtr-text-popover-quote-font-size, 0.8125em);
|
|
343
|
+
background: var(--crtr-text-popover-quote-fill, color-mix(in srgb, var(--crtr-color-accent) 8%, transparent));
|
|
344
|
+
border-left: var(--crtr-text-popover-quote-border-width, 2px)
|
|
345
|
+
var(--crtr-text-popover-quote-border-style, solid)
|
|
346
|
+
var(--crtr-text-popover-quote-border-color, color-mix(in srgb, var(--crtr-color-accent) 40%, transparent));
|
|
347
|
+
overflow-wrap: anywhere;
|
|
275
348
|
}
|
|
276
349
|
|
|
350
|
+
/* The composer is its own scope: it is the one writing box a person can meet beside the
|
|
351
|
+
passage's own, so the two are separately restylable. */
|
|
352
|
+
${writingBoxStyle({ scope: 'text-popover-input' }, '[part="popover-input"]')}
|
|
277
353
|
[part="popover-input"] {
|
|
278
|
-
|
|
279
|
-
width: 100%;
|
|
280
|
-
min-height: 5rem;
|
|
281
|
-
padding: var(--crtr-space-2);
|
|
282
|
-
color: var(--crtr-color-text);
|
|
283
|
-
font: inherit;
|
|
284
|
-
background: var(--crtr-color-surface);
|
|
285
|
-
border: 1px solid var(--crtr-color-border);
|
|
286
|
-
border-radius: var(--crtr-radius-small);
|
|
354
|
+
min-height: var(--crtr-text-popover-input-min-height, 5em);
|
|
287
355
|
resize: vertical;
|
|
288
356
|
}
|
|
289
357
|
|
|
@@ -291,41 +359,47 @@ const STYLE = `
|
|
|
291
359
|
display: flex;
|
|
292
360
|
align-items: center;
|
|
293
361
|
justify-content: space-between;
|
|
294
|
-
gap: var(--crtr-space-2);
|
|
362
|
+
gap: var(--crtr-text-popover-actions-gap, var(--crtr-space-2));
|
|
295
363
|
}
|
|
296
364
|
|
|
297
365
|
[part="popover-buttons"] {
|
|
298
366
|
display: flex;
|
|
299
|
-
gap: var(--crtr-space-2);
|
|
367
|
+
gap: var(--crtr-text-popover-buttons-gap, var(--crtr-space-2));
|
|
300
368
|
}
|
|
301
369
|
|
|
302
370
|
[part="popover-save"] {
|
|
303
|
-
color: var(--crtr-color-accent-contrast);
|
|
304
|
-
background: var(--crtr-color-accent);
|
|
371
|
+
color: var(--crtr-text-popover-save-text-color, var(--crtr-color-accent-contrast));
|
|
372
|
+
background: var(--crtr-text-popover-save-fill, var(--crtr-color-accent));
|
|
305
373
|
}
|
|
306
374
|
|
|
375
|
+
/* Mixing the accent toward the text colour darkens on a light token set and lightens on a
|
|
376
|
+
dark one — an opacity or brightness step moves the wrong way on one of them. */
|
|
307
377
|
[part="popover-save"]:hover {
|
|
308
|
-
color: var(--crtr-color-accent-contrast);
|
|
309
|
-
background: var(--crtr-
|
|
310
|
-
|
|
378
|
+
color: var(--crtr-text-popover-save-hover-text-color, var(--crtr-color-accent-contrast));
|
|
379
|
+
background: var(--crtr-text-popover-save-hover-fill,
|
|
380
|
+
color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
|
|
311
381
|
}
|
|
312
382
|
|
|
383
|
+
/* Cancel is the quiet half of every editor in this bundle: no border until you reach for it. */
|
|
313
384
|
[part="popover-cancel"] {
|
|
314
|
-
border-color: var(--crtr-
|
|
385
|
+
border-color: var(--crtr-text-popover-cancel-border-color, transparent);
|
|
315
386
|
}
|
|
316
387
|
|
|
388
|
+
/* Flat: this sits in the flow of the page. A shadow in this bundle means floating — the
|
|
389
|
+
comment bubble, the selection button, the popover, and nothing else. */
|
|
317
390
|
[part="unavailable"] {
|
|
318
|
-
padding: var(--crtr-space-3);
|
|
319
|
-
color: var(--crtr-color-text-muted);
|
|
320
|
-
background: var(--crtr-color-surface);
|
|
321
|
-
border:
|
|
322
|
-
|
|
323
|
-
|
|
391
|
+
padding: var(--crtr-text-unavailable-padding, var(--crtr-space-3));
|
|
392
|
+
color: var(--crtr-text-unavailable-text-color, var(--crtr-color-text-muted));
|
|
393
|
+
background: var(--crtr-text-unavailable-fill, var(--crtr-color-surface));
|
|
394
|
+
border: var(--crtr-text-unavailable-border-width, 1px)
|
|
395
|
+
var(--crtr-text-unavailable-border-style, solid)
|
|
396
|
+
var(--crtr-text-unavailable-border-color, var(--crtr-color-border));
|
|
397
|
+
border-radius: var(--crtr-text-unavailable-radius, var(--crtr-radius-medium));
|
|
324
398
|
}
|
|
325
399
|
|
|
326
400
|
[part="reason"] {
|
|
327
|
-
color: var(--crtr-color-text);
|
|
328
|
-
font-family: var(--crtr-font-mono);
|
|
401
|
+
color: var(--crtr-text-reason-text-color, var(--crtr-color-text));
|
|
402
|
+
font-family: var(--crtr-text-reason-font-family, var(--crtr-font-mono));
|
|
329
403
|
}
|
|
330
404
|
`;
|
|
331
405
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
@@ -479,8 +553,11 @@ function markTrigger(element) {
|
|
|
479
553
|
element.setAttribute('aria-expanded', 'false');
|
|
480
554
|
}
|
|
481
555
|
class CrtrTextElement extends HTMLElement {
|
|
556
|
+
/** The host marks a resolved or canceled page `inert`; that is this element's read-only signal. */
|
|
557
|
+
static observedAttributes = ['inert'];
|
|
482
558
|
#root;
|
|
483
559
|
#container = document.createElement('div');
|
|
560
|
+
#label = document.createElement('p');
|
|
484
561
|
#toolbar = document.createElement('div');
|
|
485
562
|
#editedBadge = document.createElement('span');
|
|
486
563
|
#wholeComment = document.createElement('button');
|
|
@@ -488,6 +565,7 @@ class CrtrTextElement extends HTMLElement {
|
|
|
488
565
|
#editLabel = document.createElement('span');
|
|
489
566
|
#surface = document.createElement('div');
|
|
490
567
|
#editor = document.createElement('textarea');
|
|
568
|
+
#input = document.createElement('input');
|
|
491
569
|
#body = document.createElement('div');
|
|
492
570
|
#prose = document.createElement('div');
|
|
493
571
|
#selectionComment = document.createElement('button');
|
|
@@ -503,18 +581,28 @@ class CrtrTextElement extends HTMLElement {
|
|
|
503
581
|
#popoverCancel = document.createElement('button');
|
|
504
582
|
#config = { initialText: '' };
|
|
505
583
|
#slotId;
|
|
584
|
+
#singleLine = false;
|
|
506
585
|
#text = '';
|
|
507
586
|
#comments = [];
|
|
508
587
|
#editing = false;
|
|
509
588
|
#live = false;
|
|
510
589
|
#draft = null;
|
|
511
590
|
#open = null;
|
|
591
|
+
/** Live only while the popover is open: its composer resizes, and the room reserved for it must follow. */
|
|
592
|
+
#popoverResize = new ResizeObserver(() => {
|
|
593
|
+
const open = this.#open;
|
|
594
|
+
if (open !== null)
|
|
595
|
+
this.#positionPopover(open.trigger, open.align);
|
|
596
|
+
});
|
|
512
597
|
constructor() {
|
|
513
598
|
super();
|
|
514
599
|
this.#root = this.attachShadow({ mode: 'open' });
|
|
515
600
|
const style = document.createElement('style');
|
|
516
601
|
style.textContent = STYLE;
|
|
517
602
|
this.#container.part.add('container');
|
|
603
|
+
this.#label.part.add('label');
|
|
604
|
+
this.#label.id = LABEL_ID;
|
|
605
|
+
this.#label.hidden = true;
|
|
518
606
|
this.#toolbar.part.add('toolbar');
|
|
519
607
|
this.#editedBadge.part.add('edited');
|
|
520
608
|
this.#editedBadge.textContent = 'edited';
|
|
@@ -548,6 +636,19 @@ class CrtrTextElement extends HTMLElement {
|
|
|
548
636
|
this.#renderToolbar();
|
|
549
637
|
this.#emit();
|
|
550
638
|
});
|
|
639
|
+
this.#input.type = 'text';
|
|
640
|
+
this.#input.part.add('input');
|
|
641
|
+
this.#input.addEventListener('input', () => {
|
|
642
|
+
this.#text = this.#input.value;
|
|
643
|
+
this.#renderToolbar();
|
|
644
|
+
this.#emit();
|
|
645
|
+
});
|
|
646
|
+
// A form field's Enter types nothing and submits nothing here: the page's own submit is the
|
|
647
|
+
// pager's modified chord, which this leaves alone.
|
|
648
|
+
this.#input.addEventListener('keydown', (event) => {
|
|
649
|
+
if (event.key === 'Enter' && !event.metaKey && !event.ctrlKey && !event.altKey)
|
|
650
|
+
event.preventDefault();
|
|
651
|
+
});
|
|
551
652
|
this.#body.part.add('body');
|
|
552
653
|
this.#body.tabIndex = 0;
|
|
553
654
|
this.#prose.part.add('prose');
|
|
@@ -602,7 +703,7 @@ class CrtrTextElement extends HTMLElement {
|
|
|
602
703
|
buttons.append(this.#popoverCancel, this.#popoverSave);
|
|
603
704
|
actions.append(this.#popoverHint, buttons);
|
|
604
705
|
this.#popover.append(this.#popoverSubject, this.#popoverQuote, this.#popoverInput, actions);
|
|
605
|
-
this.#container.append(this.#toolbar, this.#surface, this.#rail, this.#popover);
|
|
706
|
+
this.#container.append(this.#label, this.#toolbar, this.#surface, this.#rail, this.#popover);
|
|
606
707
|
this.#root.append(style, this.#container);
|
|
607
708
|
this.addEventListener('keydown', this.#onKeyDown);
|
|
608
709
|
}
|
|
@@ -620,16 +721,28 @@ class CrtrTextElement extends HTMLElement {
|
|
|
620
721
|
}
|
|
621
722
|
this.#live = true;
|
|
622
723
|
this.#config = read.config;
|
|
724
|
+
this.#singleLine = read.config.singleLine === true;
|
|
725
|
+
this.#surface.toggleAttribute('data-single-line', this.#singleLine);
|
|
623
726
|
this.#slotId = readSlotId(this);
|
|
624
727
|
this.#text = read.config.initialText;
|
|
728
|
+
this.#renderLabel();
|
|
625
729
|
this.#hydrate();
|
|
626
|
-
|
|
730
|
+
// A text slot the person never touches still has an answer — the words on screen — so the
|
|
731
|
+
// pager can report it at submit even though no change ever called `respond()`.
|
|
732
|
+
if (this.#slotId !== undefined)
|
|
733
|
+
registerSlotResponse(this, this.#slotId, () => this.#response());
|
|
734
|
+
this.#editing = this.#editable && this.#text.length === 0;
|
|
627
735
|
this.#renderToolbar();
|
|
628
736
|
this.#renderSurface();
|
|
629
737
|
this.#renderRail();
|
|
630
738
|
}
|
|
739
|
+
attributeChangedCallback() {
|
|
740
|
+
if (this.isConnected)
|
|
741
|
+
this.#syncReadOnly();
|
|
742
|
+
}
|
|
631
743
|
disconnectedCallback() {
|
|
632
744
|
document.removeEventListener('pointerdown', this.#onDocumentPointerDown, true);
|
|
745
|
+
this.#popoverResize.disconnect();
|
|
633
746
|
}
|
|
634
747
|
/**
|
|
635
748
|
* Seed local state from the answer the host is already holding, so a reopened page shows
|
|
@@ -643,7 +756,7 @@ class CrtrTextElement extends HTMLElement {
|
|
|
643
756
|
const saved = savedResponse(this.#slotId);
|
|
644
757
|
if (saved === undefined)
|
|
645
758
|
return;
|
|
646
|
-
if (this.#
|
|
759
|
+
if ((this.#editable || this.#singleLine) && typeof saved.text === 'string')
|
|
647
760
|
this.#text = saved.text;
|
|
648
761
|
this.#comments = adoptComments(saved.comments);
|
|
649
762
|
}
|
|
@@ -667,18 +780,80 @@ class CrtrTextElement extends HTMLElement {
|
|
|
667
780
|
box.append('Text unavailable: ', detail);
|
|
668
781
|
this.#container.replaceChildren(box);
|
|
669
782
|
}
|
|
783
|
+
/**
|
|
784
|
+
* The prompt names both faces of the surface, because the writing box and the prose body
|
|
785
|
+
* swap places under it and either one can be what the user is on.
|
|
786
|
+
*/
|
|
787
|
+
#renderLabel() {
|
|
788
|
+
const label = this.#config.label;
|
|
789
|
+
const shown = label !== undefined && label !== '';
|
|
790
|
+
this.#label.hidden = !shown;
|
|
791
|
+
this.#label.textContent = shown ? label : '';
|
|
792
|
+
for (const surface of [this.#editor, this.#input, this.#body]) {
|
|
793
|
+
if (shown)
|
|
794
|
+
surface.setAttribute('aria-labelledby', LABEL_ID);
|
|
795
|
+
else
|
|
796
|
+
surface.removeAttribute('aria-labelledby');
|
|
797
|
+
}
|
|
798
|
+
}
|
|
670
799
|
#renderToolbar() {
|
|
671
|
-
|
|
800
|
+
// A field the agent left empty has nothing to differ FROM, so the badge would fire on the
|
|
801
|
+
// first keystroke and say nothing; it earns its place only over a prefill.
|
|
802
|
+
this.#editedBadge.hidden = this.#text === this.#config.initialText || (this.#singleLine && this.#config.initialText === '');
|
|
672
803
|
const whole = findComment(this.#comments, WHOLE);
|
|
673
804
|
this.#wholeComment.replaceChildren(icon(whole ? ICON_COMMENT : ICON_COMMENT_ADD));
|
|
674
805
|
this.#wholeComment.dataset.state = whole ? 'on' : 'off';
|
|
675
806
|
this.#wholeComment.setAttribute('aria-label', `${whole ? 'Edit comment on' : 'Comment on'} ${WHOLE_TARGET}`);
|
|
676
|
-
|
|
807
|
+
// No comment affordance where a comment has nowhere to go — see `canRecordComments`.
|
|
808
|
+
this.#wholeComment.hidden = this.#singleLine || !this.#commentable;
|
|
809
|
+
this.#editToggle.hidden = !this.#editable;
|
|
810
|
+
// In single-line mode the toolbar carries the badge alone, so it goes with it.
|
|
811
|
+
this.#toolbar.hidden = this.#singleLine && this.#editedBadge.hidden;
|
|
677
812
|
this.#editToggle.dataset.state = this.#editing ? 'on' : 'off';
|
|
678
|
-
this.#editLabel.textContent = this.#editing ? 'Done' : 'Edit';
|
|
813
|
+
this.#editLabel.textContent = this.#editing ? 'Done editing' : 'Edit text';
|
|
679
814
|
this.#editToggle.replaceChildren(icon(this.#editing ? ICON_DONE : ICON_EDIT), this.#editLabel);
|
|
815
|
+
this.#syncReadOnly();
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Repainted with the toolbar, which is repainted on every state change. The writing box
|
|
819
|
+
* goes read-only rather than disabled, so a saved answer stays selectable and copyable;
|
|
820
|
+
* the edit toggle goes disabled, because a page that can no longer take an answer has
|
|
821
|
+
* nothing to toggle into.
|
|
822
|
+
*/
|
|
823
|
+
#syncReadOnly() {
|
|
824
|
+
const readOnly = isReadOnly(this);
|
|
825
|
+
applyReadOnly(this.#root, readOnly);
|
|
826
|
+
// The editor lives outside the tree whenever the surface is showing prose, so the
|
|
827
|
+
// sweep over the shadow root never reaches it. The compact field is a writing box too:
|
|
828
|
+
// the sweep disables every input it finds, and this puts it back as read-only, which
|
|
829
|
+
// keeps a saved one-line answer selectable and copyable.
|
|
830
|
+
this.#editor.readOnly = readOnly;
|
|
831
|
+
this.#input.disabled = false;
|
|
832
|
+
this.#input.readOnly = readOnly;
|
|
833
|
+
this.#editToggle.disabled = readOnly;
|
|
834
|
+
// A page can go read-only while the popover is open — the host sets `inert` on a ticket
|
|
835
|
+
// that resolves under it. A box that can no longer save is closed rather than left there
|
|
836
|
+
// taking words nothing will keep.
|
|
837
|
+
if (readOnly && this.#open !== null)
|
|
838
|
+
this.#closeComment(false);
|
|
839
|
+
}
|
|
840
|
+
/** Multi-line text is a writing surface unless the author explicitly makes it review-only. */
|
|
841
|
+
get #editable() {
|
|
842
|
+
return !this.#singleLine && this.#config.editable !== false;
|
|
843
|
+
}
|
|
844
|
+
/** Whether this slot can keep a comment — see `canRecordComments`. */
|
|
845
|
+
get #commentable() {
|
|
846
|
+
return canRecordComments(this.#slotId);
|
|
680
847
|
}
|
|
681
848
|
#renderSurface() {
|
|
849
|
+
if (this.#singleLine) {
|
|
850
|
+
if (this.#input.value !== this.#text)
|
|
851
|
+
this.#input.value = this.#text;
|
|
852
|
+
this.#input.placeholder = this.#config.placeholder ?? '';
|
|
853
|
+
if (this.#surface.firstChild !== this.#input)
|
|
854
|
+
this.#surface.replaceChildren(this.#input);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
682
857
|
if (this.#editing) {
|
|
683
858
|
if (this.#editor.value !== this.#text)
|
|
684
859
|
this.#editor.value = this.#text;
|
|
@@ -795,7 +970,7 @@ class CrtrTextElement extends HTMLElement {
|
|
|
795
970
|
* starts where the user is looking.
|
|
796
971
|
*/
|
|
797
972
|
#captureSelection() {
|
|
798
|
-
if (this.#editing || this.#open !== null)
|
|
973
|
+
if (this.#editing || this.#open !== null || !this.#commentable)
|
|
799
974
|
return;
|
|
800
975
|
const draft = this.#readSelection();
|
|
801
976
|
if (draft === null) {
|
|
@@ -813,6 +988,11 @@ class CrtrTextElement extends HTMLElement {
|
|
|
813
988
|
}
|
|
814
989
|
// — the comment popover ————————————————————————————————————————————————————
|
|
815
990
|
#openComment(anchor, target, trigger, align, quote) {
|
|
991
|
+
// The popover opens only where its comment can land: never on a slot that cannot record
|
|
992
|
+
// one, and never on a read-only page, whose textareas are already `readOnly`, so it would
|
|
993
|
+
// take focus and refuse every keystroke.
|
|
994
|
+
if (!this.#commentable || isReadOnly(this))
|
|
995
|
+
return;
|
|
816
996
|
// Invoking the trigger of the open popover closes it, and moving to another trigger
|
|
817
997
|
// hands the open state over — the popover is one dialog, and exactly one control at a
|
|
818
998
|
// time reports itself as expanded.
|
|
@@ -825,34 +1005,55 @@ class CrtrTextElement extends HTMLElement {
|
|
|
825
1005
|
previous.trigger.setAttribute('aria-expanded', 'false');
|
|
826
1006
|
}
|
|
827
1007
|
const existing = findComment(this.#comments, anchor);
|
|
828
|
-
this.#open = { anchor, trigger };
|
|
1008
|
+
this.#open = { anchor, trigger, align };
|
|
829
1009
|
trigger.setAttribute('aria-expanded', 'true');
|
|
830
1010
|
this.#popoverSubject.textContent = `Comment on ${target}`;
|
|
831
1011
|
this.#popover.setAttribute('aria-label', `Comment on ${target}`);
|
|
832
|
-
|
|
833
|
-
this.#popoverQuote.
|
|
1012
|
+
const preview = quotePreview(quote ?? '');
|
|
1013
|
+
this.#popoverQuote.hidden = preview === '';
|
|
1014
|
+
this.#popoverQuote.textContent = preview;
|
|
834
1015
|
// The box is seeded from the saved comment every time it opens, so an abandoned edit
|
|
835
1016
|
// never leaks into the next one.
|
|
836
1017
|
this.#popoverInput.value = existing?.text ?? '';
|
|
837
1018
|
this.#popoverHint.textContent = existing ? 'empty to delete' : '\u2318\u21b5 to save';
|
|
838
1019
|
this.#popover.hidden = false;
|
|
839
1020
|
this.#positionPopover(trigger, align);
|
|
1021
|
+
// The composer is draggable-tall; observing the open popover keeps the reserved room in
|
|
1022
|
+
// step with it, so a resized box never pushes its own actions past the frame.
|
|
1023
|
+
this.#popoverResize.observe(this.#popover);
|
|
840
1024
|
this.#popoverInput.focus();
|
|
841
1025
|
document.addEventListener('pointerdown', this.#onDocumentPointerDown, true);
|
|
842
1026
|
}
|
|
843
1027
|
#positionPopover(trigger, align) {
|
|
1028
|
+
// Start from the page's natural height. A previous open may have reserved room for a
|
|
1029
|
+
// lower-edge composer, and measuring against that stale room would make it accumulate.
|
|
1030
|
+
this.#container.style.paddingBottom = '';
|
|
844
1031
|
const bounds = this.#container.getBoundingClientRect();
|
|
845
1032
|
const rect = trigger.getBoundingClientRect();
|
|
846
1033
|
const width = this.#popover.offsetWidth;
|
|
1034
|
+
const height = this.#popover.offsetHeight;
|
|
847
1035
|
const raw = align === 'end' ? rect.right - bounds.left - width : rect.left - bounds.left;
|
|
848
1036
|
const left = Math.min(Math.max(raw, 0), Math.max(0, bounds.width - width));
|
|
1037
|
+
const below = rect.bottom + POPOVER_GAP + height <= window.innerHeight;
|
|
1038
|
+
const above = rect.top - POPOVER_GAP - height >= 0;
|
|
1039
|
+
const top = !below && above
|
|
1040
|
+
? rect.top - bounds.top - POPOVER_GAP - height
|
|
1041
|
+
: rect.bottom - bounds.top + POPOVER_GAP;
|
|
849
1042
|
this.#popover.style.left = `${left}px`;
|
|
850
|
-
this.#popover.style.top = `${
|
|
1043
|
+
this.#popover.style.top = `${top}px`;
|
|
1044
|
+
// An inline Gateway frame reports the body's in-flow height. Absolute popovers do not
|
|
1045
|
+
// contribute to that measurement, so reserve only the missing tail when neither side can
|
|
1046
|
+
// hold the composer. The host then grows the iframe and the entire writing box stays live.
|
|
1047
|
+
const overflow = this.#popover.getBoundingClientRect().bottom - document.body.getBoundingClientRect().bottom;
|
|
1048
|
+
if (overflow > 0)
|
|
1049
|
+
this.#container.style.paddingBottom = `${overflow}px`;
|
|
851
1050
|
}
|
|
852
1051
|
#closeComment(restoreFocus = true) {
|
|
853
1052
|
const open = this.#open;
|
|
854
1053
|
this.#open = null;
|
|
855
1054
|
this.#popover.hidden = true;
|
|
1055
|
+
this.#popoverResize.disconnect();
|
|
1056
|
+
this.#container.style.paddingBottom = '';
|
|
856
1057
|
open?.trigger.setAttribute('aria-expanded', 'false');
|
|
857
1058
|
document.removeEventListener('pointerdown', this.#onDocumentPointerDown, true);
|
|
858
1059
|
const returnFocus = open !== null && open.trigger !== this.#selectionComment ? open.trigger : this.#body;
|
|
@@ -863,7 +1064,7 @@ class CrtrTextElement extends HTMLElement {
|
|
|
863
1064
|
/** Clearing the box and saving deletes the comment; `setComment` is where that lives. */
|
|
864
1065
|
#commitComment() {
|
|
865
1066
|
const open = this.#open;
|
|
866
|
-
if (open === null)
|
|
1067
|
+
if (open === null || isReadOnly(this))
|
|
867
1068
|
return;
|
|
868
1069
|
// Saving replaces every highlight and every chip, so a trigger living in either is
|
|
869
1070
|
// about to be detached: close without restoring focus to that doomed node, then hand
|
|
@@ -872,7 +1073,7 @@ class CrtrTextElement extends HTMLElement {
|
|
|
872
1073
|
this.#comments = setComment(this.#comments, open.anchor, this.#popoverInput.value);
|
|
873
1074
|
this.#closeComment(!anchored);
|
|
874
1075
|
this.#renderToolbar();
|
|
875
|
-
if (!this.#editing)
|
|
1076
|
+
if (!this.#editing && !this.#singleLine)
|
|
876
1077
|
this.#renderProse();
|
|
877
1078
|
this.#renderRail();
|
|
878
1079
|
if (anchored)
|
|
@@ -943,6 +1144,12 @@ class CrtrTextElement extends HTMLElement {
|
|
|
943
1144
|
* the whole passage when neither says otherwise.
|
|
944
1145
|
*/
|
|
945
1146
|
#commentFocusedAnchor() {
|
|
1147
|
+
// A one-line field has no passage to anchor against, and the shape offers no way to write
|
|
1148
|
+
// a comment; the chips a saved answer arrived with stay clickable, and that is the whole path.
|
|
1149
|
+
// The same key must not raise the selection button on a slot that cannot record what it
|
|
1150
|
+
// would take — this is the one comment path that shows an affordance before opening it.
|
|
1151
|
+
if (this.#singleLine || !this.#commentable)
|
|
1152
|
+
return;
|
|
946
1153
|
const draft = this.#draft ?? this.#readSelection();
|
|
947
1154
|
if (draft !== null) {
|
|
948
1155
|
this.#draft = draft;
|