@promptctl/rich-js 0.23.0 → 0.24.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 (51) hide show
  1. package/dist/core/clock.d.ts +9 -1
  2. package/dist/core/clock.js +22 -2
  3. package/dist/core/color.d.ts +10 -0
  4. package/dist/core/color.js +36 -1
  5. package/dist/core/easing.d.ts +31 -0
  6. package/dist/core/easing.js +106 -1
  7. package/dist/core/segment.d.ts +10 -0
  8. package/dist/core/segment.js +15 -0
  9. package/dist/core/strip.d.ts +104 -27
  10. package/dist/core/strip.js +231 -124
  11. package/dist/core/style.d.ts +18 -1
  12. package/dist/core/style.js +32 -3
  13. package/dist/core/text.js +4 -1
  14. package/dist/host/app.js +2 -4
  15. package/dist/host/index.d.ts +1 -1
  16. package/dist/host/input.d.ts +37 -13
  17. package/dist/host/input.js +146 -56
  18. package/dist/host/terminal-host.d.ts +16 -0
  19. package/dist/host/terminal-host.js +1 -0
  20. package/dist/index.d.ts +11 -9
  21. package/dist/index.js +5 -4
  22. package/dist/node/terminal-host.d.ts +8 -1
  23. package/dist/node/terminal-host.js +2 -0
  24. package/dist/renderables/effect.d.ts +78 -10
  25. package/dist/renderables/effect.js +143 -39
  26. package/dist/renderables/effects.d.ts +73 -26
  27. package/dist/renderables/effects.js +412 -92
  28. package/dist/renderables/embed.d.ts +10 -0
  29. package/dist/renderables/embed.js +15 -7
  30. package/dist/renderables/frames.d.ts +95 -0
  31. package/dist/renderables/frames.js +106 -0
  32. package/dist/renderables/live.d.ts +23 -7
  33. package/dist/renderables/live.js +68 -34
  34. package/dist/renderables/markdown.d.ts +5 -2
  35. package/dist/renderables/markdown.js +210 -19
  36. package/dist/renderables/progress.d.ts +1 -1
  37. package/dist/renderables/progress.js +3 -1
  38. package/dist/renderables/progressBar.js +3 -3
  39. package/dist/renderables/spinner.d.ts +62 -28
  40. package/dist/renderables/spinner.js +99 -52
  41. package/dist/renderables/status.d.ts +8 -3
  42. package/dist/renderables/status.js +8 -5
  43. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  44. package/dist/template-bindings/animation-funcs.js +83 -0
  45. package/dist/template-bindings/color-funcs.d.ts +13 -0
  46. package/dist/template-bindings/color-funcs.js +1 -1
  47. package/dist/template-bindings/helpers.d.ts +6 -0
  48. package/dist/template-bindings/helpers.js +12 -3
  49. package/dist/template-bindings/index.d.ts +6 -2
  50. package/dist/template-bindings/index.js +6 -2
  51. package/package.json +1 -1
@@ -1,14 +1,65 @@
1
1
  /**
2
- * Spinner — animated terminal spinner with optional text label.
2
+ * Spinner — a named spinner, asked for its frame at a time, with an optional
3
+ * label after it.
4
+ *
5
+ * A spinner is a `FrameSequence` (`./frames.ts`) over one entry of the named spinner
6
+ * data (`core/spinnerData.ts`): each frame held for the spinner's interval,
7
+ * looping, the frame in progress at `t` drawn. The data names the frames and
8
+ * the interval; the beat, the width every frame draws at and which frame `t`
9
+ * lands on are the frames primitive's, so a spinner is one more sequence on
10
+ * it and not a second way of turning frames. Custom frames are a `FrameSequence`
11
+ * drawn through `Frames` directly. [LAW:one-source-of-truth]
12
+ *
13
+ * It is built once and asked for each frame, as Rich's `Spinner` is built
14
+ * once and has `render(time)` called on it: the name is parsed and the
15
+ * styled frames are prepared where the spinner is given, and `at(t)` is what
16
+ * draws, so a `Status` or `SpinnerColumn` holding one pays for the name and
17
+ * the styling once, and a bad name fails at the call that gave it.
18
+ * [LAW:parse-dont-validate] What a `FrameSequence` makes of the frames —
19
+ * the beat, the width, each frame padded to it — is made there too, so a
20
+ * drawn frame costs only finding which one `t` lands on.
21
+ *
22
+ * [LAW:no-ambient-temporal-coupling] A Spinner draws nothing on its own:
23
+ * only `at(t)` is a renderable, so a spinner cannot be drawn without being
24
+ * handed its time, and nothing here reads a clock. Whoever draws frames
25
+ * hands each one its time (`core/clock.ts`) — `SpinnerColumn` the task's
26
+ * `time`, a `Live` the frame's time on its clock — as Rich's `Spinner.render`
27
+ * is handed `console.get_time()` or `task.get_time()`. `t` is the clock's
28
+ * time, as `App` and `Task.time` hand it, where Rich's spinner counts from
29
+ * its first draw: here nothing is kept between frames, so the same `t` draws
30
+ * the same bytes at one redraw a second or sixty, two spinners on one clock
31
+ * agree at every `t`, a `Progress` on a hand-moved clock turns its spinners
32
+ * on that clock, and a draw costs the same at `t = 0` and `t = 10⁶`.
33
+ *
34
+ * `speed` scales the holds, not the time: at speed 2 each frame is held half
35
+ * the interval, and a negative speed is the frames in reverse order, each held
36
+ * the interval over the speed's size. [LAW:one-source-of-truth] Rich scales
37
+ * the time instead, `t × speed` into the beat. At a positive speed the frame
38
+ * drawn is the same; but when the frame next changes (`Style.nextChange`) is
39
+ * a time on the host's timeline, and a beat over scaled time answers on the
40
+ * scaled one — so the sequence is scaled where it is built, and the
41
+ * `FrameSequence` stamps host time with nothing to map back. Backwards, this is the plain
42
+ * reversed sequence from `t = 0`, where Rich's `int()` truncation holds
43
+ * frame 0 for a first interval and then counts down: a departure, made so a
44
+ * negative speed is one more sequence on the beat and not a second rule
45
+ * about time. A speed of zero would hold a frame forever, which is not a
46
+ * beat, and is refused where it is given.
3
47
  */
4
- import { cellLen } from "../core/cells.js";
5
48
  import { Segment } from "../core/segment.js";
6
49
  import { NULL_STYLE } from "../core/style.js";
7
50
  import { RichText } from "../core/text.js";
8
51
  import { SPINNERS, DEFAULT_SPINNER, LINE_SPINNER } from "../core/spinnerData.js";
9
52
  import { drawable } from "../core/protocol.js";
10
53
  import { EmbeddedText } from "./embed.js";
11
- const spinnerGlyphs = ({ frames }) => frames.join("");
54
+ import { FrameSequence } from "./frames.js";
55
+ const sequence = (data, style, speed) => {
56
+ const frames = data.frames.map((frame) => ({
57
+ text: new RichText("", { end: "" }).append(frame, style),
58
+ seconds: data.interval / 1000 / Math.abs(speed),
59
+ }));
60
+ return { frames: new FrameSequence(speed < 0 ? frames.reverse() : frames), glyphs: data.frames.join("") };
61
+ };
62
+ const sequenceGlyphs = ({ glyphs }) => glyphs;
12
63
  export class Spinner {
13
64
  name;
14
65
  speed;
@@ -22,82 +73,78 @@ export class Spinner {
22
73
  */
23
74
  text;
24
75
  _data;
25
- _frameIndex;
26
- _lastUpdate;
76
+ // The named data's frames, and the `line` spinner's that stand in for them
77
+ // on an ASCII-only output: prepared once, which is drawn chosen at `at`.
78
+ _named;
79
+ _ascii;
27
80
  constructor(name, text = "", options) {
28
81
  const spinnerName = name ?? DEFAULT_SPINNER;
29
82
  const data = SPINNERS[spinnerName];
30
83
  if (!data) {
31
84
  throw new Error(`Unknown spinner: "${spinnerName}"`);
32
85
  }
86
+ const speed = options?.speed ?? 1;
87
+ // [LAW:parse-dont-validate] A hold is a positive number of seconds, so the
88
+ // speed it is divided by is finite and not zero, settled here.
89
+ if (!Number.isFinite(speed) || speed === 0) {
90
+ throw new RangeError(`Spinner speed must be finite and not zero, got ${speed}`);
91
+ }
33
92
  this.name = spinnerName;
34
93
  this.text = text;
35
- this.speed = options?.speed ?? 1;
94
+ this.speed = speed;
36
95
  this.style = options?.style ?? NULL_STYLE;
37
96
  this._data = data;
38
- this._frameIndex = 0;
39
- this._lastUpdate = Date.now();
97
+ this._named = sequence(data, this.style, speed);
98
+ this._ascii = sequence(LINE_SPINNER, this.style, speed);
40
99
  }
41
100
  get frames() {
42
101
  return this._data.frames;
43
102
  }
103
+ /** Milliseconds each frame is held, as the spinner data gives it. */
44
104
  get interval() {
45
105
  return this._data.interval;
46
106
  }
47
- /** Advance the frame count by the time elapsed, and return it. */
48
- _advance() {
49
- const now = Date.now();
50
- // Whole frames passed at this speed; a negative speed steps backwards, as
51
- // Rich's does, so the index wraps with a modulo that is never negative.
52
- const steps = Math.trunc(((now - this._lastUpdate) * this.speed) / this.interval);
53
- if (steps !== 0) {
54
- const count = this._data.frames.length;
55
- this._frameIndex = (((this._frameIndex + steps) % count) + count) % count;
56
- this._lastUpdate = now;
57
- }
58
- return this._frameIndex;
59
- }
60
- /** The frames this output can draw: the `line` spinner stands in for any other on an ASCII-only one. */
61
- _drawnFrames(options) {
62
- return drawable(options, this._data, LINE_SPINNER, spinnerGlyphs).frames;
63
- }
64
107
  /**
65
- * The current frame and its label as one line wrapped to the width and
66
- * ended, as the reference's `Text` is — printed, stacked in a `Group`, or
67
- * laid out in a `Progress` cell alike.
108
+ * The frame in progress at `t` seconds and the label, as one renderable:
109
+ * the line wrapped to the width and ended, as the reference's `Text` is —
110
+ * printed, stacked in a `Group`, or laid out in a `Progress` cell alike.
111
+ * The frames this output can draw are chosen where the output is known.
68
112
  */
69
- *render(options) {
70
- yield* this._currentLine(options).render(options);
71
- yield Segment.line();
113
+ at(t) {
114
+ if (!Number.isFinite(t)) {
115
+ throw new RangeError(`Spinner.at needs a finite t, got ${t}`);
116
+ }
117
+ const frames = (options) => drawable(options, this._named, this._ascii, sequenceGlyphs).frames;
118
+ return {
119
+ render: (options) => this._render(frames(options), t, options),
120
+ // Measured at the frames' one width, the widest frame's, so a column
121
+ // holding it never narrows as it turns, and never offered less than
122
+ // that frame: a frame is one picture, which the reference's `Text`
123
+ // measure would let wrap apart at its spaces.
124
+ measure: (options) => {
125
+ const drawn = frames(options);
126
+ const { minimum, maximum } = this._line(drawn, t, options).measure(options);
127
+ return { minimum: Math.max(minimum, Math.min(drawn.width, maximum)), maximum };
128
+ },
129
+ };
72
130
  }
73
- _currentLine(options) {
74
- const frames = this._drawnFrames(options);
75
- return this._line(frames[this._advance() % frames.length], options);
131
+ *_render(frames, t, options) {
132
+ yield* this._line(frames, t, options).render(options);
133
+ yield Segment.line();
76
134
  }
77
135
  /**
78
- * `frame` and the label as one text, as Rich's `Text.assemble(frame, " ",
79
- * text)`: a long label wraps as one line with its frame. A label that draws
80
- * nothing — the default `""`, or markup that styles nothing — is no label,
81
- * not a trailing space, as an empty `Text` is falsy there.
136
+ * The frame in progress and the label as one text, as Rich's
137
+ * `Text.assemble(frame, " ", text)`: a long label wraps as one line with its
138
+ * frame. A label that draws nothing — the default `""`, or markup that
139
+ * styles nothing — is no label, not a trailing space, as an empty `Text` is
140
+ * falsy there.
82
141
  */
83
- _line(frame, options) {
84
- const line = new RichText("", { end: "" }).append(frame, this.style);
142
+ _line(frames, t, options) {
143
+ const line = new RichText("", { end: "" }).append(frames.text(t));
85
144
  // Unhighlighted: Rich's label is a `Text` from the moment it is handed over, and a `Text` is never highlighted.
86
145
  const label = new EmbeddedText(this.text).text({ ...options, highlight: false });
87
146
  if (label.plain !== "")
88
147
  line.append(" ").append(label);
89
148
  return line;
90
149
  }
91
- /**
92
- * Measured at its widest frame, so a column holding it never narrows as it
93
- * turns, and never offered less than that frame: a frame is one picture,
94
- * which the reference's `Text` measure would let wrap apart at its spaces.
95
- */
96
- measure(options) {
97
- const frames = this._drawnFrames(options);
98
- const widths = frames.map((frame) => cellLen(frame));
99
- const frameWidth = Math.max(...widths);
100
- const { minimum, maximum } = this._line(frames[widths.indexOf(frameWidth)], options).measure(options);
101
- return { minimum: Math.max(minimum, Math.min(frameWidth, maximum)), maximum };
102
- }
103
150
  }
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Status — displays a spinner animation with a status message.
3
3
  *
4
- * Its renderable is a `Spinner` whose label is the message, as Rich's is, so
5
- * the message is drawn the way the spinner draws its label: a string is markup
6
- * under the console that draws it.
4
+ * Its view is a `Spinner` whose label is the message, as Rich's is, so the
5
+ * message is drawn the way the spinner draws its label: a string is markup
6
+ * under the console that draws it. The `Live` hands each frame its time, and
7
+ * the spinner is asked for its frame at it, so the frame is the clock's, not
8
+ * the platform's. [LAW:no-ambient-temporal-coupling]
7
9
  */
10
+ import type { Clock } from "../core/clock.js";
8
11
  import type { Style } from "../core/style.js";
9
12
  import type { RichText } from "../core/text.js";
10
13
  import { Console } from "../core/console.js";
@@ -14,6 +17,8 @@ export interface StatusOptions {
14
17
  /** The spinner's frame style; the message styles itself with its markup. */
15
18
  spinnerStyle?: string | Style;
16
19
  console?: Console;
20
+ /** What the frames are timed and drawn on. The platform's, unless given. */
21
+ clock?: Clock;
17
22
  }
18
23
  export declare class Status {
19
24
  private _live;
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Status — displays a spinner animation with a status message.
3
3
  *
4
- * Its renderable is a `Spinner` whose label is the message, as Rich's is, so
5
- * the message is drawn the way the spinner draws its label: a string is markup
6
- * under the console that draws it.
4
+ * Its view is a `Spinner` whose label is the message, as Rich's is, so the
5
+ * message is drawn the way the spinner draws its label: a string is markup
6
+ * under the console that draws it. The `Live` hands each frame its time, and
7
+ * the spinner is asked for its frame at it, so the frame is the clock's, not
8
+ * the platform's. [LAW:no-ambient-temporal-coupling]
7
9
  */
8
10
  import { Console } from "../core/console.js";
9
11
  import { Spinner } from "./spinner.js";
@@ -18,8 +20,9 @@ export class Status {
18
20
  speed: options?.speed,
19
21
  style: options?.spinnerStyle ?? "status.spinner",
20
22
  });
21
- this._live = new Live(this._spinner, {
23
+ this._live = new Live((t) => this._spinner.at(t), {
22
24
  console: this._console,
25
+ clock: options?.clock,
23
26
  transient: true,
24
27
  refreshPerSecond: 12.5,
25
28
  });
@@ -41,6 +44,6 @@ export class Status {
41
44
  }
42
45
  update(message) {
43
46
  this._spinner.text = message;
44
- this._live.update(this._spinner, { refresh: true });
47
+ this._live.refresh();
45
48
  }
46
49
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Animation registrations for the rich-js template binding: what a template
3
+ * draws for the moment it is evaluated at.
4
+ *
5
+ * A host that builds its cells in templates — cc-candybar's statusline, say —
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.
10
+ * [LAW:no-ambient-temporal-coupling] A host hands in the same `t` and gets
11
+ * the same fragment back; a host that redraws once a second or sixty times
12
+ * a second registers the same functions.
13
+ *
14
+ * `frames <seconds> <frame> <seconds> <frame> …` is a `FrameSequence`
15
+ * (`renderables/frames.ts`) in a template: the frame in progress at the
16
+ * moment, as a fragment at the width of the widest. Its frames arrive as
17
+ * arguments on every evaluation, so the sequence is prepared on every one:
18
+ * a template has nowhere to keep it, and keeping it here would be the memo
19
+ * keyed on the arguments that `FrameSequence` is instead of. `stepped <seconds>
20
+ * <color> <seconds> <color> …` is the colour in progress at the moment, a
21
+ * colour value like every colour function's (`./color-funcs.ts`), so it
22
+ * nests where a colour goes — `fg (stepped …)`, `readableOn (stepped …) $bg
23
+ * 4.5` — and the ground it must read on is the template's to name, as it
24
+ * is for any colour. Either argument list is one float/value cycle end to
25
+ * end, so the engine's `alternating` gate types every slot and lifts each
26
+ * string frame to a fragment; the pairing — a trailing hold with no frame or
27
+ * colour — is parsed in `pairs`, once. [LAW:types-are-the-program]
28
+ */
29
+ import type { FuncMap } from "@promptctl/go-template-js";
30
+ /**
31
+ * Register `frames` and `stepped`, each drawn for the moment `frameTime`
32
+ * reports when the template is evaluated: seconds on the host's own
33
+ * timeline, from whatever zero it counts from. The functions exist whatever
34
+ * `frameTime` returns, so a template compiles once and is evaluated at every
35
+ * moment the host draws.
36
+ */
37
+ export declare function animationFuncs(frameTime: () => number): FuncMap;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Animation registrations for the rich-js template binding: what a template
3
+ * draws for the moment it is evaluated at.
4
+ *
5
+ * A host that builds its cells in templates — cc-candybar's statusline, say —
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.
10
+ * [LAW:no-ambient-temporal-coupling] A host hands in the same `t` and gets
11
+ * the same fragment back; a host that redraws once a second or sixty times
12
+ * a second registers the same functions.
13
+ *
14
+ * `frames <seconds> <frame> <seconds> <frame> …` is a `FrameSequence`
15
+ * (`renderables/frames.ts`) in a template: the frame in progress at the
16
+ * moment, as a fragment at the width of the widest. Its frames arrive as
17
+ * arguments on every evaluation, so the sequence is prepared on every one:
18
+ * a template has nowhere to keep it, and keeping it here would be the memo
19
+ * keyed on the arguments that `FrameSequence` is instead of. `stepped <seconds>
20
+ * <color> <seconds> <color> …` is the colour in progress at the moment, a
21
+ * colour value like every colour function's (`./color-funcs.ts`), so it
22
+ * nests where a colour goes — `fg (stepped …)`, `readableOn (stepped …) $bg
23
+ * 4.5` — and the ground it must read on is the template's to name, as it
24
+ * is for any colour. Either argument list is one float/value cycle end to
25
+ * end, so the engine's `alternating` gate types every slot and lifts each
26
+ * string frame to a fragment; the pairing — a trailing hold with no frame or
27
+ * colour — is parsed in `pairs`, once. [LAW:types-are-the-program]
28
+ */
29
+ import { beat } from "../core/easing.js";
30
+ import { FrameSequence } from "../renderables/frames.js";
31
+ import { asColor } from "./color-funcs.js";
32
+ import { asFragment } from "./helpers.js";
33
+ /**
34
+ * The `(seconds, value)` pairs a call spells, each value read through
35
+ * `value`. The gate has typed every even slot as a float and every odd one
36
+ * as `value`'s input; what it cannot see is the pairing, or that a hold is
37
+ * a positive number of seconds: both settled here, under the engine's
38
+ * "error calling <function>" and nothing else, since a template author
39
+ * knows neither `FrameSequence` nor `beat`, which refuse the same hold in their
40
+ * own names. [LAW:parse-dont-validate]
41
+ */
42
+ function pairs(what, tail, value) {
43
+ if (tail.length % 2 !== 0) {
44
+ throw new RangeError(`last hold (${String(tail[tail.length - 1])} s) has no ${what} — the arguments are <seconds> <${what}> pairs`);
45
+ }
46
+ const paired = [];
47
+ for (let i = 0; i < tail.length; i += 2) {
48
+ const seconds = tail[i];
49
+ if (!Number.isFinite(seconds) || seconds <= 0) {
50
+ throw new RangeError(`hold ${i / 2} must be a positive number of seconds, got ${seconds}`);
51
+ }
52
+ paired.push({ seconds, value: value(tail[i + 1]) });
53
+ }
54
+ return paired;
55
+ }
56
+ /**
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.
62
+ */
63
+ export function animationFuncs(frameTime) {
64
+ const framesFunc = {
65
+ fn: ((...tail) => {
66
+ const frames = pairs("frame", tail, asFragment).map(({ seconds, value }) => ({ seconds, text: value }));
67
+ return new FrameSequence(frames).text(frameTime());
68
+ }),
69
+ argTypes: ["float", "liftable"],
70
+ arity: { kind: "alternating", minimum: 2 },
71
+ returnType: "T",
72
+ };
73
+ const steppedFunc = {
74
+ fn: ((...tail) => {
75
+ const steps = pairs("color", tail, (v) => asColor(v, "stepped"));
76
+ return steps[beat(steps.map((step) => step.seconds)).at(frameTime())].value.hex;
77
+ }),
78
+ argTypes: ["float", "string"],
79
+ arity: { kind: "alternating", minimum: 2 },
80
+ returnType: "string",
81
+ };
82
+ return { frames: framesFunc, stepped: steppedFunc };
83
+ }
@@ -57,6 +57,19 @@
57
57
  */
58
58
  import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
59
59
  import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
60
+ /**
61
+ * The gate every color argument crosses. [LAW:parse-dont-validate] — the
62
+ * function bodies below receive `ColorRgba` and never re-check.
63
+ *
64
+ * Parsing itself is `parseHexColor`'s job; this wrapper exists only to replace
65
+ * its error *wording*, because the mistake it catches is almost always the
66
+ * same one — a palette name passed where a color belongs (`darken "primary" 2`)
67
+ * — and an author who reads "wrap it: darken (color "primary")" is corrected in
68
+ * one step, where "expected #RRGGBB" leaves them guessing. What it parses
69
+ * must then be a colour these functions can compute with (`computable`).
70
+ * [LAW:no-silent-failure]
71
+ */
72
+ export declare function asColor(value: string, func: string): ColorRgba;
60
73
  /**
61
74
  * A colour the colour functions can compute with. A fully transparent one — a
62
75
  * theme's "no background", `#00000000` — has no colour of its own: what shows
@@ -70,7 +70,7 @@ import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
70
70
  * must then be a colour these functions can compute with (`computable`).
71
71
  * [LAW:no-silent-failure]
72
72
  */
73
- function asColor(value, func) {
73
+ export function asColor(value, func) {
74
74
  let colour;
75
75
  try {
76
76
  colour = parseHexColor(value.trim());
@@ -24,3 +24,9 @@ import { RichText } from "../core/text.js";
24
24
  * before the render whose theme says what the name stands for.
25
25
  */
26
26
  export declare function applyStyleToFragment(child: unknown, style: Style): RichText;
27
+ /**
28
+ * The gate every `"liftable"` slot's value crosses: the binding's own `T`, or
29
+ * a loud failure. [LAW:parse-dont-validate] What leaves is a `RichText`, so
30
+ * no body re-checks it.
31
+ */
32
+ export declare function asFragment(child: unknown): RichText;
@@ -24,10 +24,19 @@ import { RichText } from "../core/text.js";
24
24
  * before the render whose theme says what the name stands for.
25
25
  */
26
26
  export function applyStyleToFragment(child, style) {
27
+ const fragment = asFragment(child);
28
+ const result = fragment.copy();
29
+ result.style = layerStyle(fragment.style, style);
30
+ return result;
31
+ }
32
+ /**
33
+ * The gate every `"liftable"` slot's value crosses: the binding's own `T`, or
34
+ * a loud failure. [LAW:parse-dont-validate] What leaves is a `RichText`, so
35
+ * no body re-checks it.
36
+ */
37
+ export function asFragment(child) {
27
38
  if (!(child instanceof RichText)) {
28
39
  throw new TypeError(`template function expected a RichText fragment, got ${typeof child === "object" ? Object.prototype.toString.call(child) : typeof child}`);
29
40
  }
30
- const result = child.copy();
31
- result.style = layerStyle(child.style, style);
32
- return result;
41
+ return child;
33
42
  }
@@ -13,8 +13,11 @@
13
13
  * feeding it hex literals.
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
- * palette exists, which is why it is the only one that takes an argument.
17
- * [LAW:one-way-deps]
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`).
18
21
  *
19
22
  * `createRichTextEngine()` wires up `richTextFuncs()` only — it cannot supply a
20
23
  * palette. Consumers that name theme colours call `paletteFuncs(getPalette)`
@@ -40,6 +43,7 @@ import type { RenderOptions } from "../core/protocol.js";
40
43
  import { type DrawnAt } from "./color-funcs.js";
41
44
  export { paletteFuncs } from "./palette-funcs.js";
42
45
  export { colorFuncs, readableOnFunc, type DrawnAt } from "./color-funcs.js";
46
+ export { animationFuncs } from "./animation-funcs.js";
43
47
  /**
44
48
  * Funcs registered by the rich-js binding — the colour sinks, the palette-free
45
49
  * colour math, text attributes, and the `link` cell-splitter. Nothing here
@@ -13,8 +13,11 @@
13
13
  * feeding it hex literals.
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
- * palette exists, which is why it is the only one that takes an argument.
17
- * [LAW:one-way-deps]
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`).
18
21
  *
19
22
  * `createRichTextEngine()` wires up `richTextFuncs()` only — it cannot supply a
20
23
  * palette. Consumers that name theme colours call `paletteFuncs(getPalette)`
@@ -41,6 +44,7 @@ import { richTextStyleFuncs } from "./style-funcs.js";
41
44
  import { colorFuncs } from "./color-funcs.js";
42
45
  export { paletteFuncs } from "./palette-funcs.js";
43
46
  export { colorFuncs, readableOnFunc } from "./color-funcs.js";
47
+ export { animationFuncs } from "./animation-funcs.js";
44
48
  /**
45
49
  * Funcs registered by the rich-js binding — the colour sinks, the palette-free
46
50
  * colour math, text attributes, and the `link` cell-splitter. Nothing here
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/rich-js",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Rich text and beautiful formatting in the terminal — a TypeScript port of Python's Rich",
5
5
  "repository": {
6
6
  "type": "git",