@promptctl/rich-js 0.18.0 → 0.18.1

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 (221) hide show
  1. package/dist/core/anchor.d.ts +45 -0
  2. package/dist/core/anchor.js +39 -0
  3. package/dist/core/ansi.d.ts +65 -0
  4. package/dist/core/ansi.js +231 -0
  5. package/dist/core/box.d.ts +143 -0
  6. package/dist/core/box.js +379 -0
  7. package/dist/core/cells.d.ts +159 -0
  8. package/dist/core/cells.js +287 -0
  9. package/dist/core/color.d.ts +246 -0
  10. package/dist/core/color.js +1038 -0
  11. package/dist/core/console.d.ts +165 -0
  12. package/dist/core/console.js +527 -0
  13. package/dist/core/emoji.d.ts +24 -0
  14. package/dist/core/emoji.js +391 -0
  15. package/dist/core/export-html.d.ts +59 -0
  16. package/dist/core/export-html.js +200 -0
  17. package/dist/core/export-lines.d.ts +121 -0
  18. package/dist/core/export-lines.js +141 -0
  19. package/dist/core/highlighter.d.ts +33 -0
  20. package/dist/core/highlighter.js +124 -0
  21. package/dist/core/json.d.ts +27 -0
  22. package/dist/core/json.js +53 -0
  23. package/dist/core/markup.d.ts +119 -0
  24. package/dist/core/markup.js +731 -0
  25. package/dist/core/measure.d.ts +27 -0
  26. package/dist/core/measure.js +62 -0
  27. package/dist/core/oklch.d.ts +161 -0
  28. package/dist/core/oklch.js +312 -0
  29. package/dist/core/osc8.d.ts +68 -0
  30. package/dist/core/osc8.js +90 -0
  31. package/dist/core/place.d.ts +30 -0
  32. package/dist/core/place.js +51 -0
  33. package/dist/core/pretty.d.ts +144 -0
  34. package/dist/core/pretty.js +476 -0
  35. package/dist/core/protocol.d.ts +267 -0
  36. package/dist/core/protocol.js +163 -0
  37. package/dist/core/render.d.ts +84 -0
  38. package/dist/core/render.js +145 -0
  39. package/dist/core/segment.d.ts +161 -0
  40. package/dist/core/segment.js +492 -0
  41. package/dist/core/spinnerData.d.ts +12 -0
  42. package/dist/core/spinnerData.js +117 -0
  43. package/dist/core/strip.d.ts +141 -0
  44. package/dist/core/strip.js +305 -0
  45. package/dist/core/style.d.ts +169 -0
  46. package/dist/core/style.js +695 -0
  47. package/dist/core/subscription.d.ts +12 -0
  48. package/dist/core/subscription.js +12 -0
  49. package/dist/core/text.d.ts +256 -0
  50. package/dist/core/text.js +976 -0
  51. package/dist/core/wrap.d.ts +41 -0
  52. package/dist/core/wrap.js +127 -0
  53. package/dist/host/app.d.ts +121 -0
  54. package/dist/host/app.js +255 -0
  55. package/dist/host/host-environment.d.ts +26 -0
  56. package/dist/host/host-environment.js +44 -0
  57. package/dist/host/index.d.ts +5 -0
  58. package/dist/host/index.js +3 -0
  59. package/dist/host/terminal-host.d.ts +179 -0
  60. package/dist/host/terminal-host.js +186 -0
  61. package/dist/index.d.ts +91 -0
  62. package/dist/index.js +107 -0
  63. package/dist/node/prompt.d.ts +21 -0
  64. package/dist/node/prompt.js +30 -0
  65. package/dist/node/save.d.ts +17 -0
  66. package/dist/node/save.js +21 -0
  67. package/dist/node/terminal-host.d.ts +113 -0
  68. package/dist/node/terminal-host.js +227 -0
  69. package/dist/node/traceback.d.ts +37 -0
  70. package/dist/node/traceback.js +134 -0
  71. package/dist/renderables/align.d.ts +16 -0
  72. package/dist/renderables/align.js +33 -0
  73. package/dist/renderables/columns.d.ts +61 -0
  74. package/dist/renderables/columns.js +152 -0
  75. package/dist/renderables/constrain.d.ts +15 -0
  76. package/dist/renderables/constrain.js +37 -0
  77. package/dist/renderables/embed.d.ts +50 -0
  78. package/dist/renderables/embed.js +71 -0
  79. package/dist/renderables/flexStrip.d.ts +41 -0
  80. package/dist/renderables/flexStrip.js +200 -0
  81. package/dist/renderables/group.d.ts +11 -0
  82. package/dist/renderables/group.js +17 -0
  83. package/dist/renderables/layout.d.ts +92 -0
  84. package/dist/renderables/layout.js +312 -0
  85. package/dist/renderables/live.d.ts +50 -0
  86. package/dist/renderables/live.js +175 -0
  87. package/dist/renderables/markdown.d.ts +24 -0
  88. package/dist/renderables/markdown.js +184 -0
  89. package/dist/renderables/padding.d.ts +38 -0
  90. package/dist/renderables/padding.js +141 -0
  91. package/dist/renderables/panel.d.ts +116 -0
  92. package/dist/renderables/panel.js +307 -0
  93. package/dist/renderables/progress.d.ts +90 -0
  94. package/dist/renderables/progress.js +238 -0
  95. package/dist/renderables/progressBar.d.ts +31 -0
  96. package/dist/renderables/progressBar.js +53 -0
  97. package/dist/renderables/prompt.d.ts +49 -0
  98. package/dist/renderables/prompt.js +125 -0
  99. package/dist/renderables/rule.d.ts +25 -0
  100. package/dist/renderables/rule.js +90 -0
  101. package/dist/renderables/spinner.d.ts +31 -0
  102. package/dist/renderables/spinner.js +70 -0
  103. package/dist/renderables/status.d.ts +23 -0
  104. package/dist/renderables/status.js +60 -0
  105. package/dist/renderables/syntax.d.ts +35 -0
  106. package/dist/renderables/syntax.js +115 -0
  107. package/dist/renderables/table.d.ts +194 -0
  108. package/dist/renderables/table.js +795 -0
  109. package/dist/renderables/traceback.d.ts +31 -0
  110. package/dist/renderables/traceback.js +108 -0
  111. package/dist/renderables/tree.d.ts +68 -0
  112. package/dist/renderables/tree.js +150 -0
  113. package/dist/renderables/viewport.d.ts +114 -0
  114. package/dist/renderables/viewport.js +198 -0
  115. package/dist/template-bindings/color-funcs.d.ts +87 -0
  116. package/dist/template-bindings/color-funcs.js +217 -0
  117. package/dist/template-bindings/helpers.d.ts +24 -0
  118. package/dist/template-bindings/helpers.js +48 -0
  119. package/dist/template-bindings/index.d.ts +107 -0
  120. package/dist/template-bindings/index.js +146 -0
  121. package/dist/template-bindings/palette-funcs.d.ts +94 -0
  122. package/dist/template-bindings/palette-funcs.js +141 -0
  123. package/dist/template-bindings/style-funcs.d.ts +28 -0
  124. package/dist/template-bindings/style-funcs.js +155 -0
  125. package/dist/themes/buildPalette.d.ts +36 -0
  126. package/dist/themes/buildPalette.js +56 -0
  127. package/dist/themes/colorMath.d.ts +82 -0
  128. package/dist/themes/colorMath.js +254 -0
  129. package/dist/themes/colorRef.d.ts +51 -0
  130. package/dist/themes/colorRef.js +96 -0
  131. package/dist/themes/data/ansi-tables.d.ts +14 -0
  132. package/dist/themes/data/ansi-tables.js +49 -0
  133. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  134. package/dist/themes/data/atom-one-dark.js +177 -0
  135. package/dist/themes/data/atom-one-light.d.ts +3 -0
  136. package/dist/themes/data/atom-one-light.js +177 -0
  137. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  138. package/dist/themes/data/catppuccin-frappe.js +177 -0
  139. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  140. package/dist/themes/data/catppuccin-latte.js +177 -0
  141. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  142. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  143. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  144. package/dist/themes/data/catppuccin-mocha.js +177 -0
  145. package/dist/themes/data/cyberpunk.d.ts +17 -0
  146. package/dist/themes/data/cyberpunk.js +193 -0
  147. package/dist/themes/data/default.d.ts +3 -0
  148. package/dist/themes/data/default.js +160 -0
  149. package/dist/themes/data/dracula.d.ts +3 -0
  150. package/dist/themes/data/dracula.js +177 -0
  151. package/dist/themes/data/flexoki.d.ts +3 -0
  152. package/dist/themes/data/flexoki.js +177 -0
  153. package/dist/themes/data/gruvbox.d.ts +3 -0
  154. package/dist/themes/data/gruvbox.js +177 -0
  155. package/dist/themes/data/index.d.ts +35 -0
  156. package/dist/themes/data/index.js +64 -0
  157. package/dist/themes/data/monokai.d.ts +3 -0
  158. package/dist/themes/data/monokai.js +177 -0
  159. package/dist/themes/data/nord.d.ts +3 -0
  160. package/dist/themes/data/nord.js +177 -0
  161. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  162. package/dist/themes/data/rose-pine-dawn.js +177 -0
  163. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  164. package/dist/themes/data/rose-pine-moon.js +177 -0
  165. package/dist/themes/data/rose-pine.d.ts +3 -0
  166. package/dist/themes/data/rose-pine.js +177 -0
  167. package/dist/themes/data/solarized-dark.d.ts +3 -0
  168. package/dist/themes/data/solarized-dark.js +177 -0
  169. package/dist/themes/data/solarized-light.d.ts +3 -0
  170. package/dist/themes/data/solarized-light.js +177 -0
  171. package/dist/themes/data/svg-export.d.ts +3 -0
  172. package/dist/themes/data/svg-export.js +177 -0
  173. package/dist/themes/data/textual-ansi.d.ts +3 -0
  174. package/dist/themes/data/textual-ansi.js +160 -0
  175. package/dist/themes/data/textual-dark.d.ts +3 -0
  176. package/dist/themes/data/textual-dark.js +160 -0
  177. package/dist/themes/data/textual-light.d.ts +3 -0
  178. package/dist/themes/data/textual-light.js +160 -0
  179. package/dist/themes/data/tokyo-night.d.ts +3 -0
  180. package/dist/themes/data/tokyo-night.js +177 -0
  181. package/dist/themes/data/types.d.ts +31 -0
  182. package/dist/themes/data/types.js +10 -0
  183. package/dist/themes/palette.d.ts +21 -0
  184. package/dist/themes/palette.js +28 -0
  185. package/dist/themes/ramp.d.ts +83 -0
  186. package/dist/themes/ramp.js +122 -0
  187. package/dist/themes/registry.d.ts +44 -0
  188. package/dist/themes/registry.js +94 -0
  189. package/dist/themes/terminalThemes.d.ts +49 -0
  190. package/dist/themes/terminalThemes.js +63 -0
  191. package/dist/themes/transpose.d.ts +87 -0
  192. package/dist/themes/transpose.js +138 -0
  193. package/dist/widgets/button.d.ts +49 -0
  194. package/dist/widgets/button.js +208 -0
  195. package/dist/widgets/checkbox.d.ts +41 -0
  196. package/dist/widgets/checkbox.js +157 -0
  197. package/dist/widgets/dropdown.d.ts +67 -0
  198. package/dist/widgets/dropdown.js +450 -0
  199. package/dist/widgets/event-router.d.ts +71 -0
  200. package/dist/widgets/event-router.js +629 -0
  201. package/dist/widgets/focus-manager.d.ts +45 -0
  202. package/dist/widgets/focus-manager.js +191 -0
  203. package/dist/widgets/hit.d.ts +64 -0
  204. package/dist/widgets/hit.js +113 -0
  205. package/dist/widgets/index.d.ts +20 -0
  206. package/dist/widgets/index.js +14 -0
  207. package/dist/widgets/slider.d.ts +57 -0
  208. package/dist/widgets/slider.js +270 -0
  209. package/dist/widgets/static-item.d.ts +34 -0
  210. package/dist/widgets/static-item.js +54 -0
  211. package/dist/widgets/text-input.d.ts +274 -0
  212. package/dist/widgets/text-input.js +1165 -0
  213. package/dist/widgets/toggle.d.ts +44 -0
  214. package/dist/widgets/toggle.js +180 -0
  215. package/dist/widgets/types.d.ts +87 -0
  216. package/dist/widgets/types.js +30 -0
  217. package/dist/widgets/widget-app.d.ts +63 -0
  218. package/dist/widgets/widget-app.js +199 -0
  219. package/dist/widgets/widget-base.d.ts +40 -0
  220. package/dist/widgets/widget-base.js +165 -0
  221. package/package.json +6 -2
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Measurement — min/max cell width calculation for renderables.
3
+ */
4
+ import { type Measurable, type RenderOptions } from "./protocol.js";
5
+ export declare class Measurement {
6
+ readonly minimum: number;
7
+ readonly maximum: number;
8
+ constructor(minimum: number, maximum: number);
9
+ get span(): number;
10
+ /**
11
+ * A range with no cell meaning becomes one with the nearest meaning there is:
12
+ * negative floors to zero, an inverted pair collapses to its ceiling, and NaN
13
+ * reads as zero cells the same way `cellCount` reads it.
14
+ *
15
+ * NaN and Infinity are not treated alike here, and the difference is the whole
16
+ * point. `Infinity` is a maximum a renderable means — `Table` reports it when
17
+ * a column asks for every cell there is, and `withBoundedWidth` throws on it
18
+ * so a caller learns their offer was unanswerable. Flooring it to zero would
19
+ * turn that loud failure into a table measured at no width at all.
20
+ */
21
+ normalize(): Measurement;
22
+ withMaximum(width: number): Measurement;
23
+ withMinimum(width: number): Measurement;
24
+ clamp(minWidth: number, maxWidth: number): Measurement;
25
+ static get(options: RenderOptions, measurable: Measurable): Measurement;
26
+ }
27
+ export declare function measureRenderables(options: RenderOptions, measurables: Measurable[]): Measurement;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Measurement — min/max cell width calculation for renderables.
3
+ */
4
+ import { measuring } from "./protocol.js";
5
+ export class Measurement {
6
+ minimum;
7
+ maximum;
8
+ constructor(minimum, maximum) {
9
+ this.minimum = minimum;
10
+ this.maximum = maximum;
11
+ }
12
+ get span() {
13
+ return this.maximum - this.minimum;
14
+ }
15
+ /**
16
+ * A range with no cell meaning becomes one with the nearest meaning there is:
17
+ * negative floors to zero, an inverted pair collapses to its ceiling, and NaN
18
+ * reads as zero cells the same way `cellCount` reads it.
19
+ *
20
+ * NaN and Infinity are not treated alike here, and the difference is the whole
21
+ * point. `Infinity` is a maximum a renderable means — `Table` reports it when
22
+ * a column asks for every cell there is, and `withBoundedWidth` throws on it
23
+ * so a caller learns their offer was unanswerable. Flooring it to zero would
24
+ * turn that loud failure into a table measured at no width at all.
25
+ */
26
+ normalize() {
27
+ const cells = (n) => (Number.isNaN(n) ? 0 : n);
28
+ const min = Math.max(0, Math.min(cells(this.minimum), cells(this.maximum)));
29
+ const max = Math.max(0, cells(this.maximum));
30
+ return new Measurement(min, max);
31
+ }
32
+ withMaximum(width) {
33
+ return new Measurement(Math.min(this.minimum, width), Math.min(this.maximum, width));
34
+ }
35
+ withMinimum(width) {
36
+ const min = Math.max(this.minimum, width);
37
+ const max = Math.max(this.maximum, min);
38
+ return new Measurement(min, max);
39
+ }
40
+ clamp(minWidth, maxWidth) {
41
+ return new Measurement(Math.min(Math.max(this.minimum, minWidth), maxWidth), Math.min(Math.max(this.maximum, minWidth), maxWidth));
42
+ }
43
+ // [LAW:single-enforcer] Single entry point for measuring a Measurable
44
+ static get(options, measurable) {
45
+ if (options.maxWidth < 1)
46
+ return new Measurement(0, 0);
47
+ const { minimum, maximum } = measurable.measure(measuring(options));
48
+ return new Measurement(minimum, Math.min(maximum, options.maxWidth)).normalize();
49
+ }
50
+ }
51
+ export function measureRenderables(options, measurables) {
52
+ if (measurables.length === 0)
53
+ return new Measurement(0, 0);
54
+ let minOfAll = 0;
55
+ let maxOfAll = 0;
56
+ for (const m of measurables) {
57
+ const measurement = Measurement.get(options, m);
58
+ minOfAll = Math.max(minOfAll, measurement.minimum);
59
+ maxOfAll = Math.max(maxOfAll, measurement.maximum);
60
+ }
61
+ return new Measurement(minOfAll, maxOfAll);
62
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * OKLCH color space — perceptually-uniform polar form of OKLab.
3
+ *
4
+ * Math from Björn Ottosson's OKLab specification:
5
+ * https://bottosson.github.io/posts/oklab/
6
+ *
7
+ * The full pipeline:
8
+ * sRGB (0..255 int) ↔ sRGB linear (0..1 float) ↔ OKLab ↔ OKLCH polar
9
+ * `fromRgba` walks left→right, `toRgba` walks right→left. The only lossy
10
+ * step is the final 0..255 quantization; the float math itself is
11
+ * reversible to within a few ULP.
12
+ *
13
+ * [LAW:one-type-per-behavior] OKLCH and sRGB are different *behaviors*:
14
+ * sRGB is the wire format terminals consume (what ANSI escape codes
15
+ * carry); OKLCH is the perceptual manipulation space (where equal
16
+ * numerical deltas mean equal perceptual deltas, which is the property
17
+ * "transposition" requires). Two types, mutual conversion, no hybrid.
18
+ *
19
+ * [LAW:one-way-deps] This module imports `ColorRgba` from core/color and
20
+ * nothing else from inside the project. Nothing in core/ depends back on
21
+ * this file.
22
+ */
23
+ import { ColorRgba } from "./color.js";
24
+ /**
25
+ * A perceptually-uniform color transform.
26
+ *
27
+ * [LAW:dataflow-not-control-flow] Identity is *data* (`IDENTITY`), not a
28
+ * special case. Every transposition runs the same code path — the values
29
+ * decide whether the operation is a no-op or a 180° rotation.
30
+ *
31
+ * Lightness is parameterized as `L' = clamp01(L * scale + shift)` to make
32
+ * "invert" expressible as `{scale: -1, shift: 1}` without a boolean knob.
33
+ */
34
+ export interface ThemeKey {
35
+ /** Hue offset in degrees; positive rotates toward warmer→cooler→back. */
36
+ readonly hueShift: number;
37
+ /** Multiplier on chroma. 0 = grayscale, 1 = identity, >1 = more saturated. */
38
+ readonly chromaScale: number;
39
+ /** Multiplier on lightness. 1 = identity, -1 = invert around the L axis. */
40
+ readonly lightnessScale: number;
41
+ /** Additive on lightness; applied *after* the scale. */
42
+ readonly lightnessShift: number;
43
+ }
44
+ /** How far `Oklch.mixAxes` moves each axis toward its target, each in [0, 1]. */
45
+ export interface OklchWeights {
46
+ readonly l: number;
47
+ readonly c: number;
48
+ readonly h: number;
49
+ readonly alpha: number;
50
+ }
51
+ export declare const IDENTITY: ThemeKey;
52
+ /**
53
+ * Flip lightness around the midpoint (`L' = 1 - L`) with hue and chroma
54
+ * untouched. Combined with the anchor logic in `transposePalette`, this
55
+ * converts a dark theme to its light "octave": semantic anchors
56
+ * (error/success/warning) still lightness-invert — only their *hue* is
57
+ * preserved — so errors stay red and dark-on-light becomes light-on-dark.
58
+ */
59
+ export declare const INVERT_LIGHTNESS: ThemeKey;
60
+ /**
61
+ * Whether a key is the no-op transform. Exported so that callers
62
+ * (`transposePalette`) can fast-path byte-exact identity without going
63
+ * through the lossy sRGB↔OKLCH round-trip. [LAW:one-source-of-truth] —
64
+ * the predicate lives once, here.
65
+ */
66
+ export declare function isIdentityKey(k: ThemeKey): boolean;
67
+ /**
68
+ * Immutable OKLCH color. `l` ∈ [0, 1] (lightness), `c` ≥ 0 (chroma; typical
69
+ * sRGB-representable colors top out near 0.32), `h` ∈ [0, 360) (hue
70
+ * degrees), `alpha` ∈ [0, 1].
71
+ *
72
+ * [LAW:single-enforcer] Constructor is the only validation site, matching
73
+ * the `ColorRgba` pattern. Out-of-range `alpha` throws. l/c/h must be
74
+ * finite but are *not* clamped at construction — they're clamped during
75
+ * `toRgba` instead, so transforms can pass through intermediate
76
+ * representations (e.g. h = 720°, c = -0.1 mid-pipeline) before the final
77
+ * sRGB-quantization step normalizes them.
78
+ */
79
+ export declare class Oklch {
80
+ #private;
81
+ readonly l: number;
82
+ readonly c: number;
83
+ readonly h: number;
84
+ readonly alpha: number;
85
+ constructor(l: number, c: number, h: number, alpha?: number);
86
+ /** sRGB → linear → OKLab → polar. Pure; no I/O, no allocation beyond the result. */
87
+ static fromRgba(color: ColorRgba): Oklch;
88
+ /**
89
+ * Polar → OKLab → linear → sRGB (with chroma-bisection gamut clamping).
90
+ *
91
+ * [LAW:single-enforcer] All sRGB-gamut clamping for OKLCH happens here.
92
+ * Callers don't see intermediate float channels; they get a valid
93
+ * `ColorRgba` or a thrown error.
94
+ */
95
+ toRgba(): ColorRgba;
96
+ /**
97
+ * Apply a `ThemeKey`. Returns a new `Oklch`; pure.
98
+ *
99
+ * [LAW:dataflow-not-control-flow] Identity short-circuits to `this` —
100
+ * same shape as `ColorRgba.compositeOver(bg)` returning `this` when
101
+ * `alpha === 1`. Callers invoke unconditionally; the data decides.
102
+ */
103
+ applyKey(k: ThemeKey): Oklch;
104
+ /**
105
+ * The color `t` of the way from this one toward `toward`, in OKLCH.
106
+ * `t = 0` is this color, `t = 1` is `toward`; pure.
107
+ *
108
+ * Lightness, chroma and alpha interpolate linearly. Hue takes the shorter
109
+ * arc around the wheel, so blue → red passes through magenta rather than
110
+ * sweeping across green. An achromatic endpoint has no hue of its own —
111
+ * `fromRgba` pins it to 0, which is red — so it adopts the other
112
+ * endpoint's hue (CSS Color 4's "powerless" rule): gray → red stays a red
113
+ * that gains chroma, instead of rotating through the wheel from 0°.
114
+ *
115
+ * `t` must lie in [0, 1]: this is interpolation, not extrapolation, and a
116
+ * `t` outside it (NaN included) is a RangeError, never a colour off the
117
+ * far end of the segment. [LAW:no-silent-failure]
118
+ *
119
+ * [LAW:one-source-of-truth] Achromatic means the same `ACHROMATIC_EPS`
120
+ * that `fromRgba` and `applyKey` pin hue by, so a gray produced by either
121
+ * is a gray here — and the result's hue goes through the same `hueOf`, so
122
+ * a gray produced HERE (t = 0 from a gray, t = 1 toward one) is a gray
123
+ * there too.
124
+ */
125
+ mix(toward: Oklch, t: number): Oklch;
126
+ /**
127
+ * `mix` with each axis moved its own share of the way: `weights.l` of the
128
+ * lightness gap, `weights.c` of the chroma gap, and so on, each in [0, 1].
129
+ * `mix(toward, t)` is `mixAxes` with every weight `t` — one interpolation,
130
+ * so the shorter-arc hue and the powerless-endpoint rule are the same
131
+ * whichever is called.
132
+ *
133
+ * It exists because perceptual axes are independent: a tint can take most of
134
+ * a hue's colourfulness while keeping close to the lightness it started at,
135
+ * which a single `t` cannot say — raising `t` for chroma drags lightness with
136
+ * it. Every weight is required: an axis left out would need a default, and
137
+ * "unchanged" (0) and "same as the others" are both plausible readings.
138
+ *
139
+ * A weak `h` beside a strong `c` shows the starting colour's hue at high
140
+ * chroma. The powerless rule covers only a truly achromatic start, so a
141
+ * near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
142
+ * noise in proportion. To land on the target's hue, pass `h: 1`.
143
+ */
144
+ mixAxes(toward: Oklch, weights: OklchWeights): Oklch;
145
+ /**
146
+ * ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
147
+ * (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
148
+ * resolves. Alpha is not a coordinate of the space and does not count.
149
+ * Symmetric and pure.
150
+ */
151
+ deltaE(other: Oklch): number;
152
+ /** Linear-sRGB coordinates for an explicit (l, C, h). Pure; `toRgba` passes
153
+ * already-normalized values so this never sees out-of-range inputs. */
154
+ private toLinearRgb;
155
+ /**
156
+ * Largest chroma ≤ `c` whose (l, h) projection lies in sRGB.
157
+ * Standard chroma-reduction-by-bisection: 24 iterations gives ~1e-7
158
+ * precision, more than enough since the next step quantizes to 1/255.
159
+ */
160
+ private findInGamutChroma;
161
+ }
@@ -0,0 +1,312 @@
1
+ /**
2
+ * OKLCH color space — perceptually-uniform polar form of OKLab.
3
+ *
4
+ * Math from Björn Ottosson's OKLab specification:
5
+ * https://bottosson.github.io/posts/oklab/
6
+ *
7
+ * The full pipeline:
8
+ * sRGB (0..255 int) ↔ sRGB linear (0..1 float) ↔ OKLab ↔ OKLCH polar
9
+ * `fromRgba` walks left→right, `toRgba` walks right→left. The only lossy
10
+ * step is the final 0..255 quantization; the float math itself is
11
+ * reversible to within a few ULP.
12
+ *
13
+ * [LAW:one-type-per-behavior] OKLCH and sRGB are different *behaviors*:
14
+ * sRGB is the wire format terminals consume (what ANSI escape codes
15
+ * carry); OKLCH is the perceptual manipulation space (where equal
16
+ * numerical deltas mean equal perceptual deltas, which is the property
17
+ * "transposition" requires). Two types, mutual conversion, no hybrid.
18
+ *
19
+ * [LAW:one-way-deps] This module imports `ColorRgba` from core/color and
20
+ * nothing else from inside the project. Nothing in core/ depends back on
21
+ * this file.
22
+ */
23
+ var _a;
24
+ import { ColorRgba } from "./color.js";
25
+ // The axes `mixAxes` checks, named once so a weights object's own keys — a
26
+ // missing one, or an extra field riding along — never decide what is checked.
27
+ const OKLCH_AXES = ["l", "c", "h", "alpha"];
28
+ export const IDENTITY = Object.freeze({
29
+ hueShift: 0,
30
+ chromaScale: 1,
31
+ lightnessScale: 1,
32
+ lightnessShift: 0,
33
+ });
34
+ /**
35
+ * Flip lightness around the midpoint (`L' = 1 - L`) with hue and chroma
36
+ * untouched. Combined with the anchor logic in `transposePalette`, this
37
+ * converts a dark theme to its light "octave": semantic anchors
38
+ * (error/success/warning) still lightness-invert — only their *hue* is
39
+ * preserved — so errors stay red and dark-on-light becomes light-on-dark.
40
+ */
41
+ export const INVERT_LIGHTNESS = Object.freeze({
42
+ hueShift: 0,
43
+ chromaScale: 1,
44
+ lightnessScale: -1,
45
+ lightnessShift: 1,
46
+ });
47
+ /**
48
+ * Whether a key is the no-op transform. Exported so that callers
49
+ * (`transposePalette`) can fast-path byte-exact identity without going
50
+ * through the lossy sRGB↔OKLCH round-trip. [LAW:one-source-of-truth] —
51
+ * the predicate lives once, here.
52
+ */
53
+ export function isIdentityKey(k) {
54
+ return (k.hueShift % 360 === 0 && // a whole turn (0, 360, -360, …) is a no-op
55
+ k.chromaScale === 1 &&
56
+ k.lightnessScale === 1 &&
57
+ k.lightnessShift === 0);
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Pure conversion helpers (private).
61
+ // ---------------------------------------------------------------------------
62
+ function srgbToLinear(v) {
63
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
64
+ }
65
+ function linearToSrgb(v) {
66
+ return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
67
+ }
68
+ function clamp01(v) {
69
+ return v < 0 ? 0 : v > 1 ? 1 : v;
70
+ }
71
+ // Small tolerance so float-fuzz at the gamut edge doesn't trigger needless
72
+ // chroma reduction on colors that are already representable.
73
+ const GAMUT_EPS = 1e-4;
74
+ // [LAW:one-source-of-truth] Below this chroma a color is achromatic and its
75
+ // hue is pinned to 0 so round-trips stay stable — the one threshold every
76
+ // chroma reader here shares.
77
+ const ACHROMATIC_EPS = 1e-7;
78
+ // The one spelling of that pin: every producer of an `Oklch` (`fromRgba`,
79
+ // `applyKey`, `mix`) passes its result's chroma and hue through here, so a
80
+ // gray from any of them carries the same hue. The constructor does not —
81
+ // it passes intermediates through by contract. [LAW:single-enforcer]
82
+ const hueOf = (c, h) => (c < ACHROMATIC_EPS ? 0 : h);
83
+ // Endpoint-exact: `t = 0` returns `a` and `t = 1` returns `b` bit-for-bit,
84
+ // which `a + (b - a) * t` does not (the subtraction rounds). One shape for
85
+ // every axis `mix` interpolates. [LAW:one-source-of-truth]
86
+ const lerp = (a, b, t) => a * (1 - t) + b * t;
87
+ // Hue into [0, 360). Exact on a hue already there (fmod is exact), and exact
88
+ // on `x - 360` for a representable x (the sum is x itself), which is what
89
+ // `mix` relies on. One spelling for every producer. [LAW:one-source-of-truth]
90
+ const wrapHue = (h) => {
91
+ const w = h % 360;
92
+ return w < 0 ? w + 360 : w;
93
+ };
94
+ function inGamut(r, g, b) {
95
+ return (r >= -GAMUT_EPS && r <= 1 + GAMUT_EPS &&
96
+ g >= -GAMUT_EPS && g <= 1 + GAMUT_EPS &&
97
+ b >= -GAMUT_EPS && b <= 1 + GAMUT_EPS);
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Oklch — the value type.
101
+ // ---------------------------------------------------------------------------
102
+ /**
103
+ * Immutable OKLCH color. `l` ∈ [0, 1] (lightness), `c` ≥ 0 (chroma; typical
104
+ * sRGB-representable colors top out near 0.32), `h` ∈ [0, 360) (hue
105
+ * degrees), `alpha` ∈ [0, 1].
106
+ *
107
+ * [LAW:single-enforcer] Constructor is the only validation site, matching
108
+ * the `ColorRgba` pattern. Out-of-range `alpha` throws. l/c/h must be
109
+ * finite but are *not* clamped at construction — they're clamped during
110
+ * `toRgba` instead, so transforms can pass through intermediate
111
+ * representations (e.g. h = 720°, c = -0.1 mid-pipeline) before the final
112
+ * sRGB-quantization step normalizes them.
113
+ */
114
+ export class Oklch {
115
+ l;
116
+ c;
117
+ h;
118
+ alpha;
119
+ constructor(l, c, h, alpha = 1) {
120
+ this.l = l;
121
+ this.c = c;
122
+ this.h = h;
123
+ this.alpha = alpha;
124
+ if (!Number.isFinite(l))
125
+ throw new RangeError(`Oklch.l must be finite; got ${l}`);
126
+ if (!Number.isFinite(c))
127
+ throw new RangeError(`Oklch.c must be finite; got ${c}`);
128
+ if (!Number.isFinite(h))
129
+ throw new RangeError(`Oklch.h must be finite; got ${h}`);
130
+ if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) {
131
+ throw new RangeError(`Oklch.alpha must be a finite number in [0,1]; got ${alpha}`);
132
+ }
133
+ }
134
+ /** sRGB → linear → OKLab → polar. Pure; no I/O, no allocation beyond the result. */
135
+ static fromRgba(color) {
136
+ const [r, g, b, a] = color.normalized;
137
+ const lr = srgbToLinear(r);
138
+ const lg = srgbToLinear(g);
139
+ const lb = srgbToLinear(b);
140
+ const lLong = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
141
+ const mLong = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
142
+ const sLong = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
143
+ const lCube = Math.cbrt(lLong);
144
+ const mCube = Math.cbrt(mLong);
145
+ const sCube = Math.cbrt(sLong);
146
+ const L = 0.2104542553 * lCube + 0.7936177850 * mCube - 0.0040720468 * sCube;
147
+ const aLab = 1.9779984951 * lCube - 2.4285922050 * mCube + 0.4505937099 * sCube;
148
+ const bLab = 0.0259040371 * lCube + 0.7827717662 * mCube - 0.8086757660 * sCube;
149
+ const C = Math.sqrt(aLab * aLab + bLab * bLab);
150
+ const H = wrapHue(Math.atan2(bLab, aLab) * (180 / Math.PI));
151
+ return new _a(L, C, hueOf(C, H), a);
152
+ }
153
+ /**
154
+ * Polar → OKLab → linear → sRGB (with chroma-bisection gamut clamping).
155
+ *
156
+ * [LAW:single-enforcer] All sRGB-gamut clamping for OKLCH happens here.
157
+ * Callers don't see intermediate float channels; they get a valid
158
+ * `ColorRgba` or a thrown error.
159
+ */
160
+ toRgba() {
161
+ // The normalization boundary the constructor promises: clamp L into [0,1]
162
+ // and C to non-negative here, so out-of-range intermediates (e.g. c = -0.1
163
+ // from a mid-pipeline transform) canonicalize to a valid color rather than
164
+ // flipping hue through a negative chroma. H needs no wrap — the cos/sin in
165
+ // the conversion are periodic. [LAW:single-enforcer]
166
+ const l = clamp01(this.l);
167
+ const c = Math.max(0, this.c);
168
+ const cInGamut = this.findInGamutChroma(l, c, this.h);
169
+ const { r, g, b } = this.toLinearRgb(l, cInGamut, this.h);
170
+ const sR = Math.round(clamp01(linearToSrgb(r)) * 255);
171
+ const sG = Math.round(clamp01(linearToSrgb(g)) * 255);
172
+ const sB = Math.round(clamp01(linearToSrgb(b)) * 255);
173
+ return new ColorRgba(sR, sG, sB, this.alpha);
174
+ }
175
+ /**
176
+ * Apply a `ThemeKey`. Returns a new `Oklch`; pure.
177
+ *
178
+ * [LAW:dataflow-not-control-flow] Identity short-circuits to `this` —
179
+ * same shape as `ColorRgba.compositeOver(bg)` returning `this` when
180
+ * `alpha === 1`. Callers invoke unconditionally; the data decides.
181
+ */
182
+ applyKey(k) {
183
+ if (isIdentityKey(k))
184
+ return this;
185
+ const newL = clamp01(this.l * k.lightnessScale + k.lightnessShift);
186
+ const newC = Math.max(0, this.c * k.chromaScale);
187
+ const newH = wrapHue(this.h + k.hueShift);
188
+ return new _a(newL, newC, hueOf(newC, newH), this.alpha);
189
+ }
190
+ /**
191
+ * The color `t` of the way from this one toward `toward`, in OKLCH.
192
+ * `t = 0` is this color, `t = 1` is `toward`; pure.
193
+ *
194
+ * Lightness, chroma and alpha interpolate linearly. Hue takes the shorter
195
+ * arc around the wheel, so blue → red passes through magenta rather than
196
+ * sweeping across green. An achromatic endpoint has no hue of its own —
197
+ * `fromRgba` pins it to 0, which is red — so it adopts the other
198
+ * endpoint's hue (CSS Color 4's "powerless" rule): gray → red stays a red
199
+ * that gains chroma, instead of rotating through the wheel from 0°.
200
+ *
201
+ * `t` must lie in [0, 1]: this is interpolation, not extrapolation, and a
202
+ * `t` outside it (NaN included) is a RangeError, never a colour off the
203
+ * far end of the segment. [LAW:no-silent-failure]
204
+ *
205
+ * [LAW:one-source-of-truth] Achromatic means the same `ACHROMATIC_EPS`
206
+ * that `fromRgba` and `applyKey` pin hue by, so a gray produced by either
207
+ * is a gray here — and the result's hue goes through the same `hueOf`, so
208
+ * a gray produced HERE (t = 0 from a gray, t = 1 toward one) is a gray
209
+ * there too.
210
+ */
211
+ mix(toward, t) {
212
+ if (!(t >= 0 && t <= 1)) {
213
+ throw new RangeError(`Oklch.mix: t must be in [0, 1]; got ${t}`);
214
+ }
215
+ return this.#interpolate(toward, t, t, t, t);
216
+ }
217
+ /**
218
+ * `mix` with each axis moved its own share of the way: `weights.l` of the
219
+ * lightness gap, `weights.c` of the chroma gap, and so on, each in [0, 1].
220
+ * `mix(toward, t)` is `mixAxes` with every weight `t` — one interpolation,
221
+ * so the shorter-arc hue and the powerless-endpoint rule are the same
222
+ * whichever is called.
223
+ *
224
+ * It exists because perceptual axes are independent: a tint can take most of
225
+ * a hue's colourfulness while keeping close to the lightness it started at,
226
+ * which a single `t` cannot say — raising `t` for chroma drags lightness with
227
+ * it. Every weight is required: an axis left out would need a default, and
228
+ * "unchanged" (0) and "same as the others" are both plausible readings.
229
+ *
230
+ * A weak `h` beside a strong `c` shows the starting colour's hue at high
231
+ * chroma. The powerless rule covers only a truly achromatic start, so a
232
+ * near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
233
+ * noise in proportion. To land on the target's hue, pass `h: 1`.
234
+ */
235
+ mixAxes(toward, weights) {
236
+ for (const axis of OKLCH_AXES) {
237
+ const w = weights[axis];
238
+ if (!(w >= 0 && w <= 1)) {
239
+ throw new RangeError(`Oklch.mixAxes: ${axis} weight must be in [0, 1]; got ${w}`);
240
+ }
241
+ }
242
+ return this.#interpolate(toward, weights.l, weights.c, weights.h, weights.alpha);
243
+ }
244
+ // The one interpolation both entry points share; each has already checked
245
+ // its own weights, so this is the arithmetic alone.
246
+ #interpolate(toward, wl, wc, wh, walpha) {
247
+ const thisHasHue = this.c >= ACHROMATIC_EPS;
248
+ const towardHasHue = toward.c >= ACHROMATIC_EPS;
249
+ const fromH = wrapHue(thisHasHue ? this.h : towardHasHue ? toward.h : 0);
250
+ const toH = wrapHue(towardHasHue ? toward.h : fromH);
251
+ // Shorter arc: endpoints more than half a turn apart lerp across 0°, the
252
+ // larger one taken a turn down. `x - 360` for x ≥ 180 is exact
253
+ // (Sterbenz), so the endpoints survive the shift and the wrap bit-for-bit
254
+ // — an addition of 360 would not.
255
+ const arc = toH - fromH;
256
+ const fromU = arc < -180 ? fromH - 360 : fromH;
257
+ const toU = arc > 180 ? toH - 360 : toH;
258
+ const c = lerp(this.c, toward.c, wc);
259
+ return new _a(lerp(this.l, toward.l, wl), c, hueOf(c, wrapHue(lerp(fromU, toU, wh))), lerp(this.alpha, toward.alpha, walpha));
260
+ }
261
+ /**
262
+ * ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
263
+ * (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
264
+ * resolves. Alpha is not a coordinate of the space and does not count.
265
+ * Symmetric and pure.
266
+ */
267
+ deltaE(other) {
268
+ const a = (this.h * Math.PI) / 180;
269
+ const b = (other.h * Math.PI) / 180;
270
+ return Math.hypot(this.l - other.l, this.c * Math.cos(a) - other.c * Math.cos(b), this.c * Math.sin(a) - other.c * Math.sin(b));
271
+ }
272
+ /** Linear-sRGB coordinates for an explicit (l, C, h). Pure; `toRgba` passes
273
+ * already-normalized values so this never sees out-of-range inputs. */
274
+ toLinearRgb(l, C, h) {
275
+ const hRad = h * (Math.PI / 180);
276
+ const aLab = C * Math.cos(hRad);
277
+ const bLab = C * Math.sin(hRad);
278
+ const lCube = l + 0.3963377774 * aLab + 0.2158037573 * bLab;
279
+ const mCube = l - 0.1055613458 * aLab - 0.0638541728 * bLab;
280
+ const sCube = l - 0.0894841775 * aLab - 1.2914855480 * bLab;
281
+ const lLong = lCube * lCube * lCube;
282
+ const mLong = mCube * mCube * mCube;
283
+ const sLong = sCube * sCube * sCube;
284
+ return {
285
+ r: +4.0767416621 * lLong - 3.3077115913 * mLong + 0.2309699292 * sLong,
286
+ g: -1.2684380046 * lLong + 2.6097574011 * mLong - 0.3413193965 * sLong,
287
+ b: -0.0041960863 * lLong - 0.7034186147 * mLong + 1.7076147010 * sLong,
288
+ };
289
+ }
290
+ /**
291
+ * Largest chroma ≤ `c` whose (l, h) projection lies in sRGB.
292
+ * Standard chroma-reduction-by-bisection: 24 iterations gives ~1e-7
293
+ * precision, more than enough since the next step quantizes to 1/255.
294
+ */
295
+ findInGamutChroma(l, c, h) {
296
+ const direct = this.toLinearRgb(l, c, h);
297
+ if (inGamut(direct.r, direct.g, direct.b))
298
+ return c;
299
+ let lo = 0;
300
+ let hi = c;
301
+ for (let i = 0; i < 24; i++) {
302
+ const mid = (lo + hi) / 2;
303
+ const probe = this.toLinearRgb(l, mid, h);
304
+ if (inGamut(probe.r, probe.g, probe.b))
305
+ lo = mid;
306
+ else
307
+ hi = mid;
308
+ }
309
+ return lo;
310
+ }
311
+ }
312
+ _a = Oklch;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The OSC 8 hyperlink wire grammar — which bytes may not appear inside a
3
+ * link, how a link becomes bytes, and how bytes are read back as a link.
4
+ *
5
+ * [LAW:one-source-of-truth] The producer (`segmentsToString`, the one encoder
6
+ * every link reaches the wire through) opens a link with `osc8Open` and closes
7
+ * it with `OSC8_CLOSE`; every consumer that reads rendered bytes (a width measure, a
8
+ * test extracting URLs) matches them with `OSC8`; the data-model boundary
9
+ * (RichText) cleans URLs with `stripOscTerminators`. All four read the one
10
+ * terminator set below, so the bytes a sanitizer removes and the bytes a
11
+ * reader stops at cannot disagree.
12
+ *
13
+ * [LAW:locality-or-seam] This module depends on nothing; every consumer
14
+ * imports downward.
15
+ *
16
+ * The `id=` parameter is what makes one link hover as one link. A terminal
17
+ * treats cells as the same hyperlink when they share BOTH the URI and the id
18
+ * (the OSC 8 spec, and VTE / iTerm2 / kitty / WezTerm alike); without an id,
19
+ * each open sequence is its own link. The coalescer can only share one OSC 8
20
+ * pair across a run of identical SGR, so a link whose text changes style
21
+ * mid-span — a bold glyph beside plain text, a padded cell — is emitted as
22
+ * several pairs, and would highlight piecewise on hover.
23
+ *
24
+ * [LAW:types-are-the-program] The id is a pure function of the URI, so the
25
+ * byte stream stays a pure function of (style, text, destination): no counter,
26
+ * no construction-order dependence. The consequence is deliberate: two spans
27
+ * with the same URI hover as one link wherever they sit on screen, adjacent
28
+ * or not — a click on either does the same thing, so they ARE one link.
29
+ * Because terminals key on the (id, URI) PAIR, a hash collision between two
30
+ * different URIs merges nothing — the URIs still differ — so a 32-bit hash is
31
+ * exact, not approximate.
32
+ */
33
+ /** Remove every byte that could break a URL out of its OSC 8 wrap. */
34
+ export declare function stripOscTerminators(url: string): string;
35
+ /**
36
+ * The bytes that open a hyperlink to `url`.
37
+ *
38
+ * [LAW:single-enforcer] Wire-byte trust boundary — the URL is sanitized at
39
+ * the one place it becomes an OSC 8 sequence, whichever upstream API attached
40
+ * it to the Style (RichText sanitizes at its data-model boundary too; a Style
41
+ * built directly with `new Style({ link })` reaches here unsanitized).
42
+ */
43
+ export declare function osc8Open(url: string): string;
44
+ /** The bytes that close the current hyperlink. */
45
+ export declare const OSC8_CLOSE = "\u001B]8;;\u001B\\";
46
+ /**
47
+ * Matches one OSC 8 sequence — an open or a close — terminated by `ESC \`,
48
+ * BEL, or the 8-bit ST. Group 1 is the params, group 2 the URI; both are empty
49
+ * on a close. To read bytes back use `osc8Sequences`; this pattern is exported
50
+ * for composing into a larger one (a width measure's zero-width alternation).
51
+ */
52
+ export declare const OSC8: RegExp;
53
+ /** One OSC 8 sequence found in rendered bytes. A close has `uri === ""`. */
54
+ export interface Osc8Sequence {
55
+ /** Offset of the sequence in the scanned string. */
56
+ readonly index: number;
57
+ /** Length of the whole sequence, terminator included. */
58
+ readonly length: number;
59
+ /** The params field (`id=…`); empty on a close. */
60
+ readonly params: string;
61
+ /** The URI; empty on a close. */
62
+ readonly uri: string;
63
+ }
64
+ /**
65
+ * Every OSC 8 sequence in `text`, in order — the typed way to read rendered
66
+ * bytes back.
67
+ */
68
+ export declare function osc8Sequences(text: string): Osc8Sequence[];