@promptctl/rich-js 0.13.0 → 0.14.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.
@@ -31,6 +31,14 @@ export declare class ColorRgba {
31
31
  */
32
32
  compositeOver(bg: ColorRgba): ColorRgba;
33
33
  }
34
+ /**
35
+ * The surface a terminal draws a translucent colour over. A terminal cannot
36
+ * know what lies under its cells, so the SGR writer (`Style.toSgrCodes`), the
37
+ * strip's seam test and the contrast choosers (`themes/colorMath`) all
38
+ * composite over this one colour. [LAW:one-source-of-truth] One constant, so
39
+ * the colour text is chosen against is the colour the writer draws.
40
+ */
41
+ export declare const SURFACE_BLACK: ColorRgba;
34
42
  /**
35
43
  * WCAG 2.x relative luminance (0..1) of an opaque color. The single
36
44
  * luminance function in the codebase — `contrastFor`, `contrastRatio`, and
@@ -65,6 +65,14 @@ export class ColorRgba {
65
65
  return new ColorRgba(Math.round(bg.red + (this.red - bg.red) * t), Math.round(bg.green + (this.green - bg.green) * t), Math.round(bg.blue + (this.blue - bg.blue) * t), 1);
66
66
  }
67
67
  }
68
+ /**
69
+ * The surface a terminal draws a translucent colour over. A terminal cannot
70
+ * know what lies under its cells, so the SGR writer (`Style.toSgrCodes`), the
71
+ * strip's seam test and the contrast choosers (`themes/colorMath`) all
72
+ * composite over this one colour. [LAW:one-source-of-truth] One constant, so
73
+ * the colour text is chosen against is the colour the writer draws.
74
+ */
75
+ export const SURFACE_BLACK = new ColorRgba(0, 0, 0);
68
76
  /**
69
77
  * WCAG 2.x relative luminance (0..1) of an opaque color. The single
70
78
  * luminance function in the codebase — `contrastFor`, `contrastRatio`, and
@@ -26,8 +26,8 @@
26
26
  * styling, so the cell type does too.
27
27
  */
28
28
  import { Segment } from "./segment.js";
29
- import { Style, SURFACE_BLACK } from "./style.js";
30
- import { ColorSpec, blendRgb } from "./color.js";
29
+ import { Style } from "./style.js";
30
+ import { ColorSpec, SURFACE_BLACK, blendRgb } from "./color.js";
31
31
  import { Oklch } from "./oklch.js";
32
32
  // --- Strip ---
33
33
  export class Strip {
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Immutable style descriptors — colors, text attributes, links, metadata.
3
3
  */
4
- import { ColorRgba, ColorSpec, ColorDepth } from "./color.js";
5
- export declare const SURFACE_BLACK: ColorRgba;
4
+ import { ColorSpec, ColorDepth } from "./color.js";
6
5
  /**
7
6
  * Canonical text-attribute inventory. Single source of truth consumed by
8
7
  * `Style.parse` / `Style.toString` and the template bindings — adding an
@@ -1,14 +1,11 @@
1
1
  /**
2
2
  * Immutable style descriptors — colors, text attributes, links, metadata.
3
3
  */
4
- import { COLOR_NAMES, ColorRgba, ColorSpec, } from "./color.js";
4
+ import { COLOR_NAMES, ColorSpec, SURFACE_BLACK, } from "./color.js";
5
5
  import { OSC8_CLOSE, osc8Open } from "./osc8.js";
6
6
  // [LAW:one-way-deps] `core/style` depends only on `core/color` and the leaf
7
- // `core/osc8` (the link wire grammar). The substrate fallback is the
8
- // canonical canvas color (black), inlined to avoid pulling in any preset
9
- // theme constants. Preset themes live in `src/themes/` and depend on core,
10
- // never the reverse.
11
- export const SURFACE_BLACK = new ColorRgba(0, 0, 0);
7
+ // `core/osc8` (the link wire grammar). The substrate a translucent colour is
8
+ // flattened over is `SURFACE_BLACK`, defined beside `compositeOver`.
12
9
  // --- Attribute definitions ---
13
10
  /**
14
11
  * Canonical text-attribute inventory. Single source of truth consumed by
@@ -17,9 +17,10 @@ export declare function alphaBlend(fg: ColorRgba, bg: ColorRgba, alpha: number):
17
17
  /**
18
18
  * Pick a contrasting foreground (black or white) for a background, using the
19
19
  * WCAG relative-luminance threshold of 0.179 (the perceptually correct cutoff
20
- * where black and white are equally readable).
20
+ * where black and white are equally readable). A translucent `bg` is judged
21
+ * as drawn: composited over `substrate` (see `drawnBackground`).
21
22
  */
22
- export declare function contrastFor(bg: ColorRgba): ColorRgba;
23
+ export declare function contrastFor(bg: ColorRgba, substrate?: ColorRgba): ColorRgba;
23
24
  export { relativeLuminance, contrastRatio };
24
25
  /**
25
26
  * Return a foreground guaranteed to clear `minRatio` against `bg`, keeping the
@@ -32,10 +33,11 @@ export { relativeLuminance, contrastRatio };
32
33
  * background where even pure black-or-white tops out below the target) does it
33
34
  * fall back to `contrastFor`'s black/white — the true maximum-contrast pick.
34
35
  *
35
- * A translucent `fg` is flattened over `bg` first (the displayed color is
36
- * `fg` composited over `bg`), so the ratio is measured on what the eye
37
- * actually sees and the returned color is opaque. `bg` is treated as the
38
- * opaque substrate.
36
+ * A translucent `bg` is measured as it is drawn — composited over
37
+ * `substrate`, the SGR writer's black by default — and a translucent `fg` is
38
+ * then flattened over that drawn background, the order the writer composites
39
+ * in, so the ratio is measured on what the eye actually sees and the returned
40
+ * color is opaque.
39
41
  *
40
42
  * `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
41
43
  * the terminal rounds text and background independently, and two roundings
@@ -51,4 +53,4 @@ export { relativeLuminance, contrastRatio };
51
53
  * lightness moves — there is no caller-side "should I check contrast" branch.
52
54
  */
53
55
  export declare function ensureContrast(fg: ColorRgba, bg: ColorRgba, minRatio?: number, // WCAG AA for normal text
54
- drawnAt?: ColorDepth): ColorRgba;
56
+ drawnAt?: ColorDepth, substrate?: ColorRgba): ColorRgba;
@@ -1,4 +1,4 @@
1
- import { ColorDepth, ColorRgba, EIGHT_BIT_DOWNGRADE_TABLE, blendRgb, contrastRatio, relativeLuminance, } from "../core/color.js";
1
+ import { ColorDepth, ColorRgba, EIGHT_BIT_DOWNGRADE_TABLE, blendRgb, contrastRatio, relativeLuminance, SURFACE_BLACK, } from "../core/color.js";
2
2
  import { Oklch } from "../core/oklch.js";
3
3
  const LEVEL_STEP = 0.1;
4
4
  function rgbToHsl(c) {
@@ -82,10 +82,11 @@ export function alphaBlend(fg, bg, alpha) {
82
82
  /**
83
83
  * Pick a contrasting foreground (black or white) for a background, using the
84
84
  * WCAG relative-luminance threshold of 0.179 (the perceptually correct cutoff
85
- * where black and white are equally readable).
85
+ * where black and white are equally readable). A translucent `bg` is judged
86
+ * as drawn: composited over `substrate` (see `drawnBackground`).
86
87
  */
87
- export function contrastFor(bg) {
88
- const lum = relativeLuminance(bg);
88
+ export function contrastFor(bg, substrate = SURFACE_BLACK) {
89
+ const lum = relativeLuminance(drawnBackground(bg, substrate));
89
90
  return lum > 0.179
90
91
  ? new ColorRgba(0, 0, 0)
91
92
  : new ColorRgba(255, 255, 255);
@@ -109,10 +110,11 @@ const CONTRAST_ITERS = 20;
109
110
  * background where even pure black-or-white tops out below the target) does it
110
111
  * fall back to `contrastFor`'s black/white — the true maximum-contrast pick.
111
112
  *
112
- * A translucent `fg` is flattened over `bg` first (the displayed color is
113
- * `fg` composited over `bg`), so the ratio is measured on what the eye
114
- * actually sees and the returned color is opaque. `bg` is treated as the
115
- * opaque substrate.
113
+ * A translucent `bg` is measured as it is drawn — composited over
114
+ * `substrate`, the SGR writer's black by default — and a translucent `fg` is
115
+ * then flattened over that drawn background, the order the writer composites
116
+ * in, so the ratio is measured on what the eye actually sees and the returned
117
+ * color is opaque.
116
118
  *
117
119
  * `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
118
120
  * the terminal rounds text and background independently, and two roundings
@@ -128,19 +130,40 @@ const CONTRAST_ITERS = 20;
128
130
  * lightness moves — there is no caller-side "should I check contrast" branch.
129
131
  */
130
132
  export function ensureContrast(fg, bg, minRatio = 4.5, // WCAG AA for normal text
131
- drawnAt = ColorDepth.TRUECOLOR) {
132
- const chosen = ensureTruecolorContrast(fg, bg, minRatio);
133
+ drawnAt = ColorDepth.TRUECOLOR, substrate = SURFACE_BLACK) {
134
+ const ground = drawnBackground(bg, substrate);
135
+ const chosen = ensureTruecolorContrast(fg, ground, minRatio);
133
136
  // [LAW:dataflow-not-control-flow] The depth names the table the terminal
134
137
  // draws from; only one whose entries have a known RGB can be measured.
135
138
  const table = MEASURABLE_DOWNGRADE[drawnAt];
136
139
  if (table === undefined)
137
140
  return chosen;
138
- const drawnBg = table.get(table.match(bg));
141
+ const drawnBg = table.get(table.match(ground));
139
142
  const drawn = table.get(table.match(chosen));
140
143
  if (contrastRatio(drawn, drawnBg) >= minRatio)
141
144
  return chosen;
142
145
  return table.get(table.matchReadable(chosen, drawnBg, minRatio));
143
146
  }
147
+ /**
148
+ * A background as it is drawn: composited over the surface beneath it. That
149
+ * surface is a fact about where the pair is drawn, so it arrives as a value:
150
+ * the SGR writer (`Style.toSgrCodes`) composites over `SURFACE_BLACK`, the
151
+ * default here; a caller choosing text for a different surface — an export's
152
+ * canvas, `exportCanvas(theme).background` — names that one.
153
+ * [LAW:no-silent-failure] A surface has nothing under it, so a translucent one
154
+ * has no drawn colour to offer; `compositeOver` would read its raw RGB as if
155
+ * it were opaque, so it is refused here rather than measured wrong.
156
+ * [LAW:one-source-of-truth] Text is chosen against the colour the surface will
157
+ * show — measuring the raw RGBA reads a colour that is drawn nowhere, and text
158
+ * that "clears" it can land below the floor. Opaque colours composite to
159
+ * themselves.
160
+ */
161
+ function drawnBackground(bg, substrate) {
162
+ if (substrate.alpha !== 1) {
163
+ throw new RangeError(`a contrast substrate is the opaque surface under a translucent background; got ${substrate.hex}`);
164
+ }
165
+ return bg.compositeOver(substrate);
166
+ }
144
167
  /**
145
168
  * The downgrade tables whose entries the terminal draws at a known RGB, by the
146
169
  * depth that draws from them. 256 colours is the one: its cube and grey ramp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptctl/rich-js",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Rich text and beautiful formatting in the terminal — a TypeScript port of Python's Rich",
5
5
  "type": "module",
6
6
  "sideEffects": false,