@promptctl/rich-js 0.17.0 → 0.18.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 +13 -26
- package/package.json +7 -3
- package/dist/core/box.d.ts +0 -144
- package/dist/core/box.js +0 -391
- package/dist/core/cells.d.ts +0 -148
- package/dist/core/cells.js +0 -266
- package/dist/core/color.d.ts +0 -250
- package/dist/core/color.js +0 -1029
- package/dist/core/console.d.ts +0 -156
- package/dist/core/console.js +0 -472
- package/dist/core/emoji.d.ts +0 -24
- package/dist/core/emoji.js +0 -391
- package/dist/core/export-html.d.ts +0 -24
- package/dist/core/export-html.js +0 -106
- package/dist/core/export-lines.d.ts +0 -121
- package/dist/core/export-lines.js +0 -141
- package/dist/core/highlighter.d.ts +0 -33
- package/dist/core/highlighter.js +0 -124
- package/dist/core/json.d.ts +0 -27
- package/dist/core/json.js +0 -53
- package/dist/core/markup.d.ts +0 -97
- package/dist/core/markup.js +0 -619
- package/dist/core/measure.d.ts +0 -27
- package/dist/core/measure.js +0 -61
- package/dist/core/oklch.d.ts +0 -161
- package/dist/core/oklch.js +0 -312
- package/dist/core/osc8.d.ts +0 -68
- package/dist/core/osc8.js +0 -90
- package/dist/core/pretty.d.ts +0 -141
- package/dist/core/pretty.js +0 -472
- package/dist/core/protocol.d.ts +0 -137
- package/dist/core/protocol.js +0 -101
- package/dist/core/render.d.ts +0 -84
- package/dist/core/render.js +0 -145
- package/dist/core/segment.d.ts +0 -142
- package/dist/core/segment.js +0 -433
- package/dist/core/spinnerData.d.ts +0 -10
- package/dist/core/spinnerData.js +0 -115
- package/dist/core/strip.d.ts +0 -145
- package/dist/core/strip.js +0 -308
- package/dist/core/style.d.ts +0 -144
- package/dist/core/style.js +0 -723
- package/dist/core/subscription.d.ts +0 -12
- package/dist/core/subscription.js +0 -12
- package/dist/core/text.d.ts +0 -255
- package/dist/core/text.js +0 -990
- package/dist/core/wrap.d.ts +0 -41
- package/dist/core/wrap.js +0 -127
- package/dist/host/host-stream.d.ts +0 -30
- package/dist/host/host-stream.js +0 -35
- package/dist/host/index.d.ts +0 -3
- package/dist/host/index.js +0 -2
- package/dist/host/terminal-host.d.ts +0 -143
- package/dist/host/terminal-host.js +0 -132
- package/dist/index.d.ts +0 -87
- package/dist/index.js +0 -104
- package/dist/node/prompt.d.ts +0 -21
- package/dist/node/prompt.js +0 -30
- package/dist/node/save.d.ts +0 -17
- package/dist/node/save.js +0 -21
- package/dist/node/terminal-host.d.ts +0 -84
- package/dist/node/terminal-host.js +0 -160
- package/dist/node/traceback.d.ts +0 -27
- package/dist/node/traceback.js +0 -112
- package/dist/renderables/align.d.ts +0 -16
- package/dist/renderables/align.js +0 -46
- package/dist/renderables/columns.d.ts +0 -61
- package/dist/renderables/columns.js +0 -168
- package/dist/renderables/constrain.d.ts +0 -15
- package/dist/renderables/constrain.js +0 -37
- package/dist/renderables/flexStrip.d.ts +0 -41
- package/dist/renderables/flexStrip.js +0 -200
- package/dist/renderables/group.d.ts +0 -11
- package/dist/renderables/group.js +0 -15
- package/dist/renderables/layout.d.ts +0 -92
- package/dist/renderables/layout.js +0 -315
- package/dist/renderables/live.d.ts +0 -47
- package/dist/renderables/live.js +0 -131
- package/dist/renderables/markdown.d.ts +0 -24
- package/dist/renderables/markdown.js +0 -182
- package/dist/renderables/padding.d.ts +0 -38
- package/dist/renderables/padding.js +0 -140
- package/dist/renderables/panel.d.ts +0 -116
- package/dist/renderables/panel.js +0 -322
- package/dist/renderables/progress.d.ts +0 -90
- package/dist/renderables/progress.js +0 -241
- package/dist/renderables/progressBar.d.ts +0 -31
- package/dist/renderables/progressBar.js +0 -52
- package/dist/renderables/prompt.d.ts +0 -49
- package/dist/renderables/prompt.js +0 -125
- package/dist/renderables/rule.d.ts +0 -24
- package/dist/renderables/rule.js +0 -90
- package/dist/renderables/spinner.d.ts +0 -29
- package/dist/renderables/spinner.js +0 -64
- package/dist/renderables/status.d.ts +0 -23
- package/dist/renderables/status.js +0 -60
- package/dist/renderables/syntax.d.ts +0 -35
- package/dist/renderables/syntax.js +0 -114
- package/dist/renderables/table.d.ts +0 -182
- package/dist/renderables/table.js +0 -697
- package/dist/renderables/traceback.d.ts +0 -31
- package/dist/renderables/traceback.js +0 -109
- package/dist/renderables/tree.d.ts +0 -54
- package/dist/renderables/tree.js +0 -159
- package/dist/template-bindings/color-funcs.d.ts +0 -87
- package/dist/template-bindings/color-funcs.js +0 -219
- package/dist/template-bindings/helpers.d.ts +0 -24
- package/dist/template-bindings/helpers.js +0 -48
- package/dist/template-bindings/index.d.ts +0 -107
- package/dist/template-bindings/index.js +0 -144
- package/dist/template-bindings/palette-funcs.d.ts +0 -94
- package/dist/template-bindings/palette-funcs.js +0 -151
- package/dist/template-bindings/style-funcs.d.ts +0 -28
- package/dist/template-bindings/style-funcs.js +0 -151
- package/dist/themes/buildPalette.d.ts +0 -36
- package/dist/themes/buildPalette.js +0 -56
- package/dist/themes/colorMath.d.ts +0 -82
- package/dist/themes/colorMath.js +0 -254
- package/dist/themes/colorRef.d.ts +0 -51
- package/dist/themes/colorRef.js +0 -96
- package/dist/themes/data/atom-one-dark.d.ts +0 -3
- package/dist/themes/data/atom-one-dark.js +0 -158
- package/dist/themes/data/atom-one-light.d.ts +0 -3
- package/dist/themes/data/atom-one-light.js +0 -158
- package/dist/themes/data/catppuccin-frappe.d.ts +0 -3
- package/dist/themes/data/catppuccin-frappe.js +0 -158
- package/dist/themes/data/catppuccin-latte.d.ts +0 -3
- package/dist/themes/data/catppuccin-latte.js +0 -158
- package/dist/themes/data/catppuccin-macchiato.d.ts +0 -3
- package/dist/themes/data/catppuccin-macchiato.js +0 -158
- package/dist/themes/data/catppuccin-mocha.d.ts +0 -3
- package/dist/themes/data/catppuccin-mocha.js +0 -158
- package/dist/themes/data/cyberpunk.d.ts +0 -17
- package/dist/themes/data/cyberpunk.js +0 -191
- package/dist/themes/data/default.d.ts +0 -3
- package/dist/themes/data/default.js +0 -158
- package/dist/themes/data/dracula.d.ts +0 -3
- package/dist/themes/data/dracula.js +0 -158
- package/dist/themes/data/flexoki.d.ts +0 -3
- package/dist/themes/data/flexoki.js +0 -158
- package/dist/themes/data/gruvbox.d.ts +0 -3
- package/dist/themes/data/gruvbox.js +0 -158
- package/dist/themes/data/index.d.ts +0 -35
- package/dist/themes/data/index.js +0 -64
- package/dist/themes/data/monokai.d.ts +0 -3
- package/dist/themes/data/monokai.js +0 -158
- package/dist/themes/data/nord.d.ts +0 -3
- package/dist/themes/data/nord.js +0 -158
- package/dist/themes/data/rose-pine-dawn.d.ts +0 -3
- package/dist/themes/data/rose-pine-dawn.js +0 -158
- package/dist/themes/data/rose-pine-moon.d.ts +0 -3
- package/dist/themes/data/rose-pine-moon.js +0 -158
- package/dist/themes/data/rose-pine.d.ts +0 -3
- package/dist/themes/data/rose-pine.js +0 -158
- package/dist/themes/data/solarized-dark.d.ts +0 -3
- package/dist/themes/data/solarized-dark.js +0 -158
- package/dist/themes/data/solarized-light.d.ts +0 -3
- package/dist/themes/data/solarized-light.js +0 -158
- package/dist/themes/data/svg-export.d.ts +0 -3
- package/dist/themes/data/svg-export.js +0 -158
- package/dist/themes/data/textual-ansi.d.ts +0 -3
- package/dist/themes/data/textual-ansi.js +0 -158
- package/dist/themes/data/textual-dark.d.ts +0 -3
- package/dist/themes/data/textual-dark.js +0 -158
- package/dist/themes/data/textual-light.d.ts +0 -3
- package/dist/themes/data/textual-light.js +0 -158
- package/dist/themes/data/tokyo-night.d.ts +0 -3
- package/dist/themes/data/tokyo-night.js +0 -158
- package/dist/themes/data/types.d.ts +0 -15
- package/dist/themes/data/types.js +0 -1
- package/dist/themes/palette.d.ts +0 -21
- package/dist/themes/palette.js +0 -28
- package/dist/themes/ramp.d.ts +0 -83
- package/dist/themes/ramp.js +0 -122
- package/dist/themes/registry.d.ts +0 -43
- package/dist/themes/registry.js +0 -82
- package/dist/themes/terminalThemes.d.ts +0 -54
- package/dist/themes/terminalThemes.js +0 -68
- package/dist/themes/transpose.d.ts +0 -87
- package/dist/themes/transpose.js +0 -138
- package/dist/widgets/button.d.ts +0 -49
- package/dist/widgets/button.js +0 -206
- package/dist/widgets/checkbox.d.ts +0 -41
- package/dist/widgets/checkbox.js +0 -155
- package/dist/widgets/dropdown.d.ts +0 -67
- package/dist/widgets/dropdown.js +0 -453
- package/dist/widgets/event-router.d.ts +0 -78
- package/dist/widgets/event-router.js +0 -622
- package/dist/widgets/focus-manager.d.ts +0 -26
- package/dist/widgets/focus-manager.js +0 -193
- package/dist/widgets/index.d.ts +0 -22
- package/dist/widgets/index.js +0 -15
- package/dist/widgets/screen.d.ts +0 -98
- package/dist/widgets/screen.js +0 -530
- package/dist/widgets/slider.d.ts +0 -57
- package/dist/widgets/slider.js +0 -270
- package/dist/widgets/static-item.d.ts +0 -40
- package/dist/widgets/static-item.js +0 -60
- package/dist/widgets/text-input.d.ts +0 -274
- package/dist/widgets/text-input.js +0 -1162
- package/dist/widgets/toggle.d.ts +0 -44
- package/dist/widgets/toggle.js +0 -179
- package/dist/widgets/types.d.ts +0 -108
- package/dist/widgets/types.js +0 -31
- package/dist/widgets/widget-base.d.ts +0 -42
- package/dist/widgets/widget-base.js +0 -173
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Traceback — renders error tracebacks with formatting.
|
|
3
|
-
*
|
|
4
|
-
* [LAW:effects-at-boundaries] Pure rendering: an `Error` in, `Segment`s out.
|
|
5
|
-
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
|
-
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
|
-
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
8
|
-
* main barrel, importable in a browser.
|
|
9
|
-
*
|
|
10
|
-
* [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
|
|
11
|
-
* reads. There is no `showLocals`: an `Error` carries a stack of locations and
|
|
12
|
-
* nothing of the values in scope at them. The one way to get those in node —
|
|
13
|
-
* an inspector session pausing on exceptions — would have to pause on every
|
|
14
|
-
* throw, caught ones included, to serve `new Traceback(caughtError)`, and it
|
|
15
|
-
* cannot exist in a browser at all. Nor is there a `width` or `theme`: those
|
|
16
|
-
* sized and coloured a source-code excerpt this renderer does not produce.
|
|
17
|
-
*/
|
|
18
|
-
import { Segment } from "../core/segment.js";
|
|
19
|
-
import type { Renderable, RenderOptions } from "../core/protocol.js";
|
|
20
|
-
export interface TracebackOptions {
|
|
21
|
-
suppress?: string[];
|
|
22
|
-
maxFrames?: number;
|
|
23
|
-
}
|
|
24
|
-
export declare class Traceback implements Renderable {
|
|
25
|
-
readonly error: Error;
|
|
26
|
-
readonly maxFrames: number;
|
|
27
|
-
readonly suppress: string[];
|
|
28
|
-
constructor(error: Error, options?: TracebackOptions);
|
|
29
|
-
render(options: RenderOptions): Iterable<Segment>;
|
|
30
|
-
private _renderFrame;
|
|
31
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Traceback — renders error tracebacks with formatting.
|
|
3
|
-
*
|
|
4
|
-
* [LAW:effects-at-boundaries] Pure rendering: an `Error` in, `Segment`s out.
|
|
5
|
-
* Installing this as the process-wide crash handler touches `process.on` and
|
|
6
|
-
* `process.exit`, so that lives behind the node seam as `installTraceback` in
|
|
7
|
-
* `src/node/traceback.ts` — which is what keeps this module, and therefore the
|
|
8
|
-
* main barrel, importable in a browser.
|
|
9
|
-
*
|
|
10
|
-
* [LAW:types-are-the-program] `TracebackOptions` names only what the renderer
|
|
11
|
-
* reads. There is no `showLocals`: an `Error` carries a stack of locations and
|
|
12
|
-
* nothing of the values in scope at them. The one way to get those in node —
|
|
13
|
-
* an inspector session pausing on exceptions — would have to pause on every
|
|
14
|
-
* throw, caught ones included, to serve `new Traceback(caughtError)`, and it
|
|
15
|
-
* cannot exist in a browser at all. Nor is there a `width` or `theme`: those
|
|
16
|
-
* sized and coloured a source-code excerpt this renderer does not produce.
|
|
17
|
-
*/
|
|
18
|
-
import { Segment } from "../core/segment.js";
|
|
19
|
-
import { Style } from "../core/style.js";
|
|
20
|
-
import { getStyle } from "../core/protocol.js";
|
|
21
|
-
function parseStack(error) {
|
|
22
|
-
const stack = error.stack ?? "";
|
|
23
|
-
const lines = stack.split("\n");
|
|
24
|
-
const frames = [];
|
|
25
|
-
for (const line of lines) {
|
|
26
|
-
const trimmed = line.trim();
|
|
27
|
-
// Node.js format: " at functionName (file:line:column)"
|
|
28
|
-
// or: " at file:line:column"
|
|
29
|
-
const match = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?/.exec(trimmed);
|
|
30
|
-
if (match) {
|
|
31
|
-
frames.push({
|
|
32
|
-
function: match[1] || undefined,
|
|
33
|
-
file: match[2],
|
|
34
|
-
line: parseInt(match[3], 10),
|
|
35
|
-
column: parseInt(match[4], 10),
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return frames;
|
|
40
|
-
}
|
|
41
|
-
export class Traceback {
|
|
42
|
-
error;
|
|
43
|
-
maxFrames;
|
|
44
|
-
suppress;
|
|
45
|
-
constructor(error, options) {
|
|
46
|
-
this.error = error;
|
|
47
|
-
this.maxFrames = options?.maxFrames ?? 100;
|
|
48
|
-
this.suppress = options?.suppress ?? [];
|
|
49
|
-
}
|
|
50
|
-
*render(options) {
|
|
51
|
-
const excTypeStyle = getStyle(options, "traceback.exc_type");
|
|
52
|
-
const textStyle = getStyle(options, "traceback.text");
|
|
53
|
-
// Error type and message
|
|
54
|
-
const errorName = this.error.name || "Error";
|
|
55
|
-
const errorMessage = this.error.message || "";
|
|
56
|
-
yield new Segment(errorName, excTypeStyle);
|
|
57
|
-
yield new Segment(": ");
|
|
58
|
-
yield new Segment(errorMessage, textStyle);
|
|
59
|
-
yield Segment.line();
|
|
60
|
-
yield Segment.line();
|
|
61
|
-
// Stack frames — suppressed frames show file/line only (not removed)
|
|
62
|
-
const frames = parseStack(this.error);
|
|
63
|
-
const filteredFrames = this.suppress.length > 0
|
|
64
|
-
? frames.map((f) => ({
|
|
65
|
-
...f,
|
|
66
|
-
suppressed: this.suppress.some((s) => f.file.includes(s)),
|
|
67
|
-
}))
|
|
68
|
-
: frames;
|
|
69
|
-
let displayFrames = filteredFrames;
|
|
70
|
-
if (this.maxFrames > 0 && displayFrames.length > this.maxFrames) {
|
|
71
|
-
// Spend the budget exactly: the tail takes `maxFrames - head` rather than
|
|
72
|
-
// a second `head`, so an odd budget shows every frame it counts, and it
|
|
73
|
-
// slices from an index because `slice(-0)` is the whole array.
|
|
74
|
-
const head = Math.floor(this.maxFrames / 2);
|
|
75
|
-
const first = displayFrames.slice(0, head);
|
|
76
|
-
const last = displayFrames.slice(displayFrames.length - (this.maxFrames - head));
|
|
77
|
-
const omitted = displayFrames.length - this.maxFrames;
|
|
78
|
-
for (const frame of first) {
|
|
79
|
-
yield* this._renderFrame(frame, options);
|
|
80
|
-
}
|
|
81
|
-
yield new Segment(` ... ${omitted} frames omitted ...`, textStyle);
|
|
82
|
-
yield Segment.line();
|
|
83
|
-
for (const frame of last) {
|
|
84
|
-
yield* this._renderFrame(frame, options);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
for (const frame of displayFrames) {
|
|
89
|
-
yield* this._renderFrame(frame, options);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
*_renderFrame(frame, options) {
|
|
94
|
-
const pathStyle = Style.parse("dim");
|
|
95
|
-
const lineNoStyle = getStyle(options, "traceback.offset");
|
|
96
|
-
yield new Segment(" ");
|
|
97
|
-
// Spec: suppressed frames show file and line only — no function name
|
|
98
|
-
if (frame.function && !frame.suppressed) {
|
|
99
|
-
yield new Segment(frame.function, Style.parse("bold"));
|
|
100
|
-
yield new Segment(" ");
|
|
101
|
-
}
|
|
102
|
-
yield new Segment(frame.file, pathStyle);
|
|
103
|
-
if (frame.line !== undefined) {
|
|
104
|
-
yield new Segment(":");
|
|
105
|
-
yield new Segment(String(frame.line), lineNoStyle);
|
|
106
|
-
}
|
|
107
|
-
yield Segment.line();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tree — hierarchical view with guide lines.
|
|
3
|
-
*/
|
|
4
|
-
import { Segment } from "../core/segment.js";
|
|
5
|
-
import { Style } from "../core/style.js";
|
|
6
|
-
import { RichText } from "../core/text.js";
|
|
7
|
-
import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
|
|
8
|
-
export interface TreeOptions {
|
|
9
|
-
expanded?: boolean;
|
|
10
|
-
hideRoot?: boolean;
|
|
11
|
-
guide_style?: string | Style;
|
|
12
|
-
style?: string | Style;
|
|
13
|
-
}
|
|
14
|
-
export declare class Tree implements Renderable, Measurable {
|
|
15
|
-
readonly label: Renderable;
|
|
16
|
-
readonly children: Tree[];
|
|
17
|
-
expanded: boolean;
|
|
18
|
-
readonly hideRoot: boolean;
|
|
19
|
-
readonly guideStyle: string | Style;
|
|
20
|
-
readonly style: string | Style;
|
|
21
|
-
constructor(label: string | RichText | Renderable, options?: TreeOptions);
|
|
22
|
-
add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
|
|
23
|
-
render(rawOptions: RenderOptions): Iterable<Segment>;
|
|
24
|
-
/**
|
|
25
|
-
* The tree walked into the rows it emits, one row per label, guides first.
|
|
26
|
-
*
|
|
27
|
-
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
28
|
-
* leads them is here and only here. `render` turns each row into segments and
|
|
29
|
-
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
30
|
-
* come to disagree about how many rows there are or how wide the guides on
|
|
31
|
-
* them run — which they would the moment a second walk existed.
|
|
32
|
-
*/
|
|
33
|
-
private _rows;
|
|
34
|
-
/**
|
|
35
|
-
* One row of the tree: its guides, then its label in whatever width the
|
|
36
|
-
* guides left.
|
|
37
|
-
*
|
|
38
|
-
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
39
|
-
* Before this, each call site emitted its guides and then handed the label
|
|
40
|
-
* `options` unchanged — the label was told it had the whole outer width while
|
|
41
|
-
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
42
|
-
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
43
|
-
* and the same fix: divide the requested width once and let every part read
|
|
44
|
-
* its share off the division. `options` arrives parsed from `render`.
|
|
45
|
-
*
|
|
46
|
-
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
47
|
-
* depending on label renderables to invent trailing newlines.
|
|
48
|
-
*/
|
|
49
|
-
private _renderRow;
|
|
50
|
-
measure(options: RenderOptions): {
|
|
51
|
-
minimum: number;
|
|
52
|
-
maximum: number;
|
|
53
|
-
};
|
|
54
|
-
}
|
package/dist/renderables/tree.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tree — hierarchical view with guide lines.
|
|
3
|
-
*/
|
|
4
|
-
import { Segment } from "../core/segment.js";
|
|
5
|
-
import { cellFit, cellLen, asCellCol } from "../core/cells.js";
|
|
6
|
-
import { NULL_STYLE } from "../core/style.js";
|
|
7
|
-
import { RichText } from "../core/text.js";
|
|
8
|
-
import { getStyle, isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
|
|
9
|
-
import { Measurement } from "../core/measure.js";
|
|
10
|
-
// Guide characters
|
|
11
|
-
const GUIDE_BRANCH = "├── ";
|
|
12
|
-
const GUIDE_LAST = "└── ";
|
|
13
|
-
const GUIDE_VERT = "│ ";
|
|
14
|
-
const GUIDE_SPACE = " ";
|
|
15
|
-
// ASCII fallback
|
|
16
|
-
const GUIDE_BRANCH_ASCII = "+-- ";
|
|
17
|
-
const GUIDE_LAST_ASCII = "+-- ";
|
|
18
|
-
const GUIDE_VERT_ASCII = "| ";
|
|
19
|
-
function toRenderable(label) {
|
|
20
|
-
if (typeof label === "string")
|
|
21
|
-
return new RichText(label, { end: "" });
|
|
22
|
-
if (label instanceof RichText) {
|
|
23
|
-
// Ensure no trailing newline in tree labels
|
|
24
|
-
const copy = label.copy();
|
|
25
|
-
copy.end = "";
|
|
26
|
-
return copy;
|
|
27
|
-
}
|
|
28
|
-
return label;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* How wide a label wants to be. A label with no `measure` cannot say, and the
|
|
32
|
-
* offer is the honest stand-in — including when the offer is unbounded, which
|
|
33
|
-
* is where `withBoundedWidth` reports it rather than guessing a number.
|
|
34
|
-
*/
|
|
35
|
-
function labelWidth(options, label) {
|
|
36
|
-
if (!isMeasurable(label))
|
|
37
|
-
return options.maxWidth;
|
|
38
|
-
return Measurement.get(options, label).maximum;
|
|
39
|
-
}
|
|
40
|
-
export class Tree {
|
|
41
|
-
label;
|
|
42
|
-
children;
|
|
43
|
-
expanded;
|
|
44
|
-
hideRoot;
|
|
45
|
-
guideStyle;
|
|
46
|
-
style;
|
|
47
|
-
constructor(label, options) {
|
|
48
|
-
this.label = toRenderable(label);
|
|
49
|
-
this.children = [];
|
|
50
|
-
this.expanded = options?.expanded !== false;
|
|
51
|
-
this.hideRoot = options?.hideRoot ?? false;
|
|
52
|
-
this.guideStyle = options?.guide_style ?? NULL_STYLE;
|
|
53
|
-
this.style = options?.style ?? NULL_STYLE;
|
|
54
|
-
}
|
|
55
|
-
add(label, options) {
|
|
56
|
-
const child = new Tree(label, {
|
|
57
|
-
guide_style: options?.guide_style,
|
|
58
|
-
style: options?.style,
|
|
59
|
-
expanded: options?.expanded,
|
|
60
|
-
});
|
|
61
|
-
this.children.push(child);
|
|
62
|
-
return child;
|
|
63
|
-
}
|
|
64
|
-
*render(rawOptions) {
|
|
65
|
-
const options = withBoundedWidth(rawOptions, this);
|
|
66
|
-
for (const row of this._rows(options, [], !this.hideRoot)) {
|
|
67
|
-
yield* this._renderRow(options, row);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* The tree walked into the rows it emits, one row per label, guides first.
|
|
72
|
-
*
|
|
73
|
-
* [LAW:one-source-of-truth] The walk that decides which rows exist and what
|
|
74
|
-
* leads them is here and only here. `render` turns each row into segments and
|
|
75
|
-
* `measure` reads each row's width off the same sequence, so the two cannot
|
|
76
|
-
* come to disagree about how many rows there are or how wide the guides on
|
|
77
|
-
* them run — which they would the moment a second walk existed.
|
|
78
|
-
*/
|
|
79
|
-
*_rows(options, prefixes, showLabel) {
|
|
80
|
-
const ascii = options.asciiOnly ?? false;
|
|
81
|
-
if (showLabel) {
|
|
82
|
-
yield { guides: prefixes, guideStyles: [this.guideStyle], label: this.label };
|
|
83
|
-
}
|
|
84
|
-
if (!this.expanded)
|
|
85
|
-
return;
|
|
86
|
-
for (let i = 0; i < this.children.length; i++) {
|
|
87
|
-
const child = this.children[i];
|
|
88
|
-
const isLast = i === this.children.length - 1;
|
|
89
|
-
const branch = ascii
|
|
90
|
-
? (isLast ? GUIDE_LAST_ASCII : GUIDE_BRANCH_ASCII)
|
|
91
|
-
: (isLast ? GUIDE_LAST : GUIDE_BRANCH);
|
|
92
|
-
const continuation = ascii
|
|
93
|
-
? (isLast ? GUIDE_SPACE : GUIDE_VERT_ASCII)
|
|
94
|
-
: (isLast ? GUIDE_SPACE : GUIDE_VERT);
|
|
95
|
-
// Child label with branch guide
|
|
96
|
-
yield {
|
|
97
|
-
guides: [...prefixes, branch],
|
|
98
|
-
guideStyles: [child.guideStyle, this.guideStyle],
|
|
99
|
-
label: child.label,
|
|
100
|
-
};
|
|
101
|
-
// Grandchildren with continuation prefix
|
|
102
|
-
if (child.expanded && child.children.length > 0) {
|
|
103
|
-
const grandPrefixes = [...prefixes, continuation];
|
|
104
|
-
for (let j = 0; j < child.children.length; j++) {
|
|
105
|
-
yield* child.children[j]._rows(options, grandPrefixes, true);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* One row of the tree: its guides, then its label in whatever width the
|
|
112
|
-
* guides left.
|
|
113
|
-
*
|
|
114
|
-
* [LAW:single-enforcer] The row's width is divided in exactly one place, here.
|
|
115
|
-
* Before this, each call site emitted its guides and then handed the label
|
|
116
|
-
* `options` unchanged — the label was told it had the whole outer width while
|
|
117
|
-
* the guides had already spent four cells of it, so a tree at maxWidth 5
|
|
118
|
-
* emitted rows of 9. That is the same defect Panel and Table were fixed for,
|
|
119
|
-
* and the same fix: divide the requested width once and let every part read
|
|
120
|
-
* its share off the division. `options` arrives parsed from `render`.
|
|
121
|
-
*
|
|
122
|
-
* [LAW:single-enforcer] Tree also owns its row boundaries here rather than
|
|
123
|
-
* depending on label renderables to invent trailing newlines.
|
|
124
|
-
*/
|
|
125
|
-
*_renderRow(options, row) {
|
|
126
|
-
const guideStyle = row.guideStyles.map((style) => getStyle(options, style)).find((style) => !style.isNull);
|
|
127
|
-
let left = options.maxWidth;
|
|
128
|
-
for (const text of row.guides) {
|
|
129
|
-
const piece = cellFit(text, asCellCol(left));
|
|
130
|
-
if (piece.length > 0)
|
|
131
|
-
yield new Segment(piece, guideStyle);
|
|
132
|
-
left -= cellLen(piece);
|
|
133
|
-
}
|
|
134
|
-
// The guides above are cropped by `cellFit`; the label was not, so a label
|
|
135
|
-
// that ignores the width it is handed pushed the row past the offer the
|
|
136
|
-
// guides had just been fitted into.
|
|
137
|
-
yield* Segment.cropLines(row.label.render({ ...options, maxWidth: left }), left);
|
|
138
|
-
yield Segment.line();
|
|
139
|
-
}
|
|
140
|
-
measure(options) {
|
|
141
|
-
// The ceiling is parsed for the same reason the rows are: an unparsed NaN
|
|
142
|
-
// ceiling yields a range no parent layout can divide.
|
|
143
|
-
const parsed = withCellWidth(options);
|
|
144
|
-
const ceiling = parsed.maxWidth;
|
|
145
|
-
// The widest row the walk emits, not the offer. Reported as the offer, a
|
|
146
|
-
// tree told any parent that asked "I want all of it" — so `Panel` in fit
|
|
147
|
-
// mode drew a 40-cell frame around nine cells of tree, and an unbounded
|
|
148
|
-
// offer came back unbounded.
|
|
149
|
-
let natural = 0;
|
|
150
|
-
for (const row of this._rows(parsed, [], !this.hideRoot)) {
|
|
151
|
-
const guideWidth = row.guides.reduce((sum, text) => sum + cellLen(text), 0);
|
|
152
|
-
natural = Math.max(natural, guideWidth + labelWidth(parsed, row.label));
|
|
153
|
-
}
|
|
154
|
-
const maximum = Math.min(natural, ceiling);
|
|
155
|
-
// A row is its guides plus at least one cell of label — but never more than
|
|
156
|
-
// this tree turned out to want, or the floor would sit above its own ceiling.
|
|
157
|
-
return { minimum: Math.min(4, maximum), maximum };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Color *values* for the rich-js template binding.
|
|
3
|
-
*
|
|
4
|
-
* ### Why colors are values
|
|
5
|
-
*
|
|
6
|
-
* Every other function in this binding layer takes a child fragment and
|
|
7
|
-
* returns a styled fragment. These do not: they take colors and return a
|
|
8
|
-
* color. That is the whole point. Before this module existed, a color could
|
|
9
|
-
* not survive between two operations — every color function resolved and
|
|
10
|
-
* applied in one breath — so composing two color operations had to be spelled
|
|
11
|
-
* as a *string grammar* (`"primary-darken-3 50%"`), a second language
|
|
12
|
-
* reimplementing function application inside a string literal. Every new color
|
|
13
|
-
* operation meant a new grammar production. [LAW:no-mode-explosion]
|
|
14
|
-
*
|
|
15
|
-
* With colors as values the surface is one shape that grows by nesting:
|
|
16
|
-
*
|
|
17
|
-
* ```
|
|
18
|
-
* {{ fg (color "primary") .branch }}
|
|
19
|
-
* {{ fg (darken (color "primary") 2) .branch }}
|
|
20
|
-
* {{ fg (mix (color "foreground") (color "surface") 65) .label }}
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* The simple case is a literal prefix of the composed case — an author adding
|
|
24
|
-
* a `darken` to working code wraps it, rather than rewriting it into an
|
|
25
|
-
* unfamiliar form. [LAW:composability]
|
|
26
|
-
*
|
|
27
|
-
* ### The carrier is a hex string, deliberately
|
|
28
|
-
*
|
|
29
|
-
* A color crosses the template seam as `#RRGGBB`, not as an opaque object,
|
|
30
|
-
* for three reasons:
|
|
31
|
-
*
|
|
32
|
-
* 1. **The type separates.** `"string"` is the strictest arg slot the template
|
|
33
|
-
* engine has — it *refuses* typed `T`. So a color slot cannot silently
|
|
34
|
-
* accept a text fragment. An opaque object would land in `"T"`/`"liftable"`,
|
|
35
|
-
* both of which admit any non-primitive, and `{{ bold (color "primary") }}`
|
|
36
|
-
* would type-check its way into nonsense. [LAW:types-are-the-program]
|
|
37
|
-
* 2. **It flows through the language.** `{{ $muted := mix … }}` holds it,
|
|
38
|
-
* `printf` prints it, `eq` compares it — no engine changes required. Naming
|
|
39
|
-
* a color once and using it ten times is the ergonomic payoff.
|
|
40
|
-
* 3. **Misuse is visible.** A color that lands in text position renders as
|
|
41
|
-
* `#7aa2f7` — wrong in a way an author immediately sees, never a silently
|
|
42
|
-
* dropped style. [LAW:no-silent-failure]
|
|
43
|
-
*
|
|
44
|
-
* No precision is given up: `darken`, `blendRgb`, and `contrastFor` already
|
|
45
|
-
* take and return 8-bit `ColorRgba`. The one caveat is OKLCH: each of the
|
|
46
|
-
* axis functions below round-trips sRGB→OKLCH→sRGB, so chaining three or more
|
|
47
|
-
* of them quantizes visibly. Compose two, or reach for `transposePalette` when
|
|
48
|
-
* a whole palette needs adapting — the same caveat that already applies to
|
|
49
|
-
* chained transpositions.
|
|
50
|
-
*
|
|
51
|
-
* ### Palette-free on purpose
|
|
52
|
-
*
|
|
53
|
-
* Nothing here knows what a palette is. `color` and `ramp` (the two
|
|
54
|
-
* palette-dependent functions, in `palette-funcs.ts`) turn names into hex;
|
|
55
|
-
* from that point on the math is pure. A consumer with no theme system at all still gets
|
|
56
|
-
* the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
|
|
57
|
-
*/
|
|
58
|
-
import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
|
|
59
|
-
import { ColorDepth } from "../core/color.js";
|
|
60
|
-
/**
|
|
61
|
-
* The `readableOn` binding, measuring at the depth `drawnAt` names when it is
|
|
62
|
-
* evaluated — a consumer that renders at a per-call depth registers its own.
|
|
63
|
-
*/
|
|
64
|
-
export declare const readableOnFunc: (drawnAt: () => ColorDepth) => TemplateFunc;
|
|
65
|
-
/**
|
|
66
|
-
* The palette-free color vocabulary: every function takes colors and returns a
|
|
67
|
-
* color, so they compose by nesting.
|
|
68
|
-
*
|
|
69
|
-
* | function | meaning |
|
|
70
|
-
* |---|---|
|
|
71
|
-
* | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
|
|
72
|
-
* | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
|
|
73
|
-
* | `contrastOn bg` | black or white, whichever is readable on `bg` |
|
|
74
|
-
* | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
|
|
75
|
-
* | `shiftHue c deg` | rotate hue in OKLCH |
|
|
76
|
-
* | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
|
|
77
|
-
* | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
|
|
78
|
-
* | `shiftLightness c d` | add to lightness, after any scale |
|
|
79
|
-
*
|
|
80
|
-
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
81
|
-
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
82
|
-
*
|
|
83
|
-
* @param drawnAt the depth the terminal will draw at, read on every
|
|
84
|
-
* `readableOn` evaluation so a host that learns its depth per render passes
|
|
85
|
-
* one getter; defaults to truecolor.
|
|
86
|
-
*/
|
|
87
|
-
export declare function colorFuncs(drawnAt?: () => ColorDepth): FuncMap;
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Color *values* for the rich-js template binding.
|
|
3
|
-
*
|
|
4
|
-
* ### Why colors are values
|
|
5
|
-
*
|
|
6
|
-
* Every other function in this binding layer takes a child fragment and
|
|
7
|
-
* returns a styled fragment. These do not: they take colors and return a
|
|
8
|
-
* color. That is the whole point. Before this module existed, a color could
|
|
9
|
-
* not survive between two operations — every color function resolved and
|
|
10
|
-
* applied in one breath — so composing two color operations had to be spelled
|
|
11
|
-
* as a *string grammar* (`"primary-darken-3 50%"`), a second language
|
|
12
|
-
* reimplementing function application inside a string literal. Every new color
|
|
13
|
-
* operation meant a new grammar production. [LAW:no-mode-explosion]
|
|
14
|
-
*
|
|
15
|
-
* With colors as values the surface is one shape that grows by nesting:
|
|
16
|
-
*
|
|
17
|
-
* ```
|
|
18
|
-
* {{ fg (color "primary") .branch }}
|
|
19
|
-
* {{ fg (darken (color "primary") 2) .branch }}
|
|
20
|
-
* {{ fg (mix (color "foreground") (color "surface") 65) .label }}
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* The simple case is a literal prefix of the composed case — an author adding
|
|
24
|
-
* a `darken` to working code wraps it, rather than rewriting it into an
|
|
25
|
-
* unfamiliar form. [LAW:composability]
|
|
26
|
-
*
|
|
27
|
-
* ### The carrier is a hex string, deliberately
|
|
28
|
-
*
|
|
29
|
-
* A color crosses the template seam as `#RRGGBB`, not as an opaque object,
|
|
30
|
-
* for three reasons:
|
|
31
|
-
*
|
|
32
|
-
* 1. **The type separates.** `"string"` is the strictest arg slot the template
|
|
33
|
-
* engine has — it *refuses* typed `T`. So a color slot cannot silently
|
|
34
|
-
* accept a text fragment. An opaque object would land in `"T"`/`"liftable"`,
|
|
35
|
-
* both of which admit any non-primitive, and `{{ bold (color "primary") }}`
|
|
36
|
-
* would type-check its way into nonsense. [LAW:types-are-the-program]
|
|
37
|
-
* 2. **It flows through the language.** `{{ $muted := mix … }}` holds it,
|
|
38
|
-
* `printf` prints it, `eq` compares it — no engine changes required. Naming
|
|
39
|
-
* a color once and using it ten times is the ergonomic payoff.
|
|
40
|
-
* 3. **Misuse is visible.** A color that lands in text position renders as
|
|
41
|
-
* `#7aa2f7` — wrong in a way an author immediately sees, never a silently
|
|
42
|
-
* dropped style. [LAW:no-silent-failure]
|
|
43
|
-
*
|
|
44
|
-
* No precision is given up: `darken`, `blendRgb`, and `contrastFor` already
|
|
45
|
-
* take and return 8-bit `ColorRgba`. The one caveat is OKLCH: each of the
|
|
46
|
-
* axis functions below round-trips sRGB→OKLCH→sRGB, so chaining three or more
|
|
47
|
-
* of them quantizes visibly. Compose two, or reach for `transposePalette` when
|
|
48
|
-
* a whole palette needs adapting — the same caveat that already applies to
|
|
49
|
-
* chained transpositions.
|
|
50
|
-
*
|
|
51
|
-
* ### Palette-free on purpose
|
|
52
|
-
*
|
|
53
|
-
* Nothing here knows what a palette is. `color` and `ramp` (the two
|
|
54
|
-
* palette-dependent functions, in `palette-funcs.ts`) turn names into hex;
|
|
55
|
-
* from that point on the math is pure. A consumer with no theme system at all still gets
|
|
56
|
-
* the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
|
|
57
|
-
*/
|
|
58
|
-
import { blendRgb, ColorDepth } from "../core/color.js";
|
|
59
|
-
import { Oklch, IDENTITY } from "../core/oklch.js";
|
|
60
|
-
import { HEX_COLOR_RE, parseHexColor } from "../themes/colorRef.js";
|
|
61
|
-
import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
|
|
62
|
-
/**
|
|
63
|
-
* The gate every color argument crosses. [LAW:parse-dont-validate] — the
|
|
64
|
-
* function bodies below receive `ColorRgba` and never re-check.
|
|
65
|
-
*
|
|
66
|
-
* Parsing itself is `parseHexColor`'s job; this wrapper exists only to replace
|
|
67
|
-
* its error *wording*, because the mistake it catches is almost always the
|
|
68
|
-
* same one — a palette name passed where a color belongs (`darken "primary" 2`)
|
|
69
|
-
* — and an author who reads "wrap it: darken (color "primary")" is corrected in
|
|
70
|
-
* one step, where "expected #RRGGBB" leaves them guessing.
|
|
71
|
-
* [LAW:no-silent-failure]
|
|
72
|
-
*/
|
|
73
|
-
function asColor(value, func) {
|
|
74
|
-
if (!HEX_COLOR_RE.test(value.trim())) {
|
|
75
|
-
throw new TypeError(`${func} expected a color (#RRGGBB or #RRGGBBAA), got ${JSON.stringify(value)}` +
|
|
76
|
-
` — to use a palette name here, wrap it: ${func} (color ${JSON.stringify(value)}) …`);
|
|
77
|
-
}
|
|
78
|
-
return parseHexColor(value);
|
|
79
|
-
}
|
|
80
|
-
function colorFunc(argTypes, fn) {
|
|
81
|
-
return { fn, argTypes, returnType: "string" };
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* The gate every *numeric* argument crosses, with an optional inclusive range.
|
|
85
|
-
*
|
|
86
|
-
* [LAW:single-enforcer] One check for the whole family. The template engine
|
|
87
|
-
* treats a trailing slot as variadic, so an under-supplied call
|
|
88
|
-
* (`{{ darken "#102030" }}`) reaches the body with `undefined` rather than
|
|
89
|
-
* failing at the gate. Without this, that `undefined` propagated into the color
|
|
90
|
-
* math and surfaced as `ColorRgba.red must be an integer in [0, 255]; got NaN`
|
|
91
|
-
* — loud, but pointing at a channel the author never wrote, three layers from
|
|
92
|
-
* the missing argument. An error that does not locate its cause is barely
|
|
93
|
-
* better than a silent one. [LAW:no-silent-failure]
|
|
94
|
-
*/
|
|
95
|
-
function asAmount(value, func, param, range) {
|
|
96
|
-
const inRange = range === undefined || (value >= range.min && value <= range.max);
|
|
97
|
-
if (!Number.isFinite(value) || !inRange) {
|
|
98
|
-
const bound = range === undefined
|
|
99
|
-
? "a finite number"
|
|
100
|
-
: `within ${range.min}..${range.max} (${range.note})`;
|
|
101
|
-
throw new RangeError(`${func}'s ${param} must be ${bound}, got ${value}` +
|
|
102
|
-
(value === undefined ? ` — ${func} takes it as its last argument` : ""));
|
|
103
|
-
}
|
|
104
|
-
return value;
|
|
105
|
-
}
|
|
106
|
-
// --- HSL lightness ---
|
|
107
|
-
//
|
|
108
|
-
// `lighten` is `darken` with the sign flipped — the same operation, not a
|
|
109
|
-
// second one (colorMath's `lighten` is literally `darken(c, -levels)`). Both
|
|
110
|
-
// names ship because "lighten by 2" and "darken by -2" are not equally
|
|
111
|
-
// readable at a call site, and the operation is genuinely bidirectional.
|
|
112
|
-
const darkenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "darken"), asAmount(levels, "darken", "levels")).hex));
|
|
113
|
-
const lightenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "lighten"), -asAmount(levels, "lighten", "levels")).hex));
|
|
114
|
-
// --- Blending ---
|
|
115
|
-
//
|
|
116
|
-
// [LAW:one-type-per-behavior] There is no separate `alphaBlend` binding.
|
|
117
|
-
// `alphaBlend(fg, bg, a)` in colorMath is *defined* as `blendRgb(bg, fg, a)` —
|
|
118
|
-
// one operation wearing two names. Compositing a translucent color over a
|
|
119
|
-
// background and mixing two colors by a percentage are the same arithmetic,
|
|
120
|
-
// so they get one function. Terminals have no alpha channel, which is also why
|
|
121
|
-
// no function here *produces* a translucent color: it could not be rendered,
|
|
122
|
-
// and a color that silently does nothing in the sink is worse than no
|
|
123
|
-
// function at all.
|
|
124
|
-
const MIX_PCT_MAX = 100;
|
|
125
|
-
const MIX_RANGE = { min: 0, max: MIX_PCT_MAX, note: "0 = from, 100 = toward" };
|
|
126
|
-
const mixFunc = colorFunc(["string", "string", "float"], ((fromHex, towardHex, pct) => blendRgb(asColor(fromHex, "mix"), asColor(towardHex, "mix"), asAmount(pct, "mix", "percentage", MIX_RANGE) / MIX_PCT_MAX).hex));
|
|
127
|
-
// --- Readability ---
|
|
128
|
-
//
|
|
129
|
-
// Two different questions, so two functions rather than one with a flag:
|
|
130
|
-
// `contrastOn` asks "what is maximally readable here" (black or white, WCAG
|
|
131
|
-
// relative-luminance cutoff); `readableOn` asks "keep this color recognizably
|
|
132
|
-
// itself, but guarantee it clears `ratio`" — it slides OKLCH lightness and
|
|
133
|
-
// preserves hue, so a blue on dark blue becomes a lighter blue rather than
|
|
134
|
-
// white.
|
|
135
|
-
//
|
|
136
|
-
// [LAW:no-silent-failure] `ratio` is required, not defaulted to WCAG AA's 4.5.
|
|
137
|
-
// The threshold is the whole decision this function makes, and the right value
|
|
138
|
-
// genuinely differs by intent — 4.5 for body text, 3.0 for large or
|
|
139
|
-
// deliberately recessive text. A hidden default would silently govern an
|
|
140
|
-
// accessibility outcome the author never saw, and would quietly over-correct a
|
|
141
|
-
// caller whose *point* was to be quiet: de-emphasized text floored at 4.5 is no
|
|
142
|
-
// longer de-emphasized.
|
|
143
|
-
// WCAG 2.x contrast ratios live in [1, 21] — 1 is a color on itself.
|
|
144
|
-
const RATIO_RANGE = {
|
|
145
|
-
min: 1,
|
|
146
|
-
max: 21,
|
|
147
|
-
note: "4.5 = AA body text, 3 = AA large text",
|
|
148
|
-
};
|
|
149
|
-
const contrastOnFunc = colorFunc(["string"], ((bgHex) => contrastFor(asColor(bgHex, "contrastOn")).hex));
|
|
150
|
-
/**
|
|
151
|
-
* The `readableOn` binding, measuring at the depth `drawnAt` names when it is
|
|
152
|
-
* evaluated — a consumer that renders at a per-call depth registers its own.
|
|
153
|
-
*/
|
|
154
|
-
export const readableOnFunc = (drawnAt) => colorFunc(["string", "string", "float"], ((fgHex, bgHex, ratio) => ensureContrast(asColor(fgHex, "readableOn"), asColor(bgHex, "readableOn"), asAmount(ratio, "readableOn", "ratio", RATIO_RANGE), drawnAt()).hex));
|
|
155
|
-
// --- OKLCH axes ---
|
|
156
|
-
//
|
|
157
|
-
// [LAW:one-source-of-truth] One function per `ThemeKey` axis, and the table is
|
|
158
|
-
// keyed BY the axis so the compiler proves it total: add a field to `ThemeKey`
|
|
159
|
-
// and this object stops satisfying `Record<keyof ThemeKey, string>` until a
|
|
160
|
-
// binding name is chosen for it. The same derive-registrations-from-the-real-
|
|
161
|
-
// inventory trick `attributeFuncs()` uses over `ATTRIBUTE_NAMES`.
|
|
162
|
-
//
|
|
163
|
-
// Naming follows the axis semantics rather than the field spelling: `shift`
|
|
164
|
-
// for the additive axes, `scale` for the multiplicative ones, so a reader can
|
|
165
|
-
// tell from the call site whether `1` means "no change" or "one unit".
|
|
166
|
-
const OKLCH_AXIS_FUNCS = {
|
|
167
|
-
hueShift: "shiftHue",
|
|
168
|
-
chromaScale: "scaleChroma",
|
|
169
|
-
lightnessScale: "scaleLightness",
|
|
170
|
-
lightnessShift: "shiftLightness",
|
|
171
|
-
};
|
|
172
|
-
function oklchAxisFuncs() {
|
|
173
|
-
const out = {};
|
|
174
|
-
for (const [axis, funcName] of Object.entries(OKLCH_AXIS_FUNCS)) {
|
|
175
|
-
// [LAW:dataflow-not-control-flow] Every axis takes the identical path —
|
|
176
|
-
// splice one value into the identity key and apply it. The axis is data.
|
|
177
|
-
const key = axis;
|
|
178
|
-
out[funcName] = colorFunc(["string", "float"], ((hex, amount) => {
|
|
179
|
-
const themeKey = {
|
|
180
|
-
...IDENTITY,
|
|
181
|
-
[key]: asAmount(amount, funcName, "amount"),
|
|
182
|
-
};
|
|
183
|
-
return Oklch.fromRgba(asColor(hex, funcName)).applyKey(themeKey).toRgba().hex;
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
188
|
-
/**
|
|
189
|
-
* The palette-free color vocabulary: every function takes colors and returns a
|
|
190
|
-
* color, so they compose by nesting.
|
|
191
|
-
*
|
|
192
|
-
* | function | meaning |
|
|
193
|
-
* |---|---|
|
|
194
|
-
* | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
|
|
195
|
-
* | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
|
|
196
|
-
* | `contrastOn bg` | black or white, whichever is readable on `bg` |
|
|
197
|
-
* | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
|
|
198
|
-
* | `shiftHue c deg` | rotate hue in OKLCH |
|
|
199
|
-
* | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
|
|
200
|
-
* | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
|
|
201
|
-
* | `shiftLightness c d` | add to lightness, after any scale |
|
|
202
|
-
*
|
|
203
|
-
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
204
|
-
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
205
|
-
*
|
|
206
|
-
* @param drawnAt the depth the terminal will draw at, read on every
|
|
207
|
-
* `readableOn` evaluation so a host that learns its depth per render passes
|
|
208
|
-
* one getter; defaults to truecolor.
|
|
209
|
-
*/
|
|
210
|
-
export function colorFuncs(drawnAt = () => ColorDepth.TRUECOLOR) {
|
|
211
|
-
return {
|
|
212
|
-
darken: darkenFunc,
|
|
213
|
-
lighten: lightenFunc,
|
|
214
|
-
mix: mixFunc,
|
|
215
|
-
contrastOn: contrastOnFunc,
|
|
216
|
-
readableOn: readableOnFunc(drawnAt),
|
|
217
|
-
...oklchAxisFuncs(),
|
|
218
|
-
};
|
|
219
|
-
}
|