@promptctl/rich-js 0.18.1 → 0.20.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 (169) hide show
  1. package/README.md +11 -8
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +99 -18
  9. package/dist/core/color.js +334 -146
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/noise.d.ts +31 -0
  33. package/dist/core/noise.js +94 -0
  34. package/dist/core/osc8.d.ts +6 -6
  35. package/dist/core/osc8.js +9 -21
  36. package/dist/core/paint.d.ts +107 -0
  37. package/dist/core/paint.js +161 -0
  38. package/dist/core/place.d.ts +1 -1
  39. package/dist/core/place.js +4 -6
  40. package/dist/core/pretty.d.ts +126 -2
  41. package/dist/core/pretty.js +571 -105
  42. package/dist/core/protocol.d.ts +43 -3
  43. package/dist/core/protocol.js +23 -2
  44. package/dist/core/render.d.ts +33 -30
  45. package/dist/core/render.js +43 -83
  46. package/dist/core/segment.d.ts +6 -5
  47. package/dist/core/segment.js +11 -15
  48. package/dist/core/selection.d.ts +12 -0
  49. package/dist/core/selection.js +14 -0
  50. package/dist/core/strip.d.ts +116 -74
  51. package/dist/core/strip.js +266 -217
  52. package/dist/core/style.d.ts +27 -7
  53. package/dist/core/style.js +106 -41
  54. package/dist/core/text.d.ts +43 -16
  55. package/dist/core/text.js +180 -59
  56. package/dist/core/wrap.d.ts +5 -0
  57. package/dist/core/wrap.js +16 -3
  58. package/dist/host/app.d.ts +95 -17
  59. package/dist/host/app.js +201 -61
  60. package/dist/host/index.d.ts +4 -1
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +84 -0
  63. package/dist/host/input.js +498 -0
  64. package/dist/host/terminal-host.d.ts +15 -2
  65. package/dist/host/terminal-host.js +2 -0
  66. package/dist/index.d.ts +26 -14
  67. package/dist/index.js +17 -7
  68. package/dist/node/prompt.d.ts +13 -0
  69. package/dist/node/prompt.js +30 -9
  70. package/dist/node/save.d.ts +4 -3
  71. package/dist/node/save.js +6 -3
  72. package/dist/node/terminal-host.d.ts +10 -2
  73. package/dist/node/terminal-host.js +7 -0
  74. package/dist/node/traceback.d.ts +2 -1
  75. package/dist/node/traceback.js +3 -39
  76. package/dist/renderables/align.js +2 -9
  77. package/dist/renderables/columns.d.ts +45 -29
  78. package/dist/renderables/columns.js +142 -103
  79. package/dist/renderables/constrain.js +6 -12
  80. package/dist/renderables/effect.d.ts +113 -0
  81. package/dist/renderables/effect.js +182 -0
  82. package/dist/renderables/effects.d.ts +252 -0
  83. package/dist/renderables/effects.js +565 -0
  84. package/dist/renderables/embed.d.ts +94 -31
  85. package/dist/renderables/embed.js +141 -45
  86. package/dist/renderables/group.d.ts +6 -3
  87. package/dist/renderables/group.js +5 -0
  88. package/dist/renderables/layout.d.ts +6 -23
  89. package/dist/renderables/layout.js +24 -113
  90. package/dist/renderables/list-view.d.ts +42 -0
  91. package/dist/renderables/list-view.js +78 -0
  92. package/dist/renderables/live.d.ts +40 -16
  93. package/dist/renderables/live.js +96 -83
  94. package/dist/renderables/markdown.d.ts +16 -2
  95. package/dist/renderables/markdown.js +521 -118
  96. package/dist/renderables/padding.d.ts +22 -1
  97. package/dist/renderables/padding.js +30 -24
  98. package/dist/renderables/panel.d.ts +31 -33
  99. package/dist/renderables/panel.js +137 -141
  100. package/dist/renderables/progress.d.ts +110 -16
  101. package/dist/renderables/progress.js +212 -81
  102. package/dist/renderables/progressBar.d.ts +24 -4
  103. package/dist/renderables/progressBar.js +78 -23
  104. package/dist/renderables/prompt.d.ts +37 -15
  105. package/dist/renderables/prompt.js +125 -85
  106. package/dist/renderables/ratio.d.ts +123 -0
  107. package/dist/renderables/ratio.js +213 -0
  108. package/dist/renderables/rule.d.ts +3 -2
  109. package/dist/renderables/rule.js +50 -52
  110. package/dist/renderables/spinner.d.ts +28 -2
  111. package/dist/renderables/spinner.js +52 -19
  112. package/dist/renderables/status.d.ts +13 -7
  113. package/dist/renderables/status.js +15 -29
  114. package/dist/renderables/syntax.d.ts +37 -7
  115. package/dist/renderables/syntax.js +134 -83
  116. package/dist/renderables/table.d.ts +58 -20
  117. package/dist/renderables/table.js +373 -197
  118. package/dist/renderables/traceback.d.ts +5 -4
  119. package/dist/renderables/traceback.js +239 -68
  120. package/dist/renderables/tree.d.ts +10 -2
  121. package/dist/renderables/tree.js +61 -34
  122. package/dist/renderables/viewport.d.ts +30 -0
  123. package/dist/renderables/viewport.js +61 -7
  124. package/dist/template-bindings/color-funcs.d.ts +31 -9
  125. package/dist/template-bindings/color-funcs.js +40 -14
  126. package/dist/template-bindings/helpers.d.ts +4 -2
  127. package/dist/template-bindings/helpers.js +5 -20
  128. package/dist/template-bindings/index.d.ts +29 -17
  129. package/dist/template-bindings/index.js +36 -20
  130. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  131. package/dist/template-bindings/palette-funcs.js +11 -7
  132. package/dist/template-bindings/style-funcs.js +4 -2
  133. package/dist/themes/buildPalette.d.ts +24 -3
  134. package/dist/themes/buildPalette.js +52 -8
  135. package/dist/themes/colorMath.d.ts +29 -17
  136. package/dist/themes/colorMath.js +74 -48
  137. package/dist/themes/colorRef.d.ts +2 -17
  138. package/dist/themes/colorRef.js +13 -31
  139. package/dist/themes/palette.d.ts +24 -0
  140. package/dist/themes/palette.js +46 -3
  141. package/dist/themes/ramp.d.ts +21 -34
  142. package/dist/themes/ramp.js +23 -42
  143. package/dist/themes/registry.js +39 -28
  144. package/dist/widgets/button.d.ts +2 -6
  145. package/dist/widgets/button.js +9 -40
  146. package/dist/widgets/checkbox.d.ts +2 -5
  147. package/dist/widgets/checkbox.js +9 -31
  148. package/dist/widgets/dropdown.d.ts +2 -5
  149. package/dist/widgets/dropdown.js +17 -51
  150. package/dist/widgets/event-router.d.ts +31 -40
  151. package/dist/widgets/event-router.js +52 -483
  152. package/dist/widgets/index.d.ts +2 -1
  153. package/dist/widgets/ink.d.ts +22 -0
  154. package/dist/widgets/ink.js +31 -0
  155. package/dist/widgets/slider.d.ts +2 -5
  156. package/dist/widgets/slider.js +11 -34
  157. package/dist/widgets/text-input.d.ts +19 -10
  158. package/dist/widgets/text-input.js +156 -147
  159. package/dist/widgets/themed-widget.d.ts +20 -0
  160. package/dist/widgets/themed-widget.js +89 -0
  161. package/dist/widgets/toggle.d.ts +2 -5
  162. package/dist/widgets/toggle.js +8 -31
  163. package/dist/widgets/types.d.ts +8 -25
  164. package/dist/widgets/types.js +8 -0
  165. package/dist/widgets/widget-app.d.ts +24 -7
  166. package/dist/widgets/widget-app.js +35 -18
  167. package/dist/widgets/widget-base.d.ts +2 -0
  168. package/dist/widgets/widget-base.js +9 -2
  169. package/package.json +31 -17
@@ -35,7 +35,7 @@ import { Segment } from "../core/segment.js";
35
35
  import { Measurement } from "../core/measure.js";
36
36
  import { cellCount, cellLen } from "../core/cells.js";
37
37
  import { NULL_STYLE } from "../core/style.js";
38
- import { drawable, fitHeight, getStyle, isMeasurable, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
38
+ import { drawable, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
39
39
  /** A heavy line for the thumb on a light line for the track. */
40
40
  export const SCROLLBAR = {
41
41
  thumb: { glyph: "┃", style: "scrollbar.thumb" },
@@ -58,6 +58,8 @@ export class Viewport {
58
58
  // Nothing rendered, nothing shown: no move can take until the first render.
59
59
  _extent = { rows: 0, lines: 0 };
60
60
  _moves = [];
61
+ // No range followed yet: NaN equals nothing, so the first `follow` moves.
62
+ _followed = { start: NaN, end: NaN, rows: NaN };
61
63
  constructor(content, options = {}) {
62
64
  this.content = content;
63
65
  this.rows = options.rows;
@@ -96,20 +98,47 @@ export class Viewport {
96
98
  ensureVisible(start, end) {
97
99
  this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
98
100
  }
101
+ /**
102
+ * `ensureVisible(start, end)`, when the range differs from the one the last
103
+ * `follow` named. A view rebuilt every frame calls this in every frame with
104
+ * its selection's lines, and the selection is brought into view when it
105
+ * moves — not on every frame, which would undo on its very render each
106
+ * scroll the mouse wheel queues. A followed range the last render showed
107
+ * whole stays shown when the row count changes: the same lines can fall off
108
+ * the bottom of a viewport that got shorter. One the wheel had scrolled
109
+ * away from stays away.
110
+ */
111
+ follow(start, end) {
112
+ if (start === this._followed.start && end === this._followed.end)
113
+ return;
114
+ this._followed = { start, end, rows: NaN };
115
+ this.ensureVisible(start, end);
116
+ }
99
117
  /**
100
118
  * The width its content renders at, out of `width`: what the scrollbar's
101
119
  * gutter leaves. Lines handed to `ensureVisible` are lines at this width.
102
120
  */
103
121
  contentWidth(width) {
104
- return cellCount(width - gutterWidth(this.scrollbar));
122
+ return cellCount(width - this.gutter);
123
+ }
124
+ /** The cells the scrollbar's gutter takes beside the content. */
125
+ get gutter() {
126
+ return gutterWidth(this.scrollbar);
105
127
  }
106
128
  *render(rawOptions) {
107
129
  const { height, ...options } = withBoundedWidth(rawOptions, this);
108
- const gutter = gutterWidth(this.scrollbar);
130
+ const gutter = this.gutter;
109
131
  const contentWidth = this.contentWidth(options.maxWidth);
110
132
  const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
111
133
  const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
134
+ const followed = this._followed;
135
+ const wasShown = followed.start >= this._offset && followed.end <= this._offset + this._extent.rows;
112
136
  this._extent = extent;
137
+ if (!Number.isNaN(followed.rows) && followed.rows !== extent.rows && wasShown) {
138
+ this.ensureVisible(followed.start, followed.end);
139
+ }
140
+ if (!Number.isNaN(followed.start))
141
+ this._followed = { ...followed, rows: extent.rows };
113
142
  this._offset = this.resolve(extent);
114
143
  this._moves = [];
115
144
  const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
@@ -153,14 +182,39 @@ export class Viewport {
153
182
  // `withBoundedWidth`: the bounded parse asks this very method for the
154
183
  // natural width.
155
184
  const options = withCellWidth(rawOptions);
156
- const gutter = gutterWidth(this.scrollbar);
185
+ const gutter = this.gutter;
157
186
  const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
158
- const content = isMeasurable(this.content)
159
- ? Measurement.get(inner, this.content)
160
- : new Measurement(Math.min(1, inner.maxWidth), inner.maxWidth);
187
+ const content = Measurement.get(inner, this.content);
161
188
  return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
162
189
  }
163
190
  }
191
+ /**
192
+ * One viewport per thing a pane shows, named by a key. The same viewport comes
193
+ * back while the key holds, so its scroll position does too; a new key gets a
194
+ * fresh one, so a new subject opens at its top rather than wherever the last
195
+ * one was left. A frame is rebuilt on every key press and a scroll position is
196
+ * a viewport's, so this is what outlives the frame for a pane whose subject
197
+ * changes under it.
198
+ */
199
+ export class ViewportPerKey {
200
+ options;
201
+ held;
202
+ constructor(options = {}) {
203
+ this.options = options;
204
+ this.held = { key: "", viewport: this.fresh() };
205
+ }
206
+ /** The viewport for `key`: the held one while the key holds, a fresh one otherwise. */
207
+ of(key) {
208
+ if (key !== this.held.key)
209
+ this.held = { key, viewport: this.fresh() };
210
+ return this.held.viewport;
211
+ }
212
+ fresh() {
213
+ // Empty until a view hands it content: the content is built where the frame is.
214
+ return new Viewport(EMPTY, this.options);
215
+ }
216
+ }
217
+ const EMPTY = { *render() { } };
164
218
  const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
165
219
  /** `scrollbar` in its own styles, drawn in ASCII. */
166
220
  function asciiScrollbar({ thumb, track }) {
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * ### The carrier is a hex string, deliberately
28
28
  *
29
- * A color crosses the template seam as `#RRGGBB`, not as an opaque object,
29
+ * A color crosses the template seam as `#RRGGBB` (or `#RRGGBBAA`), not as an opaque object,
30
30
  * for three reasons:
31
31
  *
32
32
  * 1. **The type separates.** `"string"` is the strictest arg slot the template
@@ -56,12 +56,34 @@
56
56
  * the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
57
57
  */
58
58
  import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
59
- import { ColorDepth } from "../core/color.js";
59
+ import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
60
60
  /**
61
- * The `readableOn` binding, measuring at the depth `drawnAt` names when it is
62
- * evaluated — a consumer that renders at a per-call depth registers its own.
61
+ * A colour the colour functions can compute with. A fully transparent one — a
62
+ * theme's "no background", `#00000000` — has no colour of its own: what shows
63
+ * through it is whatever surface it lands on, which a template cannot know.
64
+ * Darkening, mixing or contrasting it would answer for a surface nobody named,
65
+ * so it is refused here, for every colour function and every `ramp` stop.
66
+ * [LAW:single-enforcer]
63
67
  */
64
- export declare const readableOnFunc: (drawnAt: () => ColorDepth) => TemplateFunc;
68
+ export declare function computable(colour: ColorRgba, func: string, written: string): ColorRgba;
69
+ /**
70
+ * Where `readableOn`'s text will be drawn: the depth the output is encoded at,
71
+ * and the terminal that draws it when that is known. At sixteen colours a
72
+ * terminal paints each slot in its own theme's colour, so the floor is measured
73
+ * in `terminal`'s sixteen; without one, in the VGA defaults (the Windows
74
+ * console's at `WINDOWS`). Both are facts about the one place the text lands,
75
+ * so they arrive together.
76
+ */
77
+ export interface DrawnAt {
78
+ readonly depth: ColorDepth;
79
+ readonly terminal?: TerminalTheme;
80
+ }
81
+ /**
82
+ * The `readableOn` binding, measuring where `drawnAt` says the text lands when
83
+ * it is evaluated — a consumer that renders to a per-call destination
84
+ * registers its own.
85
+ */
86
+ export declare const readableOnFunc: (drawnAt: () => DrawnAt) => TemplateFunc;
65
87
  /**
66
88
  * The palette-free color vocabulary: every function takes colors and returns a
67
89
  * color, so they compose by nesting.
@@ -80,8 +102,8 @@ export declare const readableOnFunc: (drawnAt: () => ColorDepth) => TemplateFunc
80
102
  * Pair with `paletteFuncs()` to name colors from a theme, and with
81
103
  * `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
82
104
  *
83
- * @param drawnAt the depth the terminal will draw at, read on every
84
- * `readableOn` evaluation so a host that learns its depth per render passes
85
- * one getter; defaults to truecolor.
105
+ * @param drawnAt where the text will be drawn — the depth, and the terminal
106
+ * when known — read on every `readableOn` evaluation so a host that learns
107
+ * its destination per render passes one getter; defaults to truecolor.
86
108
  */
87
- export declare function colorFuncs(drawnAt?: () => ColorDepth): FuncMap;
109
+ export declare function colorFuncs(drawnAt?: () => DrawnAt): FuncMap;
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * ### The carrier is a hex string, deliberately
28
28
  *
29
- * A color crosses the template seam as `#RRGGBB`, not as an opaque object,
29
+ * A color crosses the template seam as `#RRGGBB` (or `#RRGGBBAA`), not as an opaque object,
30
30
  * for three reasons:
31
31
  *
32
32
  * 1. **The type separates.** `"string"` is the strictest arg slot the template
@@ -55,9 +55,8 @@
55
55
  * from that point on the math is pure. A consumer with no theme system at all still gets
56
56
  * the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
57
57
  */
58
- import { blendRgb, ColorDepth } from "../core/color.js";
58
+ import { blendRgb, ColorDepth, ColorParseError, parseHexColor } from "../core/color.js";
59
59
  import { Oklch, IDENTITY } from "../core/oklch.js";
60
- import { HEX_COLOR_RE, parseHexColor } from "../themes/colorRef.js";
61
60
  import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
62
61
  /**
63
62
  * The gate every color argument crosses. [LAW:parse-dont-validate] — the
@@ -67,15 +66,37 @@ import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
67
66
  * its error *wording*, because the mistake it catches is almost always the
68
67
  * same one — a palette name passed where a color belongs (`darken "primary" 2`)
69
68
  * — and an author who reads "wrap it: darken (color "primary")" is corrected in
70
- * one step, where "expected #RRGGBB" leaves them guessing.
69
+ * one step, where "expected #RRGGBB" leaves them guessing. What it parses
70
+ * must then be a colour these functions can compute with (`computable`).
71
71
  * [LAW:no-silent-failure]
72
72
  */
73
73
  function asColor(value, func) {
74
- if (!HEX_COLOR_RE.test(value.trim())) {
74
+ let colour;
75
+ try {
76
+ colour = parseHexColor(value.trim());
77
+ }
78
+ catch (cause) {
79
+ if (!(cause instanceof ColorParseError))
80
+ throw cause;
75
81
  throw new TypeError(`${func} expected a color (#RRGGBB or #RRGGBBAA), got ${JSON.stringify(value)}` +
76
- ` — to use a palette name here, wrap it: ${func} (color ${JSON.stringify(value)}) …`);
82
+ ` — to use a palette name here, wrap it: ${func} (color ${JSON.stringify(value)}) …`, { cause });
83
+ }
84
+ return computable(colour, func, JSON.stringify(value));
85
+ }
86
+ /**
87
+ * A colour the colour functions can compute with. A fully transparent one — a
88
+ * theme's "no background", `#00000000` — has no colour of its own: what shows
89
+ * through it is whatever surface it lands on, which a template cannot know.
90
+ * Darkening, mixing or contrasting it would answer for a surface nobody named,
91
+ * so it is refused here, for every colour function and every `ramp` stop.
92
+ * [LAW:single-enforcer]
93
+ */
94
+ export function computable(colour, func, written) {
95
+ if (colour.alpha === 0) {
96
+ throw new RangeError(`${func} got ${written}, which is fully transparent: it draws no colour of its own, ` +
97
+ `so there is nothing to compute with — use the colour of the surface it sits on, e.g. (color "background")`);
77
98
  }
78
- return parseHexColor(value);
99
+ return colour;
79
100
  }
80
101
  // [LAW:single-enforcer] Every colour function is fixed-arity, so the engine's
81
102
  // arity gate refuses a missing or surplus argument before the body runs.
@@ -145,11 +166,16 @@ const RATIO_RANGE = {
145
166
  note: "4.5 = AA body text, 3 = AA large text",
146
167
  };
147
168
  const contrastOnFunc = colorFunc(["string"], ((bgHex) => contrastFor(asColor(bgHex, "contrastOn")).hex));
169
+ const TRUECOLOR = { depth: ColorDepth.TRUECOLOR };
148
170
  /**
149
- * The `readableOn` binding, measuring at the depth `drawnAt` names when it is
150
- * evaluated — a consumer that renders at a per-call depth registers its own.
171
+ * The `readableOn` binding, measuring where `drawnAt` says the text lands when
172
+ * it is evaluated — a consumer that renders to a per-call destination
173
+ * registers its own.
151
174
  */
152
- export const readableOnFunc = (drawnAt) => colorFunc(["string", "string", "float"], ((fgHex, bgHex, ratio) => ensureContrast(asColor(fgHex, "readableOn"), asColor(bgHex, "readableOn"), asAmount(ratio, "readableOn", "ratio", RATIO_RANGE), drawnAt()).hex));
175
+ export const readableOnFunc = (drawnAt) => colorFunc(["string", "string", "float"], ((fgHex, bgHex, ratio) => {
176
+ const { depth, terminal } = drawnAt();
177
+ return ensureContrast(asColor(fgHex, "readableOn"), asColor(bgHex, "readableOn"), asAmount(ratio, "readableOn", "ratio", RATIO_RANGE), depth, undefined, terminal).hex;
178
+ }));
153
179
  // --- OKLCH axes ---
154
180
  //
155
181
  // [LAW:one-source-of-truth] One function per `ThemeKey` axis, and the table is
@@ -201,11 +227,11 @@ function oklchAxisFuncs() {
201
227
  * Pair with `paletteFuncs()` to name colors from a theme, and with
202
228
  * `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
203
229
  *
204
- * @param drawnAt the depth the terminal will draw at, read on every
205
- * `readableOn` evaluation so a host that learns its depth per render passes
206
- * one getter; defaults to truecolor.
230
+ * @param drawnAt where the text will be drawn — the depth, and the terminal
231
+ * when known — read on every `readableOn` evaluation so a host that learns
232
+ * its destination per render passes one getter; defaults to truecolor.
207
233
  */
208
- export function colorFuncs(drawnAt = () => ColorDepth.TRUECOLOR) {
234
+ export function colorFuncs(drawnAt = () => TRUECOLOR) {
209
235
  return {
210
236
  darken: darkenFunc,
211
237
  lighten: lightenFunc,
@@ -6,7 +6,7 @@
6
6
  * registrations and the palette-function registrations use this same operation;
7
7
  * keeping one copy prevents the two from drifting.
8
8
  */
9
- import { Style } from "../core/style.js";
9
+ import { type Style } from "../core/style.js";
10
10
  import { RichText } from "../core/text.js";
11
11
  /**
12
12
  * Apply a style on top of an already-styled RichText fragment.
@@ -19,6 +19,8 @@ import { RichText } from "../core/text.js";
19
19
  * is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
20
20
  * rather than producing a malformed fragment.
21
21
  *
22
- * Conflict resolution: `Style.add` — the outer (newly applied) style wins.
22
+ * Conflict resolution: `Style.add` — the outer (newly applied) style wins. A
23
+ * base style that is a name is layered under, not resolved: a template runs
24
+ * before the render whose theme says what the name stands for.
23
25
  */
24
26
  export declare function applyStyleToFragment(child: unknown, style: Style): RichText;
@@ -6,7 +6,7 @@
6
6
  * registrations and the palette-function registrations use this same operation;
7
7
  * keeping one copy prevents the two from drifting.
8
8
  */
9
- import { Style, StyleSyntaxError } from "../core/style.js";
9
+ import { layerStyle } from "../core/style.js";
10
10
  import { RichText } from "../core/text.js";
11
11
  /**
12
12
  * Apply a style on top of an already-styled RichText fragment.
@@ -19,30 +19,15 @@ import { RichText } from "../core/text.js";
19
19
  * is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
20
20
  * rather than producing a malformed fragment.
21
21
  *
22
- * Conflict resolution: `Style.add` — the outer (newly applied) style wins.
22
+ * Conflict resolution: `Style.add` — the outer (newly applied) style wins. A
23
+ * base style that is a name is layered under, not resolved: a template runs
24
+ * before the render whose theme says what the name stands for.
23
25
  */
24
26
  export function applyStyleToFragment(child, style) {
25
27
  if (!(child instanceof RichText)) {
26
28
  throw new TypeError(`template function expected a RichText fragment, got ${typeof child === "object" ? Object.prototype.toString.call(child) : typeof child}`);
27
29
  }
28
30
  const result = child.copy();
29
- result.style = baseStyleOf(child.style).add(style);
31
+ result.style = layerStyle(child.style, style);
30
32
  return result;
31
33
  }
32
- /**
33
- * A fragment's base style as a `Style`. A string must parse as a definition: a
34
- * name resolves only against the theme of the render that draws it, and a
35
- * template runs before any render exists.
36
- */
37
- function baseStyleOf(style) {
38
- if (style instanceof Style)
39
- return style;
40
- try {
41
- return Style.parse(style);
42
- }
43
- catch (err) {
44
- if (!(err instanceof StyleSyntaxError))
45
- throw err;
46
- throw new TypeError(`template function cannot style a fragment whose base style "${style}" is not a style definition: a style name resolves only against the theme of a render`, { cause: err });
47
- }
48
- }
@@ -36,9 +36,10 @@
36
36
  import { type Engine, type FuncMap } from "@promptctl/go-template-js";
37
37
  import { RichText } from "../core/text.js";
38
38
  import { Segment } from "../core/segment.js";
39
- import { ColorDepth } from "../core/color.js";
39
+ import type { RenderOptions } from "../core/protocol.js";
40
+ import { type DrawnAt } from "./color-funcs.js";
40
41
  export { paletteFuncs } from "./palette-funcs.js";
41
- export { colorFuncs, readableOnFunc } from "./color-funcs.js";
42
+ export { colorFuncs, readableOnFunc, type DrawnAt } from "./color-funcs.js";
42
43
  /**
43
44
  * Funcs registered by the rich-js binding — the colour sinks, the palette-free
44
45
  * colour math, text attributes, and the `link` cell-splitter. Nothing here
@@ -57,7 +58,7 @@ export { colorFuncs, readableOnFunc } from "./color-funcs.js";
57
58
  * into a wider engine must keep `T = RichText`; merging into an engine
58
59
  * whose `T` is something else will compile but fail at evaluation time.
59
60
  */
60
- export declare function richTextFuncs(drawnAt?: () => ColorDepth): FuncMap;
61
+ export declare function richTextFuncs(drawnAt?: () => DrawnAt): FuncMap;
61
62
  /**
62
63
  * Construct an `Engine<RichText>` with the rich-js style-function set
63
64
  * registered. Consumers that already manage their own engine should call
@@ -71,6 +72,18 @@ export declare function richTextFuncs(drawnAt?: () => ColorDepth): FuncMap;
71
72
  * not desired anyway.
72
73
  */
73
74
  export declare function createRichTextEngine(): Engine<RichText>;
75
+ export interface RenderTemplateOptions extends Partial<RenderOptions> {
76
+ /**
77
+ * Defaults to 400 — large enough that downstream `splitLines` /
78
+ * `adjustLineLength` clipping decides actual width, matching the typical
79
+ * "render wide, fit on output" pipeline. The error line is fitted to it too,
80
+ * so only a caller that passes the width it draws at sees a cut error end in
81
+ * `…` rather than be cropped by whatever draws it.
82
+ */
83
+ maxWidth?: number;
84
+ /** The error line's style, a `Style.parse` spec (default `"red dim"`). */
85
+ errorStyle?: string;
86
+ }
74
87
  /**
75
88
  * Compile a template source against `engine` and render the result to a
76
89
  * flat `Segment[]`. The 90%-case convenience over chaining `engine.compile`,
@@ -79,14 +92,20 @@ export declare function createRichTextEngine(): Engine<RichText>;
79
92
  * - Runs `engine.compile(source)(scope)` to get the engine's `RichText[]`.
80
93
  * - Flattens that fragment list into a single styled `RichText` via
81
94
  * `RichText.fromFragments` so every fragment's wrapping style survives.
82
- * - Renders to a `Segment[]` at the requested `maxWidth`.
83
- * - Wraps the whole flow in a try/catch — on parse/evaluate failure,
84
- * emits a single dim styled `[error: <message>]` segment the caller can
85
- * drop into their layout. No bespoke fallback wiring required at every
95
+ * - Renders to a `Segment[]` with the given render options — `maxWidth`,
96
+ * `theme`, `onStyleError` and the rest — as a terminal unless they say not.
97
+ * - On a parse/evaluate failure, emits a single dim styled
98
+ * `[error: <message>]` segment, one line fitted to `maxWidth`, the caller
99
+ * can drop into their layout. No bespoke fallback wiring required at every
86
100
  * call site.
87
101
  *
102
+ * Only compile, evaluate and flatten are inside the `try`: the error line reports a
103
+ * template that failed, nothing else. Whatever the render throws — a style
104
+ * error an `onStyleError` handler rethrew, or a fault in the render itself —
105
+ * leaves this function rather than being folded into an `[error: …]` line.
106
+ *
88
107
  * [LAW:single-enforcer] One place owns "render a template to segments,
89
- * degrade gracefully on errors" — every consumer that wants this exact
108
+ * degrade gracefully when the template fails" — every consumer that wants this exact
90
109
  * shape reads from here rather than re-implementing the same try/catch +
91
110
  * error-formatting glue.
92
111
  *
@@ -95,13 +114,6 @@ export declare function createRichTextEngine(): Engine<RichText>;
95
114
  * and use `RichText.fromFragments` to flatten. This helper is sugar for
96
115
  * the live-render case (e.g. a preview pane), not a replacement for the
97
116
  * compile-once-evaluate-many pattern.
98
- *
99
- * @param maxWidth defaults to 400 — large enough that downstream `splitLines`
100
- * / `adjustLineLength` clipping decides actual width, matching the typical
101
- * "render wide, fit on output" pipeline.
102
- * @param errorStyle is a `Style.parse` spec (default `"red dim"`).
117
+
103
118
  */
104
- export declare function renderTemplate(engine: Engine<RichText>, source: string, scope?: unknown, options?: {
105
- maxWidth?: number;
106
- errorStyle?: string;
107
- }): Segment[];
119
+ export declare function renderTemplate(engine: Engine<RichText>, source: string, scope?: unknown, options?: RenderTemplateOptions): Segment[];
@@ -91,14 +91,20 @@ export function createRichTextEngine() {
91
91
  * - Runs `engine.compile(source)(scope)` to get the engine's `RichText[]`.
92
92
  * - Flattens that fragment list into a single styled `RichText` via
93
93
  * `RichText.fromFragments` so every fragment's wrapping style survives.
94
- * - Renders to a `Segment[]` at the requested `maxWidth`.
95
- * - Wraps the whole flow in a try/catch — on parse/evaluate failure,
96
- * emits a single dim styled `[error: <message>]` segment the caller can
97
- * drop into their layout. No bespoke fallback wiring required at every
94
+ * - Renders to a `Segment[]` with the given render options — `maxWidth`,
95
+ * `theme`, `onStyleError` and the rest — as a terminal unless they say not.
96
+ * - On a parse/evaluate failure, emits a single dim styled
97
+ * `[error: <message>]` segment, one line fitted to `maxWidth`, the caller
98
+ * can drop into their layout. No bespoke fallback wiring required at every
98
99
  * call site.
99
100
  *
101
+ * Only compile, evaluate and flatten are inside the `try`: the error line reports a
102
+ * template that failed, nothing else. Whatever the render throws — a style
103
+ * error an `onStyleError` handler rethrew, or a fault in the render itself —
104
+ * leaves this function rather than being folded into an `[error: …]` line.
105
+ *
100
106
  * [LAW:single-enforcer] One place owns "render a template to segments,
101
- * degrade gracefully on errors" — every consumer that wants this exact
107
+ * degrade gracefully when the template fails" — every consumer that wants this exact
102
108
  * shape reads from here rather than re-implementing the same try/catch +
103
109
  * error-formatting glue.
104
110
  *
@@ -107,27 +113,37 @@ export function createRichTextEngine() {
107
113
  * and use `RichText.fromFragments` to flatten. This helper is sugar for
108
114
  * the live-render case (e.g. a preview pane), not a replacement for the
109
115
  * compile-once-evaluate-many pattern.
110
- *
111
- * @param maxWidth defaults to 400 — large enough that downstream `splitLines`
112
- * / `adjustLineLength` clipping decides actual width, matching the typical
113
- * "render wide, fit on output" pipeline.
114
- * @param errorStyle is a `Style.parse` spec (default `"red dim"`).
116
+
115
117
  */
116
118
  export function renderTemplate(engine, source, scope = {}, options) {
119
+ const { errorStyle, ...renderOptions } = options ?? {};
120
+ const maxWidth = renderOptions.maxWidth ?? 400;
121
+ // Flattening belongs to the template: a scope value the engine passes
122
+ // through unlifted fails here, and that is a template that failed.
123
+ let text;
117
124
  try {
118
- const frags = engine.compile(source)(scope);
119
- const rt = RichText.fromFragments(frags);
120
- return Array.from(rt.render({
121
- maxWidth: options?.maxWidth ?? 400,
122
- isTerminal: true,
123
- }));
125
+ text = RichText.fromFragments(engine.compile(source)(scope));
124
126
  }
125
127
  catch (e) {
126
- // An engine error's `message` is its one-line summary; `String(e)` adds a
127
- // multi-line code frame, which would break this single segment across lines.
128
- const summary = e instanceof Error ? e.message : String(e);
129
- return [new Segment(`[error: ${summary.slice(0, 80)}]`, safeErrorStyle(options?.errorStyle))];
128
+ return [new Segment(errorLine(e, maxWidth), safeErrorStyle(errorStyle))];
130
129
  }
130
+ return Array.from(text.render({ isTerminal: true, ...renderOptions, maxWidth }));
131
+ }
132
+ /**
133
+ * A caught error as the one line `renderTemplate` promises. The summary is the
134
+ * error's `message` — `String(e)` on an engine error appends a multi-line code
135
+ * frame — and even a `message` can carry the character it is complaining about
136
+ * (`bad escape sequence \⏎`). Any whitespace that is not a plain space — a line
137
+ * break, or a tab, whose stop depends on the column the line is set at and so
138
+ * cannot be measured here — folds, with the spaces around it, to one space.
139
+ *
140
+ * [LAW:one-source-of-truth] The line fits the same `maxWidth` the success path
141
+ * draws at, not a width of its own, and a cut ends in an ellipsis so it never
142
+ * reads as the whole message.
143
+ */
144
+ function errorLine(e, maxWidth) {
145
+ const summary = (e instanceof Error ? e.message : String(e)).replace(/\s*[^\S ]\s*/g, " ");
146
+ return new RichText(`[error: ${summary}]`).truncate(maxWidth).plain;
131
147
  }
132
148
  // [LAW:single-enforcer] The error-path Style must not itself throw, or
133
149
  // the whole "degrade gracefully" promise of renderTemplate is broken. A
@@ -67,7 +67,9 @@ import type { Palette } from "../themes/palette.js";
67
67
  /**
68
68
  * Register `color "name-or-hex"` against a live palette.
69
69
  *
70
- * `color` resolves a palette variable name to a `#RRGGBB` string, and passes
70
+ * `color` resolves a palette variable name to its hex string — `#RRGGBB`, or
71
+ * `#00000000` for a theme's "no background", which `bg` paints as no
72
+ * background and the colour functions refuse — and passes
71
73
  * an already-literal color through unchanged. That second half is not a
72
74
  * convenience — it makes `color` **idempotent**, which is what lets consumers
73
75
  * apply it unconditionally to any author-written color string without first
@@ -63,7 +63,9 @@
63
63
  * they are gone.
64
64
  */
65
65
  import { resolveColorRef } from "../themes/colorRef.js";
66
- import { ColorRamp, parseRampEasing } from "../themes/ramp.js";
66
+ import { parseEase } from "../core/easing.js";
67
+ import { ColorRamp } from "../themes/ramp.js";
68
+ import { computable } from "./color-funcs.js";
67
69
  /**
68
70
  * The `(position, color-ref)` pairs a `ramp` call's tail spells, as stops.
69
71
  *
@@ -85,7 +87,7 @@ function colorStops(tail, palette) {
85
87
  for (let i = 0; i < tail.length; i += 2) {
86
88
  stops.push({
87
89
  at: tail[i],
88
- color: resolveColorRef(palette, tail[i + 1]),
90
+ color: computable(resolveColorRef(palette, tail[i + 1]), "ramp", JSON.stringify(tail[i + 1])),
89
91
  });
90
92
  }
91
93
  return stops;
@@ -93,7 +95,9 @@ function colorStops(tail, palette) {
93
95
  /**
94
96
  * Register `color "name-or-hex"` against a live palette.
95
97
  *
96
- * `color` resolves a palette variable name to a `#RRGGBB` string, and passes
98
+ * `color` resolves a palette variable name to its hex string — `#RRGGBB`, or
99
+ * `#00000000` for a theme's "no background", which `bg` paints as no
100
+ * background and the colour functions refuse — and passes
97
101
  * an already-literal color through unchanged. That second half is not a
98
102
  * convenience — it makes `color` **idempotent**, which is what lets consumers
99
103
  * apply it unconditionally to any author-written color string without first
@@ -124,14 +128,14 @@ export function paletteFuncs(getPalette) {
124
128
  arity: { kind: "exact" },
125
129
  returnType: "string",
126
130
  };
127
- // `ramp <value> <easing> <position> <color> …` — the argument list is one
128
- // float/string cycle end to end (value, easing, then each stop's position
131
+ // `ramp <value> <ease> <position> <color> …` — the argument list is one
132
+ // float/string cycle end to end (value, ease, then each stop's position
129
133
  // and color), so the engine's `alternating` gate types every slot; the
130
134
  // pairing and the references are parsed in `colorStops`. Its minimum is the
131
- // value, the easing, and one stop, so the gate refuses a stopless call.
135
+ // value, the ease, and one stop, so the gate refuses a stopless call.
132
136
  // [LAW:types-are-the-program]
133
137
  const rampFunc = {
134
- fn: ((value, easing, ...tail) => new ColorRamp(parseRampEasing(easing), colorStops(tail, getPalette())).at(value)
138
+ fn: ((value, ease, ...tail) => new ColorRamp(parseEase(ease), colorStops(tail, getPalette())).at(value)
135
139
  .hex),
136
140
  argTypes: ["float", "string"],
137
141
  arity: { kind: "alternating", minimum: 4 },
@@ -113,11 +113,13 @@ const styleSpecFunc = {
113
113
  // `link` is the cell-splitter for the multi-cell consumer contract.
114
114
  // Implementation-wise it is the same shape as any other style function:
115
115
  // it sets the `link` slot of `Style` exactly as the existing string-form
116
- // `link URL` does, so a template-built fragment is byte-equivalent to
116
+ // `link URL` does, so `{{ link "u" "x" }}` is byte-equivalent to
117
117
  // `RichText("x", { style: Style.parse("link u") })`.
118
118
  //
119
119
  // The cell-boundary signal that consumers (cc-candybar et al.) walk is
120
- // `fragment.style.link` being truthy. `Style.add` propagates `link`
120
+ // `theme.resolve(fragment.style).link` being truthy: a fragment whose base
121
+ // style is a string keeps the link in a `LayeredStyle` until a theme says
122
+ // what the string stands for. `Style.add` propagates `link`
121
123
  // through any outer wrapping call, so `{{ fg "red" (link "u" "x") }}` and
122
124
  // `{{ link "u" "x" }}` produce shapes that both qualify as cells from
123
125
  // the consumer's perspective. Outer-wins on nested links comes for free
@@ -14,6 +14,8 @@ export interface BaseColors {
14
14
  background: ColorRgba;
15
15
  foreground: ColorRgba;
16
16
  }
17
+ type AccentKey = "primary" | "secondary" | "accent" | "success" | "warning" | "error";
18
+ export declare const ACCENT_KEYS: readonly AccentKey[];
17
19
  /**
18
20
  * Build a full semantic palette from base colors.
19
21
  *
@@ -25,12 +27,31 @@ export interface BaseColors {
25
27
  * de-emphasizing body/structural text reaches for
26
28
  * `foreground-muted` the same way it reaches for
27
29
  * `primary-muted` to de-emphasize an accent.
28
- * - `text-*` = contrast text tinted 66% with the accent color (use as
29
- * foreground in muted/background-tinted contexts)
30
+ * - `text-*` = contrast text tinted 66% with the accent color, then held
31
+ * to WCAG AA (4.5:1) on `*-muted` — the foreground for a
32
+ * `*-muted` ground
30
33
  * - `on-*` = WCAG-correct contrast colour (black or white) for use as
31
34
  * foreground when the FULL accent is the background. Picked
32
35
  * by relative luminance — single source of truth so widgets
33
36
  * never need to invert / reverse fg/bg to get readable text.
34
37
  * - `surface` = background blended 5% toward foreground
38
+ *
39
+ * A translucent base colour is drawn onto `background` before anything is
40
+ * derived from it, so `on-*` is chosen against the colour the accent is drawn
41
+ * as and every derived entry is opaque.
42
+ */
43
+ export declare function buildPalette(name: string, dark: boolean, given: BaseColors): Palette;
44
+ /**
45
+ * A palette's `text-*`: its tint held to WCAG AA on `*-muted`, moved toward
46
+ * the palette's own text side so it also reads on `background`, where widgets
47
+ * draw it too. The muted floor is the promise: where no lightness on the text
48
+ * side clears it, the colour that does is on the other side, and the
49
+ * background pair is the one given up. Every bundled palette clears both
50
+ * (test/themes/text-on-muted.test.ts).
51
+ *
52
+ * [LAW:single-enforcer] The one rule for the pair, for the palettes derived
53
+ * here and the authored ones the registry hydrates: the tint is the theme's,
54
+ * whether it reads is decided here.
35
55
  */
36
- export declare function buildPalette(name: string, dark: boolean, base: BaseColors): Palette;
56
+ export declare function accentText(tint: ColorRgba, muted: ColorRgba, background: ColorRgba): ColorRgba;
57
+ export {};