@promptctl/rich-js 0.18.0 → 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/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +145 -0
- package/dist/core/box.js +372 -0
- package/dist/core/cells.d.ts +192 -0
- package/dist/core/cells.js +334 -0
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +326 -0
- package/dist/core/color.js +1225 -0
- package/dist/core/console.d.ts +196 -0
- package/dist/core/console.js +596 -0
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +197 -0
- package/dist/core/export-lines.d.ts +130 -0
- package/dist/core/export-lines.js +159 -0
- 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 +33 -0
- package/dist/core/highlighter.js +190 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +140 -0
- package/dist/core/markup.js +654 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +67 -0
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/oklch.d.ts +161 -0
- package/dist/core/oklch.js +312 -0
- package/dist/core/osc8.d.ts +68 -0
- package/dist/core/osc8.js +78 -0
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +49 -0
- package/dist/core/pretty.d.ts +247 -0
- package/dist/core/pretty.js +906 -0
- package/dist/core/protocol.d.ts +305 -0
- package/dist/core/protocol.js +183 -0
- package/dist/core/render.d.ts +87 -0
- package/dist/core/render.js +105 -0
- package/dist/core/segment.d.ts +162 -0
- package/dist/core/segment.js +488 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +152 -0
- package/dist/core/strip.js +326 -0
- package/dist/core/style.d.ts +189 -0
- package/dist/core/style.js +758 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +283 -0
- package/dist/core/text.js +1097 -0
- package/dist/core/wrap.d.ts +46 -0
- package/dist/core/wrap.js +140 -0
- package/dist/host/app.d.ts +133 -0
- package/dist/host/app.js +285 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +180 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +114 -0
- package/dist/node/prompt.d.ts +34 -0
- package/dist/node/prompt.js +51 -0
- package/dist/node/save.d.ts +18 -0
- package/dist/node/save.js +24 -0
- package/dist/node/terminal-host.d.ts +114 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +38 -0
- package/dist/node/traceback.js +98 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +26 -0
- package/dist/renderables/columns.d.ts +77 -0
- package/dist/renderables/columns.js +191 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +31 -0
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +107 -0
- package/dist/renderables/embed.js +160 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +14 -0
- package/dist/renderables/group.js +22 -0
- package/dist/renderables/layout.d.ts +75 -0
- package/dist/renderables/layout.js +223 -0
- package/dist/renderables/live.d.ts +74 -0
- package/dist/renderables/live.js +188 -0
- package/dist/renderables/markdown.d.ts +38 -0
- package/dist/renderables/markdown.js +587 -0
- package/dist/renderables/padding.d.ts +59 -0
- package/dist/renderables/padding.js +147 -0
- package/dist/renderables/panel.d.ts +114 -0
- package/dist/renderables/panel.js +303 -0
- package/dist/renderables/progress.d.ts +178 -0
- package/dist/renderables/progress.js +362 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +64 -0
- package/dist/renderables/prompt.d.ts +71 -0
- package/dist/renderables/prompt.js +165 -0
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +26 -0
- package/dist/renderables/rule.js +86 -0
- package/dist/renderables/spinner.d.ts +57 -0
- package/dist/renderables/spinner.js +103 -0
- package/dist/renderables/status.d.ts +29 -0
- package/dist/renderables/status.js +46 -0
- package/dist/renderables/syntax.d.ts +65 -0
- package/dist/renderables/syntax.js +166 -0
- package/dist/renderables/table.d.ts +232 -0
- package/dist/renderables/table.js +973 -0
- package/dist/renderables/traceback.d.ts +32 -0
- package/dist/renderables/traceback.js +279 -0
- package/dist/renderables/tree.d.ts +76 -0
- package/dist/renderables/tree.js +177 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +196 -0
- package/dist/template-bindings/color-funcs.d.ts +109 -0
- package/dist/template-bindings/color-funcs.js +243 -0
- package/dist/template-bindings/helpers.d.ts +26 -0
- package/dist/template-bindings/helpers.js +33 -0
- package/dist/template-bindings/index.d.ts +119 -0
- package/dist/template-bindings/index.js +162 -0
- package/dist/template-bindings/palette-funcs.d.ts +96 -0
- package/dist/template-bindings/palette-funcs.js +145 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +157 -0
- package/dist/themes/buildPalette.d.ts +57 -0
- package/dist/themes/buildPalette.js +100 -0
- package/dist/themes/colorMath.d.ts +94 -0
- package/dist/themes/colorMath.js +280 -0
- package/dist/themes/colorRef.d.ts +36 -0
- package/dist/themes/colorRef.js +78 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.d.ts +3 -0
- package/dist/themes/data/atom-one-dark.js +177 -0
- package/dist/themes/data/atom-one-light.d.ts +3 -0
- package/dist/themes/data/atom-one-light.js +177 -0
- package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
- package/dist/themes/data/catppuccin-frappe.js +177 -0
- package/dist/themes/data/catppuccin-latte.d.ts +3 -0
- package/dist/themes/data/catppuccin-latte.js +177 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
- package/dist/themes/data/catppuccin-macchiato.js +177 -0
- package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
- package/dist/themes/data/catppuccin-mocha.js +177 -0
- package/dist/themes/data/cyberpunk.d.ts +17 -0
- package/dist/themes/data/cyberpunk.js +193 -0
- package/dist/themes/data/default.d.ts +3 -0
- package/dist/themes/data/default.js +160 -0
- package/dist/themes/data/dracula.d.ts +3 -0
- package/dist/themes/data/dracula.js +177 -0
- package/dist/themes/data/flexoki.d.ts +3 -0
- package/dist/themes/data/flexoki.js +177 -0
- package/dist/themes/data/gruvbox.d.ts +3 -0
- package/dist/themes/data/gruvbox.js +177 -0
- package/dist/themes/data/index.d.ts +35 -0
- package/dist/themes/data/index.js +64 -0
- package/dist/themes/data/monokai.d.ts +3 -0
- package/dist/themes/data/monokai.js +177 -0
- package/dist/themes/data/nord.d.ts +3 -0
- package/dist/themes/data/nord.js +177 -0
- package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
- package/dist/themes/data/rose-pine-dawn.js +177 -0
- package/dist/themes/data/rose-pine-moon.d.ts +3 -0
- package/dist/themes/data/rose-pine-moon.js +177 -0
- package/dist/themes/data/rose-pine.d.ts +3 -0
- package/dist/themes/data/rose-pine.js +177 -0
- package/dist/themes/data/solarized-dark.d.ts +3 -0
- package/dist/themes/data/solarized-dark.js +177 -0
- package/dist/themes/data/solarized-light.d.ts +3 -0
- package/dist/themes/data/solarized-light.js +177 -0
- package/dist/themes/data/svg-export.d.ts +3 -0
- package/dist/themes/data/svg-export.js +177 -0
- package/dist/themes/data/textual-ansi.d.ts +3 -0
- package/dist/themes/data/textual-ansi.js +160 -0
- package/dist/themes/data/textual-dark.d.ts +3 -0
- package/dist/themes/data/textual-dark.js +160 -0
- package/dist/themes/data/textual-light.d.ts +3 -0
- package/dist/themes/data/textual-light.js +160 -0
- package/dist/themes/data/tokyo-night.d.ts +3 -0
- package/dist/themes/data/tokyo-night.js +177 -0
- package/dist/themes/data/types.d.ts +31 -0
- package/dist/themes/data/types.js +10 -0
- package/dist/themes/palette.d.ts +45 -0
- package/dist/themes/palette.js +71 -0
- package/dist/themes/ramp.d.ts +70 -0
- package/dist/themes/ramp.js +103 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +105 -0
- package/dist/themes/terminalThemes.d.ts +49 -0
- package/dist/themes/terminalThemes.js +63 -0
- package/dist/themes/transpose.d.ts +87 -0
- package/dist/themes/transpose.js +138 -0
- package/dist/widgets/button.d.ts +45 -0
- package/dist/widgets/button.js +177 -0
- package/dist/widgets/checkbox.d.ts +38 -0
- package/dist/widgets/checkbox.js +135 -0
- package/dist/widgets/dropdown.d.ts +64 -0
- package/dist/widgets/dropdown.js +416 -0
- package/dist/widgets/event-router.d.ts +71 -0
- package/dist/widgets/event-router.js +629 -0
- package/dist/widgets/focus-manager.d.ts +45 -0
- package/dist/widgets/focus-manager.js +191 -0
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +14 -0
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +54 -0
- package/dist/widgets/slider.js +247 -0
- package/dist/widgets/static-item.d.ts +34 -0
- package/dist/widgets/static-item.js +54 -0
- package/dist/widgets/text-input.d.ts +283 -0
- package/dist/widgets/text-input.js +1174 -0
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +41 -0
- package/dist/widgets/toggle.js +157 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +65 -0
- package/dist/widgets/widget-app.js +206 -0
- package/dist/widgets/widget-base.d.ts +42 -0
- package/dist/widgets/widget-base.js +172 -0
- package/package.json +16 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measurement — min/max cell width calculation for renderables.
|
|
3
|
+
*/
|
|
4
|
+
import { isMeasurable, measuring } from "./protocol.js";
|
|
5
|
+
export class Measurement {
|
|
6
|
+
minimum;
|
|
7
|
+
maximum;
|
|
8
|
+
constructor(minimum, maximum) {
|
|
9
|
+
this.minimum = minimum;
|
|
10
|
+
this.maximum = maximum;
|
|
11
|
+
}
|
|
12
|
+
get span() {
|
|
13
|
+
return this.maximum - this.minimum;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A range with no cell meaning becomes one with the nearest meaning there is:
|
|
17
|
+
* negative floors to zero, an inverted pair collapses to its ceiling, and NaN
|
|
18
|
+
* reads as zero cells the same way `cellCount` reads it.
|
|
19
|
+
*
|
|
20
|
+
* NaN and Infinity are not treated alike here, and the difference is the whole
|
|
21
|
+
* point. `Infinity` is a maximum a renderable means — `Table` reports it when
|
|
22
|
+
* a column asks for every cell there is, and `withBoundedWidth` throws on it
|
|
23
|
+
* so a caller learns their offer was unanswerable. Flooring it to zero would
|
|
24
|
+
* turn that loud failure into a table measured at no width at all.
|
|
25
|
+
*/
|
|
26
|
+
normalize() {
|
|
27
|
+
const cells = (n) => (Number.isNaN(n) ? 0 : n);
|
|
28
|
+
const min = Math.max(0, Math.min(cells(this.minimum), cells(this.maximum)));
|
|
29
|
+
const max = Math.max(0, cells(this.maximum));
|
|
30
|
+
return new Measurement(min, max);
|
|
31
|
+
}
|
|
32
|
+
withMaximum(width) {
|
|
33
|
+
return new Measurement(Math.min(this.minimum, width), Math.min(this.maximum, width));
|
|
34
|
+
}
|
|
35
|
+
withMinimum(width) {
|
|
36
|
+
const min = Math.max(this.minimum, width);
|
|
37
|
+
const max = Math.max(this.maximum, min);
|
|
38
|
+
return new Measurement(min, max);
|
|
39
|
+
}
|
|
40
|
+
clamp(minWidth, maxWidth) {
|
|
41
|
+
return new Measurement(Math.min(Math.max(this.minimum, minWidth), maxWidth), Math.min(Math.max(this.maximum, minWidth), maxWidth));
|
|
42
|
+
}
|
|
43
|
+
// [LAW:single-enforcer] Single entry point for measuring a renderable. One
|
|
44
|
+
// that cannot measure itself asks for the whole offer, as the reference's
|
|
45
|
+
// `Measurement.get` answers for it — so under an unbounded offer it asks for
|
|
46
|
+
// `Infinity`, and `withBoundedWidth` refuses rather than guessing a width.
|
|
47
|
+
static get(options, renderable) {
|
|
48
|
+
if (options.maxWidth < 1)
|
|
49
|
+
return new Measurement(0, 0);
|
|
50
|
+
if (!isMeasurable(renderable))
|
|
51
|
+
return new Measurement(0, options.maxWidth);
|
|
52
|
+
const { minimum, maximum } = renderable.measure(measuring(options));
|
|
53
|
+
return new Measurement(minimum, Math.min(maximum, options.maxWidth)).normalize();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function measureRenderables(options, renderables) {
|
|
57
|
+
if (renderables.length === 0)
|
|
58
|
+
return new Measurement(0, 0);
|
|
59
|
+
let minOfAll = 0;
|
|
60
|
+
let maxOfAll = 0;
|
|
61
|
+
for (const m of renderables) {
|
|
62
|
+
const measurement = Measurement.get(options, m);
|
|
63
|
+
minOfAll = Math.max(minOfAll, measurement.minimum);
|
|
64
|
+
maxOfAll = Math.max(maxOfAll, measurement.maximum);
|
|
65
|
+
}
|
|
66
|
+
return new Measurement(minOfAll, maxOfAll);
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memo — a string-keyed memo with a size bound: the one cache shape for a
|
|
3
|
+
* pure computation whose keys a long-running host derives without end. A
|
|
4
|
+
* statusline daemon redrawing a time-varying colour parses a new hex string,
|
|
5
|
+
* matches a new RGB triple, measures a new cell, on every frame; a memo that
|
|
6
|
+
* never evicts grows by one entry per frame for the life of the process.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MEMO_MAX = 4096;
|
|
9
|
+
/**
|
|
10
|
+
* `get(key, compute)` returns the value remembered for `key`, computing and
|
|
11
|
+
* remembering it on a miss. Holds at most `MEMO_MAX` entries. `compute` must
|
|
12
|
+
* be a pure function of `key`, so a value recomputed after a clear is the one
|
|
13
|
+
* that was dropped.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Memo<V extends NonNullable<unknown>> {
|
|
16
|
+
private readonly entries;
|
|
17
|
+
get size(): number;
|
|
18
|
+
get(key: string, compute: (key: string) => V): V;
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memo — a string-keyed memo with a size bound: the one cache shape for a
|
|
3
|
+
* pure computation whose keys a long-running host derives without end. A
|
|
4
|
+
* statusline daemon redrawing a time-varying colour parses a new hex string,
|
|
5
|
+
* matches a new RGB triple, measures a new cell, on every frame; a memo that
|
|
6
|
+
* never evicts grows by one entry per frame for the life of the process.
|
|
7
|
+
*/
|
|
8
|
+
// [LAW:single-enforcer] The one size policy for every memo in the library.
|
|
9
|
+
// Clearing at the cap rather than evicting least-recently-used: a refill costs
|
|
10
|
+
// one recomputation per key, and a clear needs no bookkeeping on the hit path.
|
|
11
|
+
export const MEMO_MAX = 4096;
|
|
12
|
+
/**
|
|
13
|
+
* `get(key, compute)` returns the value remembered for `key`, computing and
|
|
14
|
+
* remembering it on a miss. Holds at most `MEMO_MAX` entries. `compute` must
|
|
15
|
+
* be a pure function of `key`, so a value recomputed after a clear is the one
|
|
16
|
+
* that was dropped.
|
|
17
|
+
*/
|
|
18
|
+
export class Memo {
|
|
19
|
+
entries = new Map();
|
|
20
|
+
get size() {
|
|
21
|
+
return this.entries.size;
|
|
22
|
+
}
|
|
23
|
+
get(key, compute) {
|
|
24
|
+
const remembered = this.entries.get(key);
|
|
25
|
+
if (remembered !== undefined)
|
|
26
|
+
return remembered;
|
|
27
|
+
// Computed before the clear, so a compute that throws costs no entries.
|
|
28
|
+
const value = compute(key);
|
|
29
|
+
if (this.entries.size >= MEMO_MAX)
|
|
30
|
+
this.entries.clear();
|
|
31
|
+
this.entries.set(key, value);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKLCH color space — perceptually-uniform polar form of OKLab.
|
|
3
|
+
*
|
|
4
|
+
* Math from Björn Ottosson's OKLab specification:
|
|
5
|
+
* https://bottosson.github.io/posts/oklab/
|
|
6
|
+
*
|
|
7
|
+
* The full pipeline:
|
|
8
|
+
* sRGB (0..255 int) ↔ sRGB linear (0..1 float) ↔ OKLab ↔ OKLCH polar
|
|
9
|
+
* `fromRgba` walks left→right, `toRgba` walks right→left. The only lossy
|
|
10
|
+
* step is the final 0..255 quantization; the float math itself is
|
|
11
|
+
* reversible to within a few ULP.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:one-type-per-behavior] OKLCH and sRGB are different *behaviors*:
|
|
14
|
+
* sRGB is the wire format terminals consume (what ANSI escape codes
|
|
15
|
+
* carry); OKLCH is the perceptual manipulation space (where equal
|
|
16
|
+
* numerical deltas mean equal perceptual deltas, which is the property
|
|
17
|
+
* "transposition" requires). Two types, mutual conversion, no hybrid.
|
|
18
|
+
*
|
|
19
|
+
* [LAW:one-way-deps] This module imports `ColorRgba` from core/color and
|
|
20
|
+
* nothing else from inside the project. Nothing in core/ depends back on
|
|
21
|
+
* this file.
|
|
22
|
+
*/
|
|
23
|
+
import { ColorRgba } from "./color.js";
|
|
24
|
+
/**
|
|
25
|
+
* A perceptually-uniform color transform.
|
|
26
|
+
*
|
|
27
|
+
* [LAW:dataflow-not-control-flow] Identity is *data* (`IDENTITY`), not a
|
|
28
|
+
* special case. Every transposition runs the same code path — the values
|
|
29
|
+
* decide whether the operation is a no-op or a 180° rotation.
|
|
30
|
+
*
|
|
31
|
+
* Lightness is parameterized as `L' = clamp01(L * scale + shift)` to make
|
|
32
|
+
* "invert" expressible as `{scale: -1, shift: 1}` without a boolean knob.
|
|
33
|
+
*/
|
|
34
|
+
export interface ThemeKey {
|
|
35
|
+
/** Hue offset in degrees; positive rotates toward warmer→cooler→back. */
|
|
36
|
+
readonly hueShift: number;
|
|
37
|
+
/** Multiplier on chroma. 0 = grayscale, 1 = identity, >1 = more saturated. */
|
|
38
|
+
readonly chromaScale: number;
|
|
39
|
+
/** Multiplier on lightness. 1 = identity, -1 = invert around the L axis. */
|
|
40
|
+
readonly lightnessScale: number;
|
|
41
|
+
/** Additive on lightness; applied *after* the scale. */
|
|
42
|
+
readonly lightnessShift: number;
|
|
43
|
+
}
|
|
44
|
+
/** How far `Oklch.mixAxes` moves each axis toward its target, each in [0, 1]. */
|
|
45
|
+
export interface OklchWeights {
|
|
46
|
+
readonly l: number;
|
|
47
|
+
readonly c: number;
|
|
48
|
+
readonly h: number;
|
|
49
|
+
readonly alpha: number;
|
|
50
|
+
}
|
|
51
|
+
export declare const IDENTITY: ThemeKey;
|
|
52
|
+
/**
|
|
53
|
+
* Flip lightness around the midpoint (`L' = 1 - L`) with hue and chroma
|
|
54
|
+
* untouched. Combined with the anchor logic in `transposePalette`, this
|
|
55
|
+
* converts a dark theme to its light "octave": semantic anchors
|
|
56
|
+
* (error/success/warning) still lightness-invert — only their *hue* is
|
|
57
|
+
* preserved — so errors stay red and dark-on-light becomes light-on-dark.
|
|
58
|
+
*/
|
|
59
|
+
export declare const INVERT_LIGHTNESS: ThemeKey;
|
|
60
|
+
/**
|
|
61
|
+
* Whether a key is the no-op transform. Exported so that callers
|
|
62
|
+
* (`transposePalette`) can fast-path byte-exact identity without going
|
|
63
|
+
* through the lossy sRGB↔OKLCH round-trip. [LAW:one-source-of-truth] —
|
|
64
|
+
* the predicate lives once, here.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isIdentityKey(k: ThemeKey): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Immutable OKLCH color. `l` ∈ [0, 1] (lightness), `c` ≥ 0 (chroma; typical
|
|
69
|
+
* sRGB-representable colors top out near 0.32), `h` ∈ [0, 360) (hue
|
|
70
|
+
* degrees), `alpha` ∈ [0, 1].
|
|
71
|
+
*
|
|
72
|
+
* [LAW:single-enforcer] Constructor is the only validation site, matching
|
|
73
|
+
* the `ColorRgba` pattern. Out-of-range `alpha` throws. l/c/h must be
|
|
74
|
+
* finite but are *not* clamped at construction — they're clamped during
|
|
75
|
+
* `toRgba` instead, so transforms can pass through intermediate
|
|
76
|
+
* representations (e.g. h = 720°, c = -0.1 mid-pipeline) before the final
|
|
77
|
+
* sRGB-quantization step normalizes them.
|
|
78
|
+
*/
|
|
79
|
+
export declare class Oklch {
|
|
80
|
+
#private;
|
|
81
|
+
readonly l: number;
|
|
82
|
+
readonly c: number;
|
|
83
|
+
readonly h: number;
|
|
84
|
+
readonly alpha: number;
|
|
85
|
+
constructor(l: number, c: number, h: number, alpha?: number);
|
|
86
|
+
/** sRGB → linear → OKLab → polar. Pure; no I/O, no allocation beyond the result. */
|
|
87
|
+
static fromRgba(color: ColorRgba): Oklch;
|
|
88
|
+
/**
|
|
89
|
+
* Polar → OKLab → linear → sRGB (with chroma-bisection gamut clamping).
|
|
90
|
+
*
|
|
91
|
+
* [LAW:single-enforcer] All sRGB-gamut clamping for OKLCH happens here.
|
|
92
|
+
* Callers don't see intermediate float channels; they get a valid
|
|
93
|
+
* `ColorRgba` or a thrown error.
|
|
94
|
+
*/
|
|
95
|
+
toRgba(): ColorRgba;
|
|
96
|
+
/**
|
|
97
|
+
* Apply a `ThemeKey`. Returns a new `Oklch`; pure.
|
|
98
|
+
*
|
|
99
|
+
* [LAW:dataflow-not-control-flow] Identity short-circuits to `this` —
|
|
100
|
+
* same shape as `ColorRgba.compositeOver(bg)` returning `this` when
|
|
101
|
+
* `alpha === 1`. Callers invoke unconditionally; the data decides.
|
|
102
|
+
*/
|
|
103
|
+
applyKey(k: ThemeKey): Oklch;
|
|
104
|
+
/**
|
|
105
|
+
* The color `t` of the way from this one toward `toward`, in OKLCH.
|
|
106
|
+
* `t = 0` is this color, `t = 1` is `toward`; pure.
|
|
107
|
+
*
|
|
108
|
+
* Lightness, chroma and alpha interpolate linearly. Hue takes the shorter
|
|
109
|
+
* arc around the wheel, so blue → red passes through magenta rather than
|
|
110
|
+
* sweeping across green. An achromatic endpoint has no hue of its own —
|
|
111
|
+
* `fromRgba` pins it to 0, which is red — so it adopts the other
|
|
112
|
+
* endpoint's hue (CSS Color 4's "powerless" rule): gray → red stays a red
|
|
113
|
+
* that gains chroma, instead of rotating through the wheel from 0°.
|
|
114
|
+
*
|
|
115
|
+
* `t` must lie in [0, 1]: this is interpolation, not extrapolation, and a
|
|
116
|
+
* `t` outside it (NaN included) is a RangeError, never a colour off the
|
|
117
|
+
* far end of the segment. [LAW:no-silent-failure]
|
|
118
|
+
*
|
|
119
|
+
* [LAW:one-source-of-truth] Achromatic means the same `ACHROMATIC_EPS`
|
|
120
|
+
* that `fromRgba` and `applyKey` pin hue by, so a gray produced by either
|
|
121
|
+
* is a gray here — and the result's hue goes through the same `hueOf`, so
|
|
122
|
+
* a gray produced HERE (t = 0 from a gray, t = 1 toward one) is a gray
|
|
123
|
+
* there too.
|
|
124
|
+
*/
|
|
125
|
+
mix(toward: Oklch, t: number): Oklch;
|
|
126
|
+
/**
|
|
127
|
+
* `mix` with each axis moved its own share of the way: `weights.l` of the
|
|
128
|
+
* lightness gap, `weights.c` of the chroma gap, and so on, each in [0, 1].
|
|
129
|
+
* `mix(toward, t)` is `mixAxes` with every weight `t` — one interpolation,
|
|
130
|
+
* so the shorter-arc hue and the powerless-endpoint rule are the same
|
|
131
|
+
* whichever is called.
|
|
132
|
+
*
|
|
133
|
+
* It exists because perceptual axes are independent: a tint can take most of
|
|
134
|
+
* a hue's colourfulness while keeping close to the lightness it started at,
|
|
135
|
+
* which a single `t` cannot say — raising `t` for chroma drags lightness with
|
|
136
|
+
* it. Every weight is required: an axis left out would need a default, and
|
|
137
|
+
* "unchanged" (0) and "same as the others" are both plausible readings.
|
|
138
|
+
*
|
|
139
|
+
* A weak `h` beside a strong `c` shows the starting colour's hue at high
|
|
140
|
+
* chroma. The powerless rule covers only a truly achromatic start, so a
|
|
141
|
+
* near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
|
|
142
|
+
* noise in proportion. To land on the target's hue, pass `h: 1`.
|
|
143
|
+
*/
|
|
144
|
+
mixAxes(toward: Oklch, weights: OklchWeights): Oklch;
|
|
145
|
+
/**
|
|
146
|
+
* ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
|
|
147
|
+
* (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
|
|
148
|
+
* resolves. Alpha is not a coordinate of the space and does not count.
|
|
149
|
+
* Symmetric and pure.
|
|
150
|
+
*/
|
|
151
|
+
deltaE(other: Oklch): number;
|
|
152
|
+
/** Linear-sRGB coordinates for an explicit (l, C, h). Pure; `toRgba` passes
|
|
153
|
+
* already-normalized values so this never sees out-of-range inputs. */
|
|
154
|
+
private toLinearRgb;
|
|
155
|
+
/**
|
|
156
|
+
* Largest chroma ≤ `c` whose (l, h) projection lies in sRGB.
|
|
157
|
+
* Standard chroma-reduction-by-bisection: 24 iterations gives ~1e-7
|
|
158
|
+
* precision, more than enough since the next step quantizes to 1/255.
|
|
159
|
+
*/
|
|
160
|
+
private findInGamutChroma;
|
|
161
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKLCH color space — perceptually-uniform polar form of OKLab.
|
|
3
|
+
*
|
|
4
|
+
* Math from Björn Ottosson's OKLab specification:
|
|
5
|
+
* https://bottosson.github.io/posts/oklab/
|
|
6
|
+
*
|
|
7
|
+
* The full pipeline:
|
|
8
|
+
* sRGB (0..255 int) ↔ sRGB linear (0..1 float) ↔ OKLab ↔ OKLCH polar
|
|
9
|
+
* `fromRgba` walks left→right, `toRgba` walks right→left. The only lossy
|
|
10
|
+
* step is the final 0..255 quantization; the float math itself is
|
|
11
|
+
* reversible to within a few ULP.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:one-type-per-behavior] OKLCH and sRGB are different *behaviors*:
|
|
14
|
+
* sRGB is the wire format terminals consume (what ANSI escape codes
|
|
15
|
+
* carry); OKLCH is the perceptual manipulation space (where equal
|
|
16
|
+
* numerical deltas mean equal perceptual deltas, which is the property
|
|
17
|
+
* "transposition" requires). Two types, mutual conversion, no hybrid.
|
|
18
|
+
*
|
|
19
|
+
* [LAW:one-way-deps] This module imports `ColorRgba` from core/color and
|
|
20
|
+
* nothing else from inside the project. Nothing in core/ depends back on
|
|
21
|
+
* this file.
|
|
22
|
+
*/
|
|
23
|
+
var _a;
|
|
24
|
+
import { ColorRgba } from "./color.js";
|
|
25
|
+
// The axes `mixAxes` checks, named once so a weights object's own keys — a
|
|
26
|
+
// missing one, or an extra field riding along — never decide what is checked.
|
|
27
|
+
const OKLCH_AXES = ["l", "c", "h", "alpha"];
|
|
28
|
+
export const IDENTITY = Object.freeze({
|
|
29
|
+
hueShift: 0,
|
|
30
|
+
chromaScale: 1,
|
|
31
|
+
lightnessScale: 1,
|
|
32
|
+
lightnessShift: 0,
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Flip lightness around the midpoint (`L' = 1 - L`) with hue and chroma
|
|
36
|
+
* untouched. Combined with the anchor logic in `transposePalette`, this
|
|
37
|
+
* converts a dark theme to its light "octave": semantic anchors
|
|
38
|
+
* (error/success/warning) still lightness-invert — only their *hue* is
|
|
39
|
+
* preserved — so errors stay red and dark-on-light becomes light-on-dark.
|
|
40
|
+
*/
|
|
41
|
+
export const INVERT_LIGHTNESS = Object.freeze({
|
|
42
|
+
hueShift: 0,
|
|
43
|
+
chromaScale: 1,
|
|
44
|
+
lightnessScale: -1,
|
|
45
|
+
lightnessShift: 1,
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Whether a key is the no-op transform. Exported so that callers
|
|
49
|
+
* (`transposePalette`) can fast-path byte-exact identity without going
|
|
50
|
+
* through the lossy sRGB↔OKLCH round-trip. [LAW:one-source-of-truth] —
|
|
51
|
+
* the predicate lives once, here.
|
|
52
|
+
*/
|
|
53
|
+
export function isIdentityKey(k) {
|
|
54
|
+
return (k.hueShift % 360 === 0 && // a whole turn (0, 360, -360, …) is a no-op
|
|
55
|
+
k.chromaScale === 1 &&
|
|
56
|
+
k.lightnessScale === 1 &&
|
|
57
|
+
k.lightnessShift === 0);
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Pure conversion helpers (private).
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
function srgbToLinear(v) {
|
|
63
|
+
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
64
|
+
}
|
|
65
|
+
function linearToSrgb(v) {
|
|
66
|
+
return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
|
|
67
|
+
}
|
|
68
|
+
function clamp01(v) {
|
|
69
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
70
|
+
}
|
|
71
|
+
// Small tolerance so float-fuzz at the gamut edge doesn't trigger needless
|
|
72
|
+
// chroma reduction on colors that are already representable.
|
|
73
|
+
const GAMUT_EPS = 1e-4;
|
|
74
|
+
// [LAW:one-source-of-truth] Below this chroma a color is achromatic and its
|
|
75
|
+
// hue is pinned to 0 so round-trips stay stable — the one threshold every
|
|
76
|
+
// chroma reader here shares.
|
|
77
|
+
const ACHROMATIC_EPS = 1e-7;
|
|
78
|
+
// The one spelling of that pin: every producer of an `Oklch` (`fromRgba`,
|
|
79
|
+
// `applyKey`, `mix`) passes its result's chroma and hue through here, so a
|
|
80
|
+
// gray from any of them carries the same hue. The constructor does not —
|
|
81
|
+
// it passes intermediates through by contract. [LAW:single-enforcer]
|
|
82
|
+
const hueOf = (c, h) => (c < ACHROMATIC_EPS ? 0 : h);
|
|
83
|
+
// Endpoint-exact: `t = 0` returns `a` and `t = 1` returns `b` bit-for-bit,
|
|
84
|
+
// which `a + (b - a) * t` does not (the subtraction rounds). One shape for
|
|
85
|
+
// every axis `mix` interpolates. [LAW:one-source-of-truth]
|
|
86
|
+
const lerp = (a, b, t) => a * (1 - t) + b * t;
|
|
87
|
+
// Hue into [0, 360). Exact on a hue already there (fmod is exact), and exact
|
|
88
|
+
// on `x - 360` for a representable x (the sum is x itself), which is what
|
|
89
|
+
// `mix` relies on. One spelling for every producer. [LAW:one-source-of-truth]
|
|
90
|
+
const wrapHue = (h) => {
|
|
91
|
+
const w = h % 360;
|
|
92
|
+
return w < 0 ? w + 360 : w;
|
|
93
|
+
};
|
|
94
|
+
function inGamut(r, g, b) {
|
|
95
|
+
return (r >= -GAMUT_EPS && r <= 1 + GAMUT_EPS &&
|
|
96
|
+
g >= -GAMUT_EPS && g <= 1 + GAMUT_EPS &&
|
|
97
|
+
b >= -GAMUT_EPS && b <= 1 + GAMUT_EPS);
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Oklch — the value type.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Immutable OKLCH color. `l` ∈ [0, 1] (lightness), `c` ≥ 0 (chroma; typical
|
|
104
|
+
* sRGB-representable colors top out near 0.32), `h` ∈ [0, 360) (hue
|
|
105
|
+
* degrees), `alpha` ∈ [0, 1].
|
|
106
|
+
*
|
|
107
|
+
* [LAW:single-enforcer] Constructor is the only validation site, matching
|
|
108
|
+
* the `ColorRgba` pattern. Out-of-range `alpha` throws. l/c/h must be
|
|
109
|
+
* finite but are *not* clamped at construction — they're clamped during
|
|
110
|
+
* `toRgba` instead, so transforms can pass through intermediate
|
|
111
|
+
* representations (e.g. h = 720°, c = -0.1 mid-pipeline) before the final
|
|
112
|
+
* sRGB-quantization step normalizes them.
|
|
113
|
+
*/
|
|
114
|
+
export class Oklch {
|
|
115
|
+
l;
|
|
116
|
+
c;
|
|
117
|
+
h;
|
|
118
|
+
alpha;
|
|
119
|
+
constructor(l, c, h, alpha = 1) {
|
|
120
|
+
this.l = l;
|
|
121
|
+
this.c = c;
|
|
122
|
+
this.h = h;
|
|
123
|
+
this.alpha = alpha;
|
|
124
|
+
if (!Number.isFinite(l))
|
|
125
|
+
throw new RangeError(`Oklch.l must be finite; got ${l}`);
|
|
126
|
+
if (!Number.isFinite(c))
|
|
127
|
+
throw new RangeError(`Oklch.c must be finite; got ${c}`);
|
|
128
|
+
if (!Number.isFinite(h))
|
|
129
|
+
throw new RangeError(`Oklch.h must be finite; got ${h}`);
|
|
130
|
+
if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) {
|
|
131
|
+
throw new RangeError(`Oklch.alpha must be a finite number in [0,1]; got ${alpha}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** sRGB → linear → OKLab → polar. Pure; no I/O, no allocation beyond the result. */
|
|
135
|
+
static fromRgba(color) {
|
|
136
|
+
const [r, g, b, a] = color.normalized;
|
|
137
|
+
const lr = srgbToLinear(r);
|
|
138
|
+
const lg = srgbToLinear(g);
|
|
139
|
+
const lb = srgbToLinear(b);
|
|
140
|
+
const lLong = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
|
|
141
|
+
const mLong = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
|
|
142
|
+
const sLong = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
|
|
143
|
+
const lCube = Math.cbrt(lLong);
|
|
144
|
+
const mCube = Math.cbrt(mLong);
|
|
145
|
+
const sCube = Math.cbrt(sLong);
|
|
146
|
+
const L = 0.2104542553 * lCube + 0.7936177850 * mCube - 0.0040720468 * sCube;
|
|
147
|
+
const aLab = 1.9779984951 * lCube - 2.4285922050 * mCube + 0.4505937099 * sCube;
|
|
148
|
+
const bLab = 0.0259040371 * lCube + 0.7827717662 * mCube - 0.8086757660 * sCube;
|
|
149
|
+
const C = Math.sqrt(aLab * aLab + bLab * bLab);
|
|
150
|
+
const H = wrapHue(Math.atan2(bLab, aLab) * (180 / Math.PI));
|
|
151
|
+
return new _a(L, C, hueOf(C, H), a);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Polar → OKLab → linear → sRGB (with chroma-bisection gamut clamping).
|
|
155
|
+
*
|
|
156
|
+
* [LAW:single-enforcer] All sRGB-gamut clamping for OKLCH happens here.
|
|
157
|
+
* Callers don't see intermediate float channels; they get a valid
|
|
158
|
+
* `ColorRgba` or a thrown error.
|
|
159
|
+
*/
|
|
160
|
+
toRgba() {
|
|
161
|
+
// The normalization boundary the constructor promises: clamp L into [0,1]
|
|
162
|
+
// and C to non-negative here, so out-of-range intermediates (e.g. c = -0.1
|
|
163
|
+
// from a mid-pipeline transform) canonicalize to a valid color rather than
|
|
164
|
+
// flipping hue through a negative chroma. H needs no wrap — the cos/sin in
|
|
165
|
+
// the conversion are periodic. [LAW:single-enforcer]
|
|
166
|
+
const l = clamp01(this.l);
|
|
167
|
+
const c = Math.max(0, this.c);
|
|
168
|
+
const cInGamut = this.findInGamutChroma(l, c, this.h);
|
|
169
|
+
const { r, g, b } = this.toLinearRgb(l, cInGamut, this.h);
|
|
170
|
+
const sR = Math.round(clamp01(linearToSrgb(r)) * 255);
|
|
171
|
+
const sG = Math.round(clamp01(linearToSrgb(g)) * 255);
|
|
172
|
+
const sB = Math.round(clamp01(linearToSrgb(b)) * 255);
|
|
173
|
+
return new ColorRgba(sR, sG, sB, this.alpha);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Apply a `ThemeKey`. Returns a new `Oklch`; pure.
|
|
177
|
+
*
|
|
178
|
+
* [LAW:dataflow-not-control-flow] Identity short-circuits to `this` —
|
|
179
|
+
* same shape as `ColorRgba.compositeOver(bg)` returning `this` when
|
|
180
|
+
* `alpha === 1`. Callers invoke unconditionally; the data decides.
|
|
181
|
+
*/
|
|
182
|
+
applyKey(k) {
|
|
183
|
+
if (isIdentityKey(k))
|
|
184
|
+
return this;
|
|
185
|
+
const newL = clamp01(this.l * k.lightnessScale + k.lightnessShift);
|
|
186
|
+
const newC = Math.max(0, this.c * k.chromaScale);
|
|
187
|
+
const newH = wrapHue(this.h + k.hueShift);
|
|
188
|
+
return new _a(newL, newC, hueOf(newC, newH), this.alpha);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The color `t` of the way from this one toward `toward`, in OKLCH.
|
|
192
|
+
* `t = 0` is this color, `t = 1` is `toward`; pure.
|
|
193
|
+
*
|
|
194
|
+
* Lightness, chroma and alpha interpolate linearly. Hue takes the shorter
|
|
195
|
+
* arc around the wheel, so blue → red passes through magenta rather than
|
|
196
|
+
* sweeping across green. An achromatic endpoint has no hue of its own —
|
|
197
|
+
* `fromRgba` pins it to 0, which is red — so it adopts the other
|
|
198
|
+
* endpoint's hue (CSS Color 4's "powerless" rule): gray → red stays a red
|
|
199
|
+
* that gains chroma, instead of rotating through the wheel from 0°.
|
|
200
|
+
*
|
|
201
|
+
* `t` must lie in [0, 1]: this is interpolation, not extrapolation, and a
|
|
202
|
+
* `t` outside it (NaN included) is a RangeError, never a colour off the
|
|
203
|
+
* far end of the segment. [LAW:no-silent-failure]
|
|
204
|
+
*
|
|
205
|
+
* [LAW:one-source-of-truth] Achromatic means the same `ACHROMATIC_EPS`
|
|
206
|
+
* that `fromRgba` and `applyKey` pin hue by, so a gray produced by either
|
|
207
|
+
* is a gray here — and the result's hue goes through the same `hueOf`, so
|
|
208
|
+
* a gray produced HERE (t = 0 from a gray, t = 1 toward one) is a gray
|
|
209
|
+
* there too.
|
|
210
|
+
*/
|
|
211
|
+
mix(toward, t) {
|
|
212
|
+
if (!(t >= 0 && t <= 1)) {
|
|
213
|
+
throw new RangeError(`Oklch.mix: t must be in [0, 1]; got ${t}`);
|
|
214
|
+
}
|
|
215
|
+
return this.#interpolate(toward, t, t, t, t);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* `mix` with each axis moved its own share of the way: `weights.l` of the
|
|
219
|
+
* lightness gap, `weights.c` of the chroma gap, and so on, each in [0, 1].
|
|
220
|
+
* `mix(toward, t)` is `mixAxes` with every weight `t` — one interpolation,
|
|
221
|
+
* so the shorter-arc hue and the powerless-endpoint rule are the same
|
|
222
|
+
* whichever is called.
|
|
223
|
+
*
|
|
224
|
+
* It exists because perceptual axes are independent: a tint can take most of
|
|
225
|
+
* a hue's colourfulness while keeping close to the lightness it started at,
|
|
226
|
+
* which a single `t` cannot say — raising `t` for chroma drags lightness with
|
|
227
|
+
* it. Every weight is required: an axis left out would need a default, and
|
|
228
|
+
* "unchanged" (0) and "same as the others" are both plausible readings.
|
|
229
|
+
*
|
|
230
|
+
* A weak `h` beside a strong `c` shows the starting colour's hue at high
|
|
231
|
+
* chroma. The powerless rule covers only a truly achromatic start, so a
|
|
232
|
+
* near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
|
|
233
|
+
* noise in proportion. To land on the target's hue, pass `h: 1`.
|
|
234
|
+
*/
|
|
235
|
+
mixAxes(toward, weights) {
|
|
236
|
+
for (const axis of OKLCH_AXES) {
|
|
237
|
+
const w = weights[axis];
|
|
238
|
+
if (!(w >= 0 && w <= 1)) {
|
|
239
|
+
throw new RangeError(`Oklch.mixAxes: ${axis} weight must be in [0, 1]; got ${w}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return this.#interpolate(toward, weights.l, weights.c, weights.h, weights.alpha);
|
|
243
|
+
}
|
|
244
|
+
// The one interpolation both entry points share; each has already checked
|
|
245
|
+
// its own weights, so this is the arithmetic alone.
|
|
246
|
+
#interpolate(toward, wl, wc, wh, walpha) {
|
|
247
|
+
const thisHasHue = this.c >= ACHROMATIC_EPS;
|
|
248
|
+
const towardHasHue = toward.c >= ACHROMATIC_EPS;
|
|
249
|
+
const fromH = wrapHue(thisHasHue ? this.h : towardHasHue ? toward.h : 0);
|
|
250
|
+
const toH = wrapHue(towardHasHue ? toward.h : fromH);
|
|
251
|
+
// Shorter arc: endpoints more than half a turn apart lerp across 0°, the
|
|
252
|
+
// larger one taken a turn down. `x - 360` for x ≥ 180 is exact
|
|
253
|
+
// (Sterbenz), so the endpoints survive the shift and the wrap bit-for-bit
|
|
254
|
+
// — an addition of 360 would not.
|
|
255
|
+
const arc = toH - fromH;
|
|
256
|
+
const fromU = arc < -180 ? fromH - 360 : fromH;
|
|
257
|
+
const toU = arc > 180 ? toH - 360 : toH;
|
|
258
|
+
const c = lerp(this.c, toward.c, wc);
|
|
259
|
+
return new _a(lerp(this.l, toward.l, wl), c, hueOf(c, wrapHue(lerp(fromU, toU, wh))), lerp(this.alpha, toward.alpha, walpha));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
|
|
263
|
+
* (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
|
|
264
|
+
* resolves. Alpha is not a coordinate of the space and does not count.
|
|
265
|
+
* Symmetric and pure.
|
|
266
|
+
*/
|
|
267
|
+
deltaE(other) {
|
|
268
|
+
const a = (this.h * Math.PI) / 180;
|
|
269
|
+
const b = (other.h * Math.PI) / 180;
|
|
270
|
+
return Math.hypot(this.l - other.l, this.c * Math.cos(a) - other.c * Math.cos(b), this.c * Math.sin(a) - other.c * Math.sin(b));
|
|
271
|
+
}
|
|
272
|
+
/** Linear-sRGB coordinates for an explicit (l, C, h). Pure; `toRgba` passes
|
|
273
|
+
* already-normalized values so this never sees out-of-range inputs. */
|
|
274
|
+
toLinearRgb(l, C, h) {
|
|
275
|
+
const hRad = h * (Math.PI / 180);
|
|
276
|
+
const aLab = C * Math.cos(hRad);
|
|
277
|
+
const bLab = C * Math.sin(hRad);
|
|
278
|
+
const lCube = l + 0.3963377774 * aLab + 0.2158037573 * bLab;
|
|
279
|
+
const mCube = l - 0.1055613458 * aLab - 0.0638541728 * bLab;
|
|
280
|
+
const sCube = l - 0.0894841775 * aLab - 1.2914855480 * bLab;
|
|
281
|
+
const lLong = lCube * lCube * lCube;
|
|
282
|
+
const mLong = mCube * mCube * mCube;
|
|
283
|
+
const sLong = sCube * sCube * sCube;
|
|
284
|
+
return {
|
|
285
|
+
r: +4.0767416621 * lLong - 3.3077115913 * mLong + 0.2309699292 * sLong,
|
|
286
|
+
g: -1.2684380046 * lLong + 2.6097574011 * mLong - 0.3413193965 * sLong,
|
|
287
|
+
b: -0.0041960863 * lLong - 0.7034186147 * mLong + 1.7076147010 * sLong,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Largest chroma ≤ `c` whose (l, h) projection lies in sRGB.
|
|
292
|
+
* Standard chroma-reduction-by-bisection: 24 iterations gives ~1e-7
|
|
293
|
+
* precision, more than enough since the next step quantizes to 1/255.
|
|
294
|
+
*/
|
|
295
|
+
findInGamutChroma(l, c, h) {
|
|
296
|
+
const direct = this.toLinearRgb(l, c, h);
|
|
297
|
+
if (inGamut(direct.r, direct.g, direct.b))
|
|
298
|
+
return c;
|
|
299
|
+
let lo = 0;
|
|
300
|
+
let hi = c;
|
|
301
|
+
for (let i = 0; i < 24; i++) {
|
|
302
|
+
const mid = (lo + hi) / 2;
|
|
303
|
+
const probe = this.toLinearRgb(l, mid, h);
|
|
304
|
+
if (inGamut(probe.r, probe.g, probe.b))
|
|
305
|
+
lo = mid;
|
|
306
|
+
else
|
|
307
|
+
hi = mid;
|
|
308
|
+
}
|
|
309
|
+
return lo;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
_a = Oklch;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OSC 8 hyperlink wire grammar — which bytes may not appear inside a
|
|
3
|
+
* link, how a link becomes bytes, and how bytes are read back as a link.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:one-source-of-truth] The producer (`segmentToString`, the one encoder
|
|
6
|
+
* every link reaches the wire through) opens a link with `osc8Open` and closes
|
|
7
|
+
* it with `OSC8_CLOSE`; every consumer that reads rendered bytes (a width measure, a
|
|
8
|
+
* test extracting URLs) matches them with `OSC8`; the data-model boundary
|
|
9
|
+
* (RichText) cleans URLs with `stripOscTerminators`. All four read the one
|
|
10
|
+
* terminator set below, so the bytes a sanitizer removes and the bytes a
|
|
11
|
+
* reader stops at cannot disagree.
|
|
12
|
+
*
|
|
13
|
+
* [LAW:locality-or-seam] This module depends only on `fnv1a`; every consumer
|
|
14
|
+
* imports downward.
|
|
15
|
+
*
|
|
16
|
+
* The `id=` parameter is what makes one link hover as one link. A terminal
|
|
17
|
+
* treats cells as the same hyperlink when they share BOTH the URI and the id
|
|
18
|
+
* (the OSC 8 spec, and VTE / iTerm2 / kitty / WezTerm alike); without an id,
|
|
19
|
+
* each open sequence is its own link. The encoder writes one OSC 8 pair per
|
|
20
|
+
* segment, as the reference does, so a link whose text spans several segments
|
|
21
|
+
* — a bold glyph beside plain text, a padded cell — is emitted as several
|
|
22
|
+
* pairs, and would highlight piecewise on hover.
|
|
23
|
+
*
|
|
24
|
+
* [LAW:types-are-the-program] The id is a pure function of the URI, so the
|
|
25
|
+
* byte stream stays a pure function of (style, text, destination): no counter,
|
|
26
|
+
* no construction-order dependence. The consequence is deliberate: two spans
|
|
27
|
+
* with the same URI hover as one link wherever they sit on screen, adjacent
|
|
28
|
+
* or not — a click on either does the same thing, so they ARE one link.
|
|
29
|
+
* Because terminals key on the (id, URI) PAIR, a hash collision between two
|
|
30
|
+
* different URIs merges nothing — the URIs still differ — so a 32-bit hash is
|
|
31
|
+
* exact, not approximate.
|
|
32
|
+
*/
|
|
33
|
+
/** Remove every byte that could break a URL out of its OSC 8 wrap. */
|
|
34
|
+
export declare function stripOscTerminators(url: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* The bytes that open a hyperlink to `url`.
|
|
37
|
+
*
|
|
38
|
+
* [LAW:single-enforcer] Wire-byte trust boundary — the URL is sanitized at
|
|
39
|
+
* the one place it becomes an OSC 8 sequence, whichever upstream API attached
|
|
40
|
+
* it to the Style (RichText sanitizes at its data-model boundary too; a Style
|
|
41
|
+
* built directly with `new Style({ link })` reaches here unsanitized).
|
|
42
|
+
*/
|
|
43
|
+
export declare function osc8Open(url: string): string;
|
|
44
|
+
/** The bytes that close the current hyperlink. */
|
|
45
|
+
export declare const OSC8_CLOSE = "\u001B]8;;\u001B\\";
|
|
46
|
+
/**
|
|
47
|
+
* Matches one OSC 8 sequence — an open or a close — terminated by `ESC \`,
|
|
48
|
+
* BEL, or the 8-bit ST. Group 1 is the params, group 2 the URI; both are empty
|
|
49
|
+
* on a close. To read bytes back use `osc8Sequences`; this pattern is exported
|
|
50
|
+
* for composing into a larger one (a width measure's zero-width alternation).
|
|
51
|
+
*/
|
|
52
|
+
export declare const OSC8: RegExp;
|
|
53
|
+
/** One OSC 8 sequence found in rendered bytes. A close has `uri === ""`. */
|
|
54
|
+
export interface Osc8Sequence {
|
|
55
|
+
/** Offset of the sequence in the scanned string. */
|
|
56
|
+
readonly index: number;
|
|
57
|
+
/** Length of the whole sequence, terminator included. */
|
|
58
|
+
readonly length: number;
|
|
59
|
+
/** The params field (`id=…`); empty on a close. */
|
|
60
|
+
readonly params: string;
|
|
61
|
+
/** The URI; empty on a close. */
|
|
62
|
+
readonly uri: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every OSC 8 sequence in `text`, in order — the typed way to read rendered
|
|
66
|
+
* bytes back.
|
|
67
|
+
*/
|
|
68
|
+
export declare function osc8Sequences(text: string): Osc8Sequence[];
|