@promptctl/rich-js 0.18.1 → 0.19.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/README.md +9 -7
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +98 -18
- package/dist/core/color.js +331 -144
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +105 -2
- package/dist/core/pretty.js +527 -97
- package/dist/core/protocol.d.ts +41 -3
- package/dist/core/protocol.js +20 -0
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/strip.d.ts +13 -2
- package/dist/core/strip.js +44 -23
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +97 -34
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +31 -19
- package/dist/host/app.js +90 -60
- package/dist/host/index.d.ts +2 -1
- package/dist/host/terminal-host.d.ts +3 -2
- package/dist/index.d.ts +17 -10
- package/dist/index.js +11 -4
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +3 -2
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +87 -30
- package/dist/renderables/embed.js +134 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +104 -16
- package/dist/renderables/progress.js +205 -81
- package/dist/renderables/progressBar.d.ts +2 -2
- package/dist/renderables/progressBar.js +30 -19
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +1 -0
- package/dist/renderables/rule.js +44 -48
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +370 -192
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.js +2 -4
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +14 -48
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/widget-app.d.ts +2 -0
- package/dist/widgets/widget-app.js +13 -6
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +11 -1
package/dist/themes/colorMath.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColorDepth, ColorRgba,
|
|
1
|
+
import { ColorDepth, ColorRgba, ColorSpec, blendRgb, contrastRatio, downgradeTable, relativeLuminance, resolveTerminal, SURFACE_BLACK, } from "../core/color.js";
|
|
2
2
|
import { Oklch } from "../core/oklch.js";
|
|
3
3
|
const LEVEL_STEP = 0.1;
|
|
4
4
|
function rgbToHsl(c) {
|
|
@@ -121,10 +121,11 @@ const CONTRAST_ITERS = 20;
|
|
|
121
121
|
* can meet in the middle, so the ratio is measured on the drawn pair: a
|
|
122
122
|
* colour that loses the floor there is replaced by the nearest cube/grey entry
|
|
123
123
|
* that clears it (whose own rounding is itself). At ANSI the terminal draws
|
|
124
|
-
* its own theme's
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
124
|
+
* each index in its own theme's colour, so the ratio is measured on
|
|
125
|
+
* `terminal`'s sixteen: a theme whose black is light and white dark (Rosé Pine
|
|
126
|
+
* Dawn) turns the nominal table's sides over. A replacement is the colour the
|
|
127
|
+
* writer emits as the index nearest `fg` that clears the floor as `terminal`
|
|
128
|
+
* draws it. Truecolor draws the colour chosen.
|
|
128
129
|
*
|
|
129
130
|
* [LAW:single-enforcer] The one place "is this text readable, and if not fix
|
|
130
131
|
* it" is decided. Callers route every fg/bg pair through here and the
|
|
@@ -133,26 +134,27 @@ const CONTRAST_ITERS = 20;
|
|
|
133
134
|
* lightness moves — there is no caller-side "should I check contrast" branch.
|
|
134
135
|
*/
|
|
135
136
|
export function ensureContrast(fg, bg, minRatio = 4.5, // WCAG AA for normal text
|
|
136
|
-
drawnAt = ColorDepth.TRUECOLOR, substrate = SURFACE_BLACK) {
|
|
137
|
+
drawnAt = ColorDepth.TRUECOLOR, substrate = SURFACE_BLACK, terminal) {
|
|
137
138
|
const ground = drawnBackground(bg, substrate);
|
|
138
|
-
const chosen =
|
|
139
|
+
const chosen = ensureContrastToward(fg, ground, minRatio, contrastFor(ground));
|
|
139
140
|
// [LAW:dataflow-not-control-flow] The depth names the table the terminal
|
|
140
141
|
// draws from; every table is measured the same way.
|
|
141
|
-
const table =
|
|
142
|
+
const table = drawnTable(drawnAt, terminal);
|
|
142
143
|
if (table === undefined)
|
|
143
144
|
return chosen;
|
|
144
|
-
const
|
|
145
|
-
if (contrastRatio(
|
|
145
|
+
const shownBg = shownAs(table, ground);
|
|
146
|
+
if (contrastRatio(shownAs(table, chosen), shownBg) >= minRatio)
|
|
146
147
|
return chosen;
|
|
147
|
-
return table.get(table.matchReadable(
|
|
148
|
+
return table.written.get(table.shown.matchReadable(fg.compositeOver(ground), shownBg, minRatio));
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
150
151
|
* `chosen` (composited opaque over `substrate`), or — when the depth the
|
|
151
|
-
* terminal draws at rounds it to a colour `accept` refuses — the
|
|
152
|
-
* `accept` takes. `accept` sees the
|
|
153
|
-
*
|
|
154
|
-
* floor once and it holds on the
|
|
155
|
-
* the
|
|
152
|
+
* terminal draws at rounds it to a colour `accept` refuses — the colour the
|
|
153
|
+
* writer emits as the nearest entry `accept` takes. `accept` sees the
|
|
154
|
+
* candidate as drawn, and `drawn`, the same rounding for any other colour it
|
|
155
|
+
* measures against, so the caller states a floor once and it holds on the
|
|
156
|
+
* colours the terminal shows — at ANSI, `terminal`'s own sixteen, as
|
|
157
|
+
* `ensureContrast` measures there. Truecolor
|
|
156
158
|
* draws from no table, so a colour refused there has no replacement;
|
|
157
159
|
* `undefined` means nothing the depth draws is accepted.
|
|
158
160
|
*
|
|
@@ -160,38 +162,45 @@ drawnAt = ColorDepth.TRUECOLOR, substrate = SURFACE_BLACK) {
|
|
|
160
162
|
* floor that is not text on its background (an open state standing off every
|
|
161
163
|
* closed cell, two planes standing off each other).
|
|
162
164
|
*/
|
|
163
|
-
export function ensureDrawn(chosen, drawnAt, accept, substrate = SURFACE_BLACK) {
|
|
165
|
+
export function ensureDrawn(chosen, drawnAt, accept, substrate = SURFACE_BLACK, terminal) {
|
|
164
166
|
// The floor was measured on `chosen` composited over `substrate`, so that
|
|
165
167
|
// opaque colour is what is returned — never a translucent one a different
|
|
166
168
|
// ground would composite into a colour `accept` never saw.
|
|
167
169
|
const opaque = drawnBackground(chosen, substrate);
|
|
168
|
-
const
|
|
170
|
+
const table = drawnTable(drawnAt, terminal);
|
|
171
|
+
const drawn = (c) => shownAs(table, drawnBackground(c, substrate));
|
|
169
172
|
if (accept(drawn(opaque), drawn))
|
|
170
173
|
return opaque;
|
|
171
174
|
// [LAW:dataflow-not-control-flow] Truecolor draws from no table, so it has
|
|
172
175
|
// no candidates: a refusal there is `undefined` like any exhausted search.
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
return index === undefined ? undefined : table.get(index);
|
|
176
|
+
const index = table?.shown.matchWhere(opaque, (entry) => accept(entry, drawn));
|
|
177
|
+
return index === undefined ? undefined : table.written.get(index);
|
|
176
178
|
}
|
|
177
179
|
/**
|
|
178
180
|
* The colour a ground is shown as at `drawnAt`: composited over `substrate`
|
|
179
|
-
* (the SGR writer's black by default),
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* a caller that measures a floor of its own.
|
|
181
|
+
* (the SGR writer's black by default), rounded to the index the writer emits
|
|
182
|
+
* at that depth, and shown as the terminal shows that index — at ANSI, in
|
|
183
|
+
* `terminal`'s own colour. The one account `ensureDrawn` and `ensureContrast`
|
|
184
|
+
* measure by, for a caller that measures a floor of its own.
|
|
183
185
|
*/
|
|
184
|
-
export function drawnColour(colour, drawnAt, substrate = SURFACE_BLACK) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
export function drawnColour(colour, drawnAt, substrate = SURFACE_BLACK, terminal) {
|
|
187
|
+
return shownAs(drawnTable(drawnAt, terminal), drawnBackground(colour, substrate));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* An opaque colour as the terminal shows it: downgraded to the index the
|
|
191
|
+
* writer emits and drawn as the terminal draws that index.
|
|
192
|
+
* [LAW:one-source-of-truth] That rounding is `ColorSpec.downgrade`'s, the one
|
|
193
|
+
* `Style.drawnColors` writes; the tables here only list the candidates.
|
|
194
|
+
*/
|
|
195
|
+
function shownAs(table, opaque) {
|
|
196
|
+
return table === undefined ? opaque : ColorSpec.fromRgba(opaque).downgrade(table.depth).getTruecolor(table.terminal);
|
|
188
197
|
}
|
|
189
198
|
/**
|
|
190
199
|
* A background as it is drawn: composited over the surface beneath it. That
|
|
191
200
|
* surface is a fact about where the pair is drawn, so it arrives as a value:
|
|
192
|
-
* the SGR writer (`Style.toSgrCodes`)
|
|
193
|
-
* default here; a caller
|
|
194
|
-
*
|
|
201
|
+
* the SGR writer (`Style.toSgrCodes`) and the exports that picture its output
|
|
202
|
+
* composite over `SURFACE_BLACK`, the default here; a caller that composites
|
|
203
|
+
* its colours onto a different surface itself names that one.
|
|
195
204
|
* [LAW:no-silent-failure] A surface has nothing under it, so a translucent one
|
|
196
205
|
* has no drawn colour to offer; `compositeOver` would read its raw RGB as if
|
|
197
206
|
* it were opaque, so it is refused here rather than measured wrong.
|
|
@@ -207,28 +216,45 @@ function drawnBackground(bg, substrate) {
|
|
|
207
216
|
return bg.compositeOver(substrate);
|
|
208
217
|
}
|
|
209
218
|
/**
|
|
210
|
-
* The
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* the
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
219
|
+
* The table the terminal draws from at `drawnAt`. The 256-colour cube and
|
|
220
|
+
* grey ramp are fixed by xterm, so the colour written is the colour shown.
|
|
221
|
+
* ANSI 0–15 are the terminal theme's own: the writer still picks the index by
|
|
222
|
+
* the nominal table — the Windows console's at WINDOWS — and the terminal
|
|
223
|
+
* shows `terminal`'s colour there.
|
|
224
|
+
* Truecolor draws the chosen colour itself, as do the depths no table stands
|
|
225
|
+
* behind.
|
|
226
|
+
*/
|
|
227
|
+
function drawnTable(drawnAt, terminal) {
|
|
228
|
+
const drawnBy = resolveTerminal(terminal, drawnAt);
|
|
229
|
+
switch (drawnAt) {
|
|
230
|
+
case ColorDepth.EIGHT_BIT:
|
|
231
|
+
return { depth: drawnAt, terminal: drawnBy, written: downgradeTable(drawnAt), shown: downgradeTable(drawnAt) };
|
|
232
|
+
case ColorDepth.STANDARD:
|
|
233
|
+
case ColorDepth.WINDOWS:
|
|
234
|
+
return { depth: drawnAt, terminal: drawnBy, written: downgradeTable(drawnAt), shown: drawnBy.ansiColors };
|
|
235
|
+
case ColorDepth.TRUECOLOR:
|
|
236
|
+
case ColorDepth.DEFAULT:
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* `fg` held to `minRatio` on `bg` in truecolor, moving in lightness toward
|
|
242
|
+
* `toward` (black or white). `ensureContrast` moves toward the pole `bg`
|
|
243
|
+
* itself contrasts with; a colour that already has a side (a palette's
|
|
244
|
+
* `text-*` is its text, tinted) passes that side, since on a ground near the
|
|
245
|
+
* 0.179 cutoff both poles clear and only one also reads on everything else
|
|
246
|
+
* the colour is drawn on. Where `toward` cannot clear `minRatio` on `bg`,
|
|
247
|
+
* the floor wins over the side: the answer is `contrastFor(bg)`.
|
|
217
248
|
*/
|
|
218
|
-
|
|
219
|
-
[ColorDepth.EIGHT_BIT]: EIGHT_BIT_DOWNGRADE_TABLE,
|
|
220
|
-
[ColorDepth.STANDARD]: STANDARD_TABLE,
|
|
221
|
-
};
|
|
222
|
-
function ensureTruecolorContrast(fg, bg, minRatio) {
|
|
249
|
+
export function ensureContrastToward(fg, bg, minRatio, toward) {
|
|
223
250
|
// Flatten translucency so the guarantee holds for the displayed color, not
|
|
224
|
-
// the raw bytes (e.g. a "#FFFFFF60"
|
|
251
|
+
// the raw bytes (e.g. a hand-written "#FFFFFF60" over a light surface).
|
|
225
252
|
const opaqueFg = fg.compositeOver(bg);
|
|
226
253
|
if (contrastRatio(opaqueFg, bg) >= minRatio)
|
|
227
254
|
return opaqueFg;
|
|
228
255
|
const lab = Oklch.fromRgba(opaqueFg);
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
const poleL = relativeLuminance(bg) > 0.179 ? 0 : 1;
|
|
256
|
+
// `toward` is black or white, the pole the lightness moves to.
|
|
257
|
+
const poleL = relativeLuminance(toward) > 0.179 ? 1 : 0;
|
|
232
258
|
// If even the pole of this hue can't reach the ratio, the hue physically
|
|
233
259
|
// can't — return the true maximum-contrast pick (pure black/white from
|
|
234
260
|
// contrastFor). The gamut-clamped OKLCH pole is only *near* b/w, so
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColorRgba } from "../core/color.js";
|
|
1
|
+
import { type ColorRgba } from "../core/color.js";
|
|
2
2
|
import type { Palette } from "./palette.js";
|
|
3
3
|
/**
|
|
4
4
|
* A **color reference** is the one string form an author uses to name a
|
|
@@ -18,23 +18,8 @@ import type { Palette } from "./palette.js";
|
|
|
18
18
|
*/
|
|
19
19
|
export declare class ColorRefError extends Error {
|
|
20
20
|
readonly ref: string;
|
|
21
|
-
constructor(ref: string, detail: string);
|
|
21
|
+
constructor(ref: string, detail: string, options?: ErrorOptions);
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
* The literal-color shape, exported so the template bindings gate on the same
|
|
25
|
-
* pattern this module parses. [LAW:one-source-of-truth] — one regex, one
|
|
26
|
-
* parser body; the bindings add only their own error wording.
|
|
27
|
-
*/
|
|
28
|
-
export declare const HEX_COLOR_RE: RegExp;
|
|
29
|
-
/**
|
|
30
|
-
* Parse a `#RRGGBB` / `#RRGGBBAA` literal. The hex arm of
|
|
31
|
-
* {@link resolveColorRef}, separated so callers that accept *only* literal
|
|
32
|
-
* colors (color math, which cannot meaningfully operate on a palette name)
|
|
33
|
-
* share the one implementation instead of re-deriving it.
|
|
34
|
-
*
|
|
35
|
-
* @throws {ColorRefError} when `hex` is not a well-formed literal.
|
|
36
|
-
*/
|
|
37
|
-
export declare function parseHexColor(hex: string): ColorRgba;
|
|
38
23
|
/**
|
|
39
24
|
* Resolve a color reference against `palette`.
|
|
40
25
|
*
|
package/dist/themes/colorRef.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ColorParseError, parseHexColor } from "../core/color.js";
|
|
2
2
|
/**
|
|
3
3
|
* A **color reference** is the one string form an author uses to name a
|
|
4
4
|
* concrete color: a palette variable name (`"primary"`, `"surface-active"`,
|
|
@@ -17,8 +17,8 @@ import { parseRgbHex, parseRgbaHex } from "../core/color.js";
|
|
|
17
17
|
*/
|
|
18
18
|
export class ColorRefError extends Error {
|
|
19
19
|
ref;
|
|
20
|
-
constructor(ref, detail) {
|
|
21
|
-
super(`color reference ${JSON.stringify(ref)} did not resolve — ${detail}
|
|
20
|
+
constructor(ref, detail, options) {
|
|
21
|
+
super(`color reference ${JSON.stringify(ref)} did not resolve — ${detail}`, options);
|
|
22
22
|
this.ref = ref;
|
|
23
23
|
this.name = "ColorRefError";
|
|
24
24
|
}
|
|
@@ -30,32 +30,6 @@ export class ColorRefError extends Error {
|
|
|
30
30
|
// palette lookup that would report a confusing "unknown name" instead of
|
|
31
31
|
// "malformed hex".
|
|
32
32
|
const HEX_LEAD = "#";
|
|
33
|
-
/**
|
|
34
|
-
* The literal-color shape, exported so the template bindings gate on the same
|
|
35
|
-
* pattern this module parses. [LAW:one-source-of-truth] — one regex, one
|
|
36
|
-
* parser body; the bindings add only their own error wording.
|
|
37
|
-
*/
|
|
38
|
-
export const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/;
|
|
39
|
-
const OPAQUE_HEX_LENGTH = 7; // "#RRGGBB"
|
|
40
|
-
/**
|
|
41
|
-
* Parse a `#RRGGBB` / `#RRGGBBAA` literal. The hex arm of
|
|
42
|
-
* {@link resolveColorRef}, separated so callers that accept *only* literal
|
|
43
|
-
* colors (color math, which cannot meaningfully operate on a palette name)
|
|
44
|
-
* share the one implementation instead of re-deriving it.
|
|
45
|
-
*
|
|
46
|
-
* @throws {ColorRefError} when `hex` is not a well-formed literal.
|
|
47
|
-
*/
|
|
48
|
-
export function parseHexColor(hex) {
|
|
49
|
-
const trimmed = hex.trim();
|
|
50
|
-
if (!HEX_COLOR_RE.test(trimmed)) {
|
|
51
|
-
throw new ColorRefError(hex, "expected #RRGGBB or #RRGGBBAA");
|
|
52
|
-
}
|
|
53
|
-
// Both parsers take the digits without the leading `#`.
|
|
54
|
-
const digits = trimmed.slice(1);
|
|
55
|
-
return trimmed.length > OPAQUE_HEX_LENGTH
|
|
56
|
-
? parseRgbaHex(digits)
|
|
57
|
-
: parseRgbHex(digits);
|
|
58
|
-
}
|
|
59
33
|
// Cap the "did you mean" list. Palettes carry ~150 variables; dumping all of
|
|
60
34
|
// them buries the message. Nearby names are what a mistyped reference needs.
|
|
61
35
|
const SUGGESTION_LIMIT = 8;
|
|
@@ -74,8 +48,16 @@ const SUGGESTION_LIMIT = 8;
|
|
|
74
48
|
*/
|
|
75
49
|
export function resolveColorRef(palette, ref) {
|
|
76
50
|
const trimmed = ref.trim();
|
|
77
|
-
if (trimmed.startsWith(HEX_LEAD))
|
|
78
|
-
|
|
51
|
+
if (trimmed.startsWith(HEX_LEAD)) {
|
|
52
|
+
try {
|
|
53
|
+
return parseHexColor(trimmed);
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
if (!(cause instanceof ColorParseError))
|
|
57
|
+
throw cause;
|
|
58
|
+
throw new ColorRefError(ref, cause.message, { cause });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
79
61
|
const hit = palette.get(trimmed);
|
|
80
62
|
if (hit === undefined) {
|
|
81
63
|
throw new ColorRefError(ref, `no such variable in palette ${JSON.stringify(palette.name)}` +
|
package/dist/themes/palette.d.ts
CHANGED
|
@@ -11,6 +11,22 @@ import type { ColorRgba } from "../core/color.js";
|
|
|
11
11
|
* author-written *reference* (a name, or a `#RRGGBB` literal) into a colour
|
|
12
12
|
* is `resolveColorRef`'s job in `colorRef.ts`, so a Palette never has to
|
|
13
13
|
* know about the syntax callers write.
|
|
14
|
+
*
|
|
15
|
+
* Every colour a Palette hands out is opaque or fully transparent. A theme's translucent variable
|
|
16
|
+
* (Textual's `boost`, a hover overlay) is a tint meant for that theme's own
|
|
17
|
+
* background, and a colour leaving the palette no longer knows which
|
|
18
|
+
* background that was: the SGR writer would composite it over the terminal's
|
|
19
|
+
* black, which is not the theme's background, and draw a tint the theme never
|
|
20
|
+
* meant.
|
|
21
|
+
* So it is drawn onto `background` here, once, and the writer, the exporters
|
|
22
|
+
* and the contrast choosers all read the colour it has there. On any other
|
|
23
|
+
* surface it keeps that colour: the tint does not follow what is under it.
|
|
24
|
+
* The exception is a fully transparent colour, Textual's "no background": it
|
|
25
|
+
* draws nothing on any surface, so it needs none and is handed out as it is,
|
|
26
|
+
* and a `Style` reads it as no background at all.
|
|
27
|
+
*
|
|
28
|
+
* @throws RangeError when `background` is translucent, or when a colour is
|
|
29
|
+
* partly transparent and there is no `background` to draw it on.
|
|
14
30
|
*/
|
|
15
31
|
export declare class Palette {
|
|
16
32
|
readonly name: string;
|
|
@@ -19,3 +35,11 @@ export declare class Palette {
|
|
|
19
35
|
constructor(name: string, dark: boolean, vars: ReadonlyMap<string, ColorRgba>);
|
|
20
36
|
get(key: string): ColorRgba | undefined;
|
|
21
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A palette colour as it is drawn: composited onto the palette's own
|
|
40
|
+
* `background`, which has nothing under it and so must itself be opaque.
|
|
41
|
+
* [LAW:one-source-of-truth] The one statement of what a translucent palette
|
|
42
|
+
* colour means; `buildPalette` draws its base colours through it before
|
|
43
|
+
* deriving from them.
|
|
44
|
+
*/
|
|
45
|
+
export declare function drawnOn(name: string, background: ColorRgba): (colour: ColorRgba) => ColorRgba;
|
package/dist/themes/palette.js
CHANGED
|
@@ -10,6 +10,22 @@
|
|
|
10
10
|
* author-written *reference* (a name, or a `#RRGGBB` literal) into a colour
|
|
11
11
|
* is `resolveColorRef`'s job in `colorRef.ts`, so a Palette never has to
|
|
12
12
|
* know about the syntax callers write.
|
|
13
|
+
*
|
|
14
|
+
* Every colour a Palette hands out is opaque or fully transparent. A theme's translucent variable
|
|
15
|
+
* (Textual's `boost`, a hover overlay) is a tint meant for that theme's own
|
|
16
|
+
* background, and a colour leaving the palette no longer knows which
|
|
17
|
+
* background that was: the SGR writer would composite it over the terminal's
|
|
18
|
+
* black, which is not the theme's background, and draw a tint the theme never
|
|
19
|
+
* meant.
|
|
20
|
+
* So it is drawn onto `background` here, once, and the writer, the exporters
|
|
21
|
+
* and the contrast choosers all read the colour it has there. On any other
|
|
22
|
+
* surface it keeps that colour: the tint does not follow what is under it.
|
|
23
|
+
* The exception is a fully transparent colour, Textual's "no background": it
|
|
24
|
+
* draws nothing on any surface, so it needs none and is handed out as it is,
|
|
25
|
+
* and a `Style` reads it as no background at all.
|
|
26
|
+
*
|
|
27
|
+
* @throws RangeError when `background` is translucent, or when a colour is
|
|
28
|
+
* partly transparent and there is no `background` to draw it on.
|
|
13
29
|
*/
|
|
14
30
|
export class Palette {
|
|
15
31
|
name;
|
|
@@ -18,11 +34,38 @@ export class Palette {
|
|
|
18
34
|
constructor(name, dark, vars) {
|
|
19
35
|
this.name = name;
|
|
20
36
|
this.dark = dark;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
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)]));
|
|
24
44
|
}
|
|
25
45
|
get(key) {
|
|
26
46
|
return this.vars.get(key);
|
|
27
47
|
}
|
|
28
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* A palette colour as it is drawn: composited onto the palette's own
|
|
51
|
+
* `background`, which has nothing under it and so must itself be opaque.
|
|
52
|
+
* [LAW:one-source-of-truth] The one statement of what a translucent palette
|
|
53
|
+
* colour means; `buildPalette` draws its base colours through it before
|
|
54
|
+
* deriving from them.
|
|
55
|
+
*/
|
|
56
|
+
export function drawnOn(name, background) {
|
|
57
|
+
if (background.alpha !== 1) {
|
|
58
|
+
throw new RangeError(`palette ${JSON.stringify(name)}: background is the surface its translucent colours are drawn on, so it must be opaque; got ${background.hex}`);
|
|
59
|
+
}
|
|
60
|
+
// A fully transparent colour is not a tint: it draws nothing on any surface.
|
|
61
|
+
return (colour) => (colour.alpha === 0 ? colour : colour.compositeOver(background));
|
|
62
|
+
}
|
|
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
|
+
}
|
package/dist/themes/ramp.d.ts
CHANGED
|
@@ -9,46 +9,28 @@
|
|
|
9
9
|
* can no longer transpose with the palette it was meant to belong to. A ramp
|
|
10
10
|
* is the one function whose input is a number, so the decision stays inside.
|
|
11
11
|
*
|
|
12
|
-
* A threshold cascade is a ramp too: the same stops with
|
|
12
|
+
* A threshold cascade is a ramp too: the same stops with the `step` ease hold
|
|
13
13
|
* each color until the next position, which is exactly `≥ threshold → hotter`
|
|
14
14
|
* written as data. [LAW:one-type-per-behavior] One primitive; the easing is a
|
|
15
|
-
* value
|
|
16
|
-
* was
|
|
15
|
+
* value — any `Ease` from `core/easing`, the vocabulary every effect shares —
|
|
16
|
+
* so a gradient and a cascade differ by one word, not by which function was
|
|
17
|
+
* called. [LAW:dataflow-not-control-flow] `at` applies it unconditionally.
|
|
17
18
|
*
|
|
18
19
|
* Interpolation is in OKLCH (`Oklch.mix`), so a `linear` ramp between two
|
|
19
20
|
* theme colors passes through perceptually even steps rather than the muddy
|
|
20
21
|
* midpoints of an sRGB blend.
|
|
21
22
|
*
|
|
22
|
-
* [LAW:one-way-deps] Imports `core/color` and `core/oklch`
|
|
23
|
-
* knows what a palette is. Resolving stop *names* is the template binding's
|
|
23
|
+
* [LAW:one-way-deps] Imports `core/color`, `core/easing` and `core/oklch`
|
|
24
|
+
* only; nothing here knows what a palette is. Resolving stop *names* is the template binding's
|
|
24
25
|
* job (`paletteFuncs`), which hands this module resolved colors.
|
|
25
26
|
*/
|
|
26
27
|
import type { ColorRgba } from "../core/color.js";
|
|
28
|
+
import type { Ease } from "../core/easing.js";
|
|
27
29
|
/** One stop: the color the ramp is exactly `color` at position `at`. */
|
|
28
30
|
export interface ColorStop {
|
|
29
31
|
readonly at: number;
|
|
30
32
|
readonly color: ColorRgba;
|
|
31
33
|
}
|
|
32
|
-
/**
|
|
33
|
-
* How a value between two stops maps to progress along them. Each easing is
|
|
34
|
-
* a function on segment progress `t ∈ [0, 1)` — `linear` keeps it, `step`
|
|
35
|
-
* holds the left stop for the whole segment.
|
|
36
|
-
*
|
|
37
|
-
* [LAW:dataflow-not-control-flow] The easing is looked up by name and applied
|
|
38
|
-
* unconditionally; `at` runs the same code for a gradient and a cascade.
|
|
39
|
-
*/
|
|
40
|
-
export declare const RAMP_EASINGS: {
|
|
41
|
-
readonly linear: (t: number) => number;
|
|
42
|
-
readonly step: () => number;
|
|
43
|
-
};
|
|
44
|
-
export type RampEasing = keyof typeof RAMP_EASINGS;
|
|
45
|
-
export declare const RAMP_EASING_NAMES: readonly RampEasing[];
|
|
46
|
-
/**
|
|
47
|
-
* The gate a spelled easing crosses. [LAW:parse-dont-validate] — returns the
|
|
48
|
-
* narrowed name, so `ColorRamp` never re-checks. Unknown names throw naming
|
|
49
|
-
* every legal one. [LAW:no-silent-failure]
|
|
50
|
-
*/
|
|
51
|
-
export declare function parseRampEasing(name: string): RampEasing;
|
|
52
34
|
/**
|
|
53
35
|
* An immutable ramp over stops sorted by position.
|
|
54
36
|
*
|
|
@@ -65,19 +47,24 @@ export declare function parseRampEasing(name: string): RampEasing;
|
|
|
65
47
|
* same way).
|
|
66
48
|
*/
|
|
67
49
|
export declare class ColorRamp {
|
|
68
|
-
readonly
|
|
50
|
+
readonly ease: Ease;
|
|
69
51
|
readonly stops: readonly ColorStop[];
|
|
70
|
-
constructor(
|
|
52
|
+
constructor(ease: Ease, stops: readonly ColorStop[]);
|
|
71
53
|
/**
|
|
72
54
|
* The color at `value`. Below the first stop it is the first color; at or
|
|
73
|
-
* above the last stop it is the last; between two stops
|
|
74
|
-
*
|
|
75
|
-
*
|
|
55
|
+
* above the last stop it is the last; between two stops the raw progress
|
|
56
|
+
* from the lower to the upper goes through `ease`, and the eased progress
|
|
57
|
+
* says how far to mix.
|
|
76
58
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* paint a color the author
|
|
59
|
+
* Eased progress 0 is the lower stop's color and 1 the upper's, byte for
|
|
60
|
+
* byte — the exactness contract, not a shortcut: the sRGB → OKLCH → sRGB
|
|
61
|
+
* round-trip can land a channel one unit off, and a ramp that did not hit
|
|
62
|
+
* its own stops exactly would make a `step` ramp paint a color the author
|
|
63
|
+
* never wrote. So a value on a stop is that stop's color whenever the ease
|
|
64
|
+
* starts at 0, as every built-in does but `step-start` and the
|
|
65
|
+
* `jump-start`/`jump-both` steps, which jump at the start of the interval as
|
|
66
|
+
* CSS defines them. An ease that overshoots [0, 1] holds the end colors
|
|
67
|
+
* where it does: a ramp never paints outside its stops.
|
|
81
68
|
*/
|
|
82
69
|
at(value: number): ColorRgba;
|
|
83
70
|
}
|
package/dist/themes/ramp.js
CHANGED
|
@@ -9,46 +9,22 @@
|
|
|
9
9
|
* can no longer transpose with the palette it was meant to belong to. A ramp
|
|
10
10
|
* is the one function whose input is a number, so the decision stays inside.
|
|
11
11
|
*
|
|
12
|
-
* A threshold cascade is a ramp too: the same stops with
|
|
12
|
+
* A threshold cascade is a ramp too: the same stops with the `step` ease hold
|
|
13
13
|
* each color until the next position, which is exactly `≥ threshold → hotter`
|
|
14
14
|
* written as data. [LAW:one-type-per-behavior] One primitive; the easing is a
|
|
15
|
-
* value
|
|
16
|
-
* was
|
|
15
|
+
* value — any `Ease` from `core/easing`, the vocabulary every effect shares —
|
|
16
|
+
* so a gradient and a cascade differ by one word, not by which function was
|
|
17
|
+
* called. [LAW:dataflow-not-control-flow] `at` applies it unconditionally.
|
|
17
18
|
*
|
|
18
19
|
* Interpolation is in OKLCH (`Oklch.mix`), so a `linear` ramp between two
|
|
19
20
|
* theme colors passes through perceptually even steps rather than the muddy
|
|
20
21
|
* midpoints of an sRGB blend.
|
|
21
22
|
*
|
|
22
|
-
* [LAW:one-way-deps] Imports `core/color` and `core/oklch`
|
|
23
|
-
* knows what a palette is. Resolving stop *names* is the template binding's
|
|
23
|
+
* [LAW:one-way-deps] Imports `core/color`, `core/easing` and `core/oklch`
|
|
24
|
+
* only; nothing here knows what a palette is. Resolving stop *names* is the template binding's
|
|
24
25
|
* job (`paletteFuncs`), which hands this module resolved colors.
|
|
25
26
|
*/
|
|
26
27
|
import { Oklch } from "../core/oklch.js";
|
|
27
|
-
/**
|
|
28
|
-
* How a value between two stops maps to progress along them. Each easing is
|
|
29
|
-
* a function on segment progress `t ∈ [0, 1)` — `linear` keeps it, `step`
|
|
30
|
-
* holds the left stop for the whole segment.
|
|
31
|
-
*
|
|
32
|
-
* [LAW:dataflow-not-control-flow] The easing is looked up by name and applied
|
|
33
|
-
* unconditionally; `at` runs the same code for a gradient and a cascade.
|
|
34
|
-
*/
|
|
35
|
-
export const RAMP_EASINGS = {
|
|
36
|
-
linear: (t) => t,
|
|
37
|
-
step: () => 0,
|
|
38
|
-
};
|
|
39
|
-
export const RAMP_EASING_NAMES = Object.keys(RAMP_EASINGS);
|
|
40
|
-
/**
|
|
41
|
-
* The gate a spelled easing crosses. [LAW:parse-dont-validate] — returns the
|
|
42
|
-
* narrowed name, so `ColorRamp` never re-checks. Unknown names throw naming
|
|
43
|
-
* every legal one. [LAW:no-silent-failure]
|
|
44
|
-
*/
|
|
45
|
-
export function parseRampEasing(name) {
|
|
46
|
-
if (!Object.hasOwn(RAMP_EASINGS, name)) {
|
|
47
|
-
throw new RangeError(`unknown ramp easing ${JSON.stringify(name)}; expected one of ` +
|
|
48
|
-
RAMP_EASING_NAMES.map((n) => JSON.stringify(n)).join(", "));
|
|
49
|
-
}
|
|
50
|
-
return name;
|
|
51
|
-
}
|
|
52
28
|
/**
|
|
53
29
|
* An immutable ramp over stops sorted by position.
|
|
54
30
|
*
|
|
@@ -65,10 +41,10 @@ export function parseRampEasing(name) {
|
|
|
65
41
|
* same way).
|
|
66
42
|
*/
|
|
67
43
|
export class ColorRamp {
|
|
68
|
-
|
|
44
|
+
ease;
|
|
69
45
|
stops;
|
|
70
|
-
constructor(
|
|
71
|
-
this.
|
|
46
|
+
constructor(ease, stops) {
|
|
47
|
+
this.ease = ease;
|
|
72
48
|
this.stops = stops;
|
|
73
49
|
if (stops.length === 0) {
|
|
74
50
|
throw new RangeError("a ColorRamp needs at least one stop");
|
|
@@ -86,14 +62,19 @@ export class ColorRamp {
|
|
|
86
62
|
}
|
|
87
63
|
/**
|
|
88
64
|
* The color at `value`. Below the first stop it is the first color; at or
|
|
89
|
-
* above the last stop it is the last; between two stops
|
|
90
|
-
*
|
|
91
|
-
*
|
|
65
|
+
* above the last stop it is the last; between two stops the raw progress
|
|
66
|
+
* from the lower to the upper goes through `ease`, and the eased progress
|
|
67
|
+
* says how far to mix.
|
|
92
68
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* paint a color the author
|
|
69
|
+
* Eased progress 0 is the lower stop's color and 1 the upper's, byte for
|
|
70
|
+
* byte — the exactness contract, not a shortcut: the sRGB → OKLCH → sRGB
|
|
71
|
+
* round-trip can land a channel one unit off, and a ramp that did not hit
|
|
72
|
+
* its own stops exactly would make a `step` ramp paint a color the author
|
|
73
|
+
* never wrote. So a value on a stop is that stop's color whenever the ease
|
|
74
|
+
* starts at 0, as every built-in does but `step-start` and the
|
|
75
|
+
* `jump-start`/`jump-both` steps, which jump at the start of the interval as
|
|
76
|
+
* CSS defines them. An ease that overshoots [0, 1] holds the end colors
|
|
77
|
+
* where it does: a ramp never paints outside its stops.
|
|
97
78
|
*/
|
|
98
79
|
at(value) {
|
|
99
80
|
if (!Number.isFinite(value)) {
|
|
@@ -110,9 +91,9 @@ export class ColorRamp {
|
|
|
110
91
|
if (lower === stops.length - 1)
|
|
111
92
|
return from.color;
|
|
112
93
|
// `to.at > value >= from.at` by construction of `lower`, so the segment
|
|
113
|
-
// has positive width and
|
|
94
|
+
// has positive width and raw progress is in [0, 1).
|
|
114
95
|
const to = stops[lower + 1];
|
|
115
|
-
const t =
|
|
96
|
+
const t = this.ease((value - from.at) / (to.at - from.at));
|
|
116
97
|
if (t <= 0)
|
|
117
98
|
return from.color;
|
|
118
99
|
if (t >= 1)
|