@qxuken/kui 0.1.0-alpha.45 → 0.1.0-alpha.47

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/howto.md CHANGED
@@ -37,6 +37,101 @@ because the removal is judged whole rather than half-animated, and the
37
37
  [ADR 0012](docs/adr/0012-the-exit-budget.md) ·
38
38
  [alpha.8](CHANGELOG.md#010-alpha8-2026-09-07)
39
39
 
40
+ ### How do I throw a card left or right from a button?
41
+
42
+ Aim the exit in the handler that removes the card:
43
+ `ui.exit_with(key, Enter::from(400.0, 0.0).opacity(0.0))` in Rust
44
+ (`env.exit_with` in Lua, `kui_exit_with` in C, `ctx.exitWith(key, { dx:
45
+ 400, opacity: 0 })` in Node), then stop declaring the card. The exit a
46
+ node leaves by is otherwise the one its last frame declared, so a Like
47
+ button pressed with the card at rest would send it the default way; the
48
+ named exit wins for the frame the card goes in, and lapses when that
49
+ frame finishes if the card stayed. It aims a node that declares an
50
+ `exit` and a `transition`, and plays with that transition — so give the
51
+ card a default `exit`, and a drag that already aims it by the lean on
52
+ every frame needs nothing more.
53
+
54
+ [`exit` row](props.md#container-props) ·
55
+ [`Ui::exit_with` door](props.md#doors)
56
+
57
+ ### How do I tilt a card as it is dragged?
58
+
59
+ `rotate` on the card, in turns clockwise (`0.03` is a tilt, `0.25` a
60
+ quarter turn), read from the drag's `dx` in `on_event`; `scale` and
61
+ `pivotX` / `pivotY` (fractions of the box, the centre by default) sit
62
+ beside it. The turn is paint-only — the card keeps the room its upright
63
+ self takes — and everything under it turns with it: the photo it clips
64
+ inside its rounded corners, a badge floating on its corner, its text,
65
+ and its hit region, so it is grabbed on its tilted edge. Leave the
66
+ `transition` off while the pointer holds the card, so the tilt follows
67
+ at once, and put it back on release: the turn is a slot like `bg` or
68
+ `width`, so the spring brings it home. `enter: { scale: 0.8 }` settles
69
+ a chip in, `exit: { rotate: 0.1, scale: 0 }` spins one away, and a
70
+ spinner is a box with `keyframes: [{ rotate: 0 }, { rotate: 1 }]`. A
71
+ `path`'s own `rotate` is ADR 0041's and does not tween; put the path in
72
+ a box for one that does.
73
+
74
+ [`rotate` row](props.md#container-props) ·
75
+ [ADR 0043](docs/adr/0043-a-node-turns-about-its-pivot.md) ·
76
+ [`transform` example](../examples/rust/features/transform.rs)
77
+
78
+ ### How do I make a node bob, drift or shake by itself?
79
+
80
+ Give it `keyframes` whose stops name `dx` / `dy`: logical px from where
81
+ layout put it, sampled off the cycle like any other slot, so the view
82
+ declares the stops once and never redraws for them. `[{ dy: 0 }, { dy:
83
+ -6 }]` with `repeat: 'alternate'` bobs it; stops that rise and fade drift
84
+ a sparkle up; a few small alternating `dx` stops on a short cycle shake
85
+ it. The node and everything under it are drawn and hit where the stop
86
+ puts them, and the offset adds to a `slide`, but the room it takes is its
87
+ place's: siblings do not move, and `onLayout` reports the layout rect
88
+ rather than a cycle that would post an event every frame. A cycle runs
89
+ for as long as the node is declared and asks for a frame every vsync,
90
+ unless `iterations` says how many times.
91
+
92
+ [`keyframes` row](props.md#container-props) ·
93
+ [`transition` example](../examples/rust/features/transition.rs)
94
+
95
+ ### How do I play a keyframe animation once?
96
+
97
+ `iterations: 1` on the node with the `keyframes` (CSS's
98
+ `animation-iteration-count`). The cycle plays from the first frame the
99
+ node is declared with it, rests where it ended — the last stop, or the
100
+ first for `reverse`, or the start again for `2` alternating — and then
101
+ asks for nothing, so `animating()` goes quiet and a test's `advance` runs
102
+ it out. Before its `delay` it holds its first stop, so a row of stars
103
+ given `delay: i * 150` and `iterations: 1` pops in one after another
104
+ without showing their own values first. To play it again, drop the
105
+ stops for a frame and declare them again, or give the node a new key: a
106
+ finite cycle restarts whenever a frame went without it. A blinking caret
107
+ is still not keyframes (see the caret how-to): it is a cycle with no end.
108
+
109
+ [`iterations` row](props.md#container-props) ·
110
+ [`keyframes` row](props.md#container-props) ·
111
+ [`transition` example](../examples/rust/features/transition.rs)
112
+
113
+ ### How do I show a photo or a GIF my app ships?
114
+
115
+ Decode it with the runner, which links the decoder it draws its
116
+ wallpaper with: `kui_native::decode_image(bytes)` turns PNG, JPEG, WebP
117
+ or GIF bytes into straight RGBA and a size, which `add_image` takes as it
118
+ is (`decodeImage` in Node, `kui_decode_image` in C, freed with
119
+ `kui_pixels_free`). No `image` dependency of the app's own. For an
120
+ animated GIF, APNG or WebP, `decode_animation` keeps every frame — the
121
+ whole canvas each, as a browser composites it — with the seconds each
122
+ shows. Play it on the frame clock: keep when it started, ask
123
+ `anim.at(ui.now() - started)` which frame shows and when the next
124
+ is due, `update_image_with` the pixels when the index moved, and
125
+ `request_frame_at(started + next)`. Nothing is owed between steps, so a
126
+ spinner at 80 ms a step costs twelve frames a second and an idle window
127
+ otherwise, and a paused one asks for nothing. A GIF frame that says 10 ms
128
+ or less shows for 100, the way browsers show it; a finite loop count
129
+ rests on the last frame. Lua is a guest and gets the handle from its
130
+ host.
131
+
132
+ [`decode` example](../examples/rust/features/decode.rs) ·
133
+ [`image` element](props.md#elements)
134
+
40
135
  ### How do I draw a connector between two boxes?
41
136
 
42
137
  `<line from={[x, y]} to={[x, y]} width color/>` is one round-capped stroke,
@@ -1207,7 +1302,8 @@ blinks in the background. Headless the phase stays true;
1207
1302
  `setCaretVisible(false)` / `kui_set_caret_visible` is how a test sees
1208
1303
  the off phase drawn, and how a C host with its own window drives it
1209
1304
  (`kui_has_caret`, `kui_caret_stamp` are the clock's inputs). Never use
1210
- `keyframes` for this: they ask for a frame every vsync and never stop.
1305
+ `keyframes` for this: a blink has no end, and an endless cycle asks for a
1306
+ frame every vsync.
1211
1307
 
1212
1308
  A caret that does not blink — the block of a modal editor's normal mode —
1213
1309
  declares `caretSolid` (Lua `caret_solid = true`, C `KUI_VALUE_CARET_SOLID`
@@ -1700,7 +1796,48 @@ re-renders. `ctx.setTime` under a loop throws and names `advance` — the loop
1700
1796
  owns the clock — and `startTime` in the options pins where that clock starts
1701
1797
  so assertions on `tick.msg(now)` are exact.
1702
1798
 
1703
- [alpha.8 `**What breaks.**`](CHANGELOG.md#010-alpha8-2026-09-07)
1799
+ Keep the app's own deadlines on that clock too. `now()` — `ui.now()` in
1800
+ Rust, `env.now` in Lua, `kui_now` in C, `ctx.now()` in Node — is the
1801
+ frame clock in seconds, the one `transition` and `keyframes` read (0
1802
+ before a driver sets it), so a toast due at `shownAt + 3` and the fade
1803
+ that takes it away agree, and `advance` or `Drive::advance` moves both.
1804
+ A view that reads `Instant::now()` or `Date.now()` keeps
1805
+ a second clock no test can move.
1806
+
1807
+ [alpha.8 `**What breaks.**`](CHANGELOG.md#010-alpha8-2026-09-07) ·
1808
+ [`now` in the env](props.md#env)
1809
+
1810
+ ### How do I hide a toast after three seconds without a timer thread?
1811
+
1812
+ Keep the time it is due on the frame clock, and ask for a frame then:
1813
+ `ui.request_frame_at(shown_at + 3.0)` in Rust (`env.request_frame_at` in
1814
+ Lua, `kui_request_frame_at` in C, `ctx.requestFrameAt` in Node), read
1815
+ `now()` in the view, and stop drawing the toast once it is past. The
1816
+ runner sleeps to that time and runs the view, and nothing is owed in
1817
+ between, so `animating()` stays false and an idle app stays idle. The
1818
+ earliest time asked for wins and is kept until a frame reaches it, so a
1819
+ view may ask every frame or once. From a thread that is not the view's —
1820
+ a download finishing on its own schedule — `Waker::wake_at(instant)`
1821
+ says the same thing to every window. A test moves the clock past the
1822
+ time and draws; a host driving its own window reads
1823
+ `kui_next_frame_at` and sleeps to it.
1824
+
1825
+ [`now` in the env](props.md#env)
1826
+
1827
+ ### How do I press "the button named Like" in a test?
1828
+
1829
+ Look it up by the name a reader hears: `Drive::key_named("Like")` in
1830
+ Rust (`Core::key_named` under it, `ctx.keyNamed` in Node, `kui_key_named`
1831
+ in C), then `click_key` it. The name is the node's `label` row, else the
1832
+ text inside a control, so a test that finds a button this way also checks
1833
+ that a screen reader can. `key_of` asks a different question: the *key
1834
+ label*, the name the view opened the node under (`with_keyed("like",
1835
+ ..)`, a `key` prop), which a reader never hears and `texts_under` reads
1836
+ too. Two nodes with one name resolve to the first in tree order and raise
1837
+ `ambiguous-name` — the same two a reader cannot tell apart.
1838
+
1839
+ [`label` row](props.md#container-props) ·
1840
+ [`ambiguous-name`](props.md#warnings)
1704
1841
 
1705
1842
  ### How do I test the real window, not a headless core?
1706
1843
 
@@ -1938,7 +2075,8 @@ it again after an upgrade; it is generated, never edited.
1938
2075
  Tell the launcher, once, and every window it creates carries it:
1939
2076
  `kui_native::app("t").icon(rgba, w, h)` with straight RGBA pixels, row by row
1940
2077
  — something a taskbar shrinks cleanly, 64 to 256 px, rendered from your
1941
- drawing at build time or decoded from a PNG you ship — and, for a
2078
+ drawing at build time — or `.icon_bytes(include_bytes!("icon.png"))` for
2079
+ a PNG you ship, decoded by the runner — and, for a
1942
2080
  Windows program, `.icon_resource(1)` too. A Windows program's icon is a
1943
2081
  resource linked into its executable (a `1 ICON "app.ico"` line in its
1944
2082
  `.rc`, compiled by `embed-resource` or the like in `build.rs`), which is
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AppMsg,
3
3
  BoxProps,
4
+ EnterProp,
4
5
  GeneratedSpecProps,
5
6
  KuiNode,
6
7
  MenuItemInput,
@@ -805,6 +806,12 @@ export type WarningCode =
805
806
  * declares, or pass the hex key an event carried. Two nodes with the *same*
806
807
  * key are `duplicate-key`. */
807
808
  | 'ambiguous-key'
809
+ /** A lookup by accessible name (`Core::key_named`, `ctx.keyNamed`,
810
+ * `kui_key_named`) found more than one node with that name in the last frame
811
+ * — two buttons both read as "Delete" — and used the first in tree order. A
812
+ * reader hears the same name twice too: give each a `label` that says which,
813
+ * or look the one meant up by its key label. */
814
+ | 'ambiguous-name'
808
815
  /** A `focusRegion(name)` (`Core::focus_region`, `env.focus_region`,
809
816
  * `kui_focus_region`) named a node the frame after it did not declare as a
810
817
  * `focusRegion` — no node under the label, or a node without the row — so
@@ -1381,6 +1388,10 @@ export interface NodeInfo {
1381
1388
  opacity: number;
1382
1389
  /** Its `backdropBlur` radius in px, 0 for none (backlog F129). */
1383
1390
  backdropBlur: number;
1391
+ /** Its turn in turns and its scale as drawn this frame, eased (ADR
1392
+ * 0043): 0 and 1 for a node that declares none. */
1393
+ rotate: number;
1394
+ scale: number;
1384
1395
  /** A scroller's offset; `null` for a node that does not scroll. */
1385
1396
  scroll: { x: number; y: number } | null;
1386
1397
  /** Every handler it declared with the payload it would post: `click`,
@@ -3047,6 +3058,37 @@ export declare class Ctx {
3047
3058
  * while the window has no keyboard. Always `true` headless.
3048
3059
  */
3049
3060
  caretVisible(): boolean
3061
+ /**
3062
+ * The frame clock in seconds (backlog F134): the one the
3063
+ * tweens and cycles read, `setTime` or the loop's own, 0
3064
+ * before either. Read a deadline off it rather than off
3065
+ * `Date.now()`, so `advance` moves both.
3066
+ */
3067
+ now(): number
3068
+ /**
3069
+ * Asks for a frame at `at` on the frame clock (backlog F135),
3070
+ * in seconds: a toast's expiry, a sequence's next beat. Nothing
3071
+ * is owed until then, so `animating()` stays false; a window
3072
+ * sleeps to it and draws, and the earliest time asked for wins.
3073
+ * A headless loop's `advance` past it draws the frame it ends
3074
+ * on, as it does for a `tick`.
3075
+ */
3076
+ requestFrameAt(at: number): void
3077
+ /**
3078
+ * The exit the node `key` leaves by if it leaves in the frame
3079
+ * that finishes next (backlog F136), over the `exit` it
3080
+ * declared: `{dx: 400, opacity: 0}`, the same fields as
3081
+ * `exit`. A card a button throws aside is removed in the same
3082
+ * handler, with no frame drawn first to aim it. It aims a node
3083
+ * that declares an `exit`, with that node's transition, and is
3084
+ * forgotten when that frame finishes.
3085
+ */
3086
+ exitWith(key: string, exit: EnterProp): void
3087
+ /**
3088
+ * The frame-clock time the next asked-for frame is due at, in
3089
+ * seconds; `null` when none was asked for.
3090
+ */
3091
+ nextFrameAt(): number | null
3050
3092
  /**
3051
3093
  * Whether there is a caret to blink: a focused `<edit>`'s, or
3052
3094
  * the `caret` a `line` under the focused sink declares — unless
@@ -3078,8 +3120,9 @@ export declare class Ctx {
3078
3120
  */
3079
3121
  focus(key: string): void
3080
3122
  /**
3081
- * The hex key of the node a label names — the label a `key`
3082
- * prop declared, resolved through the frame being built so
3123
+ * The hex key of the node a key label names — the label a `key`
3124
+ * prop declared, not the accessible name a reader hears
3125
+ * (`keyNamed`), resolved through the frame being built so
3083
3126
  * far and then the last finished one — or null when no node
3084
3127
  * declared it. The door for holding a key across frames;
3085
3128
  * every call that takes a key takes the label too, so this
@@ -3090,6 +3133,16 @@ export declare class Ctx {
3090
3133
  * plugin filling a slot is answered from its own nodes only.
3091
3134
  */
3092
3135
  keyOf(label: string): string | null
3136
+ /**
3137
+ * The hex key of the first node in the last finished frame
3138
+ * whose accessible name is `name` (backlog F137) — its
3139
+ * `label` prop, else its own text, else a control's derived
3140
+ * name — so a test clicks "the button named Like" as a reader
3141
+ * would: `ctx.click(ctx.keyNamed('Like'))`. Not `keyOf`'s key
3142
+ * label, which a reader never hears. Null when no node has the
3143
+ * name; two raise `ambiguous-name`.
3144
+ */
3145
+ keyNamed(name: string): string | null
3093
3146
  blur(): void
3094
3147
  /**
3095
3148
  * What Tab does, as a call — for an `onKey` sink that binds Tab
@@ -3509,6 +3562,66 @@ export declare class Ctx {
3509
3562
  setEditText(key: string, text: string): void
3510
3563
  }
3511
3564
 
3565
+ /**
3566
+ * Decoded pixels (`decodeImage`): `width` by `height`, four bytes each
3567
+ * (RGBA), row by row from the top left, alpha not premultiplied — what
3568
+ * `addImage`, `updateImage` and a window's `icon` take.
3569
+ */
3570
+ export interface DecodedImage {
3571
+ width: number
3572
+ height: number
3573
+ rgba: Buffer
3574
+ }
3575
+
3576
+ /**
3577
+ * Every frame of an animated image (`decodeAnimation`), each the whole
3578
+ * canvas, with the seconds each shows and how many times the sequence
3579
+ * plays (absent: for ever).
3580
+ */
3581
+ export interface DecodedAnimation {
3582
+ width: number
3583
+ height: number
3584
+ frames: Array<Buffer>
3585
+ delays: Array<number>
3586
+ loops?: number
3587
+ }
3588
+
3589
+ /**
3590
+ * Which frame shows at a moment (`animationAt`), and the seconds after
3591
+ * the start the next is due — `Infinity` once a finite animation has
3592
+ * played out.
3593
+ */
3594
+ export interface AnimationShowing {
3595
+ index: number
3596
+ next: number
3597
+ }
3598
+
3599
+ /**
3600
+ * Decodes PNG, JPEG, WebP or GIF bytes (an animated file's first frame)
3601
+ * with the runner's decoder (backlog F138), so an app that ships a photo
3602
+ * needs no image package: `ctx.addImage(d.width, d.height, d.rgba)`.
3603
+ * Throws, naming why, for bytes that are not an image or do not decode.
3604
+ */
3605
+ export declare function decodeImage(bytes: Buffer): DecodedImage
3606
+
3607
+ /**
3608
+ * Decodes every frame of an animated GIF, PNG (APNG) or WebP, each the
3609
+ * whole canvas, and the seconds each shows; a GIF frame asking for 10 ms
3610
+ * or less shows for 100, as browsers show it. A still image is one frame
3611
+ * shown for ever. Play it on the frame clock with `animationAt`,
3612
+ * `updateImage` and `requestFrameAt`.
3613
+ */
3614
+ export declare function decodeAnimation(bytes: Buffer): DecodedAnimation
3615
+
3616
+ /**
3617
+ * Which of the frames with these `delays` shows `elapsed` seconds after
3618
+ * the animation started, playing `loops` times (null or 0 for ever), and when
3619
+ * the next is due: `animationAt(gif.delays, gif.loops, ctx.now() -
3620
+ * started)`, then `updateImage` when the index moved and
3621
+ * `requestFrameAt(started + next)`.
3622
+ */
3623
+ export declare function animationAt(delays: Array<number>, loops: number | undefined | null, elapsed: number): AnimationShowing
3624
+
3512
3625
  /** Byte stride of one quad in the `quads()` buffer. */
3513
3626
  export declare function quadStride(): number
3514
3627
 
@@ -4244,6 +4357,37 @@ export declare class KuiWindow {
4244
4357
  * while the window has no keyboard. Always `true` headless.
4245
4358
  */
4246
4359
  caretVisible(): boolean
4360
+ /**
4361
+ * The frame clock in seconds (backlog F134): the one the
4362
+ * tweens and cycles read, `setTime` or the loop's own, 0
4363
+ * before either. Read a deadline off it rather than off
4364
+ * `Date.now()`, so `advance` moves both.
4365
+ */
4366
+ now(): number
4367
+ /**
4368
+ * Asks for a frame at `at` on the frame clock (backlog F135),
4369
+ * in seconds: a toast's expiry, a sequence's next beat. Nothing
4370
+ * is owed until then, so `animating()` stays false; a window
4371
+ * sleeps to it and draws, and the earliest time asked for wins.
4372
+ * A headless loop's `advance` past it draws the frame it ends
4373
+ * on, as it does for a `tick`.
4374
+ */
4375
+ requestFrameAt(at: number): void
4376
+ /**
4377
+ * The exit the node `key` leaves by if it leaves in the frame
4378
+ * that finishes next (backlog F136), over the `exit` it
4379
+ * declared: `{dx: 400, opacity: 0}`, the same fields as
4380
+ * `exit`. A card a button throws aside is removed in the same
4381
+ * handler, with no frame drawn first to aim it. It aims a node
4382
+ * that declares an `exit`, with that node's transition, and is
4383
+ * forgotten when that frame finishes.
4384
+ */
4385
+ exitWith(key: string, exit: EnterProp): void
4386
+ /**
4387
+ * The frame-clock time the next asked-for frame is due at, in
4388
+ * seconds; `null` when none was asked for.
4389
+ */
4390
+ nextFrameAt(): number | null
4247
4391
  /**
4248
4392
  * Whether there is a caret to blink: a focused `<edit>`'s, or
4249
4393
  * the `caret` a `line` under the focused sink declares — unless
@@ -4275,8 +4419,9 @@ export declare class KuiWindow {
4275
4419
  */
4276
4420
  focus(key: string): void
4277
4421
  /**
4278
- * The hex key of the node a label names — the label a `key`
4279
- * prop declared, resolved through the frame being built so
4422
+ * The hex key of the node a key label names — the label a `key`
4423
+ * prop declared, not the accessible name a reader hears
4424
+ * (`keyNamed`), resolved through the frame being built so
4280
4425
  * far and then the last finished one — or null when no node
4281
4426
  * declared it. The door for holding a key across frames;
4282
4427
  * every call that takes a key takes the label too, so this
@@ -4287,6 +4432,16 @@ export declare class KuiWindow {
4287
4432
  * plugin filling a slot is answered from its own nodes only.
4288
4433
  */
4289
4434
  keyOf(label: string): string | null
4435
+ /**
4436
+ * The hex key of the first node in the last finished frame
4437
+ * whose accessible name is `name` (backlog F137) — its
4438
+ * `label` prop, else its own text, else a control's derived
4439
+ * name — so a test clicks "the button named Like" as a reader
4440
+ * would: `ctx.click(ctx.keyNamed('Like'))`. Not `keyOf`'s key
4441
+ * label, which a reader never hears. Null when no node has the
4442
+ * name; two raise `ambiguous-name`.
4443
+ */
4444
+ keyNamed(name: string): string | null
4290
4445
  blur(): void
4291
4446
  /**
4292
4447
  * What Tab does, as a call — for an `onKey` sink that binds Tab
package/index.js CHANGED
@@ -3,7 +3,10 @@
3
3
  import native from './native.cjs';
4
4
  import { createEncoder } from './encoder.js';
5
5
 
6
- export const { Ctx, KuiWindow, RowHeights, quadStride, clipStride, protocol } = native;
6
+ export const {
7
+ Ctx, KuiWindow, RowHeights, quadStride, clipStride, protocol,
8
+ decodeImage, decodeAnimation, animationAt,
9
+ } = native;
7
10
  export { createEncoder };
8
11
 
9
12
  // The brand on what `update` (or a function `init`) returns to hand the loop
@@ -1412,6 +1415,14 @@ export function decodeClips(buffer) {
1412
1415
  // Corner radii clockwise from the top-left: a clipping node with a
1413
1416
  // radius rounds what it clips. All zero = a plain rect clip.
1414
1417
  radii: [f[4], f[5], f[6], f[7]],
1418
+ // The turn every quad naming this entry is drawn through (ADR
1419
+ // 0043): angle in radians, scale, tx, ty — `[0, 1, 0, 0]`, the
1420
+ // identity, on a frame that turns nothing.
1421
+ transform: [f[8], f[9], f[10], f[11]],
1422
+ // A second clip in the quad's own space, before the turn, and its
1423
+ // radii: from clipping nodes inside a turned subtree.
1424
+ inner: [f[12], f[13], f[14], f[15]],
1425
+ innerRadii: [f[16], f[17], f[18], f[19]],
1415
1426
  });
1416
1427
  }
1417
1428
  return clips;
package/jsx-runtime.d.ts CHANGED
@@ -119,12 +119,20 @@ export type AlignProp = 'start' | 'center' | 'end';
119
119
  * animate their amount only, in the form the prop itself declares. */
120
120
  export interface KeyframeProp {
121
121
  at?: number;
122
+ /** Logical px across from where layout put the node, at this stop (backlog F132): the node and its subtree are drawn and hit there. Left out, 0. */
123
+ dx?: number;
124
+ /** Logical px down from where layout put the node, at this stop: `[{ dy: 0 }, { dy: -6 }]` with `repeat: 'alternate'` bobs it. Left out, 0. */
125
+ dy?: number;
122
126
  width?: SizingProp;
123
127
  height?: SizingProp;
124
128
  bg?: ColorProp;
125
129
  radius?: number;
126
130
  /** Group opacity, 0..1. */
127
131
  opacity?: number;
132
+ /** A turn in turns clockwise (ADR 0043). */
133
+ rotate?: number;
134
+ /** A uniform scale about the node's pivot. */
135
+ scale?: number;
128
136
  }
129
137
 
130
138
  /** Where a node starts the first frame it is seen, for `enter`: the slots
@@ -141,6 +149,10 @@ export interface EnterProp {
141
149
  radius?: number;
142
150
  /** Group opacity, 0..1: `{ opacity: 0 }` fades the whole subtree in. */
143
151
  opacity?: number;
152
+ /** A turn in turns clockwise (ADR 0043): `{ rotate: -0.02 }` swings in. */
153
+ rotate?: number;
154
+ /** A uniform scale about the node's pivot: `{ scale: 0.8 }` settles in. */
155
+ scale?: number;
144
156
  }
145
157
 
146
158
  /** A gradient painted over a box's `bg`, under its border and children
@@ -296,9 +308,9 @@ export interface GeneratedSpecProps {
296
308
  dropBg?: ColorProp;
297
309
  /** 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. */
298
310
  easing?: 'easeOut' | 'linear' | 'easeIn' | 'easeInOut' | 'spring' | 'bouncy' | 'smooth' | 'snappy';
299
- /** 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). */
311
+ /** Where the node starts the first frame it is seen `{ dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }`: 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, `scale: 0.8` settles it in). */
300
312
  enter?: EnterProp;
301
- /** 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. */
313
+ /** Where the node ends the frame after the view stops declaring it `{ dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }` — 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. The exit read is the one the last frame that had the node declared, unless `exit_with` named another for the frame it went in: a card a button throws left or right is aimed by the handler that removes it, with no frame drawn first to point it. Needs a stable key across frames. */
302
314
  exit?: EnterProp;
303
315
  /** 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. */
304
316
  expanded?: 'collapsed' | 'expanded';
@@ -324,13 +336,15 @@ export interface GeneratedSpecProps {
324
336
  hoverable?: boolean;
325
337
  /** 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. */
326
338
  initialFocus?: boolean;
339
+ /** How many times the `keyframes` cycle runs (CSS `animation-iteration-count`, backlog F133); left out, for ever. A finite cycle plays from the first frame the node is declared with it — a node that leaves and comes back plays again — holds its first stop through its `delay`, and rests where its last iteration ended (CSS's fill `both`): `1` plays a burst or a shake once, `2` with `repeat: "alternate"` goes out and back and ends where it began, `0.5` stops halfway. Once it is over the node owes no frame, so `animating()` and `owed()` go quiet as a settled transition's do; `delay` plus `iterations: 1` staggers one-shots. A count that is not a positive number is for ever. C: 0 is for ever. */
340
+ iterations?: LengthProp;
327
341
  /** 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. */
328
342
  keepFocus?: boolean;
329
343
  /** 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. */
330
344
  keyUp?: boolean;
331
- /** 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. */
345
+ /** CSS-style stops `[{ at?, dx?, dy?, width?, height?, bg?, radius?, opacity?, rotate?, scale? }, …]`: the slots they name cycle through them over `transition` ms, for ever unless `iterations` says how many times, without the view redrawing; `at` is 0..1 and spreads evenly when omitted. `dx` / `dy` are logical px from where layout put the node (backlog F132), as an entrance's are: the node and its subtree are drawn and hit that far away at the stop, a lane a stop leaves out is 0, and the offset adds to a `slide`'s, so `[{ dy: 0 }, { dy: -6 }]` with `repeat: 'alternate'` bobs a box and a sparkle drifts up its stops. Paint, hit and access only: layout and the room the node takes are its own place's, and an `onLayout` node reports its layout rect, not the cycle, which would post an event every frame it runs. */
332
346
  keyframes?: KeyframeProp[];
333
- /** 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`). */
347
+ /** 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`). Not the key label a `key` prop or `with_keyed` declares, which `key_of` looks up and a reader never hears; `key_named` looks a node up by this name. */
334
348
  label?: string;
335
349
  /** 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. */
336
350
  live?: 'off' | 'polite' | 'assertive';
@@ -378,6 +392,10 @@ export interface GeneratedSpecProps {
378
392
  opacity?: LengthProp;
379
393
  /** 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. */
380
394
  overscroll?: 'auto' | 'contain';
395
+ /** Where across the box `rotate` and `scale` are about, as a fraction of its width: 0 the left edge, 0.5 (the default) the middle, 1 the right edge; outside 0..1 is a point past the box. C: `pivot_x` with `pivot_set`. */
396
+ pivotX?: LengthProp;
397
+ /** Where down the box `rotate` and `scale` are about, as a fraction of its height: 0 the top, 0.5 (the default) the middle, 1 the bottom. C: `pivot_y` with `pivot_set`. */
398
+ pivotY?: LengthProp;
381
399
  /** 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. */
382
400
  pixelSnap?: boolean;
383
401
  /** Background while pressed (or while its hoverGroup is); implies hover tracking. */
@@ -396,10 +414,14 @@ export interface GeneratedSpecProps {
396
414
  repeat?: 'normal' | 'reverse' | 'alternate' | 'alternateReverse';
397
415
  /** 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. */
398
416
  role?: 'none' | 'button' | 'checkbox' | 'radio' | 'switch' | 'slider' | 'tab' | 'tabList' | 'link' | 'heading' | 'list' | 'listItem' | 'image' | 'dialog' | 'group' | 'textInput' | 'multilineTextInput' | 'line' | 'radioGroup' | 'menu' | 'menuItem' | 'terminal';
417
+ /** Turns this node and everything under it, in turns clockwise (0.25 is a quarter turn right), about its pivot — the centre unless `pivotX` / `pivotY` say — after layout (`docs/adr/0043-a-node-turns-about-its-pivot.md`). Paint-only: the node takes the room its upright self takes, nothing around it moves, `onLayout` reports the layout rect. Everything the subtree draws turns with it — backgrounds, borders, shadows, text, images, strokes, fragments — and so does what it clips: a child cut by a turned card's rounded corners stays inside them. Hit where drawn: a tilted card is grabbed on its tilted edge and a press in its box past its edge falls through; drag payloads stay in viewport px. The access rect is the bounding box. Nests by composition. Tweens with `transition` as one slot with `scale`, and an entrance, an exit or a keyframe stop may name it (`enter: { rotate: -0.02 }`, `keyframes: [{ rotate: 0 }, { rotate: 1 }]` spins a box). A float anchored to the parent turns with it; a viewport float does not. On a `path` this is the path's own turn (ADR 0041), which does not tween — wrap it in a box for one that does. Text under a turn leaves the pixel grid, as a turned mask does. `backdropBlur` under a turn blurs the upright box. */
418
+ rotate?: LengthProp;
399
419
  /** The width of a table's `rules` in logical px; 1 when unset. */
400
420
  ruleWidth?: LengthProp;
401
421
  /** 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. */
402
422
  rules?: ColorProp;
423
+ /** Scales this node and everything under it by this factor, uniformly, about its pivot, after layout (`docs/adr/0043-a-node-turns-about-its-pivot.md`); 1 is none. 0 draws nothing in Rust, Node and Lua; in C and Odin, where a zeroed field is unset, 0 is 1. Paint-only, as `rotate` is: layout, the room taken and `onLayout` are the upright node's; what it draws, clips and hits scales. Tweens with `transition` as one slot with `rotate`; `enter: { scale: 0.8 }` settles a chip in, `keyframes: [{ scale: 1.05, at: 0.5 }]` pulses. The edge ramps scale with the box, so a box scaled far up reads soft. */
424
+ scale?: LengthProp;
403
425
  /** 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`. */
404
426
  scrollAxes?: 'both' | 'x' | 'y';
405
427
  /** The modifiers `onScroll` is for (backlog F122): `"shift"`, `"ctrl"`, `"alt"` and `"super"` (⌘, the Windows key), separated by spaces or commas — `"ctrl super"`. With any named, the node hears only a scroll gesture that began with one of them held, and hears it first: ahead of every scroll container and every `onScroll` that names none, wherever under the pointer the gesture began, the innermost such node winning — so a Ctrl-wheel zoom declared on the window's root is heard over a list, and the list does not scroll. A wheel with none of them held passes the node by, as if it had no `onScroll`: a node that scrolls as well (`overflow`) scrolls for it as any container does. Its `scroll` events carry `mods`, the modifiers held when the gesture began; the gesture stays the node's to the end of its glide, whatever is let go meanwhile, and one begun without them never becomes its. A word that is none of the four is skipped. Unset, a handler like any other. Meaningless without `onScroll`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qxuken/kui",
3
- "version": "0.1.0-alpha.45",
3
+ "version": "0.1.0-alpha.47",
4
4
  "description": "kui for Node: JSX views lowered into the kui IR, Elm-style messages as data",
5
5
  "license": "MIT",
6
6
  "repository": {
Binary file
Binary file
Binary file