@promptctl/rich-js 0.25.2 → 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.
- package/dist/core/animation.d.ts +26 -8
- package/dist/core/animation.js +32 -11
- package/dist/core/color.d.ts +20 -11
- package/dist/core/color.js +26 -21
- package/dist/core/console.d.ts +8 -0
- package/dist/core/console.js +2 -1
- package/dist/core/easing.js +6 -1
- package/dist/core/oklch.d.ts +2 -0
- package/dist/core/oklch.js +3 -3
- package/dist/core/protocol.d.ts +26 -16
- package/dist/core/render.d.ts +1 -1
- package/dist/core/render.js +1 -0
- package/dist/core/view.d.ts +22 -8
- package/dist/core/view.js +32 -8
- package/dist/host/app.d.ts +4 -4
- package/dist/host/app.js +5 -5
- package/dist/index.d.ts +6 -5
- package/dist/renderables/effect.d.ts +44 -19
- package/dist/renderables/effect.js +20 -13
- package/dist/renderables/effects.d.ts +15 -9
- package/dist/renderables/effects.js +166 -34
- package/dist/renderables/lifecycle.d.ts +38 -28
- package/dist/renderables/lifecycle.js +85 -45
- package/dist/renderables/live.d.ts +3 -3
- package/dist/renderables/live.js +4 -4
- package/dist/renderables/progress.d.ts +11 -4
- package/dist/renderables/progress.js +29 -17
- package/dist/themes/buildPalette.d.ts +14 -39
- package/dist/themes/buildPalette.js +196 -67
- package/dist/themes/colorMath.d.ts +10 -4
- package/dist/themes/colorMath.js +33 -1
- package/dist/themes/data/types.d.ts +3 -1
- package/dist/themes/palette.d.ts +22 -6
- package/dist/themes/palette.js +13 -21
- package/dist/themes/registry.d.ts +2 -3
- package/dist/themes/registry.js +7 -26
- package/dist/themes/terminalThemes.d.ts +6 -5
- package/dist/themes/terminalThemes.js +6 -5
- package/dist/themes/transpose.d.ts +5 -10
- package/dist/themes/transpose.js +13 -24
- package/dist/widgets/event-router.js +2 -2
- package/dist/widgets/ink.d.ts +5 -4
- package/dist/widgets/ink.js +6 -9
- package/dist/widgets/text-input.d.ts +1 -1
- package/dist/widgets/text-input.js +41 -32
- package/dist/widgets/types.d.ts +2 -2
- package/dist/widgets/widget-app.d.ts +3 -3
- package/dist/widgets/widget-app.js +5 -5
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
*
|
|
58
|
+
* How each role not given to `buildPalette` is derived, following Textual's
|
|
59
|
+
* `ColorSystem`, which generated the bundled themes:
|
|
12
60
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - `
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
96
|
-
* whether it reads is decided here.
|
|
225
|
+
* from a tint and the authored ones alike.
|
|
97
226
|
*/
|
|
98
|
-
|
|
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
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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`
|
package/dist/themes/colorMath.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/themes/palette.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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<
|
|
35
|
-
constructor(name: string, dark: boolean,
|
|
36
|
-
get(
|
|
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
|
package/dist/themes/palette.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A semantic palette: a named map from
|
|
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
|
|
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,
|
|
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.
|
|
39
|
-
|
|
40
|
-
|
|
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(
|
|
46
|
-
return this.vars.get(
|
|
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
|
|
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;
|
package/dist/themes/registry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ColorParseError, ColorTable, parseHexColor } from "../core/color.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { drawnOn } from "./palette.js";
|
|
3
|
+
import { buildPalette } from "./buildPalette.js";
|
|
4
4
|
import { THEMES } from "./data/index.js";
|
|
5
5
|
import { ANSI_SLOTS } from "./data/types.js";
|
|
6
6
|
import { Memo } from "../core/memo.js";
|
|
@@ -30,11 +30,11 @@ function isThemeName(name) {
|
|
|
30
30
|
*/
|
|
31
31
|
export function getThemeBaseColors(name) {
|
|
32
32
|
const data = THEMES[name];
|
|
33
|
-
const bg =
|
|
33
|
+
const bg = parseHex(data.vars.background, data.name, "background");
|
|
34
34
|
// The colours `getThemePalette(name)` hands out for the same names: drawn on
|
|
35
35
|
// the theme's own background, as a Palette draws every colour it holds.
|
|
36
36
|
const draw = drawnOn(data.name, bg);
|
|
37
|
-
const base = (key) => draw(
|
|
37
|
+
const base = (key) => draw(parseHex(data.vars[key], data.name, key));
|
|
38
38
|
return {
|
|
39
39
|
name: data.name,
|
|
40
40
|
dark: data.dark,
|
|
@@ -49,34 +49,15 @@ export function getThemeBaseColors(name) {
|
|
|
49
49
|
ansi: new ColorTable(ANSI_SLOTS.map((slot) => parseOpaqueHex(data.ansi[slot], data.name, `ansi.${slot}`))),
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
function requireBaseVar(data, key) {
|
|
53
|
-
const v = data.vars[key];
|
|
54
|
-
if (v === undefined) {
|
|
55
|
-
throw new Error(`Theme ${data.name}: required base var "${key}" missing from data file`);
|
|
56
|
-
}
|
|
57
|
-
return parseHex(v, data.name, key);
|
|
58
|
-
}
|
|
59
52
|
/**
|
|
60
53
|
* Parse a `ThemePaletteData`'s hex strings into a Palette of ColorRgba.
|
|
61
54
|
*
|
|
62
55
|
* [LAW:single-enforcer] Hex → ColorRgba conversion for theme data lives
|
|
63
|
-
* here, not on Palette
|
|
64
|
-
* `core/color → themes/palette` one-way edge (palette.ts must not import
|
|
65
|
-
* ColorRgba as a value, or it cycles with core/color's own use of Palette
|
|
66
|
-
* for INTERNAL_DEFAULT_THEME).
|
|
56
|
+
* here, not on Palette; what a theme does not state, `buildPalette` derives.
|
|
67
57
|
*/
|
|
68
58
|
function hydrate(data) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
map.set(k, parseHex(v, data.name, `var ${k}`));
|
|
72
|
-
}
|
|
73
|
-
// The data is Textual's as authored; its `text-*` is held to the pair this
|
|
74
|
-
// library promises by the same rule `buildPalette` derives it with.
|
|
75
|
-
for (const key of ACCENT_KEYS) {
|
|
76
|
-
const text = requireBaseVar(data, `text-${key}`);
|
|
77
|
-
map.set(`text-${key}`, accentText(text, requireBaseVar(data, `${key}-muted`), requireBaseVar(data, "background")));
|
|
78
|
-
}
|
|
79
|
-
return new Palette(data.name, data.dark, map);
|
|
59
|
+
const stated = Object.entries(data.vars).map(([role, hex]) => [role, parseHex(hex, data.name, `var ${role}`)]);
|
|
60
|
+
return buildPalette(data.name, data.dark, Object.fromEntries(stated));
|
|
80
61
|
}
|
|
81
62
|
// Theme data is authored, so a malformed colour names the theme and the key it
|
|
82
63
|
// sits under; the grammar itself is `parseHexColor`'s.
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* matching `<NAME>` export here. There is no inline-only theme; the data
|
|
4
4
|
* file is the single source of truth for every theme's hex values.
|
|
5
5
|
*
|
|
6
|
-
* Editing `data/<name>.ts` updates both the registry's
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Editing `data/<name>.ts` updates both the registry's palette (via
|
|
7
|
+
* `getThemePalette(name)`) AND the matching `TerminalTheme` constant exported
|
|
8
|
+
* below (via `getThemeBaseColors(name)`, which pulls just the 8 substrate
|
|
9
|
+
* keys). The two views agree on those eight. Every other role of a
|
|
10
|
+
* `TerminalTheme`'s palette is derived from them, so it is not the colour the
|
|
11
|
+
* theme authored: read `getThemePalette` for that.
|
|
11
12
|
*
|
|
12
13
|
* [LAW:one-source-of-truth] Hex values live in `data/<name>.ts`. This
|
|
13
14
|
* module is a derived view that builds a `TerminalTheme` from each
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* matching `<NAME>` export here. There is no inline-only theme; the data
|
|
4
4
|
* file is the single source of truth for every theme's hex values.
|
|
5
5
|
*
|
|
6
|
-
* Editing `data/<name>.ts` updates both the registry's
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Editing `data/<name>.ts` updates both the registry's palette (via
|
|
7
|
+
* `getThemePalette(name)`) AND the matching `TerminalTheme` constant exported
|
|
8
|
+
* below (via `getThemeBaseColors(name)`, which pulls just the 8 substrate
|
|
9
|
+
* keys). The two views agree on those eight. Every other role of a
|
|
10
|
+
* `TerminalTheme`'s palette is derived from them, so it is not the colour the
|
|
11
|
+
* theme authored: read `getThemePalette` for that.
|
|
11
12
|
*
|
|
12
13
|
* [LAW:one-source-of-truth] Hex values live in `data/<name>.ts`. This
|
|
13
14
|
* module is a derived view that builds a `TerminalTheme` from each
|