@promptctl/rich-js 0.16.0 → 0.18.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 (206) hide show
  1. package/README.md +14 -27
  2. package/package.json +7 -3
  3. package/dist/core/box.d.ts +0 -144
  4. package/dist/core/box.js +0 -391
  5. package/dist/core/cells.d.ts +0 -148
  6. package/dist/core/cells.js +0 -266
  7. package/dist/core/color.d.ts +0 -229
  8. package/dist/core/color.js +0 -992
  9. package/dist/core/console.d.ts +0 -142
  10. package/dist/core/console.js +0 -477
  11. package/dist/core/emoji.d.ts +0 -24
  12. package/dist/core/emoji.js +0 -391
  13. package/dist/core/export-html.d.ts +0 -24
  14. package/dist/core/export-html.js +0 -106
  15. package/dist/core/export-lines.d.ts +0 -121
  16. package/dist/core/export-lines.js +0 -141
  17. package/dist/core/highlighter.d.ts +0 -33
  18. package/dist/core/highlighter.js +0 -124
  19. package/dist/core/json.d.ts +0 -27
  20. package/dist/core/json.js +0 -53
  21. package/dist/core/markup.d.ts +0 -97
  22. package/dist/core/markup.js +0 -619
  23. package/dist/core/measure.d.ts +0 -27
  24. package/dist/core/measure.js +0 -61
  25. package/dist/core/oklch.d.ts +0 -161
  26. package/dist/core/oklch.js +0 -312
  27. package/dist/core/osc8.d.ts +0 -68
  28. package/dist/core/osc8.js +0 -90
  29. package/dist/core/pretty.d.ts +0 -141
  30. package/dist/core/pretty.js +0 -472
  31. package/dist/core/protocol.d.ts +0 -137
  32. package/dist/core/protocol.js +0 -101
  33. package/dist/core/render.d.ts +0 -69
  34. package/dist/core/render.js +0 -140
  35. package/dist/core/segment.d.ts +0 -142
  36. package/dist/core/segment.js +0 -433
  37. package/dist/core/spinnerData.d.ts +0 -10
  38. package/dist/core/spinnerData.js +0 -115
  39. package/dist/core/strip.d.ts +0 -139
  40. package/dist/core/strip.js +0 -302
  41. package/dist/core/style.d.ts +0 -149
  42. package/dist/core/style.js +0 -741
  43. package/dist/core/subscription.d.ts +0 -12
  44. package/dist/core/subscription.js +0 -12
  45. package/dist/core/text.d.ts +0 -255
  46. package/dist/core/text.js +0 -990
  47. package/dist/core/wrap.d.ts +0 -41
  48. package/dist/core/wrap.js +0 -127
  49. package/dist/host/host-stream.d.ts +0 -30
  50. package/dist/host/host-stream.js +0 -35
  51. package/dist/host/index.d.ts +0 -3
  52. package/dist/host/index.js +0 -2
  53. package/dist/host/terminal-host.d.ts +0 -143
  54. package/dist/host/terminal-host.js +0 -132
  55. package/dist/index.d.ts +0 -87
  56. package/dist/index.js +0 -104
  57. package/dist/node/prompt.d.ts +0 -21
  58. package/dist/node/prompt.js +0 -30
  59. package/dist/node/save.d.ts +0 -17
  60. package/dist/node/save.js +0 -21
  61. package/dist/node/terminal-host.d.ts +0 -84
  62. package/dist/node/terminal-host.js +0 -160
  63. package/dist/node/traceback.d.ts +0 -27
  64. package/dist/node/traceback.js +0 -112
  65. package/dist/renderables/align.d.ts +0 -16
  66. package/dist/renderables/align.js +0 -46
  67. package/dist/renderables/columns.d.ts +0 -61
  68. package/dist/renderables/columns.js +0 -168
  69. package/dist/renderables/constrain.d.ts +0 -15
  70. package/dist/renderables/constrain.js +0 -37
  71. package/dist/renderables/flexStrip.d.ts +0 -41
  72. package/dist/renderables/flexStrip.js +0 -200
  73. package/dist/renderables/group.d.ts +0 -11
  74. package/dist/renderables/group.js +0 -15
  75. package/dist/renderables/layout.d.ts +0 -92
  76. package/dist/renderables/layout.js +0 -315
  77. package/dist/renderables/live.d.ts +0 -47
  78. package/dist/renderables/live.js +0 -131
  79. package/dist/renderables/markdown.d.ts +0 -24
  80. package/dist/renderables/markdown.js +0 -182
  81. package/dist/renderables/padding.d.ts +0 -38
  82. package/dist/renderables/padding.js +0 -140
  83. package/dist/renderables/panel.d.ts +0 -116
  84. package/dist/renderables/panel.js +0 -322
  85. package/dist/renderables/progress.d.ts +0 -90
  86. package/dist/renderables/progress.js +0 -241
  87. package/dist/renderables/progressBar.d.ts +0 -31
  88. package/dist/renderables/progressBar.js +0 -52
  89. package/dist/renderables/prompt.d.ts +0 -49
  90. package/dist/renderables/prompt.js +0 -125
  91. package/dist/renderables/rule.d.ts +0 -24
  92. package/dist/renderables/rule.js +0 -90
  93. package/dist/renderables/spinner.d.ts +0 -29
  94. package/dist/renderables/spinner.js +0 -64
  95. package/dist/renderables/status.d.ts +0 -23
  96. package/dist/renderables/status.js +0 -60
  97. package/dist/renderables/syntax.d.ts +0 -35
  98. package/dist/renderables/syntax.js +0 -114
  99. package/dist/renderables/table.d.ts +0 -182
  100. package/dist/renderables/table.js +0 -697
  101. package/dist/renderables/traceback.d.ts +0 -31
  102. package/dist/renderables/traceback.js +0 -109
  103. package/dist/renderables/tree.d.ts +0 -54
  104. package/dist/renderables/tree.js +0 -159
  105. package/dist/template-bindings/color-funcs.d.ts +0 -87
  106. package/dist/template-bindings/color-funcs.js +0 -219
  107. package/dist/template-bindings/helpers.d.ts +0 -24
  108. package/dist/template-bindings/helpers.js +0 -48
  109. package/dist/template-bindings/index.d.ts +0 -107
  110. package/dist/template-bindings/index.js +0 -144
  111. package/dist/template-bindings/palette-funcs.d.ts +0 -94
  112. package/dist/template-bindings/palette-funcs.js +0 -151
  113. package/dist/template-bindings/style-funcs.d.ts +0 -28
  114. package/dist/template-bindings/style-funcs.js +0 -151
  115. package/dist/themes/buildPalette.d.ts +0 -36
  116. package/dist/themes/buildPalette.js +0 -56
  117. package/dist/themes/colorMath.d.ts +0 -82
  118. package/dist/themes/colorMath.js +0 -254
  119. package/dist/themes/colorRef.d.ts +0 -51
  120. package/dist/themes/colorRef.js +0 -96
  121. package/dist/themes/data/atom-one-dark.d.ts +0 -3
  122. package/dist/themes/data/atom-one-dark.js +0 -158
  123. package/dist/themes/data/atom-one-light.d.ts +0 -3
  124. package/dist/themes/data/atom-one-light.js +0 -158
  125. package/dist/themes/data/catppuccin-frappe.d.ts +0 -3
  126. package/dist/themes/data/catppuccin-frappe.js +0 -158
  127. package/dist/themes/data/catppuccin-latte.d.ts +0 -3
  128. package/dist/themes/data/catppuccin-latte.js +0 -158
  129. package/dist/themes/data/catppuccin-macchiato.d.ts +0 -3
  130. package/dist/themes/data/catppuccin-macchiato.js +0 -158
  131. package/dist/themes/data/catppuccin-mocha.d.ts +0 -3
  132. package/dist/themes/data/catppuccin-mocha.js +0 -158
  133. package/dist/themes/data/cyberpunk.d.ts +0 -17
  134. package/dist/themes/data/cyberpunk.js +0 -191
  135. package/dist/themes/data/default.d.ts +0 -3
  136. package/dist/themes/data/default.js +0 -158
  137. package/dist/themes/data/dracula.d.ts +0 -3
  138. package/dist/themes/data/dracula.js +0 -158
  139. package/dist/themes/data/flexoki.d.ts +0 -3
  140. package/dist/themes/data/flexoki.js +0 -158
  141. package/dist/themes/data/gruvbox.d.ts +0 -3
  142. package/dist/themes/data/gruvbox.js +0 -158
  143. package/dist/themes/data/index.d.ts +0 -35
  144. package/dist/themes/data/index.js +0 -64
  145. package/dist/themes/data/monokai.d.ts +0 -3
  146. package/dist/themes/data/monokai.js +0 -158
  147. package/dist/themes/data/nord.d.ts +0 -3
  148. package/dist/themes/data/nord.js +0 -158
  149. package/dist/themes/data/rose-pine-dawn.d.ts +0 -3
  150. package/dist/themes/data/rose-pine-dawn.js +0 -158
  151. package/dist/themes/data/rose-pine-moon.d.ts +0 -3
  152. package/dist/themes/data/rose-pine-moon.js +0 -158
  153. package/dist/themes/data/rose-pine.d.ts +0 -3
  154. package/dist/themes/data/rose-pine.js +0 -158
  155. package/dist/themes/data/solarized-dark.d.ts +0 -3
  156. package/dist/themes/data/solarized-dark.js +0 -158
  157. package/dist/themes/data/solarized-light.d.ts +0 -3
  158. package/dist/themes/data/solarized-light.js +0 -158
  159. package/dist/themes/data/svg-export.d.ts +0 -3
  160. package/dist/themes/data/svg-export.js +0 -158
  161. package/dist/themes/data/textual-ansi.d.ts +0 -3
  162. package/dist/themes/data/textual-ansi.js +0 -158
  163. package/dist/themes/data/textual-dark.d.ts +0 -3
  164. package/dist/themes/data/textual-dark.js +0 -158
  165. package/dist/themes/data/textual-light.d.ts +0 -3
  166. package/dist/themes/data/textual-light.js +0 -158
  167. package/dist/themes/data/tokyo-night.d.ts +0 -3
  168. package/dist/themes/data/tokyo-night.js +0 -158
  169. package/dist/themes/data/types.d.ts +0 -15
  170. package/dist/themes/data/types.js +0 -1
  171. package/dist/themes/palette.d.ts +0 -21
  172. package/dist/themes/palette.js +0 -28
  173. package/dist/themes/ramp.d.ts +0 -83
  174. package/dist/themes/ramp.js +0 -122
  175. package/dist/themes/registry.d.ts +0 -43
  176. package/dist/themes/registry.js +0 -82
  177. package/dist/themes/terminalThemes.d.ts +0 -54
  178. package/dist/themes/terminalThemes.js +0 -68
  179. package/dist/themes/transpose.d.ts +0 -87
  180. package/dist/themes/transpose.js +0 -138
  181. package/dist/widgets/button.d.ts +0 -49
  182. package/dist/widgets/button.js +0 -206
  183. package/dist/widgets/checkbox.d.ts +0 -41
  184. package/dist/widgets/checkbox.js +0 -155
  185. package/dist/widgets/dropdown.d.ts +0 -67
  186. package/dist/widgets/dropdown.js +0 -453
  187. package/dist/widgets/event-router.d.ts +0 -78
  188. package/dist/widgets/event-router.js +0 -622
  189. package/dist/widgets/focus-manager.d.ts +0 -26
  190. package/dist/widgets/focus-manager.js +0 -193
  191. package/dist/widgets/index.d.ts +0 -22
  192. package/dist/widgets/index.js +0 -15
  193. package/dist/widgets/screen.d.ts +0 -98
  194. package/dist/widgets/screen.js +0 -542
  195. package/dist/widgets/slider.d.ts +0 -57
  196. package/dist/widgets/slider.js +0 -270
  197. package/dist/widgets/static-item.d.ts +0 -40
  198. package/dist/widgets/static-item.js +0 -60
  199. package/dist/widgets/text-input.d.ts +0 -274
  200. package/dist/widgets/text-input.js +0 -1162
  201. package/dist/widgets/toggle.d.ts +0 -44
  202. package/dist/widgets/toggle.js +0 -179
  203. package/dist/widgets/types.d.ts +0 -108
  204. package/dist/widgets/types.js +0 -31
  205. package/dist/widgets/widget-base.d.ts +0 -42
  206. package/dist/widgets/widget-base.js +0 -173
@@ -1,151 +0,0 @@
1
- /**
2
- * Style-function registrations for the rich-js template binding.
3
- *
4
- * [LAW:one-source-of-truth] The function inventory below mirrors the
5
- * string-syntax style vocabulary documented in `docs/style.md` — the two
6
- * colour slots (`fg`/`bg`), text attributes (positive + negated), short
7
- * aliases, and the hyperlink. Each registration is a templating-time analogue
8
- * of a piece of `Style.parse`, so a template fragment composed by these
9
- * functions round-trips through `Style` without semantic drift.
10
- *
11
- * [LAW:dataflow-not-control-flow] Every function follows the same
12
- * shape: child `RichText` in, `RichText` out. The styling difference
13
- * lives in a captured `Style` value (or constructor closure), not in
14
- * branches inside a single dispatch. That is why this file is mostly
15
- * factories — the data (the colour, the attribute name) is the
16
- * variability; the operation is fixed.
17
- */
18
- import { Style, ATTRIBUTE_NAMES, ATTRIBUTE_SHORT_ALIASES, } from "../core/style.js";
19
- import { ColorSpec } from "../core/color.js";
20
- import { applyStyleToFragment } from "./helpers.js";
21
- function attrFunc(style) {
22
- return {
23
- fn: ((child) => applyStyleToFragment(child, style)),
24
- argTypes: ["liftable"],
25
- returnType: "T",
26
- };
27
- }
28
- // --- Colour sinks ---
29
- //
30
- // `fg` and `bg` are the only two colour-applying functions, and they are the
31
- // terminal step of the colour pipeline: `color` names a colour, the functions
32
- // in `color-funcs.ts` transform it, these paint it onto text.
33
- //
34
- // [LAW:composability] They replace five separate families — one function per
35
- // ANSI colour name (`red`, `bright_blue`, …), `hex`, `rgb`, `color` (256-index),
36
- // and `on` (background). Every one of those encoded *which colour* in the
37
- // function's identity, so the vocabulary could only grow by adding names, and
38
- // a colour computed at render time could not be applied at all. Here the colour
39
- // is an argument, so the sink admits every colour that exists and every colour
40
- // that will ever exist. Two functions, unbounded reach.
41
- //
42
- // [LAW:types-are-the-program] The slot accepts the full `ColorSpec.parse`
43
- // vocabulary, which is wider than the hex the colour math produces — and that
44
- // width is deliberate, not laxity. `#7aa2f7` is a *concrete* colour; `"red"`
45
- // and `"color(42)"` are *symbolic* ones the terminal resolves against its own
46
- // theme. Only concrete colours can be darkened or blended, which is why the
47
- // colour-math functions take hex alone; but both kinds can be painted, so the
48
- // sink takes the union. The type of each slot is exactly the set of values that
49
- // slot can mean something for.
50
- function colorSinkFunc(slot) {
51
- return {
52
- fn: ((spec, child) => applyStyleToFragment(child, new Style({ [slot]: ColorSpec.parse(spec) }))),
53
- argTypes: ["string", "liftable"],
54
- returnType: "T",
55
- };
56
- }
57
- // --- Text attributes ---
58
- //
59
- // [LAW:one-source-of-truth] The attribute and short-alias inventories
60
- // come from `core/style.ts`. This file derives the template registration
61
- // set from the same data `Style.parse` consults — adding an attribute or
62
- // alias in one place propagates here automatically.
63
- function attrStyle(name, value) {
64
- return new Style({ [name]: value });
65
- }
66
- function attributeFuncs() {
67
- const out = {};
68
- for (const name of ATTRIBUTE_NAMES) {
69
- out[name] = attrFunc(attrStyle(name, true));
70
- out[`not_${name}`] = attrFunc(attrStyle(name, false));
71
- }
72
- for (const [alias, canonical] of Object.entries(ATTRIBUTE_SHORT_ALIASES)) {
73
- out[alias] = attrFunc(attrStyle(canonical, true));
74
- }
75
- return out;
76
- }
77
- // --- Style spec (multi-attribute one-shot) ---
78
- //
79
- // [LAW:one-source-of-truth] `style` accepts the same space-separated grammar
80
- // `Style.parse` consults — i.e. the inside of `[...]` markup. There is no
81
- // second parser: a spec that `Style.parse` accepts produces a fragment
82
- // byte-equivalent to the same spec inside markup, and one that `Style.parse`
83
- // rejects raises the same `StyleSyntaxError` surface.
84
- //
85
- // Motivation: the per-attribute functions (`bold`, `underline`, `fg`, …)
86
- // compose by nesting. For "apply a fixed set of styles to this child" or
87
- // "apply this named style set everywhere", nesting is awkward and the
88
- // style description is fragmented across multiple call sites. `style`
89
- // collapses that to a single call, and because the spec is a string it
90
- // flows through Go-template `$vars` and through scope without further
91
- // machinery:
92
- //
93
- // {{ $alert := "bold underline #ff6b6b" }}
94
- // {{ style $alert "alarm!" }}
95
- // {{ style $alert .otherField }}
96
- //
97
- // [LAW:dataflow-not-control-flow] Same shape as every other style function:
98
- // a `Style` value (here built by `Style.parse(spec)`) plus a child, in,
99
- // styled child out. The variability is the spec string; the operation is
100
- // fixed.
101
- const styleSpecFunc = {
102
- fn: ((spec, child) => {
103
- return applyStyleToFragment(child, Style.parse(spec));
104
- }),
105
- argTypes: ["string", "liftable"],
106
- returnType: "T",
107
- };
108
- // --- Hyperlink ---
109
- //
110
- // `link` is the cell-splitter for the multi-cell consumer contract.
111
- // Implementation-wise it is the same shape as any other style function:
112
- // it sets the `link` slot of `Style` exactly as the existing string-form
113
- // `link URL` does, so a template-built fragment is byte-equivalent to
114
- // `RichText("x", { style: Style.parse("link u") })`.
115
- //
116
- // The cell-boundary signal that consumers (cc-candybar et al.) walk is
117
- // `fragment.style.link` being truthy. `Style.add` propagates `link`
118
- // through any outer wrapping call, so `{{ fg "red" (link "u" "x") }}` and
119
- // `{{ link "u" "x" }}` produce shapes that both qualify as cells from
120
- // the consumer's perspective. Outer-wins on nested links comes for free
121
- // from `Style.add`'s right-wins-on-conflict rule.
122
- //
123
- // [LAW:dataflow-not-control-flow] No special-case AST node, no
124
- // `LinkFragment` subclass, no second emitter path — the variability is
125
- // the value of `style.link`, not whether some control-flow branch ran.
126
- const linkFunc = {
127
- fn: ((url, child) => {
128
- return applyStyleToFragment(child, new Style({ link: url }));
129
- }),
130
- argTypes: ["string", "liftable"],
131
- returnType: "T",
132
- };
133
- // --- Public assembly ---
134
- /**
135
- * Text-styling registrations: the two colour sinks (`fg`, `bg`), text
136
- * attributes (canonical names, short aliases, and `not_*` negations), the
137
- * hyperlink cell-splitter (`link`), and the multi-attribute `style` spec.
138
- *
139
- * Colours themselves come from elsewhere: `colorFuncs()` for the palette-free
140
- * math, `paletteFuncs()` for naming a theme colour. This module only paints.
141
- * [LAW:one-way-deps] — nothing here imports a palette.
142
- */
143
- export function richTextStyleFuncs() {
144
- return {
145
- fg: colorSinkFunc("color"),
146
- bg: colorSinkFunc("bgcolor"),
147
- ...attributeFuncs(),
148
- link: linkFunc,
149
- style: styleSpecFunc,
150
- };
151
- }
@@ -1,36 +0,0 @@
1
- import { ColorRgba } from "../core/color.js";
2
- import { Palette } from "./palette.js";
3
- /**
4
- * Base colors required to build a full semantic palette.
5
- * All are ColorRgba — callers construct from whatever source (hex, HSL, etc.).
6
- */
7
- export interface BaseColors {
8
- primary: ColorRgba;
9
- secondary: ColorRgba;
10
- accent: ColorRgba;
11
- success: ColorRgba;
12
- warning: ColorRgba;
13
- error: ColorRgba;
14
- background: ColorRgba;
15
- foreground: ColorRgba;
16
- }
17
- /**
18
- * Build a full semantic palette from base colors.
19
- *
20
- * Derived entries follow Textual's formulas:
21
- * - `*-muted` = color blended 70% toward its opposite base role. Defined for
22
- * every accent AND for the two base roles themselves
23
- * (`foreground-muted` blends toward `background`,
24
- * `background-muted` blends toward `foreground`) — a caller
25
- * de-emphasizing body/structural text reaches for
26
- * `foreground-muted` the same way it reaches for
27
- * `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
- * - `on-*` = WCAG-correct contrast colour (black or white) for use as
31
- * foreground when the FULL accent is the background. Picked
32
- * by relative luminance — single source of truth so widgets
33
- * never need to invert / reverse fg/bg to get readable text.
34
- * - `surface` = background blended 5% toward foreground
35
- */
36
- export declare function buildPalette(name: string, dark: boolean, base: BaseColors): Palette;
@@ -1,56 +0,0 @@
1
- import { blendRgb } from "../core/color.js";
2
- import { alphaBlend, contrastFor } from "./colorMath.js";
3
- import { Palette } from "./palette.js";
4
- const MUTED_BLEND = 0.7;
5
- const TEXT_ALPHA = 0.66;
6
- const SURFACE_LIFT = 0.05;
7
- const ACCENT_KEYS = ["primary", "secondary", "accent", "success", "warning", "error"];
8
- /**
9
- * Build a full semantic palette from base colors.
10
- *
11
- * Derived entries follow Textual's formulas:
12
- * - `*-muted` = color blended 70% toward its opposite base role. Defined for
13
- * every accent AND for the two base roles themselves
14
- * (`foreground-muted` blends toward `background`,
15
- * `background-muted` blends toward `foreground`) — a caller
16
- * de-emphasizing body/structural text reaches for
17
- * `foreground-muted` the same way it reaches for
18
- * `primary-muted` to de-emphasize an accent.
19
- * - `text-*` = contrast text tinted 66% with the accent color (use as
20
- * foreground in muted/background-tinted contexts)
21
- * - `on-*` = WCAG-correct contrast colour (black or white) for use as
22
- * foreground when the FULL accent is the background. Picked
23
- * by relative luminance — single source of truth so widgets
24
- * never need to invert / reverse fg/bg to get readable text.
25
- * - `surface` = background blended 5% toward foreground
26
- */
27
- export function buildPalette(name, dark, base) {
28
- const vars = new Map();
29
- // Base entries
30
- vars.set("background", base.background);
31
- vars.set("foreground", base.foreground);
32
- for (const key of ACCENT_KEYS) {
33
- vars.set(key, base[key]);
34
- }
35
- // Muted variants of the two base roles, same "blend 70% toward
36
- // background" formula as the accent `*-muted` family below — every
37
- // base color the palette defines gets a de-emphasized variant, not just
38
- // the accents. `foreground-muted` is what a caller reaches for to draw
39
- // structural/contextual text (labels, punctuation, ids) at reduced visual
40
- // weight without a terminal-support-dependent SGR attribute.
41
- vars.set("foreground-muted", blendRgb(base.foreground, base.background, MUTED_BLEND));
42
- vars.set("background-muted", blendRgb(base.background, base.foreground, MUTED_BLEND));
43
- // Surface — subtle lift from background
44
- vars.set("surface", blendRgb(base.background, base.foreground, SURFACE_LIFT));
45
- // Derived: muted, text-, and on- for each accent.
46
- // [LAW:single-enforcer] contrastFor is the only place that decides
47
- // black-vs-white for "text on this accent" — widgets read on-${accent}.
48
- const contrastText = contrastFor(base.background);
49
- for (const key of ACCENT_KEYS) {
50
- const color = base[key];
51
- vars.set(`${key}-muted`, blendRgb(color, base.background, MUTED_BLEND));
52
- vars.set(`text-${key}`, alphaBlend(color, contrastText, TEXT_ALPHA));
53
- vars.set(`on-${key}`, contrastFor(color));
54
- }
55
- return new Palette(name, dark, vars);
56
- }
@@ -1,82 +0,0 @@
1
- import { ColorDepth, ColorRgba, contrastRatio, relativeLuminance } from "../core/color.js";
2
- /**
3
- * Darken a color by N levels, where each level reduces HSL lightness by 10%.
4
- * Negative levels lighten. Level 0 returns an equivalent triplet (after the
5
- * RGB↔HSL roundtrip; values may differ by ±1 due to rounding).
6
- */
7
- export declare function darken(color: ColorRgba, levels: number): ColorRgba;
8
- /**
9
- * Lighten a color by N levels. Equivalent to `darken(color, -levels)`.
10
- */
11
- export declare function lighten(color: ColorRgba, levels: number): ColorRgba;
12
- /**
13
- * Composite `fg` over `bg` with the given alpha (0..1). At alpha=0 returns bg;
14
- * at alpha=1 returns fg.
15
- */
16
- export declare function alphaBlend(fg: ColorRgba, bg: ColorRgba, alpha: number): ColorRgba;
17
- /**
18
- * Pick a contrasting foreground (black or white) for a background, using the
19
- * WCAG relative-luminance threshold of 0.179 (the perceptually correct cutoff
20
- * where black and white are equally readable). A translucent `bg` is judged
21
- * as drawn: composited over `substrate` (see `drawnBackground`).
22
- */
23
- export declare function contrastFor(bg: ColorRgba, substrate?: ColorRgba): ColorRgba;
24
- export { relativeLuminance, contrastRatio };
25
- /**
26
- * Return a foreground guaranteed to clear `minRatio` against `bg`, keeping the
27
- * color *recognizably itself*. If the themed `fg` already passes it is returned
28
- * untouched. Otherwise its OKLCH lightness is slid toward the pole that raises
29
- * contrast — holding hue, and chroma where it stays in gamut (near the poles
30
- * gamut clamping may reduce chroma, but hue is preserved) — until the ratio is
31
- * met, so a blue on a dark-blue background becomes a lighter blue, not white.
32
- * Only when no lightness of that hue can meet the ratio (a mid-toned
33
- * background where even pure black-or-white tops out below the target) does it
34
- * fall back to `contrastFor`'s black/white — the true maximum-contrast pick.
35
- *
36
- * A translucent `bg` is measured as it is drawn — composited over
37
- * `substrate`, the SGR writer's black by default — and a translucent `fg` is
38
- * then flattened over that drawn background, the order the writer composites
39
- * in, so the ratio is measured on what the eye actually sees and the returned
40
- * color is opaque.
41
- *
42
- * `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
43
- * the terminal rounds text and background independently, and two roundings
44
- * can meet in the middle, so the ratio is measured on the drawn pair: a
45
- * colour that loses the floor there is replaced by the nearest cube/grey entry
46
- * that clears it (whose own rounding is itself). At ANSI the terminal draws
47
- * its own theme's colours, so the ratio is measured on the table's nominal
48
- * ones (`DRAWN_FROM`): they say which side of the ground text belongs on, and
49
- * text on its background's index measures 1:1 and is always replaced.
50
- * Truecolor draws the colour chosen.
51
- *
52
- * [LAW:single-enforcer] The one place "is this text readable, and if not fix
53
- * it" is decided. Callers route every fg/bg pair through here and the
54
- * unreadable state never reaches output. [LAW:dataflow-not-control-flow] the
55
- * function always runs; the measured ratio (data) decides how far the
56
- * lightness moves — there is no caller-side "should I check contrast" branch.
57
- */
58
- export declare function ensureContrast(fg: ColorRgba, bg: ColorRgba, minRatio?: number, // WCAG AA for normal text
59
- drawnAt?: ColorDepth, substrate?: ColorRgba): ColorRgba;
60
- /**
61
- * `chosen` (composited opaque over `substrate`), or — when the depth the
62
- * terminal draws at rounds it to a colour `accept` refuses — the nearest colour that depth draws as itself which
63
- * `accept` takes. `accept` sees the candidate as drawn, and `drawn`, the same
64
- * rounding for any other colour it measures against, so the caller states a
65
- * floor once and it holds on the colours the terminal shows — at ANSI, on
66
- * the table's nominal colours, as `ensureContrast` measures there. Truecolor
67
- * draws from no table, so a colour refused there has no replacement;
68
- * `undefined` means nothing the depth draws is accepted.
69
- *
70
- * `ensureContrast` is this with a contrast ratio as the floor; this is for a
71
- * floor that is not text on its background (an open state standing off every
72
- * closed cell, two planes standing off each other).
73
- */
74
- export declare function ensureDrawn(chosen: ColorRgba, drawnAt: ColorDepth, accept: (candidate: ColorRgba, drawn: (c: ColorRgba) => ColorRgba) => boolean, substrate?: ColorRgba): ColorRgba | undefined;
75
- /**
76
- * The colour a ground is shown as at `drawnAt`: composited over `substrate`
77
- * (the SGR writer's black by default), then rounded to the table that depth
78
- * draws from — at ANSI, that table's nominal colour stands in for the
79
- * theme's. The one account `ensureDrawn` and `ensureContrast` measure by, for
80
- * a caller that measures a floor of its own.
81
- */
82
- export declare function drawnColour(colour: ColorRgba, drawnAt: ColorDepth, substrate?: ColorRgba): ColorRgba;
@@ -1,254 +0,0 @@
1
- import { ColorDepth, ColorRgba, EIGHT_BIT_DOWNGRADE_TABLE, STANDARD_TABLE, blendRgb, contrastRatio, relativeLuminance, SURFACE_BLACK, } from "../core/color.js";
2
- import { Oklch } from "../core/oklch.js";
3
- const LEVEL_STEP = 0.1;
4
- function rgbToHsl(c) {
5
- const r = c.red / 255;
6
- const g = c.green / 255;
7
- const b = c.blue / 255;
8
- const max = Math.max(r, g, b);
9
- const min = Math.min(r, g, b);
10
- const l = (max + min) / 2;
11
- const d = max - min;
12
- if (d === 0)
13
- return { h: 0, s: 0, l };
14
- const s = d / (l > 0.5 ? 2 - max - min : max + min);
15
- let h;
16
- if (max === r)
17
- h = ((g - b) / d) % 6;
18
- else if (max === g)
19
- h = (b - r) / d + 2;
20
- else
21
- h = (r - g) / d + 4;
22
- h = (h * 60 + 360) % 360;
23
- return { h, s, l };
24
- }
25
- function clampChannel(v) {
26
- // Float HSL math + Math.round can land at -1 or 256 at the boundaries; clamp
27
- // so an invalid ColorRgba never escapes this function.
28
- const r = Math.round(v);
29
- return r < 0 ? 0 : r > 255 ? 255 : r;
30
- }
31
- function hslToRgb(hsl) {
32
- const { h, s, l } = hsl;
33
- if (s === 0) {
34
- const v = clampChannel(l * 255);
35
- return new ColorRgba(v, v, v);
36
- }
37
- const c = (1 - Math.abs(2 * l - 1)) * s;
38
- const hp = h / 60;
39
- const x = c * (1 - Math.abs((hp % 2) - 1));
40
- let r = 0, g = 0, b = 0;
41
- if (hp < 1)
42
- [r, g, b] = [c, x, 0];
43
- else if (hp < 2)
44
- [r, g, b] = [x, c, 0];
45
- else if (hp < 3)
46
- [r, g, b] = [0, c, x];
47
- else if (hp < 4)
48
- [r, g, b] = [0, x, c];
49
- else if (hp < 5)
50
- [r, g, b] = [x, 0, c];
51
- else
52
- [r, g, b] = [c, 0, x];
53
- const m = l - c / 2;
54
- return new ColorRgba(clampChannel((r + m) * 255), clampChannel((g + m) * 255), clampChannel((b + m) * 255));
55
- }
56
- function clamp01(v) {
57
- return v < 0 ? 0 : v > 1 ? 1 : v;
58
- }
59
- /**
60
- * Darken a color by N levels, where each level reduces HSL lightness by 10%.
61
- * Negative levels lighten. Level 0 returns an equivalent triplet (after the
62
- * RGB↔HSL roundtrip; values may differ by ±1 due to rounding).
63
- */
64
- export function darken(color, levels) {
65
- const hsl = rgbToHsl(color);
66
- hsl.l = clamp01(hsl.l - LEVEL_STEP * levels);
67
- return hslToRgb(hsl);
68
- }
69
- /**
70
- * Lighten a color by N levels. Equivalent to `darken(color, -levels)`.
71
- */
72
- export function lighten(color, levels) {
73
- return darken(color, -levels);
74
- }
75
- /**
76
- * Composite `fg` over `bg` with the given alpha (0..1). At alpha=0 returns bg;
77
- * at alpha=1 returns fg.
78
- */
79
- export function alphaBlend(fg, bg, alpha) {
80
- return blendRgb(bg, fg, clamp01(alpha));
81
- }
82
- /**
83
- * Pick a contrasting foreground (black or white) for a background, using the
84
- * WCAG relative-luminance threshold of 0.179 (the perceptually correct cutoff
85
- * where black and white are equally readable). A translucent `bg` is judged
86
- * as drawn: composited over `substrate` (see `drawnBackground`).
87
- */
88
- export function contrastFor(bg, substrate = SURFACE_BLACK) {
89
- const lum = relativeLuminance(drawnBackground(bg, substrate));
90
- return lum > 0.179
91
- ? new ColorRgba(0, 0, 0)
92
- : new ColorRgba(255, 255, 255);
93
- }
94
- // [LAW:one-way-deps] The WCAG measures live in core/color.ts, beside the
95
- // ColorTable whose `matchReadable` needs them to pick a drawn text colour, and
96
- // below the theme math here. Re-exported so this module stays the colour-math
97
- // surface.
98
- export { relativeLuminance, contrastRatio };
99
- // Iterations for the lightness bisection below. 20 resolves L to ~1e-6 — far
100
- // finer than 8-bit quantization or the eye.
101
- const CONTRAST_ITERS = 20;
102
- /**
103
- * Return a foreground guaranteed to clear `minRatio` against `bg`, keeping the
104
- * color *recognizably itself*. If the themed `fg` already passes it is returned
105
- * untouched. Otherwise its OKLCH lightness is slid toward the pole that raises
106
- * contrast — holding hue, and chroma where it stays in gamut (near the poles
107
- * gamut clamping may reduce chroma, but hue is preserved) — until the ratio is
108
- * met, so a blue on a dark-blue background becomes a lighter blue, not white.
109
- * Only when no lightness of that hue can meet the ratio (a mid-toned
110
- * background where even pure black-or-white tops out below the target) does it
111
- * fall back to `contrastFor`'s black/white — the true maximum-contrast pick.
112
- *
113
- * A translucent `bg` is measured as it is drawn — composited over
114
- * `substrate`, the SGR writer's black by default — and a translucent `fg` is
115
- * then flattened over that drawn background, the order the writer composites
116
- * in, so the ratio is measured on what the eye actually sees and the returned
117
- * color is opaque.
118
- *
119
- * `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
120
- * the terminal rounds text and background independently, and two roundings
121
- * can meet in the middle, so the ratio is measured on the drawn pair: a
122
- * colour that loses the floor there is replaced by the nearest cube/grey entry
123
- * that clears it (whose own rounding is itself). At ANSI the terminal draws
124
- * its own theme's colours, so the ratio is measured on the table's nominal
125
- * ones (`DRAWN_FROM`): they say which side of the ground text belongs on, and
126
- * text on its background's index measures 1:1 and is always replaced.
127
- * Truecolor draws the colour chosen.
128
- *
129
- * [LAW:single-enforcer] The one place "is this text readable, and if not fix
130
- * it" is decided. Callers route every fg/bg pair through here and the
131
- * unreadable state never reaches output. [LAW:dataflow-not-control-flow] the
132
- * function always runs; the measured ratio (data) decides how far the
133
- * lightness moves — there is no caller-side "should I check contrast" branch.
134
- */
135
- export function ensureContrast(fg, bg, minRatio = 4.5, // WCAG AA for normal text
136
- drawnAt = ColorDepth.TRUECOLOR, substrate = SURFACE_BLACK) {
137
- const ground = drawnBackground(bg, substrate);
138
- const chosen = ensureTruecolorContrast(fg, ground, minRatio);
139
- // [LAW:dataflow-not-control-flow] The depth names the table the terminal
140
- // draws from; every table is measured the same way.
141
- const table = DRAWN_FROM[drawnAt];
142
- if (table === undefined)
143
- return chosen;
144
- const drawnBg = drawnColour(ground, drawnAt, substrate);
145
- if (contrastRatio(drawnColour(chosen, drawnAt, substrate), drawnBg) >= minRatio)
146
- return chosen;
147
- return table.get(table.matchReadable(chosen, drawnBg, minRatio));
148
- }
149
- /**
150
- * `chosen` (composited opaque over `substrate`), or — when the depth the
151
- * terminal draws at rounds it to a colour `accept` refuses — the nearest colour that depth draws as itself which
152
- * `accept` takes. `accept` sees the candidate as drawn, and `drawn`, the same
153
- * rounding for any other colour it measures against, so the caller states a
154
- * floor once and it holds on the colours the terminal shows — at ANSI, on
155
- * the table's nominal colours, as `ensureContrast` measures there. Truecolor
156
- * draws from no table, so a colour refused there has no replacement;
157
- * `undefined` means nothing the depth draws is accepted.
158
- *
159
- * `ensureContrast` is this with a contrast ratio as the floor; this is for a
160
- * floor that is not text on its background (an open state standing off every
161
- * closed cell, two planes standing off each other).
162
- */
163
- export function ensureDrawn(chosen, drawnAt, accept, substrate = SURFACE_BLACK) {
164
- // The floor was measured on `chosen` composited over `substrate`, so that
165
- // opaque colour is what is returned — never a translucent one a different
166
- // ground would composite into a colour `accept` never saw.
167
- const opaque = drawnBackground(chosen, substrate);
168
- const drawn = (c) => drawnColour(c, drawnAt, substrate);
169
- if (accept(drawn(opaque), drawn))
170
- return opaque;
171
- // [LAW:dataflow-not-control-flow] Truecolor draws from no table, so it has
172
- // no candidates: a refusal there is `undefined` like any exhausted search.
173
- const table = DRAWN_FROM[drawnAt];
174
- const index = table?.matchWhere(opaque, (entry) => accept(entry, drawn));
175
- return index === undefined ? undefined : table.get(index);
176
- }
177
- /**
178
- * The colour a ground is shown as at `drawnAt`: composited over `substrate`
179
- * (the SGR writer's black by default), then rounded to the table that depth
180
- * draws from — at ANSI, that table's nominal colour stands in for the
181
- * theme's. The one account `ensureDrawn` and `ensureContrast` measure by, for
182
- * a caller that measures a floor of its own.
183
- */
184
- export function drawnColour(colour, drawnAt, substrate = SURFACE_BLACK) {
185
- const opaque = drawnBackground(colour, substrate);
186
- const table = DRAWN_FROM[drawnAt];
187
- return table === undefined ? opaque : table.get(table.match(opaque));
188
- }
189
- /**
190
- * A background as it is drawn: composited over the surface beneath it. That
191
- * surface is a fact about where the pair is drawn, so it arrives as a value:
192
- * the SGR writer (`Style.toSgrCodes`) composites over `SURFACE_BLACK`, the
193
- * default here; a caller choosing text for a different surface — an export's
194
- * canvas, `exportCanvas(theme).background` — names that one.
195
- * [LAW:no-silent-failure] A surface has nothing under it, so a translucent one
196
- * has no drawn colour to offer; `compositeOver` would read its raw RGB as if
197
- * it were opaque, so it is refused here rather than measured wrong.
198
- * [LAW:one-source-of-truth] Text is chosen against the colour the surface will
199
- * show — measuring the raw RGBA reads a colour that is drawn nowhere, and text
200
- * that "clears" it can land below the floor. Opaque colours composite to
201
- * themselves.
202
- */
203
- function drawnBackground(bg, substrate) {
204
- if (substrate.alpha !== 1) {
205
- throw new RangeError(`a contrast substrate is the opaque surface under a translucent background; got ${substrate.hex}`);
206
- }
207
- return bg.compositeOver(substrate);
208
- }
209
- /**
210
- * The downgrade table the terminal draws from, by depth, measured at each
211
- * entry's colour. The 256-colour cube and grey ramp are fixed by xterm, so
212
- * those measurements are exact. ANSI 0–15 are the terminal theme's own, so
213
- * the table's nominal colours stand in for them: they are wrong in hue from
214
- * theme to theme, but not about which side of a ground an entry sits on —
215
- * and two colours on one index are one colour in every theme, which the
216
- * nominal ratio of 1 refuses. Truecolor draws the chosen colour itself.
217
- */
218
- const DRAWN_FROM = {
219
- [ColorDepth.EIGHT_BIT]: EIGHT_BIT_DOWNGRADE_TABLE,
220
- [ColorDepth.STANDARD]: STANDARD_TABLE,
221
- };
222
- function ensureTruecolorContrast(fg, bg, minRatio) {
223
- // Flatten translucency so the guarantee holds for the displayed color, not
224
- // the raw bytes (e.g. a "#FFFFFF60" text-disabled over a light surface).
225
- const opaqueFg = fg.compositeOver(bg);
226
- if (contrastRatio(opaqueFg, bg) >= minRatio)
227
- return opaqueFg;
228
- const lab = Oklch.fromRgba(opaqueFg);
229
- // The pole that increases contrast: lighten toward white on a dark bg, darken
230
- // toward black on a light one. `contrastFor`'s 0.179 cutoff names it.
231
- const poleL = relativeLuminance(bg) > 0.179 ? 0 : 1;
232
- // If even the pole of this hue can't reach the ratio, the hue physically
233
- // can't — return the true maximum-contrast pick (pure black/white from
234
- // contrastFor). The gamut-clamped OKLCH pole is only *near* b/w, so
235
- // contrastFor is at least as strong and is the honest maximum.
236
- const pole = new Oklch(poleL, lab.c, lab.h, lab.alpha).toRgba();
237
- if (contrastRatio(pole, bg) < minRatio)
238
- return contrastFor(bg);
239
- // Bisect for the lightness nearest the original that still clears the ratio:
240
- // the smallest perceptual change that achieves accessibility. Contrast is
241
- // monotone in L over [lab.l, poleL] (everything below the crossing fails),
242
- // so the search is well-posed.
243
- let fail = lab.l;
244
- let pass = poleL;
245
- for (let i = 0; i < CONTRAST_ITERS; i++) {
246
- const mid = (fail + pass) / 2;
247
- const candidate = new Oklch(mid, lab.c, lab.h, lab.alpha).toRgba();
248
- if (contrastRatio(candidate, bg) >= minRatio)
249
- pass = mid;
250
- else
251
- fail = mid;
252
- }
253
- return new Oklch(pass, lab.c, lab.h, lab.alpha).toRgba();
254
- }
@@ -1,51 +0,0 @@
1
- import { ColorRgba } from "../core/color.js";
2
- import type { Palette } from "./palette.js";
3
- /**
4
- * A **color reference** is the one string form an author uses to name a
5
- * concrete color: a palette variable name (`"primary"`, `"surface-active"`,
6
- * `"text-primary"`) or a literal `#RRGGBB` / `#RRGGBBAA`.
7
- *
8
- * [LAW:parse-dont-validate] `resolveColorRef` is the single checkpoint that
9
- * turns an untrusted reference string into a `ColorRgba`. Everything
10
- * downstream — color math, `Style` construction, serialization — takes
11
- * `ColorRgba` and therefore cannot ask again whether the name existed. There
12
- * is no `isValidColorRef` companion, deliberately: a validator would hand back
13
- * the same `string` it was given and every consumer would re-check.
14
- *
15
- * [LAW:no-silent-failure] A miss throws. An unknown palette name is a broken
16
- * config, and the author needs to see which name and what was available — not
17
- * a silently substituted default that makes the bar merely look wrong.
18
- */
19
- export declare class ColorRefError extends Error {
20
- readonly ref: string;
21
- constructor(ref: string, detail: string);
22
- }
23
- /**
24
- * The literal-color shape, exported so the template bindings gate on the same
25
- * pattern this module parses. [LAW:one-source-of-truth] — one regex, one
26
- * parser body; the bindings add only their own error wording.
27
- */
28
- export declare const HEX_COLOR_RE: RegExp;
29
- /**
30
- * Parse a `#RRGGBB` / `#RRGGBBAA` literal. The hex arm of
31
- * {@link resolveColorRef}, separated so callers that accept *only* literal
32
- * colors (color math, which cannot meaningfully operate on a palette name)
33
- * share the one implementation instead of re-deriving it.
34
- *
35
- * @throws {ColorRefError} when `hex` is not a well-formed literal.
36
- */
37
- export declare function parseHexColor(hex: string): ColorRgba;
38
- /**
39
- * Resolve a color reference against `palette`.
40
- *
41
- * Accepting hex alongside names is what makes this **idempotent**:
42
- * `resolveColorRef(p, resolveColorRef(p, r).hex)` equals
43
- * `resolveColorRef(p, r)` for every `r` this function accepts. Callers
44
- * therefore never branch on "is this a palette name or already a color" —
45
- * they call it unconditionally on whatever the author wrote, whether that is
46
- * `"surface-active"` or the output of a chain of color math.
47
- * [LAW:dataflow-not-control-flow]
48
- *
49
- * @throws {ColorRefError} on malformed hex or an unknown palette name.
50
- */
51
- export declare function resolveColorRef(palette: Palette, ref: string): ColorRgba;