@north-light/crouter 0.3.196 → 0.3.198
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +2 -2
- package/dist/api/dto/inbox.d.ts +46 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -3
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
- package/dist/clients/attach/render/chat-view.d.ts +3 -8
- package/dist/clients/attach/render/chat-view.js +3 -24
- package/dist/clients/attach/render/markdown-source.js +0 -6
- package/dist/clients/attach/render/tool-calls.js +16 -2
- package/dist/clients/attach/viewer.js +613 -602
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
- package/dist/clients/inbox/controller.js +4 -4
- package/dist/clients/inbox/page-adapter.d.ts +2 -1
- package/dist/clients/inbox/tui/render.js +46 -25
- package/dist/clients/inbox/tui/slots.d.ts +1 -3
- package/dist/clients/inbox/tui/slots.js +8 -37
- package/dist/clients/inbox/tui/types.d.ts +4 -3
- package/dist/commands/__tests__/human.test.js +160 -23
- package/dist/commands/canvas-browse.js +1 -1
- package/dist/commands/human/doc.js +21 -74
- package/dist/commands/human/html.d.ts +2 -0
- package/dist/commands/human/html.js +81 -0
- package/dist/commands/human/prompts.d.ts +5 -4
- package/dist/commands/human/prompts.js +90 -168
- package/dist/commands/human/shared.d.ts +5 -6
- package/dist/commands/human/shared.js +13 -61
- package/dist/commands/human.js +7 -2
- package/dist/core/__tests__/daemon-boot.test.js +7 -4
- package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
- package/dist/core/__tests__/human-deliver.test.js +28 -37
- package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
- package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
- package/dist/core/canvas/__tests__/attention.test.js +2 -2
- package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
- package/dist/core/canvas/browse/app.d.ts +1 -1
- package/dist/core/canvas/browse/app.js +21 -3
- package/dist/core/canvas/browse/render.d.ts +4 -0
- package/dist/core/canvas/browse/render.js +5 -1
- package/dist/core/canvas/canvas.d.ts +26 -0
- package/dist/core/canvas/canvas.js +55 -0
- package/dist/core/command.js +6 -11
- package/dist/core/config.js +3 -2
- package/dist/core/help.d.ts +3 -0
- package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/html-markdown.test.js +52 -0
- package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-catalog.test.js +16 -0
- package/dist/core/human/__tests__/page-render.test.js +40 -23
- package/dist/core/human/__tests__/page-tickets.test.js +48 -27
- package/dist/core/human/__tests__/page.test.js +48 -65
- package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
- package/dist/core/human/answer-text.d.ts +3 -0
- package/dist/core/human/answer-text.js +98 -0
- package/dist/core/human/answer.d.ts +51 -0
- package/dist/core/human/answer.js +125 -0
- package/dist/core/human/component-docs.d.ts +14 -0
- package/dist/core/human/component-docs.js +161 -0
- package/dist/core/human/html-markdown.d.ts +3 -0
- package/dist/core/human/html-markdown.js +288 -0
- package/dist/core/human/page-catalog.d.ts +19 -3
- package/dist/core/human/page-catalog.js +73 -16
- package/dist/core/human/page-render.d.ts +3 -2
- package/dist/core/human/page-render.js +19 -45
- package/dist/core/human/page-schema.d.ts +108 -8
- package/dist/core/human/page-schema.js +88 -31
- package/dist/core/human/page-synth.d.ts +2 -4
- package/dist/core/human/page-synth.js +23 -7
- package/dist/core/human/page.d.ts +14 -7
- package/dist/core/human/page.js +178 -90
- package/dist/core/human/review-schema.d.ts +1 -0
- package/dist/core/human/review-schema.js +1 -1
- package/dist/core/human/scan.d.ts +3 -2
- package/dist/core/human/scan.js +7 -8
- package/dist/core/human/summary.d.ts +4 -7
- package/dist/core/human/summary.js +4 -60
- package/dist/core/human/tickets.d.ts +10 -9
- package/dist/core/human/tickets.js +5 -3
- package/dist/core/human/types.d.ts +4 -0
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +1 -0
- package/dist/core/review/realize.js +9 -1
- package/dist/core/runtime/boot-root.js +6 -1
- package/dist/core/runtime/broker.js +8 -6
- package/dist/core/runtime/busy.d.ts +4 -3
- package/dist/core/runtime/busy.js +4 -3
- package/dist/core/runtime/spawn.js +4 -4
- package/dist/core/termrender/display.d.ts +7 -3
- package/dist/core/termrender/display.js +45 -9
- package/dist/core/termrender/termrender.d.ts +3 -12
- package/dist/core/termrender/termrender.js +6 -37
- package/dist/core/user-settings.d.ts +17 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/daemon/api/handlers/human.js +1 -1
- package/dist/daemon/api/handlers/inbox.js +22 -7
- package/dist/daemon/cron-run.js +16 -2
- package/dist/daemon/crtrd.js +1 -1
- package/dist/daemon/fleet.d.ts +22 -10
- package/dist/daemon/fleet.js +43 -24
- package/dist/daemon/human/finish.d.ts +2 -1
- package/dist/daemon/human/finish.js +45 -28
- package/dist/daemon/human/sweep.js +4 -2
- package/dist/daemon/reconcilers/broker-supervision.js +11 -8
- package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
- package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
- package/dist/daemon/reconcilers/live-obligation.js +30 -2
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
- package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
- package/dist/daemon/review/sweep.js +12 -1
- package/dist/pages/bundle.css +1 -1
- package/dist/pages/bundle.js +1008 -577
- package/dist/pages/comments.d.ts +29 -0
- package/dist/pages/comments.js +51 -0
- package/dist/pages/controls.d.ts +98 -0
- package/dist/pages/controls.js +261 -0
- package/dist/pages/elements/cards.d.ts +15 -4
- package/dist/pages/elements/cards.js +299 -186
- package/dist/pages/elements/chart.d.ts +22 -0
- package/dist/pages/elements/chart.js +107 -91
- package/dist/pages/elements/options.d.ts +9 -0
- package/dist/pages/elements/options.js +268 -155
- package/dist/pages/elements/pages.d.ts +6 -0
- package/dist/pages/elements/pages.js +60 -38
- package/dist/pages/elements/slot.d.ts +5 -0
- package/dist/pages/elements/slot.js +17 -10
- package/dist/pages/elements/table.d.ts +17 -0
- package/dist/pages/elements/table.js +336 -228
- package/dist/pages/elements/text.d.ts +9 -3
- package/dist/pages/elements/text.js +350 -143
- package/dist/pages/host.d.ts +0 -24
- package/dist/pages/host.js +0 -17
- package/dist/pages/readonly.d.ts +24 -0
- package/dist/pages/readonly.js +31 -0
- package/dist/pages/responses.d.ts +37 -0
- package/dist/pages/responses.js +56 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
- package/dist/pi-extensions/canvas-bash-valve.js +12 -8
- package/dist/types.d.ts +19 -5
- package/dist/types.js +2 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/scripts/install-runtime.mjs +19 -24
- package/dist/clients/attach/render/html-markdown.d.ts +0 -13
- package/dist/clients/attach/render/html-markdown.js +0 -206
- package/dist/core/human/markdown-html.d.ts +0 -8
- package/dist/core/human/markdown-html.js +0 -581
package/dist/pages/comments.d.ts
CHANGED
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
* `respond()`.
|
|
13
13
|
*/
|
|
14
14
|
import type { Comment, CommentAnchor } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Whether this slot can record a comment at all.
|
|
17
|
+
*
|
|
18
|
+
* Comments ride the slot's response: `respond(slotId, …)` is the only way one leaves the
|
|
19
|
+
* frame, so a slot with no `slot-id` has nowhere to put it. That is a real page shape — a
|
|
20
|
+
* table with no row or column selection is display-only and the renderer gives it no id
|
|
21
|
+
* (`isResponseBearingSlot`), and a hand-authored html page can omit the attribute on any
|
|
22
|
+
* element — and it used to render the full comment affordance anyway: the trigger opened, the
|
|
23
|
+
* box took the words, Save closed it, and the comment was dropped on the floor. An affordance
|
|
24
|
+
* that cannot keep what it accepts should not be drawn, so the elements build no comment UI
|
|
25
|
+
* when this is false.
|
|
26
|
+
*/
|
|
27
|
+
export declare function canRecordComments(slotId: string | undefined): boolean;
|
|
15
28
|
/** A stable string identity for an anchor, used to find and key comments. */
|
|
16
29
|
export declare function anchorKey(anchor: CommentAnchor): string;
|
|
17
30
|
/** The comment on `anchor`, if the user has left one. */
|
|
@@ -39,5 +52,21 @@ export declare function adoptComments(value: unknown): Comment[];
|
|
|
39
52
|
* legible in a response the human reads.
|
|
40
53
|
*/
|
|
41
54
|
export declare function newCommentId(): string;
|
|
55
|
+
/**
|
|
56
|
+
* The quoted region as a composer shows it back: what you selected, at a glance, in about two
|
|
57
|
+
* lines however much you selected.
|
|
58
|
+
*
|
|
59
|
+
* A range anchor carries the selection verbatim, and a selection is as long as the person made
|
|
60
|
+
* it — a whole paragraph is a normal thing to comment on. Rendered verbatim that quote pushed
|
|
61
|
+
* the box and the save button off the bottom of the popover, so the thing being written could
|
|
62
|
+
* not be seen while writing it. The preview keeps both ends — the beginning says what was
|
|
63
|
+
* selected and the end says where it stopped, which is exactly what a person checks — and
|
|
64
|
+
* elides the middle. Line breaks collapse, because a preview is one run of text and a quote
|
|
65
|
+
* that keeps its paragraph shape is the blowout again in another form.
|
|
66
|
+
*
|
|
67
|
+
* The comment itself is still anchored to the full range: this is what is shown, never what is
|
|
68
|
+
* stored.
|
|
69
|
+
*/
|
|
70
|
+
export declare function quotePreview(quote: string): string;
|
|
42
71
|
/** A short, human label for what a comment is attached to. */
|
|
43
72
|
export declare function anchorLabel(anchor: CommentAnchor): string;
|
package/dist/pages/comments.js
CHANGED
|
@@ -11,6 +11,21 @@
|
|
|
11
11
|
* list in local state, replace it with the returned one, and hand the whole response to
|
|
12
12
|
* `respond()`.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Whether this slot can record a comment at all.
|
|
16
|
+
*
|
|
17
|
+
* Comments ride the slot's response: `respond(slotId, …)` is the only way one leaves the
|
|
18
|
+
* frame, so a slot with no `slot-id` has nowhere to put it. That is a real page shape — a
|
|
19
|
+
* table with no row or column selection is display-only and the renderer gives it no id
|
|
20
|
+
* (`isResponseBearingSlot`), and a hand-authored html page can omit the attribute on any
|
|
21
|
+
* element — and it used to render the full comment affordance anyway: the trigger opened, the
|
|
22
|
+
* box took the words, Save closed it, and the comment was dropped on the floor. An affordance
|
|
23
|
+
* that cannot keep what it accepts should not be drawn, so the elements build no comment UI
|
|
24
|
+
* when this is false.
|
|
25
|
+
*/
|
|
26
|
+
export function canRecordComments(slotId) {
|
|
27
|
+
return slotId !== undefined;
|
|
28
|
+
}
|
|
14
29
|
/** A stable string identity for an anchor, used to find and key comments. */
|
|
15
30
|
export function anchorKey(anchor) {
|
|
16
31
|
switch (anchor.kind) {
|
|
@@ -112,6 +127,42 @@ export function newCommentId() {
|
|
|
112
127
|
counter += 1;
|
|
113
128
|
return `c${counter}`;
|
|
114
129
|
}
|
|
130
|
+
/** How much of a quoted region a preview shows before it is worth eliding. Two lines in a
|
|
131
|
+
* 20em popover at the quote's own size, which is the whole budget the composer can spare. */
|
|
132
|
+
const QUOTE_BUDGET = 84;
|
|
133
|
+
const QUOTE_HEAD = 48;
|
|
134
|
+
const QUOTE_TAIL = 30;
|
|
135
|
+
/** Drop a trailing part-word, so a head cut lands between words rather than inside one. */
|
|
136
|
+
function headWords(text) {
|
|
137
|
+
const cut = text.lastIndexOf(' ');
|
|
138
|
+
return (cut > 0 && cut >= text.length - 12 ? text.slice(0, cut) : text).trimEnd();
|
|
139
|
+
}
|
|
140
|
+
/** Drop a leading part-word, the same way, at the other end of the elision. */
|
|
141
|
+
function tailWords(text) {
|
|
142
|
+
const cut = text.indexOf(' ');
|
|
143
|
+
return (cut !== -1 && cut < 12 ? text.slice(cut + 1) : text).trimStart();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The quoted region as a composer shows it back: what you selected, at a glance, in about two
|
|
147
|
+
* lines however much you selected.
|
|
148
|
+
*
|
|
149
|
+
* A range anchor carries the selection verbatim, and a selection is as long as the person made
|
|
150
|
+
* it — a whole paragraph is a normal thing to comment on. Rendered verbatim that quote pushed
|
|
151
|
+
* the box and the save button off the bottom of the popover, so the thing being written could
|
|
152
|
+
* not be seen while writing it. The preview keeps both ends — the beginning says what was
|
|
153
|
+
* selected and the end says where it stopped, which is exactly what a person checks — and
|
|
154
|
+
* elides the middle. Line breaks collapse, because a preview is one run of text and a quote
|
|
155
|
+
* that keeps its paragraph shape is the blowout again in another form.
|
|
156
|
+
*
|
|
157
|
+
* The comment itself is still anchored to the full range: this is what is shown, never what is
|
|
158
|
+
* stored.
|
|
159
|
+
*/
|
|
160
|
+
export function quotePreview(quote) {
|
|
161
|
+
const flat = quote.replace(/\s+/gu, ' ').trim();
|
|
162
|
+
if (flat.length <= QUOTE_BUDGET)
|
|
163
|
+
return flat;
|
|
164
|
+
return `${headWords(flat.slice(0, QUOTE_HEAD))} \u2026 ${tailWords(flat.slice(-QUOTE_TAIL))}`;
|
|
165
|
+
}
|
|
115
166
|
/** A short, human label for what a comment is attached to. */
|
|
116
167
|
export function anchorLabel(anchor) {
|
|
117
168
|
switch (anchor.kind) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared control paint: the one focus treatment every focusable thing in the page bundle
|
|
3
|
+
* wears, the one radio and checkbox recipe every element draws with, and the one writing-box
|
|
4
|
+
* recipe every textarea and text field wears.
|
|
5
|
+
*
|
|
6
|
+
* All three exist for the same reason — a control that differs element to element reads as
|
|
7
|
+
* foreign, and the host's kit draws each of them exactly one way.
|
|
8
|
+
*
|
|
9
|
+
* A page frame is a bare document rendered beside a host's own UI, and the host's controls are
|
|
10
|
+
* drawn ones — a bordered box with an accent fill and a tick, a ring with a dot. A native UA
|
|
11
|
+
* control cannot be made to look like that: `accent-color` only tints the UA's own drawing, so
|
|
12
|
+
* the frame's radios kept the platform's shape, size, checked treatment and focus ring while
|
|
13
|
+
* everything around them wore the host's. They read as foreign, because they were.
|
|
14
|
+
*
|
|
15
|
+
* So the controls are drawn here instead, out of the same `--crtr-*` tokens the rest of the
|
|
16
|
+
* bundle spends. They stay real `<input type="radio">` / `<input type="checkbox">` elements —
|
|
17
|
+
* the label association, the arrow-key behaviour of a native radio group, Space, the `change`
|
|
18
|
+
* event and every `target.type` check keep working exactly as before. Only the paint moved.
|
|
19
|
+
*
|
|
20
|
+
* The mark is a `::before` on the input, which is why `display: inline-grid` is on the control:
|
|
21
|
+
* an `appearance: none` input has no UA-drawn glyph left and no content of its own, so the grid
|
|
22
|
+
* exists purely to centre the one pseudo-element in it.
|
|
23
|
+
*
|
|
24
|
+
* ## This module is the `--crtr-control-*` family
|
|
25
|
+
*
|
|
26
|
+
* Every value these three recipes paint with is read through a named component token, so a
|
|
27
|
+
* host restyles the bundle's controls without touching crtr — custom properties inherit
|
|
28
|
+
* through shadow roots, which is the whole mechanism. The short version of the layering rule
|
|
29
|
+
* is:
|
|
30
|
+
*
|
|
31
|
+
* var(--crtr-<element>-<knob>, var(--crtr-control-<part>-<knob>, <kit default>))
|
|
32
|
+
*
|
|
33
|
+
* The element layer is the optional `{ scope }` first argument each function takes. A call
|
|
34
|
+
* that passes `{ scope: 'text-editor' }` gets `--crtr-text-editor-padding` laid over the
|
|
35
|
+
* family's `--crtr-control-writing-padding`; a call that passes no scope reads the family
|
|
36
|
+
* token alone. Every default stays inline here — the family tokens are deliberately NOT
|
|
37
|
+
* declared in `bundle.css`, because an undeclared token is what lets the fallback chain carry
|
|
38
|
+
* the kit default all the way down.
|
|
39
|
+
*
|
|
40
|
+
* The innermost fallback of each chain is a theme token read with a CSS system colour behind
|
|
41
|
+
* it, because these rules live in the element's own shadow root and therefore apply even when
|
|
42
|
+
* `bundle.css` — where the whole `--crtr-*` theme map is declared — never loaded. That way an
|
|
43
|
+
* unstyled frame still shows a control with a visible box, a visible mark and a visible focus
|
|
44
|
+
* ring rather than a blank square.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* The element-level token layer a call site lays over the shared family.
|
|
48
|
+
*
|
|
49
|
+
* `scope` is the `--crtr-`-suffix an element claims for these particular controls, so
|
|
50
|
+
* `{ scope: 'text-editor' }` mints `--crtr-text-editor-<knob>`. It is the element's name plus
|
|
51
|
+
* the anatomy part it is styling, and it is the element's to choose: the family part name
|
|
52
|
+
* (`writing`, `box`, `radio`, `checkbox`, `focus`, `mark`) describes the shared recipe, while
|
|
53
|
+
* the scope describes the thing on the page a person is looking at.
|
|
54
|
+
*/
|
|
55
|
+
export interface TokenScope {
|
|
56
|
+
readonly scope: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The focus treatment bound to the selectors of things that are neither a writing box nor a
|
|
60
|
+
* drawn control — every button in the bundle. `writingBoxStyle` and `controlStyle` already
|
|
61
|
+
* carry it, so they do not need this.
|
|
62
|
+
*
|
|
63
|
+
* The ring is outset paint, so a button close enough to the edge of the page frame to have the
|
|
64
|
+
* ring clipped needs a gutter around it, the way `<crtr-text>`'s writing surface does.
|
|
65
|
+
*/
|
|
66
|
+
export declare function focusRingStyle(...selectors: readonly string[]): string;
|
|
67
|
+
export declare function focusRingStyle(scope: TokenScope, ...selectors: readonly string[]): string;
|
|
68
|
+
/**
|
|
69
|
+
* The one writing-box recipe — every `<textarea>` and single-line `<input type="text">` the
|
|
70
|
+
* bundle puts in front of a person paints with this.
|
|
71
|
+
*
|
|
72
|
+
* It is the host kit's own field, translated token for token from
|
|
73
|
+
* `apps/gateway/components/ui/textarea.tsx` — which the kit's `input.tsx` matches declaration
|
|
74
|
+
* for declaration, because in the kit a one-line field and a writing surface are the same
|
|
75
|
+
* control at two heights: text at the body size and body weight (`font: inherit`, which against
|
|
76
|
+
* `--crtr-font-size` is the kit's `md:text-sm`), `px-3 py-2`, a hairline border at the small
|
|
77
|
+
* radius, the faintest fill against the page, `shadow-xs`, and the shared focus ring.
|
|
78
|
+
*
|
|
79
|
+
* Before this existed each element wrote its own comment box, and they had drifted: three
|
|
80
|
+
* different paddings, two different fills, and an `outline` focus ring where the kit uses a
|
|
81
|
+
* border-plus-ring. A comment box has to read as the same thing wherever a person meets it, so
|
|
82
|
+
* the recipe is shared rather than repeated — and `<crtr-text>`'s one-line field and its
|
|
83
|
+
* textarea are one call with two selectors, so they cannot drift apart again.
|
|
84
|
+
*
|
|
85
|
+
* Sizing that belongs to one box — how tall it starts, whether it may be dragged taller — stays
|
|
86
|
+
* in the element's own rule, which follows this block and wins at equal specificity.
|
|
87
|
+
*/
|
|
88
|
+
export declare function writingBoxStyle(...selectors: readonly string[]): string;
|
|
89
|
+
export declare function writingBoxStyle(scope: TokenScope, ...selectors: readonly string[]): string;
|
|
90
|
+
/**
|
|
91
|
+
* The recipe, bound to the `part` selectors an element gives its controls.
|
|
92
|
+
*
|
|
93
|
+
* The result is interpolated into that element's shadow-root stylesheet. Anything an element
|
|
94
|
+
* owns rather than shares — where the control sits against its label — stays in the element's
|
|
95
|
+
* own rule, which follows this block and therefore wins at equal specificity.
|
|
96
|
+
*/
|
|
97
|
+
export declare function controlStyle(...selectors: readonly string[]): string;
|
|
98
|
+
export declare function controlStyle(scope: TokenScope, ...selectors: readonly string[]): string;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared control paint: the one focus treatment every focusable thing in the page bundle
|
|
3
|
+
* wears, the one radio and checkbox recipe every element draws with, and the one writing-box
|
|
4
|
+
* recipe every textarea and text field wears.
|
|
5
|
+
*
|
|
6
|
+
* All three exist for the same reason — a control that differs element to element reads as
|
|
7
|
+
* foreign, and the host's kit draws each of them exactly one way.
|
|
8
|
+
*
|
|
9
|
+
* A page frame is a bare document rendered beside a host's own UI, and the host's controls are
|
|
10
|
+
* drawn ones — a bordered box with an accent fill and a tick, a ring with a dot. A native UA
|
|
11
|
+
* control cannot be made to look like that: `accent-color` only tints the UA's own drawing, so
|
|
12
|
+
* the frame's radios kept the platform's shape, size, checked treatment and focus ring while
|
|
13
|
+
* everything around them wore the host's. They read as foreign, because they were.
|
|
14
|
+
*
|
|
15
|
+
* So the controls are drawn here instead, out of the same `--crtr-*` tokens the rest of the
|
|
16
|
+
* bundle spends. They stay real `<input type="radio">` / `<input type="checkbox">` elements —
|
|
17
|
+
* the label association, the arrow-key behaviour of a native radio group, Space, the `change`
|
|
18
|
+
* event and every `target.type` check keep working exactly as before. Only the paint moved.
|
|
19
|
+
*
|
|
20
|
+
* The mark is a `::before` on the input, which is why `display: inline-grid` is on the control:
|
|
21
|
+
* an `appearance: none` input has no UA-drawn glyph left and no content of its own, so the grid
|
|
22
|
+
* exists purely to centre the one pseudo-element in it.
|
|
23
|
+
*
|
|
24
|
+
* ## This module is the `--crtr-control-*` family
|
|
25
|
+
*
|
|
26
|
+
* Every value these three recipes paint with is read through a named component token, so a
|
|
27
|
+
* host restyles the bundle's controls without touching crtr — custom properties inherit
|
|
28
|
+
* through shadow roots, which is the whole mechanism. The short version of the layering rule
|
|
29
|
+
* is:
|
|
30
|
+
*
|
|
31
|
+
* var(--crtr-<element>-<knob>, var(--crtr-control-<part>-<knob>, <kit default>))
|
|
32
|
+
*
|
|
33
|
+
* The element layer is the optional `{ scope }` first argument each function takes. A call
|
|
34
|
+
* that passes `{ scope: 'text-editor' }` gets `--crtr-text-editor-padding` laid over the
|
|
35
|
+
* family's `--crtr-control-writing-padding`; a call that passes no scope reads the family
|
|
36
|
+
* token alone. Every default stays inline here — the family tokens are deliberately NOT
|
|
37
|
+
* declared in `bundle.css`, because an undeclared token is what lets the fallback chain carry
|
|
38
|
+
* the kit default all the way down.
|
|
39
|
+
*
|
|
40
|
+
* The innermost fallback of each chain is a theme token read with a CSS system colour behind
|
|
41
|
+
* it, because these rules live in the element's own shadow root and therefore apply even when
|
|
42
|
+
* `bundle.css` — where the whole `--crtr-*` theme map is declared — never loaded. That way an
|
|
43
|
+
* unstyled frame still shows a control with a visible box, a visible mark and a visible focus
|
|
44
|
+
* ring rather than a blank square.
|
|
45
|
+
*/
|
|
46
|
+
/** Lucide's `check`, as a mask. Colour comes from the pseudo-element, so the paint is opaque
|
|
47
|
+
* black and the geometry — including the 2-unit round-capped stroke — is the kit's own. */
|
|
48
|
+
const CHECK_MASK = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E\")";
|
|
49
|
+
/** Lucide's `minus` — the glyph a partial selection wears, so "some" cannot read as "none". */
|
|
50
|
+
const MINUS_MASK = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E\")";
|
|
51
|
+
/** The selector list, each entry suffixed and joined the way a CSS rule wants it. */
|
|
52
|
+
function each(selectors, suffix = '') {
|
|
53
|
+
return selectors.map((selector) => `${selector}${suffix}`).join(',\n ');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Splits the optional leading scope off a variadic selector list.
|
|
57
|
+
*
|
|
58
|
+
* The scope is a leading argument rather than a trailing options object because the selector
|
|
59
|
+
* list is variadic and reads as one thought; an element file's call stays a single line either
|
|
60
|
+
* way.
|
|
61
|
+
*/
|
|
62
|
+
function split(args) {
|
|
63
|
+
const first = args[0];
|
|
64
|
+
if (typeof first === 'object')
|
|
65
|
+
return [first.scope, args.slice(1)];
|
|
66
|
+
return [null, args];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A chain builder for one family part: given a knob name and the kit's current value for it,
|
|
70
|
+
* it returns the `var()` chain a declaration is written through.
|
|
71
|
+
*
|
|
72
|
+
* `elementPart` is what the element layer prefixes the knob with. It is empty only for the one
|
|
73
|
+
* family per call that owns the plain knob names, and set on every other family sharing that
|
|
74
|
+
* call — two families that both spend `fill` or `mark-color` would otherwise collapse onto a
|
|
75
|
+
* single element token, and overriding the box's fill would repaint the mark inside it.
|
|
76
|
+
*/
|
|
77
|
+
function chain(scope, familyPrefix, elementPart) {
|
|
78
|
+
return (name, fallback) => {
|
|
79
|
+
const family = `var(--crtr-${familyPrefix}-${name}, ${fallback})`;
|
|
80
|
+
if (scope === null)
|
|
81
|
+
return family;
|
|
82
|
+
const element = elementPart === '' ? `${scope}-${name}` : `${scope}-${elementPart}-${name}`;
|
|
83
|
+
return `var(--crtr-${element}, ${family})`;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The one focus treatment, and the only one the bundle draws.
|
|
88
|
+
*
|
|
89
|
+
* It is the kit's, verbatim: the border takes the focus colour and a soft ring sits just
|
|
90
|
+
* outside it — `focus-visible:border-aurora-cyan/60 focus-visible:ring-aurora-cyan/25
|
|
91
|
+
* focus-visible:ring-[3px]`, which `apps/gateway/components/ui/{input,textarea,button,
|
|
92
|
+
* checkbox}.tsx` all carry identically. A UA `outline` is never it; every element in this
|
|
93
|
+
* bundle wrote its own `outline: 2px solid` before this existed, so a focused button and the
|
|
94
|
+
* focused field under it drew two visibly different rings.
|
|
95
|
+
*
|
|
96
|
+
* It is one declaration block rather than a ring plus a resting elevation, so a missing shadow
|
|
97
|
+
* token can never take the ring down with it.
|
|
98
|
+
*
|
|
99
|
+
* The ring width is a flat three pixels on every focusable thing the kit draws — a button, a
|
|
100
|
+
* field, a checkbox — so its default is in `rem` rather than `em`: an `em` ring would come out
|
|
101
|
+
* a different thickness on a button set at `0.875em` than on the field beside it, which is the
|
|
102
|
+
* drift this module exists to remove.
|
|
103
|
+
*
|
|
104
|
+
* A focusable region that is not a control — a scrolling thread, a table body, a prose block —
|
|
105
|
+
* is deliberately NOT in this family. Those run the full width of the frame the host gives the
|
|
106
|
+
* page, so a ring painted outside them is clipped and draws three sides of a rectangle; they
|
|
107
|
+
* keep an inset `outline` and say so at the rule.
|
|
108
|
+
*/
|
|
109
|
+
function focusRing(scope) {
|
|
110
|
+
const focus = chain(scope, 'control-focus', 'focus');
|
|
111
|
+
const width = focus('ring-width', '0.1875rem');
|
|
112
|
+
const color = focus('ring-color', 'color-mix(in srgb, var(--crtr-color-focus, CanvasText) 25%, transparent)');
|
|
113
|
+
return `
|
|
114
|
+
outline: ${focus('outline', 'none')};
|
|
115
|
+
border-color: ${focus('border-color', 'color-mix(in srgb, var(--crtr-color-focus, CanvasText) 60%, transparent)')};
|
|
116
|
+
box-shadow: ${focus('ring', `0 0 0 ${width}\n ${color}`)};`;
|
|
117
|
+
}
|
|
118
|
+
export function focusRingStyle(...args) {
|
|
119
|
+
const [scope, selectors] = split(args);
|
|
120
|
+
return `
|
|
121
|
+
${each(selectors, ':focus-visible')} {${focusRing(scope)}
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
export function writingBoxStyle(...args) {
|
|
126
|
+
const [scope, selectors] = split(args);
|
|
127
|
+
const box = chain(scope, 'control-writing', '');
|
|
128
|
+
const any = chain(scope, 'control', '');
|
|
129
|
+
const motion = any('transition', '120ms ease');
|
|
130
|
+
return `
|
|
131
|
+
${each(selectors)} {
|
|
132
|
+
/* Block, not the UA's inline-block: a full-width field on its own line would otherwise sit
|
|
133
|
+
in a line box and carry a descender's worth of dead space under it. */
|
|
134
|
+
display: block;
|
|
135
|
+
box-sizing: border-box;
|
|
136
|
+
width: 100%;
|
|
137
|
+
padding: ${box('padding', 'var(--crtr-space-2) var(--crtr-space-3)')};
|
|
138
|
+
color: ${box('text-color', 'var(--crtr-color-text, CanvasText)')};
|
|
139
|
+
font: ${box('font', 'inherit')};
|
|
140
|
+
line-height: ${box('line-height', 'var(--crtr-line-height, 1.5)')};
|
|
141
|
+
background: ${box('fill', 'color-mix(in srgb, var(--crtr-color-text, CanvasText) 2%, var(--crtr-color-background, Canvas))')};
|
|
142
|
+
border: ${box('border-width', '1px')} ${box('border-style', 'solid')} ${box('border-color', 'var(--crtr-color-border, CanvasText)')};
|
|
143
|
+
border-radius: ${box('radius', 'var(--crtr-radius-small)')};
|
|
144
|
+
box-shadow: ${box('shadow', 'var(--crtr-shadow)')};
|
|
145
|
+
transition: border-color ${motion}, box-shadow ${motion};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
${each(selectors, '::placeholder')} {
|
|
149
|
+
color: ${box('placeholder-color', 'var(--crtr-color-text-muted, GrayText)')};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
${each(selectors, ':focus-visible')} {${focusRing(scope)}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* A page that can no longer take an answer keeps its text selectable and copyable, and drops
|
|
156
|
+
the affordances that say it could still be rewritten. */
|
|
157
|
+
${each(selectors, ':read-only')} {
|
|
158
|
+
background: ${box('readonly-fill', 'var(--crtr-color-surface, Canvas)')};
|
|
159
|
+
box-shadow: ${box('readonly-shadow', 'none')};
|
|
160
|
+
resize: none;
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
export function controlStyle(...args) {
|
|
165
|
+
const [scope, selectors] = split(args);
|
|
166
|
+
const box = chain(scope, 'control-box', '');
|
|
167
|
+
const mark = chain(scope, 'control-mark', 'mark');
|
|
168
|
+
const radio = chain(scope, 'control-radio', 'radio');
|
|
169
|
+
const check = chain(scope, 'control-checkbox', 'checkbox');
|
|
170
|
+
const any = chain(scope, 'control', '');
|
|
171
|
+
const motion = any('transition', '120ms ease');
|
|
172
|
+
return `
|
|
173
|
+
${each(selectors)} {
|
|
174
|
+
/* The box tracks the host's font token like every other size in the bundle. The kit's
|
|
175
|
+
control is 16px against 14px text; these ratios are that pair, kept proportional. */
|
|
176
|
+
--control-box: ${box('size', '1.125em')};
|
|
177
|
+
appearance: none;
|
|
178
|
+
-webkit-appearance: none;
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
display: inline-grid;
|
|
181
|
+
place-content: center;
|
|
182
|
+
flex: none;
|
|
183
|
+
width: var(--control-box);
|
|
184
|
+
height: var(--control-box);
|
|
185
|
+
margin: 0;
|
|
186
|
+
padding: 0;
|
|
187
|
+
vertical-align: middle;
|
|
188
|
+
/* Transparent rather than the background token, so an empty control on a selected row
|
|
189
|
+
carries that row's tint the way the kit's does. */
|
|
190
|
+
background: ${box('fill', 'transparent')};
|
|
191
|
+
border: ${box('border-width', '1px')} ${box('border-style', 'solid')} ${box('border-color', 'var(--crtr-color-border, CanvasText)')};
|
|
192
|
+
border-radius: ${box('radius', 'calc(var(--control-box) * 0.25)')};
|
|
193
|
+
box-shadow: ${box('shadow', 'var(--crtr-shadow)')};
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
transition: background-color ${motion}, border-color ${motion}, box-shadow ${motion};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
${each(selectors, '[type="radio"]')} {
|
|
199
|
+
border-radius: ${radio('radius', '50%')};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* The mark. It is always present and always centred; selecting scales it in, which is the
|
|
203
|
+
state change the kit animates too. */
|
|
204
|
+
${each(selectors, '::before')} {
|
|
205
|
+
content: "";
|
|
206
|
+
background: ${mark('fill', 'currentColor')};
|
|
207
|
+
transform: scale(0);
|
|
208
|
+
transition: transform ${motion};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
${each(selectors, ':checked::before')},
|
|
212
|
+
${each(selectors, ':indeterminate::before')} {
|
|
213
|
+
transform: scale(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* A radio keeps its own surface and marks itself with a dot in the accent; a checkbox fills
|
|
217
|
+
with the accent and carries the tick in the accent's contrast colour. */
|
|
218
|
+
${each(selectors, '[type="radio"]::before')} {
|
|
219
|
+
width: ${radio('mark-size', 'calc(var(--control-box) * 0.5)')};
|
|
220
|
+
height: ${radio('mark-size', 'calc(var(--control-box) * 0.5)')};
|
|
221
|
+
border-radius: ${radio('mark-radius', '50%')};
|
|
222
|
+
color: ${radio('mark-color', 'var(--crtr-color-accent, CanvasText)')};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
${each(selectors, '[type="radio"]:checked')} {
|
|
226
|
+
border-color: ${radio('checked-border-color', 'var(--crtr-color-accent, CanvasText)')};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
${each(selectors, '[type="checkbox"]::before')} {
|
|
230
|
+
width: ${check('mark-size', 'calc(var(--control-box) * 0.875)')};
|
|
231
|
+
height: ${check('mark-size', 'calc(var(--control-box) * 0.875)')};
|
|
232
|
+
color: ${check('mark-color', 'var(--crtr-color-accent-contrast, Canvas)')};
|
|
233
|
+
-webkit-mask: ${check('mark-image', CHECK_MASK)} ${check('mark-layout', 'center / contain no-repeat')};
|
|
234
|
+
mask: ${check('mark-image', CHECK_MASK)} ${check('mark-layout', 'center / contain no-repeat')};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
${each(selectors, '[type="checkbox"]:indeterminate::before')} {
|
|
238
|
+
-webkit-mask-image: ${check('indeterminate-mark-image', MINUS_MASK)};
|
|
239
|
+
mask-image: ${check('indeterminate-mark-image', MINUS_MASK)};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
${each(selectors, '[type="checkbox"]:checked')},
|
|
243
|
+
${each(selectors, '[type="checkbox"]:indeterminate')} {
|
|
244
|
+
background: ${check('checked-fill', 'var(--crtr-color-accent, CanvasText)')};
|
|
245
|
+
border-color: ${check('checked-border-color', 'var(--crtr-color-accent, CanvasText)')};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* The bundle's one focus treatment. The box scales with the font token, but the ring does
|
|
249
|
+
not: the kit's is a flat three pixels on a checkbox and on the field beside it alike. */
|
|
250
|
+
${each(selectors, '[type]:focus-visible')} {${focusRing(scope)}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* A page that can no longer take an answer keeps its selection legible — the mark stays the
|
|
254
|
+
accent, only dimmed — and drops every affordance that says it could still be changed. */
|
|
255
|
+
${each(selectors, ':disabled')} {
|
|
256
|
+
opacity: ${any('disabled-opacity', '0.5')};
|
|
257
|
+
box-shadow: ${any('disabled-shadow', 'none')};
|
|
258
|
+
cursor: default;
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
@@ -9,9 +9,20 @@
|
|
|
9
9
|
* (`j`/`k` move, `x` selects, `c` comments), so a card grid, an option list and a table
|
|
10
10
|
* are all driven the same way.
|
|
11
11
|
*
|
|
12
|
-
* Data arrives
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Data arrives inline as `cards`. The element never touches the network: a page source file
|
|
13
|
+
* is read and embedded once at submit, before this element renders.
|
|
14
|
+
*
|
|
15
|
+
* ## Every value here is a `--crtr-cards-*` token
|
|
16
|
+
*
|
|
17
|
+
* The card surface, its heading, its tag, the comment affordance and the three notice states
|
|
18
|
+
* are all painted through `var(--crtr-cards-<part>-<knob>, <kit default>)`, so a host restyles
|
|
19
|
+
* this element by setting properties on the document root — custom properties inherit into
|
|
20
|
+
* shadow roots, which is the whole mechanism. The shared control, writing-box and focus
|
|
21
|
+
* recipes carry their own element layer through the `{ scope }` each call passes, so the
|
|
22
|
+
* card's radio reads `--crtr-cards-control-*` over the family's `--crtr-control-box-*`.
|
|
23
|
+
*
|
|
24
|
+
* Defaults stay inline at the rule that spends them and no `--crtr-cards-*` token is ever
|
|
25
|
+
* declared: an unset token is what carries the kit default down the chain, which is why an
|
|
26
|
+
* unstyled page renders exactly as it did before any of this existed.
|
|
16
27
|
*/
|
|
17
28
|
export {};
|