@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.
- 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 +5 -4
- package/dist/renderables/effect.d.ts +7 -7
- package/dist/renderables/effects.js +8 -3
- 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,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
|
-
|
|
18
|
-
export
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
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
|
-
|
|
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;
|