@qxuken/kui 0.0.0-stage → 0.1.0-alpha.35
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/CHANGELOG.md +13313 -0
- package/LICENSE +21 -0
- package/README.md +336 -3
- package/docs/adr/0001-accessibility-as-data.md +414 -0
- package/docs/adr/0002-keyboard-focus-as-data.md +452 -0
- package/docs/adr/0003-modal-surfaces.md +248 -0
- package/docs/adr/0004-multi-window.md +626 -0
- package/docs/adr/0005-the-paint-vocabulary.md +601 -0
- package/docs/adr/0006-c-abi-versioning.md +257 -0
- package/docs/adr/0007-composite-keyboard-patterns.md +350 -0
- package/docs/adr/0008-live-regions-and-announcements.md +379 -0
- package/docs/adr/0009-press-drag-release-into-a-popup.md +318 -0
- package/docs/adr/0010-a-segment-primitive.md +410 -0
- package/docs/adr/0011-keys-bubble-to-the-enclosing-sink.md +258 -0
- package/docs/adr/0012-the-exit-budget.md +411 -0
- package/docs/adr/0013-effects-as-data.md +197 -0
- package/docs/adr/0014-slots-an-extension-fills-in-place.md +411 -0
- package/docs/adr/0015-a-fragment-element-and-the-painter-it-is-not.md +559 -0
- package/docs/adr/0016-caching-against-the-last-frame.md +351 -0
- package/docs/adr/0017-selection-as-a-scope.md +696 -0
- package/docs/adr/0018-a-menu-bar-the-app-declares.md +264 -0
- package/docs/adr/0019-a-theme-derived-from-appearance-and-accent.md +382 -0
- package/docs/adr/0020-the-surface-the-schema-does-not-cover.md +351 -0
- package/docs/adr/0021-one-subject-per-example.md +729 -0
- package/docs/adr/0022-focus-regions.md +205 -0
- package/docs/adr/0023-layers-stack-in-the-order-they-open.md +376 -0
- package/docs/adr/0024-the-devtools-are-the-cores.md +425 -0
- package/docs/adr/0025-the-image-is-the-canvas.md +507 -0
- package/docs/adr/0026-hit-testing-by-shape.md +197 -0
- package/docs/adr/0027-tokens-beside-the-theme.md +521 -0
- package/docs/adr/0028-derived-tokens.md +572 -0
- package/docs/adr/0029-a-selection-follows-the-pointer-past-the-edge.md +543 -0
- package/docs/adr/0030-the-standard-menus-the-runner-keeps.md +292 -0
- package/docs/adr/0031-a-drop-zone-is-a-row-and-the-files-are-an-event.md +409 -0
- package/docs/adr/0032-a-devtools-tab-mounts-a-slot.md +480 -0
- package/docs/adr/0033-a-table-is-a-column-whose-cells-align.md +377 -0
- package/docs/adr/0034-stock-controls-over-the-roles.md +192 -0
- package/docs/adr/0035-a-rounded-background-is-joined-by-meeting.md +134 -0
- package/docs/adr/0036-an-event-handler-gets-its-window.md +142 -0
- package/docs/adr/0037-a-family-is-named.md +134 -0
- package/docs/adr/0038-a-scroll-gesture-latches-its-target.md +188 -0
- package/docs/adr/0039-a-tutorial-is-a-sequence.md +139 -0
- package/encoder.js +1282 -0
- package/howto.md +1774 -0
- package/index.d.ts +5162 -0
- package/index.js +1414 -0
- package/jsx-dev-runtime.js +1 -0
- package/jsx-runtime.d.ts +905 -0
- package/jsx-runtime.js +45 -0
- package/native.cjs +118 -0
- package/package.json +51 -6
- package/prebuilds/darwin-arm64/kui_node.node +0 -0
- package/prebuilds/darwin-x64/kui_node.node +0 -0
- package/prebuilds/linux-arm64/kui_node.node +0 -0
- package/prebuilds/linux-x64/kui_node.node +0 -0
- package/prebuilds/win32-x64/kui_node.node +0 -0
- package/props.md +636 -0
package/jsx-runtime.d.ts
ADDED
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
// Types for kui's JSX runtime. Set in tsconfig:
|
|
2
|
+
// "jsx": "react-jsx", "jsxImportSource": "@qxuken/kui"
|
|
3
|
+
|
|
4
|
+
/** Event message payloads: plain data, both directions (the Elm shape).
|
|
5
|
+
* This is the wire type — anything JSON-shaped crosses; an app narrows it
|
|
6
|
+
* to its own union with `KuiMsg` below. */
|
|
7
|
+
export type Msg = null | boolean | number | string | Msg[] | { [key: string]: Msg };
|
|
8
|
+
|
|
9
|
+
/** Declare the app's message type once, and the payload prop (`onClick`),
|
|
10
|
+
* the tag props (`onDrag`, `onHover`, `onKey`, `onLayout`) and `createApp`
|
|
11
|
+
* / `runWindowed` take it instead of "any plain data":
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* type CounterMsg = { kind: 'add'; by: number } | { kind: 'reset' };
|
|
15
|
+
*
|
|
16
|
+
* declare module '@qxuken/kui/jsx-runtime' {
|
|
17
|
+
* interface KuiMsg { msg: CounterMsg }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Register the messages you wrote — not `CounterMsg | CoreMsg`. `CoreMsg`
|
|
22
|
+
* (what the core sends by itself) is typed in terms of this registration:
|
|
23
|
+
* its `tag` fields carry your messages. Naming it here makes the alias
|
|
24
|
+
* refer to itself, and TypeScript reports a circular type. Keep the full
|
|
25
|
+
* union for `update` (`type Msg = CounterMsg | CoreMsg`); the loop types
|
|
26
|
+
* infer it from there.
|
|
27
|
+
*
|
|
28
|
+
* A tag prop also takes `null`: `<box onKey={null} keyFocus>` is a key
|
|
29
|
+
* sink whose events carry no `tag`, so a sink that only needs the node key
|
|
30
|
+
* costs no inert member in the union.
|
|
31
|
+
*
|
|
32
|
+
* A payload typo then fails where it is written rather than in `update`.
|
|
33
|
+
* Left un-augmented, payloads stay `Msg` and nothing changes. (One app per
|
|
34
|
+
* tsconfig is the shape kui already has: one window, one event loop.) */
|
|
35
|
+
export interface KuiMsg {}
|
|
36
|
+
|
|
37
|
+
/** The app's message type: whatever `KuiMsg` was augmented with, else `Msg`. */
|
|
38
|
+
export type AppMsg = KuiMsg extends { msg: infer M } ? M : Msg;
|
|
39
|
+
|
|
40
|
+
export interface KuiElement {
|
|
41
|
+
type: string;
|
|
42
|
+
key?: string;
|
|
43
|
+
props: Record<string, unknown>;
|
|
44
|
+
children: KuiNode[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type KuiNode =
|
|
48
|
+
| KuiElement
|
|
49
|
+
| KuiNode[]
|
|
50
|
+
| string
|
|
51
|
+
| number
|
|
52
|
+
| boolean
|
|
53
|
+
| null
|
|
54
|
+
| undefined;
|
|
55
|
+
|
|
56
|
+
/** A reference to a colour token by name — `'$peach'` — as `defineTokens`
|
|
57
|
+
* types it (`docs/adr/0027-tokens-beside-the-theme.md`). The string is
|
|
58
|
+
* what rides; the brand is what tells a colour's reference from a
|
|
59
|
+
* length's at the type level. Any colour prop takes one. */
|
|
60
|
+
export type ColorToken = `$${string}` & { readonly __kuiToken?: 'color' };
|
|
61
|
+
/** A reference to a length token by name — `'$sideW'` — for any length
|
|
62
|
+
* prop: a size, a pad edge, a radius, a border width, a fixed width or
|
|
63
|
+
* height. Resolved to logical px by the core's table. A colour token in a
|
|
64
|
+
* length slot is a type error here; a length token in a colour slot is
|
|
65
|
+
* caught at encode time instead (`unknown-token`), since `ColorProp`
|
|
66
|
+
* admits any string and narrowing it would refuse every helper that
|
|
67
|
+
* returns one. */
|
|
68
|
+
export type LengthToken = `$${string}` & { readonly __kuiToken?: 'length' };
|
|
69
|
+
/** Logical px, or a length token. */
|
|
70
|
+
export type LengthProp = number | LengthToken;
|
|
71
|
+
|
|
72
|
+
/** A size expression (backlog F109), resolved against the parent's content
|
|
73
|
+
* box — the box a percentage takes its cut of: spelled as CSS spells it
|
|
74
|
+
* (`"clamp(400px, 80%, 1000px)"`, `"min(720px, 100%)"`, nested), or as
|
|
75
|
+
* data, which crosses to the addon as numbers and is never parsed:
|
|
76
|
+
* `{ clamp: [400, "80%", 1000] }`, `{ min: [...] }`, `{ max: [...] }`,
|
|
77
|
+
* `{ percent: 80 }`, `{ px: 12 }`. An expression with no percentage in
|
|
78
|
+
* it is a length. */
|
|
79
|
+
export type SizeExpr =
|
|
80
|
+
| number
|
|
81
|
+
| `${number}%`
|
|
82
|
+
| `${number}px`
|
|
83
|
+
| `min(${string})`
|
|
84
|
+
| `max(${string})`
|
|
85
|
+
| `clamp(${string})`
|
|
86
|
+
| { percent: number }
|
|
87
|
+
| { px: number }
|
|
88
|
+
| { min: SizeExpr[] }
|
|
89
|
+
| { max: SizeExpr[] }
|
|
90
|
+
| { clamp: [SizeExpr, SizeExpr, SizeExpr] };
|
|
91
|
+
|
|
92
|
+
/** number = fixed logical px; "N%" of parent (`{ percent: N }` is the same
|
|
93
|
+
* number); grow soaks up leftover space; a size expression (`SizeExpr`);
|
|
94
|
+
* a length token is a fixed px the core's table resolves. */
|
|
95
|
+
export type SizingProp =
|
|
96
|
+
| 'fit'
|
|
97
|
+
| 'grow'
|
|
98
|
+
| { grow: number }
|
|
99
|
+
| SizeExpr
|
|
100
|
+
| LengthToken;
|
|
101
|
+
|
|
102
|
+
/** A lower clamp: logical px, a size expression, or "fit" for the node's
|
|
103
|
+
* own fit size on that axis — what lets a `grow` child keep a content
|
|
104
|
+
* floor (a tab never narrower than its label). */
|
|
105
|
+
export type MinProp = 'fit' | SizeExpr | LengthToken;
|
|
106
|
+
|
|
107
|
+
/** An upper clamp: logical px or a size expression (`"90%"`, a clamp). */
|
|
108
|
+
export type MaxProp = SizeExpr | LengthToken;
|
|
109
|
+
|
|
110
|
+
/** 0xRRGGBBAA number, "#rgb" / "#rrggbb" / "#rrggbbaa", or a colour
|
|
111
|
+
* token's reference (`'$peach'`, a theme role's `'$surface'`). */
|
|
112
|
+
export type ColorProp = number | string | ColorToken;
|
|
113
|
+
|
|
114
|
+
export type AlignProp = 'start' | 'center' | 'end';
|
|
115
|
+
|
|
116
|
+
/** One CSS-style keyframe stop for `keyframes`. `at` is 0..1 and spreads
|
|
117
|
+
* evenly when omitted (a lone stop sits at 1 and animates from the node's
|
|
118
|
+
* own value); a slot a stop leaves out is left to its neighbours. Sizings
|
|
119
|
+
* animate their amount only, in the form the prop itself declares. */
|
|
120
|
+
export interface KeyframeProp {
|
|
121
|
+
at?: number;
|
|
122
|
+
width?: SizingProp;
|
|
123
|
+
height?: SizingProp;
|
|
124
|
+
bg?: ColorProp;
|
|
125
|
+
radius?: number;
|
|
126
|
+
/** Group opacity, 0..1. */
|
|
127
|
+
opacity?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Where a node starts the first frame it is seen, for `enter`: the slots
|
|
131
|
+
* it names ease in from these values over `transition` ms instead of
|
|
132
|
+
* snapping. `dx`/`dy` are logical px the node slides in from; the rest
|
|
133
|
+
* take the forms the props themselves take. A node that leaves and comes
|
|
134
|
+
* back enters again. */
|
|
135
|
+
export interface EnterProp {
|
|
136
|
+
dx?: number;
|
|
137
|
+
dy?: number;
|
|
138
|
+
width?: SizingProp;
|
|
139
|
+
height?: SizingProp;
|
|
140
|
+
bg?: ColorProp;
|
|
141
|
+
radius?: number;
|
|
142
|
+
/** Group opacity, 0..1: `{ opacity: 0 }` fades the whole subtree in. */
|
|
143
|
+
opacity?: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface FloatProp {
|
|
147
|
+
/** The preset to start from; every key below overrides one of its
|
|
148
|
+
* values and leaving one out keeps the preset's own, so
|
|
149
|
+
* `{ anchor: 'below', dx: 4 }` still hangs below with its 6px gap. */
|
|
150
|
+
anchor?: 'parent' | 'viewport' | 'below' | 'above';
|
|
151
|
+
/** Attach point on the anchor, [x, y]. */
|
|
152
|
+
at?: [AlignProp, AlignProp];
|
|
153
|
+
/** Attach point on the floating node itself, [x, y]. */
|
|
154
|
+
self?: [AlignProp, AlignProp];
|
|
155
|
+
dx?: number;
|
|
156
|
+
dy?: number;
|
|
157
|
+
/** Flip across the anchor / clamp to stay inside the viewport. */
|
|
158
|
+
fit?: boolean;
|
|
159
|
+
/** Take the parent's clip instead of escaping it: a node on a `clip`
|
|
160
|
+
* canvas panned past the canvas's edge is cut there and cannot be hit
|
|
161
|
+
* past it. Read with the `parent` anchor (and `below` / `above`, which
|
|
162
|
+
* anchor to the parent) only; still drawn as a layer over its in-flow
|
|
163
|
+
* siblings. A `line` or `polygon` in its parent's box is always
|
|
164
|
+
* clipped this way. */
|
|
165
|
+
clip?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// -- generated from the core's menu roles; edit MenuRole::ALL in crates/kui-core/src/menu.rs, then `npm run gen` --
|
|
169
|
+
/** What a menu row is: the app's own (`custom`), a divider, or one of
|
|
170
|
+
* the standard rows the core performs itself. The same spelling a
|
|
171
|
+
* `menu` message reports back. */
|
|
172
|
+
export type MenuItemRole =
|
|
173
|
+
| 'custom' | 'separator' | 'cut' | 'copy' | 'paste' | 'selectAll' | 'lookUp';
|
|
174
|
+
// -- end generated --
|
|
175
|
+
|
|
176
|
+
/** One row to put in a context menu (`Ctx.openMenu`). Everything but
|
|
177
|
+
* `label` is optional, and a standard `role` takes its own wording when
|
|
178
|
+
* `label` is empty — so `{ role: 'copy' }` is the platform's Copy.
|
|
179
|
+
* `id` is what the row posts when chosen (its label, when absent). */
|
|
180
|
+
export interface MenuItemInput {
|
|
181
|
+
label?: string;
|
|
182
|
+
role?: MenuItemRole;
|
|
183
|
+
enabled?: boolean;
|
|
184
|
+
/** Draws a checkmark beside the row (and sets the platform's own check
|
|
185
|
+
* state where a host renders the menu): a setting the row *is*, not a
|
|
186
|
+
* command it runs. */
|
|
187
|
+
checked?: boolean;
|
|
188
|
+
id?: unknown;
|
|
189
|
+
/** Display only: the shortcut is the app's or the platform's — except in
|
|
190
|
+
* a menu bar the platform draws, where a spelling kui can parse
|
|
191
|
+
* (`'mod+s'`, `'⌘S'`, `'Ctrl+Shift+P'`) becomes the real key equivalent.
|
|
192
|
+
* A declaration kui can parse is rewritten into the platform's own
|
|
193
|
+
* spelling, so `'mod+s'` reads as `⌘S` on macOS and `Ctrl+S` elsewhere. */
|
|
194
|
+
accel?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** One menu of the application menu bar (the `<menuBar menu={…}/>`
|
|
198
|
+
* element's prop): a title and the rows that drop out of it
|
|
199
|
+
* (`docs/adr/0018-a-menu-bar-the-app-declares.md`). Its rows are the same
|
|
200
|
+
* `MenuItemInput` a context menu takes, so a standard `role` is performed
|
|
201
|
+
* by the core here too — an Edit menu's `{ role: 'copy' }` is the
|
|
202
|
+
* right-click Copy.
|
|
203
|
+
*
|
|
204
|
+
* On macOS the first menu is the application menu, which the OS titles
|
|
205
|
+
* with the app's own name whatever `label` says. */
|
|
206
|
+
export interface MenuInput {
|
|
207
|
+
label: string;
|
|
208
|
+
items: MenuItemInput[];
|
|
209
|
+
/** A disabled menu is dimmed and opens nothing. */
|
|
210
|
+
enabled?: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The whole bar, in order — the `<menuBar menu={…}/>` element's prop.
|
|
214
|
+
* `[]` takes the menu away; a frame that draws no `<menuBar/>` at all
|
|
215
|
+
* leaves the last declaration in force. */
|
|
216
|
+
export type MenuBarInput = MenuInput[];
|
|
217
|
+
|
|
218
|
+
interface Keyed {
|
|
219
|
+
/** Stable identity for retained state (scroll offsets, editors). */
|
|
220
|
+
key?: string | number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// -- generated from the addon's prop schema; edit schema.rs, then `npm run gen` --
|
|
224
|
+
export interface GeneratedSpecProps {
|
|
225
|
+
/** Paint this node's background in the OS accent colour — `env.system.accent` — keeping the declared `bg` on a host that cannot tell what it is. The one prop whose paint depends on the environment, which is why it is opt-in: the same tree is a different colour on two machines, and that is the point here and a surprise anywhere else. On the stock button it does the whole job — the hover and pressed shades are derived from the accent, and the label goes black or white by its luminance, so a yellow accent is still readable — which is what `<button accent>` is for. */
|
|
226
|
+
accent?: boolean;
|
|
227
|
+
/** Scroll anchoring on a scrolling node (backlog C26, CSS's `overflow-anchor`): the first child in view keeps its place on screen when the content before it changes size — a chat that prepends history, a log that inserts rows above the viewport, a list whose row heights are corrected as they are measured — with no `setScroll` and no arithmetic in the view. The core remembers which child was first in view and where its edge was, and moves the offset by however far that edge moved in the next layout, before the offset is clamped; a wheel notch or a `setScroll` between the frames is kept and the correction added to it. The child is found by key, so give the rows stable keys (a `key` or an `index`); a child that is gone anchors nothing that frame. On the scroll axis that is the node's main axis only — `scrollY` on a column, `scrollX` on a row — and content appended *after* the anchor moves nothing, so a log that is tailing still asks for the end itself. */
|
|
228
|
+
anchor?: boolean;
|
|
229
|
+
/** Ask for another frame after this one, every frame this node is declared. What a `fragment` that reads `time` needs, and what anything driving itself off the clock rather than off input needs. Opt-in like `exit`, and for the same reason: it takes the loop off input-driven and onto the display's cadence for as long as it is declared, so a still node must not carry it. One node asking is enough for the whole window. */
|
|
230
|
+
animate?: boolean;
|
|
231
|
+
/** Width over height — `16/9`, `1` for a square — CSS's `aspect-ratio`. It sizes the axis left `fit`: a fit height is the final width over the ratio (so `width: grow` and a ratio is a box that keeps its shape as the window resizes), and a fit width under a fixed height is that height times it. With both axes declared, or a fit width under a `grow` or percent height, it has nothing it can set and warns. The derived axis is neither shrunk nor fitted to the children, which overflow it; `minHeight: 'fit'` floors it at them. On an image it wins over the pixels' own aspect. */
|
|
232
|
+
aspectRatio?: LengthProp;
|
|
233
|
+
/** Background fill. */
|
|
234
|
+
bg?: ColorProp;
|
|
235
|
+
/** How far a spring overshoots its target: 0 glides in with none, 0.5 bounces visibly, and values past 0.9 are held there (a spring at 1 would never settle). It replaces a spring `easing`'s own bounce (`smooth` 0, `snappy` 0.15, `spring` 0.25, `bouncy` 0.5), and on a timed easing makes the transition a spring — so `transition` plus `bounce` is a spring of that length and bounce. */
|
|
236
|
+
bounce?: LengthProp;
|
|
237
|
+
/** Which non-primary buttons `onButton` claims (backlog F105): `"secondary"`, `"middle"` and `"other"` (every button past those), separated by spaces or commas — `"middle"`, `"secondary middle"`. Unset, all three: a node that wants the middle button and leaves the secondary one to its context menu says `"middle"`. A word that is none of the three is skipped, so a string of none of them claims nothing, and a typo never takes the secondary button from a context menu. Meaningless without `onButton`. */
|
|
238
|
+
buttons?: string;
|
|
239
|
+
/** On a `line` of a custom editor (a `textInput` / `multilineTextInput` role drawn by the app): the caret's byte offset into that line's text. */
|
|
240
|
+
caret?: LengthProp;
|
|
241
|
+
/** On a `line` declaring `caret`: the caret is solid — a block caret in a modal editor's normal mode — so the driver's blink clock is not armed on it and `caretVisible` stays true, while the offset still anchors the IME and reads to assistive technology. Without it a declared `caret` is a caret to blink, and the one thing that asks an idle app for a frame twice a second; an editor whose caret only blinks while typing declares this on every other mode's line. */
|
|
242
|
+
caretSolid?: boolean;
|
|
243
|
+
/** Center children on both axes. */
|
|
244
|
+
center?: boolean;
|
|
245
|
+
/** The on state of a `checkbox` / `radio` / `switch` role. */
|
|
246
|
+
checked?: boolean;
|
|
247
|
+
/** A registered sound (addSound) played when the node is clicked; implies hover tracking. */
|
|
248
|
+
clickSound?: string;
|
|
249
|
+
/** Child alignment across the main axis. On a row, `baseline` lines up the first baselines of the children's text, so a label and a larger value read as one line; a child with no text aligns by its bottom edge, a `grow` or percent height fills the line from its top, and a fit-height row grows to hold the aligned children. A column lays `baseline` out as `start` (as CSS does), and the three spreads mean nothing across an axis — each with a warning. */
|
|
250
|
+
crossAlign?: 'start' | 'center' | 'end' | 'spaceBetween' | 'spaceAround' | 'spaceEvenly' | 'baseline';
|
|
251
|
+
/** Space between wrap lines, across the main axis (`gap` stays the space along it). */
|
|
252
|
+
crossGap?: LengthProp;
|
|
253
|
+
/** The pointer shape over this node. Unset, the pointer is `text` over an editor or a `selectable` scope and `default` over everything else — an `onClick`, `focusable` or `onDrag` node included, as a native button is — so a hand (`pointer`) over a control, a `grab` over a handle (and `grabbing` while its drag runs, which the view declares as its drag state changes), a splitter's `ewResize` / `nsResize` and a `disabled` control's `notAllowed` are all declared. The stock `button` declares `pointer` itself. A captured drag keeps the dragged node's shape wherever the pointer goes. */
|
|
254
|
+
cursor?: 'default' | 'text' | 'pointer' | 'grab' | 'grabbing' | 'notAllowed' | 'ewResize' | 'nsResize' | 'nwseResize' | 'neswResize';
|
|
255
|
+
/** Holds the `keyframes` cycle back by this many ms (CSS `animation-delay`); siblings with different delays run out of phase. */
|
|
256
|
+
delay?: LengthProp;
|
|
257
|
+
/** The accessible description: the extra sentence a reader says after the name, for what the name cannot say on its own — what a button will do, why a control is disabled, what format a field wants. `tooltip` is the shorthand that also draws the string and hover-tracks the node; this is the description alone, for a hint that is spoken and never drawn. Both write the one slot, so a node declaring both keeps whichever its binding applied last. It reads only on a node that reaches the access tree — a role, a label, a control — since a plain box is elided and takes its description with it. */
|
|
258
|
+
description?: string;
|
|
259
|
+
/** Inert: no click, drag or key sink, no hover / pressed / focus background, skipped by Tab, reported disabled to assistive technology; hover tracking stays so a `tooltip` can say why. */
|
|
260
|
+
disabled?: boolean;
|
|
261
|
+
/** Background while files dragged in from the OS are over this node (ADR 0031); wins over pressedBg, focusBg and hoverBg, clears when they leave, land or the drag is cancelled. Implies hover tracking, eases with `transition`. */
|
|
262
|
+
dropBg?: ColorProp;
|
|
263
|
+
/** Easing for `transition` (default easeOut). The springs — `smooth` (no overshoot), `snappy`, `spring` and `bouncy` (the most), each a `bounce` of its own — integrate with momentum, so a value retargeted mid-flight keeps moving the way it was; `transition` is then about how long one takes to get there. */
|
|
264
|
+
easing?: 'easeOut' | 'linear' | 'easeIn' | 'easeInOut' | 'spring' | 'bouncy' | 'smooth' | 'snappy';
|
|
265
|
+
/** Where the node starts the first frame it is seen `{ dx?, dy?, width?, height?, bg?, radius?, opacity? }`: those slots ease in from there over `transition` ms instead of snapping (`dx`/`dy` slide it in from that far away, `opacity: 0` fades the whole subtree in). */
|
|
266
|
+
enter?: EnterProp;
|
|
267
|
+
/** Where the node ends the frame after the view stops declaring it `{ dx?, dy?, width?, height?, bg?, radius?, opacity? }` — an `enter` read the other way. It plays when the node itself is removed, its parent still declared; a node that goes because an ancestor went — a tab switched away, a panel closed around it — goes at once with it, unless that ancestor has an `exit` of its own, whose picture carries it (backlog DX19; React's `AnimatePresence` rule). With a `transition`, the departing subtree is copied out of the last frame that had it and replayed frozen, in its place (the pass it painted in, just under the node that painted after it — a panel under a HUD leaves under it) and inert (no clicks, no Tab stop, no access row) while those slots ease from where they were, then dropped; without one it vanishes at once as it always did. `width`/`height` resize the departing node's own box only — the subtree inside it is a picture and is not laid out again. Needs a stable key across frames. */
|
|
268
|
+
exit?: EnterProp;
|
|
269
|
+
/** A disclosure's state: what a node that shows and hides something (a twisty, an accordion header, a menu button) reads as. Unset, the node does not expand at all — which is why this names its state instead of being a flag. */
|
|
270
|
+
expanded?: 'collapsed' | 'expanded';
|
|
271
|
+
/** Background while the node holds keyboard-visible focus (moved there by Tab or assistive technology, not a click); replaces the default focus ring. Pressed wins over focus wins over hover; eases with `transition`. */
|
|
272
|
+
focusBg?: ColorProp;
|
|
273
|
+
/** Makes this node's subtree a focus region: a Tab ring of its own that the ring outside never enters and that never leaves — a devtools dock, an inspector beside the app (`docs/adr/0022-focus-regions.md`). Entered on purpose: `focusRegion(name)` (`Ui::focus_region`, `env.focus_region`, `kui_focus_region`) moves focus in — to the focus the region last held, else its `initialFocus`, else its first stop — and `focusRegion(null)` moves it back to the main ring the same way; a press inside the region, or an explicit focus on a node in it, enters it too. Tab then walks that ring alone, wrapping inside it; with nothing focused, Tab enters the ring of the region in effect (`region()`). A region that stops being declared hands focus back to what the main ring last held. Only the ring is scoped: keys still bubble through the boundary to the sink above (a region that wants its own keymap is an `onKey` sink), the pointer and assistive technology see a plain node, and a `modal` in effect is the ring wherever it sits. Nested regions are skipped by the outer ring the way the main ring skips them. */
|
|
274
|
+
focusRegion?: boolean;
|
|
275
|
+
/** Reachable by Tab (and focused by a click) without a click payload or a control role — a row that opens on Enter. Editors, key sinks, `onClick` boxes and the control roles are focusable already. */
|
|
276
|
+
focusable?: boolean;
|
|
277
|
+
/** Space between children along the main axis. */
|
|
278
|
+
gap?: LengthProp;
|
|
279
|
+
/** Vertical size: px | "fit" | "grow" | "N%" | a size expression (see `width`). */
|
|
280
|
+
height?: SizingProp;
|
|
281
|
+
/** Background while hovered (or while any node in its hoverGroup is); implies hover tracking, eases with `transition`. */
|
|
282
|
+
hoverBg?: ColorProp;
|
|
283
|
+
/** Nodes sharing a group name show hoverBg/pressedBg together (a split button, a multi-piece shape). */
|
|
284
|
+
hoverGroup?: string;
|
|
285
|
+
/** A registered sound (addSound) played when the pointer enters the node; implies hover tracking. */
|
|
286
|
+
hoverSound?: string;
|
|
287
|
+
/** Hover-track without a click payload (for isHovered-driven styling). */
|
|
288
|
+
hoverable?: boolean;
|
|
289
|
+
/** Where focus lands when the enclosing `modal` scope is entered: the first node in the modal's Tab ring declaring it, so a destructive confirm opens on its Cancel rather than on whichever control is declared first. Read on entry only — a Tab press afterwards stands, and the scope re-entered (a nested confirm closing) leaves focus where it was. Declared on nothing, or only on nodes the ring skips (disabled, `role="none"`, not focusable), entry stays the ring's first node. */
|
|
290
|
+
initialFocus?: boolean;
|
|
291
|
+
/** A press on this node, or anywhere inside it, leaves keyboard focus where it was: a toolbar button, a tab or a divider that acts without taking the keyboard from the editor or key sink that had it. Without it a press on an `onClick` node focuses the node, and the app's keys stop reaching the sink until it takes focus back. The press also leaves a text or cell selection and the Tab ring where they were, so a Copy button copies what was selected. An `<edit>` inside still takes its caret and focus, as the keyboard's own owner. The click, drag and hover are unchanged, and Tab and assistive technology still reach the node. */
|
|
292
|
+
keepFocus?: boolean;
|
|
293
|
+
/** With `onKey`: releases arrive too, as the same payload with phase:"up" (`text` null, `repeat` false) — for a held-key interaction (WASD, press-and-hold, a key that arms a mode while it is down). A key only comes up where it went down: a release whose press the sink never got is dropped, and focus leaving while a key is held delivers the `up` first, so nothing is left stuck down. Without it a sink hears presses only, which is what a keymap wants — one that heard both halves would run every binding twice. */
|
|
294
|
+
keyUp?: boolean;
|
|
295
|
+
/** CSS-style stops `[{ at?, width?, height?, bg?, radius?, opacity? }, …]`: the slots they name cycle through them over `transition` ms, forever, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. */
|
|
296
|
+
keyframes?: KeyframeProp[];
|
|
297
|
+
/** The accessible name. Without one a button, link, tab or heading is named by the text inside it; an image, an icon-only button and a `modal` dialog have none, and the core warns (`image-without-label`, `control-without-name`, `modal-without-name`). */
|
|
298
|
+
label?: string;
|
|
299
|
+
/** Marks this node a live region: when the text inside it changes, a screen reader reads the change without being asked — `polite` at the next pause, `assertive` interrupting. Put it on the smallest node that holds the message, since everything inside a live node is live. For a one-off with no node behind it ("Saved") the binding's `announce` verb is the other half. */
|
|
300
|
+
live?: 'off' | 'polite' | 'assertive';
|
|
301
|
+
/** Child alignment along the main axis. `start`, `center` and `end` put the children together; `spaceBetween` deals the free space out between them (none at the ends), `spaceAround` gives each child an equal share split to its two sides, and `spaceEvenly` makes every gap and both ends equal — CSS's `justify-content`. The spread is added to `gap`, and there is none when nothing is free: a `grow` child takes it all, and an overflowing run keeps its gaps. `baseline` means nothing here and lays out as `start`, with a warning. */
|
|
302
|
+
mainAlign?: 'start' | 'center' | 'end' | 'spaceBetween' | 'spaceAround' | 'spaceEvenly' | 'baseline';
|
|
303
|
+
/** Upper height clamp: logical px or a size expression (see `width`). */
|
|
304
|
+
maxHeight?: MaxProp;
|
|
305
|
+
/** Upper width clamp: logical px or a size expression (see `width`); grow+maxWidth is the responsive-width pattern. */
|
|
306
|
+
maxWidth?: MaxProp;
|
|
307
|
+
/** Lower height clamp: logical px, a size expression, or "fit" for the node's own fit height. Undeclared, it is the node's content where its column overflows — CSS's `min-height: auto`, none for a node that scrolls or clips — so a row keeps the height of its text; `0` asks for the squeeze back (see `minWidth`). */
|
|
308
|
+
minHeight?: MinProp;
|
|
309
|
+
/** Lower width clamp: logical px, a size expression (see `width`; a percentage clamp is none until the parent's width is known, as in CSS), or "fit" for the node's own fit width. "fit" under `width="grow"` is a content floor — CSS's `flex: 1 0 auto` — which is what an i3-style tab bar is: tabs that split the bar evenly while they fit and sit at their label's width, scrolling, once they do not. Opt-in, because a fit width is the unwrapped one: a paragraph in a grow column would stop wrapping under it. Left out, a child giving in an overflowing row that holds a percentage or a size expression stops at its content, CSS's `min-width: auto`; `0` lets it go below, CSS's `min-width: 0` (backlog RG92). A fit node across a column is no wider than the column's box — CSS's `fit-content` — down to this floor, 0 left out, unless the column scrolls x, so a text one wrapper deep in a capped card wraps there; "fit" keeps its content's width and runs past (backlog F116). */
|
|
310
|
+
minWidth?: MinProp;
|
|
311
|
+
/** A `checkbox` that is neither on nor off — the select-all box over a list some of whose rows are selected (ADR 0034). Read as mixed by assistive technology whatever `checked` says, and drawn as a dash by the stock `<checkbox>`. Meaningful on the checkbox role alone. */
|
|
312
|
+
mixed?: boolean;
|
|
313
|
+
/** Modal surface: the Tab ring becomes this node's subtree, everything outside it is inert to the pointer, the wheel and assistive technology, and Escape or a press outside emits {kind:"dismiss", reason:"escape"|"outside", tag} on it — the app stops declaring the node. The last one declared in tree order is the one in effect (a confirm inside a dialog); a modal that must cover the app is a float. The access tree is not pruned to the modal: it keeps every node of the frame and marks the one in effect `modal` (`docs/adr/0003-modal-surfaces.md`, decision 7), which is what assistive technology acts on. */
|
|
314
|
+
modal?: AppMsg | null;
|
|
315
|
+
/** With `onKey`: the modifier and lock keys arrive as keys of their own (backlog F108) — `code` "shift", "ctrl", "alt", "super", "capslock", "numlock", "scrolllock", which side in `location` ("left" / "right"), releases too with `keyUp`. Without it a modifier is only ever held — the next key's `shift`, `ctrl`, … and the `modifiers` event — so a keymap mid-sequence never reads a Shift as a key between two others. For a terminal speaking kitty's keyboard protocol, or a game that binds a lone Shift. */
|
|
316
|
+
modifierKeys?: boolean;
|
|
317
|
+
/** Button tag (backlog F105): a press of a non-primary button — middle, secondary, or one past those — emits {kind:"button", phase:"press", button, x, y, clicks, tag} on the node, and the button is then captured by it: every pointer move while it is held arrives as phase:"move" and its release as phase:"release", on this node wherever the pointer is. `button` is `"secondary"`, `"middle"` or a further button's number (3 and up); `x`/`y` are logical viewport coordinates, and on a `cells` grid each event carries `cell: {row, col}` as a click does. Several buttons can be held at once, each its own capture, and a primary drag is untouched. `buttons` says which buttons it claims — all of them unless it narrows them. Asked of the topmost node under the pointer, and when that node claims no such button the press reaches the nearest enclosing node that does, the way a context menu's does: a disabled node's own is skipped and the walk stops at the modal boundary. A claimed secondary press is this event *instead of* a `contextmenu` event and the stock menu (a nearer `onContextMenu` still wins, being the nested declaration). Like every non-primary press it moves no focus, places no caret and touches no selection or scrollbar. For a terminal's middle-click paste, and the mouse reports a program in it asked for. */
|
|
318
|
+
onButton?: AppMsg | null;
|
|
319
|
+
/** A `slider` role's changes (ADR 0034): the core turns a press on the node into the value under the pointer, a drag into the value under it, the arrows and assistive technology's increment / decrement into one `valueStep`, PageUp / PageDown into ten, Home / End into the range's ends — clamped to `valueMin`..`valueMax` (0..100 unset) and snapped to the step — and emits `{kind:"change", value, phase, tag}`: `phase` is `"move"` while the pointer holds the slider and `"end"` when it lets go or a key moved it. The value is proposed and never applied; the slider moves when the view declares it as `valueNow`. A key that lands where the slider already is proposes nothing. The pointer reads the node's content box along its main axis, so a `dir="column"` slider runs bottom to top. Without it a slider's arrows reach the app as `{kind:"access", action}`. Ignored on any other role. */
|
|
320
|
+
onChange?: AppMsg | null;
|
|
321
|
+
/** Message emitted when clicked (data, not a callback). */
|
|
322
|
+
onClick?: AppMsg;
|
|
323
|
+
/** Context-menu tag: a secondary-button (right) press emits {kind:"contextmenu", x, y, tag} on the node, at the logical viewport point to open the menu at. The press moves no focus, places no caret and produces no click, so right-clicking a selection keeps it. Asked of the topmost node under the pointer, and when that node offers no menu the press reaches the nearest enclosing node that does — a container declaring a menu for everything inside it is the common case — the way an unclaimed key reaches the enclosing sink (`docs/adr/0011`): the event carries the *owner's* key and tag, a nested declaration wins over its ancestor's, a disabled node's own is skipped, and the walk stops at the modal boundary. */
|
|
324
|
+
onContextMenu?: AppMsg | null;
|
|
325
|
+
/** Drag tag: emits {kind:"drag", phase, x, y, dx, dy, parent, tag} events, `dx`/`dy` measured from the press point in every phase. On a `cells` grid the events also carry `cell: {row, col}`; inside an `onKey` sink that draws `role="line"` rows they carry `line`, `byte` and `clicks` — see the events table. */
|
|
326
|
+
onDrag?: AppMsg | null;
|
|
327
|
+
/** Drop-zone tag (`docs/adr/0031-a-drop-zone-is-a-row-and-the-files-are-an-event.md`): files dragged in from the OS over this node emit {kind:"drop", phase:"enter"|"move"|"leave"|"drop", paths, x, y, tag} — `paths` the OS paths as strings, `x`/`y` the pointer in logical viewport coordinates (absent on `leave`). The zone under the files is the topmost zone by paint order: a node inside a zone is the zone's (a button in it, a field in it), and a node that is no zone and has none enclosing it is looked past, so an overlay shown on `enter` cannot make the zone lose the files. No `leave` follows a `drop`; a drop off every zone is refused by the driver. Implies hover tracking. No access row — a screen-reader user's way in is a button beside the zone. On Windows and Linux the position is the OS cursor at enter and release only, so `move` never fires there. */
|
|
328
|
+
onDrop?: AppMsg | null;
|
|
329
|
+
/** Keyboard focus entering or leaving this node's subtree — the node itself, or anything focused inside it — emits `{kind:"focus", phase:"in"|"out", by, tag}` (backlog DX18). `by` is what moved it: `pointer` (a press), `keyboard` (Tab, a key a control answered), `assistive` (a screen reader's request) or `program` (the view or the app — `keyFocus`, `setFocus`, a modal's entry). Reported once the move settles, after the input that made it or at the end of the frame that declared it, so an app hears a pane taking the keyboard instead of diffing the focused key every frame. Leaving is reported innermost first, entering outermost first. It makes nothing focusable or interactive. */
|
|
330
|
+
onFocus?: AppMsg | null;
|
|
331
|
+
/** Force-click tag: a press that deepens past the second stage of a Force Touch trackpad emits {kind:"forceclick", x, y, tag} on the node, at the logical viewport point it happened at (`docs/adr/0017-selection-as-a-scope.md`). Routed as a secondary press is — no focus moved, no caret placed, no click — but asked of the topmost node only, with no walk to an enclosing declaration — and the ordinary click the press is still producing arrives afterwards, as it does on macOS. Text needs none of this: a force click over an `edit` or a `selectable` scope selects the word under it and asks the host for its Look Up panel. macOS-only in practice, and there the user can switch the gesture off, so nothing may declare itself the only way to reach something. */
|
|
332
|
+
onForceClick?: AppMsg | null;
|
|
333
|
+
/** Hover tag: the pointer entering/leaving emits {kind:"hover", phase:"enter"|"leave", tag} events. */
|
|
334
|
+
onHover?: AppMsg | null;
|
|
335
|
+
/** Key-sink tag: with key focus held, presses arrive as {kind:"key", phase:"down", code, ...} events. Releases only with `keyUp` beside it. */
|
|
336
|
+
onKey?: AppMsg | null;
|
|
337
|
+
/** Layout tag: the node's laid-out rect arrives as {kind:"layout", x, y, w, h, parent, tag} on its first frame and whenever it changes (needs a stable key). */
|
|
338
|
+
onLayout?: AppMsg | null;
|
|
339
|
+
/** Scroll tag: the wheel over this node emits {kind:"scroll", x, y, dx, dy, lines, tag} on it instead of scrolling anything — `dx`/`dy` the delta in logical px as the driver reported it (positive `dy` is the wheel rolling up, toward earlier content), `x`/`y` the pointer, and `lines` on a `cells` grid the whole lines the delta covers (positive = later history, the sign `originLine` grows in; the fraction is carried to the next notch so a trackpad's small steps add up) and null on any other node. The node takes the wheel on the axes `scrollAxes` names (both unless it narrows them): a gesture that starts over it is its own whether or not it has anywhere to go — except on an axis the node also scrolls (`scrollX`/`scrollY`, its offset the app's to set), where it is answered by its room as a container is, so at its edge a gesture that way passes to the scroller around it (backlog F118) — and stays its own until it ends, wherever the pointer goes (backlog F107); it reaches no scroll container above it, and a scroller inside it still takes the axes it scrolls while it can move that way, passing this node the rest — the other axis, and a gesture that begins with that scroller at its limit (`overscroll: "contain"` on the scroller keeps it there). The core moves nothing — a grid re-declares `originLine`, a canvas zooms. A drag-select held past a `cells` grid's top or bottom edge arrives here too, once a frame with the lines that frame scrolled by (`docs/adr/0029-a-selection-follows-the-pointer-past-the-edge.md`). */
|
|
340
|
+
onScroll?: AppMsg | null;
|
|
341
|
+
/** Group opacity 0..1 (default 1): fades this node and its whole subtree. A per-quad alpha multiply rather than an offscreen composite, so overlapping pieces of one subtree show their seams through the fade. Layout, hit-testing and the access tree are untouched; eases with `transition`, and `enter: { opacity: 0 }` fades a panel in. */
|
|
342
|
+
opacity?: LengthProp;
|
|
343
|
+
/** What a scroll gesture that starts over this scroller does when it is already at its limit that way (backlog F107, CSS's `overscroll-behavior`): `auto` (the default) passes the gesture on to the scroller around it, `contain` keeps it here, moving nothing until it turns back. A gesture picks its target once, when it starts — the innermost scroller under the pointer that can still move the way it goes — and keeps it until it ends, wherever the pointer or the content has gone; one that reaches a limit midway stops there, whatever this says. Only on the axes the node scrolls: a `scrollY` list that contains still passes a sideways swipe to the strip around it. For a panel or a popup's list whose scrolling must never move what is behind it. */
|
|
344
|
+
overscroll?: 'auto' | 'contain';
|
|
345
|
+
/** Paint this node's background, border, shadow and fragment with each edge on a whole physical pixel: `x` and `x + width` rounded on their own, from where layout put them, as a text's span backgrounds are. Off by default, and a box is drawn where layout put it, so a 1 px `gap` between boxes is there at any scale. On, boxes that share an edge in layout meet on one pixel line, where a join inside a pixel was drawn by halves and left a seam — rows of a band stacked at a pitch that is not whole pixels, or a box that continues a text's selection. Layout, hit-testing, the clip and the children are untouched. A snapped box can draw up to half a pixel from its layout edge and its size can differ by a pixel, so a snapped hairline is 1 or 2 px thick by where it sits. */
|
|
346
|
+
pixelSnap?: boolean;
|
|
347
|
+
/** Background while pressed (or while its hoverGroup is); implies hover tracking. */
|
|
348
|
+
pressedBg?: ColorProp;
|
|
349
|
+
/** Corner radius for all four corners (logical px); the per-corner props override it when listed after it. On a node that also clips or scrolls it rounds the clip as well, so children stay inside the corners. */
|
|
350
|
+
radius?: LengthProp;
|
|
351
|
+
/** Bottom-left corner radius (logical px). */
|
|
352
|
+
radiusBL?: LengthProp;
|
|
353
|
+
/** Bottom-right corner radius (logical px). */
|
|
354
|
+
radiusBR?: LengthProp;
|
|
355
|
+
/** Top-left corner radius (logical px). */
|
|
356
|
+
radiusTL?: LengthProp;
|
|
357
|
+
/** Top-right corner radius (logical px). */
|
|
358
|
+
radiusTR?: LengthProp;
|
|
359
|
+
/** How `keyframes` cycle (CSS `animation-direction`, default normal). Lua: `direction`, since `repeat` is a keyword. */
|
|
360
|
+
repeat?: 'normal' | 'reverse' | 'alternate' | 'alternateReverse';
|
|
361
|
+
/** What the node is to assistive technology. Unset, the core derives one (an `onClick` node is a button, an editor a text input, a scrolling box a scroll view, a plain box nothing); `none` hides the node and its subtree from the access tree. A `radio` belongs inside a `radioGroup` and a `tab` inside a `tabList`, labelled with what the choice is: the pair is a composite (`docs/adr/0007-composite-keyboard-patterns.md`) — one Tab stop for the set, the arrows, Home and End moving the choice inside it (each step is the item's click, so the choice follows focus), and a screen reader reading "2 of 3". A `radio` or `tab` with no container above it is a Tab stop of its own that no arrow moves, and the core warns (`item-outside-container`). `menu` holds `menuItem`s and `list` holds `listItem`s the same way. */
|
|
362
|
+
role?: 'none' | 'button' | 'checkbox' | 'radio' | 'switch' | 'slider' | 'tab' | 'tabList' | 'link' | 'heading' | 'list' | 'listItem' | 'image' | 'dialog' | 'group' | 'textInput' | 'multilineTextInput' | 'line' | 'radioGroup' | 'menu' | 'menuItem' | 'terminal';
|
|
363
|
+
/** The width of a table's `rules` in logical px; 1 when unset. */
|
|
364
|
+
ruleWidth?: LengthProp;
|
|
365
|
+
/** On a table (`dir="table"`, ADR 0033): grid lines of this colour between its columns and between its rows (backlog DX21) — down the middle of each gap between the columns of its widest row, from the first row's top to the last row's bottom, and across the middle of each gap between rows, the content box wide. Drawn with the table's box, under its cells and on whole pixels, so give the table and its rows a `gap` at least `ruleWidth` for the lines to show between cells; the outer edge is the table's `border`. Ignored on anything but a table. */
|
|
366
|
+
rules?: ColorProp;
|
|
367
|
+
/** Which axes `onScroll` takes (backlog F107): `both` (the default), `x` or `y`. A scroll gesture on an axis the node does not take passes it by, to the scroller around it, and hears nothing here: a terminal that scrolls its history says `y`, and a sideways swipe that starts over it moves the strip it sits in. (A swipe that started elsewhere is not the node's either way: a gesture keeps the target it started with.) Meaningless without `onScroll`. */
|
|
368
|
+
scrollAxes?: 'both' | 'x' | 'y';
|
|
369
|
+
/** When a scrolling node draws its bars: `visible` (the default — the stock overlay thumb, drawn while the content overflows), `hidden` (no thumb, no track to press; the wheel, the keyboard, `reveal` and the caret still scroll it — for a list that draws its own indicator, or a pane whose bar would sit on a border), or `auto` (shown while the scroll state is changing — the offset or the content's extent moved, the pointer is on the track, a thumb is dragged — and for a second after, then faded out over a quarter of one; a node first seen shows it the same second; what an overlay bar does on macOS). `auto` needs the driver's clock and is `visible` without one. The bars are overlays and take no layout space in any mode. From the last change until it has faded — a second and a quarter — an `auto` bar asks for frames the way a transition of that length would (nothing else could wake the core when the hold ends); while the pointer holds it, it asks for none. */
|
|
370
|
+
scrollbar?: 'visible' | 'hidden' | 'auto';
|
|
371
|
+
/** The thumb under the pointer or while dragged; the default is the theme's `scrollbar_active` role. */
|
|
372
|
+
scrollbarActiveColor?: ColorProp;
|
|
373
|
+
/** The thumb at rest; the default is the theme's `scrollbar` role, a translucent wash over whatever it sits on. */
|
|
374
|
+
scrollbarColor?: ColorProp;
|
|
375
|
+
/** The thumb's width at rest, logical px (default 4); under the pointer or dragged it is 2 px wider. The grabbable track grows to fit a wide thumb. */
|
|
376
|
+
scrollbarWidth?: LengthProp;
|
|
377
|
+
/** Makes this node a selection scope: the text of every node inside it is one selectable run, in tree order, and a press-drag across them selects the lot — as do Shift with Left / Right / Home / End on a focused node inside it, a character or a word at a time, a scope with nothing selected anchoring at its start (`docs/adr/0017-selection-as-a-scope.md`). Declared on the container and not on each label, because what a reader selects is a paragraph or a card rather than one run of it — three labels in a column under one `selectable` select as three lines of one text. The selection is the window's: starting one anywhere clears the last, an editor's included. Scopes do not nest; an outer one around an inner one is warned about (`nested-selection-scope`) and the innermost owns the text. Text scrolled out of view inside the scope is still part of it — selection and copy reach it, hit-testing does not. On a `cells` grid the scope selects in cells rather than in bytes: a drag takes lines (with a modifier, a rectangle), a double click the word under the pointer and a triple click the whole row, its ends are absolute lines so a scroll does not move them, and a copy trims each line's trailing blanks. */
|
|
378
|
+
selectable?: boolean;
|
|
379
|
+
/** The current one of a set: which `tab` a `tabList` shows, which `listItem` a list has picked, which `link` is the page you are on. A `tab` always carries the state — its siblings read as "not selected" — while a list row or a link carries it only where it is set, since an ordinary list or navigation bar is not a selection and a reader saying "not selected" on every row of it is noise. */
|
|
380
|
+
selected?: boolean;
|
|
381
|
+
/** On a `line` of a custom editor: the byte offset where the selection's other end sits (the caret is `caret`, possibly on another line). */
|
|
382
|
+
selectionAnchor?: LengthProp;
|
|
383
|
+
/** Drop-shadow blur radius (logical px): the edge ramps over this distance and reaches this far past the shape. 0 = a hard edge. */
|
|
384
|
+
shadowBlur?: LengthProp;
|
|
385
|
+
/** Drop-shadow color; nothing else about a shadow draws without it. On its own it is a hard shadow exactly behind the node — add `shadowBlur` / `shadowY` to lift it. Outer shadows only, and the shape is not knocked out of the middle, so a translucent background shows it through. */
|
|
386
|
+
shadowColor?: ColorProp;
|
|
387
|
+
/** Grows (or, negative, shrinks) the drop shadow's shape before blurring (logical px). */
|
|
388
|
+
shadowSpread?: LengthProp;
|
|
389
|
+
/** Drop-shadow horizontal offset (logical px). */
|
|
390
|
+
shadowX?: LengthProp;
|
|
391
|
+
/** Drop-shadow vertical offset (logical px); positive casts downward. */
|
|
392
|
+
shadowY?: LengthProp;
|
|
393
|
+
/** With transition: also ease the node's position (reordered siblings slide). While it eases, the node is drawn between where it was and where this frame put it — not at the declared `dx`/`dy`, or its slot in the row — so anything else positioned from those numbers drifts for the transition's length: a canvas of floats eases everything or nothing. */
|
|
394
|
+
slide?: boolean;
|
|
395
|
+
/** Animate sizing/colors/radius changes over this many ms — and, on a scroll container, the offset a reveal or a set_scroll moves it to (needs a stable key). */
|
|
396
|
+
transition?: LengthProp;
|
|
397
|
+
/** A `slider` role's maximum. */
|
|
398
|
+
valueMax?: LengthProp;
|
|
399
|
+
/** A `slider` role's minimum. */
|
|
400
|
+
valueMin?: LengthProp;
|
|
401
|
+
/** A `slider` role's current value (the drawing stays yours; this is what assistive technology reads). */
|
|
402
|
+
valueNow?: LengthProp;
|
|
403
|
+
/** How far one arrow key moves a `slider` role, and the grid a value the pointer sets snaps to (ADR 0034). Unset, a hundredth of the range. PageUp / PageDown move ten steps. Read by the core only where the slider declares `onChange`; reported to assistive technology either way. */
|
|
404
|
+
valueStep?: LengthProp;
|
|
405
|
+
/** What a `slider` role's position reads as (ARIA's `aria-valuetext`). Without one a reader has only `valueNow` and the range and says a percentage — 25 in [5..60] is "36 percent" — so a value whose unit carries the meaning says it here: "25 minutes". It replaces the number in the reading rather than joining it, and a nudge announces the new text. Meaningful on the slider role alone, like the three numbers; putting the reading in `label` instead renames the control on every nudge, which is the wrong attribute. */
|
|
406
|
+
valueText?: string;
|
|
407
|
+
/** Horizontal size: px | "fit" | "grow" | "N%" | a size expression — `"clamp(400px, 80%, 1000px)"`, `"min(720px, 100%)"`, `"max(50%, 300)"`, nested — which layout resolves against the parent's content box, the box a percentage takes its cut of (backlog F109). An expression with no percentage in it is a length; a calc does not ease under `transition`. A percentage or an expression gives, with the fit children, when its parent overflows — two `"50%"` children and a gap fit their row (backlog F110) — where a px size keeps its own. A row holding one gives as CSS's flex items do: every child that can give gives in proportion to its size, and stops at its content — the widest thing in it that cannot wrap, a label's longest word — unless `minWidth` says otherwise (backlog RG92). The process keeps 65 536 distinct expressions and never lets one go: past that a new one leaves its prop at its default, with a `size-expressions-full` warning, so declare one per layout — a px size for the part that moves each frame, a splitter's drag — not one per frame (backlog RG93). */
|
|
408
|
+
width?: SizingProp;
|
|
409
|
+
/** Window-chrome role: interactions become window commands, not events. */
|
|
410
|
+
window?: 'drag' | 'close' | 'minimize' | 'maximize';
|
|
411
|
+
/** Children that don't fit the main axis start a new line instead of overflowing or shrinking. Rows only (a column is ignored, with a warning), and never on a scrollX row. */
|
|
412
|
+
wrapChildren?: boolean;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface GeneratedStyleProps {
|
|
416
|
+
/** Text color; default foreground when omitted. */
|
|
417
|
+
color?: ColorProp;
|
|
418
|
+
/** End the last line with an ellipsis when the text is cut off: a single line unless `maxLines` says otherwise. */
|
|
419
|
+
ellipsis?: boolean;
|
|
420
|
+
/** Font family: `sans`, `serif` or `mono`, kui's own, or the name of an installed family or one loaded with `loadFontsDir` / `loadFontFile` — `"Berkeley Mono"` — drawn in its face in the frame that names it (ADR 0037). A name is matched as `addSystemFont` matches it and registered in the session on first sight, exactly as the font database spells it (`"menlo"` is not `"Menlo"`); the session's first registration of any font maps the installed font files once (~30 ms on a Mac, backlog DX24), which a family named in a view pays in that frame. `systemFonts()` lists the names there are. A name nothing matches shapes as sans and raises `unknown-family`. It and `font` set the same thing, so declare one. */
|
|
421
|
+
family?: 'sans' | 'serif' | 'mono' | (string & {});
|
|
422
|
+
/** OpenType features for the shaper, as `tag=value` pairs separated by spaces or commas — a bare `tag` is 1, `-tag` is 0: `"liga=0 calt=0"` keeps a coding font from joining `->` and `!=` (what a terminal built on runs needs to hold its grid), `"tnum"` lines figures up in a gutter, `"ss01"` picks a stylistic set. Unset, the font's own defaults apply. At most 8; part of what the text is shaped as, so two texts differing only here are shaped twice. */
|
|
423
|
+
features?: string;
|
|
424
|
+
/** A registered font handle (addFont / addSystemFont); overrides `family`. */
|
|
425
|
+
font?: string;
|
|
426
|
+
/** Line height (logical px); default size * 1.35. */
|
|
427
|
+
lineHeight?: LengthProp;
|
|
428
|
+
/** Lay out at most this many lines (0 = unlimited); with `ellipsis`, a line clamp. */
|
|
429
|
+
maxLines?: LengthProp;
|
|
430
|
+
/** A line through the text, where the face puts its strikeout. Paint only; on a `<span>` the span alone, per line. */
|
|
431
|
+
strikethrough?: boolean;
|
|
432
|
+
/** A line under the text, where the face puts its underline and as thick as it says, in the text colour. Paint only. On a `<span>` it covers the span alone and follows it across a wrap, one rect per line. `underlineColor` gives it a colour of its own and `underlineStyle` a shape; either implies it. */
|
|
433
|
+
underline?: boolean;
|
|
434
|
+
/** The underline's own colour — a diagnostic's red under keyword-coloured text (backlog K4). Implies `underline`. On a `<span>` the span's; a span with no colour of its own takes the text's. */
|
|
435
|
+
underlineColor?: ColorProp;
|
|
436
|
+
/** The underline's shape (backlog K4): `solid` (the face's line), `wavy` (three strokes tall around the line, a six-stroke period — a diagnostic's squiggle, a terminal's undercurl) or `dotted` (dots two strokes across, four apart). Implies `underline`. A wave or dots are runs of the segment primitive a `line` draws, so no backend learns a kind; the cost is two quads per period. */
|
|
437
|
+
underlineStyle?: 'solid' | 'wavy' | 'dotted';
|
|
438
|
+
/** Line breaking at the node's width: between words (default), anywhere, never (one line per paragraph, clipped to the node), or between words with whitespace taking its room (`break-spaces`: a space that does not fit starts the next row rather than hanging past the edge — an editor's wrapped line). On a single-line `edit` — a field, which otherwise takes one line and scrolls it — declaring it is what makes the field fold to its width like a document, by this mode, while Enter still submits (see `edit`). */
|
|
439
|
+
wrap?: 'word' | 'glyph' | 'none' | 'break-spaces';
|
|
440
|
+
}
|
|
441
|
+
// -- end generated --
|
|
442
|
+
|
|
443
|
+
/** Composite/constructor props with hand-written handling on both sides of
|
|
444
|
+
* the boundary (everything else comes from the generated schema types). */
|
|
445
|
+
export interface CustomSpecProps {
|
|
446
|
+
/** Main axis; `column` is the default. `table` is a column whose rows'
|
|
447
|
+
* children line up in columns (ADR 0033): the nth in-flow child of
|
|
448
|
+
* every row is column n (a float in a row is not a cell), and a
|
|
449
|
+
* column is as wide as its widest cell — a cell's
|
|
450
|
+
* `width` sizes its column (`fit` and a number are content, `grow`
|
|
451
|
+
* grows the column, a percent takes its cut), a bare `<text>` is a
|
|
452
|
+
* cell held to its column, and the rows are rows: give them
|
|
453
|
+
* `width="grow"` for the columns to grow into, with their own `gap`,
|
|
454
|
+
* padding, `bg`, `hoverBg` and `onClick`. */
|
|
455
|
+
dir?: 'row' | 'column' | 'table';
|
|
456
|
+
pad?: LengthProp;
|
|
457
|
+
padX?: LengthProp;
|
|
458
|
+
padY?: LengthProp;
|
|
459
|
+
padL?: LengthProp;
|
|
460
|
+
padR?: LengthProp;
|
|
461
|
+
padT?: LengthProp;
|
|
462
|
+
padB?: LengthProp;
|
|
463
|
+
borderW?: LengthProp;
|
|
464
|
+
borderColor?: ColorProp;
|
|
465
|
+
clip?: boolean;
|
|
466
|
+
scrollX?: boolean;
|
|
467
|
+
scrollY?: boolean;
|
|
468
|
+
float?: 'below' | 'above' | 'parent' | 'viewport' | FloatProp;
|
|
469
|
+
/** Focuses this node (an `onKey` sink, an editor, any focusable node)
|
|
470
|
+
* when it starts being declared: declared every frame it takes focus
|
|
471
|
+
* once, so a later Tab press is not clobbered. `ctx.focus(key)` moves
|
|
472
|
+
* focus at any time. */
|
|
473
|
+
keyFocus?: boolean;
|
|
474
|
+
/** Hover hint: a tooltip floated below this box while it is hovered
|
|
475
|
+
* (implies hoverable). */
|
|
476
|
+
tooltip?: string;
|
|
477
|
+
/** Stable identity by *data* index rather than by name: the key
|
|
478
|
+
* auto-keying would have given this node as the `i`th child, given to
|
|
479
|
+
* it wherever it actually sits. What a virtualised list is for — a
|
|
480
|
+
* view that builds rows 900..930 opens each with its own row number,
|
|
481
|
+
* so a row keeps its hover, focus, edit buffer and tweens as the built
|
|
482
|
+
* range slides over it. Beside a `key`, the index wins. */
|
|
483
|
+
index?: number;
|
|
484
|
+
/** How many `index`ed rows this node's virtual list has, built or not.
|
|
485
|
+
* `uniformList` declares it on its container; a list composed by
|
|
486
|
+
* hand says it beside `scrollY`. Select All inside a `selectable`
|
|
487
|
+
* virtual list then selects the *data*, rows `0..rowCount`, and the
|
|
488
|
+
* copy is a `selectionrange` ask whose `to.byte` is past the last
|
|
489
|
+
* row's length when that row is not built — cut it to the row. */
|
|
490
|
+
rowCount?: number;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export interface BoxProps extends Keyed, GeneratedSpecProps, CustomSpecProps {
|
|
494
|
+
/** Root box only: declares this frame's window title. */
|
|
495
|
+
title?: string;
|
|
496
|
+
/** Root box only: asks for the window above every other app's this
|
|
497
|
+
* frame — a floating palette, a picture-in-picture player, a timer.
|
|
498
|
+
* Declare it every frame you want it; a frame that stops is what lowers
|
|
499
|
+
* the window again, so a pin button toggles by declaring or not. Whether
|
|
500
|
+
* the platform agreed is `env.window.alwaysOnTop`, which is what the
|
|
501
|
+
* button should draw from. */
|
|
502
|
+
alwaysOnTop?: boolean;
|
|
503
|
+
/** Root box only: asks for secure keyboard entry while this window has
|
|
504
|
+
* the keyboard — what a terminal turns on at a password prompt, so no
|
|
505
|
+
* other process can read the keys typed there (macOS; nothing
|
|
506
|
+
* elsewhere). Declare it every frame the prompt is up; the frame that
|
|
507
|
+
* stops is what turns it off. The runner enables it only while the
|
|
508
|
+
* window has the keyboard and keeps the platform's count balanced. */
|
|
509
|
+
secureInput?: boolean;
|
|
510
|
+
/** Root box only: which Option keys act as Alt in this window on macOS
|
|
511
|
+
* (backlog F113). Option composes on a Mac — ⌥u, ⌥e, ⌥i, ⌥n and ⌥`
|
|
512
|
+
* are dead keys that start an accent, so the press never arrives as a
|
|
513
|
+
* key — and a side named here is Alt instead: it types nothing and a
|
|
514
|
+
* key under it arrives as a chord of the layout's unmodified key, so a
|
|
515
|
+
* keymap's `<A-u>` fires. `"left"` or `"right"` leaves the other side
|
|
516
|
+
* composing; `"none"`, the default, is the Mac's own behaviour.
|
|
517
|
+
* Declare it every frame; the frame that stops gives the Option keys
|
|
518
|
+
* back to the layout. Nothing elsewhere. */
|
|
519
|
+
optionAsAlt?: 'none' | 'left' | 'right' | 'both';
|
|
520
|
+
/** Root box only: which windows exist besides the main one (see
|
|
521
|
+
* `WindowDecl` in `@qxuken/kui`). `runWindowed` / `createApp` write it
|
|
522
|
+
* from the loop config's `windows(model)`; a view driving a `Ctx` by
|
|
523
|
+
* hand declares it here. */
|
|
524
|
+
windows?: (string | { name: string; width?: number; height?: number; activates?: boolean })[];
|
|
525
|
+
children?: KuiNode;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export interface TextProps extends Keyed, GeneratedStyleProps {
|
|
529
|
+
size?: LengthProp;
|
|
530
|
+
children?: KuiNode;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export interface SpanProps extends Keyed {
|
|
534
|
+
bold?: boolean;
|
|
535
|
+
italic?: boolean;
|
|
536
|
+
/** Overrides the paragraph color; nested spans inherit. */
|
|
537
|
+
color?: ColorProp;
|
|
538
|
+
/** A line under the span, where the face puts it; nested spans inherit. */
|
|
539
|
+
underline?: boolean;
|
|
540
|
+
/** The underline's own colour — a diagnostic's red under keyword-coloured
|
|
541
|
+
* text; implies `underline`, nested spans inherit. Without it the
|
|
542
|
+
* underline is the span's colour. */
|
|
543
|
+
underlineColor?: ColorProp;
|
|
544
|
+
/** The underline's shape: `solid` (the face's line), `wavy` (a squiggle,
|
|
545
|
+
* three strokes tall with a six-stroke period) or `dotted`; implies
|
|
546
|
+
* `underline`, nested spans inherit. */
|
|
547
|
+
underlineStyle?: 'solid' | 'wavy' | 'dotted';
|
|
548
|
+
/** A line through the span, where the face puts it; nested spans inherit. */
|
|
549
|
+
strikethrough?: boolean;
|
|
550
|
+
/** A background behind the span's glyphs alone — one rect per line it
|
|
551
|
+
* spans, so it follows the span across a wrap the way a box cannot. */
|
|
552
|
+
bg?: ColorProp;
|
|
553
|
+
/** Rounds `bg` (logical px), and joins it into one shape with every
|
|
554
|
+
* rounded background of the same colour and radius it meets — edge to
|
|
555
|
+
* edge on the line above or below, or end to end on its own line, in
|
|
556
|
+
* this text or another: convex corners where a line reaches past its
|
|
557
|
+
* neighbour, a fillet where it falls short, round where nothing meets.
|
|
558
|
+
* A selection over rows, or over a paragraph's wrapped lines, is one
|
|
559
|
+
* outline. Nested spans inherit; 0 (the default) is square. */
|
|
560
|
+
bgRadius?: number;
|
|
561
|
+
children?: KuiNode;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** The stock button (`widgets::button_spec`: padding, colours, radius,
|
|
565
|
+
* hover and pressed backgrounds). Its look is its own, so the layout and
|
|
566
|
+
* paint rows are not here — declared anyway they are dropped with an
|
|
567
|
+
* `unknown-prop` warning — and what is here are the rows a reader hears:
|
|
568
|
+
* `label` when the text is not the name, `description` for what the
|
|
569
|
+
* button will do, `tooltip`, and `disabled` (inert, and dimmed to half).
|
|
570
|
+
* The one paint row it does take is `accent`, which is a question put to
|
|
571
|
+
* the OS rather than a colour: the three backgrounds come off
|
|
572
|
+
* `env.system.accent` and the label goes black or white by its luminance,
|
|
573
|
+
* and the stock blue stands on a host that never said what the accent is.
|
|
574
|
+
* A button that needs any other row is a `<box role="button">` with the
|
|
575
|
+
* same rows spelled out. Keyed by its text unless `key` says otherwise.
|
|
576
|
+
*
|
|
577
|
+
* The list is `BUTTON_ROWS_JSX` in schema.rs — the rows the encoder admits
|
|
578
|
+
* — so what it extends is generated from there with the rest of this file:
|
|
579
|
+
* add a row in Rust and it is a prop here the same `npm run gen` later. */
|
|
580
|
+
export interface ButtonProps
|
|
581
|
+
// -- generated from the addon's button rows; edit BUTTON_ROWS_JSX in schema.rs, then `npm run gen` --
|
|
582
|
+
extends Keyed,
|
|
583
|
+
Pick<GeneratedSpecProps, 'onClick' | 'label' | 'description' | 'disabled' | 'accent'>,
|
|
584
|
+
Pick<CustomSpecProps, 'index' | 'tooltip'>
|
|
585
|
+
// -- end generated --
|
|
586
|
+
{
|
|
587
|
+
children?: KuiNode;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** A stock toggle — `<checkbox>`, `<radio>`, `<switch>`
|
|
591
|
+
* (docs/adr/0034-stock-controls-over-the-roles.md): drawn from the state
|
|
592
|
+
* you declare, `checked` (and on a checkbox `mixed`, the select-all box
|
|
593
|
+
* over a partial selection), with its text beside it. A press by the
|
|
594
|
+
* pointer, Space, Enter or assistive technology posts `onClick`; your
|
|
595
|
+
* `update` flips the model and the view draws it again. Its look is its
|
|
596
|
+
* spec, so these are the only rows it reads (`TOGGLE_ROWS_JSX` in
|
|
597
|
+
* schema.rs): any other is dropped with an `unknown-prop` warning. Keyed
|
|
598
|
+
* by its text unless `key` says otherwise. */
|
|
599
|
+
export interface ToggleProps
|
|
600
|
+
extends Keyed,
|
|
601
|
+
Pick<GeneratedSpecProps, 'onClick' | 'label' | 'description' | 'disabled' | 'checked' | 'mixed'>,
|
|
602
|
+
Pick<CustomSpecProps, 'tooltip'> {
|
|
603
|
+
children?: KuiNode;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** The stock slider (docs/adr/0034-stock-controls-over-the-roles.md): a
|
|
607
|
+
* track, a fill to `valueNow` and a thumb. With `onChange` the core turns
|
|
608
|
+
* a press, a drag, the arrows, PageUp / PageDown and Home / End into
|
|
609
|
+
* `{kind: 'change', value, phase: 'move' | 'end', tag}` — clamped to
|
|
610
|
+
* `valueMin`..`valueMax` (0..100 unset) and snapped to `valueStep` (a
|
|
611
|
+
* hundredth of the range unset); store `value` and declare it as
|
|
612
|
+
* `valueNow`, since nothing moves until you do. `label` is its name and,
|
|
613
|
+
* unless `key` says otherwise, its key. Its look is its spec: the rows it
|
|
614
|
+
* reads are these (`SLIDER_ROWS_JSX` in schema.rs). */
|
|
615
|
+
export interface SliderProps
|
|
616
|
+
extends Keyed,
|
|
617
|
+
Pick<
|
|
618
|
+
GeneratedSpecProps,
|
|
619
|
+
| 'label'
|
|
620
|
+
| 'description'
|
|
621
|
+
| 'disabled'
|
|
622
|
+
| 'valueNow'
|
|
623
|
+
| 'valueMin'
|
|
624
|
+
| 'valueMax'
|
|
625
|
+
| 'valueStep'
|
|
626
|
+
| 'valueText'
|
|
627
|
+
| 'onChange'
|
|
628
|
+
| 'width'
|
|
629
|
+
| 'minWidth'
|
|
630
|
+
| 'maxWidth'
|
|
631
|
+
>,
|
|
632
|
+
Pick<CustomSpecProps, 'tooltip'> {
|
|
633
|
+
label: string;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** A container of `<radio>`s (docs/adr/0034-stock-controls-over-the-roles.md):
|
|
637
|
+
* one Tab stop whose arrows, Home and End move the choice and press the
|
|
638
|
+
* radio they land on, so radios whose `onClick` each set the choice
|
|
639
|
+
* answer the keyboard with nothing more. Every box row; the role and the
|
|
640
|
+
* name are its own, and without a `gap` it takes the stock one. */
|
|
641
|
+
export interface RadioGroupProps extends BoxProps {
|
|
642
|
+
label: string;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export interface EditProps extends TextProps, GeneratedSpecProps, CustomSpecProps {
|
|
646
|
+
/** Stable identity (state is retained by key); `key` works too. */
|
|
647
|
+
id?: string;
|
|
648
|
+
initial?: string;
|
|
649
|
+
multiline?: boolean;
|
|
650
|
+
/** Takes focus once, on the frame the flag starts being declared, and only
|
|
651
|
+
* while nothing holds focus (ADR 0022, decision 9); a blur afterwards
|
|
652
|
+
* stands. `focus(key)` moves it at any other time. */
|
|
653
|
+
autofocus?: boolean;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
type Component<P> = (props: P) => KuiNode;
|
|
657
|
+
|
|
658
|
+
export declare function jsx<P>(type: Component<P>, props: P, key?: string | number): KuiNode;
|
|
659
|
+
export declare function jsx(type: string, props: object, key?: string | number): KuiElement;
|
|
660
|
+
export declare const jsxs: typeof jsx;
|
|
661
|
+
export declare const jsxDEV: typeof jsx;
|
|
662
|
+
export declare const Fragment: unique symbol;
|
|
663
|
+
|
|
664
|
+
export declare namespace JSX {
|
|
665
|
+
type Element = KuiNode;
|
|
666
|
+
type ElementType = string | Component<any>;
|
|
667
|
+
interface ElementChildrenAttribute {
|
|
668
|
+
children: {};
|
|
669
|
+
}
|
|
670
|
+
interface IntrinsicAttributes {
|
|
671
|
+
key?: string | number;
|
|
672
|
+
}
|
|
673
|
+
interface IntrinsicElements {
|
|
674
|
+
box: BoxProps;
|
|
675
|
+
text: TextProps;
|
|
676
|
+
button: ButtonProps;
|
|
677
|
+
checkbox: ToggleProps;
|
|
678
|
+
radio: ToggleProps;
|
|
679
|
+
switch: ToggleProps;
|
|
680
|
+
radioGroup: RadioGroupProps;
|
|
681
|
+
slider: SliderProps;
|
|
682
|
+
edit: EditProps;
|
|
683
|
+
/** The stock single-line field with its chrome (`widgets::text_input`):
|
|
684
|
+
* `label` is the key and the accessible name both, `initial` the seed
|
|
685
|
+
* (a new editor only), and nothing else is read — the same door as
|
|
686
|
+
* Lua's `input { label= }` and C's `kui_text_input`. Read it back
|
|
687
|
+
* with `editText(label)`; a field that needs any other row is an
|
|
688
|
+
* `<edit>` in a box of its own. */
|
|
689
|
+
input: { key?: string | number; label?: string; initial?: string };
|
|
690
|
+
/** The stock select (`widgets::select_items`): a field showing the
|
|
691
|
+
* choice in force that, clicked, opens the core's own menu of the
|
|
692
|
+
* options under it with the current one checked — drawn, or the
|
|
693
|
+
* platform's where the host shows menus itself; Escape or a press
|
|
694
|
+
* outside closes it. `label` is the key and the accessible name;
|
|
695
|
+
* `options` are strings (posting the label) or the menu items
|
|
696
|
+
* `openMenu` takes (posting `id`); `current` is the index in force,
|
|
697
|
+
* from 0. You hold no open state: the choice arrives as a `MenuMsg`
|
|
698
|
+
* on the field's key — `{kind: 'menu', item}` — and drawing the
|
|
699
|
+
* field again with the new `current` is the whole loop. Nothing else
|
|
700
|
+
* is read; a field that needs any other row is a `<box role="button">`
|
|
701
|
+
* and `openMenu`. Lua spells it `dropdown { }`, C `kui_select`. */
|
|
702
|
+
select: { key?: string | number; label?: string; options: (string | MenuItemInput)[]; current?: number };
|
|
703
|
+
/** The node form of a tooltip (`widgets::tooltip`): a float hanging
|
|
704
|
+
* below the parent, always drawn — where the `tooltip` prop is
|
|
705
|
+
* hover-gated — for a hint the view gates itself
|
|
706
|
+
* (`{hovered && <tooltip value="hint"/>}`). `value` alone is the
|
|
707
|
+
* text; children are the float's own content. */
|
|
708
|
+
tooltip: Keyed & { value?: string; children?: KuiNode };
|
|
709
|
+
/** Styled run inside a rich <text>: bold/italic/color, nestable. */
|
|
710
|
+
span: SpanProps;
|
|
711
|
+
/** A registered image (id from addImage). Fit sizing = pixel size as
|
|
712
|
+
* logical px; Fit height against a resolved width keeps the aspect.
|
|
713
|
+
* Two rows say how the pixels meet the box
|
|
714
|
+
* (docs/adr/0025-the-image-is-the-canvas.md): `sampling` is
|
|
715
|
+
* `linear` (default) or `nearest` — pixel art, an emulator, a data
|
|
716
|
+
* grid that must stay square under zoom; `fit` is `fill` (default:
|
|
717
|
+
* the pixels stretch to the box), `contain` (the largest rect of the
|
|
718
|
+
* image's aspect that fits, centred) or `cover` (the box filled and
|
|
719
|
+
* the rest cropped, centred). The box itself — layout, hit region,
|
|
720
|
+
* access rect — is the same in every mode. The pixels come from the
|
|
721
|
+
* atlas, or from a texture of the image's own once `updateImage` has
|
|
722
|
+
* replaced them; the node cannot tell and need not. */
|
|
723
|
+
image: Omit<BoxProps, 'children'> & {
|
|
724
|
+
src: string;
|
|
725
|
+
sampling?: 'linear' | 'nearest';
|
|
726
|
+
fit?: 'fill' | 'contain' | 'cover';
|
|
727
|
+
};
|
|
728
|
+
/** A box a registered WGSL function paints
|
|
729
|
+
* (docs/adr/0015-a-fragment-element-and-the-painter-it-is-not.md):
|
|
730
|
+
* gradients, rings, noise, shimmer — anything the paint vocabulary has
|
|
731
|
+
* no prop for. An ordinary node otherwise: it lays out, rounds, clips,
|
|
732
|
+
* fades, takes input and holds children, which paint over it. It has
|
|
733
|
+
* **no intrinsic size**, so give it a `width`/`height` or `fill`.
|
|
734
|
+
* `src` is an id from `addFragment`; `params` is up to sixteen numbers
|
|
735
|
+
* the shader reads as four `vec4<f32>`, and more are dropped with a
|
|
736
|
+
* warning; `animate` asks for a frame every frame, which a fragment
|
|
737
|
+
* reading `time` needs and a still one must not declare. `image` is an
|
|
738
|
+
* id from `addImage` the function reads through `kui_sample(uv)` /
|
|
739
|
+
* `kui_sample_nearest(uv)`, its texel rect in `in.image` — a waveform,
|
|
740
|
+
* a heatmap, an image effect from a texture the app replaces
|
|
741
|
+
* (docs/adr/0025-the-image-is-the-canvas.md, decision 7); an image
|
|
742
|
+
* that is not live draws nothing, as a dead `src` does. */
|
|
743
|
+
fragment: BoxProps & { src: string; image?: string; params?: number[] };
|
|
744
|
+
/** A position an extension fills, in place
|
|
745
|
+
* (docs/adr/0014-slots-an-extension-fills-in-place.md): whatever was
|
|
746
|
+
* loaded under the name's namespace draws here, as a child of this
|
|
747
|
+
* node, at this position among its siblings.
|
|
748
|
+
*
|
|
749
|
+
* `name` is the full `namespace/slot` — the namespace you loaded the
|
|
750
|
+
* plugin under (`ctx.addExtension('todos', path)`, or a window's
|
|
751
|
+
* `extensions` option) and the slot in the plugin's own vocabulary.
|
|
752
|
+
* `params` is whatever the plugin should read this frame: plain data,
|
|
753
|
+
* declared every frame, retained by nobody, like `onClick`'s payload.
|
|
754
|
+
* `"ns/root"` is the fill that follows the view for a plugin naming no
|
|
755
|
+
* slots, and declaring it moves that fill here.
|
|
756
|
+
*
|
|
757
|
+
* A position, not a box: it takes no other props, and with nothing
|
|
758
|
+
* loaded under that namespace it places an empty node so a view can
|
|
759
|
+
* declare its layout before it has a plugin to put in it. */
|
|
760
|
+
slot: { name: string; params?: unknown };
|
|
761
|
+
/** A tab in the core's devtools panel, beside facts, events and tree
|
|
762
|
+
* (docs/adr/0032-a-devtools-tab-mounts-a-slot.md). `name` is the tab's
|
|
763
|
+
* identity, `label` what the strip shows (the name when left out).
|
|
764
|
+
*
|
|
765
|
+
* Two forms. `slot` names a slot an extension fills, the full
|
|
766
|
+
* `namespace/slot` a plugin you loaded names: while the tab is on show
|
|
767
|
+
* the panel declares that slot in the tab's body and the plugin draws
|
|
768
|
+
* there; otherwise the plugin is not asked, and its naming the slot
|
|
769
|
+
* raises no `unknown-slot`. A **function child** is the app's own
|
|
770
|
+
* content, `{() => <column>…</column>}`: called only while the tab is
|
|
771
|
+
* on show — `frame` / `setView` read which tab that is once before
|
|
772
|
+
* encoding — so a tab nobody looks at costs its declaration and
|
|
773
|
+
* nothing else. What it returns is the app's: its keys, its events
|
|
774
|
+
* reaching `update` untouched, laid out and painted as a layer over
|
|
775
|
+
* the panel's tab body, clipped to it, in the dock's focus region.
|
|
776
|
+
* Read the panel's facts through `devtoolsSelected()` and its
|
|
777
|
+
* siblings; drive its highlight with `setDevtoolsSelected(key)`.
|
|
778
|
+
*
|
|
779
|
+
* Not a node: declare it anywhere in the tree, every frame. A slot and
|
|
780
|
+
* a child together, or a child that is not a function, is a throw. A
|
|
781
|
+
* name declared twice in a frame is one tab and a `duplicate-tab`
|
|
782
|
+
* warning; the first stands. Docked only for the function form: with
|
|
783
|
+
* the panel in its own window the body says so. */
|
|
784
|
+
devtoolsTab: { name: string; label?: string; slot?: string; children?: () => unknown };
|
|
785
|
+
/** A round-capped stroke (docs/adr/0010-a-segment-primitive.md): one
|
|
786
|
+
* segment from `from` to `to`, a polyline through `points`, or a smooth
|
|
787
|
+
* curve through them with `curve`. Points are in the parent's box space
|
|
788
|
+
* (`float="viewport"` for viewport space). Never in layout: it floats,
|
|
789
|
+
* sized to its own bounding box, so it takes no room in a row or
|
|
790
|
+
* column. A stroke in its parent's box space is held by the parent's
|
|
791
|
+
* clip as a child is, its hit region with it, so it is cut at a
|
|
792
|
+
* scroller's edge with the row it is drawn in; a declared float is
|
|
793
|
+
* held that way only when it declares `clip` with a parent anchor, and
|
|
794
|
+
* a `float="viewport"` stroke escapes (backlog F78). `width` is the stroke width in px (default 1), `color` the
|
|
795
|
+
* stroke colour (default the foreground); `transition` eases the colour,
|
|
796
|
+
* and with `slide` beside it the stroke's position too — the points ride
|
|
797
|
+
* its box, so a stroke whose ends all move together slides with them,
|
|
798
|
+
* while one whose ends move apart resizes at once. A canvas of floats
|
|
799
|
+
* eases everything or nothing, connectors included.
|
|
800
|
+
* Hit by its shape (docs/adr/0026-hit-testing-by-shape.md): with
|
|
801
|
+
* `onClick`, `onDrag`, `onHover` or `hoverable`, a press within half
|
|
802
|
+
* its width of any piece (at least 4 px of grab) hits it and one
|
|
803
|
+
* elsewhere in its box falls through; with none it takes no input and
|
|
804
|
+
* has no access row, and with input it is a control — name it. */
|
|
805
|
+
line: Keyed &
|
|
806
|
+
Pick<
|
|
807
|
+
GeneratedSpecProps,
|
|
808
|
+
| 'opacity' | 'transition' | 'slide' | 'enter' | 'exit' | 'onLayout' | 'label' | 'role'
|
|
809
|
+
| 'onClick' | 'onDrag' | 'onHover' | 'hoverable' | 'cursor' | 'description'
|
|
810
|
+
> & Pick<CustomSpecProps, 'tooltip'> & {
|
|
811
|
+
from?: [number, number];
|
|
812
|
+
to?: [number, number];
|
|
813
|
+
points?: [number, number][];
|
|
814
|
+
curve?: boolean;
|
|
815
|
+
width?: number;
|
|
816
|
+
color?: ColorProp;
|
|
817
|
+
float?: 'parent' | 'viewport';
|
|
818
|
+
};
|
|
819
|
+
/** A filled polygon through up to eight `points`, the fill in `bg`
|
|
820
|
+
* (docs/adr/0025-the-image-is-the-canvas.md, decision 6): an arrowhead,
|
|
821
|
+
* a pie slice, the area under a curve. Placed as a `line` is — always
|
|
822
|
+
* a float in its parent's box space (`float="viewport"` for viewport
|
|
823
|
+
* space), sized to its own bounding box a pixel out on each side, so
|
|
824
|
+
* it takes no room in a row or column. Like a `line`, it is held by its
|
|
825
|
+
* parent's clip, hit region included, unless it is
|
|
826
|
+
* `float="viewport"` (backlog F78). `transition` eases the fill and,
|
|
827
|
+
* with `slide`, its position. The outline may be concave; a
|
|
828
|
+
* self-intersecting one fills even-odd, its overlaps unfilled. Hit by
|
|
829
|
+
* its outline
|
|
830
|
+
* (docs/adr/0026-hit-testing-by-shape.md): with `onClick`, `onDrag`,
|
|
831
|
+
* `onHover` or `hoverable`, a press inside the outline hits it and one
|
|
832
|
+
* in its box past the outline falls through — a pie's wedges need no
|
|
833
|
+
* hit boxes; with none it takes no input and has no access row, and
|
|
834
|
+
* with input it is a button — name it. A ninth point and later are
|
|
835
|
+
* dropped with `polygon-points-truncated`; fewer than three draw
|
|
836
|
+
* nothing; no `bg`, no fill. One `fragment` quad on the wire, painted
|
|
837
|
+
* by a WGSL function the core registers itself. */
|
|
838
|
+
polygon: Keyed &
|
|
839
|
+
Pick<
|
|
840
|
+
GeneratedSpecProps,
|
|
841
|
+
| 'opacity' | 'transition' | 'slide' | 'enter' | 'exit' | 'onLayout' | 'label' | 'role'
|
|
842
|
+
| 'onClick' | 'onDrag' | 'onHover' | 'hoverable' | 'hoverBg' | 'cursor' | 'description'
|
|
843
|
+
> & Pick<CustomSpecProps, 'tooltip'> & {
|
|
844
|
+
points: [number, number][];
|
|
845
|
+
bg?: ColorProp;
|
|
846
|
+
float?: 'parent' | 'viewport';
|
|
847
|
+
};
|
|
848
|
+
/** Adaptive titlebar (drag strip + window buttons per env facts).
|
|
849
|
+
* `title` alone draws the standard title; children host custom content. */
|
|
850
|
+
titlebar: Keyed & { title?: string; children?: KuiNode };
|
|
851
|
+
/** Just the min/max/close buttons, for fully custom titlebars. */
|
|
852
|
+
windowButtons: Keyed;
|
|
853
|
+
/** The application menu (`docs/adr/0018-a-menu-bar-the-app-declares.md`):
|
|
854
|
+
* `menu` is what it is, and where this element sits is where its
|
|
855
|
+
* titles go when they have to be drawn in the window. Draws nothing
|
|
856
|
+
* where the platform owns the bar (macOS, where the driver hands the
|
|
857
|
+
* same declaration to the OS), so one view is portable. `[]` takes
|
|
858
|
+
* the menu away. */
|
|
859
|
+
menuBar: Keyed & { menu?: MenuBarInput };
|
|
860
|
+
/** Per-phase frame-latency bars (input/view/layout/render) vs the
|
|
861
|
+
* display budget. Reads the runner's frame stats — renders empty
|
|
862
|
+
* when headless. */
|
|
863
|
+
latencyGraph: Keyed;
|
|
864
|
+
/** latencyGraph in a translucent panel floating in a viewport corner. */
|
|
865
|
+
latencyHud: Keyed & { at?: [AlignProp, AlignProp] };
|
|
866
|
+
/** A playback retained by node key (id from addSound): present = playing
|
|
867
|
+
* (once, or looped), gone = stopped; `volume` / `paused` apply live, a
|
|
868
|
+
* changed `src` restarts. `finish` changes what gone means — the
|
|
869
|
+
* removal releases the playback to play itself out, so a one-shot need
|
|
870
|
+
* not stay declared for a length the view would have to guess (a loop
|
|
871
|
+
* still stops). `tag` comes back as a `SoundMsg` when it ends on its
|
|
872
|
+
* own, a released playback included. A released playback holds one of
|
|
873
|
+
* the device's 128 voices until its file ends — voices held = sound
|
|
874
|
+
* length × release rate, so a 1.4 s chime released four times a second
|
|
875
|
+
* holds 6 — and the 129th play is refused (`phase: "refused"` on the
|
|
876
|
+
* `tag`). Draws nothing and takes no space. */
|
|
877
|
+
audio: Keyed & { src: string; loop?: boolean; volume?: number; paused?: boolean; finish?: boolean; tag?: AppMsg };
|
|
878
|
+
/** A terminal's screen as one node (backlog C20): `rows × cols` cells,
|
|
879
|
+
* four entries each in `cells` — codepoint, fg, bg (`0xRRGGBBAA`, 0 = no
|
|
880
|
+
* background), flags (1 bold, 2 italic, 4 underline, 8 strikethrough,
|
|
881
|
+
* 16 wide) — row-major, in a `Uint32Array` an app keeps and mutates. A
|
|
882
|
+
* glyph is shaped once per character and placed at `col × cell_w` ever
|
|
883
|
+
* after, so a screen new every frame costs what a still one costs. The
|
|
884
|
+
* style rows size the cells (`size`, `family`/`font`, `lineHeight`);
|
|
885
|
+
* the node rows apply — `onKey` makes it the terminal's sink, `onClick`
|
|
886
|
+
* / `onDrag` events carry `cell: {row, col}`. `cursorAt` is a cell to
|
|
887
|
+
* paint under its glyph in `cursorColor`, as a block, bar or underline
|
|
888
|
+
* (`cursor` stays the pointer shape).
|
|
889
|
+
* Its access role is `terminal`, the rows joined as its value. */
|
|
890
|
+
cells: Omit<TextProps, 'children'> &
|
|
891
|
+
GeneratedSpecProps &
|
|
892
|
+
CustomSpecProps & {
|
|
893
|
+
rows: number;
|
|
894
|
+
cols: number;
|
|
895
|
+
/** Four entries a cell — codepoint, fg, bg, flags — or five, the
|
|
896
|
+
* fifth the underline's own colour (0 = fg), row-major. Flags: 1
|
|
897
|
+
* bold, 2 italic, 4 underline, 8 strikethrough, 16 wide, 32 the
|
|
898
|
+
* underline is a wave (undercurl), 64 dotted. */
|
|
899
|
+
cells: Uint32Array | number[];
|
|
900
|
+
cursorAt?: [number, number];
|
|
901
|
+
cursorShape?: 'block' | 'bar' | 'underline';
|
|
902
|
+
cursorColor?: ColorProp;
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
}
|