@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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport — a fixed number of rows onto a taller renderable, scrolled by an
|
|
3
|
+
* offset that cannot leave the content.
|
|
4
|
+
*
|
|
5
|
+
* Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
|
|
6
|
+
* when it is given one; otherwise its own configured rows, or the content's
|
|
7
|
+
* full length when it has none, capped by any ceiling. It renders its content
|
|
8
|
+
* with no height, because the content's natural length is the scroll extent,
|
|
9
|
+
* and pads or crops what comes back to its rows — its rows are its own height,
|
|
10
|
+
* not a region someone else will shape.
|
|
11
|
+
*
|
|
12
|
+
* Its width is the width it is given, in both directions: each row is cropped
|
|
13
|
+
* or padded to it, so content that ignores its width cannot spill past the
|
|
14
|
+
* viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
|
|
15
|
+
* down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
|
|
16
|
+
* has a gutter zero cells wide, and every row takes the same path either way.
|
|
17
|
+
*
|
|
18
|
+
* [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
|
|
19
|
+
* against exists before a render: the rows come from the budget the render is
|
|
20
|
+
* handed, and the content's length from rendering it at the width it is handed.
|
|
21
|
+
* So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
|
|
22
|
+
* are called. Each queues a move, and the next render resolves the queue in
|
|
23
|
+
* call order, clamping after every move, against the rows and length that
|
|
24
|
+
* render found. Calling an operation before the first render, or three of them
|
|
25
|
+
* between two renders, is the same code path as calling one.
|
|
26
|
+
*
|
|
27
|
+
* Every cell of its rows, gutter included, is stamped as its own
|
|
28
|
+
* (`Segment.anchorLines`), around whatever anchors its content drew. So the
|
|
29
|
+
* composed frame names the viewport under any of its cells however deep it is
|
|
30
|
+
* nested, and a cell of a widget inside it names both: that is how the wheel
|
|
31
|
+
* finds the viewport to scroll. `canScrollBy` is how it passes over one that
|
|
32
|
+
* cannot move that way, to the viewport around it.
|
|
33
|
+
*/
|
|
34
|
+
import { Segment } from "../core/segment.js";
|
|
35
|
+
import { Measurement } from "../core/measure.js";
|
|
36
|
+
import { cellCount, cellLen } from "../core/cells.js";
|
|
37
|
+
import { NULL_STYLE } from "../core/style.js";
|
|
38
|
+
import { drawable, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
|
|
39
|
+
/** A heavy line for the thumb on a light line for the track. */
|
|
40
|
+
export const SCROLLBAR = {
|
|
41
|
+
thumb: { glyph: "┃", style: "scrollbar.thumb" },
|
|
42
|
+
track: { glyph: "│", style: "scrollbar.track" },
|
|
43
|
+
};
|
|
44
|
+
/** The absence of a scrollbar, as a gutter zero cells wide. */
|
|
45
|
+
const NO_SCROLLBAR = {
|
|
46
|
+
thumb: { glyph: "", style: NULL_STYLE },
|
|
47
|
+
track: { glyph: "", style: NULL_STYLE },
|
|
48
|
+
};
|
|
49
|
+
export class Viewport {
|
|
50
|
+
/**
|
|
51
|
+
* What the viewport shows. Replace it to show new content from the same
|
|
52
|
+
* scroll position — a view rebuilt every frame keeps one `Viewport`.
|
|
53
|
+
*/
|
|
54
|
+
content;
|
|
55
|
+
rows;
|
|
56
|
+
scrollbar;
|
|
57
|
+
_offset = 0;
|
|
58
|
+
// Nothing rendered, nothing shown: no move can take until the first render.
|
|
59
|
+
_extent = { rows: 0, lines: 0 };
|
|
60
|
+
_moves = [];
|
|
61
|
+
constructor(content, options = {}) {
|
|
62
|
+
this.content = content;
|
|
63
|
+
this.rows = options.rows;
|
|
64
|
+
this.scrollbar = options.scrollbar ?? NO_SCROLLBAR;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The first content line the last render showed. A move requested since is
|
|
68
|
+
* not reflected until the next render resolves it.
|
|
69
|
+
*/
|
|
70
|
+
get offset() {
|
|
71
|
+
return this._offset;
|
|
72
|
+
}
|
|
73
|
+
/** Scroll so `line` is the first line shown. */
|
|
74
|
+
scrollTo(line) {
|
|
75
|
+
this._moves.push(() => line);
|
|
76
|
+
}
|
|
77
|
+
/** Scroll by `lines`: down when positive, up when negative. */
|
|
78
|
+
scrollBy(lines) {
|
|
79
|
+
this._moves.push((offset) => offset + lines);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether `scrollBy(lines)` would move the viewport from where the moves
|
|
83
|
+
* queued so far leave it, against the rows and length the last render
|
|
84
|
+
* found.
|
|
85
|
+
*/
|
|
86
|
+
canScrollBy(lines) {
|
|
87
|
+
const offset = this.resolve(this._extent);
|
|
88
|
+
return clampOffset(offset + lines, this._extent) !== offset;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Scroll the least distance that shows lines `start` up to but not
|
|
92
|
+
* including `end` — lines of the content as it renders at the viewport's
|
|
93
|
+
* width, so an item that wraps spans more than one. A range already in view does not move; one taller than
|
|
94
|
+
* the viewport shows its first line at the top.
|
|
95
|
+
*/
|
|
96
|
+
ensureVisible(start, end) {
|
|
97
|
+
this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The width its content renders at, out of `width`: what the scrollbar's
|
|
101
|
+
* gutter leaves. Lines handed to `ensureVisible` are lines at this width.
|
|
102
|
+
*/
|
|
103
|
+
contentWidth(width) {
|
|
104
|
+
return cellCount(width - gutterWidth(this.scrollbar));
|
|
105
|
+
}
|
|
106
|
+
*render(rawOptions) {
|
|
107
|
+
const { height, ...options } = withBoundedWidth(rawOptions, this);
|
|
108
|
+
const gutter = gutterWidth(this.scrollbar);
|
|
109
|
+
const contentWidth = this.contentWidth(options.maxWidth);
|
|
110
|
+
const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
|
|
111
|
+
const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
|
|
112
|
+
this._extent = extent;
|
|
113
|
+
this._offset = this.resolve(extent);
|
|
114
|
+
this._moves = [];
|
|
115
|
+
const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
|
|
116
|
+
// [LAW:no-ambient-temporal-coupling] The thumb is drawn from the offset
|
|
117
|
+
// this render just resolved, not from `offset` as it stood before it.
|
|
118
|
+
const thumb = thumbRows(extent, this._offset);
|
|
119
|
+
// The content's cells and the gutter's drawn cells sum to the offer: a
|
|
120
|
+
// gutter wider than the whole offer is drawn cropped to it.
|
|
121
|
+
const drawn = Math.min(gutter, options.maxWidth);
|
|
122
|
+
// The style reaches the padding too, so a glyph narrower than the gutter
|
|
123
|
+
// still fills its cell with its part's background.
|
|
124
|
+
const cell = ({ glyph, style }) => {
|
|
125
|
+
const resolved = getStyle(options, style);
|
|
126
|
+
return Segment.adjustLineLength([new Segment(glyph, resolved)], drawn, resolved);
|
|
127
|
+
};
|
|
128
|
+
// The gutter keeps the chosen scrollbar's width, so an ASCII stand-in
|
|
129
|
+
// padded into it leaves the content where `contentWidth` said it would be.
|
|
130
|
+
const bar = drawable(options, this.scrollbar, asciiScrollbar(this.scrollbar), scrollbarGlyphs);
|
|
131
|
+
const thumbCell = cell(bar.thumb);
|
|
132
|
+
const trackCell = cell(bar.track);
|
|
133
|
+
const rows = shown.map((line, row) => [
|
|
134
|
+
...Segment.adjustLineLength(line, contentWidth),
|
|
135
|
+
...(row >= thumb.start && row < thumb.end ? thumbCell : trackCell),
|
|
136
|
+
]);
|
|
137
|
+
for (const line of Segment.anchorLines(rows, this)) {
|
|
138
|
+
yield* line;
|
|
139
|
+
yield Segment.line();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* [LAW:one-source-of-truth] Where the queued moves leave the offset against
|
|
144
|
+
* `extent`: what a render commits, and what `canScrollBy` asks from.
|
|
145
|
+
* [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
|
|
146
|
+
* queued moves: content that shrank since the last render pulls it back.
|
|
147
|
+
*/
|
|
148
|
+
resolve(extent) {
|
|
149
|
+
return this._moves.reduce((offset, move) => clampOffset(move(offset, extent), extent), clampOffset(this._offset, extent));
|
|
150
|
+
}
|
|
151
|
+
measure(rawOptions) {
|
|
152
|
+
// Its width is its content's and its gutter's. `withCellWidth` and not
|
|
153
|
+
// `withBoundedWidth`: the bounded parse asks this very method for the
|
|
154
|
+
// natural width.
|
|
155
|
+
const options = withCellWidth(rawOptions);
|
|
156
|
+
const gutter = gutterWidth(this.scrollbar);
|
|
157
|
+
const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
|
|
158
|
+
const content = Measurement.get(inner, this.content);
|
|
159
|
+
return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
|
|
163
|
+
/** `scrollbar` in its own styles, drawn in ASCII. */
|
|
164
|
+
function asciiScrollbar({ thumb, track }) {
|
|
165
|
+
return { thumb: { ...thumb, glyph: "#" }, track: { ...track, glyph: "|" } };
|
|
166
|
+
}
|
|
167
|
+
/** The cells a scrollbar's gutter takes from the content: its wider glyph. */
|
|
168
|
+
function gutterWidth({ thumb, track }) {
|
|
169
|
+
return Math.max(cellLen(thumb.glyph), cellLen(track.glyph));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The rows of the track the thumb covers, from `start` up to but not including
|
|
173
|
+
* `end`: its length is the share of the content in view, and its position the
|
|
174
|
+
* share of the scroll travelled. Content that fits fills the track.
|
|
175
|
+
*
|
|
176
|
+
* An end of the track means an end of the content. Rounded alone, the position
|
|
177
|
+
* reached the bottom while lines were still hidden below — at 10 rows of 20
|
|
178
|
+
* lines, offset 9 rounds onto the last position. So an offset short of an end
|
|
179
|
+
* is held one row off it, wherever the track has a row between its ends.
|
|
180
|
+
*/
|
|
181
|
+
function thumbRows({ rows, lines }, offset) {
|
|
182
|
+
const size = Math.min(rows, Math.max(1, Math.round((rows * rows) / Math.max(lines, rows, 1))));
|
|
183
|
+
const travel = rows - size;
|
|
184
|
+
const last = Math.max(1, lines - rows);
|
|
185
|
+
const proportional = Math.round((offset * travel) / last);
|
|
186
|
+
const start = Math.max(Math.min(proportional, travel - Math.min(last - offset, 1)), Math.min(offset, 1, travel));
|
|
187
|
+
return { start, end: start + size };
|
|
188
|
+
}
|
|
189
|
+
/** The rows a viewport shows, from its budget, its own configured rows, and its content's length. */
|
|
190
|
+
function viewRows(height, rows, lines) {
|
|
191
|
+
return regionRows(height) ?? cellCount(Math.min(rows ?? lines, height?.rows ?? Infinity));
|
|
192
|
+
}
|
|
193
|
+
/** An offset held to the lines that exist: never above the first, never past the last full view. */
|
|
194
|
+
function clampOffset(offset, { rows, lines }) {
|
|
195
|
+
return cellCount(Math.min(offset, lines - rows));
|
|
196
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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` (or `#RRGGBBAA`), 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, type ColorRgba, type TerminalTheme } from "../core/color.js";
|
|
60
|
+
/**
|
|
61
|
+
* A colour the colour functions can compute with. A fully transparent one — a
|
|
62
|
+
* theme's "no background", `#00000000` — has no colour of its own: what shows
|
|
63
|
+
* through it is whatever surface it lands on, which a template cannot know.
|
|
64
|
+
* Darkening, mixing or contrasting it would answer for a surface nobody named,
|
|
65
|
+
* so it is refused here, for every colour function and every `ramp` stop.
|
|
66
|
+
* [LAW:single-enforcer]
|
|
67
|
+
*/
|
|
68
|
+
export declare function computable(colour: ColorRgba, func: string, written: string): ColorRgba;
|
|
69
|
+
/**
|
|
70
|
+
* Where `readableOn`'s text will be drawn: the depth the output is encoded at,
|
|
71
|
+
* and the terminal that draws it when that is known. At sixteen colours a
|
|
72
|
+
* terminal paints each slot in its own theme's colour, so the floor is measured
|
|
73
|
+
* in `terminal`'s sixteen; without one, in the VGA defaults (the Windows
|
|
74
|
+
* console's at `WINDOWS`). Both are facts about the one place the text lands,
|
|
75
|
+
* so they arrive together.
|
|
76
|
+
*/
|
|
77
|
+
export interface DrawnAt {
|
|
78
|
+
readonly depth: ColorDepth;
|
|
79
|
+
readonly terminal?: TerminalTheme;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The `readableOn` binding, measuring where `drawnAt` says the text lands when
|
|
83
|
+
* it is evaluated — a consumer that renders to a per-call destination
|
|
84
|
+
* registers its own.
|
|
85
|
+
*/
|
|
86
|
+
export declare const readableOnFunc: (drawnAt: () => DrawnAt) => TemplateFunc;
|
|
87
|
+
/**
|
|
88
|
+
* The palette-free color vocabulary: every function takes colors and returns a
|
|
89
|
+
* color, so they compose by nesting.
|
|
90
|
+
*
|
|
91
|
+
* | function | meaning |
|
|
92
|
+
* |---|---|
|
|
93
|
+
* | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
|
|
94
|
+
* | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
|
|
95
|
+
* | `contrastOn bg` | black or white, whichever is readable on `bg` |
|
|
96
|
+
* | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
|
|
97
|
+
* | `shiftHue c deg` | rotate hue in OKLCH |
|
|
98
|
+
* | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
|
|
99
|
+
* | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
|
|
100
|
+
* | `shiftLightness c d` | add to lightness, after any scale |
|
|
101
|
+
*
|
|
102
|
+
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
103
|
+
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
104
|
+
*
|
|
105
|
+
* @param drawnAt where the text will be drawn — the depth, and the terminal
|
|
106
|
+
* when known — read on every `readableOn` evaluation so a host that learns
|
|
107
|
+
* its destination per render passes one getter; defaults to truecolor.
|
|
108
|
+
*/
|
|
109
|
+
export declare function colorFuncs(drawnAt?: () => DrawnAt): FuncMap;
|
|
@@ -0,0 +1,243 @@
|
|
|
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` (or `#RRGGBBAA`), 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, ColorParseError, parseHexColor } from "../core/color.js";
|
|
59
|
+
import { Oklch, IDENTITY } from "../core/oklch.js";
|
|
60
|
+
import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
|
|
61
|
+
/**
|
|
62
|
+
* The gate every color argument crosses. [LAW:parse-dont-validate] — the
|
|
63
|
+
* function bodies below receive `ColorRgba` and never re-check.
|
|
64
|
+
*
|
|
65
|
+
* Parsing itself is `parseHexColor`'s job; this wrapper exists only to replace
|
|
66
|
+
* its error *wording*, because the mistake it catches is almost always the
|
|
67
|
+
* same one — a palette name passed where a color belongs (`darken "primary" 2`)
|
|
68
|
+
* — and an author who reads "wrap it: darken (color "primary")" is corrected in
|
|
69
|
+
* one step, where "expected #RRGGBB" leaves them guessing. What it parses
|
|
70
|
+
* must then be a colour these functions can compute with (`computable`).
|
|
71
|
+
* [LAW:no-silent-failure]
|
|
72
|
+
*/
|
|
73
|
+
function asColor(value, func) {
|
|
74
|
+
let colour;
|
|
75
|
+
try {
|
|
76
|
+
colour = parseHexColor(value.trim());
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
if (!(cause instanceof ColorParseError))
|
|
80
|
+
throw cause;
|
|
81
|
+
throw new TypeError(`${func} expected a color (#RRGGBB or #RRGGBBAA), got ${JSON.stringify(value)}` +
|
|
82
|
+
` — to use a palette name here, wrap it: ${func} (color ${JSON.stringify(value)}) …`, { cause });
|
|
83
|
+
}
|
|
84
|
+
return computable(colour, func, JSON.stringify(value));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A colour the colour functions can compute with. A fully transparent one — a
|
|
88
|
+
* theme's "no background", `#00000000` — has no colour of its own: what shows
|
|
89
|
+
* through it is whatever surface it lands on, which a template cannot know.
|
|
90
|
+
* Darkening, mixing or contrasting it would answer for a surface nobody named,
|
|
91
|
+
* so it is refused here, for every colour function and every `ramp` stop.
|
|
92
|
+
* [LAW:single-enforcer]
|
|
93
|
+
*/
|
|
94
|
+
export function computable(colour, func, written) {
|
|
95
|
+
if (colour.alpha === 0) {
|
|
96
|
+
throw new RangeError(`${func} got ${written}, which is fully transparent: it draws no colour of its own, ` +
|
|
97
|
+
`so there is nothing to compute with — use the colour of the surface it sits on, e.g. (color "background")`);
|
|
98
|
+
}
|
|
99
|
+
return colour;
|
|
100
|
+
}
|
|
101
|
+
// [LAW:single-enforcer] Every colour function is fixed-arity, so the engine's
|
|
102
|
+
// arity gate refuses a missing or surplus argument before the body runs.
|
|
103
|
+
function colorFunc(argTypes, fn) {
|
|
104
|
+
return { fn, argTypes, arity: { kind: "exact" }, returnType: "string" };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The gate every *numeric* argument crosses, with an optional inclusive range.
|
|
108
|
+
*
|
|
109
|
+
* [LAW:single-enforcer] One check for every `"float"` slot. An `"int"` slot is
|
|
110
|
+
* a safe integer by the engine's gate and needs none, but a `"float"` slot admits
|
|
111
|
+
* NaN and Infinity (legitimate IEEE-754 values), which would otherwise surface
|
|
112
|
+
* as `ColorRgba.red must be an integer in [0, 255]; got NaN` — loud, but
|
|
113
|
+
* pointing at a channel the author never wrote. [LAW:no-silent-failure]
|
|
114
|
+
*/
|
|
115
|
+
function asAmount(value, func, param, range) {
|
|
116
|
+
const inRange = range === undefined || (value >= range.min && value <= range.max);
|
|
117
|
+
if (!Number.isFinite(value) || !inRange) {
|
|
118
|
+
const bound = range === undefined
|
|
119
|
+
? "a finite number"
|
|
120
|
+
: `within ${range.min}..${range.max} (${range.note})`;
|
|
121
|
+
throw new RangeError(`${func}'s ${param} must be ${bound}, got ${value}`);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
// --- HSL lightness ---
|
|
126
|
+
//
|
|
127
|
+
// `lighten` is `darken` with the sign flipped — the same operation, not a
|
|
128
|
+
// second one (colorMath's `lighten` is literally `darken(c, -levels)`). Both
|
|
129
|
+
// names ship because "lighten by 2" and "darken by -2" are not equally
|
|
130
|
+
// readable at a call site, and the operation is genuinely bidirectional.
|
|
131
|
+
const darkenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "darken"), levels).hex));
|
|
132
|
+
const lightenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "lighten"), -levels).hex));
|
|
133
|
+
// --- Blending ---
|
|
134
|
+
//
|
|
135
|
+
// [LAW:one-type-per-behavior] There is no separate `alphaBlend` binding.
|
|
136
|
+
// `alphaBlend(fg, bg, a)` in colorMath is *defined* as `blendRgb(bg, fg, a)` —
|
|
137
|
+
// one operation wearing two names. Compositing a translucent color over a
|
|
138
|
+
// background and mixing two colors by a percentage are the same arithmetic,
|
|
139
|
+
// so they get one function. Terminals have no alpha channel, which is also why
|
|
140
|
+
// no function here *produces* a translucent color: it could not be rendered,
|
|
141
|
+
// and a color that silently does nothing in the sink is worse than no
|
|
142
|
+
// function at all.
|
|
143
|
+
const MIX_PCT_MAX = 100;
|
|
144
|
+
const MIX_RANGE = { min: 0, max: MIX_PCT_MAX, note: "0 = from, 100 = toward" };
|
|
145
|
+
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));
|
|
146
|
+
// --- Readability ---
|
|
147
|
+
//
|
|
148
|
+
// Two different questions, so two functions rather than one with a flag:
|
|
149
|
+
// `contrastOn` asks "what is maximally readable here" (black or white, WCAG
|
|
150
|
+
// relative-luminance cutoff); `readableOn` asks "keep this color recognizably
|
|
151
|
+
// itself, but guarantee it clears `ratio`" — it slides OKLCH lightness and
|
|
152
|
+
// preserves hue, so a blue on dark blue becomes a lighter blue rather than
|
|
153
|
+
// white.
|
|
154
|
+
//
|
|
155
|
+
// [LAW:no-silent-failure] `ratio` is required, not defaulted to WCAG AA's 4.5.
|
|
156
|
+
// The threshold is the whole decision this function makes, and the right value
|
|
157
|
+
// genuinely differs by intent — 4.5 for body text, 3.0 for large or
|
|
158
|
+
// deliberately recessive text. A hidden default would silently govern an
|
|
159
|
+
// accessibility outcome the author never saw, and would quietly over-correct a
|
|
160
|
+
// caller whose *point* was to be quiet: de-emphasized text floored at 4.5 is no
|
|
161
|
+
// longer de-emphasized.
|
|
162
|
+
// WCAG 2.x contrast ratios live in [1, 21] — 1 is a color on itself.
|
|
163
|
+
const RATIO_RANGE = {
|
|
164
|
+
min: 1,
|
|
165
|
+
max: 21,
|
|
166
|
+
note: "4.5 = AA body text, 3 = AA large text",
|
|
167
|
+
};
|
|
168
|
+
const contrastOnFunc = colorFunc(["string"], ((bgHex) => contrastFor(asColor(bgHex, "contrastOn")).hex));
|
|
169
|
+
const TRUECOLOR = { depth: ColorDepth.TRUECOLOR };
|
|
170
|
+
/**
|
|
171
|
+
* The `readableOn` binding, measuring where `drawnAt` says the text lands when
|
|
172
|
+
* it is evaluated — a consumer that renders to a per-call destination
|
|
173
|
+
* registers its own.
|
|
174
|
+
*/
|
|
175
|
+
export const readableOnFunc = (drawnAt) => colorFunc(["string", "string", "float"], ((fgHex, bgHex, ratio) => {
|
|
176
|
+
const { depth, terminal } = drawnAt();
|
|
177
|
+
return ensureContrast(asColor(fgHex, "readableOn"), asColor(bgHex, "readableOn"), asAmount(ratio, "readableOn", "ratio", RATIO_RANGE), depth, undefined, terminal).hex;
|
|
178
|
+
}));
|
|
179
|
+
// --- OKLCH axes ---
|
|
180
|
+
//
|
|
181
|
+
// [LAW:one-source-of-truth] One function per `ThemeKey` axis, and the table is
|
|
182
|
+
// keyed BY the axis so the compiler proves it total: add a field to `ThemeKey`
|
|
183
|
+
// and this object stops satisfying `Record<keyof ThemeKey, string>` until a
|
|
184
|
+
// binding name is chosen for it. The same derive-registrations-from-the-real-
|
|
185
|
+
// inventory trick `attributeFuncs()` uses over `ATTRIBUTE_NAMES`.
|
|
186
|
+
//
|
|
187
|
+
// Naming follows the axis semantics rather than the field spelling: `shift`
|
|
188
|
+
// for the additive axes, `scale` for the multiplicative ones, so a reader can
|
|
189
|
+
// tell from the call site whether `1` means "no change" or "one unit".
|
|
190
|
+
const OKLCH_AXIS_FUNCS = {
|
|
191
|
+
hueShift: "shiftHue",
|
|
192
|
+
chromaScale: "scaleChroma",
|
|
193
|
+
lightnessScale: "scaleLightness",
|
|
194
|
+
lightnessShift: "shiftLightness",
|
|
195
|
+
};
|
|
196
|
+
function oklchAxisFuncs() {
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const [axis, funcName] of Object.entries(OKLCH_AXIS_FUNCS)) {
|
|
199
|
+
// [LAW:dataflow-not-control-flow] Every axis takes the identical path —
|
|
200
|
+
// splice one value into the identity key and apply it. The axis is data.
|
|
201
|
+
const key = axis;
|
|
202
|
+
out[funcName] = colorFunc(["string", "float"], ((hex, amount) => {
|
|
203
|
+
const themeKey = {
|
|
204
|
+
...IDENTITY,
|
|
205
|
+
[key]: asAmount(amount, funcName, "amount"),
|
|
206
|
+
};
|
|
207
|
+
return Oklch.fromRgba(asColor(hex, funcName)).applyKey(themeKey).toRgba().hex;
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* The palette-free color vocabulary: every function takes colors and returns a
|
|
214
|
+
* color, so they compose by nesting.
|
|
215
|
+
*
|
|
216
|
+
* | function | meaning |
|
|
217
|
+
* |---|---|
|
|
218
|
+
* | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
|
|
219
|
+
* | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
|
|
220
|
+
* | `contrastOn bg` | black or white, whichever is readable on `bg` |
|
|
221
|
+
* | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
|
|
222
|
+
* | `shiftHue c deg` | rotate hue in OKLCH |
|
|
223
|
+
* | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
|
|
224
|
+
* | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
|
|
225
|
+
* | `shiftLightness c d` | add to lightness, after any scale |
|
|
226
|
+
*
|
|
227
|
+
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
228
|
+
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
229
|
+
*
|
|
230
|
+
* @param drawnAt where the text will be drawn — the depth, and the terminal
|
|
231
|
+
* when known — read on every `readableOn` evaluation so a host that learns
|
|
232
|
+
* its destination per render passes one getter; defaults to truecolor.
|
|
233
|
+
*/
|
|
234
|
+
export function colorFuncs(drawnAt = () => TRUECOLOR) {
|
|
235
|
+
return {
|
|
236
|
+
darken: darkenFunc,
|
|
237
|
+
lighten: lightenFunc,
|
|
238
|
+
mix: mixFunc,
|
|
239
|
+
contrastOn: contrastOnFunc,
|
|
240
|
+
readableOn: readableOnFunc(drawnAt),
|
|
241
|
+
...oklchAxisFuncs(),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internal helpers for the rich-js template binding layer.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:one-source-of-truth] `applyStyleToFragment` is the single implementation
|
|
5
|
+
* of "apply a Style to an existing RichText fragment." Both the style-function
|
|
6
|
+
* registrations and the palette-function registrations use this same operation;
|
|
7
|
+
* keeping one copy prevents the two from drifting.
|
|
8
|
+
*/
|
|
9
|
+
import { type Style } from "../core/style.js";
|
|
10
|
+
import { RichText } from "../core/text.js";
|
|
11
|
+
/**
|
|
12
|
+
* Apply a style on top of an already-styled RichText fragment.
|
|
13
|
+
*
|
|
14
|
+
* [LAW:single-enforcer] Type validation lives here; callers trust the result.
|
|
15
|
+
* The `"liftable"` arg type lifts string literals to `RichText` via the
|
|
16
|
+
* engine's `fromString` before the body runs, so `child` is always a
|
|
17
|
+
* `RichText` by the time this runs. The instanceof check exists because
|
|
18
|
+
* `"liftable"` admits any non-primitive — the engine cannot prove the object
|
|
19
|
+
* is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
|
|
20
|
+
* rather than producing a malformed fragment.
|
|
21
|
+
*
|
|
22
|
+
* Conflict resolution: `Style.add` — the outer (newly applied) style wins. A
|
|
23
|
+
* base style that is a name is layered under, not resolved: a template runs
|
|
24
|
+
* before the render whose theme says what the name stands for.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyStyleToFragment(child: unknown, style: Style): RichText;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internal helpers for the rich-js template binding layer.
|
|
3
|
+
*
|
|
4
|
+
* [LAW:one-source-of-truth] `applyStyleToFragment` is the single implementation
|
|
5
|
+
* of "apply a Style to an existing RichText fragment." Both the style-function
|
|
6
|
+
* registrations and the palette-function registrations use this same operation;
|
|
7
|
+
* keeping one copy prevents the two from drifting.
|
|
8
|
+
*/
|
|
9
|
+
import { layerStyle } from "../core/style.js";
|
|
10
|
+
import { RichText } from "../core/text.js";
|
|
11
|
+
/**
|
|
12
|
+
* Apply a style on top of an already-styled RichText fragment.
|
|
13
|
+
*
|
|
14
|
+
* [LAW:single-enforcer] Type validation lives here; callers trust the result.
|
|
15
|
+
* The `"liftable"` arg type lifts string literals to `RichText` via the
|
|
16
|
+
* engine's `fromString` before the body runs, so `child` is always a
|
|
17
|
+
* `RichText` by the time this runs. The instanceof check exists because
|
|
18
|
+
* `"liftable"` admits any non-primitive — the engine cannot prove the object
|
|
19
|
+
* is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
|
|
20
|
+
* rather than producing a malformed fragment.
|
|
21
|
+
*
|
|
22
|
+
* Conflict resolution: `Style.add` — the outer (newly applied) style wins. A
|
|
23
|
+
* base style that is a name is layered under, not resolved: a template runs
|
|
24
|
+
* before the render whose theme says what the name stands for.
|
|
25
|
+
*/
|
|
26
|
+
export function applyStyleToFragment(child, style) {
|
|
27
|
+
if (!(child instanceof RichText)) {
|
|
28
|
+
throw new TypeError(`template function expected a RichText fragment, got ${typeof child === "object" ? Object.prototype.toString.call(child) : typeof child}`);
|
|
29
|
+
}
|
|
30
|
+
const result = child.copy();
|
|
31
|
+
result.style = layerStyle(child.style, style);
|
|
32
|
+
return result;
|
|
33
|
+
}
|