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