@promptctl/rich-js 0.26.0 → 0.27.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 (47) hide show
  1. package/dist/core/animation.d.ts +26 -8
  2. package/dist/core/animation.js +32 -11
  3. package/dist/core/color.d.ts +20 -11
  4. package/dist/core/color.js +26 -21
  5. package/dist/core/console.d.ts +8 -0
  6. package/dist/core/console.js +2 -1
  7. package/dist/core/easing.js +6 -1
  8. package/dist/core/oklch.d.ts +2 -0
  9. package/dist/core/oklch.js +3 -3
  10. package/dist/core/protocol.d.ts +26 -16
  11. package/dist/core/render.d.ts +1 -1
  12. package/dist/core/render.js +1 -0
  13. package/dist/core/view.d.ts +22 -8
  14. package/dist/core/view.js +32 -8
  15. package/dist/host/app.d.ts +4 -4
  16. package/dist/host/app.js +5 -5
  17. package/dist/index.d.ts +5 -4
  18. package/dist/renderables/effect.d.ts +7 -7
  19. package/dist/renderables/effects.js +8 -3
  20. package/dist/renderables/lifecycle.d.ts +38 -28
  21. package/dist/renderables/lifecycle.js +85 -45
  22. package/dist/renderables/live.d.ts +3 -3
  23. package/dist/renderables/live.js +4 -4
  24. package/dist/renderables/progress.d.ts +11 -4
  25. package/dist/renderables/progress.js +29 -17
  26. package/dist/themes/buildPalette.d.ts +14 -39
  27. package/dist/themes/buildPalette.js +196 -67
  28. package/dist/themes/colorMath.d.ts +10 -4
  29. package/dist/themes/colorMath.js +33 -1
  30. package/dist/themes/data/types.d.ts +3 -1
  31. package/dist/themes/palette.d.ts +22 -6
  32. package/dist/themes/palette.js +13 -21
  33. package/dist/themes/registry.d.ts +2 -3
  34. package/dist/themes/registry.js +7 -26
  35. package/dist/themes/terminalThemes.d.ts +6 -5
  36. package/dist/themes/terminalThemes.js +6 -5
  37. package/dist/themes/transpose.d.ts +5 -10
  38. package/dist/themes/transpose.js +13 -24
  39. package/dist/widgets/event-router.js +2 -2
  40. package/dist/widgets/ink.d.ts +5 -4
  41. package/dist/widgets/ink.js +6 -9
  42. package/dist/widgets/text-input.d.ts +1 -1
  43. package/dist/widgets/text-input.js +41 -32
  44. package/dist/widgets/types.d.ts +2 -2
  45. package/dist/widgets/widget-app.d.ts +3 -3
  46. package/dist/widgets/widget-app.js +5 -5
  47. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { ColorRgba } from "../core/color.js";
2
- import { Palette } from "./palette.js";
2
+ import { Palette, type PaletteRole, type PaletteRoles } from "./palette.js";
3
3
  /**
4
4
  * Base colors required to build a full semantic palette.
5
5
  * All are ColorRgba — callers construct from whatever source (hex, HSL, etc.).
@@ -14,44 +14,19 @@ 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
+ /** The eight roles every palette is given, and every role it may also state. */
18
+ export type StatedRoles = BaseColors & Partial<PaletteRoles>;
19
+ /** Every role of every palette, read off the rules. */
20
+ export declare const PALETTE_ROLES: readonly PaletteRole[];
19
21
  /**
20
- * Build a full semantic palette from base colors.
22
+ * Build a full semantic palette from its base colors and any other role it
23
+ * states: a stated role keeps its colour, and everything derived from it
24
+ * follows it, but for `text-*`, which is held to read on its `*-muted`. An
25
+ * authored theme is this with most roles stated; see `RULES` for what a role
26
+ * not stated becomes.
21
27
  *
22
- * Derived entries follow Textual's formulas:
23
- * - `*-muted` = color blended 70% toward its opposite base role. Defined for
24
- * every accent AND for the two base roles themselves
25
- * (`foreground-muted` blends toward `background`,
26
- * `background-muted` blends toward `foreground`) — a caller
27
- * de-emphasizing body/structural text reaches for
28
- * `foreground-muted` the same way it reaches for
29
- * `primary-muted` to de-emphasize an accent.
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
33
- * - `on-*` = WCAG-correct contrast colour (black or white) for use as
34
- * foreground when the FULL accent is the background. Picked
35
- * by relative luminance — single source of truth so widgets
36
- * never need to invert / reverse fg/bg to get readable text.
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.
28
+ * A translucent colour is drawn onto `background` before anything is derived
29
+ * from it, so `on-*` is chosen against the colour the accent is drawn as.
30
+ * `boost` alone stays a layer: it is what `panel` composites.
55
31
  */
56
- export declare function accentText(tint: ColorRgba, muted: ColorRgba, background: ColorRgba): ColorRgba;
57
- export {};
32
+ export declare function buildPalette(name: string, dark: boolean, given: StatedRoles): Palette;
@@ -1,87 +1,217 @@
1
- import { blendRgb } from "../core/color.js";
2
- import { alphaBlend, contrastFor, ensureContrastToward } from "./colorMath.js";
1
+ import { ColorRgba, blendRgb } from "../core/color.js";
2
+ import { alphaBlend, contrastFor, ensureContrastToward, shiftLightness } from "./colorMath.js";
3
3
  import { Palette, drawnOn } from "./palette.js";
4
4
  const MUTED_BLEND = 0.7;
5
5
  const TEXT_ALPHA = 0.66;
6
6
  const SURFACE_LIFT = 0.05;
7
+ const LADDER_STEP = 0.075;
8
+ const SURFACE_ACTIVE_LIFT = 0.06;
9
+ const BORDER_BLUR_DROP = 0.025;
10
+ const PANEL_TINT = 0.1;
11
+ const BACKGROUND_TINT = 0.15;
12
+ const BACKGROUND_TINT_LIFT = 0.15;
13
+ const BOOST_ALPHA = 0.04;
14
+ const TEXT_FULL_ALPHA = 0.87;
15
+ const TEXT_MUTED_ALPHA = 0.6;
16
+ const DISABLED_ALPHA = 0.38;
17
+ const BLURRED_CURSOR_ALPHA = 0.3;
18
+ const HOVER_ALPHA = 0.1;
19
+ const SELECTION_ALPHA = 0.4;
20
+ const SCROLLBAR_ALPHA = 0.4;
21
+ const SCROLLBAR_HOVER_ALPHA = 0.5;
7
22
  // WCAG AA for body text: `text-*` is the label drawn on `*-muted`.
8
23
  const TEXT_ON_MUTED = 4.5;
9
- export const ACCENT_KEYS = ["primary", "secondary", "accent", "success", "warning", "error"];
24
+ const ACCENT_KEYS = ["primary", "secondary", "accent", "success", "warning", "error"];
25
+ const TRANSPARENT = new ColorRgba(0, 0, 0, 0);
26
+ const WHITE = new ColorRgba(255, 255, 255);
27
+ const withAlpha = (colour, alpha) => new ColorRgba(colour.red, colour.green, colour.blue, alpha);
28
+ const textOn = (alpha) => (at) => withAlpha(contrastFor(at("background")), alpha);
29
+ // [LAW:one-type-per-behavior] The roles naming another role's colour outright.
30
+ const same = (role) => (at) => at(role);
31
+ const none = () => TRANSPARENT;
32
+ /** The six rungs around `root`, each `-darken-3`…`-lighten-3` one `LADDER_STEP` apart. */
33
+ function ladder(root, rung) {
34
+ const rungs = ["darken", "lighten"].flatMap((direction) => [1, 2, 3].map((n) => {
35
+ return [`${root}-${direction}-${n}`, rung((direction === "darken" ? -n : n) * LADDER_STEP)];
36
+ }));
37
+ return Object.fromEntries(rungs);
38
+ }
39
+ const lightness = (root) => ladder(root, (step) => (at) => shiftLightness(at(root), step));
40
+ /**
41
+ * `primary-background` at `step` from its root: on a dark palette a tint of the
42
+ * background lifted toward white and floored at the tint, so the bottom rungs
43
+ * meet rather than fall below it; on a light one the accent's own ladder. Every
44
+ * rung reads the accent, as Textual's do, so a stated root does not move them.
45
+ */
46
+ const accentBackground = (accent, step) => (at, dark) => dark
47
+ ? blendRgb(blendRgb(at("background"), at(accent), BACKGROUND_TINT), WHITE, Math.max(0, BACKGROUND_TINT_LIFT + step))
48
+ : shiftLightness(at(accent), step);
49
+ function accentRules(accent) {
50
+ return {
51
+ ...lightness(accent),
52
+ [`${accent}-muted`]: ((at) => blendRgb(at(accent), at("background"), MUTED_BLEND)),
53
+ [`on-${accent}`]: ((at) => contrastFor(at(accent))),
54
+ [`text-${accent}`]: ((at) => alphaBlend(at(accent), contrastFor(at("background")), TEXT_ALPHA)),
55
+ };
56
+ }
10
57
  /**
11
- * Build a full semantic palette from base colors.
58
+ * How each role not given to `buildPalette` is derived, following Textual's
59
+ * `ColorSystem`, which generated the bundled themes:
12
60
  *
13
- * Derived entries follow Textual's formulas:
14
- * - `*-muted` = color blended 70% toward its opposite base role. Defined for
15
- * every accent AND for the two base roles themselves
16
- * (`foreground-muted` blends toward `background`,
17
- * `background-muted` blends toward `foreground`) — a caller
18
- * de-emphasizing body/structural text reaches for
19
- * `foreground-muted` the same way it reaches for
20
- * `primary-muted` to de-emphasize an accent.
21
- * - `text-*` = contrast text tinted 66% with the accent color, then held
22
- * to WCAG AA (4.5:1) on `*-muted` — the foreground for a
23
- * `*-muted` ground
24
- * - `on-*` = WCAG-correct contrast colour (black or white) for use as
25
- * foreground when the FULL accent is the background. Picked
26
- * by relative luminance — single source of truth so widgets
27
- * never need to invert / reverse fg/bg to get readable text.
28
- * - `surface` = background blended 5% toward foreground
61
+ * - `*-muted` = the colour blended 70% toward its opposite base role — every
62
+ * accent AND the two base roles themselves, so a caller
63
+ * de-emphasizing body text reaches for `foreground-muted` the
64
+ * way it reaches for `primary-muted` to de-emphasize an accent.
65
+ * `foreground-muted` is opaque here where Textual's is translucent,
66
+ * drawing structural text at reduced weight without a
67
+ * terminal-support-dependent SGR attribute.
68
+ * - `text-*` = contrast text tinted 66% with the accent, the foreground for
69
+ * a `*-muted` ground; `buildPalette` then holds it to that ground
70
+ * - `on-*` = WCAG-correct contrast colour (black or white) for use as
71
+ * foreground when the FULL accent is the background. Picked by
72
+ * relative luminance — single source of truth so widgets never
73
+ * need to invert fg/bg to get readable text. `contrastFor` is
74
+ * the only place that decides black-vs-white.
75
+ * - `surface` = background blended 5% toward foreground, a subtle lift
76
+ * - `*-darken-N` / `*-lighten-N` = the root moved 7.5% of CIE L* per rung
77
+ * - the component roles (`footer-*`, `markdown-*`, …) name the base role
78
+ * Textual draws that component in
29
79
  *
30
- * A translucent base colour is drawn onto `background` before anything is
31
- * derived from it, so `on-*` is chosen against the colour the accent is drawn
32
- * as and every derived entry is opaque.
80
+ * [LAW:types-are-the-program] Total over every role but the eight given, so a
81
+ * palette cannot lack one and a role cannot exist without a way to derive it.
82
+ */
83
+ const RULES = {
84
+ ...accentRules("primary"),
85
+ ...accentRules("secondary"),
86
+ ...accentRules("accent"),
87
+ ...accentRules("success"),
88
+ ...accentRules("warning"),
89
+ ...accentRules("error"),
90
+ ...lightness("background"),
91
+ ...lightness("foreground"),
92
+ ...lightness("panel"),
93
+ ...lightness("boost"),
94
+ ...lightness("surface"),
95
+ ...ladder("primary-background", (step) => accentBackground("primary", step)),
96
+ ...ladder("secondary-background", (step) => accentBackground("secondary", step)),
97
+ "background-muted": (at) => blendRgb(at("background"), at("foreground"), MUTED_BLEND),
98
+ "foreground-muted": (at) => blendRgb(at("foreground"), at("background"), MUTED_BLEND),
99
+ "foreground-disabled": (at) => withAlpha(at("foreground"), DISABLED_ALPHA),
100
+ surface: (at) => blendRgb(at("background"), at("foreground"), SURFACE_LIFT),
101
+ "surface-active": (at) => shiftLightness(at("surface"), SURFACE_ACTIVE_LIFT),
102
+ boost: textOn(BOOST_ALPHA),
103
+ panel: (at, dark) => {
104
+ const tinted = blendRgb(at("surface"), at("primary"), PANEL_TINT);
105
+ return dark ? at("boost").compositeOver(tinted) : tinted;
106
+ },
107
+ "primary-background": accentBackground("primary", 0),
108
+ "secondary-background": accentBackground("secondary", 0),
109
+ text: textOn(TEXT_FULL_ALPHA),
110
+ "text-muted": textOn(TEXT_MUTED_ALPHA),
111
+ "text-disabled": textOn(DISABLED_ALPHA),
112
+ border: same("primary"),
113
+ "border-blurred": (at) => shiftLightness(at("surface"), -BORDER_BLUR_DROP),
114
+ "block-cursor-background": same("primary"),
115
+ "block-cursor-foreground": same("on-primary"),
116
+ "block-cursor-blurred-background": (at) => withAlpha(at("primary"), BLURRED_CURSOR_ALPHA),
117
+ "block-cursor-blurred-foreground": same("foreground"),
118
+ "block-hover-background": (at) => withAlpha(at("boost"), HOVER_ALPHA),
119
+ "button-foreground": same("foreground"),
120
+ "button-color-foreground": same("on-primary"),
121
+ "footer-background": same("panel"),
122
+ "footer-foreground": same("foreground"),
123
+ "footer-description-background": none,
124
+ "footer-description-foreground": same("foreground"),
125
+ "footer-item-background": none,
126
+ "footer-key-background": none,
127
+ "footer-key-foreground": same("accent"),
128
+ "input-cursor-background": same("foreground"),
129
+ "input-cursor-foreground": same("background"),
130
+ "input-selection-background": (at) => withAlpha(at("primary-lighten-1"), SELECTION_ALPHA),
131
+ "link-background": none,
132
+ "link-background-hover": same("primary"),
133
+ "link-color": same("text"),
134
+ "link-color-hover": same("on-primary"),
135
+ "markdown-h1-background": none,
136
+ "markdown-h1-color": same("primary"),
137
+ "markdown-h2-background": none,
138
+ "markdown-h2-color": same("primary"),
139
+ "markdown-h3-background": none,
140
+ "markdown-h3-color": same("primary"),
141
+ "markdown-h4-background": none,
142
+ "markdown-h4-color": same("foreground"),
143
+ "markdown-h5-background": none,
144
+ "markdown-h5-color": same("foreground"),
145
+ "markdown-h6-background": none,
146
+ "markdown-h6-color": same("text-muted"),
147
+ scrollbar: (at) => alphaBlend(at("primary"), at("background-darken-1"), SCROLLBAR_ALPHA),
148
+ "scrollbar-active": same("primary"),
149
+ "scrollbar-hover": (at) => alphaBlend(at("primary"), at("background-darken-1"), SCROLLBAR_HOVER_ALPHA),
150
+ "scrollbar-background": same("background-darken-1"),
151
+ "scrollbar-background-active": same("scrollbar-background"),
152
+ "scrollbar-background-hover": same("scrollbar-background"),
153
+ "scrollbar-corner-color": same("scrollbar-background"),
154
+ };
155
+ const BASE_KEYS = [...ACCENT_KEYS, "background", "foreground"];
156
+ /** Every role of every palette, read off the rules. */
157
+ export const PALETTE_ROLES = [...BASE_KEYS, ...Object.keys(RULES)];
158
+ const ROLE_NAMES = new Set(PALETTE_ROLES);
159
+ /**
160
+ * Build a full semantic palette from its base colors and any other role it
161
+ * states: a stated role keeps its colour, and everything derived from it
162
+ * follows it, but for `text-*`, which is held to read on its `*-muted`. An
163
+ * authored theme is this with most roles stated; see `RULES` for what a role
164
+ * not stated becomes.
165
+ *
166
+ * A translucent colour is drawn onto `background` before anything is derived
167
+ * from it, so `on-*` is chosen against the colour the accent is drawn as.
168
+ * `boost` alone stays a layer: it is what `panel` composites.
33
169
  */
34
170
  export function buildPalette(name, dark, given) {
171
+ const known = statedRoles(name, given);
172
+ // [LAW:no-ambient-temporal-coupling] A rule asks for the roles it reads, so
173
+ // no order among the rules matters. The eight base roles are always known.
174
+ const at = (role) => {
175
+ const colour = known.get(role) ?? RULES[role](at, dark);
176
+ known.set(role, colour);
177
+ return colour;
178
+ };
179
+ // [LAW:single-enforcer] Stated and derived `text-*` alike: the tint is the
180
+ // palette's, whether it reads is decided here. A stated tint stays translucent
181
+ // because it composites on its `*-muted`, and a `*-muted` that draws nothing
182
+ // leaves it on `background`.
183
+ for (const key of ACCENT_KEYS) {
184
+ const ground = at(`${key}-muted`).compositeOver(at("background"));
185
+ known.set(`text-${key}`, accentText(given[`text-${key}`] ?? at(`text-${key}`), ground, at("background")));
186
+ }
187
+ return new Palette(name, dark, Object.fromEntries(PALETTE_ROLES.map((role) => [role, at(role)])));
188
+ }
189
+ /**
190
+ * The roles `given` states, drawn on `background`. The one place a caller's
191
+ * roles are read: a key that names no role is refused, and one holding
192
+ * `undefined` states nothing. [LAW:parse-dont-validate]
193
+ */
194
+ function statedRoles(name, given) {
35
195
  const drawn = drawnOn(name, given.background);
196
+ const stated = Object.entries(given).flatMap(([role, colour]) => {
197
+ if (!ROLE_NAMES.has(role))
198
+ throw new RangeError(`palette ${JSON.stringify(name)}: ${JSON.stringify(role)} is not a role`);
199
+ if (colour === undefined)
200
+ return [];
201
+ return [[role, role === "boost" ? colour : drawn(colour)]];
202
+ });
203
+ const known = new Map(stated);
36
204
  // Every base colour is derived from, and a fully transparent one draws no
37
205
  // colour of its own to derive a muted shade or a contrast from.
38
206
  // [LAW:no-silent-failure]
39
- const draw = (key) => {
207
+ for (const key of BASE_KEYS) {
40
208
  const colour = given[key];
41
209
  if (colour.alpha === 0) {
42
210
  throw new RangeError(`palette ${JSON.stringify(name)}: ${key} is fully transparent (${colour.hex}), so there is no colour to derive the palette from`);
43
211
  }
44
- return drawn(colour);
45
- };
46
- const base = {
47
- primary: draw("primary"),
48
- secondary: draw("secondary"),
49
- accent: draw("accent"),
50
- success: draw("success"),
51
- warning: draw("warning"),
52
- error: draw("error"),
53
- background: given.background,
54
- foreground: draw("foreground"),
55
- };
56
- const vars = new Map();
57
- // Base entries
58
- vars.set("background", base.background);
59
- vars.set("foreground", base.foreground);
60
- for (const key of ACCENT_KEYS) {
61
- vars.set(key, base[key]);
62
- }
63
- // Muted variants of the two base roles, same "blend 70% toward
64
- // background" formula as the accent `*-muted` family below — every
65
- // base color the palette defines gets a de-emphasized variant, not just
66
- // the accents. `foreground-muted` is what a caller reaches for to draw
67
- // structural/contextual text (labels, punctuation, ids) at reduced visual
68
- // weight without a terminal-support-dependent SGR attribute.
69
- vars.set("foreground-muted", blendRgb(base.foreground, base.background, MUTED_BLEND));
70
- vars.set("background-muted", blendRgb(base.background, base.foreground, MUTED_BLEND));
71
- // Surface — subtle lift from background
72
- vars.set("surface", blendRgb(base.background, base.foreground, SURFACE_LIFT));
73
- // Derived: muted, text-, and on- for each accent.
74
- // [LAW:single-enforcer] contrastFor is the only place that decides
75
- // black-vs-white for "text on this accent" — widgets read on-${accent}.
76
- const contrastText = contrastFor(base.background);
77
- for (const key of ACCENT_KEYS) {
78
- const color = base[key];
79
- const muted = blendRgb(color, base.background, MUTED_BLEND);
80
- vars.set(`${key}-muted`, muted);
81
- vars.set(`text-${key}`, accentText(alphaBlend(color, contrastText, TEXT_ALPHA), muted, base.background));
82
- vars.set(`on-${key}`, contrastFor(color));
212
+ known.set(key, drawn(colour));
83
213
  }
84
- return new Palette(name, dark, vars);
214
+ return known;
85
215
  }
86
216
  /**
87
217
  * A palette's `text-*`: its tint held to WCAG AA on `*-muted`, moved toward
@@ -92,9 +222,8 @@ export function buildPalette(name, dark, given) {
92
222
  * (test/themes/text-on-muted.test.ts).
93
223
  *
94
224
  * [LAW:single-enforcer] The one rule for the pair, for the palettes derived
95
- * here and the authored ones the registry hydrates: the tint is the theme's,
96
- * whether it reads is decided here.
225
+ * from a tint and the authored ones alike.
97
226
  */
98
- export function accentText(tint, muted, background) {
227
+ function accentText(tint, muted, background) {
99
228
  return ensureContrastToward(tint, muted, TEXT_ON_MUTED, contrastFor(background));
100
229
  }
@@ -1,4 +1,4 @@
1
- import { ColorDepth, ColorRgba, contrastRatio, relativeLuminance, type TerminalTheme } from "../core/color.js";
1
+ import { ColorDepth, ColorRgba, contrastRatio, relativeLuminance, type TerminalColors } from "../core/color.js";
2
2
  /**
3
3
  * Darken a color by N levels, where each level reduces HSL lightness by 10%.
4
4
  * Negative levels lighten. Level 0 returns an equivalent triplet (after the
@@ -9,6 +9,12 @@ export declare function darken(color: ColorRgba, levels: number): ColorRgba;
9
9
  * Lighten a color by N levels. Equivalent to `darken(color, -levels)`.
10
10
  */
11
11
  export declare function lighten(color: ColorRgba, levels: number): ColorRgba;
12
+ /**
13
+ * Move a colour's CIE L* by `amount` (0..1 of the L* scale), keeping its alpha.
14
+ * Textual's `Color.lighten`, which drew every shade ladder in the bundled
15
+ * themes, so a ladder derived here continues the ones authored there.
16
+ */
17
+ export declare function shiftLightness(color: ColorRgba, amount: number): ColorRgba;
12
18
  /**
13
19
  * Composite `fg` over `bg` with the given alpha (0..1). At alpha=0 returns bg;
14
20
  * at alpha=1 returns fg.
@@ -57,7 +63,7 @@ export { relativeLuminance, contrastRatio };
57
63
  * lightness moves — there is no caller-side "should I check contrast" branch.
58
64
  */
59
65
  export declare function ensureContrast(fg: ColorRgba, bg: ColorRgba, minRatio?: number, // WCAG AA for normal text
60
- drawnAt?: ColorDepth, substrate?: ColorRgba, terminal?: TerminalTheme): ColorRgba;
66
+ drawnAt?: ColorDepth, substrate?: ColorRgba, terminal?: TerminalColors): ColorRgba;
61
67
  /**
62
68
  * `chosen` (composited opaque over `substrate`), or — when the depth the
63
69
  * terminal draws at rounds it to a colour `accept` refuses — the colour the
@@ -73,7 +79,7 @@ drawnAt?: ColorDepth, substrate?: ColorRgba, terminal?: TerminalTheme): ColorRgb
73
79
  * floor that is not text on its background (an open state standing off every
74
80
  * closed cell, two planes standing off each other).
75
81
  */
76
- export declare function ensureDrawn(chosen: ColorRgba, drawnAt: ColorDepth, accept: (candidate: ColorRgba, drawn: (c: ColorRgba) => ColorRgba) => boolean, substrate?: ColorRgba, terminal?: TerminalTheme): ColorRgba | undefined;
82
+ export declare function ensureDrawn(chosen: ColorRgba, drawnAt: ColorDepth, accept: (candidate: ColorRgba, drawn: (c: ColorRgba) => ColorRgba) => boolean, substrate?: ColorRgba, terminal?: TerminalColors): ColorRgba | undefined;
77
83
  /**
78
84
  * The colour a ground is shown as at `drawnAt`: composited over `substrate`
79
85
  * (the SGR writer's black by default), rounded to the index the writer emits
@@ -81,7 +87,7 @@ export declare function ensureDrawn(chosen: ColorRgba, drawnAt: ColorDepth, acce
81
87
  * `terminal`'s own colour. The one account `ensureDrawn` and `ensureContrast`
82
88
  * measure by, for a caller that measures a floor of its own.
83
89
  */
84
- export declare function drawnColour(colour: ColorRgba, drawnAt: ColorDepth, substrate?: ColorRgba, terminal?: TerminalTheme): ColorRgba;
90
+ export declare function drawnColour(colour: ColorRgba, drawnAt: ColorDepth, substrate?: ColorRgba, terminal?: TerminalColors): ColorRgba;
85
91
  /**
86
92
  * `fg` held to `minRatio` on `bg` in truecolor, moving in lightness toward
87
93
  * `toward` (black or white). `ensureContrast` moves toward the pole `bg`
@@ -1,5 +1,5 @@
1
1
  import { ColorDepth, ColorRgba, ColorSpec, blendRgb, contrastRatio, downgradeTable, relativeLuminance, resolveTerminal, SURFACE_BLACK, } from "../core/color.js";
2
- import { Oklch } from "../core/oklch.js";
2
+ import { Oklch, linearToSrgb, srgbToLinear } from "../core/oklch.js";
3
3
  const LEVEL_STEP = 0.1;
4
4
  function rgbToHsl(c) {
5
5
  const r = c.red / 255;
@@ -72,6 +72,38 @@ export function darken(color, levels) {
72
72
  export function lighten(color, levels) {
73
73
  return darken(color, -levels);
74
74
  }
75
+ /** sRGB → CIE L*a*b* (D65, 2°): the space `shiftLightness` moves a colour in. */
76
+ function rgbToLab(c) {
77
+ const [r, g, b] = [c.red, c.green, c.blue].map((v) => srgbToLinear(v / 255));
78
+ const [x, y, z] = [
79
+ (r * 41.24 + g * 35.76 + b * 18.05) / 95.047,
80
+ (r * 21.26 + g * 71.52 + b * 7.22) / 100,
81
+ (r * 1.93 + g * 11.92 + b * 95.05) / 108.883,
82
+ ].map((v) => (v > 0.008856 ? Math.cbrt(v) : 7.787 * v + 16 / 116));
83
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
84
+ }
85
+ function labToRgb(l, a, b, alpha) {
86
+ const cube = (v) => (v > 0.2068930344 ? v ** 3 : (v - 16 / 116) / 7.787);
87
+ const fy = (l + 16) / 116;
88
+ const y = cube(fy);
89
+ const x = 0.95047 * cube(a / 500 + fy);
90
+ const z = 1.08883 * cube(fy - b / 200);
91
+ const [red, green, blue] = [
92
+ x * 3.2406 + y * -1.5372 + z * -0.4986,
93
+ x * -0.9689 + y * 1.8758 + z * 0.0415,
94
+ x * 0.0557 + y * -0.204 + z * 1.057,
95
+ ].map((v) => clampChannel(255 * linearToSrgb(v)));
96
+ return new ColorRgba(red, green, blue, alpha);
97
+ }
98
+ /**
99
+ * Move a colour's CIE L* by `amount` (0..1 of the L* scale), keeping its alpha.
100
+ * Textual's `Color.lighten`, which drew every shade ladder in the bundled
101
+ * themes, so a ladder derived here continues the ones authored there.
102
+ */
103
+ export function shiftLightness(color, amount) {
104
+ const [l, a, b] = rgbToLab(color);
105
+ return labToRgb(l + amount * 100, a, b, color.alpha);
106
+ }
75
107
  /**
76
108
  * Composite `fg` over `bg` with the given alpha (0..1). At alpha=0 returns bg;
77
109
  * at alpha=1 returns fg.
@@ -1,3 +1,4 @@
1
+ import type { BaseRole, PaletteRole } from "../palette.js";
1
2
  /**
2
3
  * Static shape of a theme palette as authored. Each `data/<name>.ts` file
3
4
  * exports one of these as its default export. The barrel (`./index.ts`)
@@ -17,7 +18,8 @@ export interface ThemePaletteData {
17
18
  * them in the VGA defaults (`blue` as #000080), which belong to no theme.
18
19
  */
19
20
  readonly ansi: AnsiColorsData;
20
- readonly vars: Readonly<Record<string, string>>;
21
+ /** The base roles and any other role this theme states; the rest are derived. */
22
+ readonly vars: Readonly<Record<BaseRole, string> & Partial<Record<PaletteRole, string>>>;
21
23
  }
22
24
  /**
23
25
  * ANSI colour numbers 0–15 in order: the eight normal colours, then their
@@ -1,6 +1,20 @@
1
1
  import type { ColorRgba } from "../core/color.js";
2
+ export type AccentRole = "primary" | "secondary" | "accent" | "success" | "warning" | "error";
3
+ export type BaseRole = AccentRole | "background" | "foreground";
4
+ export type ShadedRole = BaseRole | "panel" | "boost" | "surface" | "primary-background" | "secondary-background";
5
+ export type Shade = `-${"darken" | "lighten"}-${1 | 2 | 3}`;
2
6
  /**
3
- * A semantic palette: a named map from variable name → ColorRgba.
7
+ * Every role every palette has. [LAW:one-source-of-truth] This type is the one
8
+ * statement of the set: `buildPalette`'s rules are a total map over it, so a
9
+ * role added here does not compile until it can be derived, and the runtime
10
+ * list is read off those rules.
11
+ */
12
+ export type PaletteRole = ShadedRole | `${ShadedRole}${Shade}` | `${AccentRole}-muted` | `text-${AccentRole}` | `on-${AccentRole}` | "background-muted" | "foreground-muted" | "foreground-disabled" | "surface-active" | "text" | "text-muted" | "text-disabled" | "border" | "border-blurred" | "block-cursor-background" | "block-cursor-foreground" | "block-cursor-blurred-background" | "block-cursor-blurred-foreground" | "block-hover-background" | "button-foreground" | "button-color-foreground" | "footer-background" | "footer-foreground" | "footer-description-background" | "footer-description-foreground" | "footer-item-background" | "footer-key-background" | "footer-key-foreground" | "input-cursor-background" | "input-cursor-foreground" | "input-selection-background" | "link-background" | "link-background-hover" | "link-color" | "link-color-hover" | `markdown-h${1 | 2 | 3 | 4 | 5 | 6}-${"background" | "color"}` | "scrollbar" | "scrollbar-active" | "scrollbar-hover" | "scrollbar-background" | "scrollbar-background-active" | "scrollbar-background-hover" | "scrollbar-corner-color";
13
+ /** A colour for each role: what proves a palette complete. */
14
+ export type PaletteRoles = Readonly<Record<PaletteRole, ColorRgba>>;
15
+ /**
16
+ * A semantic palette: a named map from role → ColorRgba, holding every
17
+ * `PaletteRole`.
4
18
  *
5
19
  * Distinct from `ColorTable` (the integer-indexed quantization LUT used by
6
20
  * the downgrade pipeline). Palettes carry aesthetic intent — `primary`,
@@ -25,15 +39,17 @@ import type { ColorRgba } from "../core/color.js";
25
39
  * draws nothing on any surface, so it needs none and is handed out as it is,
26
40
  * and a `Style` reads it as no background at all.
27
41
  *
28
- * @throws RangeError when `background` is translucent, or when a colour is
29
- * partly transparent and there is no `background` to draw it on.
42
+ * @throws RangeError when `background` is translucent.
30
43
  */
31
44
  export declare class Palette {
32
45
  readonly name: string;
33
46
  readonly dark: boolean;
34
- readonly vars: ReadonlyMap<string, ColorRgba>;
35
- constructor(name: string, dark: boolean, vars: ReadonlyMap<string, ColorRgba>);
36
- get(key: string): ColorRgba | undefined;
47
+ readonly vars: ReadonlyMap<PaletteRole, ColorRgba>;
48
+ constructor(name: string, dark: boolean, roles: PaletteRoles);
49
+ get(role: PaletteRole): ColorRgba;
50
+ get(role: string): ColorRgba | undefined;
51
+ /** This palette's roles, each put through `colour`: what a new palette is built from. */
52
+ mapRoles(colour: (colour: ColorRgba, role: PaletteRole) => ColorRgba): PaletteRoles;
37
53
  }
38
54
  /**
39
55
  * A palette colour as it is drawn: composited onto the palette's own
@@ -1,5 +1,6 @@
1
1
  /**
2
- * A semantic palette: a named map from variable name → ColorRgba.
2
+ * A semantic palette: a named map from role → ColorRgba, holding every
3
+ * `PaletteRole`.
3
4
  *
4
5
  * Distinct from `ColorTable` (the integer-indexed quantization LUT used by
5
6
  * the downgrade pipeline). Palettes carry aesthetic intent — `primary`,
@@ -24,26 +25,26 @@
24
25
  * draws nothing on any surface, so it needs none and is handed out as it is,
25
26
  * and a `Style` reads it as no background at all.
26
27
  *
27
- * @throws RangeError when `background` is translucent, or when a colour is
28
- * partly transparent and there is no `background` to draw it on.
28
+ * @throws RangeError when `background` is translucent.
29
29
  */
30
30
  export class Palette {
31
31
  name;
32
32
  dark;
33
33
  vars;
34
- constructor(name, dark, vars) {
34
+ constructor(name, dark, roles) {
35
35
  this.name = name;
36
36
  this.dark = dark;
37
37
  // [LAW:single-enforcer] Every palette is built through here, so this is
38
- // where "a palette colour is opaque or draws nothing" holds. The fresh map is also the
39
- // defensive copy: ReadonlyMap is a compile-time aliasing constraint, not a
40
- // runtime one.
41
- const background = vars.get("background");
42
- const draw = background === undefined ? opaqueOnly(name) : drawnOn(name, background);
43
- this.vars = new Map([...vars].map(([key, colour]) => [key, draw(colour, key)]));
38
+ // where "a palette colour is opaque or draws nothing" holds.
39
+ const draw = drawnOn(name, roles.background);
40
+ this.vars = new Map(Object.entries(roles).map(([role, colour]) => [role, draw(colour)]));
44
41
  }
45
- get(key) {
46
- return this.vars.get(key);
42
+ get(role) {
43
+ return this.vars.get(role);
44
+ }
45
+ /** This palette's roles, each put through `colour`: what a new palette is built from. */
46
+ mapRoles(colour) {
47
+ return Object.fromEntries([...this.vars].map(([role, was]) => [role, colour(was, role)]));
47
48
  }
48
49
  }
49
50
  /**
@@ -60,12 +61,3 @@ export function drawnOn(name, background) {
60
61
  // A fully transparent colour is not a tint: it draws nothing on any surface.
61
62
  return (colour) => (colour.alpha === 0 ? colour : colour.compositeOver(background));
62
63
  }
63
- // A palette with no background has nothing to draw a translucent colour on.
64
- function opaqueOnly(name) {
65
- return (colour, key) => {
66
- if (colour.alpha !== 1 && colour.alpha !== 0) {
67
- throw new RangeError(`palette ${JSON.stringify(name)}: ${key} is translucent (${colour.hex}) but the palette has no "background" to draw it on`);
68
- }
69
- return colour;
70
- };
71
- }
@@ -1,5 +1,5 @@
1
1
  import { ColorRgba, ColorTable } from "../core/color.js";
2
- import { Palette } from "./palette.js";
2
+ import { type Palette } from "./palette.js";
3
3
  import { type ThemeName } from "./data/index.js";
4
4
  export type { ThemeName };
5
5
  /**
@@ -19,8 +19,7 @@ export declare function getThemePalette(name: string): Palette | null;
19
19
  /**
20
20
  * The eight base colors every theme must declare, and its ANSI table. Returned by
21
21
  * `getThemeBaseColors` for callers (notably `terminalThemes.ts`) that only
22
- * need the substrate and would otherwise pay the cost of hydrating the
23
- * full ~150-var palette and polluting the registry cache.
22
+ * need the substrate and would otherwise fill the registry cache.
24
23
  */
25
24
  export interface ThemeBaseColors {
26
25
  readonly name: string;