@promptctl/rich-js 0.24.0 → 0.25.0

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.
Files changed (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -14,10 +14,10 @@
14
14
  * - `paletteFuncs(getPalette)` — `color` and `ramp`, the two functions that
15
15
  * resolve a palette name. This is the *only* registration that knows a
16
16
  * palette exists. [LAW:one-way-deps]
17
- * - `animationFuncs(frameTime)` — `frames` and `stepped`, a fragment and a
18
- * colour drawn for a moment. It is the only registration that knows the
19
- * template is evaluated at a time, and the time is the host's to hand in
20
- * (`./animation-funcs.ts`).
17
+ * - `animationFuncs(frame)` — `frames`, `stepped`, `tween`, `loop` and
18
+ * `glyphs`, drawn for a moment on a timeline. It is the only registration
19
+ * that knows the template is evaluated at a time, and the time and the
20
+ * timeline are the host's to hand in (`./animation-funcs.ts`).
21
21
  *
22
22
  * `createRichTextEngine()` wires up `richTextFuncs()` only — it cannot supply a
23
23
  * palette. Consumers that name theme colours call `paletteFuncs(getPalette)`
@@ -40,11 +40,12 @@ import { createEngine } from "@promptctl/go-template-js";
40
40
  import { RichText } from "../core/text.js";
41
41
  import { Style } from "../core/style.js";
42
42
  import { Segment } from "../core/segment.js";
43
+ import { render } from "../core/protocol.js";
43
44
  import { richTextStyleFuncs } from "./style-funcs.js";
44
45
  import { colorFuncs } from "./color-funcs.js";
45
46
  export { paletteFuncs } from "./palette-funcs.js";
46
47
  export { colorFuncs, readableOnFunc } from "./color-funcs.js";
47
- export { animationFuncs } from "./animation-funcs.js";
48
+ export { animationFuncs, templateFrame } from "./animation-funcs.js";
48
49
  /**
49
50
  * Funcs registered by the rich-js binding — the colour sinks, the palette-free
50
51
  * colour math, text attributes, and the `link` cell-splitter. Nothing here
@@ -131,7 +132,7 @@ export function renderTemplate(engine, source, scope = {}, options) {
131
132
  catch (e) {
132
133
  return [new Segment(errorLine(e, maxWidth), safeErrorStyle(errorStyle))];
133
134
  }
134
- return Array.from(text.render({ isTerminal: true, ...renderOptions, maxWidth }));
135
+ return Array.from(render(text, { isTerminal: true, ...renderOptions, maxWidth }));
135
136
  }
136
137
  /**
137
138
  * A caught error as the one line `renderTemplate` promises. The summary is the
@@ -20,17 +20,38 @@
20
20
  * theme colors passes through perceptually even steps rather than the muddy
21
21
  * midpoints of an sRGB blend.
22
22
  *
23
- * [LAW:one-way-deps] Imports `core/color`, `core/easing` and `core/oklch`
24
- * only; nothing here knows what a palette is. Resolving stop *names* is the template binding's
23
+ * A ramp also carries how far it has been moved: `scroll`, in spans, how
24
+ * far its colours have travelled along it, and `hue`, in turns, how far
25
+ * they are rotated round the OKLCH hue circle. A moved ramp is still a
26
+ * `ColorRamp`, so anything that reads a ramp reads a moving one frame by
27
+ * frame (`moving`) with nothing new to learn. [LAW:one-type-per-behavior]
28
+ *
29
+ * [LAW:one-way-deps] Imports `core/color`, `core/easing`, `core/oklch`,
30
+ * `core/animation` for `over` and `moving`, and `core/tween` for `over`; nothing
31
+ * here knows what a palette is. Resolving stop *names* is the template binding's
25
32
  * job (`paletteFuncs`), which hands this module resolved colors.
26
33
  */
34
+ import { Animation } from "../core/animation.js";
27
35
  import type { ColorRgba } from "../core/color.js";
28
- import type { Ease } from "../core/easing.js";
36
+ import { type Ease } from "../core/easing.js";
29
37
  /** One stop: the color the ramp is exactly `color` at position `at`. */
30
38
  export interface ColorStop {
31
39
  readonly at: number;
32
40
  readonly color: ColorRgba;
33
41
  }
42
+ /**
43
+ * How a ramp moves over time, each part an `Animation<number>` that is
44
+ * `Animation.constant(0)` when not given. `scroll` is in spans: at 1 the
45
+ * colours have travelled the whole way from the first stop to the last and
46
+ * wrapped back, so `Animation.continuous(Phase.loop(seconds))` scrolls once
47
+ * every `seconds`. `hue` is in turns, CSS's `turn`: at 1 every hue has gone
48
+ * once round the circle. A one-shot that settles is a `tween` to where it
49
+ * rests.
50
+ */
51
+ export interface RampMotion {
52
+ readonly scroll?: Animation<number>;
53
+ readonly hue?: Animation<number>;
54
+ }
34
55
  /**
35
56
  * An immutable ramp over stops sorted by position.
36
57
  *
@@ -49,7 +70,11 @@ export interface ColorStop {
49
70
  export declare class ColorRamp {
50
71
  readonly ease: Ease;
51
72
  readonly stops: readonly ColorStop[];
52
- constructor(ease: Ease, stops: readonly ColorStop[]);
73
+ /** How far the colours have moved along the span, in spans, in [0, 1). */
74
+ readonly scroll: number;
75
+ /** How far every hue is rotated in OKLCH, in turns, in [0, 1). */
76
+ readonly hue: number;
77
+ constructor(ease: Ease, stops: readonly ColorStop[], scroll?: number, hue?: number);
53
78
  /**
54
79
  * The color at `value`. Below the first stop it is the first color; at or
55
80
  * above the last stop it is the last; between two stops the raw progress
@@ -65,6 +90,39 @@ export declare class ColorRamp {
65
90
  * `jump-start`/`jump-both` steps, which jump at the start of the interval as
66
91
  * CSS defines them. An ease that overshoots [0, 1] holds the end colors
67
92
  * where it does: a ramp never paints outside its stops.
93
+ *
94
+ * A scrolled ramp reads, inside its span, the colour `scroll` spans behind
95
+ * `value`, wrapped back round the span as a repeating gradient does —
96
+ * seamless when the first and last stops are one colour; outside the span
97
+ * its end colours hold, however far it has scrolled. A rotated ramp turns
98
+ * the colour read by `hue` at its own lightness and chroma. Unmoved, every
99
+ * read keeps the exactness above.
68
100
  */
69
101
  at(value: number): ColorRgba;
102
+ /** The colour at `value` with the ramp unmoved: the stops and the ease alone. */
103
+ private unmoved;
104
+ /**
105
+ * This ramp read over time: its span of positions, from the first stop's
106
+ * to the last's, crossed at a steady rate in `seconds` and held at the last
107
+ * stop from then on, each colour along the way the ramp's own. A colour
108
+ * tween is a two-stop ramp over its seconds — the ramp's ease shaping the
109
+ * move, so `new ColorRamp(ease, [{ at: 0, color: a }, { at: 1, color: b
110
+ * }]).over(seconds)` is `a` eased to `b` — and a ramp of more stops over
111
+ * time is a sweep through all of them. [LAW:one-source-of-truth] The
112
+ * blending is `at`'s; what time adds is `tween`'s (`core/tween.ts`), so a
113
+ * host sampling once a second or sixty times reads the stop colours byte
114
+ * for byte where `at` paints them.
115
+ */
116
+ over(seconds: number): Animation<ColorRgba>;
117
+ /**
118
+ * This ramp moving: at `t`, scrolled by `motion.scroll` and rotated by
119
+ * `motion.hue` at `t`, on top of however far it was already moved, and
120
+ * changing when either does — so a loop moves every frame and a ramp a
121
+ * tween settles never changes again once it arrives.
122
+ * [LAW:one-source-of-truth] When it next changes is `Animation.all`'s
123
+ * answer over the two parts; nothing here says it a second time. Whether
124
+ * the depth it is drawn at can show the move is decided where it is drawn
125
+ * (`gradient`, through `stillOnSlots`).
126
+ */
127
+ moving(motion: RampMotion): Animation<ColorRamp>;
70
128
  }
@@ -20,11 +20,21 @@
20
20
  * theme colors passes through perceptually even steps rather than the muddy
21
21
  * midpoints of an sRGB blend.
22
22
  *
23
- * [LAW:one-way-deps] Imports `core/color`, `core/easing` and `core/oklch`
24
- * only; nothing here knows what a palette is. Resolving stop *names* is the template binding's
23
+ * A ramp also carries how far it has been moved: `scroll`, in spans, how
24
+ * far its colours have travelled along it, and `hue`, in turns, how far
25
+ * they are rotated round the OKLCH hue circle. A moved ramp is still a
26
+ * `ColorRamp`, so anything that reads a ramp reads a moving one frame by
27
+ * frame (`moving`) with nothing new to learn. [LAW:one-type-per-behavior]
28
+ *
29
+ * [LAW:one-way-deps] Imports `core/color`, `core/easing`, `core/oklch`,
30
+ * `core/animation` for `over` and `moving`, and `core/tween` for `over`; nothing
31
+ * here knows what a palette is. Resolving stop *names* is the template binding's
25
32
  * job (`paletteFuncs`), which hands this module resolved colors.
26
33
  */
27
- import { Oklch } from "../core/oklch.js";
34
+ import { Animation } from "../core/animation.js";
35
+ import { EASES } from "../core/easing.js";
36
+ import { IDENTITY, Oklch } from "../core/oklch.js";
37
+ import { tween } from "../core/tween.js";
28
38
  /**
29
39
  * An immutable ramp over stops sorted by position.
30
40
  *
@@ -43,9 +53,15 @@ import { Oklch } from "../core/oklch.js";
43
53
  export class ColorRamp {
44
54
  ease;
45
55
  stops;
46
- constructor(ease, stops) {
56
+ /** How far the colours have moved along the span, in spans, in [0, 1). */
57
+ scroll;
58
+ /** How far every hue is rotated in OKLCH, in turns, in [0, 1). */
59
+ hue;
60
+ constructor(ease, stops, scroll = 0, hue = 0) {
47
61
  this.ease = ease;
48
62
  this.stops = stops;
63
+ this.scroll = turnOf("scroll", scroll);
64
+ this.hue = turnOf("hue", hue);
49
65
  if (stops.length === 0) {
50
66
  throw new RangeError("a ColorRamp needs at least one stop");
51
67
  }
@@ -75,11 +91,29 @@ export class ColorRamp {
75
91
  * `jump-start`/`jump-both` steps, which jump at the start of the interval as
76
92
  * CSS defines them. An ease that overshoots [0, 1] holds the end colors
77
93
  * where it does: a ramp never paints outside its stops.
94
+ *
95
+ * A scrolled ramp reads, inside its span, the colour `scroll` spans behind
96
+ * `value`, wrapped back round the span as a repeating gradient does —
97
+ * seamless when the first and last stops are one colour; outside the span
98
+ * its end colours hold, however far it has scrolled. A rotated ramp turns
99
+ * the colour read by `hue` at its own lightness and chroma. Unmoved, every
100
+ * read keeps the exactness above.
78
101
  */
79
102
  at(value) {
80
103
  if (!Number.isFinite(value)) {
81
104
  throw new RangeError(`ColorRamp.at needs a finite value, got ${value}`);
82
105
  }
106
+ const first = this.stops[0].at;
107
+ const last = this.stops[this.stops.length - 1].at;
108
+ const span = last - first;
109
+ const inside = first <= value && value <= last;
110
+ const moved = inside ? value - this.scroll * span : value;
111
+ const read = this.unmoved(inside && moved < first ? moved + span : moved);
112
+ // Exact when unrotated: the OKLCH round trip can land a channel one off.
113
+ return this.hue === 0 ? read : Oklch.fromRgba(read).applyKey({ ...IDENTITY, hueShift: 360 * this.hue }).toRgba();
114
+ }
115
+ /** The colour at `value` with the ramp unmoved: the stops and the ease alone. */
116
+ unmoved(value) {
83
117
  const stops = this.stops;
84
118
  // The last stop at or below `value`; -1 when `value` is below them all.
85
119
  let lower = -1;
@@ -100,4 +134,48 @@ export class ColorRamp {
100
134
  return to.color;
101
135
  return Oklch.fromRgba(from.color).mix(Oklch.fromRgba(to.color), t).toRgba();
102
136
  }
137
+ /**
138
+ * This ramp read over time: its span of positions, from the first stop's
139
+ * to the last's, crossed at a steady rate in `seconds` and held at the last
140
+ * stop from then on, each colour along the way the ramp's own. A colour
141
+ * tween is a two-stop ramp over its seconds — the ramp's ease shaping the
142
+ * move, so `new ColorRamp(ease, [{ at: 0, color: a }, { at: 1, color: b
143
+ * }]).over(seconds)` is `a` eased to `b` — and a ramp of more stops over
144
+ * time is a sweep through all of them. [LAW:one-source-of-truth] The
145
+ * blending is `at`'s; what time adds is `tween`'s (`core/tween.ts`), so a
146
+ * host sampling once a second or sixty times reads the stop colours byte
147
+ * for byte where `at` paints them.
148
+ */
149
+ over(seconds) {
150
+ const first = this.stops[0].at;
151
+ const last = this.stops[this.stops.length - 1].at;
152
+ return Animation.map(tween(first, last, { seconds, ease: EASES.linear }), (value) => this.at(value));
153
+ }
154
+ /**
155
+ * This ramp moving: at `t`, scrolled by `motion.scroll` and rotated by
156
+ * `motion.hue` at `t`, on top of however far it was already moved, and
157
+ * changing when either does — so a loop moves every frame and a ramp a
158
+ * tween settles never changes again once it arrives.
159
+ * [LAW:one-source-of-truth] When it next changes is `Animation.all`'s
160
+ * answer over the two parts; nothing here says it a second time. Whether
161
+ * the depth it is drawn at can show the move is decided where it is drawn
162
+ * (`gradient`, through `stillOnSlots`).
163
+ */
164
+ moving(motion) {
165
+ const still = Animation.constant(0);
166
+ return Animation.map(Animation.all([motion.scroll ?? still, motion.hue ?? still]), ([scroll, hue]) => new ColorRamp(this.ease, this.stops, this.scroll + scroll, this.hue + hue));
167
+ }
168
+ }
169
+ /**
170
+ * `v` as a fraction of one turn, in [0, 1): a whole turn is no turn, and a
171
+ * negative one counts back from a whole turn. A value so close under a whole
172
+ * turn that the fraction rounds to it is the turn itself. [LAW:single-enforcer]
173
+ * The constructor reads both moves through here, so a non-finite one fails
174
+ * here, loudly, naming which.
175
+ */
176
+ function turnOf(what, v) {
177
+ if (!Number.isFinite(v))
178
+ throw new RangeError(`ColorRamp ${what} must be finite, got ${v}`);
179
+ const into = v - Math.floor(v);
180
+ return into < 1 ? into : 0;
103
181
  }
@@ -3,8 +3,11 @@ import { Palette, drawnOn } from "./palette.js";
3
3
  import { ACCENT_KEYS, accentText } from "./buildPalette.js";
4
4
  import { THEMES } from "./data/index.js";
5
5
  import { ANSI_SLOTS } from "./data/types.js";
6
+ import { Memo } from "../core/memo.js";
6
7
  const THEME_NAMES = Object.freeze(Object.keys(THEMES).sort());
7
- const cache = new Map();
8
+ // Fewer themes than a Memo holds, so it never clears and a name keeps one
9
+ // Palette — test/themes/registry.test.ts holds that.
10
+ const cache = new Memo();
8
11
  /**
9
12
  * Returns the static list of available theme names. Pure and synchronous.
10
13
  */
@@ -14,12 +17,7 @@ export function listThemePalettes() {
14
17
  export function getThemePalette(name) {
15
18
  if (!isThemeName(name))
16
19
  return null;
17
- const cached = cache.get(name);
18
- if (cached !== undefined)
19
- return cached;
20
- const palette = hydrate(THEMES[name]);
21
- cache.set(name, palette);
22
- return palette;
20
+ return cache.get(name, () => hydrate(THEMES[name]));
23
21
  }
24
22
  function isThemeName(name) {
25
23
  return Object.hasOwn(THEMES, name);
@@ -49,7 +49,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
49
49
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
50
50
  done = true;
51
51
  };
52
- import { observable, action, observableRef } from "mobx";
52
+ import { action } from "mobx";
53
+ import { observableRef } from "./observable.js";
53
54
  import { Segment } from "../core/segment.js";
54
55
  import { Style } from "../core/style.js";
55
56
  import { cellLen } from "../core/cells.js";
@@ -83,7 +84,7 @@ let Button = (() => {
83
84
  return class Button extends _classSuper {
84
85
  static {
85
86
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
86
- _label_decorators = [observable];
87
+ _label_decorators = [observableRef];
87
88
  _variant_decorators = [observableRef];
88
89
  _handleKey_decorators = [action];
89
90
  _handleMouse_decorators = [action];
@@ -44,7 +44,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
44
44
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
45
45
  done = true;
46
46
  };
47
- import { observable, action } from "mobx";
47
+ import { action } from "mobx";
48
+ import { observableRef } from "./observable.js";
48
49
  import { Segment } from "../core/segment.js";
49
50
  import { Style } from "../core/style.js";
50
51
  import { cellLen } from "../core/cells.js";
@@ -65,8 +66,8 @@ let Checkbox = (() => {
65
66
  return class Checkbox extends _classSuper {
66
67
  static {
67
68
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
68
- _label_decorators = [observable];
69
- _checked_decorators = [observable];
69
+ _label_decorators = [observableRef];
70
+ _checked_decorators = [observableRef];
70
71
  _handleKey_decorators = [action];
71
72
  _handleMouse_decorators = [action];
72
73
  __esDecorate(this, null, _label_decorators, { kind: "accessor", name: "label", static: false, private: false, access: { has: obj => "label" in obj, get: obj => obj.label, set: (obj, value) => { obj.label = value; } }, metadata: _metadata }, _label_initializers, _label_extraInitializers);
@@ -37,7 +37,9 @@ export interface DropdownOptions {
37
37
  export declare class Dropdown extends ThemedWidget implements OverlayRenderable {
38
38
  readonly id: string;
39
39
  readonly focusable = true;
40
- accessor options: string[];
40
+ private accessor _options;
41
+ get options(): readonly string[];
42
+ set options(options: readonly string[]);
41
43
  accessor selectedIndex: number;
42
44
  accessor expanded: boolean;
43
45
  accessor highlightedIndex: number;
@@ -56,7 +56,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
56
56
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
57
57
  done = true;
58
58
  };
59
- import { observable, action, observableShallow } from "mobx";
59
+ import { action } from "mobx";
60
+ import { observableRef } from "./observable.js";
60
61
  import { Segment } from "../core/segment.js";
61
62
  import { Style } from "../core/style.js";
62
63
  import { cellLen, setCellSize, splitText, asCellCol } from "../core/cells.js";
@@ -67,9 +68,9 @@ import { ink } from "./ink.js";
67
68
  let Dropdown = (() => {
68
69
  let _classSuper = ThemedWidget;
69
70
  let _instanceExtraInitializers = [];
70
- let _options_decorators;
71
- let _options_initializers = [];
72
- let _options_extraInitializers = [];
71
+ let __options_decorators;
72
+ let __options_initializers = [];
73
+ let __options_extraInitializers = [];
73
74
  let _selectedIndex_decorators;
74
75
  let _selectedIndex_initializers = [];
75
76
  let _selectedIndex_extraInitializers = [];
@@ -88,15 +89,15 @@ let Dropdown = (() => {
88
89
  return class Dropdown extends _classSuper {
89
90
  static {
90
91
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
91
- _options_decorators = [observableShallow];
92
- _selectedIndex_decorators = [observable];
93
- _expanded_decorators = [observable];
94
- _highlightedIndex_decorators = [observable];
95
- _filter_decorators = [observable];
92
+ __options_decorators = [observableRef];
93
+ _selectedIndex_decorators = [observableRef];
94
+ _expanded_decorators = [observableRef];
95
+ _highlightedIndex_decorators = [observableRef];
96
+ _filter_decorators = [observableRef];
96
97
  _handleKey_decorators = [action];
97
98
  _handleMouse_decorators = [action];
98
99
  _handleFocus_decorators = [action];
99
- __esDecorate(this, null, _options_decorators, { kind: "accessor", name: "options", static: false, private: false, access: { has: obj => "options" in obj, get: obj => obj.options, set: (obj, value) => { obj.options = value; } }, metadata: _metadata }, _options_initializers, _options_extraInitializers);
100
+ __esDecorate(this, null, __options_decorators, { kind: "accessor", name: "_options", static: false, private: false, access: { has: obj => "_options" in obj, get: obj => obj._options, set: (obj, value) => { obj._options = value; } }, metadata: _metadata }, __options_initializers, __options_extraInitializers);
100
101
  __esDecorate(this, null, _selectedIndex_decorators, { kind: "accessor", name: "selectedIndex", static: false, private: false, access: { has: obj => "selectedIndex" in obj, get: obj => obj.selectedIndex, set: (obj, value) => { obj.selectedIndex = value; } }, metadata: _metadata }, _selectedIndex_initializers, _selectedIndex_extraInitializers);
101
102
  __esDecorate(this, null, _expanded_decorators, { kind: "accessor", name: "expanded", static: false, private: false, access: { has: obj => "expanded" in obj, get: obj => obj.expanded, set: (obj, value) => { obj.expanded = value; } }, metadata: _metadata }, _expanded_initializers, _expanded_extraInitializers);
102
103
  __esDecorate(this, null, _highlightedIndex_decorators, { kind: "accessor", name: "highlightedIndex", static: false, private: false, access: { has: obj => "highlightedIndex" in obj, get: obj => obj.highlightedIndex, set: (obj, value) => { obj.highlightedIndex = value; } }, metadata: _metadata }, _highlightedIndex_initializers, _highlightedIndex_extraInitializers);
@@ -108,10 +109,14 @@ let Dropdown = (() => {
108
109
  }
109
110
  id = __runInitializers(this, _instanceExtraInitializers);
110
111
  focusable = true;
111
- #options_accessor_storage = __runInitializers(this, _options_initializers, void 0);
112
- get options() { return this.#options_accessor_storage; }
113
- set options(value) { this.#options_accessor_storage = value; }
114
- #selectedIndex_accessor_storage = (__runInitializers(this, _options_extraInitializers), __runInitializers(this, _selectedIndex_initializers, void 0));
112
+ #_options_accessor_storage = __runInitializers(this, __options_initializers, void 0);
113
+ get _options() { return this.#_options_accessor_storage; }
114
+ set _options(value) { this.#_options_accessor_storage = value; }
115
+ // A copy is held, so the caller changing the array it assigned changes
116
+ // nothing here: the labels change only by assignment, which repaints.
117
+ get options() { return this._options; }
118
+ set options(options) { this._options = [...options]; }
119
+ #selectedIndex_accessor_storage = (__runInitializers(this, __options_extraInitializers), __runInitializers(this, _selectedIndex_initializers, void 0));
115
120
  get selectedIndex() { return this.#selectedIndex_accessor_storage; }
116
121
  set selectedIndex(value) { this.#selectedIndex_accessor_storage = value; }
117
122
  #expanded_accessor_storage = (__runInitializers(this, _selectedIndex_extraInitializers), __runInitializers(this, _expanded_initializers, false));
@@ -140,7 +145,7 @@ let Dropdown = (() => {
140
145
  super(options.theme);
141
146
  __runInitializers(this, _filter_extraInitializers);
142
147
  this.id = options.id ?? `dropdown-${Math.random().toString(36).slice(2, 8)}`;
143
- this.options = [...options.options];
148
+ this._options = [...options.options];
144
149
  // [LAW:types-are-the-program] selectedIndex must point to a valid
145
150
  // option (or be 0 for the empty-options case). Clamp at the trust
146
151
  // boundary so headerText / render / commit logic can all assume
@@ -6,7 +6,7 @@
6
6
  * host's input (`host/input`), and `WidgetApp` hands this what the app hears.
7
7
  */
8
8
  import { KeyEvent } from "./types.js";
9
- import { drew, originOf, scrollTargetAt, widgetAt } from "./hit.js";
9
+ import { cellOf, drew, originOf, scrollTargetAt, widgetAt } from "./hit.js";
10
10
  export class EventRouter {
11
11
  source;
12
12
  focusOnClick;
@@ -78,7 +78,7 @@ export class EventRouter {
78
78
  }
79
79
  /** Route a key the app heard. */
80
80
  key(press) {
81
- this.dispatchKey(new KeyEvent(press));
81
+ this.dispatchKey(new KeyEvent(press, this.source.frameTime));
82
82
  }
83
83
  /** Route a pointer event the app heard. */
84
84
  mouse(event) {
@@ -172,6 +172,8 @@ export class EventRouter {
172
172
  x: event.x - target.originX,
173
173
  y: event.y - target.originY,
174
174
  over: drew(frame, target.widget, event.x, event.y),
175
+ cell: cellOf(frame, target.widget, event.x, event.y),
176
+ time: this.source.frameTime,
175
177
  });
176
178
  if (event.type === "mouse_down")
177
179
  this.captured = target;
@@ -6,18 +6,25 @@
6
6
  */
7
7
  import type { InteractiveWidget, FocusManager, KeyEvent } from "./types.js";
8
8
  import type { Unsubscribe } from "../core/subscription.js";
9
+ /** What focus follows: the frame a `WidgetApp` just painted. */
10
+ export interface FocusView {
11
+ /** Every widget the view holds, in document order, whether or not it has a cell on screen. */
12
+ present(): readonly InteractiveWidget[];
13
+ /** The widgets with a cell on the frame on screen, in document order. */
14
+ onScreen(): readonly InteractiveWidget[];
15
+ /** The time a frame drawn now is drawn for (`RenderOptions.frame`), which a focus change is stamped with. */
16
+ frameTime(): number;
17
+ }
9
18
  export declare class DefaultFocusManager implements FocusManager {
10
- private readonly onScreen;
19
+ private readonly view;
11
20
  accessor currentWidget: InteractiveWidget | null;
12
21
  private readonly changeHandlers;
13
22
  /**
14
- * `onScreen` is the widgets on the frame on screen, in document order.
15
- *
16
- * [LAW:one-source-of-truth] It is asked every time rather than kept: a list
17
- * of the widgets held here would be a second picture of the frame, and
18
- * wrong from the next paint.
23
+ * [LAW:one-source-of-truth] The view is asked every time rather than kept:
24
+ * a list of the widgets held here would be a second picture of the frame,
25
+ * and wrong from the next paint.
19
26
  */
20
- constructor(onScreen: () => readonly InteractiveWidget[]);
27
+ constructor(view: FocusView);
21
28
  get current(): InteractiveWidget | null;
22
29
  get widgets(): readonly InteractiveWidget[];
23
30
  next(): void;
@@ -25,20 +32,22 @@ export declare class DefaultFocusManager implements FocusManager {
25
32
  focus(widget: InteractiveWidget): void;
26
33
  /**
27
34
  * Put focus where the frame just painted lets it rest: it stays on a widget
28
- * the view drew (`drawn`) that can take it, and otherwise moves to the first
29
- * one on screen that can, or to none.
35
+ * the view holds (`present`) that can take it, and otherwise moves to the
36
+ * first one on screen that can, or to none.
30
37
  *
31
- * Drawn, not on screen: a widget cropped for the moment — the terminal
32
- * shrank, a viewport scrolled, an overlay covers it — is still in the view,
33
- * and keeps focus and whatever focus holds, like a half-typed filter.
38
+ * In the view, not on screen: a widget cropped for the moment — the
39
+ * terminal shrank, a viewport scrolled, an overlay covers it, its container
40
+ * handed it no column — is still in the view, and keeps focus and whatever
41
+ * focus holds, like a half-typed filter.
34
42
  *
35
43
  * [LAW:single-enforcer] The one place focus follows the frame — onto the
36
- * first widget when the app starts, off a widget that stopped being drawn
44
+ * first widget when the app starts, off a widget that left the view
37
45
  * or was disabled.
38
46
  */
39
- settle(drawn: readonly InteractiveWidget[]): void;
47
+ settle(): void;
40
48
  onChange(handler: (current: InteractiveWidget | null) => void): Unsubscribe;
41
49
  handleKey(event: KeyEvent): void;
50
+ private step;
42
51
  private clear;
43
52
  private setFocus;
44
53
  private emitChange;
@@ -38,7 +38,8 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
38
38
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
39
39
  done = true;
40
40
  };
41
- import { action, observableRef } from "mobx";
41
+ import { action } from "mobx";
42
+ import { observableRef } from "./observable.js";
42
43
  let DefaultFocusManager = (() => {
43
44
  let _instanceExtraInitializers = [];
44
45
  let _currentWidget_decorators;
@@ -69,42 +70,30 @@ let DefaultFocusManager = (() => {
69
70
  __esDecorate(this, null, _setFocus_decorators, { kind: "method", name: "setFocus", static: false, private: false, access: { has: obj => "setFocus" in obj, get: obj => obj.setFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
70
71
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
71
72
  }
72
- onScreen = __runInitializers(this, _instanceExtraInitializers);
73
+ view = __runInitializers(this, _instanceExtraInitializers);
73
74
  #currentWidget_accessor_storage = __runInitializers(this, _currentWidget_initializers, null);
74
75
  get currentWidget() { return this.#currentWidget_accessor_storage; }
75
76
  set currentWidget(value) { this.#currentWidget_accessor_storage = value; }
76
77
  changeHandlers = (__runInitializers(this, _currentWidget_extraInitializers), new Set());
77
78
  /**
78
- * `onScreen` is the widgets on the frame on screen, in document order.
79
- *
80
- * [LAW:one-source-of-truth] It is asked every time rather than kept: a list
81
- * of the widgets held here would be a second picture of the frame, and
82
- * wrong from the next paint.
79
+ * [LAW:one-source-of-truth] The view is asked every time rather than kept:
80
+ * a list of the widgets held here would be a second picture of the frame,
81
+ * and wrong from the next paint.
83
82
  */
84
- constructor(onScreen) {
85
- this.onScreen = onScreen;
83
+ constructor(view) {
84
+ this.view = view;
86
85
  }
87
86
  get current() {
88
87
  return this.currentWidget;
89
88
  }
90
89
  get widgets() {
91
- return this.onScreen().filter(takesFocus);
90
+ return this.view.onScreen().filter(takesFocus);
92
91
  }
93
92
  next() {
94
- const focusable = this.widgets;
95
- if (focusable.length === 0)
96
- return;
97
- const currentIdx = this.currentWidget ? focusable.indexOf(this.currentWidget) : -1;
98
- const nextIdx = (currentIdx + 1) % focusable.length;
99
- this.setFocus(focusable[nextIdx]);
93
+ this.step(this.view.present());
100
94
  }
101
95
  prev() {
102
- const focusable = this.widgets;
103
- if (focusable.length === 0)
104
- return;
105
- const currentIdx = this.currentWidget ? focusable.indexOf(this.currentWidget) : -1;
106
- const prevIdx = currentIdx <= 0 ? focusable.length - 1 : currentIdx - 1;
107
- this.setFocus(focusable[prevIdx]);
96
+ this.step([...this.view.present()].reverse());
108
97
  }
109
98
  // A widget not on screen yet can take focus: an app that is about to show
110
99
  // it focuses it first, and the frame that shows it keeps it (`settle`).
@@ -115,20 +104,21 @@ let DefaultFocusManager = (() => {
115
104
  }
116
105
  /**
117
106
  * Put focus where the frame just painted lets it rest: it stays on a widget
118
- * the view drew (`drawn`) that can take it, and otherwise moves to the first
119
- * one on screen that can, or to none.
107
+ * the view holds (`present`) that can take it, and otherwise moves to the
108
+ * first one on screen that can, or to none.
120
109
  *
121
- * Drawn, not on screen: a widget cropped for the moment — the terminal
122
- * shrank, a viewport scrolled, an overlay covers it — is still in the view,
123
- * and keeps focus and whatever focus holds, like a half-typed filter.
110
+ * In the view, not on screen: a widget cropped for the moment — the
111
+ * terminal shrank, a viewport scrolled, an overlay covers it, its container
112
+ * handed it no column — is still in the view, and keeps focus and whatever
113
+ * focus holds, like a half-typed filter.
124
114
  *
125
115
  * [LAW:single-enforcer] The one place focus follows the frame — onto the
126
- * first widget when the app starts, off a widget that stopped being drawn
116
+ * first widget when the app starts, off a widget that left the view
127
117
  * or was disabled.
128
118
  */
129
- settle(drawn) {
119
+ settle() {
130
120
  const current = this.currentWidget;
131
- if (current && takesFocus(current) && drawn.includes(current))
121
+ if (current && takesFocus(current) && this.view.present().includes(current))
132
122
  return;
133
123
  const first = this.widgets[0];
134
124
  if (first)
@@ -154,6 +144,16 @@ let DefaultFocusManager = (() => {
154
144
  event.stop();
155
145
  }
156
146
  // --- Private ---
147
+ // To the first widget Tab moves between that follows the focused one in
148
+ // `order`, wrapping. It is found by its place in the view, not among those
149
+ // on screen, so Tab from a widget on no cell moves to its neighbour.
150
+ step(order) {
151
+ const tabbable = new Set(this.widgets);
152
+ const at = this.currentWidget ? order.indexOf(this.currentWidget) : -1;
153
+ const target = [...order.slice(at + 1), ...order.slice(0, at + 1)].find((widget) => tabbable.has(widget));
154
+ if (target)
155
+ this.setFocus(target);
156
+ }
157
157
  // No focus is where `settle` leaves an app with nothing to focus, and only
158
158
  // there: anywhere else the next frame would put focus back on the first
159
159
  // widget, so there is no public way in.
@@ -163,7 +163,7 @@ let DefaultFocusManager = (() => {
163
163
  // [LAW:single-enforcer] WidgetBase.focus()/blur() route through
164
164
  // handleFocus; calling it here as well would dispatch twice and run any
165
165
  // subclass side effect (e.g. Dropdown clearing its filter) twice.
166
- this.currentWidget.blur();
166
+ this.currentWidget.blur(this.view.frameTime());
167
167
  this.currentWidget = null;
168
168
  this.emitChange();
169
169
  }
@@ -172,10 +172,11 @@ let DefaultFocusManager = (() => {
172
172
  return;
173
173
  // [LAW:single-enforcer] focus()/blur() already dispatch handleFocus —
174
174
  // see clear() for the rationale.
175
+ const time = this.view.frameTime();
175
176
  if (this.currentWidget)
176
- this.currentWidget.blur();
177
+ this.currentWidget.blur(time);
177
178
  this.currentWidget = widget;
178
- widget.focus();
179
+ widget.focus(time);
179
180
  this.emitChange();
180
181
  }
181
182
  emitChange() {