@promptctl/rich-js 0.24.1 → 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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +21 -29
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +13 -16
  47. package/dist/core/strip.js +119 -110
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +132 -54
  73. package/dist/renderables/effect.js +188 -115
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  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 +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
@@ -4,13 +4,25 @@
4
4
  *
5
5
  * A host that builds its cells in templates — cc-candybar's statusline, say —
6
6
  * evaluates each template once per redraw and owns the clock that says when
7
- * that is. So the time is a capability the registration takes, the way
8
- * `richTextFuncs(drawnAt)` takes the drawn depth: `frameTime()` is read once
9
- * per call, when the template is evaluated, and nothing here reads a clock.
7
+ * that is. So the frame is a capability the registration takes, the way
8
+ * `richTextFuncs(drawnAt)` takes the drawn depth: `frame()` is read once per
9
+ * call, when the template is evaluated, and nothing here reads a clock.
10
10
  * [LAW:no-ambient-temporal-coupling] A host hands in the same `t` and gets
11
11
  * the same fragment back; a host that redraws once a second or sixty times
12
12
  * a second registers the same functions.
13
13
  *
14
+ * Every function draws its animation on a `Timeline` (`../core/timeline.ts`)
15
+ * the host hands in as a getter beside the time — playing from host time 0
16
+ * at rate 1, the host's own clock, when it hands none — so a daemon pauses,
17
+ * reverses, scrubs or speeds one session's animations by changing one value
18
+ * it holds, with nothing recompiled, as `paletteFuncs` follows a theme
19
+ * through its getter. The timeline is a map the drawing runs through, not a
20
+ * number the time is turned into first: a frame's stamp says when it changes
21
+ * on the host's clock, which only `Timeline.run` can carry it back onto —
22
+ * paused, never; backwards, at the hold it is about to drop under.
23
+ * [LAW:single-enforcer] `drawing` is the one place the time, the timeline
24
+ * and the reader's preference for motion are read, for every function.
25
+ *
14
26
  * `frames <seconds> <frame> <seconds> <frame> …` is a `FrameSequence`
15
27
  * (`renderables/frames.ts`) in a template: the frame in progress at the
16
28
  * moment, as a fragment at the width of the widest. Its frames arrive as
@@ -25,11 +37,49 @@
25
37
  * end, so the engine's `alternating` gate types every slot and lifts each
26
38
  * string frame to a fragment; the pairing — a trailing hold with no frame or
27
39
  * colour — is parsed in `pairs`, once. [LAW:types-are-the-program]
40
+ *
41
+ * The rest start at a moment the template names, on the timeline's clock —
42
+ * a field of the host's state, the moment a value changed — and each is the
43
+ * animation `Timeline.play(start)` runs, so a start is a timeline like any
44
+ * other and nothing here offsets a time by hand. `tween <seconds> <ease>
45
+ * <start> <from> <to>` is a number eased from one value to another and held
46
+ * (`core/tween.ts`), its ease spelled by name (`parseEase`); `loop <seconds>
47
+ * <start>` is the way through a period, 0 to 1 and round again — two
48
+ * cells started a step apart are a stagger. Each is a number, so it goes
49
+ * wherever a number goes: a colour tween is `ramp (tween …) "linear" 0 $from
50
+ * 1 $to` and a ramp scrolling is `ramp (loop …) …`, because `ColorRamp` is
51
+ * the one place two colours are blended and a colour tween is a two-stop
52
+ * ramp read over time (`ColorRamp.over`). [LAW:one-source-of-truth] A
53
+ * second blender here would be a second answer to what lies between two
54
+ * colours. `glyphs <name> <start> <fragment>` is the fragment through a
55
+ * glyph effect from the catalogue (`GLYPH_EFFECT_CATALOGUE`), at its
56
+ * defaults, before any render (`reglyphText`). Every argument list is an
57
+ * exact arity of typed slots, so a slot of the wrong kind fails at the gate.
58
+ *
59
+ * Every function says when what it drew changes, so a host paints a
60
+ * template while it moves and not after. `frames` and `glyphs` stamp the
61
+ * fragment they draw (`textFrameAt`), covering its own glyphs. A number or a
62
+ * colour carries no cells to stamp, so `tween`, `loop` and `stepped` tell
63
+ * their hold to the frame instead (`TemplateFrame.onHold`), as a placed
64
+ * animation of no cells does (`RenderOptions.onHold`). Nothing here claims
65
+ * a moment it does not know.
28
66
  */
29
- import { beat } from "../core/easing.js";
30
- import { FrameSequence, frameAt } from "../renderables/frames.js";
67
+ import { Animation, frameTimeOf, holdOf, shownAt } from "../core/animation.js";
68
+ import { Phase, beat, finiteTime, parseEase } from "../core/easing.js";
69
+ import { Timeline } from "../core/timeline.js";
70
+ import { tween } from "../core/tween.js";
71
+ import { cellSeed } from "../renderables/effect.js";
72
+ import { reglyphText } from "../renderables/glyph-effect.js";
73
+ import { GLYPH_EFFECT_CATALOGUE } from "../renderables/glyph-effects.js";
74
+ import { FrameSequence, textFrameAt } from "../renderables/frames.js";
31
75
  import { asColor } from "./color-funcs.js";
32
76
  import { asFragment } from "./helpers.js";
77
+ /** The frame a renderable is drawn in, as the frame a template it evaluates there is drawn for. */
78
+ export function templateFrame(options) {
79
+ return { time: frameTimeOf(options), onHold: options.onHold, reducedMotion: options.reducedMotion, asciiOnly: options.asciiOnly };
80
+ }
81
+ /** The host's own clock as a timeline: host time 0 at animation time 0, at rate 1. */
82
+ const HOST_CLOCK = Timeline.play(0);
33
83
  /**
34
84
  * The `(seconds, value)` pairs a call spells, each value read through
35
85
  * `value`. The gate has typed every even slot as a float and every odd one
@@ -54,17 +104,59 @@ function pairs(what, tail, value) {
54
104
  return paired;
55
105
  }
56
106
  /**
57
- * Register `frames` and `stepped`, each drawn for the moment `frameTime`
58
- * reports when the template is evaluated: seconds on the host's own
59
- * timeline, from whatever zero it counts from. The functions exist whatever
60
- * `frameTime` returns, so a template compiles once and is evaluated at every
61
- * moment the host draws.
107
+ * Every glyph effect in the catalogue by the name a template spells it
108
+ * with, a transition as the effect it runs. [LAW:types-are-the-program]
109
+ * Keyed by both name types, so a name added to the catalogue does not
110
+ * compile until it is spelled here too.
111
+ */
112
+ const GLYPH_EFFECTS = {
113
+ ...GLYPH_EFFECT_CATALOGUE.loops,
114
+ typewriter: (element) => GLYPH_EFFECT_CATALOGUE.transitions.typewriter(element).effect,
115
+ scramble: (element) => GLYPH_EFFECT_CATALOGUE.transitions.scramble(element).effect,
116
+ backspace: (element) => GLYPH_EFFECT_CATALOGUE.transitions.backspace(element).effect,
117
+ scrambleOut: (element) => GLYPH_EFFECT_CATALOGUE.transitions.scrambleOut(element).effect,
118
+ };
119
+ /**
120
+ * The gate a spelled glyph effect crosses: the name in, the effect's maker
121
+ * out, or a refusal naming every legal one. [LAW:parse-dont-validate]
122
+ */
123
+ function glyphEffect(name) {
124
+ if (!Object.hasOwn(GLYPH_EFFECTS, name)) {
125
+ throw new RangeError(`unknown glyph effect ${JSON.stringify(name)}; expected one of ${Object.keys(GLYPH_EFFECTS).map((n) => JSON.stringify(n)).join(", ")}`);
126
+ }
127
+ return GLYPH_EFFECTS[name];
128
+ }
129
+ /**
130
+ * Register `frames`, `stepped`, `tween`, `loop` and `glyphs`, each drawn for
131
+ * the frame `frame` reports when the template is evaluated — its time in
132
+ * seconds, from whatever zero the host counts from — through
133
+ * `options.timeline`, and each telling that frame when what it drew changes.
134
+ * The functions exist whatever `frame` returns, so a template compiles once
135
+ * and is evaluated at every moment the host draws. In a frame that says
136
+ * `reducedMotion` each draws its animation reduced (`Animation.reduced`): a
137
+ * loop at its start, a one-shot cut from where it starts to where it arrives
138
+ * when it starts.
62
139
  */
63
- export function animationFuncs(frameTime) {
140
+ export function animationFuncs(frame, options = {}) {
141
+ const timeline = options.timeline ?? (() => HOST_CLOCK);
142
+ // `animation` on this call's timeline, and the time of `at`, the frame it
143
+ // is drawn in, read once a call so a value and its hold are one frame's.
144
+ const drawing = (what, animation, at) => [Timeline.run(timeline(), animation), finiteTime(what, at.time)];
145
+ // What a value animation shows now, under the reader's preference, its
146
+ // hold told to the frame: a value has no cells to carry it.
147
+ const now = (what, animation) => {
148
+ const at = frame();
149
+ const [drawn, t] = drawing(what, animation, at);
150
+ const shown = shownAt(drawn, t, at);
151
+ at.onHold?.(holdOf(shown, t));
152
+ return shown.value;
153
+ };
154
+ const startedAt = (start, animation) => Timeline.run(Timeline.play(start), animation);
64
155
  const framesFunc = {
65
156
  fn: ((...tail) => {
66
157
  const frames = pairs("frame", tail, asFragment).map(({ seconds, value }) => ({ seconds, text: value }));
67
- return frameAt(new FrameSequence(frames), frameTime());
158
+ const at = frame();
159
+ return textFrameAt(...drawing("frames", new FrameSequence(frames), at), at);
68
160
  }),
69
161
  argTypes: ["float", "liftable"],
70
162
  arity: { kind: "alternating", minimum: 2 },
@@ -73,11 +165,37 @@ export function animationFuncs(frameTime) {
73
165
  const steppedFunc = {
74
166
  fn: ((...tail) => {
75
167
  const steps = pairs("color", tail, (v) => asColor(v, "stepped"));
76
- return steps[beat(steps.map((step) => step.seconds)).at(frameTime())].value.hex;
168
+ return steps[now("stepped", beat(steps.map((step) => step.seconds)))].value.hex;
77
169
  }),
78
170
  argTypes: ["float", "string"],
79
171
  arity: { kind: "alternating", minimum: 2 },
80
172
  returnType: "string",
81
173
  };
82
- return { frames: framesFunc, stepped: steppedFunc };
174
+ const tweenFunc = {
175
+ fn: ((seconds, ease, start, from, to) => now("tween", startedAt(start, tween(from, to, { seconds, ease: parseEase(ease) })))),
176
+ argTypes: ["float", "string", "float", "float", "float"],
177
+ arity: { kind: "exact" },
178
+ returnType: "float",
179
+ };
180
+ const loopFunc = {
181
+ fn: ((seconds, start) => now("loop", startedAt(start, Animation.continuous(Phase.loop(seconds))))),
182
+ argTypes: ["float", "float"],
183
+ arity: { kind: "exact" },
184
+ returnType: "float",
185
+ };
186
+ const glyphsFunc = {
187
+ fn: ((name, start, child) => {
188
+ const make = glyphEffect(name);
189
+ const fragment = asFragment(child);
190
+ // One text, one seed: the element is named by what it says.
191
+ const key = fragment.plain;
192
+ const at = frame();
193
+ const reglyphed = reglyphText(fragment, make({ z: cellSeed(key, 0, 0) }), { key, asciiOnly: at.asciiOnly === true });
194
+ return textFrameAt(...drawing("glyphs", startedAt(start, reglyphed), at), at);
195
+ }),
196
+ argTypes: ["string", "float", "liftable"],
197
+ arity: { kind: "exact" },
198
+ returnType: "T",
199
+ };
200
+ return { frames: framesFunc, stepped: steppedFunc, tween: tweenFunc, loop: loopFunc, glyphs: glyphsFunc };
83
201
  }
@@ -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)`
@@ -43,7 +43,7 @@ import type { RenderOptions } from "../core/protocol.js";
43
43
  import { type DrawnAt } from "./color-funcs.js";
44
44
  export { paletteFuncs } from "./palette-funcs.js";
45
45
  export { colorFuncs, readableOnFunc, type DrawnAt } from "./color-funcs.js";
46
- export { animationFuncs } from "./animation-funcs.js";
46
+ export { animationFuncs, templateFrame, type AnimationFuncsOptions, type TemplateFrame } from "./animation-funcs.js";
47
47
  /**
48
48
  * Funcs registered by the rich-js binding — the colour sinks, the palette-free
49
49
  * colour math, text attributes, and the `link` cell-splitter. Nothing here
@@ -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)`
@@ -45,7 +45,7 @@ import { richTextStyleFuncs } from "./style-funcs.js";
45
45
  import { colorFuncs } from "./color-funcs.js";
46
46
  export { paletteFuncs } from "./palette-funcs.js";
47
47
  export { colorFuncs, readableOnFunc } from "./color-funcs.js";
48
- export { animationFuncs } from "./animation-funcs.js";
48
+ export { animationFuncs, templateFrame } from "./animation-funcs.js";
49
49
  /**
50
50
  * Funcs registered by the rich-js binding — the colour sinks, the palette-free
51
51
  * colour math, text attributes, and the `link` cell-splitter. Nothing here
@@ -20,9 +20,15 @@
20
20
  * theme colors passes through perceptually even steps rather than the muddy
21
21
  * midpoints of an sRGB blend.
22
22
  *
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
+ *
23
29
  * [LAW:one-way-deps] Imports `core/color`, `core/easing`, `core/oklch`,
24
- * and for `over`, `core/animation` and `core/tween`; nothing here knows
25
- * what a palette is. Resolving stop *names* is the template binding's
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
26
32
  * job (`paletteFuncs`), which hands this module resolved colors.
27
33
  */
28
34
  import { Animation } from "../core/animation.js";
@@ -33,6 +39,19 @@ export interface ColorStop {
33
39
  readonly at: number;
34
40
  readonly color: ColorRgba;
35
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
+ }
36
55
  /**
37
56
  * An immutable ramp over stops sorted by position.
38
57
  *
@@ -51,7 +70,11 @@ export interface ColorStop {
51
70
  export declare class ColorRamp {
52
71
  readonly ease: Ease;
53
72
  readonly stops: readonly ColorStop[];
54
- 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);
55
78
  /**
56
79
  * The color at `value`. Below the first stop it is the first color; at or
57
80
  * above the last stop it is the last; between two stops the raw progress
@@ -67,8 +90,17 @@ export declare class ColorRamp {
67
90
  * `jump-start`/`jump-both` steps, which jump at the start of the interval as
68
91
  * CSS defines them. An ease that overshoots [0, 1] holds the end colors
69
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.
70
100
  */
71
101
  at(value: number): ColorRgba;
102
+ /** The colour at `value` with the ramp unmoved: the stops and the ease alone. */
103
+ private unmoved;
72
104
  /**
73
105
  * This ramp read over time: its span of positions, from the first stop's
74
106
  * to the last's, crossed at a steady rate in `seconds` and held at the last
@@ -82,4 +114,15 @@ export declare class ColorRamp {
82
114
  * for byte where `at` paints them.
83
115
  */
84
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>;
85
128
  }
@@ -20,14 +20,20 @@
20
20
  * theme colors passes through perceptually even steps rather than the muddy
21
21
  * midpoints of an sRGB blend.
22
22
  *
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
+ *
23
29
  * [LAW:one-way-deps] Imports `core/color`, `core/easing`, `core/oklch`,
24
- * and for `over`, `core/animation` and `core/tween`; nothing here knows
25
- * what a palette is. Resolving stop *names* is the template binding's
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
26
32
  * job (`paletteFuncs`), which hands this module resolved colors.
27
33
  */
28
34
  import { Animation } from "../core/animation.js";
29
35
  import { EASES } from "../core/easing.js";
30
- import { Oklch } from "../core/oklch.js";
36
+ import { IDENTITY, Oklch } from "../core/oklch.js";
31
37
  import { tween } from "../core/tween.js";
32
38
  /**
33
39
  * An immutable ramp over stops sorted by position.
@@ -47,9 +53,15 @@ import { tween } from "../core/tween.js";
47
53
  export class ColorRamp {
48
54
  ease;
49
55
  stops;
50
- 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) {
51
61
  this.ease = ease;
52
62
  this.stops = stops;
63
+ this.scroll = turnOf("scroll", scroll);
64
+ this.hue = turnOf("hue", hue);
53
65
  if (stops.length === 0) {
54
66
  throw new RangeError("a ColorRamp needs at least one stop");
55
67
  }
@@ -79,11 +91,29 @@ export class ColorRamp {
79
91
  * `jump-start`/`jump-both` steps, which jump at the start of the interval as
80
92
  * CSS defines them. An ease that overshoots [0, 1] holds the end colors
81
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.
82
101
  */
83
102
  at(value) {
84
103
  if (!Number.isFinite(value)) {
85
104
  throw new RangeError(`ColorRamp.at needs a finite value, got ${value}`);
86
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) {
87
117
  const stops = this.stops;
88
118
  // The last stop at or below `value`; -1 when `value` is below them all.
89
119
  let lower = -1;
@@ -121,4 +151,31 @@ export class ColorRamp {
121
151
  const last = this.stops[this.stops.length - 1].at;
122
152
  return Animation.map(tween(first, last, { seconds, ease: EASES.linear }), (value) => this.at(value));
123
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;
124
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;
@@ -12,6 +12,8 @@ export interface FocusView {
12
12
  present(): readonly InteractiveWidget[];
13
13
  /** The widgets with a cell on the frame on screen, in document order. */
14
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;
15
17
  }
16
18
  export declare class DefaultFocusManager implements FocusManager {
17
19
  private readonly view;