@north-light/crouter 0.3.197 → 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 +81 -153
- 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/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
|
@@ -15,51 +15,106 @@
|
|
|
15
15
|
* Ported from the agent interaction kit's `agent-table.tsx` (northlight `3705fe9e2`), with the
|
|
16
16
|
* kit's `classic-list-keys` grammar — j/k move, x selects, c comments — reimplemented here
|
|
17
17
|
* over the shadow root.
|
|
18
|
+
*
|
|
19
|
+
* ## Styling
|
|
20
|
+
*
|
|
21
|
+
* Every styling value below is read through a `--crtr-table-*` component token, so a host
|
|
22
|
+
* restyles this element without touching crtr — custom properties inherit through the shadow
|
|
23
|
+
* boundary, which is the whole mechanism. The parts a person can name each own a scope:
|
|
24
|
+
* `container`, `caption`, `header`, `column-toggle`, `row`, `cell`, `selection` (the row
|
|
25
|
+
* control and the header's select-all, deliberately one thing), `comment-trigger`,
|
|
26
|
+
* `comment-editor`, `comment-input`, `comment-button` and its `comment-cancel` /
|
|
27
|
+
* `comment-save` paint, `notice`, `reason`, `empty`. Where a shared recipe draws the value the
|
|
28
|
+
* chain is `--crtr-table-<part>-<knob>` over the family's `--crtr-control-*` and then the kit
|
|
29
|
+
* default; every default stays inline at the rule that spends it, and no token is declared
|
|
30
|
+
* anywhere, which is what keeps the kit's own look when a host sets nothing.
|
|
31
|
+
*
|
|
32
|
+
* Layout and behaviour stay literal: `display`, positioning, the shrink-to-content column
|
|
33
|
+
* widths, text alignment, and the trigger's reveal-on-hover `opacity` are anatomy rather than
|
|
34
|
+
* paint, so they are not knobs.
|
|
18
35
|
*/
|
|
36
|
+
import { controlStyle, focusRingStyle, writingBoxStyle } from '../controls.js';
|
|
19
37
|
import { registerElement } from '../register.js';
|
|
20
38
|
import { readSlotConfig, readSlotId } from '../slot-config.js';
|
|
21
|
-
import {
|
|
22
|
-
import { adoptComments, anchorLabel, findComment, setComment } from '../comments.js';
|
|
39
|
+
import { respond, savedIds, savedResponse } from '../host.js';
|
|
40
|
+
import { adoptComments, anchorLabel, canRecordComments, findComment, setComment } from '../comments.js';
|
|
41
|
+
import { applyReadOnly, isReadOnly } from '../readonly.js';
|
|
42
|
+
import { registerSlotResponse } from '../responses.js';
|
|
43
|
+
/** The gap between a comment trigger and the editor that opens off it. */
|
|
44
|
+
const EDITOR_GAP = 4;
|
|
45
|
+
/**
|
|
46
|
+
* The motion the table's control-shaped things wear. A column toggle, a comment trigger and
|
|
47
|
+
* the editor's two buttons are all controls, so their timing chains through the family's
|
|
48
|
+
* `--crtr-control-transition` before reaching the kit default; a row is not a control, so its
|
|
49
|
+
* tint animation carries an element token alone.
|
|
50
|
+
*/
|
|
51
|
+
const TOGGLE_MOTION = 'var(--crtr-table-column-toggle-transition, var(--crtr-control-transition, 120ms ease))';
|
|
52
|
+
const ROW_MOTION = 'var(--crtr-table-row-transition, 120ms ease)';
|
|
53
|
+
const TRIGGER_MOTION = 'var(--crtr-table-comment-trigger-transition, var(--crtr-control-transition, 120ms ease))';
|
|
54
|
+
const COMMENT_BUTTON_MOTION = 'var(--crtr-table-comment-button-transition, var(--crtr-control-transition, 120ms ease))';
|
|
23
55
|
const COMMENT_BUBBLE = 'M3.25 3.5h9.5A1.75 1.75 0 0 1 14.5 5.25v5A1.75 1.75 0 0 1 12.75 12H7.4l-3.15 2.36A.5.5 0 0 1 3.5 14v-2H3.25A1.75 1.75 0 0 1 1.5 10.25v-5A1.75 1.75 0 0 1 3.25 3.5Z';
|
|
24
56
|
const STYLE = `
|
|
25
57
|
:host {
|
|
26
58
|
display: block;
|
|
27
59
|
}
|
|
28
60
|
|
|
29
|
-
|
|
61
|
+
/* The positioning context for the comment editor, and the ONLY one. The editor cannot live
|
|
62
|
+
inside the container: that box clips to its own rounded corners, so a popover anchored to
|
|
63
|
+
a row was cut off at the table's edge instead of floating over it. The frame has no paint
|
|
64
|
+
and no clipping, so the editor overlays whatever the table sits on. */
|
|
65
|
+
[part="frame"] {
|
|
30
66
|
position: relative;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Flat: the table sits in the flow of the page. A shadow in this bundle means floating —
|
|
70
|
+
the comment editor, and nothing else here. */
|
|
71
|
+
[part="container"] {
|
|
72
|
+
background: var(--crtr-table-container-fill, var(--crtr-color-background));
|
|
73
|
+
border: var(--crtr-table-container-border-width, 1px)
|
|
74
|
+
var(--crtr-table-container-border-style, solid)
|
|
75
|
+
var(--crtr-table-container-border-color, var(--crtr-color-border));
|
|
76
|
+
border-radius: var(--crtr-table-container-radius, var(--crtr-radius-medium));
|
|
35
77
|
overflow: hidden;
|
|
36
78
|
}
|
|
37
79
|
|
|
80
|
+
/* Inside the border, not outside it: the container is full-bleed, so an outset ring is
|
|
81
|
+
clipped at the edges of the frame the host gives the page and draws only three sides.
|
|
82
|
+
That is why this is NOT the shared control focus family — it is a focusable region rather
|
|
83
|
+
than a control, and it says so with its own inset outline. */
|
|
38
84
|
[part="container"]:focus-visible {
|
|
39
|
-
outline: 2px solid var(--crtr-color-focus);
|
|
40
|
-
outline-offset: 2px;
|
|
85
|
+
outline: var(--crtr-table-container-focus-outline, 2px solid var(--crtr-color-focus));
|
|
86
|
+
outline-offset: var(--crtr-table-container-focus-outline-offset, -2px);
|
|
41
87
|
}
|
|
42
88
|
|
|
89
|
+
/* Typography here is the element's host typography, not a second set of knobs: font stays
|
|
90
|
+
literal inherit so --crtr-table-font-family, --crtr-table-font-size and
|
|
91
|
+
--crtr-table-line-height reach the table, and the colour reads the host's own
|
|
92
|
+
--crtr-table-color, whose fallback is the colour this rule used to hard-code. */
|
|
43
93
|
[part="table"] {
|
|
44
94
|
width: 100%;
|
|
45
95
|
border-collapse: collapse;
|
|
46
96
|
font: inherit;
|
|
47
|
-
color: var(--crtr-color-text);
|
|
97
|
+
color: var(--crtr-table-color, var(--crtr-color-text));
|
|
48
98
|
}
|
|
49
99
|
|
|
50
100
|
[part="caption"] {
|
|
51
101
|
caption-side: top;
|
|
52
|
-
padding: var(--crtr-space-3) var(--crtr-space-3) var(--crtr-space-2);
|
|
102
|
+
padding: var(--crtr-table-caption-padding, var(--crtr-space-3) var(--crtr-space-3) var(--crtr-space-2));
|
|
53
103
|
text-align: left;
|
|
54
|
-
color: var(--crtr-color-text-muted);
|
|
104
|
+
color: var(--crtr-table-caption-text-color, var(--crtr-color-text-muted));
|
|
55
105
|
}
|
|
56
106
|
|
|
57
107
|
th {
|
|
58
|
-
padding: var(--crtr-space-2) var(--crtr-space-3);
|
|
108
|
+
padding: var(--crtr-table-header-padding, var(--crtr-space-2) var(--crtr-space-3));
|
|
59
109
|
text-align: left;
|
|
60
|
-
font-weight: 600;
|
|
61
|
-
color: var(--crtr-color-text-muted);
|
|
62
|
-
|
|
110
|
+
font-weight: var(--crtr-table-header-font-weight, 600);
|
|
111
|
+
color: var(--crtr-table-header-text-color, var(--crtr-color-text-muted));
|
|
112
|
+
/* A tinted header reads as the table's own chrome; the column-selected tint still wins,
|
|
113
|
+
on specificity (0-1-0 against this 0-0-1). */
|
|
114
|
+
background: var(--crtr-table-header-fill, var(--crtr-color-surface));
|
|
115
|
+
border-bottom: var(--crtr-table-header-border-width, 1px)
|
|
116
|
+
var(--crtr-table-header-border-style, solid)
|
|
117
|
+
var(--crtr-table-header-border-color, var(--crtr-color-border));
|
|
63
118
|
white-space: nowrap;
|
|
64
119
|
}
|
|
65
120
|
|
|
@@ -70,7 +125,7 @@ const STYLE = `
|
|
|
70
125
|
[part="header-cell-inner"] {
|
|
71
126
|
display: inline-flex;
|
|
72
127
|
align-items: center;
|
|
73
|
-
gap: var(--crtr-space-1);
|
|
128
|
+
gap: var(--crtr-table-header-gap, var(--crtr-space-1));
|
|
74
129
|
}
|
|
75
130
|
|
|
76
131
|
th[data-align="right"] [part="header-cell-inner"] {
|
|
@@ -78,125 +133,170 @@ const STYLE = `
|
|
|
78
133
|
}
|
|
79
134
|
|
|
80
135
|
[part="column-toggle"] {
|
|
81
|
-
font: inherit;
|
|
82
|
-
font-weight: 600;
|
|
83
|
-
color: inherit;
|
|
84
|
-
background: transparent;
|
|
85
|
-
border: 1px
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
136
|
+
font: var(--crtr-table-column-toggle-font, inherit);
|
|
137
|
+
font-weight: var(--crtr-table-column-toggle-font-weight, 600);
|
|
138
|
+
color: var(--crtr-table-column-toggle-text-color, inherit);
|
|
139
|
+
background: var(--crtr-table-column-toggle-fill, transparent);
|
|
140
|
+
border: var(--crtr-table-column-toggle-border-width, 1px)
|
|
141
|
+
var(--crtr-table-column-toggle-border-style, solid)
|
|
142
|
+
var(--crtr-table-column-toggle-border-color, transparent);
|
|
143
|
+
border-radius: var(--crtr-table-column-toggle-radius, var(--crtr-radius-small));
|
|
144
|
+
padding: var(--crtr-table-column-toggle-padding, var(--crtr-space-1) var(--crtr-space-2));
|
|
145
|
+
/* The toggle is pulled back out of its own padding so the header label sits where an
|
|
146
|
+
unselectable one does; a host that repads the toggle repays this too. */
|
|
147
|
+
margin: var(--crtr-table-column-toggle-margin, calc(var(--crtr-space-1) * -1) calc(var(--crtr-space-2) * -1));
|
|
89
148
|
cursor: pointer;
|
|
149
|
+
transition: color ${TOGGLE_MOTION}, background-color ${TOGGLE_MOTION}, border-color ${TOGGLE_MOTION};
|
|
90
150
|
}
|
|
91
151
|
|
|
92
152
|
[part="column-toggle"]:hover {
|
|
93
|
-
background: var(--crtr-color-surface-hover);
|
|
153
|
+
background: var(--crtr-table-column-toggle-hover-fill, var(--crtr-color-surface-hover));
|
|
94
154
|
}
|
|
95
155
|
|
|
96
156
|
[part="column-toggle"][aria-pressed="true"] {
|
|
97
|
-
color: var(--crtr-color-text);
|
|
98
|
-
background:
|
|
99
|
-
|
|
157
|
+
color: var(--crtr-table-column-toggle-selected-text-color, var(--crtr-color-text));
|
|
158
|
+
background: var(--crtr-table-column-toggle-selected-fill,
|
|
159
|
+
color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
|
|
160
|
+
border-color: var(--crtr-table-column-toggle-selected-border-color,
|
|
161
|
+
color-mix(in srgb, var(--crtr-color-accent) 45%, transparent));
|
|
100
162
|
}
|
|
101
163
|
|
|
102
|
-
[part="column-toggle"]:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
[part="select-all"]:focus-visible {
|
|
106
|
-
outline: 2px solid var(--crtr-color-focus);
|
|
107
|
-
outline-offset: 1px;
|
|
164
|
+
[part="column-toggle"][aria-pressed="true"]:hover {
|
|
165
|
+
background: var(--crtr-table-column-toggle-selected-hover-fill,
|
|
166
|
+
color-mix(in srgb, var(--crtr-color-accent) 14%, transparent));
|
|
108
167
|
}
|
|
109
168
|
|
|
169
|
+
${focusRingStyle({ scope: 'table-column-toggle' }, '[part="column-toggle"]')}
|
|
170
|
+
${focusRingStyle({ scope: 'table-comment-trigger' }, '[part="comment-trigger"]')}
|
|
171
|
+
|
|
110
172
|
[part="row"] {
|
|
111
|
-
border-bottom:
|
|
173
|
+
border-bottom: var(--crtr-table-row-border-width, 1px)
|
|
174
|
+
var(--crtr-table-row-border-style, solid)
|
|
175
|
+
var(--crtr-table-row-border-color, color-mix(in srgb, var(--crtr-color-border) 60%, transparent));
|
|
176
|
+
transition: background-color ${ROW_MOTION};
|
|
112
177
|
}
|
|
113
178
|
|
|
114
179
|
[part="row"]:last-child {
|
|
115
|
-
border-bottom: none;
|
|
180
|
+
border-bottom: var(--crtr-table-row-last-border, none);
|
|
116
181
|
}
|
|
117
182
|
|
|
118
183
|
[part="row"]:hover,
|
|
119
184
|
[part="row"][data-active="true"] {
|
|
120
|
-
background: var(--crtr-color-surface-hover);
|
|
185
|
+
background: var(--crtr-table-row-hover-fill, var(--crtr-color-surface-hover));
|
|
121
186
|
}
|
|
122
187
|
|
|
123
|
-
[part="row"][data-state="selected"]
|
|
124
|
-
|
|
125
|
-
background: color-mix(in srgb, var(--crtr-color-accent) 8%, transparent);
|
|
188
|
+
[part="row"][data-state="selected"] {
|
|
189
|
+
background: var(--crtr-table-row-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 10%, transparent));
|
|
126
190
|
}
|
|
127
191
|
|
|
192
|
+
[part="row"][data-state="selected"]:hover,
|
|
193
|
+
[part="row"][data-state="selected"][data-active="true"] {
|
|
194
|
+
background: var(--crtr-table-row-selected-hover-fill,
|
|
195
|
+
color-mix(in srgb, var(--crtr-color-accent) 14%, transparent));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Inset for the same reason the container's is: a row runs the full width of the table. */
|
|
128
199
|
[part="row"]:focus-visible {
|
|
129
|
-
outline: 2px solid var(--crtr-color-focus);
|
|
130
|
-
outline-offset: -2px;
|
|
200
|
+
outline: var(--crtr-table-row-focus-outline, 2px solid var(--crtr-color-focus));
|
|
201
|
+
outline-offset: var(--crtr-table-row-focus-outline-offset, -2px);
|
|
131
202
|
}
|
|
132
203
|
|
|
133
204
|
td {
|
|
134
|
-
padding: var(--crtr-space-2) var(--crtr-space-3);
|
|
205
|
+
padding: var(--crtr-table-cell-padding, var(--crtr-space-2) var(--crtr-space-3));
|
|
135
206
|
vertical-align: middle;
|
|
136
207
|
}
|
|
137
208
|
|
|
138
209
|
td[data-align="right"] {
|
|
139
210
|
text-align: right;
|
|
140
|
-
font-variant-numeric: tabular-nums;
|
|
211
|
+
font-variant-numeric: var(--crtr-table-cell-numeric-font-variant, tabular-nums);
|
|
141
212
|
}
|
|
142
213
|
|
|
143
214
|
td[data-mono="true"] {
|
|
144
|
-
font-family: var(--crtr-font-mono);
|
|
145
|
-
font-size: 0.8125em;
|
|
215
|
+
font-family: var(--crtr-table-cell-mono-font-family, var(--crtr-font-mono));
|
|
216
|
+
font-size: var(--crtr-table-cell-mono-font-size, 0.8125em);
|
|
146
217
|
}
|
|
147
218
|
|
|
148
219
|
[data-column-selected="true"] {
|
|
149
|
-
background: color-mix(in srgb, var(--crtr-color-accent) 6%, transparent);
|
|
220
|
+
background: var(--crtr-table-column-selected-fill, color-mix(in srgb, var(--crtr-color-accent) 6%, transparent));
|
|
150
221
|
}
|
|
151
222
|
|
|
152
223
|
[part="select-cell"],
|
|
153
224
|
[part="select-head"] {
|
|
154
225
|
width: 1px;
|
|
155
|
-
padding-right: 0;
|
|
226
|
+
padding-right: var(--crtr-table-select-cell-padding-right, 0);
|
|
156
227
|
}
|
|
157
228
|
|
|
158
229
|
[part="comment-cell"],
|
|
159
230
|
[part="comment-head"] {
|
|
160
231
|
width: 1px;
|
|
161
232
|
text-align: right;
|
|
162
|
-
padding-left: 0;
|
|
233
|
+
padding-left: var(--crtr-table-comment-cell-padding-left, 0);
|
|
163
234
|
}
|
|
164
235
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
236
|
+
/* One scope over both selectors: the header's select-all IS the row control, drawn once in
|
|
237
|
+
the head, and they are meant to be indistinguishable. */
|
|
238
|
+
${controlStyle({ scope: 'table-selection' }, '[part="row-control"]', '[part="select-all"]')}
|
|
239
|
+
|
|
240
|
+
/* A page that can no longer take an answer keeps its selection legible and drops every
|
|
241
|
+
affordance that says it could still be changed. */
|
|
242
|
+
[part="column-toggle"]:disabled {
|
|
243
|
+
cursor: default;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
[part="column-toggle"]:disabled:hover {
|
|
247
|
+
color: var(--crtr-table-column-toggle-disabled-text-color, var(--crtr-color-text-muted));
|
|
248
|
+
background: var(--crtr-table-column-toggle-disabled-fill, none);
|
|
170
249
|
}
|
|
171
250
|
|
|
251
|
+
/* One size knob for a square button, and one for the glyph in it — width and height are one
|
|
252
|
+
visual decision each. The opacity is the reveal-on-hover mechanic, which is behaviour. */
|
|
172
253
|
[part="comment-trigger"] {
|
|
173
254
|
display: inline-flex;
|
|
174
255
|
align-items: center;
|
|
175
256
|
justify-content: center;
|
|
176
|
-
width: 1.
|
|
177
|
-
height: 1.
|
|
178
|
-
padding: 0;
|
|
179
|
-
color: var(--crtr-color-text-muted);
|
|
180
|
-
background: transparent;
|
|
181
|
-
border:
|
|
182
|
-
|
|
257
|
+
width: var(--crtr-table-comment-trigger-size, 1.5em);
|
|
258
|
+
height: var(--crtr-table-comment-trigger-size, 1.5em);
|
|
259
|
+
padding: var(--crtr-table-comment-trigger-padding, 0);
|
|
260
|
+
color: var(--crtr-table-comment-trigger-text-color, var(--crtr-color-text-muted));
|
|
261
|
+
background: var(--crtr-table-comment-trigger-fill, transparent);
|
|
262
|
+
border: var(--crtr-table-comment-trigger-border-width, 0)
|
|
263
|
+
var(--crtr-table-comment-trigger-border-style, none)
|
|
264
|
+
var(--crtr-table-comment-trigger-border-color, currentColor);
|
|
265
|
+
border-radius: var(--crtr-table-comment-trigger-radius, var(--crtr-radius-small));
|
|
183
266
|
cursor: pointer;
|
|
184
267
|
opacity: 0;
|
|
185
|
-
transition: opacity
|
|
268
|
+
transition: opacity ${TRIGGER_MOTION}, color ${TRIGGER_MOTION}, background-color ${TRIGGER_MOTION};
|
|
186
269
|
}
|
|
187
270
|
|
|
188
271
|
[part="comment-trigger"] svg {
|
|
189
|
-
width: 0.
|
|
190
|
-
height: 0.
|
|
272
|
+
width: var(--crtr-table-comment-trigger-icon-size, 0.875em);
|
|
273
|
+
height: var(--crtr-table-comment-trigger-icon-size, 0.875em);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* The path geometry stays the icon's anatomy; its paint is component styling. Attribute
|
|
277
|
+
selectors preserve the filled-comment state that syncState writes while allowing both
|
|
278
|
+
appearances to inherit document-root overrides through this shadow root. */
|
|
279
|
+
[part="comment-trigger"] path {
|
|
280
|
+
stroke: var(--crtr-table-comment-trigger-icon-stroke, currentColor);
|
|
281
|
+
stroke-width: var(--crtr-table-comment-trigger-icon-stroke-width, 1.25);
|
|
282
|
+
stroke-linejoin: var(--crtr-table-comment-trigger-icon-stroke-linejoin, round);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
[part="comment-trigger"] path[fill="none"] {
|
|
286
|
+
fill: var(--crtr-table-comment-trigger-icon-fill, none);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
[part="comment-trigger"] path[fill="currentColor"] {
|
|
290
|
+
fill: var(--crtr-table-comment-trigger-commented-icon-fill, currentColor);
|
|
191
291
|
}
|
|
192
292
|
|
|
193
293
|
[part="comment-trigger"][data-has-comment="true"] {
|
|
194
|
-
color: var(--crtr-color-accent);
|
|
294
|
+
color: var(--crtr-table-comment-trigger-commented-text-color, var(--crtr-color-accent));
|
|
195
295
|
opacity: 1;
|
|
196
296
|
}
|
|
197
297
|
|
|
198
298
|
[part="comment-trigger"]:hover {
|
|
199
|
-
background: var(--crtr-color-surface-hover);
|
|
299
|
+
background: var(--crtr-table-comment-trigger-hover-fill, var(--crtr-color-surface-hover));
|
|
200
300
|
}
|
|
201
301
|
|
|
202
302
|
tr:hover [part="comment-trigger"],
|
|
@@ -207,108 +307,122 @@ const STYLE = `
|
|
|
207
307
|
opacity: 1;
|
|
208
308
|
}
|
|
209
309
|
|
|
310
|
+
/* A floating panel over the page, not a box in the table's layout: the background token
|
|
311
|
+
rather than the surface tint, so it reads as lifted off whatever it covers. */
|
|
210
312
|
[part="comment-editor"] {
|
|
211
313
|
position: absolute;
|
|
212
|
-
z-index:
|
|
213
|
-
|
|
314
|
+
z-index: 5;
|
|
315
|
+
box-sizing: border-box;
|
|
316
|
+
width: var(--crtr-table-comment-editor-width, min(20em, 100%));
|
|
214
317
|
display: grid;
|
|
215
|
-
gap: var(--crtr-space-2);
|
|
216
|
-
padding: var(--crtr-space-3);
|
|
217
|
-
background: var(--crtr-color-
|
|
218
|
-
border:
|
|
219
|
-
|
|
220
|
-
|
|
318
|
+
gap: var(--crtr-table-comment-editor-gap, var(--crtr-space-2));
|
|
319
|
+
padding: var(--crtr-table-comment-editor-padding, var(--crtr-space-3));
|
|
320
|
+
background: var(--crtr-table-comment-editor-fill, var(--crtr-color-background));
|
|
321
|
+
border: var(--crtr-table-comment-editor-border-width, 1px)
|
|
322
|
+
var(--crtr-table-comment-editor-border-style, solid)
|
|
323
|
+
var(--crtr-table-comment-editor-border-color, var(--crtr-color-border));
|
|
324
|
+
border-radius: var(--crtr-table-comment-editor-radius, var(--crtr-radius-medium));
|
|
325
|
+
box-shadow: var(--crtr-table-comment-editor-shadow, var(--crtr-shadow));
|
|
221
326
|
}
|
|
222
327
|
|
|
223
328
|
[part="comment-editor"][hidden] {
|
|
224
329
|
display: none;
|
|
225
330
|
}
|
|
226
331
|
|
|
332
|
+
/* The editor's own prose, in the page's voice. It was set in caps mono, which shouted a
|
|
333
|
+
row id at someone who had just clicked that row. */
|
|
227
334
|
[part="comment-target"] {
|
|
228
|
-
margin: 0;
|
|
229
|
-
font-
|
|
230
|
-
|
|
231
|
-
text-transform: uppercase;
|
|
232
|
-
letter-spacing: 0.06em;
|
|
233
|
-
color: var(--crtr-color-text-muted);
|
|
335
|
+
margin: var(--crtr-table-comment-target-margin, 0);
|
|
336
|
+
font-size: var(--crtr-table-comment-target-font-size, 0.875em);
|
|
337
|
+
color: var(--crtr-table-comment-target-text-color, var(--crtr-color-text-muted));
|
|
234
338
|
}
|
|
235
339
|
|
|
340
|
+
${writingBoxStyle({ scope: 'table-comment-input' }, '[part="comment-input"]')}
|
|
236
341
|
[part="comment-input"] {
|
|
237
|
-
|
|
238
|
-
color: var(--crtr-color-text);
|
|
239
|
-
background: var(--crtr-color-background);
|
|
240
|
-
border: 1px solid var(--crtr-color-border);
|
|
241
|
-
border-radius: var(--crtr-radius-small);
|
|
242
|
-
padding: var(--crtr-space-2);
|
|
243
|
-
min-height: 4rem;
|
|
342
|
+
min-height: var(--crtr-table-comment-input-min-height, 4.5em);
|
|
244
343
|
resize: vertical;
|
|
245
344
|
}
|
|
246
345
|
|
|
247
|
-
[part="comment-input"]:focus-visible {
|
|
248
|
-
outline: 2px solid var(--crtr-color-focus);
|
|
249
|
-
outline-offset: -1px;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
346
|
[part="comment-actions"] {
|
|
253
347
|
display: flex;
|
|
254
348
|
align-items: center;
|
|
255
349
|
justify-content: space-between;
|
|
256
|
-
gap: var(--crtr-space-2);
|
|
350
|
+
gap: var(--crtr-table-comment-actions-gap, var(--crtr-space-2));
|
|
257
351
|
}
|
|
258
352
|
|
|
259
353
|
[part="comment-hint"] {
|
|
260
|
-
font-
|
|
261
|
-
|
|
262
|
-
text-transform: uppercase;
|
|
263
|
-
letter-spacing: 0.06em;
|
|
264
|
-
color: var(--crtr-color-text-muted);
|
|
354
|
+
font-size: var(--crtr-table-comment-hint-font-size, 0.875em);
|
|
355
|
+
color: var(--crtr-table-comment-hint-text-color, var(--crtr-color-text-muted));
|
|
265
356
|
}
|
|
266
357
|
|
|
358
|
+
/* The chrome both editor buttons share reads one scope — a person sees one pair of buttons —
|
|
359
|
+
and each button's own paint carries its own. */
|
|
267
360
|
[part="comment-cancel"],
|
|
268
361
|
[part="comment-save"] {
|
|
269
|
-
font: inherit;
|
|
270
|
-
font-size: 0.875em;
|
|
271
|
-
padding: var(--crtr-space-1) var(--crtr-space-2);
|
|
272
|
-
border-radius: var(--crtr-radius-small);
|
|
273
|
-
border:
|
|
362
|
+
font: var(--crtr-table-comment-button-font, inherit);
|
|
363
|
+
font-size: var(--crtr-table-comment-button-font-size, 0.875em);
|
|
364
|
+
padding: var(--crtr-table-comment-button-padding, var(--crtr-space-1) var(--crtr-space-2));
|
|
365
|
+
border-radius: var(--crtr-table-comment-button-radius, var(--crtr-radius-small));
|
|
366
|
+
border: var(--crtr-table-comment-button-border-width, 1px)
|
|
367
|
+
var(--crtr-table-comment-button-border-style, solid)
|
|
368
|
+
var(--crtr-table-comment-button-border-color, var(--crtr-color-border));
|
|
274
369
|
cursor: pointer;
|
|
370
|
+
transition: color ${COMMENT_BUTTON_MOTION}, background-color ${COMMENT_BUTTON_MOTION},
|
|
371
|
+
border-color ${COMMENT_BUTTON_MOTION};
|
|
275
372
|
}
|
|
276
373
|
|
|
277
374
|
[part="comment-cancel"] {
|
|
278
|
-
color: var(--crtr-color-text-muted);
|
|
279
|
-
background: transparent;
|
|
375
|
+
color: var(--crtr-table-comment-cancel-text-color, var(--crtr-color-text-muted));
|
|
376
|
+
background: var(--crtr-table-comment-cancel-fill, transparent);
|
|
377
|
+
border-color: var(--crtr-table-comment-cancel-border-color,
|
|
378
|
+
var(--crtr-table-comment-button-border-color, transparent));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
[part="comment-cancel"]:hover {
|
|
382
|
+
color: var(--crtr-table-comment-cancel-hover-text-color, var(--crtr-color-text));
|
|
383
|
+
background: var(--crtr-table-comment-cancel-hover-fill, var(--crtr-color-surface-hover));
|
|
280
384
|
}
|
|
281
385
|
|
|
282
386
|
[part="comment-save"] {
|
|
283
|
-
color: var(--crtr-color-accent-contrast);
|
|
284
|
-
background: var(--crtr-color-accent);
|
|
285
|
-
border-color: var(--crtr-color
|
|
387
|
+
color: var(--crtr-table-comment-save-text-color, var(--crtr-color-accent-contrast));
|
|
388
|
+
background: var(--crtr-table-comment-save-fill, var(--crtr-color-accent));
|
|
389
|
+
border-color: var(--crtr-table-comment-save-border-color,
|
|
390
|
+
var(--crtr-table-comment-button-border-color, var(--crtr-color-accent)));
|
|
286
391
|
}
|
|
287
392
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
393
|
+
/* Mixing toward the text colour darkens on a light token set and lightens on a dark one —
|
|
394
|
+
a brightness filter moves the wrong way on one of them. */
|
|
395
|
+
[part="comment-save"]:hover {
|
|
396
|
+
background: var(--crtr-table-comment-save-hover-fill,
|
|
397
|
+
color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
|
|
398
|
+
border-color: var(--crtr-table-comment-save-hover-border-color,
|
|
399
|
+
color-mix(in srgb, var(--crtr-color-accent) 88%, var(--crtr-color-text)));
|
|
292
400
|
}
|
|
293
401
|
|
|
402
|
+
${focusRingStyle({ scope: 'table-comment-cancel' }, '[part="comment-cancel"]')}
|
|
403
|
+
${focusRingStyle({ scope: 'table-comment-save' }, '[part="comment-save"]')}
|
|
404
|
+
|
|
405
|
+
/* One scope over both: a loading line and an unavailable notice are the same box, saying
|
|
406
|
+
the table has nothing to show yet or ever. */
|
|
294
407
|
[part="status"],
|
|
295
408
|
[part="unavailable"] {
|
|
296
|
-
padding: var(--crtr-space-3);
|
|
297
|
-
color: var(--crtr-color-text-muted);
|
|
298
|
-
background: var(--crtr-color-surface);
|
|
299
|
-
border:
|
|
300
|
-
|
|
301
|
-
|
|
409
|
+
padding: var(--crtr-table-notice-padding, var(--crtr-space-3));
|
|
410
|
+
color: var(--crtr-table-notice-text-color, var(--crtr-color-text-muted));
|
|
411
|
+
background: var(--crtr-table-notice-fill, var(--crtr-color-surface));
|
|
412
|
+
border: var(--crtr-table-notice-border-width, 1px)
|
|
413
|
+
var(--crtr-table-notice-border-style, solid)
|
|
414
|
+
var(--crtr-table-notice-border-color, var(--crtr-color-border));
|
|
415
|
+
border-radius: var(--crtr-table-notice-radius, var(--crtr-radius-medium));
|
|
302
416
|
}
|
|
303
417
|
|
|
304
418
|
[part="reason"] {
|
|
305
|
-
color: var(--crtr-color-text);
|
|
306
|
-
font-family: var(--crtr-font-mono);
|
|
419
|
+
color: var(--crtr-table-reason-text-color, var(--crtr-color-text));
|
|
420
|
+
font-family: var(--crtr-table-reason-font-family, var(--crtr-font-mono));
|
|
307
421
|
}
|
|
308
422
|
|
|
309
423
|
[part="empty"] {
|
|
310
|
-
padding: var(--crtr-space-3);
|
|
311
|
-
color: var(--crtr-color-text-muted);
|
|
424
|
+
padding: var(--crtr-table-empty-padding, var(--crtr-space-3));
|
|
425
|
+
color: var(--crtr-table-empty-text-color, var(--crtr-color-text-muted));
|
|
312
426
|
text-align: center;
|
|
313
427
|
}
|
|
314
428
|
`;
|
|
@@ -321,34 +435,6 @@ function cellText(value) {
|
|
|
321
435
|
function isPlainObject(value) {
|
|
322
436
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
323
437
|
}
|
|
324
|
-
/**
|
|
325
|
-
* The rows inside whatever the host handed back for a `source`. Accepted shapes are the same
|
|
326
|
-
* `{ id, cells }` rows an inline config carries, either as a bare array or under a `rows` key;
|
|
327
|
-
* anything else is a named unavailable state rather than a guess at the author's intent.
|
|
328
|
-
*/
|
|
329
|
-
function readSourceRows(data) {
|
|
330
|
-
const raw = Array.isArray(data) ? data : isPlainObject(data) && Array.isArray(data.rows) ? data.rows : undefined;
|
|
331
|
-
if (raw === undefined)
|
|
332
|
-
return { status: 'invalid', reason: 'source data is not a list of rows' };
|
|
333
|
-
const rows = [];
|
|
334
|
-
for (let index = 0; index < raw.length; index += 1) {
|
|
335
|
-
const row = raw[index];
|
|
336
|
-
if (!isPlainObject(row) || typeof row.id !== 'string' || row.id === '' || !isPlainObject(row.cells)) {
|
|
337
|
-
return { status: 'invalid', reason: `source row #${index} is not { id, cells }` };
|
|
338
|
-
}
|
|
339
|
-
const cells = {};
|
|
340
|
-
for (const [key, value] of Object.entries(row.cells)) {
|
|
341
|
-
if (value === undefined)
|
|
342
|
-
continue;
|
|
343
|
-
if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
344
|
-
return { status: 'invalid', reason: `source row "${row.id}" cell "${key}" is not a string, number, boolean or null` };
|
|
345
|
-
}
|
|
346
|
-
cells[key] = value;
|
|
347
|
-
}
|
|
348
|
-
rows.push({ id: row.id, cells });
|
|
349
|
-
}
|
|
350
|
-
return { status: 'ok', rows };
|
|
351
|
-
}
|
|
352
438
|
function commentIcon() {
|
|
353
439
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
354
440
|
svg.setAttribute('viewBox', '0 0 16 16');
|
|
@@ -369,6 +455,8 @@ function isTextEntry(target) {
|
|
|
369
455
|
return target.closest('input[type="text"], textarea, [contenteditable="true"]') !== null;
|
|
370
456
|
}
|
|
371
457
|
class CrtrTableElement extends HTMLElement {
|
|
458
|
+
/** The host marks a resolved or canceled page `inert`; that is this element's read-only signal. */
|
|
459
|
+
static observedAttributes = ['inert'];
|
|
372
460
|
#root;
|
|
373
461
|
#slotId;
|
|
374
462
|
#config;
|
|
@@ -379,8 +467,9 @@ class CrtrTableElement extends HTMLElement {
|
|
|
379
467
|
#activeRowId = null;
|
|
380
468
|
#editorAnchor = null;
|
|
381
469
|
#editorTrigger;
|
|
382
|
-
#connected = false;
|
|
383
470
|
#container;
|
|
471
|
+
/** The editor's positioning context — the unclipped wrapper around the container. */
|
|
472
|
+
#frame;
|
|
384
473
|
#editor;
|
|
385
474
|
#editorTarget;
|
|
386
475
|
#editorInput;
|
|
@@ -394,7 +483,6 @@ class CrtrTableElement extends HTMLElement {
|
|
|
394
483
|
this.#root = this.attachShadow({ mode: 'open' });
|
|
395
484
|
}
|
|
396
485
|
connectedCallback() {
|
|
397
|
-
this.#connected = true;
|
|
398
486
|
const read = readSlotConfig(this);
|
|
399
487
|
if (read.status !== 'ok') {
|
|
400
488
|
this.#renderUnavailable(read.reason);
|
|
@@ -405,49 +493,43 @@ class CrtrTableElement extends HTMLElement {
|
|
|
405
493
|
this.#renderUnavailable('table config has no columns');
|
|
406
494
|
return;
|
|
407
495
|
}
|
|
408
|
-
if (config.rows !== undefined && config.source !== undefined) {
|
|
409
|
-
this.#renderUnavailable('table rows and source are mutually exclusive');
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
496
|
this.#config = config;
|
|
413
497
|
this.#slotId = readSlotId(this);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
498
|
+
// Only a selectable table bears a response — the host rejects a map naming a display-only
|
|
499
|
+
// slot — and the modes are known here before rendering.
|
|
500
|
+
if (this.#slotId !== undefined && (this.#rowSelect !== 'none' || this.#columnSelect !== 'none')) {
|
|
501
|
+
registerSlotResponse(this, this.#slotId, () => this.#response());
|
|
418
502
|
}
|
|
419
|
-
|
|
420
|
-
this.#renderUnavailable('table config has neither rows nor source');
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
this.#rows = config.rows;
|
|
503
|
+
this.#rows = config.rows ?? [];
|
|
424
504
|
this.#hydrate();
|
|
425
505
|
this.#renderTable();
|
|
426
506
|
}
|
|
427
|
-
|
|
428
|
-
this
|
|
507
|
+
attributeChangedCallback() {
|
|
508
|
+
if (this.isConnected)
|
|
509
|
+
this.#syncReadOnly();
|
|
429
510
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return;
|
|
511
|
+
/**
|
|
512
|
+
* Repainted after every state sync, because a rebuild replaces the controls. Column
|
|
513
|
+
* toggles are buttons rather than inputs, so they are named here; the mutation paths
|
|
514
|
+
* refuse anyway, and this is what makes the refusal visible.
|
|
515
|
+
*/
|
|
516
|
+
#syncReadOnly() {
|
|
517
|
+
const readOnly = isReadOnly(this);
|
|
518
|
+
applyReadOnly(this.#root, readOnly);
|
|
519
|
+
for (const parts of this.#columnParts.values()) {
|
|
520
|
+
if (parts.toggle !== undefined)
|
|
521
|
+
parts.toggle.disabled = readOnly;
|
|
442
522
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
523
|
+
// A page can go read-only while the composer is open — the host sets `inert` on a ticket
|
|
524
|
+
// that resolves under it. A box that can no longer save is closed rather than left there
|
|
525
|
+
// taking words nothing will keep.
|
|
526
|
+
if (readOnly && this.#editorAnchor !== null)
|
|
527
|
+
this.#closeEditor();
|
|
446
528
|
}
|
|
447
529
|
/**
|
|
448
530
|
* Seed local state from the answer the host is already holding, so a reopened page shows
|
|
449
|
-
* the rows and columns that were picked. It runs once
|
|
450
|
-
*
|
|
531
|
+
* the rows and columns that were picked. It runs once rows are known because a saved id is
|
|
532
|
+
* only honoured while it still names something on
|
|
451
533
|
* screen. The select modes are re-imposed for the same reason.
|
|
452
534
|
*/
|
|
453
535
|
#hydrate() {
|
|
@@ -500,7 +582,7 @@ class CrtrTableElement extends HTMLElement {
|
|
|
500
582
|
}
|
|
501
583
|
#setRowSelected(rowId, selected) {
|
|
502
584
|
const mode = this.#rowSelect;
|
|
503
|
-
if (mode === 'none')
|
|
585
|
+
if (mode === 'none' || isReadOnly(this))
|
|
504
586
|
return;
|
|
505
587
|
if (mode === 'single') {
|
|
506
588
|
this.#selectedRowIds = selected ? [rowId] : [];
|
|
@@ -519,7 +601,7 @@ class CrtrTableElement extends HTMLElement {
|
|
|
519
601
|
}
|
|
520
602
|
#setColumnSelected(columnId, selected) {
|
|
521
603
|
const mode = this.#columnSelect;
|
|
522
|
-
if (mode === 'none')
|
|
604
|
+
if (mode === 'none' || isReadOnly(this))
|
|
523
605
|
return;
|
|
524
606
|
if (mode === 'single') {
|
|
525
607
|
this.#selectedColumnIds = selected ? [columnId] : [];
|
|
@@ -533,7 +615,7 @@ class CrtrTableElement extends HTMLElement {
|
|
|
533
615
|
this.#setColumnSelected(columnId, !this.#selectedColumnIds.includes(columnId));
|
|
534
616
|
}
|
|
535
617
|
#setAllRows(selected) {
|
|
536
|
-
if (this.#rowSelect !== 'multi')
|
|
618
|
+
if (this.#rowSelect !== 'multi' || isReadOnly(this))
|
|
537
619
|
return;
|
|
538
620
|
this.#selectedRowIds = selected ? this.#rows.map((row) => row.id) : [];
|
|
539
621
|
this.#commit();
|
|
@@ -544,6 +626,7 @@ class CrtrTableElement extends HTMLElement {
|
|
|
544
626
|
this.#rowParts.clear();
|
|
545
627
|
this.#columnParts.clear();
|
|
546
628
|
this.#container = undefined;
|
|
629
|
+
this.#frame = undefined;
|
|
547
630
|
this.#editor = undefined;
|
|
548
631
|
this.#editorTrigger = undefined;
|
|
549
632
|
this.#editorAnchor = null;
|
|
@@ -564,14 +647,6 @@ class CrtrTableElement extends HTMLElement {
|
|
|
564
647
|
box.append('Table unavailable: ', detail);
|
|
565
648
|
this.#root.append(box);
|
|
566
649
|
}
|
|
567
|
-
#renderStatus(text) {
|
|
568
|
-
this.#reset();
|
|
569
|
-
const box = document.createElement('div');
|
|
570
|
-
box.part.add('status');
|
|
571
|
-
box.setAttribute('role', 'status');
|
|
572
|
-
box.textContent = text;
|
|
573
|
-
this.#root.append(box);
|
|
574
|
-
}
|
|
575
650
|
#renderTable() {
|
|
576
651
|
const config = this.#config;
|
|
577
652
|
if (config === undefined)
|
|
@@ -590,10 +665,10 @@ class CrtrTableElement extends HTMLElement {
|
|
|
590
665
|
this.#container = container;
|
|
591
666
|
const table = document.createElement('table');
|
|
592
667
|
table.part.add('table');
|
|
593
|
-
if (config.
|
|
668
|
+
if (config.label !== undefined && config.label !== '') {
|
|
594
669
|
const caption = document.createElement('caption');
|
|
595
670
|
caption.part.add('caption');
|
|
596
|
-
caption.textContent = config.
|
|
671
|
+
caption.textContent = config.label;
|
|
597
672
|
table.append(caption);
|
|
598
673
|
}
|
|
599
674
|
table.append(this.#buildHead(config), this.#buildBody(config));
|
|
@@ -604,8 +679,13 @@ class CrtrTableElement extends HTMLElement {
|
|
|
604
679
|
empty.textContent = 'No rows.';
|
|
605
680
|
container.append(empty);
|
|
606
681
|
}
|
|
607
|
-
|
|
608
|
-
|
|
682
|
+
const frame = document.createElement('div');
|
|
683
|
+
frame.part.add('frame');
|
|
684
|
+
frame.append(container);
|
|
685
|
+
if (this.#commentable)
|
|
686
|
+
frame.append(this.#buildEditor());
|
|
687
|
+
this.#frame = frame;
|
|
688
|
+
this.#root.append(frame);
|
|
609
689
|
this.#syncState();
|
|
610
690
|
}
|
|
611
691
|
#buildHead(config) {
|
|
@@ -653,18 +733,25 @@ class CrtrTableElement extends HTMLElement {
|
|
|
653
733
|
toggle.addEventListener('click', () => this.#toggleColumn(column.id));
|
|
654
734
|
inner.append(toggle);
|
|
655
735
|
}
|
|
656
|
-
const comment = this.#
|
|
657
|
-
|
|
736
|
+
const comment = this.#commentable
|
|
737
|
+
? this.#buildCommentTrigger({ kind: 'column', columnId: column.id }, `the ${column.label} column`)
|
|
738
|
+
: undefined;
|
|
739
|
+
if (comment !== undefined)
|
|
740
|
+
inner.append(comment);
|
|
658
741
|
th.append(inner);
|
|
659
742
|
tr.append(th);
|
|
660
|
-
this.#columnParts.set(column.id, { toggle, comment, cells: [], head: th });
|
|
743
|
+
this.#columnParts.set(column.id, { toggle, ...(comment === undefined ? {} : { comment }), cells: [], head: th });
|
|
744
|
+
}
|
|
745
|
+
// The trailing column exists to hold the whole-table trigger; without comments there is
|
|
746
|
+
// nothing to put in it, and an empty column would just be a stripe of dead width.
|
|
747
|
+
if (this.#commentable) {
|
|
748
|
+
const trailing = document.createElement('th');
|
|
749
|
+
trailing.part.add('comment-head');
|
|
750
|
+
trailing.setAttribute('scope', 'col');
|
|
751
|
+
this.#wholeComment = this.#buildCommentTrigger({ kind: 'whole' }, 'this table');
|
|
752
|
+
trailing.append(this.#wholeComment);
|
|
753
|
+
tr.append(trailing);
|
|
661
754
|
}
|
|
662
|
-
const trailing = document.createElement('th');
|
|
663
|
-
trailing.part.add('comment-head');
|
|
664
|
-
trailing.setAttribute('scope', 'col');
|
|
665
|
-
this.#wholeComment = this.#buildCommentTrigger({ kind: 'whole' }, 'this table');
|
|
666
|
-
trailing.append(this.#wholeComment);
|
|
667
|
-
tr.append(trailing);
|
|
668
755
|
head.append(tr);
|
|
669
756
|
return head;
|
|
670
757
|
}
|
|
@@ -705,16 +792,23 @@ class CrtrTableElement extends HTMLElement {
|
|
|
705
792
|
tr.append(td);
|
|
706
793
|
this.#columnParts.get(column.id)?.cells.push(td);
|
|
707
794
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
795
|
+
let comment;
|
|
796
|
+
if (this.#commentable) {
|
|
797
|
+
const commentCell = document.createElement('td');
|
|
798
|
+
commentCell.part.add('comment-cell');
|
|
799
|
+
comment = this.#buildCommentTrigger({ kind: 'row', rowId: row.id }, `row ${row.id}`);
|
|
800
|
+
commentCell.append(comment);
|
|
801
|
+
tr.append(commentCell);
|
|
802
|
+
}
|
|
713
803
|
body.append(tr);
|
|
714
|
-
this.#rowParts.set(row.id, { tr, ...(control === undefined ? {} : { control }), comment });
|
|
804
|
+
this.#rowParts.set(row.id, { tr, ...(control === undefined ? {} : { control }), ...(comment === undefined ? {} : { comment }) });
|
|
715
805
|
}
|
|
716
806
|
return body;
|
|
717
807
|
}
|
|
808
|
+
/** Whether this table can keep a comment — see `canRecordComments`. */
|
|
809
|
+
get #commentable() {
|
|
810
|
+
return canRecordComments(this.#slotId);
|
|
811
|
+
}
|
|
718
812
|
#buildCommentTrigger(anchor, target) {
|
|
719
813
|
const button = document.createElement('button');
|
|
720
814
|
button.type = 'button';
|
|
@@ -776,8 +870,10 @@ class CrtrTableElement extends HTMLElement {
|
|
|
776
870
|
// ── comments ───────────────────────────────────────────────────────────────────────────
|
|
777
871
|
#openEditor(anchor, trigger) {
|
|
778
872
|
const editor = this.#editor;
|
|
779
|
-
const
|
|
780
|
-
|
|
873
|
+
const frame = this.#frame;
|
|
874
|
+
// A composer opens only where its comment can land: never on a read-only page, whose
|
|
875
|
+
// textareas are already `readOnly`, so the box would take focus and refuse every keystroke.
|
|
876
|
+
if (editor === undefined || frame === undefined || !this.#commentable || isReadOnly(this))
|
|
781
877
|
return;
|
|
782
878
|
this.#editorAnchor = anchor;
|
|
783
879
|
this.#editorTrigger = trigger;
|
|
@@ -791,11 +887,20 @@ class CrtrTableElement extends HTMLElement {
|
|
|
791
887
|
}
|
|
792
888
|
if (this.#editorHint !== undefined)
|
|
793
889
|
this.#editorHint.textContent = existing === undefined ? '⌘↵ to save' : 'empty to delete';
|
|
794
|
-
const bounds =
|
|
890
|
+
const bounds = frame.getBoundingClientRect();
|
|
795
891
|
const rect = trigger.getBoundingClientRect();
|
|
796
892
|
editor.hidden = false;
|
|
797
|
-
editor.style.top = `${Math.max(0, rect.bottom - bounds.top + 4)}px`;
|
|
798
893
|
editor.style.right = `${Math.max(0, bounds.right - rect.right)}px`;
|
|
894
|
+
// Below the trigger by default; above it when the viewport has no room below and does
|
|
895
|
+
// have room above. Measured after unhiding, because a hidden panel has no height.
|
|
896
|
+
editor.style.top = '0px';
|
|
897
|
+
const height = editor.getBoundingClientRect().height;
|
|
898
|
+
const below = rect.bottom + EDITOR_GAP + height <= window.innerHeight;
|
|
899
|
+
const above = rect.top - EDITOR_GAP - height >= 0;
|
|
900
|
+
editor.style.top =
|
|
901
|
+
!below && above
|
|
902
|
+
? `${rect.top - EDITOR_GAP - height - bounds.top}px`
|
|
903
|
+
: `${rect.bottom + EDITOR_GAP - bounds.top}px`;
|
|
799
904
|
this.#syncState();
|
|
800
905
|
this.#editorInput?.focus();
|
|
801
906
|
}
|
|
@@ -815,7 +920,7 @@ class CrtrTableElement extends HTMLElement {
|
|
|
815
920
|
}
|
|
816
921
|
#saveComment() {
|
|
817
922
|
const anchor = this.#editorAnchor;
|
|
818
|
-
if (anchor === null)
|
|
923
|
+
if (anchor === null || isReadOnly(this))
|
|
819
924
|
return;
|
|
820
925
|
this.#comments = setComment(this.#comments, anchor, this.#editorInput?.value ?? '');
|
|
821
926
|
this.#closeEditor();
|
|
@@ -854,9 +959,9 @@ class CrtrTableElement extends HTMLElement {
|
|
|
854
959
|
if (key === 'x')
|
|
855
960
|
this.#toggleColumn(columnId);
|
|
856
961
|
else {
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
this.#openEditor({ kind: 'column', columnId },
|
|
962
|
+
const comment = this.#columnParts.get(columnId)?.comment;
|
|
963
|
+
if (comment !== undefined)
|
|
964
|
+
this.#openEditor({ kind: 'column', columnId }, comment);
|
|
860
965
|
}
|
|
861
966
|
return;
|
|
862
967
|
}
|
|
@@ -877,9 +982,9 @@ class CrtrTableElement extends HTMLElement {
|
|
|
877
982
|
this.#toggleRow(currentId);
|
|
878
983
|
return;
|
|
879
984
|
}
|
|
880
|
-
const
|
|
881
|
-
if (
|
|
882
|
-
this.#openEditor({ kind: 'row', rowId: currentId },
|
|
985
|
+
const comment = this.#rowParts.get(currentId)?.comment;
|
|
986
|
+
if (comment !== undefined)
|
|
987
|
+
this.#openEditor({ kind: 'row', rowId: currentId }, comment);
|
|
883
988
|
}
|
|
884
989
|
// ── state → DOM ────────────────────────────────────────────────────────────────────────
|
|
885
990
|
#syncCommentTrigger(button, anchor) {
|
|
@@ -913,7 +1018,8 @@ class CrtrTableElement extends HTMLElement {
|
|
|
913
1018
|
parts.tr.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
914
1019
|
if (parts.control !== undefined)
|
|
915
1020
|
parts.control.checked = selected;
|
|
916
|
-
|
|
1021
|
+
if (parts.comment !== undefined)
|
|
1022
|
+
this.#syncCommentTrigger(parts.comment, { kind: 'row', rowId });
|
|
917
1023
|
}
|
|
918
1024
|
for (const [columnId, parts] of this.#columnParts) {
|
|
919
1025
|
const selected = this.#selectedColumnIds.includes(columnId);
|
|
@@ -921,7 +1027,8 @@ class CrtrTableElement extends HTMLElement {
|
|
|
921
1027
|
parts.head.dataset.columnSelected = selected ? 'true' : 'false';
|
|
922
1028
|
for (const cell of parts.cells)
|
|
923
1029
|
cell.dataset.columnSelected = selected ? 'true' : 'false';
|
|
924
|
-
|
|
1030
|
+
if (parts.comment !== undefined)
|
|
1031
|
+
this.#syncCommentTrigger(parts.comment, { kind: 'column', columnId });
|
|
925
1032
|
}
|
|
926
1033
|
if (this.#wholeComment !== undefined)
|
|
927
1034
|
this.#syncCommentTrigger(this.#wholeComment, { kind: 'whole' });
|
|
@@ -930,6 +1037,7 @@ class CrtrTableElement extends HTMLElement {
|
|
|
930
1037
|
this.#selectAll.checked = all;
|
|
931
1038
|
this.#selectAll.indeterminate = this.#selectedRowIds.length > 0 && !all;
|
|
932
1039
|
}
|
|
1040
|
+
this.#syncReadOnly();
|
|
933
1041
|
}
|
|
934
1042
|
}
|
|
935
1043
|
registerElement('crtr-table', CrtrTableElement);
|