silvery 0.19.2 → 0.21.1
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 -4
- package/dist/Text-BRf-59j2.mjs +237 -0
- package/dist/Text-BRf-59j2.mjs.map +1 -0
- package/dist/ag-BeFC4S2N.mjs +8727 -0
- package/dist/ag-BeFC4S2N.mjs.map +1 -0
- package/dist/{animation-Cn64yepo.mjs → animation-N8MFybTk.mjs} +2 -2
- package/dist/animation-N8MFybTk.mjs.map +1 -0
- package/dist/{ansi-CLOitHKx.mjs → ansi-C6Qs1Wn2.mjs} +1 -1
- package/dist/{ansi-CLOitHKx.mjs.map → ansi-C6Qs1Wn2.mjs.map} +1 -1
- package/dist/{ansi-Cc33mW54.d.mts → ansi-CBkam1ty.d.mts} +1 -1
- package/dist/{ansi-Cc33mW54.d.mts.map → ansi-CBkam1ty.d.mts.map} +1 -1
- package/dist/ansi-OYRLxxZQ.mjs +10669 -0
- package/dist/ansi-OYRLxxZQ.mjs.map +1 -0
- package/dist/bound-term-BumfuXXW.d.mts +4902 -0
- package/dist/bound-term-BumfuXXW.d.mts.map +1 -0
- package/dist/{chunk-Vs_PY4HZ.mjs → chunk-hT5z_Zn9.mjs} +1 -3
- package/dist/cli-DG6zsfsS.mjs +4 -0
- package/dist/context-BU5LkkIy.mjs.map +1 -1
- package/dist/{devtools-DxkSLXDA.mjs → devtools-9zfhpFyG.mjs} +3 -3
- package/dist/{devtools-DxkSLXDA.mjs.map → devtools-9zfhpFyG.mjs.map} +1 -1
- package/dist/devtools-nmPUmYU_.mjs +2 -0
- package/dist/easing-B0oZKDki.d.mts +24 -0
- package/dist/easing-B0oZKDki.d.mts.map +1 -0
- package/dist/{eta-Bb3RH3wh.mjs → eta-DGOuC8yU.mjs} +5 -1
- package/dist/{eta-Bb3RH3wh.mjs.map → eta-DGOuC8yU.mjs.map} +1 -1
- package/dist/flexily-zero-adapter-CEJOcbNp.mjs +306 -0
- package/dist/flexily-zero-adapter-CEJOcbNp.mjs.map +1 -0
- package/dist/{flexily-zero-adapter-CMxXhdOL.mjs → flexily-zero-adapter-D6hcFgrH.mjs} +1 -1
- package/dist/image-1nRyKa60.mjs +5960 -0
- package/dist/image-1nRyKa60.mjs.map +1 -0
- package/dist/{index-D3saHouR.d.mts → index-2E1jYgak.d.mts} +1057 -1133
- package/dist/index-2E1jYgak.d.mts.map +1 -0
- package/dist/index-Bi4Jdz5g.d.mts +453 -0
- package/dist/index-Bi4Jdz5g.d.mts.map +1 -0
- package/dist/index-Dg1YaeJb.d.mts +336 -0
- package/dist/index-Dg1YaeJb.d.mts.map +1 -0
- package/dist/{index-BXslOebb.d.mts → index-DnuadDNL.d.mts} +5750 -4158
- package/dist/index-DnuadDNL.d.mts.map +1 -0
- package/dist/index.d.mts +8 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +16 -12
- package/dist/index.mjs.map +1 -1
- package/dist/layout-engine-Ca_nbtfL.mjs +67 -0
- package/dist/layout-engine-Ca_nbtfL.mjs.map +1 -0
- package/dist/{layout-engine-B6Cdz1yZ.mjs → layout-engine-CgsoBRIn.mjs} +1 -1
- package/dist/layout-signals-Dch2EiCy.mjs +1111 -0
- package/dist/layout-signals-Dch2EiCy.mjs.map +1 -0
- package/dist/mouse-events-hnbJZRwK.mjs +1071 -0
- package/dist/mouse-events-hnbJZRwK.mjs.map +1 -0
- package/dist/{multi-progress-DAQC7eap.d.mts → multi-progress-Bg4ngK80.d.mts} +2 -2
- package/dist/{multi-progress-DAQC7eap.d.mts.map → multi-progress-Bg4ngK80.d.mts.map} +1 -1
- package/dist/{multi-progress-Bq9Oi_WI.mjs → multi-progress-CaXTuL9G.mjs} +3 -3
- package/dist/{multi-progress-Bq9Oi_WI.mjs.map → multi-progress-CaXTuL9G.mjs.map} +1 -1
- package/dist/{node-BeWlnCPY.mjs → node-BiFu8I9Y.mjs} +4 -4
- package/dist/node-BiFu8I9Y.mjs.map +1 -0
- package/dist/progress-B_UPy6zk.mjs +675 -0
- package/dist/progress-B_UPy6zk.mjs.map +1 -0
- package/dist/{progress-bar-CXE5Qfkd.mjs → progress-bar-DmIMPdL0.mjs} +4 -4
- package/dist/{progress-bar-CXE5Qfkd.mjs.map → progress-bar-DmIMPdL0.mjs.map} +1 -1
- package/dist/reconciler-NBDSEm8k.mjs +2178 -0
- package/dist/reconciler-NBDSEm8k.mjs.map +1 -0
- package/dist/render-string-B4h4SmK7.mjs +211 -0
- package/dist/render-string-B4h4SmK7.mjs.map +1 -0
- package/dist/{render-string-CDCeYkS3.mjs → render-string-BntLj7Xq.mjs} +1 -1
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.mjs +3 -3
- package/dist/{src-B5GjfG7g.mjs → schemes-DYt2ushj.mjs} +23 -1812
- package/dist/schemes-DYt2ushj.mjs.map +1 -0
- package/dist/{spinner-CGo34vyR.d.mts → spinner-CLgzJ_QF.d.mts} +2 -2
- package/dist/{spinner-CGo34vyR.d.mts.map → spinner-CLgzJ_QF.d.mts.map} +1 -1
- package/dist/{spinner-CeOmcuw_.mjs → spinner-Py8_-hn9.mjs} +23 -8
- package/dist/spinner-Py8_-hn9.mjs.map +1 -0
- package/dist/src-B9S_woYc.mjs +25024 -0
- package/dist/src-B9S_woYc.mjs.map +1 -0
- package/dist/src-CbhWmUnF.mjs +3928 -0
- package/dist/src-CbhWmUnF.mjs.map +1 -0
- package/dist/src-Dvq-s8iD.mjs +939 -0
- package/dist/src-Dvq-s8iD.mjs.map +1 -0
- package/dist/src-Oe6x5PrS.mjs +4621 -0
- package/dist/src-Oe6x5PrS.mjs.map +1 -0
- package/dist/{types-Bk2yw9Qj.mjs → src-WeA_J4BV.mjs} +34 -94
- package/dist/src-WeA_J4BV.mjs.map +1 -0
- package/dist/steps-DYrzCUCK.d.mts +202 -0
- package/dist/steps-DYrzCUCK.d.mts.map +1 -0
- package/dist/svg-DhxQkz-O.mjs +255 -0
- package/dist/svg-DhxQkz-O.mjs.map +1 -0
- package/dist/svg-Hk7lIl4F.d.mts +82 -0
- package/dist/svg-Hk7lIl4F.d.mts.map +1 -0
- package/dist/term.d.mts +3 -0
- package/dist/term.mjs +4 -0
- package/dist/theme.d.mts +95 -2
- package/dist/theme.d.mts.map +1 -0
- package/dist/theme.mjs +4 -3
- package/dist/{types-BH_v3iMT.d.mts → types-Bx-XZNbE.d.mts} +2 -15
- package/dist/types-Bx-XZNbE.d.mts.map +1 -0
- package/dist/ui/animation.d.mts +2 -1
- package/dist/ui/animation.mjs +1 -1
- package/dist/ui/ansi.d.mts +1 -1
- package/dist/ui/ansi.mjs +1 -1
- package/dist/ui/cli.d.mts +3 -3
- package/dist/ui/cli.mjs +5 -5
- package/dist/ui/display.d.mts +1 -1
- package/dist/ui/display.mjs.map +1 -1
- package/dist/ui/image.d.mts +2 -2
- package/dist/ui/image.mjs +2 -2
- package/dist/ui/input.d.mts +1 -1
- package/dist/ui/input.mjs.map +1 -1
- package/dist/ui/progress.d.mts +5 -249
- package/dist/ui/progress.mjs +5 -858
- package/dist/ui/react.d.mts +1 -1
- package/dist/ui/react.mjs +2 -2
- package/dist/ui/react.mjs.map +1 -1
- package/dist/ui/recording-chrome-react.d.mts +21 -0
- package/dist/ui/recording-chrome-react.d.mts.map +1 -0
- package/dist/ui/recording-chrome-react.mjs +105 -0
- package/dist/ui/recording-chrome-react.mjs.map +1 -0
- package/dist/ui/recording-chrome.d.mts +2 -0
- package/dist/ui/recording-chrome.mjs +2 -0
- package/dist/ui/utils.mjs +1 -1
- package/dist/ui/wrappers.d.mts +3 -3
- package/dist/ui/wrappers.mjs +2 -2
- package/dist/ui.d.mts +7 -6
- package/dist/ui.mjs +8 -7
- package/dist/{useLatest-Bg2x4bfP.d.mts → useLatest-DC8i7guK.d.mts} +5 -25
- package/dist/useLatest-DC8i7guK.d.mts.map +1 -0
- package/dist/useLayout-BKsQl2Or.mjs +424 -0
- package/dist/useLayout-BKsQl2Or.mjs.map +1 -0
- package/dist/{with-text-input-CRfoiFFG.d.mts → with-text-input-DG4f7JII.d.mts} +4 -55
- package/dist/with-text-input-DG4f7JII.d.mts.map +1 -0
- package/dist/wrapper-D7gNSsgf.mjs +3589 -0
- package/dist/wrapper-D7gNSsgf.mjs.map +1 -0
- package/dist/{wrappers-UTADQkSY.mjs → wrappers-CypAzrMO.mjs} +19 -161
- package/dist/wrappers-CypAzrMO.mjs.map +1 -0
- package/dist/yoga-adapter-1ex8r0ws.mjs +2 -0
- package/dist/{yoga-adapter-8oRGRw8V.mjs → yoga-adapter-SsEIqMc1.mjs} +28 -2
- package/dist/yoga-adapter-SsEIqMc1.mjs.map +1 -0
- package/package.json +63 -12
- package/dist/animation-Cn64yepo.mjs.map +0 -1
- package/dist/cli-BKp0YtBD.mjs +0 -4
- package/dist/devtools-9QY4teqI.mjs +0 -2
- package/dist/flexily-zero-adapter-BlQa46nr.mjs +0 -3385
- package/dist/flexily-zero-adapter-BlQa46nr.mjs.map +0 -1
- package/dist/image-CTII5QWI.mjs +0 -477
- package/dist/image-CTII5QWI.mjs.map +0 -1
- package/dist/index-BXslOebb.d.mts.map +0 -1
- package/dist/index-BnA7mNpo.d.mts +0 -175
- package/dist/index-BnA7mNpo.d.mts.map +0 -1
- package/dist/index-D3saHouR.d.mts.map +0 -1
- package/dist/layout-engine-ClUgv6jB.mjs +0 -50
- package/dist/layout-engine-ClUgv6jB.mjs.map +0 -1
- package/dist/node-BeWlnCPY.mjs.map +0 -1
- package/dist/reconciler-Cwgm8hRR.mjs +0 -8459
- package/dist/reconciler-Cwgm8hRR.mjs.map +0 -1
- package/dist/render-string-Darrg7ku.mjs +0 -5529
- package/dist/render-string-Darrg7ku.mjs.map +0 -1
- package/dist/spinner-CeOmcuw_.mjs.map +0 -1
- package/dist/src-B5GjfG7g.mjs.map +0 -1
- package/dist/src-CChwjk0Z.mjs +0 -738
- package/dist/src-CChwjk0Z.mjs.map +0 -1
- package/dist/src-CF-6UN01.mjs +0 -19434
- package/dist/src-CF-6UN01.mjs.map +0 -1
- package/dist/src-NCKb8kE5.mjs +0 -2660
- package/dist/src-NCKb8kE5.mjs.map +0 -1
- package/dist/types-BH_v3iMT.d.mts.map +0 -1
- package/dist/types-Bk2yw9Qj.mjs.map +0 -1
- package/dist/ui/progress.d.mts.map +0 -1
- package/dist/ui/progress.mjs.map +0 -1
- package/dist/useLatest-Bg2x4bfP.d.mts.map +0 -1
- package/dist/with-text-input-CRfoiFFG.d.mts.map +0 -1
- package/dist/wrappers-UTADQkSY.mjs.map +0 -1
- package/dist/yoga-adapter-8oRGRw8V.mjs.map +0 -1
- package/dist/yoga-adapter-D_CcxSt5.mjs +0 -2
|
@@ -0,0 +1,4621 @@
|
|
|
1
|
+
import { blend, brighten, checkContrast, colorDistance, complement, contrastFg, darken, deltaE, ensureContrast, hexToOklch, hexToRgb as hexToRgb$1, oklchToHex, relativeLuminance } from "@silvery/color";
|
|
2
|
+
import "string-width";
|
|
3
|
+
//#region packages/ansi/src/constants.ts
|
|
4
|
+
/**
|
|
5
|
+
* ANSI escape code constants for extended terminal features.
|
|
6
|
+
*
|
|
7
|
+
* @see https://sw.kovidgoyal.net/kitty/underlines/
|
|
8
|
+
* @see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Extended underline style codes.
|
|
12
|
+
* Uses colon-separated parameters (ISO 8613-6): \x1b[4:Nm
|
|
13
|
+
*/
|
|
14
|
+
const UNDERLINE_CODES = {
|
|
15
|
+
/** No underline */
|
|
16
|
+
none: "\x1B[4:0m",
|
|
17
|
+
/** Standard single underline */
|
|
18
|
+
single: "\x1B[4:1m",
|
|
19
|
+
/** Double underline (two parallel lines) */
|
|
20
|
+
double: "\x1B[4:2m",
|
|
21
|
+
/** Curly/wavy underline (spell check style) */
|
|
22
|
+
curly: "\x1B[4:3m",
|
|
23
|
+
/** Dotted underline */
|
|
24
|
+
dotted: "\x1B[4:4m",
|
|
25
|
+
/** Dashed underline */
|
|
26
|
+
dashed: "\x1B[4:5m",
|
|
27
|
+
/** Reset extended underline (same as none) */
|
|
28
|
+
reset: "\x1B[4:0m"
|
|
29
|
+
};
|
|
30
|
+
/** Standard underline on (SGR 4) - works on all terminals */
|
|
31
|
+
const UNDERLINE_STANDARD = "\x1B[4m";
|
|
32
|
+
/** Standard underline off (SGR 24) */
|
|
33
|
+
const UNDERLINE_RESET_STANDARD = "\x1B[24m";
|
|
34
|
+
/**
|
|
35
|
+
* Reset underline color to default (SGR 59)
|
|
36
|
+
*/
|
|
37
|
+
const UNDERLINE_COLOR_RESET = "\x1B[59m";
|
|
38
|
+
/**
|
|
39
|
+
* Build underline color escape code for RGB values.
|
|
40
|
+
* Format: \x1b[58:2::r:g:bm (SGR 58 with RGB color space)
|
|
41
|
+
*/
|
|
42
|
+
function buildUnderlineColorCode(r, g, b) {
|
|
43
|
+
return `\x1b[58:2::${r}:${g}:${b}m`;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region packages/ansi/src/caps.ts
|
|
47
|
+
/**
|
|
48
|
+
* Default capabilities — modern-terminal-ish defaults for headless / emulator /
|
|
49
|
+
* unknown contexts. Heuristic fields (`maybe*`) bake in "probably dark, no
|
|
50
|
+
* nerd font, wide emojis like Ghostty/iTerm" — callers override via
|
|
51
|
+
* `createTerminalProfile({caps})`.
|
|
52
|
+
*/
|
|
53
|
+
function defaultCaps() {
|
|
54
|
+
return {
|
|
55
|
+
cursor: false,
|
|
56
|
+
input: false,
|
|
57
|
+
colorLevel: "truecolor",
|
|
58
|
+
colorForced: false,
|
|
59
|
+
colorProvenance: "auto",
|
|
60
|
+
unicode: true,
|
|
61
|
+
underlineStyles: [
|
|
62
|
+
"double",
|
|
63
|
+
"curly",
|
|
64
|
+
"dotted",
|
|
65
|
+
"dashed"
|
|
66
|
+
],
|
|
67
|
+
underlineColor: true,
|
|
68
|
+
overline: true,
|
|
69
|
+
textSizing: false,
|
|
70
|
+
kittyKeyboard: false,
|
|
71
|
+
bracketedPaste: true,
|
|
72
|
+
mouse: true,
|
|
73
|
+
kittyGraphics: false,
|
|
74
|
+
sixel: false,
|
|
75
|
+
osc52: false,
|
|
76
|
+
hyperlinks: false,
|
|
77
|
+
notifications: false,
|
|
78
|
+
syncOutput: false,
|
|
79
|
+
maybeDarkBackground: true,
|
|
80
|
+
maybeNerdFont: false,
|
|
81
|
+
maybeWideEmojis: true
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region packages/ansi/src/emulator.ts
|
|
86
|
+
/**
|
|
87
|
+
* Default emulator identity — unknown terminal, unversioned, no `TERM` set.
|
|
88
|
+
* Matches what a non-TTY Node process sees when run from CI without env vars.
|
|
89
|
+
*/
|
|
90
|
+
function defaultEmulator() {
|
|
91
|
+
return {
|
|
92
|
+
program: "",
|
|
93
|
+
version: "",
|
|
94
|
+
TERM: ""
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region packages/ansi/src/theme/invariants.ts
|
|
99
|
+
/**
|
|
100
|
+
* Theme invariants — post-derivation visibility + optional WCAG checks.
|
|
101
|
+
*
|
|
102
|
+
* Two independent invariant groups:
|
|
103
|
+
*
|
|
104
|
+
* 1. **Visibility (always checked)** — selection and cursor must be
|
|
105
|
+
* distinguishable from bg. These fail silently because ensureContrast
|
|
106
|
+
* doesn't touch `bg-selected`/`bg-cursor`; you get "invisible selection"
|
|
107
|
+
* bugs that only surface via user complaints.
|
|
108
|
+
*
|
|
109
|
+
* 2. **WCAG contrast (opt-in)** — `deriveTheme()` already runs `ensureContrast`
|
|
110
|
+
* on every text/bg pair as it builds the Theme (lenient auto-adjust). A
|
|
111
|
+
* second validation pass is redundant for normal use. Enable it explicitly
|
|
112
|
+
* at build time to *verify* that shipped themes meet the targets, or when
|
|
113
|
+
* loading a hand-authored Theme object that skipped `deriveTheme`.
|
|
114
|
+
*
|
|
115
|
+
* This is why `validateThemeInvariants` defaults to `{ wcag: false }` — the
|
|
116
|
+
* existing derivation already handles contrast. Callers opt in via
|
|
117
|
+
* `validateThemeInvariants(theme, { wcag: true })` for strict pre-ship audits.
|
|
118
|
+
*
|
|
119
|
+
* All token access is Sterling-shaped — flat hyphen keys (`bg-accent`,
|
|
120
|
+
* `fg-on-error`, `border-focus`) exist on every Theme via the derive +
|
|
121
|
+
* bakeFlat pipeline. No concat-kebab legacy names (`primaryfg`, `mutedbg`, …)
|
|
122
|
+
* — those were removed in silvery 0.19.0 (Sterling interior migration).
|
|
123
|
+
*/
|
|
124
|
+
const AA_RATIO = 4.5;
|
|
125
|
+
const FAINT_RATIO = 1.5;
|
|
126
|
+
const SELECTION_DELTA_L$1 = .08;
|
|
127
|
+
const CURSOR_DELTA_E$1 = .15;
|
|
128
|
+
/**
|
|
129
|
+
* Contrast invariant pairs — all Sterling flat-token keys.
|
|
130
|
+
*
|
|
131
|
+
* These keys are populated on every Theme at derivation time via `bakeFlat`;
|
|
132
|
+
* bracket access (`theme["bg-accent"]`) is the canonical lookup form and is
|
|
133
|
+
* what `validateThemeInvariants` uses below.
|
|
134
|
+
*/
|
|
135
|
+
const CONTRAST_PAIRS = [
|
|
136
|
+
{
|
|
137
|
+
rule: "contrast:fg/bg",
|
|
138
|
+
fg: "fg",
|
|
139
|
+
bg: "bg",
|
|
140
|
+
min: AA_RATIO
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
rule: "contrast:fg/bg-surface-default",
|
|
144
|
+
fg: "fg",
|
|
145
|
+
bg: "bg-surface-default",
|
|
146
|
+
min: AA_RATIO
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
rule: "contrast:fg/bg-surface-subtle",
|
|
150
|
+
fg: "fg",
|
|
151
|
+
bg: "bg-surface-subtle",
|
|
152
|
+
min: AA_RATIO
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
rule: "contrast:fg/bg-surface-raised",
|
|
156
|
+
fg: "fg",
|
|
157
|
+
bg: "bg-surface-raised",
|
|
158
|
+
min: AA_RATIO
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
rule: "contrast:fg/bg-surface-hover",
|
|
162
|
+
fg: "fg",
|
|
163
|
+
bg: "bg-surface-hover",
|
|
164
|
+
min: AA_RATIO
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
rule: "contrast:fg/bg-surface-overlay",
|
|
168
|
+
fg: "fg",
|
|
169
|
+
bg: "bg-surface-overlay",
|
|
170
|
+
min: AA_RATIO
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
rule: "contrast:fg/bg-muted",
|
|
174
|
+
fg: "fg",
|
|
175
|
+
bg: "bg-muted",
|
|
176
|
+
min: AA_RATIO
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
rule: "contrast:fg-muted/bg",
|
|
180
|
+
fg: "fg-muted",
|
|
181
|
+
bg: "bg",
|
|
182
|
+
min: 3
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
rule: "contrast:fg-muted/bg-muted",
|
|
186
|
+
fg: "fg-muted",
|
|
187
|
+
bg: "bg-muted",
|
|
188
|
+
min: 3
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
rule: "contrast:fg-accent/bg",
|
|
192
|
+
fg: "fg-accent",
|
|
193
|
+
bg: "bg",
|
|
194
|
+
min: AA_RATIO
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
rule: "contrast:fg-error/bg",
|
|
198
|
+
fg: "fg-error",
|
|
199
|
+
bg: "bg",
|
|
200
|
+
min: AA_RATIO
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
rule: "contrast:fg-warning/bg",
|
|
204
|
+
fg: "fg-warning",
|
|
205
|
+
bg: "bg",
|
|
206
|
+
min: AA_RATIO
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
rule: "contrast:fg-success/bg",
|
|
210
|
+
fg: "fg-success",
|
|
211
|
+
bg: "bg",
|
|
212
|
+
min: AA_RATIO
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
rule: "contrast:fg-info/bg",
|
|
216
|
+
fg: "fg-info",
|
|
217
|
+
bg: "bg",
|
|
218
|
+
min: AA_RATIO
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
rule: "contrast:fg-on-accent/bg-accent",
|
|
222
|
+
fg: "fg-on-accent",
|
|
223
|
+
bg: "bg-accent",
|
|
224
|
+
min: AA_RATIO
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
rule: "contrast:fg-on-error/bg-error",
|
|
228
|
+
fg: "fg-on-error",
|
|
229
|
+
bg: "bg-error",
|
|
230
|
+
min: AA_RATIO
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
rule: "contrast:fg-on-warning/bg-warning",
|
|
234
|
+
fg: "fg-on-warning",
|
|
235
|
+
bg: "bg-warning",
|
|
236
|
+
min: AA_RATIO
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
rule: "contrast:fg-on-success/bg-success",
|
|
240
|
+
fg: "fg-on-success",
|
|
241
|
+
bg: "bg-success",
|
|
242
|
+
min: AA_RATIO
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
rule: "contrast:fg-on-info/bg-info",
|
|
246
|
+
fg: "fg-on-info",
|
|
247
|
+
bg: "bg-info",
|
|
248
|
+
min: AA_RATIO
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
rule: "contrast:fg-on-selected/bg-selected",
|
|
252
|
+
fg: "fg-on-selected",
|
|
253
|
+
bg: "bg-selected",
|
|
254
|
+
min: AA_RATIO
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
rule: "contrast:fg-cursor/bg-cursor",
|
|
258
|
+
fg: "fg-cursor",
|
|
259
|
+
bg: "bg-cursor",
|
|
260
|
+
min: AA_RATIO
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
rule: "contrast:border-default/bg",
|
|
264
|
+
fg: "border-default",
|
|
265
|
+
bg: "bg",
|
|
266
|
+
min: 3
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
rule: "contrast:border-focus/bg",
|
|
270
|
+
fg: "border-focus",
|
|
271
|
+
bg: "bg",
|
|
272
|
+
min: 3
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
rule: "contrast:border-muted/bg",
|
|
276
|
+
fg: "border-muted",
|
|
277
|
+
bg: "bg",
|
|
278
|
+
min: FAINT_RATIO
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
function lightness(hex) {
|
|
282
|
+
const o = hexToOklch(hex);
|
|
283
|
+
return o ? o.L : null;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Validate post-derivation invariants on a Theme.
|
|
287
|
+
*
|
|
288
|
+
* Default: visibility checks only (selection ΔL, cursor ΔE). These are
|
|
289
|
+
* invariants that `deriveTheme` doesn't enforce and that matter for every
|
|
290
|
+
* theme regardless of authoring pedigree.
|
|
291
|
+
*
|
|
292
|
+
* Opt into WCAG contrast checks via `{ wcag: true }`. Use at build-time to
|
|
293
|
+
* verify bundled themes, or when loading hand-authored Theme objects that
|
|
294
|
+
* didn't flow through `deriveTheme`'s `ensureContrast` pass.
|
|
295
|
+
*
|
|
296
|
+
* Non-hex values (ANSI names from `ansi16` mode) are skipped with no
|
|
297
|
+
* violation — ANSI 16 themes can't be contrast-checked in hex space.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```ts
|
|
301
|
+
* // Default — visibility only, fast
|
|
302
|
+
* const { ok, violations } = validateThemeInvariants(theme)
|
|
303
|
+
*
|
|
304
|
+
* // Build-time audit — full WCAG check
|
|
305
|
+
* const audit = validateThemeInvariants(theme, { wcag: true })
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
function validateThemeInvariants(theme, opts = {}) {
|
|
309
|
+
const checkWcag = opts.wcag ?? false;
|
|
310
|
+
const checkVisibility = opts.visibility ?? true;
|
|
311
|
+
const violations = [];
|
|
312
|
+
if (checkWcag) {
|
|
313
|
+
const themeRecord = theme;
|
|
314
|
+
for (const pair of CONTRAST_PAIRS) {
|
|
315
|
+
const fg = themeRecord[pair.fg];
|
|
316
|
+
const bg = themeRecord[pair.bg];
|
|
317
|
+
if (typeof fg !== "string" || typeof bg !== "string") continue;
|
|
318
|
+
const r = checkContrast(fg, bg);
|
|
319
|
+
if (r === null) continue;
|
|
320
|
+
if (r.ratio < pair.min) violations.push({
|
|
321
|
+
rule: pair.rule,
|
|
322
|
+
tokens: [pair.fg, pair.bg],
|
|
323
|
+
actual: r.ratio,
|
|
324
|
+
required: pair.min,
|
|
325
|
+
message: `${pair.fg} (${fg}) on ${pair.bg} (${bg}) is ${r.ratio.toFixed(2)}:1, needs ${pair.min.toFixed(1)}:1`
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (checkVisibility) {
|
|
330
|
+
const themeAny = theme;
|
|
331
|
+
const selectionBg = themeAny["bg-selected"] ?? "";
|
|
332
|
+
const cursorBg = themeAny["bg-cursor"] ?? themeAny["cursorbg"] ?? "";
|
|
333
|
+
const selectionKey = "bg-selected";
|
|
334
|
+
const cursorKey = themeAny["bg-cursor"] !== void 0 ? "bg-cursor" : "cursorbg";
|
|
335
|
+
const lBg = lightness(theme.bg);
|
|
336
|
+
const lSelBg = lightness(selectionBg);
|
|
337
|
+
if (lBg !== null && lSelBg !== null) {
|
|
338
|
+
const dL = Math.abs(lSelBg - lBg);
|
|
339
|
+
if (dL < .08) violations.push({
|
|
340
|
+
rule: "visibility:selection",
|
|
341
|
+
tokens: [selectionKey, "bg"],
|
|
342
|
+
actual: dL,
|
|
343
|
+
required: SELECTION_DELTA_L$1,
|
|
344
|
+
message: `${selectionKey} (${selectionBg}) differs from bg (${theme.bg}) by ΔL=${dL.toFixed(3)}, needs ≥ ${SELECTION_DELTA_L$1.toFixed(2)}`
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const oBg = hexToOklch(theme.bg);
|
|
348
|
+
const oCursorBg = hexToOklch(cursorBg);
|
|
349
|
+
if (oBg && oCursorBg) {
|
|
350
|
+
const de = deltaE(oBg, oCursorBg);
|
|
351
|
+
if (de < .15) violations.push({
|
|
352
|
+
rule: "visibility:cursor",
|
|
353
|
+
tokens: [cursorKey, "bg"],
|
|
354
|
+
actual: de,
|
|
355
|
+
required: CURSOR_DELTA_E$1,
|
|
356
|
+
message: `${cursorKey} (${cursorBg}) differs from bg (${theme.bg}) by ΔE=${de.toFixed(3)}, needs ≥ ${CURSOR_DELTA_E$1.toFixed(2)}`
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
ok: violations.length === 0,
|
|
362
|
+
violations
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Format violations as a multiline error message for throws/logs.
|
|
367
|
+
*/
|
|
368
|
+
function formatViolations(violations) {
|
|
369
|
+
if (violations.length === 0) return "";
|
|
370
|
+
return violations.map((v) => ` - [${v.rule}] ${v.message}`).join("\n");
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Thrown by `loadTheme({ mode: "strict" })` when invariants fail.
|
|
374
|
+
* Carries the violations array for programmatic inspection.
|
|
375
|
+
*/
|
|
376
|
+
var ThemeInvariantError = class extends Error {
|
|
377
|
+
violations;
|
|
378
|
+
constructor(violations) {
|
|
379
|
+
super(`Theme invariants failed (${violations.length} violation${violations.length === 1 ? "" : "s"}):\n${formatViolations(violations)}`);
|
|
380
|
+
this.name = "ThemeInvariantError";
|
|
381
|
+
this.violations = violations;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region packages/ansi/src/theme/derived.ts
|
|
386
|
+
/**
|
|
387
|
+
* deriveFields — single helper that fills all derived theme sections.
|
|
388
|
+
*
|
|
389
|
+
* Eliminates 4-way duplication of brand, categorical ring, state-variant, and
|
|
390
|
+
* variants population across:
|
|
391
|
+
* - derive.ts (deriveTruecolorTheme + deriveAnsi16Theme)
|
|
392
|
+
* - default-schemes.ts (ansi16DarkTheme + ansi16LightTheme)
|
|
393
|
+
* - @silvery/theme/generate.ts (generateTheme)
|
|
394
|
+
* - @silvery/theme/schemes/index.ts (ansi16DarkTheme + ansi16LightTheme)
|
|
395
|
+
*
|
|
396
|
+
* Canonical authority: derive.ts truecolor path. ANSI16 paths are aligned to
|
|
397
|
+
* deriveAnsi16Theme output (which is itself the canonical ANSI16 reference).
|
|
398
|
+
*/
|
|
399
|
+
/** Default typography variants — token-based, works across any theme. */
|
|
400
|
+
const DEFAULT_VARIANTS$1 = {
|
|
401
|
+
h1: {
|
|
402
|
+
color: "$primary",
|
|
403
|
+
bold: true
|
|
404
|
+
},
|
|
405
|
+
h2: {
|
|
406
|
+
color: "$accent",
|
|
407
|
+
bold: true
|
|
408
|
+
},
|
|
409
|
+
h3: { bold: true },
|
|
410
|
+
body: {},
|
|
411
|
+
"body-muted": { color: "$muted" },
|
|
412
|
+
"fine-print": {
|
|
413
|
+
color: "$muted",
|
|
414
|
+
dim: true
|
|
415
|
+
},
|
|
416
|
+
strong: { bold: true },
|
|
417
|
+
em: { italic: true },
|
|
418
|
+
link: {
|
|
419
|
+
color: "$fg-link",
|
|
420
|
+
underlineStyle: "single"
|
|
421
|
+
},
|
|
422
|
+
key: {
|
|
423
|
+
color: "$accent",
|
|
424
|
+
bold: true
|
|
425
|
+
},
|
|
426
|
+
code: { backgroundColor: "$mutedbg" },
|
|
427
|
+
kbd: {
|
|
428
|
+
backgroundColor: "$mutedbg",
|
|
429
|
+
color: "$accent",
|
|
430
|
+
bold: true
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Derive the shared "delta" fields common to every theme object:
|
|
435
|
+
* brand tokens, categorical ring, state variants, and typography variants.
|
|
436
|
+
*
|
|
437
|
+
* Pass a `shift` function for truecolor hover/active derivation (OKLCH
|
|
438
|
+
* brighten/darken). Omit `shift` for ANSI16 — hover/active fall back to the
|
|
439
|
+
* base color (no intermediate intensities available on 16-color terminals).
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* // Truecolor (dark theme)
|
|
443
|
+
* import { brighten, darken } from "@silvery/color"
|
|
444
|
+
* deriveFields({ shift: (hex, a) => brighten(hex, a), primary, ... })
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* // ANSI16 — no shift function
|
|
448
|
+
* deriveFields({ primary, accent, fg, selectionbg, surfacebg, ring })
|
|
449
|
+
*/
|
|
450
|
+
function deriveFields(input) {
|
|
451
|
+
const { dark, shift, primary, accent, fg, selectionbg, surfacebg, ring } = input;
|
|
452
|
+
const applyShift = dark !== void 0 ? (color, amount) => dark ? brighten(color, amount) : darken(color, amount) : shift ?? ((color, _amount) => color);
|
|
453
|
+
return {
|
|
454
|
+
brand: primary,
|
|
455
|
+
"brand-hover": applyShift(primary, .04),
|
|
456
|
+
"brand-active": applyShift(primary, .08),
|
|
457
|
+
...ring,
|
|
458
|
+
"primary-hover": applyShift(primary, .04),
|
|
459
|
+
"primary-active": applyShift(primary, .08),
|
|
460
|
+
"accent-hover": applyShift(accent, .04),
|
|
461
|
+
"accent-active": applyShift(accent, .08),
|
|
462
|
+
"fg-hover": applyShift(fg, .04),
|
|
463
|
+
"fg-active": applyShift(fg, .08),
|
|
464
|
+
"bg-selected-hover": applyShift(selectionbg, .04),
|
|
465
|
+
"bg-surface-hover": applyShift(surfacebg, .04),
|
|
466
|
+
variants: DEFAULT_VARIANTS$1
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region packages/ansi/src/sterling/contrast.ts
|
|
471
|
+
/**
|
|
472
|
+
* Sterling contrast guardrails — D3 from sterling-preflight.md.
|
|
473
|
+
*
|
|
474
|
+
* Two modes:
|
|
475
|
+
* - `strict` — throw when a core role pair fails WCAG AA 4.5:1.
|
|
476
|
+
* Used by the catalog test (all 84 shipped schemes must pass).
|
|
477
|
+
* - `auto-lift` — adjust OKLCH lightness until AA passes (±0.04L increments
|
|
478
|
+
* up to ~0.20L). Logs at debug; silent by default. Used for user schemes
|
|
479
|
+
* at runtime.
|
|
480
|
+
*
|
|
481
|
+
* Pinned tokens (per-role overrides supplied by scheme authors) are excluded
|
|
482
|
+
* from auto-lift and from strict-mode enforcement — the author accepts the
|
|
483
|
+
* contrast consequence of pinning.
|
|
484
|
+
*/
|
|
485
|
+
/** WCAG AA threshold for normal text. */
|
|
486
|
+
const WCAG_AA = 4.5;
|
|
487
|
+
var ContrastError = class extends Error {
|
|
488
|
+
violations;
|
|
489
|
+
constructor(violations) {
|
|
490
|
+
const summary = violations.slice(0, 5).map((v) => `${v.token}: ${v.ratio.toFixed(2)} < ${v.target} (fg=${v.fg}, bg=${v.bg})`).join("; ");
|
|
491
|
+
const extra = violations.length > 5 ? ` (+${violations.length - 5} more)` : "";
|
|
492
|
+
super(`Sterling contrast: ${violations.length} violation(s): ${summary}${extra}`);
|
|
493
|
+
this.name = "ContrastError";
|
|
494
|
+
this.violations = violations;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* Verify `fg` on `bg` meets `target` ratio. Returns `null` when already
|
|
499
|
+
* passing; otherwise returns a ContrastViolation.
|
|
500
|
+
*/
|
|
501
|
+
function checkAA(token, fg, bg, target = WCAG_AA) {
|
|
502
|
+
const r = checkContrast(fg, bg);
|
|
503
|
+
if (!r) return null;
|
|
504
|
+
if (r.ratio >= target) return null;
|
|
505
|
+
return {
|
|
506
|
+
token,
|
|
507
|
+
fg,
|
|
508
|
+
bg,
|
|
509
|
+
ratio: r.ratio,
|
|
510
|
+
target
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Auto-lift `fg` against `bg` until the `target` contrast ratio is met,
|
|
515
|
+
* via OKLCH L shifts (hue + chroma preserved). Light bg → darken;
|
|
516
|
+
* dark bg → lighten.
|
|
517
|
+
*
|
|
518
|
+
* Implementation note: binary-searches the minimum L shift achieving the
|
|
519
|
+
* target. Falls back to a best-effort value if the target is unreachable
|
|
520
|
+
* (e.g., yellow against white can never hit 4.5:1 at any lightness while
|
|
521
|
+
* preserving yellow hue; the result is the darkest in-gamut yellow).
|
|
522
|
+
*/
|
|
523
|
+
function autoLift(fg, bg, target = WCAG_AA) {
|
|
524
|
+
const current = checkContrast(fg, bg);
|
|
525
|
+
if (!current) return {
|
|
526
|
+
value: fg,
|
|
527
|
+
lifted: false
|
|
528
|
+
};
|
|
529
|
+
if (current.ratio >= target) return {
|
|
530
|
+
value: fg,
|
|
531
|
+
lifted: false
|
|
532
|
+
};
|
|
533
|
+
const adjusted = ensureContrast(fg, bg, target);
|
|
534
|
+
return {
|
|
535
|
+
value: adjusted,
|
|
536
|
+
lifted: adjusted !== fg
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region packages/ansi/src/sterling/derive.ts
|
|
541
|
+
/**
|
|
542
|
+
* Sterling derivation — preservative OKLCH rules over a 22-color ColorScheme.
|
|
543
|
+
*
|
|
544
|
+
* Implements design-system.md §"Derivation rules" with guardrails from D3.
|
|
545
|
+
* Produces the nested `Roles` shape. `flatten.ts` projects the flat keys.
|
|
546
|
+
*
|
|
547
|
+
* Derivation is:
|
|
548
|
+
* 1. `scheme.primary` (or fallback) → accent.fg / accent.bg / info.fg
|
|
549
|
+
* 2. status roles from `scheme.red / yellow / green / primary`
|
|
550
|
+
* 3. Adaptive OKLCH hover/active L-shift (direction = base-L, not scheme.dark):
|
|
551
|
+
* baseL > 0.6 → darken (hover −0.04L, active −0.08L)
|
|
552
|
+
* baseL ≤ 0.6 → brighten (hover +0.04L, active +0.08L)
|
|
553
|
+
* At L extremes (target L > 0.9 or < 0.1) chroma is proportionally
|
|
554
|
+
* reduced so the color pushes toward gray instead of collapsing to
|
|
555
|
+
* white/black — fixes the Frappe yellow/light-accent whiteout.
|
|
556
|
+
* 4. `fgOn` picked for WCAG AA against role's `bg` (prefers scheme bg/fg)
|
|
557
|
+
* 5. surface ramp via OKLCH blend
|
|
558
|
+
* 6. contrast guardrail: strict throws, auto-lift adjusts
|
|
559
|
+
*
|
|
560
|
+
* Per-hue delta adaptation: yellows (H ∈ [80, 110]) get ±0.06L / ±0.10L,
|
|
561
|
+
* low-chroma schemes (C < 0.05) get ±0.06L / ±0.10L. Everything else uses
|
|
562
|
+
* the standard ±0.04L / ±0.08L.
|
|
563
|
+
*
|
|
564
|
+
* Pinned tokens (via `DeriveOptions.pins`) bypass both the rule and
|
|
565
|
+
* auto-lift; they're written verbatim onto the Theme.
|
|
566
|
+
*/
|
|
567
|
+
/**
|
|
568
|
+
* Default typography variants — token-based, works across any Sterling theme.
|
|
569
|
+
* Consumed by `<Text variant="h1">` via the theme's `variants` record.
|
|
570
|
+
*
|
|
571
|
+
* Keys use Sterling flat-token names in their color slots (`$fg-accent`,
|
|
572
|
+
* `$fg-muted`, `$bg-muted`) so the defaults resolve against every Sterling-
|
|
573
|
+
* derived Theme without further wiring.
|
|
574
|
+
*/
|
|
575
|
+
const DEFAULT_VARIANTS = {
|
|
576
|
+
h1: {
|
|
577
|
+
color: "$fg-accent",
|
|
578
|
+
bold: true
|
|
579
|
+
},
|
|
580
|
+
h2: {
|
|
581
|
+
color: "$fg-accent",
|
|
582
|
+
bold: true
|
|
583
|
+
},
|
|
584
|
+
h3: { bold: true },
|
|
585
|
+
h4: {
|
|
586
|
+
color: "$fg-muted",
|
|
587
|
+
bold: true
|
|
588
|
+
},
|
|
589
|
+
h5: {
|
|
590
|
+
color: "$fg-muted",
|
|
591
|
+
italic: true
|
|
592
|
+
},
|
|
593
|
+
h6: {
|
|
594
|
+
color: "$fg-muted",
|
|
595
|
+
dim: true
|
|
596
|
+
},
|
|
597
|
+
body: {},
|
|
598
|
+
"body-muted": { color: "$fg-muted" },
|
|
599
|
+
"fine-print": {
|
|
600
|
+
color: "$fg-muted",
|
|
601
|
+
dim: true
|
|
602
|
+
},
|
|
603
|
+
strong: { bold: true },
|
|
604
|
+
em: { italic: true },
|
|
605
|
+
link: {
|
|
606
|
+
color: "$fg-accent",
|
|
607
|
+
underlineStyle: "single"
|
|
608
|
+
},
|
|
609
|
+
key: {
|
|
610
|
+
color: "$fg-accent",
|
|
611
|
+
bold: true
|
|
612
|
+
},
|
|
613
|
+
code: { backgroundColor: "$bg-muted" },
|
|
614
|
+
kbd: {
|
|
615
|
+
backgroundColor: "$bg-muted",
|
|
616
|
+
color: "$fg-accent",
|
|
617
|
+
bold: true
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
/**
|
|
621
|
+
* Build the 16-slot ANSI palette from a ColorScheme. Indexed `$color0` …
|
|
622
|
+
* `$color15` by the framework's token resolver.
|
|
623
|
+
*/
|
|
624
|
+
function buildPalette(scheme) {
|
|
625
|
+
return [
|
|
626
|
+
scheme.black,
|
|
627
|
+
scheme.red,
|
|
628
|
+
scheme.green,
|
|
629
|
+
scheme.yellow,
|
|
630
|
+
scheme.blue,
|
|
631
|
+
scheme.magenta,
|
|
632
|
+
scheme.cyan,
|
|
633
|
+
scheme.white,
|
|
634
|
+
scheme.brightBlack,
|
|
635
|
+
scheme.brightRed,
|
|
636
|
+
scheme.brightGreen,
|
|
637
|
+
scheme.brightYellow,
|
|
638
|
+
scheme.brightBlue,
|
|
639
|
+
scheme.brightMagenta,
|
|
640
|
+
scheme.brightCyan,
|
|
641
|
+
scheme.brightWhite
|
|
642
|
+
];
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Derive the 8-hue categorical ring from a ColorScheme. Mirrors the legacy
|
|
646
|
+
* derive.ts logic — blends scheme hues for the missing Sterling slots
|
|
647
|
+
* (orange from red+yellow, teal from green+cyan, pink from magenta+red).
|
|
648
|
+
*/
|
|
649
|
+
function buildCategoricalHues(scheme) {
|
|
650
|
+
const dark = scheme.dark ?? true;
|
|
651
|
+
return {
|
|
652
|
+
red: scheme.red,
|
|
653
|
+
orange: blend(scheme.red, scheme.yellow, .5),
|
|
654
|
+
yellow: scheme.yellow,
|
|
655
|
+
green: scheme.green,
|
|
656
|
+
teal: blend(scheme.green, scheme.cyan, .5),
|
|
657
|
+
blue: dark ? scheme.brightBlue : scheme.blue,
|
|
658
|
+
purple: scheme.magenta,
|
|
659
|
+
pink: blend(scheme.magenta, scheme.red, .5)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function isYellowish(hex) {
|
|
663
|
+
const o = hexToOklch(hex);
|
|
664
|
+
if (!o) return false;
|
|
665
|
+
return o.H >= 80 && o.H <= 120;
|
|
666
|
+
}
|
|
667
|
+
function isLowChroma(hex) {
|
|
668
|
+
const o = hexToOklch(hex);
|
|
669
|
+
if (!o) return false;
|
|
670
|
+
return o.C < .05;
|
|
671
|
+
}
|
|
672
|
+
/** Compute state-shift deltas for a given base color. Wider for yellows + low-chroma. */
|
|
673
|
+
function stateDeltas(base) {
|
|
674
|
+
if (isYellowish(base) || isLowChroma(base)) return {
|
|
675
|
+
hover: .06,
|
|
676
|
+
active: .1
|
|
677
|
+
};
|
|
678
|
+
return {
|
|
679
|
+
hover: .04,
|
|
680
|
+
active: .08
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Adaptive L-shift: direction follows the token's own luminance, NOT
|
|
685
|
+
* scheme.dark. High-L tokens (yellows, light accents) darken; low-L tokens
|
|
686
|
+
* brighten. Uniform handling — yields a reliable "more active than hover"
|
|
687
|
+
* relationship no matter what hue/lightness the base is.
|
|
688
|
+
*
|
|
689
|
+
* Chroma preservation at L extremes: when the target L pushes past 0.9
|
|
690
|
+
* (approaching white) or below 0.1 (approaching black), chroma is scaled
|
|
691
|
+
* down proportionally so the color drifts toward gray rather than
|
|
692
|
+
* collapsing to #FFFFFF or #000000. This preserves perceptual differences
|
|
693
|
+
* between the base / hover / active states even on intrinsically-bright
|
|
694
|
+
* tokens (catppuccin-frappe yellow, light blue accents, etc.).
|
|
695
|
+
*
|
|
696
|
+
* Returns the original hex unchanged when OKLCH parsing fails.
|
|
697
|
+
*/
|
|
698
|
+
function shiftL(hex, amount) {
|
|
699
|
+
const o = hexToOklch(hex);
|
|
700
|
+
if (!o) return hex;
|
|
701
|
+
const direction = o.L > .6 ? -1 : 1;
|
|
702
|
+
const targetL = clamp01(o.L + direction * amount);
|
|
703
|
+
let nextC = o.C;
|
|
704
|
+
if (targetL > .9 || targetL < .1) {
|
|
705
|
+
const factor = clamp01(1 - Math.abs(targetL - .5) * 2);
|
|
706
|
+
nextC = o.C * factor;
|
|
707
|
+
}
|
|
708
|
+
return oklchToHex({
|
|
709
|
+
L: targetL,
|
|
710
|
+
C: nextC,
|
|
711
|
+
H: o.H
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function clamp01(x) {
|
|
715
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
716
|
+
}
|
|
717
|
+
/** Label the direction the adaptive L-shift took, for trace rule strings. */
|
|
718
|
+
function shiftLabel(hex) {
|
|
719
|
+
const o = hexToOklch(hex);
|
|
720
|
+
if (!o) return "brighten";
|
|
721
|
+
return o.L > .6 ? "darken" : "brighten";
|
|
722
|
+
}
|
|
723
|
+
function inferMode(scheme, explicit) {
|
|
724
|
+
if (explicit) return explicit;
|
|
725
|
+
if (typeof scheme.dark === "boolean") return scheme.dark ? "dark" : "light";
|
|
726
|
+
const lum = relativeLuminance(scheme.background);
|
|
727
|
+
return lum !== null && lum < .5 ? "dark" : "light";
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Pick a foreground color to draw on a filled `bg` of a role. Prefers
|
|
731
|
+
* `scheme.background` if it beats AA against the role bg (i.e. the role bg
|
|
732
|
+
* is bright enough that using dark text reads); otherwise `scheme.foreground`;
|
|
733
|
+
* otherwise falls back to white/black by bg luminance.
|
|
734
|
+
*/
|
|
735
|
+
function pickFgOn(roleBg, scheme) {
|
|
736
|
+
const candidates = [
|
|
737
|
+
scheme.foreground,
|
|
738
|
+
scheme.background,
|
|
739
|
+
"#FFFFFF",
|
|
740
|
+
"#000000"
|
|
741
|
+
];
|
|
742
|
+
let best = candidates[0];
|
|
743
|
+
let bestRatio = 0;
|
|
744
|
+
for (const c of candidates) {
|
|
745
|
+
const r = checkAA("fgOn", c, roleBg);
|
|
746
|
+
if (r === null) return c;
|
|
747
|
+
if (r.ratio > bestRatio) {
|
|
748
|
+
best = c;
|
|
749
|
+
bestRatio = r.ratio;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return best;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Resolve a pin for a token path. Accepts both nested (`"accent.hover.bg"`)
|
|
756
|
+
* and flat (`"bg-accent-hover"`) forms. Returns the pinned hex or undefined.
|
|
757
|
+
*/
|
|
758
|
+
function pin(pins, nested, flat) {
|
|
759
|
+
if (!pins) return void 0;
|
|
760
|
+
return pins[nested] ?? pins[flat];
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Shared guard: handles pin → rule → contrast check → auto-lift → record.
|
|
764
|
+
* `target` defaults to WCAG_AA (4.5); callers use 3.0 for "muted" tokens
|
|
765
|
+
* that are deemphasized by design.
|
|
766
|
+
*/
|
|
767
|
+
function guardTarget(nestedPath, flatPath, rule, inputs, value, against, target, contrast, pins, trace, violations) {
|
|
768
|
+
const pinned = pin(pins, nestedPath, flatPath);
|
|
769
|
+
if (pinned !== void 0) {
|
|
770
|
+
trace.push({
|
|
771
|
+
token: nestedPath,
|
|
772
|
+
rule: "pinned by scheme author",
|
|
773
|
+
inputs: [pinned],
|
|
774
|
+
output: pinned,
|
|
775
|
+
pinned: true
|
|
776
|
+
});
|
|
777
|
+
return pinned;
|
|
778
|
+
}
|
|
779
|
+
if (against === void 0) {
|
|
780
|
+
trace.push({
|
|
781
|
+
token: nestedPath,
|
|
782
|
+
rule,
|
|
783
|
+
inputs,
|
|
784
|
+
output: value
|
|
785
|
+
});
|
|
786
|
+
return value;
|
|
787
|
+
}
|
|
788
|
+
if (checkAA(nestedPath, value, against, target) === null) {
|
|
789
|
+
trace.push({
|
|
790
|
+
token: nestedPath,
|
|
791
|
+
rule,
|
|
792
|
+
inputs,
|
|
793
|
+
output: value
|
|
794
|
+
});
|
|
795
|
+
return value;
|
|
796
|
+
}
|
|
797
|
+
const lifted = autoLift(value, against, target);
|
|
798
|
+
const finalValue = lifted.value;
|
|
799
|
+
const residual = checkAA(nestedPath, finalValue, against, target);
|
|
800
|
+
if (residual !== null) violations.push(residual);
|
|
801
|
+
trace.push({
|
|
802
|
+
token: nestedPath,
|
|
803
|
+
rule: lifted.lifted ? `${rule} + auto-lift` : rule,
|
|
804
|
+
inputs,
|
|
805
|
+
output: finalValue,
|
|
806
|
+
...lifted.lifted ? { liftedFrom: value } : {}
|
|
807
|
+
});
|
|
808
|
+
return finalValue;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Derive a Theme's nested roles from a ColorScheme. Guardrails applied.
|
|
812
|
+
*/
|
|
813
|
+
function deriveRoles(scheme, opts) {
|
|
814
|
+
const mode = inferMode(scheme, opts.mode);
|
|
815
|
+
const contrast = opts.contrast ?? "auto-lift";
|
|
816
|
+
const pins = opts.pins;
|
|
817
|
+
const trace = [];
|
|
818
|
+
const violations = [];
|
|
819
|
+
const primary = scheme.primary ?? (mode === "dark" ? scheme.brightBlue : scheme.blue);
|
|
820
|
+
const bg = scheme.background;
|
|
821
|
+
const fg = scheme.foreground;
|
|
822
|
+
function guard(nestedPath, flatPath, rule, inputs, value, against, target = WCAG_AA) {
|
|
823
|
+
return guardTarget(nestedPath, flatPath, rule, inputs, value, against, target, contrast, pins, trace, violations);
|
|
824
|
+
}
|
|
825
|
+
const accentBase = guard("accent.fg", "fg-accent", "scheme.primary", [primary], primary, bg);
|
|
826
|
+
const accentBg = guard("accent.bg", "bg-accent", "scheme.primary", [primary], primary);
|
|
827
|
+
const deltaA = stateDeltas(accentBg);
|
|
828
|
+
const bgDir = shiftLabel(accentBg);
|
|
829
|
+
const accentHoverBg = guard("accent.hover.bg", "bg-accent-hover", `OKLCH ${bgDir} ${deltaA.hover}L on accent.bg`, [accentBg], shiftL(accentBg, deltaA.hover));
|
|
830
|
+
const accentActiveBg = guard("accent.active.bg", "bg-accent-active", `OKLCH ${bgDir} ${deltaA.active}L on accent.bg`, [accentBg], shiftL(accentBg, deltaA.active));
|
|
831
|
+
const accentFgOn = guard("accent.fgOn", "fg-on-accent", "contrast-pick(scheme.fg/bg/BW)", [accentBg], pickFgOn(accentBg, scheme), accentBg);
|
|
832
|
+
const accentBorder = guard("accent.border", "border-accent", "= accent.bg", [accentBg], accentBg);
|
|
833
|
+
const fgDir = shiftLabel(accentBase);
|
|
834
|
+
const accentHoverFg = guard("accent.hover.fg", "fg-accent-hover", `OKLCH ${fgDir} ${deltaA.hover}L on accent.fg`, [accentBase], shiftL(accentBase, deltaA.hover), bg);
|
|
835
|
+
const accentActiveFg = guard("accent.active.fg", "fg-accent-active", `OKLCH ${fgDir} ${deltaA.active}L on accent.fg`, [accentBase], shiftL(accentBase, deltaA.active), bg);
|
|
836
|
+
const accent = {
|
|
837
|
+
fg: accentBase,
|
|
838
|
+
bg: accentBg,
|
|
839
|
+
fgOn: accentFgOn,
|
|
840
|
+
border: accentBorder,
|
|
841
|
+
hover: {
|
|
842
|
+
fg: accentHoverFg,
|
|
843
|
+
bg: accentHoverBg
|
|
844
|
+
},
|
|
845
|
+
active: {
|
|
846
|
+
fg: accentActiveFg,
|
|
847
|
+
bg: accentActiveBg
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
const info = buildInteractive("info", primary, scheme, opts, trace, violations);
|
|
851
|
+
const success = buildInteractive("success", scheme.green, scheme, opts, trace, violations);
|
|
852
|
+
const warning = buildInteractive("warning", scheme.yellow, scheme, opts, trace, violations);
|
|
853
|
+
const error = buildInteractive("error", scheme.red, scheme, opts, trace, violations);
|
|
854
|
+
const mutedBg = guard("muted.bg", "bg-muted", "blend(bg, fg, 0.08)", [bg, fg], blend(bg, fg, .08));
|
|
855
|
+
const muted = {
|
|
856
|
+
fg: guard("muted.fg", "fg-muted", "blend(fg, bg, 0.4)", [fg, bg], blend(fg, bg, .4), mutedBg, 3),
|
|
857
|
+
bg: mutedBg
|
|
858
|
+
};
|
|
859
|
+
const fgForSurfaceLift = ensureContrast(fg, blend(bg, fg, .08), WCAG_AA);
|
|
860
|
+
const surfaceDefault = guard("surface.default", "bg-surface-default", "scheme.background", [bg], bg);
|
|
861
|
+
const surface = {
|
|
862
|
+
default: surfaceDefault,
|
|
863
|
+
subtle: guard("surface.subtle", "bg-surface-subtle", "blend(bg, fg, 0.03)", [bg, fg], blend(bg, fg, .03), fgForSurfaceLift, WCAG_AA),
|
|
864
|
+
raised: guard("surface.raised", "bg-surface-raised", "blend(bg, fg, 0.10)", [bg, fg], blend(bg, fg, .1), fgForSurfaceLift, WCAG_AA),
|
|
865
|
+
overlay: guard("surface.overlay", "bg-surface-overlay", "blend(bg, fg, 0.12)", [bg, fg], blend(bg, fg, .12), fgForSurfaceLift, WCAG_AA),
|
|
866
|
+
hover: guard("surface.hover", "bg-surface-hover", "blend(bg, fg, 0.10)", [bg, fg], blend(bg, fg, .1), fgForSurfaceLift, WCAG_AA)
|
|
867
|
+
};
|
|
868
|
+
const borderDefault = guard("border.default", "border-default", "blend(bg, fg, 0.18)", [bg, fg], blend(bg, fg, .18), bg, 3);
|
|
869
|
+
const border = {
|
|
870
|
+
default: borderDefault,
|
|
871
|
+
focus: guard("border.focus", "border-focus", "= accent.bg", [accentBg], accentBg, bg),
|
|
872
|
+
muted: guard("border.muted", "border-muted", "blend(bg, fg, 0.10)", [bg, fg], blend(bg, fg, .1), bg, 1.5)
|
|
873
|
+
};
|
|
874
|
+
const cursorBgRaw = guard("cursor.bg", "bg-cursor", "scheme.cursorColor (visibility-repaired ΔE ≥ 0.15 vs bg)", [scheme.cursorColor, bg], repairCursorBg$1(scheme.cursorColor, bg));
|
|
875
|
+
const cursor = {
|
|
876
|
+
fg: guard("cursor.fg", "fg-cursor", "scheme.cursorText", [scheme.cursorText], scheme.cursorText, cursorBgRaw),
|
|
877
|
+
bg: cursorBgRaw
|
|
878
|
+
};
|
|
879
|
+
const selectedBg = guard("selected.bg", "bg-selected", "scheme.selectionBackground (visibility-repaired ΔL ≥ 0.08 vs bg)", [scheme.selectionBackground, bg], repairSelectionBg$1(scheme.selectionBackground, bg));
|
|
880
|
+
const selectedFgOn = guard("selected.fgOn", "fg-on-selected", "scheme.selectionForeground", [scheme.selectionForeground], scheme.selectionForeground, selectedBg);
|
|
881
|
+
const selectedDeltaH = stateDeltas(selectedBg);
|
|
882
|
+
const selected = {
|
|
883
|
+
bg: selectedBg,
|
|
884
|
+
fgOn: selectedFgOn,
|
|
885
|
+
hover: { bg: guard("selected.hover.bg", "bg-selected-hover", `OKLCH ${shiftLabel(selectedBg)} ${selectedDeltaH.hover}L on selected.bg`, [selectedBg], shiftL(selectedBg, selectedDeltaH.hover)) }
|
|
886
|
+
};
|
|
887
|
+
const inverseBg = guard("inverse.bg", "bg-inverse", "blend(fg, bg, 0.1)", [fg, bg], blend(fg, bg, .1));
|
|
888
|
+
const inverse = {
|
|
889
|
+
bg: inverseBg,
|
|
890
|
+
fgOn: guard("inverse.fgOn", "fg-on-inverse", "contrast-pick(scheme.fg/bg/BW)", [inverseBg], pickFgOn(inverseBg, scheme), inverseBg)
|
|
891
|
+
};
|
|
892
|
+
const link = { fg: guard("link.fg", "fg-link", mode === "dark" ? "scheme.brightBlue" : "scheme.blue", [mode === "dark" ? scheme.brightBlue : scheme.blue], mode === "dark" ? scheme.brightBlue : scheme.blue, bg) };
|
|
893
|
+
const fgDisabledRaw = blend(surfaceDefault, fg, .38);
|
|
894
|
+
const fgDisabled = guard("disabled.fg", "fg-disabled", "composite(fg @ 0.38, surface.default), ≥3:1", [fg, surfaceDefault], fgDisabledRaw, surfaceDefault, 3);
|
|
895
|
+
const borderDisabled = guard("disabled.border", "border-disabled", "composite(border-default @ 0.24, surface.default)", [borderDefault, surfaceDefault], blend(surfaceDefault, borderDefault, .24));
|
|
896
|
+
return {
|
|
897
|
+
roles: {
|
|
898
|
+
accent,
|
|
899
|
+
info,
|
|
900
|
+
success,
|
|
901
|
+
warning,
|
|
902
|
+
error,
|
|
903
|
+
muted,
|
|
904
|
+
surface,
|
|
905
|
+
border,
|
|
906
|
+
cursor,
|
|
907
|
+
selected,
|
|
908
|
+
inverse,
|
|
909
|
+
link,
|
|
910
|
+
disabled: {
|
|
911
|
+
fg: fgDisabled,
|
|
912
|
+
bg: guard("disabled.bg", "bg-disabled", "composite(border-default @ 0.12, surface.default)", [borderDefault, surfaceDefault], blend(surfaceDefault, borderDefault, .12)),
|
|
913
|
+
border: borderDisabled
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
mode,
|
|
917
|
+
trace,
|
|
918
|
+
violations
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const SELECTION_DELTA_L = .08;
|
|
922
|
+
/**
|
|
923
|
+
* Nudge selectionBg's OKLCH L until it differs from bg by ≥ SELECTION_DELTA_L.
|
|
924
|
+
* Mirrors the legacy theme's repairSelectionBg behavior — preserves hue +
|
|
925
|
+
* chroma but guarantees the highlight reads against any background. Non-hex
|
|
926
|
+
* input returns unchanged.
|
|
927
|
+
*/
|
|
928
|
+
function repairSelectionBg$1(selectionBg, bg) {
|
|
929
|
+
const oSel = hexToOklch(selectionBg);
|
|
930
|
+
const oBg = hexToOklch(bg);
|
|
931
|
+
if (!oSel || !oBg) return selectionBg;
|
|
932
|
+
const dL = Math.abs(oSel.L - oBg.L);
|
|
933
|
+
if (dL >= SELECTION_DELTA_L) return selectionBg;
|
|
934
|
+
const needed = SELECTION_DELTA_L - dL + .005;
|
|
935
|
+
const direction = oSel.L >= oBg.L ? 1 : -1;
|
|
936
|
+
return oklchToHex({
|
|
937
|
+
L: clamp01(oSel.L + direction * needed),
|
|
938
|
+
C: oSel.C,
|
|
939
|
+
H: oSel.H
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const CURSOR_DELTA_E = .15;
|
|
943
|
+
/**
|
|
944
|
+
* Nudge cursorBg's OKLCH L until it differs from bg by ≥ CURSOR_DELTA_E
|
|
945
|
+
* (perceptual distance, not just lightness). Preserves hue + chroma but
|
|
946
|
+
* guarantees the cursor reads against the surrounding bg.
|
|
947
|
+
*
|
|
948
|
+
* Uses ΔE (OKLCH perceptual distance) rather than ΔL because two colors at
|
|
949
|
+
* the same lightness but different hue/chroma are still visibly distinct —
|
|
950
|
+
* a yellow cursor on a blue bg of equal L is perfectly visible. Only when
|
|
951
|
+
* ΔE falls below the visibility floor do we lift L to compensate.
|
|
952
|
+
*
|
|
953
|
+
* Mirrors `repairSelectionBg` in shape; the repair primitive is L because
|
|
954
|
+
* shifting hue or chroma would change the author-intended cursor color
|
|
955
|
+
* identity. L is the "size" knob — bigger ΔL → more visible without
|
|
956
|
+
* recoloring.
|
|
957
|
+
*
|
|
958
|
+
* Non-hex input returns unchanged.
|
|
959
|
+
*/
|
|
960
|
+
function repairCursorBg$1(cursorBg, bg) {
|
|
961
|
+
const oCur = hexToOklch(cursorBg);
|
|
962
|
+
const oBg = hexToOklch(bg);
|
|
963
|
+
if (!oCur || !oBg) return cursorBg;
|
|
964
|
+
if (deltaE(oCur, oBg) >= CURSOR_DELTA_E) return cursorBg;
|
|
965
|
+
const direction = oCur.L >= oBg.L ? 1 : -1;
|
|
966
|
+
const TARGET = CURSOR_DELTA_E + .005;
|
|
967
|
+
let lo = 0;
|
|
968
|
+
let hi = direction > 0 ? 1 - oCur.L : oCur.L;
|
|
969
|
+
for (let i = 0; i < 24; i++) {
|
|
970
|
+
const mid = (lo + hi) / 2;
|
|
971
|
+
if (deltaE({
|
|
972
|
+
L: clamp01(oCur.L + direction * mid),
|
|
973
|
+
C: oCur.C,
|
|
974
|
+
H: oCur.H
|
|
975
|
+
}, oBg) >= TARGET) hi = mid;
|
|
976
|
+
else lo = mid;
|
|
977
|
+
}
|
|
978
|
+
return oklchToHex({
|
|
979
|
+
L: clamp01(oCur.L + direction * hi),
|
|
980
|
+
C: oCur.C,
|
|
981
|
+
H: oCur.H
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function buildInteractive(name, seed, scheme, opts, trace, violations) {
|
|
985
|
+
const pins = opts.pins;
|
|
986
|
+
const contrast = opts.contrast ?? "auto-lift";
|
|
987
|
+
const bg = scheme.background;
|
|
988
|
+
const guard = (nestedPath, flatPath, rule, inputs, value, against, target = WCAG_AA) => guardTarget(nestedPath, flatPath, rule, inputs, value, against, target, contrast, pins, trace, violations);
|
|
989
|
+
const fg = guard(`${name}.fg`, `fg-${name}`, seedRule$1(name), [seed], seed, bg);
|
|
990
|
+
const roleBg = guard(`${name}.bg`, `bg-${name}`, seedRule$1(name), [seed], seed);
|
|
991
|
+
const delta = stateDeltas(roleBg);
|
|
992
|
+
const bgDir = shiftLabel(roleBg);
|
|
993
|
+
const fgOn = guard(`${name}.fgOn`, `fg-on-${name}`, "contrast-pick(scheme.fg/bg/BW)", [roleBg], pickFgOn(roleBg, scheme), roleBg);
|
|
994
|
+
const hoverBg = guard(`${name}.hover.bg`, `bg-${name}-hover`, `OKLCH ${bgDir} ${delta.hover}L`, [roleBg], shiftL(roleBg, delta.hover));
|
|
995
|
+
const activeBg = guard(`${name}.active.bg`, `bg-${name}-active`, `OKLCH ${bgDir} ${delta.active}L`, [roleBg], shiftL(roleBg, delta.active));
|
|
996
|
+
return {
|
|
997
|
+
fg,
|
|
998
|
+
bg: roleBg,
|
|
999
|
+
fgOn,
|
|
1000
|
+
hover: { bg: hoverBg },
|
|
1001
|
+
active: { bg: activeBg }
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function seedRule$1(name) {
|
|
1005
|
+
switch (name) {
|
|
1006
|
+
case "info": return "scheme.primary (info mirrors accent's seed, derived independently)";
|
|
1007
|
+
case "success": return "scheme.green";
|
|
1008
|
+
case "warning": return "scheme.yellow";
|
|
1009
|
+
case "error": return "scheme.red";
|
|
1010
|
+
case "accent": return "scheme.primary";
|
|
1011
|
+
default: return `scheme.${name}`;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Derive a full Theme (pre-flatten) from a ColorScheme. Throws `ContrastError`
|
|
1016
|
+
* in strict mode if any role pair fails WCAG AA. Callers typically wrap this
|
|
1017
|
+
* with `flatten()` (from `flatten.ts`) to get the user-facing Theme.
|
|
1018
|
+
*
|
|
1019
|
+
* Returned Theme is NOT frozen and DOES NOT contain flat keys yet.
|
|
1020
|
+
*/
|
|
1021
|
+
function deriveTheme$1(scheme, opts = {}) {
|
|
1022
|
+
const { roles, mode, trace, violations } = deriveRoles(scheme, opts);
|
|
1023
|
+
if ((opts.contrast ?? "auto-lift") === "strict" && violations.length > 0) throw new ContrastError(violations);
|
|
1024
|
+
return {
|
|
1025
|
+
...roles,
|
|
1026
|
+
...buildCategoricalHues(scheme),
|
|
1027
|
+
name: scheme.name,
|
|
1028
|
+
mode,
|
|
1029
|
+
variants: DEFAULT_VARIANTS,
|
|
1030
|
+
palette: buildPalette(scheme),
|
|
1031
|
+
...opts.trace ? { derivationTrace: trace } : {}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Merge a DeepPartial<Theme> onto an existing Theme (for `sterling.theme()`).
|
|
1036
|
+
* Nested role objects are spread deeply; flat keys are replaced if present.
|
|
1037
|
+
*/
|
|
1038
|
+
function mergePartial(base, patch) {
|
|
1039
|
+
if (!patch) return base;
|
|
1040
|
+
const out = { ...base };
|
|
1041
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
1042
|
+
if (v === void 0) continue;
|
|
1043
|
+
const cur = base[k];
|
|
1044
|
+
if (cur && typeof cur === "object" && typeof v === "object" && !Array.isArray(v)) {
|
|
1045
|
+
out[k] = {
|
|
1046
|
+
...cur,
|
|
1047
|
+
...v
|
|
1048
|
+
};
|
|
1049
|
+
for (const [k2, v2] of Object.entries(v)) if (v2 && typeof v2 === "object" && !Array.isArray(v2) && cur[k2] && typeof cur[k2] === "object") out[k][k2] = {
|
|
1050
|
+
...cur[k2],
|
|
1051
|
+
...v2
|
|
1052
|
+
};
|
|
1053
|
+
} else out[k] = v;
|
|
1054
|
+
}
|
|
1055
|
+
return out;
|
|
1056
|
+
}
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region packages/ansi/src/sterling/inline.ts
|
|
1059
|
+
/**
|
|
1060
|
+
* Sterling flat-token inlining — merges Sterling flat tokens onto a Theme.
|
|
1061
|
+
*
|
|
1062
|
+
* Invoked implicitly by `deriveTheme`, `loadTheme`, and `deriveAnsi16Theme`
|
|
1063
|
+
* so every Theme `@silvery/ansi` produces has Sterling flat tokens baked in.
|
|
1064
|
+
* Callers do not need to call this directly.
|
|
1065
|
+
*
|
|
1066
|
+
* Behavior:
|
|
1067
|
+
* - Preserves every existing Theme field unchanged
|
|
1068
|
+
* - Writes Sterling flat tokens (`bg-accent`, `fg-on-accent`, `border-focus`, …)
|
|
1069
|
+
* only when the key isn't already present as a string (so author pins /
|
|
1070
|
+
* palette-provided values win)
|
|
1071
|
+
* - Not frozen (theme overlays mutate in a few callers)
|
|
1072
|
+
*
|
|
1073
|
+
* A ColorScheme can be supplied for full fidelity. When omitted, Sterling
|
|
1074
|
+
* derives from a ColorScheme reconstructed from the theme's own palette —
|
|
1075
|
+
* lossy for ANSI slot colors but sufficient for Sterling's 6-slot surface.
|
|
1076
|
+
*
|
|
1077
|
+
* Exported from `@silvery/ansi` for advanced users who author Theme objects
|
|
1078
|
+
* by hand and want to ensure the flat tokens are populated; for scheme →
|
|
1079
|
+
* Theme construction `deriveTheme`/`loadTheme` handle this automatically.
|
|
1080
|
+
*/
|
|
1081
|
+
/**
|
|
1082
|
+
* Build a ColorScheme-shaped input from a Theme when the original
|
|
1083
|
+
* scheme isn't available (hand-crafted themes, picker round-trips).
|
|
1084
|
+
*
|
|
1085
|
+
* Reads legacy single-hex hints that the legacy `deriveTheme` path still
|
|
1086
|
+
* emits (`theme.primary`, `theme.accent`, `theme.cursorbg`, …) via bracket
|
|
1087
|
+
* access. Selection / inverse / link aliases were dropped in 0.21.0 — pulls
|
|
1088
|
+
* those from Sterling's nested role objects (`theme.selected.bg`,
|
|
1089
|
+
* `theme.inverse.bg`, `theme.link.fg`).
|
|
1090
|
+
*/
|
|
1091
|
+
function schemeFromTheme(theme) {
|
|
1092
|
+
const palette = theme.palette ?? [];
|
|
1093
|
+
const legacy = theme;
|
|
1094
|
+
const primary = legacy["primary"];
|
|
1095
|
+
const accent = legacy["accent"];
|
|
1096
|
+
const errorHex = (typeof legacy["error"] === "string" ? legacy["error"] : theme.error?.fg) ?? "#000000";
|
|
1097
|
+
const successHex = (typeof legacy["success"] === "string" ? legacy["success"] : theme.success?.fg) ?? "#000000";
|
|
1098
|
+
const warningHex = (typeof legacy["warning"] === "string" ? legacy["warning"] : theme.warning?.fg) ?? "#000000";
|
|
1099
|
+
const infoHex = (typeof legacy["info"] === "string" ? legacy["info"] : theme.info?.fg) ?? "#000000";
|
|
1100
|
+
const accentHex = accent ?? theme.accent?.fg ?? primary ?? "#000000";
|
|
1101
|
+
const primaryHex = primary ?? theme.accent?.fg ?? "#000000";
|
|
1102
|
+
const mutedHex = (typeof legacy["muted"] === "string" ? legacy["muted"] : theme.muted?.fg) ?? "#888888";
|
|
1103
|
+
const cursorBg = legacy["cursorbg"] ?? theme.cursor?.bg ?? theme.bg;
|
|
1104
|
+
const cursorFg = legacy["cursor"] ?? theme.cursor?.fg ?? theme.fg;
|
|
1105
|
+
const selectionBg = theme.selected?.bg ?? theme.bg;
|
|
1106
|
+
const selectionFg = theme.selected?.fgOn ?? theme.fg;
|
|
1107
|
+
return {
|
|
1108
|
+
name: theme.name,
|
|
1109
|
+
dark: isDark(theme.bg),
|
|
1110
|
+
primary: primaryHex,
|
|
1111
|
+
black: palette[0] ?? "#000000",
|
|
1112
|
+
red: palette[1] ?? errorHex,
|
|
1113
|
+
green: palette[2] ?? successHex,
|
|
1114
|
+
yellow: palette[3] ?? warningHex,
|
|
1115
|
+
blue: palette[4] ?? primaryHex,
|
|
1116
|
+
magenta: palette[5] ?? accentHex,
|
|
1117
|
+
cyan: palette[6] ?? infoHex,
|
|
1118
|
+
white: palette[7] ?? theme.fg,
|
|
1119
|
+
brightBlack: palette[8] ?? mutedHex,
|
|
1120
|
+
brightRed: palette[9] ?? errorHex,
|
|
1121
|
+
brightGreen: palette[10] ?? successHex,
|
|
1122
|
+
brightYellow: palette[11] ?? warningHex,
|
|
1123
|
+
brightBlue: palette[12] ?? primaryHex,
|
|
1124
|
+
brightMagenta: palette[13] ?? accentHex,
|
|
1125
|
+
brightCyan: palette[14] ?? infoHex,
|
|
1126
|
+
brightWhite: palette[15] ?? theme.fg,
|
|
1127
|
+
foreground: theme.fg,
|
|
1128
|
+
background: theme.bg,
|
|
1129
|
+
cursorColor: cursorBg,
|
|
1130
|
+
cursorText: cursorFg,
|
|
1131
|
+
selectionBackground: selectionBg,
|
|
1132
|
+
selectionForeground: selectionFg
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Quick luminance check — matches relativeLuminance threshold (0.5). Avoids
|
|
1137
|
+
* pulling in @silvery/color for a single boolean.
|
|
1138
|
+
*/
|
|
1139
|
+
function isDark(hex) {
|
|
1140
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex);
|
|
1141
|
+
if (!m?.[1]) return true;
|
|
1142
|
+
const n = parseInt(m[1], 16);
|
|
1143
|
+
const r = n >> 16 & 255;
|
|
1144
|
+
const g = n >> 8 & 255;
|
|
1145
|
+
const b = n & 255;
|
|
1146
|
+
return (.2126 * r + .7152 * g + .0722 * b) / 255 < .5;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Write Sterling flat tokens onto a Theme and return the augmented object.
|
|
1150
|
+
* Sets a key only when it's not already a string on the theme (so author
|
|
1151
|
+
* pins / palette-provided values win).
|
|
1152
|
+
*
|
|
1153
|
+
* When `scheme` is provided, Sterling derives directly from it (full fidelity).
|
|
1154
|
+
* Otherwise a scheme is reconstructed from the theme's palette (lossy on ANSI
|
|
1155
|
+
* slots but fine for Sterling's derivation surface).
|
|
1156
|
+
*/
|
|
1157
|
+
function inlineSterlingTokens(theme, scheme) {
|
|
1158
|
+
const src = scheme ?? schemeFromTheme(theme);
|
|
1159
|
+
const { roles } = deriveRoles(src, { contrast: "auto-lift" });
|
|
1160
|
+
const out = { ...theme };
|
|
1161
|
+
const setIfAbsent = (key, value) => {
|
|
1162
|
+
if (!(key in out) || typeof out[key] !== "string") out[key] = value;
|
|
1163
|
+
};
|
|
1164
|
+
const accent = roles.accent;
|
|
1165
|
+
if (accent) {
|
|
1166
|
+
setIfAbsent("fg-accent", accent.fg);
|
|
1167
|
+
setIfAbsent("bg-accent", accent.bg);
|
|
1168
|
+
setIfAbsent("fg-on-accent", accent.fgOn);
|
|
1169
|
+
for (const state of ["hover", "active"]) {
|
|
1170
|
+
const s = accent[state];
|
|
1171
|
+
if (!s) continue;
|
|
1172
|
+
setIfAbsent(`fg-accent-${state}`, s.fg);
|
|
1173
|
+
setIfAbsent(`bg-accent-${state}`, s.bg);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
for (const role of [
|
|
1177
|
+
"info",
|
|
1178
|
+
"success",
|
|
1179
|
+
"warning",
|
|
1180
|
+
"error"
|
|
1181
|
+
]) {
|
|
1182
|
+
const r = roles[role];
|
|
1183
|
+
if (!r) continue;
|
|
1184
|
+
setIfAbsent(`fg-${role}`, r.fg);
|
|
1185
|
+
setIfAbsent(`bg-${role}`, r.bg);
|
|
1186
|
+
setIfAbsent(`fg-on-${role}`, r.fgOn);
|
|
1187
|
+
for (const state of ["hover", "active"]) {
|
|
1188
|
+
const s = r[state];
|
|
1189
|
+
if (!s) continue;
|
|
1190
|
+
setIfAbsent(`bg-${role}-${state}`, s.bg);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
if (roles.accent && "border" in roles.accent) setIfAbsent("border-accent", roles.accent.border);
|
|
1194
|
+
const surf = roles.surface;
|
|
1195
|
+
if (surf) {
|
|
1196
|
+
out["bg-surface-default"] = surf.default;
|
|
1197
|
+
out["bg-surface-subtle"] = surf.subtle;
|
|
1198
|
+
out["bg-surface-raised"] = surf.raised;
|
|
1199
|
+
out["bg-surface-overlay"] = surf.overlay;
|
|
1200
|
+
out["bg-surface-hover"] = surf.hover;
|
|
1201
|
+
}
|
|
1202
|
+
const b = roles.border;
|
|
1203
|
+
if (b) {
|
|
1204
|
+
setIfAbsent("border-default", b.default);
|
|
1205
|
+
setIfAbsent("border-focus", b.focus);
|
|
1206
|
+
setIfAbsent("border-muted", b.muted);
|
|
1207
|
+
}
|
|
1208
|
+
const c = roles.cursor;
|
|
1209
|
+
if (c) {
|
|
1210
|
+
setIfAbsent("fg-cursor", c.fg);
|
|
1211
|
+
setIfAbsent("bg-cursor", c.bg);
|
|
1212
|
+
}
|
|
1213
|
+
const m = roles.muted;
|
|
1214
|
+
if (m) {
|
|
1215
|
+
setIfAbsent("fg-muted", m.fg);
|
|
1216
|
+
setIfAbsent("bg-muted", m.bg);
|
|
1217
|
+
}
|
|
1218
|
+
const sel = roles.selected;
|
|
1219
|
+
if (sel) {
|
|
1220
|
+
setIfAbsent("bg-selected", sel.bg);
|
|
1221
|
+
setIfAbsent("fg-on-selected", sel.fgOn);
|
|
1222
|
+
setIfAbsent("bg-selected-hover", sel.hover.bg);
|
|
1223
|
+
}
|
|
1224
|
+
const inv = roles.inverse;
|
|
1225
|
+
if (inv) {
|
|
1226
|
+
setIfAbsent("bg-inverse", inv.bg);
|
|
1227
|
+
setIfAbsent("fg-on-inverse", inv.fgOn);
|
|
1228
|
+
}
|
|
1229
|
+
const lnk = roles.link;
|
|
1230
|
+
if (lnk) setIfAbsent("fg-link", lnk.fg);
|
|
1231
|
+
const dis = roles.disabled;
|
|
1232
|
+
if (dis) {
|
|
1233
|
+
setIfAbsent("fg-disabled", dis.fg);
|
|
1234
|
+
setIfAbsent("bg-disabled", dis.bg);
|
|
1235
|
+
setIfAbsent("border-disabled", dis.border);
|
|
1236
|
+
}
|
|
1237
|
+
setIfAbsent("fg", src.foreground);
|
|
1238
|
+
setIfAbsent("bg", src.background);
|
|
1239
|
+
setIfAbsent("fg-default", src.foreground);
|
|
1240
|
+
setIfAbsent("bg-default", src.background);
|
|
1241
|
+
setIfAbsent("bg-backdrop", blend(src.background, "#000000", .4));
|
|
1242
|
+
return out;
|
|
1243
|
+
}
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region packages/ansi/src/theme/derive.ts
|
|
1246
|
+
/**
|
|
1247
|
+
* Theme derivation — transforms a ColorScheme into a Theme.
|
|
1248
|
+
*/
|
|
1249
|
+
/**
|
|
1250
|
+
* Derive a Theme from a ColorScheme, with Sterling flat tokens baked in.
|
|
1251
|
+
*
|
|
1252
|
+
* Every Theme `@silvery/ansi` produces passes through `inlineSterlingTokens`
|
|
1253
|
+
* so consumers can read `$bg-accent`, `$bg-surface-overlay`, `$border-default`,
|
|
1254
|
+
* `$fg-muted`, etc. directly off the returned object. This is the one
|
|
1255
|
+
* canonical Theme shape in silvery — there is no separate "partial" Theme.
|
|
1256
|
+
*/
|
|
1257
|
+
function deriveTheme(palette, mode = "truecolor", adjustments) {
|
|
1258
|
+
return inlineSterlingTokens(mode === "ansi16" ? deriveAnsi16ThemeRaw(palette) : deriveTruecolorTheme(palette, adjustments), palette);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Load and validate a theme from a ColorScheme.
|
|
1262
|
+
*
|
|
1263
|
+
* Combines `deriveTheme()` (auto-adjust via ensureContrast with project-tuned
|
|
1264
|
+
* thresholds) with `validateThemeInvariants()` (post-derivation visibility +
|
|
1265
|
+
* optional WCAG).
|
|
1266
|
+
*
|
|
1267
|
+
* We don't re-impose WCAG on top of derive's tweaked thresholds — default
|
|
1268
|
+
* validation checks visibility invariants only (selection/cursor vs bg) that
|
|
1269
|
+
* derive doesn't handle.
|
|
1270
|
+
*
|
|
1271
|
+
* @example
|
|
1272
|
+
* ```ts
|
|
1273
|
+
* // Default: lenient + visibility-only (derive already handled contrast)
|
|
1274
|
+
* const theme = loadTheme(myScheme)
|
|
1275
|
+
*
|
|
1276
|
+
* // Build-time audit: strict + full WCAG
|
|
1277
|
+
* const theme = loadTheme(myScheme, { enforce: "strict", wcag: true })
|
|
1278
|
+
* ```
|
|
1279
|
+
*/
|
|
1280
|
+
function loadTheme(palette, opts = {}) {
|
|
1281
|
+
const mode = opts.mode ?? "truecolor";
|
|
1282
|
+
const enforce = opts.enforce ?? "lenient";
|
|
1283
|
+
const theme = deriveTheme(palette, mode, opts.adjustments);
|
|
1284
|
+
if (enforce === "off") return theme;
|
|
1285
|
+
const { ok, violations } = validateThemeInvariants(theme, { wcag: opts.wcag });
|
|
1286
|
+
if (!ok) {
|
|
1287
|
+
if (enforce === "strict") throw new ThemeInvariantError(violations);
|
|
1288
|
+
if (opts.violations) opts.violations.push(...violations);
|
|
1289
|
+
}
|
|
1290
|
+
return theme;
|
|
1291
|
+
}
|
|
1292
|
+
const AA$1 = 4.5;
|
|
1293
|
+
const DIM = 3;
|
|
1294
|
+
const FAINT$1 = 1.5;
|
|
1295
|
+
const CONTROL = 3;
|
|
1296
|
+
/**
|
|
1297
|
+
* Build a "raw" Theme with legacy single-hex role fields. The output is NOT a
|
|
1298
|
+
* complete Sterling Theme — Sterling roles + flat tokens are layered on by
|
|
1299
|
+
* `inlineSterlingTokens` at the end of `deriveTheme`. The cast at the bottom
|
|
1300
|
+
* (`as unknown as Theme`) acknowledges the staged construction; the contract
|
|
1301
|
+
* `deriveTheme()` returns a fully-shaped Sterling Theme is honored at the
|
|
1302
|
+
* `deriveTheme` boundary, not here.
|
|
1303
|
+
*
|
|
1304
|
+
* Legacy fields (`primary`, `primaryfg`, `accent`, `accentfg`, `errorfg`,
|
|
1305
|
+
* `successfg`, `warningfg`, `infofg`, `secondaryfg`, `focusborder`,
|
|
1306
|
+
* `inputborder`, `disabledfg`, `mutedbg`, `surfacebg`, `popoverbg`, `cursor`,
|
|
1307
|
+
* `cursorbg`, `secondary`, `border`) are still emitted at runtime so app code
|
|
1308
|
+
* that still uses `theme.primary` / `theme.errorfg` keeps working. The selection
|
|
1309
|
+
* / inverse / link aliases (`inverse`, `inversebg`, `selection`, `selectionbg`,
|
|
1310
|
+
* `link`) were dropped in 0.21.0 (sterling-purge-legacy-tokens) — consumers must
|
|
1311
|
+
* read Sterling's flat tokens (`bg-selected`, `fg-on-selected`, `bg-inverse`,
|
|
1312
|
+
* `fg-on-inverse`, `fg-link`).
|
|
1313
|
+
*/
|
|
1314
|
+
function deriveTruecolorTheme(p, adjustments) {
|
|
1315
|
+
const dark = p.dark ?? true;
|
|
1316
|
+
const bg = p.background;
|
|
1317
|
+
function ensure(token, color, against, target) {
|
|
1318
|
+
const result = ensureContrast(color, against, target);
|
|
1319
|
+
if (adjustments && result !== color) {
|
|
1320
|
+
const before = checkContrast(color, against);
|
|
1321
|
+
const after = checkContrast(result, against);
|
|
1322
|
+
adjustments.push({
|
|
1323
|
+
token,
|
|
1324
|
+
from: color,
|
|
1325
|
+
to: result,
|
|
1326
|
+
against,
|
|
1327
|
+
target,
|
|
1328
|
+
ratioBefore: before?.ratio ?? 0,
|
|
1329
|
+
ratioAfter: after?.ratio ?? 0
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return result;
|
|
1333
|
+
}
|
|
1334
|
+
const surfacebg = blend(bg, p.foreground, .03);
|
|
1335
|
+
const popoverbg = blend(bg, p.foreground, .08);
|
|
1336
|
+
const fg = ensure("fg", p.foreground, popoverbg, AA$1);
|
|
1337
|
+
const primary = ensure("primary", p.primary ?? (dark ? p.yellow : p.blue), bg, AA$1);
|
|
1338
|
+
const accent = ensure("accent", complement(primary), bg, AA$1);
|
|
1339
|
+
const secondary = ensure("secondary", blend(primary, accent, .35), bg, AA$1);
|
|
1340
|
+
const error = ensure("error", p.red, bg, AA$1);
|
|
1341
|
+
const warning = ensure("warning", p.yellow, bg, AA$1);
|
|
1342
|
+
const success = ensure("success", p.green, bg, AA$1);
|
|
1343
|
+
const info = ensure("info", blend(fg, accent, .5), bg, AA$1);
|
|
1344
|
+
const red = ensure("red", p.red, bg, AA$1);
|
|
1345
|
+
const orange = ensure("orange", blend(p.red, p.yellow, .5), bg, AA$1);
|
|
1346
|
+
const yellow = ensure("yellow", p.yellow, bg, AA$1);
|
|
1347
|
+
const green = ensure("green", p.green, bg, AA$1);
|
|
1348
|
+
const teal = ensure("teal", blend(p.green, p.cyan, .5), bg, AA$1);
|
|
1349
|
+
const blue = ensure("blue", dark ? p.brightBlue : p.blue, bg, AA$1);
|
|
1350
|
+
const purple = ensure("purple", p.magenta, bg, AA$1);
|
|
1351
|
+
const pink = ensure("pink", blend(p.magenta, p.red, .5), bg, AA$1);
|
|
1352
|
+
const mutedbg = blend(bg, p.foreground, .04);
|
|
1353
|
+
const muted = ensure("muted", blend(fg, bg, .4), mutedbg, AA$1);
|
|
1354
|
+
const disabledfg = ensure("disabledfg", blend(fg, bg, .5), bg, DIM);
|
|
1355
|
+
const border = ensure("border", blend(bg, p.foreground, .15), bg, FAINT$1);
|
|
1356
|
+
const inputborder = ensure("inputborder", blend(bg, p.foreground, .25), bg, CONTROL);
|
|
1357
|
+
const selectionBg = repairSelectionBg(p.selectionBackground, bg);
|
|
1358
|
+
const cursorBgRepaired = repairCursorBg(p.cursorColor, bg);
|
|
1359
|
+
const cursor = ensure("cursor", p.cursorText, cursorBgRepaired, AA$1);
|
|
1360
|
+
const derived = deriveFields({
|
|
1361
|
+
dark,
|
|
1362
|
+
primary,
|
|
1363
|
+
accent,
|
|
1364
|
+
fg,
|
|
1365
|
+
selectionbg: selectionBg,
|
|
1366
|
+
surfacebg,
|
|
1367
|
+
ring: {
|
|
1368
|
+
red,
|
|
1369
|
+
orange,
|
|
1370
|
+
yellow,
|
|
1371
|
+
green,
|
|
1372
|
+
teal,
|
|
1373
|
+
blue,
|
|
1374
|
+
purple,
|
|
1375
|
+
pink
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
return {
|
|
1379
|
+
name: p.name ?? (dark ? "derived-dark" : "derived-light"),
|
|
1380
|
+
bg,
|
|
1381
|
+
fg,
|
|
1382
|
+
muted,
|
|
1383
|
+
mutedbg,
|
|
1384
|
+
surface: fg,
|
|
1385
|
+
surfacebg,
|
|
1386
|
+
popover: fg,
|
|
1387
|
+
popoverbg,
|
|
1388
|
+
cursor,
|
|
1389
|
+
cursorbg: cursorBgRepaired,
|
|
1390
|
+
primary,
|
|
1391
|
+
primaryfg: contrastFg(primary),
|
|
1392
|
+
secondary,
|
|
1393
|
+
secondaryfg: contrastFg(secondary),
|
|
1394
|
+
accent,
|
|
1395
|
+
accentfg: contrastFg(accent),
|
|
1396
|
+
error,
|
|
1397
|
+
errorfg: contrastFg(error),
|
|
1398
|
+
warning,
|
|
1399
|
+
warningfg: contrastFg(warning),
|
|
1400
|
+
success,
|
|
1401
|
+
successfg: contrastFg(success),
|
|
1402
|
+
info,
|
|
1403
|
+
infofg: contrastFg(info),
|
|
1404
|
+
border,
|
|
1405
|
+
inputborder,
|
|
1406
|
+
focusborder: ensure("focusborder", dark ? p.brightBlue : p.blue, bg, AA$1),
|
|
1407
|
+
disabledfg,
|
|
1408
|
+
palette: [
|
|
1409
|
+
p.black,
|
|
1410
|
+
p.red,
|
|
1411
|
+
p.green,
|
|
1412
|
+
p.yellow,
|
|
1413
|
+
p.blue,
|
|
1414
|
+
p.magenta,
|
|
1415
|
+
p.cyan,
|
|
1416
|
+
p.white,
|
|
1417
|
+
p.brightBlack,
|
|
1418
|
+
p.brightRed,
|
|
1419
|
+
p.brightGreen,
|
|
1420
|
+
p.brightYellow,
|
|
1421
|
+
p.brightBlue,
|
|
1422
|
+
p.brightMagenta,
|
|
1423
|
+
p.brightCyan,
|
|
1424
|
+
p.brightWhite
|
|
1425
|
+
],
|
|
1426
|
+
...derived
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function deriveAnsi16Theme(p) {
|
|
1430
|
+
return inlineSterlingTokens(deriveAnsi16ThemeRaw(p), p);
|
|
1431
|
+
}
|
|
1432
|
+
function deriveAnsi16ThemeRaw(p) {
|
|
1433
|
+
const dark = p.dark ?? true;
|
|
1434
|
+
const primaryColor = dark ? p.yellow : p.blue;
|
|
1435
|
+
const accentColor = p.cyan;
|
|
1436
|
+
const derived = deriveFields({
|
|
1437
|
+
primary: primaryColor,
|
|
1438
|
+
accent: accentColor,
|
|
1439
|
+
fg: p.foreground,
|
|
1440
|
+
selectionbg: p.selectionBackground,
|
|
1441
|
+
surfacebg: p.black,
|
|
1442
|
+
ring: {
|
|
1443
|
+
red: dark ? p.brightRed : p.red,
|
|
1444
|
+
orange: dark ? p.brightRed : p.red,
|
|
1445
|
+
yellow: p.yellow,
|
|
1446
|
+
green: dark ? p.brightGreen : p.green,
|
|
1447
|
+
teal: p.cyan,
|
|
1448
|
+
blue: dark ? p.brightBlue : p.blue,
|
|
1449
|
+
purple: p.magenta,
|
|
1450
|
+
pink: dark ? p.brightMagenta : p.magenta
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
return {
|
|
1454
|
+
name: p.name ?? (dark ? "derived-ansi16-dark" : "derived-ansi16-light"),
|
|
1455
|
+
bg: p.background,
|
|
1456
|
+
fg: p.foreground,
|
|
1457
|
+
muted: p.white,
|
|
1458
|
+
mutedbg: p.black,
|
|
1459
|
+
surface: p.foreground,
|
|
1460
|
+
surfacebg: p.black,
|
|
1461
|
+
popover: p.foreground,
|
|
1462
|
+
popoverbg: p.black,
|
|
1463
|
+
cursor: p.cursorText,
|
|
1464
|
+
cursorbg: p.cursorColor,
|
|
1465
|
+
primary: primaryColor,
|
|
1466
|
+
primaryfg: p.black,
|
|
1467
|
+
secondary: p.magenta,
|
|
1468
|
+
secondaryfg: p.black,
|
|
1469
|
+
accent: accentColor,
|
|
1470
|
+
accentfg: p.black,
|
|
1471
|
+
error: dark ? p.brightRed : p.red,
|
|
1472
|
+
errorfg: p.black,
|
|
1473
|
+
warning: p.yellow,
|
|
1474
|
+
warningfg: p.black,
|
|
1475
|
+
success: dark ? p.brightGreen : p.green,
|
|
1476
|
+
successfg: p.black,
|
|
1477
|
+
info: p.cyan,
|
|
1478
|
+
infofg: p.black,
|
|
1479
|
+
border: p.brightBlack,
|
|
1480
|
+
inputborder: p.brightBlack,
|
|
1481
|
+
focusborder: dark ? p.brightBlue : p.blue,
|
|
1482
|
+
disabledfg: p.brightBlack,
|
|
1483
|
+
palette: [
|
|
1484
|
+
p.black,
|
|
1485
|
+
p.red,
|
|
1486
|
+
p.green,
|
|
1487
|
+
p.yellow,
|
|
1488
|
+
p.blue,
|
|
1489
|
+
p.magenta,
|
|
1490
|
+
p.cyan,
|
|
1491
|
+
p.white,
|
|
1492
|
+
p.brightBlack,
|
|
1493
|
+
p.brightRed,
|
|
1494
|
+
p.brightGreen,
|
|
1495
|
+
p.brightYellow,
|
|
1496
|
+
p.brightBlue,
|
|
1497
|
+
p.brightMagenta,
|
|
1498
|
+
p.brightCyan,
|
|
1499
|
+
p.brightWhite
|
|
1500
|
+
],
|
|
1501
|
+
...derived
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Nudge `selectionBg`'s OKLCH lightness until it differs from `bg` by at least
|
|
1506
|
+
* `SELECTION_DELTA_L`. Preserves hue + chroma. Non-hex input returns unchanged.
|
|
1507
|
+
*
|
|
1508
|
+
* Direction: shift away from bg — if bg is dark, lift L; if bg is light, drop L.
|
|
1509
|
+
* If the input already meets the threshold, it's returned unchanged.
|
|
1510
|
+
*/
|
|
1511
|
+
function repairSelectionBg(selectionBg, bg) {
|
|
1512
|
+
const oSel = hexToOklch(selectionBg);
|
|
1513
|
+
const oBg = hexToOklch(bg);
|
|
1514
|
+
if (!oSel || !oBg) return selectionBg;
|
|
1515
|
+
const dL = Math.abs(oSel.L - oBg.L);
|
|
1516
|
+
if (dL >= .08) return selectionBg;
|
|
1517
|
+
const needed = SELECTION_DELTA_L$1 - dL + .005;
|
|
1518
|
+
const direction = oSel.L >= oBg.L ? 1 : -1;
|
|
1519
|
+
return oklchToHex({
|
|
1520
|
+
L: Math.max(0, Math.min(1, oSel.L + direction * needed)),
|
|
1521
|
+
C: oSel.C,
|
|
1522
|
+
H: oSel.H
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Nudge `cursorBg`'s OKLCH values until it differs from `bg` by at least
|
|
1527
|
+
* `CURSOR_DELTA_E`. Shifts lightness first (preserves hue/chroma aesthetics).
|
|
1528
|
+
* Non-hex input returns unchanged.
|
|
1529
|
+
*/
|
|
1530
|
+
function repairCursorBg(cursorBg, bg) {
|
|
1531
|
+
const d = colorDistance(cursorBg, bg);
|
|
1532
|
+
if (d === null || d >= .15) return cursorBg;
|
|
1533
|
+
const oCur = hexToOklch(cursorBg);
|
|
1534
|
+
const oBg = hexToOklch(bg);
|
|
1535
|
+
const lGap = SELECTION_DELTA_L$1 + .02;
|
|
1536
|
+
const direction = oCur.L >= oBg.L ? 1 : -1;
|
|
1537
|
+
const candidate = oklchToHex({
|
|
1538
|
+
L: Math.max(0, Math.min(1, oCur.L + direction * lGap)),
|
|
1539
|
+
C: oCur.C,
|
|
1540
|
+
H: oCur.H
|
|
1541
|
+
});
|
|
1542
|
+
const d2 = colorDistance(candidate, bg);
|
|
1543
|
+
if (d2 !== null && d2 >= .15) return candidate;
|
|
1544
|
+
return oBg.L > .5 ? "#000000" : "#FFFFFF";
|
|
1545
|
+
}
|
|
1546
|
+
//#endregion
|
|
1547
|
+
//#region packages/ansi/src/theme/default-schemes.ts
|
|
1548
|
+
const defaultDarkScheme = {
|
|
1549
|
+
name: "default-dark",
|
|
1550
|
+
dark: true,
|
|
1551
|
+
black: "#2e3440",
|
|
1552
|
+
red: "#bf616a",
|
|
1553
|
+
green: "#a3be8c",
|
|
1554
|
+
yellow: "#ebcb8b",
|
|
1555
|
+
blue: "#81a1c1",
|
|
1556
|
+
magenta: "#b48ead",
|
|
1557
|
+
cyan: "#88c0d0",
|
|
1558
|
+
white: "#d8dee9",
|
|
1559
|
+
brightBlack: "#4c566a",
|
|
1560
|
+
brightRed: "#bf616a",
|
|
1561
|
+
brightGreen: "#a3be8c",
|
|
1562
|
+
brightYellow: "#ebcb8b",
|
|
1563
|
+
brightBlue: "#81a1c1",
|
|
1564
|
+
brightMagenta: "#b48ead",
|
|
1565
|
+
brightCyan: "#8fbcbb",
|
|
1566
|
+
brightWhite: "#eceff4",
|
|
1567
|
+
foreground: "#d8dee9",
|
|
1568
|
+
background: "#2e3440",
|
|
1569
|
+
cursorColor: "#d8dee9",
|
|
1570
|
+
cursorText: "#2e3440",
|
|
1571
|
+
selectionBackground: "#434c5e",
|
|
1572
|
+
selectionForeground: "#d8dee9"
|
|
1573
|
+
};
|
|
1574
|
+
const defaultLightScheme = {
|
|
1575
|
+
name: "default-light",
|
|
1576
|
+
dark: false,
|
|
1577
|
+
black: "#5c6370",
|
|
1578
|
+
red: "#d20f39",
|
|
1579
|
+
green: "#40a02b",
|
|
1580
|
+
yellow: "#df8e1d",
|
|
1581
|
+
blue: "#1e66f5",
|
|
1582
|
+
magenta: "#8839ef",
|
|
1583
|
+
cyan: "#179299",
|
|
1584
|
+
white: "#dce0e8",
|
|
1585
|
+
brightBlack: "#6c7086",
|
|
1586
|
+
brightRed: "#d20f39",
|
|
1587
|
+
brightGreen: "#40a02b",
|
|
1588
|
+
brightYellow: "#df8e1d",
|
|
1589
|
+
brightBlue: "#1e66f5",
|
|
1590
|
+
brightMagenta: "#8839ef",
|
|
1591
|
+
brightCyan: "#179299",
|
|
1592
|
+
brightWhite: "#eff1f5",
|
|
1593
|
+
foreground: "#4c4f69",
|
|
1594
|
+
background: "#eff1f5",
|
|
1595
|
+
cursorColor: "#dc8a78",
|
|
1596
|
+
cursorText: "#eff1f5",
|
|
1597
|
+
selectionBackground: "#ccd0da",
|
|
1598
|
+
selectionForeground: "#4c4f69"
|
|
1599
|
+
};
|
|
1600
|
+
/**
|
|
1601
|
+
* Dark ANSI 16 theme — hex-valued, derived from the default dark scheme.
|
|
1602
|
+
*
|
|
1603
|
+
* All token values are hex strings. Terminal rendering quantizes hex to
|
|
1604
|
+
* 4-bit ANSI codes at paint time when colorLevel === "ansi16".
|
|
1605
|
+
*/
|
|
1606
|
+
const ansi16DarkTheme = deriveAnsi16Theme(defaultDarkScheme);
|
|
1607
|
+
/**
|
|
1608
|
+
* Light ANSI 16 theme — hex-valued, derived from the default light scheme.
|
|
1609
|
+
*
|
|
1610
|
+
* All token values are hex strings. Terminal rendering quantizes hex to
|
|
1611
|
+
* 4-bit ANSI codes at paint time when colorLevel === "ansi16".
|
|
1612
|
+
*/
|
|
1613
|
+
const ansi16LightTheme = deriveAnsi16Theme(defaultLightScheme);
|
|
1614
|
+
//#endregion
|
|
1615
|
+
//#region packages/ansi/src/osc-palette.ts
|
|
1616
|
+
/**
|
|
1617
|
+
* OSC 4 Terminal Color Palette Query/Set — pure ANSI protocol.
|
|
1618
|
+
*/
|
|
1619
|
+
const ESC$4 = "\x1B";
|
|
1620
|
+
const BEL$2 = "\x07";
|
|
1621
|
+
function queryPaletteColor(index, write) {
|
|
1622
|
+
if (index < 0 || index > 255) throw new RangeError(`Palette index must be 0-255, got ${index}`);
|
|
1623
|
+
write(`${ESC$4}]4;${index};?${BEL$2}`);
|
|
1624
|
+
}
|
|
1625
|
+
function queryMultiplePaletteColors(indices, write) {
|
|
1626
|
+
for (const index of indices) queryPaletteColor(index, write);
|
|
1627
|
+
}
|
|
1628
|
+
function setPaletteColor(index, color, write) {
|
|
1629
|
+
if (index < 0 || index > 255) throw new RangeError(`Palette index must be 0-255, got ${index}`);
|
|
1630
|
+
write(`${ESC$4}]4;${index};${color}${BEL$2}`);
|
|
1631
|
+
}
|
|
1632
|
+
const OSC4_PREFIX = `${ESC$4}]4;`;
|
|
1633
|
+
const OSC4_BODY_RE = /^(\d+);rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})$/;
|
|
1634
|
+
function parsePaletteResponse(input) {
|
|
1635
|
+
const prefixIdx = input.indexOf(OSC4_PREFIX);
|
|
1636
|
+
if (prefixIdx === -1) return null;
|
|
1637
|
+
const bodyStart = prefixIdx + OSC4_PREFIX.length;
|
|
1638
|
+
let bodyEnd = input.indexOf(BEL$2, bodyStart);
|
|
1639
|
+
if (bodyEnd === -1) bodyEnd = input.indexOf(`${ESC$4}\\`, bodyStart);
|
|
1640
|
+
if (bodyEnd === -1) return null;
|
|
1641
|
+
const body = input.slice(bodyStart, bodyEnd);
|
|
1642
|
+
const match = OSC4_BODY_RE.exec(body);
|
|
1643
|
+
if (!match) return null;
|
|
1644
|
+
const index = Number.parseInt(match[1], 10);
|
|
1645
|
+
if (index < 0 || index > 255) return null;
|
|
1646
|
+
return {
|
|
1647
|
+
index,
|
|
1648
|
+
color: `#${normalizeHexChannel$1(match[2])}${normalizeHexChannel$1(match[3])}${normalizeHexChannel$1(match[4])}`
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function normalizeHexChannel$1(hex) {
|
|
1652
|
+
switch (hex.length) {
|
|
1653
|
+
case 1: return hex + hex;
|
|
1654
|
+
case 2: return hex;
|
|
1655
|
+
default: return hex.slice(0, 2);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
//#endregion
|
|
1659
|
+
//#region packages/ansi/src/protocol-error.ts
|
|
1660
|
+
/**
|
|
1661
|
+
* ProtocolError — structured error for terminal protocol parse failures.
|
|
1662
|
+
*
|
|
1663
|
+
* Used by parsers to signal that input WAS identified as belonging to this
|
|
1664
|
+
* protocol (prefix/marker matched) but is malformed in a way the parser
|
|
1665
|
+
* cannot recover from (missing terminator, invalid base64, missing required
|
|
1666
|
+
* field, etc.).
|
|
1667
|
+
*
|
|
1668
|
+
* Distinct from a `null` return:
|
|
1669
|
+
*
|
|
1670
|
+
* - `null` = "no input matched, but input was valid" — the parser does not
|
|
1671
|
+
* recognize this input as belonging to its protocol family at all (no
|
|
1672
|
+
* prefix, no marker). Used by discriminator chains to mean "next parser
|
|
1673
|
+
* please."
|
|
1674
|
+
*
|
|
1675
|
+
* - `throw ProtocolError` = "this WAS for us but is broken" — the parser
|
|
1676
|
+
* committed to the protocol (prefix matched), then the body failed
|
|
1677
|
+
* validation. Callers should log and continue, NOT crash.
|
|
1678
|
+
*
|
|
1679
|
+
* Carries structured context so callers can route, dedupe, and log
|
|
1680
|
+
* without re-parsing the raw bytes:
|
|
1681
|
+
*
|
|
1682
|
+
* - `parser` — name of the parser that threw (e.g. "parseClipboardResponse")
|
|
1683
|
+
* - `input` — raw input bytes (truncated for safety; full length retained
|
|
1684
|
+
* in `inputLength`)
|
|
1685
|
+
* - `reason` — short human-readable description of why-invalid
|
|
1686
|
+
*
|
|
1687
|
+
* Acceptance for bead `@km/silvery/15127-custom-protocol-implementation/
|
|
1688
|
+
* protocol-loud-errors`: parsers must fail loudly on protocol violation
|
|
1689
|
+
* instead of silently dropping malformed input.
|
|
1690
|
+
*
|
|
1691
|
+
* @module
|
|
1692
|
+
*/
|
|
1693
|
+
/** Maximum chars of raw input retained on the error (rest is truncated). */
|
|
1694
|
+
const INPUT_TRUNCATE_LENGTH = 256;
|
|
1695
|
+
/**
|
|
1696
|
+
* Thrown by protocol parsers when input is identified as belonging to the
|
|
1697
|
+
* protocol but is malformed.
|
|
1698
|
+
*
|
|
1699
|
+
* Callers (dispatch boundaries in `runtime/input-owner.ts`, `renderer.ts`,
|
|
1700
|
+
* and similar) should catch ProtocolError, log via the appropriate logger,
|
|
1701
|
+
* and continue — never let a protocol parse error crash the app.
|
|
1702
|
+
*/
|
|
1703
|
+
var ProtocolError = class extends Error {
|
|
1704
|
+
name = "ProtocolError";
|
|
1705
|
+
parser;
|
|
1706
|
+
input;
|
|
1707
|
+
inputLength;
|
|
1708
|
+
reason;
|
|
1709
|
+
constructor(opts) {
|
|
1710
|
+
const inputLength = opts.input.length;
|
|
1711
|
+
const truncated = inputLength > INPUT_TRUNCATE_LENGTH ? `${opts.input.slice(0, INPUT_TRUNCATE_LENGTH)}…<${inputLength - INPUT_TRUNCATE_LENGTH} more chars>` : opts.input;
|
|
1712
|
+
super(`${opts.parser}: ${opts.reason} (input=${JSON.stringify(truncated)})`);
|
|
1713
|
+
this.parser = opts.parser;
|
|
1714
|
+
this.input = truncated;
|
|
1715
|
+
this.inputLength = inputLength;
|
|
1716
|
+
this.reason = opts.reason;
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
/** Narrowing helper — true if `err` is a ProtocolError. */
|
|
1720
|
+
function isProtocolError(err) {
|
|
1721
|
+
return err instanceof ProtocolError;
|
|
1722
|
+
}
|
|
1723
|
+
//#endregion
|
|
1724
|
+
//#region packages/ansi/src/osc-colors.ts
|
|
1725
|
+
/**
|
|
1726
|
+
* OSC 10/11/12 Terminal Color Queries — pure ANSI protocol.
|
|
1727
|
+
*/
|
|
1728
|
+
const ESC$3 = "\x1B";
|
|
1729
|
+
const BEL$1 = "\x07";
|
|
1730
|
+
const RGB_BODY_RE$1 = /rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})/;
|
|
1731
|
+
function normalizeHexChannel(hex) {
|
|
1732
|
+
switch (hex.length) {
|
|
1733
|
+
case 1: return hex + hex;
|
|
1734
|
+
case 2: return hex;
|
|
1735
|
+
default: return hex.slice(0, 2);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Parse an OSC 10/11/12 color query response.
|
|
1740
|
+
*
|
|
1741
|
+
* Return semantics (see {@link ProtocolError} for the full contract):
|
|
1742
|
+
* - `null` — input does not contain the OSC `oscCode` prefix (not for us).
|
|
1743
|
+
* - `throw ProtocolError` — prefix matched (we committed to this protocol)
|
|
1744
|
+
* but the response is malformed: missing terminator, body is not a valid
|
|
1745
|
+
* `rgb:RRRR/GGGG/BBBB` spec, etc.
|
|
1746
|
+
*
|
|
1747
|
+
* Exported for testing and so callers in chained-discriminator pipelines
|
|
1748
|
+
* can dispatch raw input through the parser directly. Most users should
|
|
1749
|
+
* use {@link queryForegroundColor} / {@link queryBackgroundColor} /
|
|
1750
|
+
* {@link queryCursorColor} which wrap this with the write+read cycle.
|
|
1751
|
+
*/
|
|
1752
|
+
function parseOscColorResponse(input, oscCode) {
|
|
1753
|
+
const prefix = `${ESC$3}]${oscCode};`;
|
|
1754
|
+
const prefixIdx = input.indexOf(prefix);
|
|
1755
|
+
if (prefixIdx === -1) return null;
|
|
1756
|
+
const bodyStart = prefixIdx + prefix.length;
|
|
1757
|
+
let bodyEnd = input.indexOf(BEL$1, bodyStart);
|
|
1758
|
+
if (bodyEnd === -1) bodyEnd = input.indexOf(`${ESC$3}\\`, bodyStart);
|
|
1759
|
+
if (bodyEnd === -1) throw new ProtocolError({
|
|
1760
|
+
parser: "parseOscColorResponse",
|
|
1761
|
+
input,
|
|
1762
|
+
reason: `OSC ${oscCode} prefix present but missing terminator (expected BEL or ST)`
|
|
1763
|
+
});
|
|
1764
|
+
const body = input.slice(bodyStart, bodyEnd);
|
|
1765
|
+
const match = RGB_BODY_RE$1.exec(body);
|
|
1766
|
+
if (!match) throw new ProtocolError({
|
|
1767
|
+
parser: "parseOscColorResponse",
|
|
1768
|
+
input,
|
|
1769
|
+
reason: `OSC ${oscCode} body is not a valid rgb:RRRR/GGGG/BBBB spec (body=${JSON.stringify(body)})`
|
|
1770
|
+
});
|
|
1771
|
+
return `#${normalizeHexChannel(match[1])}${normalizeHexChannel(match[2])}${normalizeHexChannel(match[3])}`;
|
|
1772
|
+
}
|
|
1773
|
+
async function queryOscColor(write, read, oscCode, timeoutMs) {
|
|
1774
|
+
write(`${ESC$3}]${oscCode};?${BEL$1}`);
|
|
1775
|
+
const data = await read(timeoutMs);
|
|
1776
|
+
if (data == null) return null;
|
|
1777
|
+
return parseOscColorResponse(data, oscCode);
|
|
1778
|
+
}
|
|
1779
|
+
async function queryForegroundColor(write, read, timeoutMs = 200) {
|
|
1780
|
+
return queryOscColor(write, read, 10, timeoutMs);
|
|
1781
|
+
}
|
|
1782
|
+
async function queryBackgroundColor(write, read, timeoutMs = 200) {
|
|
1783
|
+
return queryOscColor(write, read, 11, timeoutMs);
|
|
1784
|
+
}
|
|
1785
|
+
async function queryCursorColor(write, read, timeoutMs = 200) {
|
|
1786
|
+
return queryOscColor(write, read, 12, timeoutMs);
|
|
1787
|
+
}
|
|
1788
|
+
function setForegroundColor(write, color) {
|
|
1789
|
+
write(`${ESC$3}]10;${color}${BEL$1}`);
|
|
1790
|
+
}
|
|
1791
|
+
function setBackgroundColor(write, color) {
|
|
1792
|
+
write(`${ESC$3}]11;${color}${BEL$1}`);
|
|
1793
|
+
}
|
|
1794
|
+
function setCursorColor(write, color) {
|
|
1795
|
+
write(`${ESC$3}]12;${color}${BEL$1}`);
|
|
1796
|
+
}
|
|
1797
|
+
function resetForegroundColor(write) {
|
|
1798
|
+
write(`${ESC$3}]110${BEL$1}`);
|
|
1799
|
+
}
|
|
1800
|
+
function resetBackgroundColor(write) {
|
|
1801
|
+
write(`${ESC$3}]111${BEL$1}`);
|
|
1802
|
+
}
|
|
1803
|
+
function resetCursorColor(write) {
|
|
1804
|
+
write(`${ESC$3}]112${BEL$1}`);
|
|
1805
|
+
}
|
|
1806
|
+
async function detectColorScheme(write, read, timeoutMs = 200) {
|
|
1807
|
+
const bg = await queryBackgroundColor(write, read, timeoutMs);
|
|
1808
|
+
if (bg == null) return null;
|
|
1809
|
+
const r = parseInt(bg.slice(1, 3), 16) / 255;
|
|
1810
|
+
const g = parseInt(bg.slice(3, 5), 16) / 255;
|
|
1811
|
+
const b = parseInt(bg.slice(5, 7), 16) / 255;
|
|
1812
|
+
return .2126 * r + .7152 * g + .0722 * b > .5 ? "light" : "dark";
|
|
1813
|
+
}
|
|
1814
|
+
//#endregion
|
|
1815
|
+
//#region packages/ansi/src/theme/detect.ts
|
|
1816
|
+
/**
|
|
1817
|
+
* Probe the terminal for its 22-slot color scheme via OSC 4/10/11 queries.
|
|
1818
|
+
*
|
|
1819
|
+
* Pure terminal primitive — no fingerprinting, no theme derivation. Returns the
|
|
1820
|
+
* raw probed slots (or `null` if probing isn't available, e.g. non-TTY).
|
|
1821
|
+
*
|
|
1822
|
+
* For the full detection cascade (override → probe → fingerprint → fallback +
|
|
1823
|
+
* theme derivation), use `detectScheme` from `@silvery/ansi` or
|
|
1824
|
+
* `detectTheme` from `@silvery/theme`.
|
|
1825
|
+
*
|
|
1826
|
+
* `probeColors` is the canonical name; `detectTerminalScheme` is the legacy
|
|
1827
|
+
* alias kept for backward compatibility.
|
|
1828
|
+
*
|
|
1829
|
+
* Call styles:
|
|
1830
|
+
* await probeColors() // default timeout, standalone
|
|
1831
|
+
* await probeColors(150) // legacy positional timeout
|
|
1832
|
+
* await probeColors({ timeoutMs: 150 }) // options form
|
|
1833
|
+
* await probeColors({ input: inputOwner, timeoutMs: 150 }) // routed through InputOwner
|
|
1834
|
+
*/
|
|
1835
|
+
async function probeColors(timeoutOrOpts) {
|
|
1836
|
+
const opts = typeof timeoutOrOpts === "number" ? { timeoutMs: timeoutOrOpts } : timeoutOrOpts ?? {};
|
|
1837
|
+
const timeoutMs = opts.timeoutMs ?? 150;
|
|
1838
|
+
if (opts.input) return probeColorsViaOwner(opts.input, timeoutMs);
|
|
1839
|
+
const stdin = process.stdin;
|
|
1840
|
+
const stdout = process.stdout;
|
|
1841
|
+
if (!stdin.isTTY || !stdout.isTTY) return null;
|
|
1842
|
+
const otherListeners = stdin.listenerCount("data") > 0;
|
|
1843
|
+
const wasRaw = stdin.isRaw;
|
|
1844
|
+
let didSetRaw = false;
|
|
1845
|
+
if (!wasRaw && !otherListeners) {
|
|
1846
|
+
stdin.setRawMode(true);
|
|
1847
|
+
didSetRaw = true;
|
|
1848
|
+
}
|
|
1849
|
+
let buffer = "";
|
|
1850
|
+
const onData = (chunk) => {
|
|
1851
|
+
buffer += chunk.toString();
|
|
1852
|
+
};
|
|
1853
|
+
stdin.on("data", onData);
|
|
1854
|
+
try {
|
|
1855
|
+
const write = (s) => {
|
|
1856
|
+
stdout.write(s);
|
|
1857
|
+
};
|
|
1858
|
+
const read = (ms) => new Promise((resolve) => {
|
|
1859
|
+
if (buffer.length > 0) {
|
|
1860
|
+
const result = buffer;
|
|
1861
|
+
buffer = "";
|
|
1862
|
+
resolve(result);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const timer = setTimeout(() => {
|
|
1866
|
+
resolve(buffer.length > 0 ? buffer : null);
|
|
1867
|
+
buffer = "";
|
|
1868
|
+
}, ms);
|
|
1869
|
+
const check = (_chunk) => {
|
|
1870
|
+
clearTimeout(timer);
|
|
1871
|
+
stdin.removeListener("data", check);
|
|
1872
|
+
const result = buffer;
|
|
1873
|
+
buffer = "";
|
|
1874
|
+
resolve(result);
|
|
1875
|
+
};
|
|
1876
|
+
stdin.on("data", check);
|
|
1877
|
+
});
|
|
1878
|
+
const bg = await queryBackgroundColor(write, read, timeoutMs);
|
|
1879
|
+
const fg = await queryForegroundColor(write, read, timeoutMs);
|
|
1880
|
+
const ansi = new Array(16).fill(null);
|
|
1881
|
+
queryMultiplePaletteColors(Array.from({ length: 16 }, (_, i) => i), write);
|
|
1882
|
+
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
1883
|
+
const remaining = buffer;
|
|
1884
|
+
buffer = "";
|
|
1885
|
+
if (remaining) {
|
|
1886
|
+
const oscPrefix = "\x1B]4;";
|
|
1887
|
+
let pos = 0;
|
|
1888
|
+
while (pos < remaining.length) {
|
|
1889
|
+
const nextOsc = remaining.indexOf(oscPrefix, pos);
|
|
1890
|
+
if (nextOsc === -1) break;
|
|
1891
|
+
let end = remaining.indexOf("\x07", nextOsc);
|
|
1892
|
+
if (end === -1) end = remaining.indexOf("\x1B\\", nextOsc);
|
|
1893
|
+
if (end === -1) break;
|
|
1894
|
+
const parsed = parsePaletteResponse(remaining.slice(nextOsc, end + 1));
|
|
1895
|
+
if (parsed && parsed.index >= 0 && parsed.index < 16) ansi[parsed.index] = parsed.color;
|
|
1896
|
+
pos = end + 1;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const dark = bg ? isDarkColor(bg) : true;
|
|
1900
|
+
const palette = { dark };
|
|
1901
|
+
if (bg) palette.background = bg;
|
|
1902
|
+
if (fg) palette.foreground = fg;
|
|
1903
|
+
const ansiFields = [
|
|
1904
|
+
"black",
|
|
1905
|
+
"red",
|
|
1906
|
+
"green",
|
|
1907
|
+
"yellow",
|
|
1908
|
+
"blue",
|
|
1909
|
+
"magenta",
|
|
1910
|
+
"cyan",
|
|
1911
|
+
"white",
|
|
1912
|
+
"brightBlack",
|
|
1913
|
+
"brightRed",
|
|
1914
|
+
"brightGreen",
|
|
1915
|
+
"brightYellow",
|
|
1916
|
+
"brightBlue",
|
|
1917
|
+
"brightMagenta",
|
|
1918
|
+
"brightCyan",
|
|
1919
|
+
"brightWhite"
|
|
1920
|
+
];
|
|
1921
|
+
for (let i = 0; i < 16; i++) if (ansi[i]) palette[ansiFields[i]] = ansi[i];
|
|
1922
|
+
if (fg) palette.cursorColor = fg;
|
|
1923
|
+
if (bg) palette.cursorText = bg;
|
|
1924
|
+
return {
|
|
1925
|
+
fg,
|
|
1926
|
+
bg,
|
|
1927
|
+
ansi,
|
|
1928
|
+
dark,
|
|
1929
|
+
palette
|
|
1930
|
+
};
|
|
1931
|
+
} finally {
|
|
1932
|
+
stdin.removeListener("data", onData);
|
|
1933
|
+
if (didSetRaw) stdin.setRawMode(false);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
const ESC$2 = "\x1B";
|
|
1937
|
+
const BEL = "\x07";
|
|
1938
|
+
const RGB_BODY_RE = /rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})/;
|
|
1939
|
+
/** OSC response parser for a specific OSC code (10 or 11). Returns the
|
|
1940
|
+
* first matching response in the buffer and the byte count to consume
|
|
1941
|
+
* (the end of that response, so leading garbage is cleared as well).
|
|
1942
|
+
*/
|
|
1943
|
+
function parseOscColor(acc, oscCode) {
|
|
1944
|
+
const prefix = `${ESC$2}]${oscCode};`;
|
|
1945
|
+
const prefixIdx = acc.indexOf(prefix);
|
|
1946
|
+
if (prefixIdx === -1) return null;
|
|
1947
|
+
const bodyStart = prefixIdx + prefix.length;
|
|
1948
|
+
let bodyEnd = acc.indexOf(BEL, bodyStart);
|
|
1949
|
+
let terminatorLen = 1;
|
|
1950
|
+
if (bodyEnd === -1) {
|
|
1951
|
+
bodyEnd = acc.indexOf(`${ESC$2}\\`, bodyStart);
|
|
1952
|
+
terminatorLen = 2;
|
|
1953
|
+
if (bodyEnd === -1) return null;
|
|
1954
|
+
}
|
|
1955
|
+
const body = acc.slice(bodyStart, bodyEnd);
|
|
1956
|
+
const match = RGB_BODY_RE.exec(body);
|
|
1957
|
+
if (!match) return null;
|
|
1958
|
+
return {
|
|
1959
|
+
result: `#${normalizeHex(match[1])}${normalizeHex(match[2])}${normalizeHex(match[3])}`,
|
|
1960
|
+
consumed: bodyEnd + terminatorLen
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
function normalizeHex(channel) {
|
|
1964
|
+
if (channel.length === 1) return channel + channel;
|
|
1965
|
+
if (channel.length === 2) return channel;
|
|
1966
|
+
return channel.slice(0, 2);
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Route OSC 10/11/4 queries through an InputOwner. Same semantics as the
|
|
1970
|
+
* standalone `probeColors` path (FG + BG sequentially, 16 palette slots in
|
|
1971
|
+
* one burst with a final drain), but all stdin access is owner-mediated —
|
|
1972
|
+
* no direct raw-mode toggles, no stdin.on("data").
|
|
1973
|
+
*/
|
|
1974
|
+
async function probeColorsViaOwner(input, timeoutMs) {
|
|
1975
|
+
const fgQuery = `${ESC$2}]10;?${BEL}`;
|
|
1976
|
+
const fg = await input.probe({
|
|
1977
|
+
query: fgQuery,
|
|
1978
|
+
parse: (acc) => parseOscColor(acc, 10),
|
|
1979
|
+
timeoutMs
|
|
1980
|
+
});
|
|
1981
|
+
const bgQuery = `${ESC$2}]11;?${BEL}`;
|
|
1982
|
+
const bg = await input.probe({
|
|
1983
|
+
query: bgQuery,
|
|
1984
|
+
parse: (acc) => parseOscColor(acc, 11),
|
|
1985
|
+
timeoutMs
|
|
1986
|
+
});
|
|
1987
|
+
const ansi = new Array(16).fill(null);
|
|
1988
|
+
let filled = 0;
|
|
1989
|
+
const oscPrefix = `${ESC$2}]4;`;
|
|
1990
|
+
let burstQuery = "";
|
|
1991
|
+
for (let i = 0; i < 16; i++) burstQuery += `${ESC$2}]4;${i};?${BEL}`;
|
|
1992
|
+
await input.probe({
|
|
1993
|
+
query: burstQuery,
|
|
1994
|
+
parse: (acc) => {
|
|
1995
|
+
let pos = 0;
|
|
1996
|
+
while (pos < acc.length) {
|
|
1997
|
+
const next = acc.indexOf(oscPrefix, pos);
|
|
1998
|
+
if (next === -1) break;
|
|
1999
|
+
let end = acc.indexOf(BEL, next);
|
|
2000
|
+
let termLen = 1;
|
|
2001
|
+
if (end === -1) {
|
|
2002
|
+
end = acc.indexOf(`${ESC$2}\\`, next);
|
|
2003
|
+
termLen = 2;
|
|
2004
|
+
if (end === -1) break;
|
|
2005
|
+
}
|
|
2006
|
+
const parsed = parsePaletteResponse(acc.slice(next, end + termLen));
|
|
2007
|
+
if (parsed && parsed.index >= 0 && parsed.index < 16 && ansi[parsed.index] == null) {
|
|
2008
|
+
ansi[parsed.index] = parsed.color;
|
|
2009
|
+
filled++;
|
|
2010
|
+
}
|
|
2011
|
+
pos = end + termLen;
|
|
2012
|
+
}
|
|
2013
|
+
if (filled === 16) return {
|
|
2014
|
+
result: true,
|
|
2015
|
+
consumed: acc.length
|
|
2016
|
+
};
|
|
2017
|
+
return null;
|
|
2018
|
+
},
|
|
2019
|
+
timeoutMs
|
|
2020
|
+
});
|
|
2021
|
+
const dark = bg ? isDarkColor(bg) : true;
|
|
2022
|
+
const palette = { dark };
|
|
2023
|
+
if (bg) palette.background = bg;
|
|
2024
|
+
if (fg) palette.foreground = fg;
|
|
2025
|
+
const ansiFields = [
|
|
2026
|
+
"black",
|
|
2027
|
+
"red",
|
|
2028
|
+
"green",
|
|
2029
|
+
"yellow",
|
|
2030
|
+
"blue",
|
|
2031
|
+
"magenta",
|
|
2032
|
+
"cyan",
|
|
2033
|
+
"white",
|
|
2034
|
+
"brightBlack",
|
|
2035
|
+
"brightRed",
|
|
2036
|
+
"brightGreen",
|
|
2037
|
+
"brightYellow",
|
|
2038
|
+
"brightBlue",
|
|
2039
|
+
"brightMagenta",
|
|
2040
|
+
"brightCyan",
|
|
2041
|
+
"brightWhite"
|
|
2042
|
+
];
|
|
2043
|
+
for (let i = 0; i < 16; i++) if (ansi[i]) palette[ansiFields[i]] = ansi[i];
|
|
2044
|
+
if (fg) palette.cursorColor = fg;
|
|
2045
|
+
if (bg) palette.cursorText = bg;
|
|
2046
|
+
if (fg == null && bg == null && filled === 0) return null;
|
|
2047
|
+
return {
|
|
2048
|
+
fg,
|
|
2049
|
+
bg,
|
|
2050
|
+
ansi,
|
|
2051
|
+
dark,
|
|
2052
|
+
palette
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Legacy alias for {@link probeColors}. Prefer `probeColors` in new code —
|
|
2057
|
+
* the name says what it does (probes terminal color slots), and "detect" is
|
|
2058
|
+
* reserved for the full cascade (`detectScheme`, `detectTheme`). Retained
|
|
2059
|
+
* as a stable alias — no deprecation schedule.
|
|
2060
|
+
*/
|
|
2061
|
+
const detectTerminalScheme = probeColors;
|
|
2062
|
+
async function detectTheme(opts = {}) {
|
|
2063
|
+
const colorLevel = opts.caps?.colorLevel;
|
|
2064
|
+
if (colorLevel === "mono" || colorLevel === "ansi16") return opts.caps?.darkBackground ?? true ? ansi16DarkTheme : ansi16LightTheme;
|
|
2065
|
+
const detected = await probeColors({
|
|
2066
|
+
timeoutMs: opts.timeoutMs,
|
|
2067
|
+
input: opts.input
|
|
2068
|
+
});
|
|
2069
|
+
const isDark = detected?.dark ?? opts.caps?.darkBackground ?? true;
|
|
2070
|
+
const fallback = opts.fallback ?? (isDark ? opts.fallbackDark ?? defaultDarkScheme : opts.fallbackLight ?? defaultLightScheme);
|
|
2071
|
+
if (!detected) return deriveTheme(fallback);
|
|
2072
|
+
return deriveTheme({
|
|
2073
|
+
...fallback,
|
|
2074
|
+
...stripNulls$1(detected.palette)
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
function stripNulls$1(partial) {
|
|
2078
|
+
const result = {};
|
|
2079
|
+
for (const [k, v] of Object.entries(partial)) if (v != null) result[k] = v;
|
|
2080
|
+
return result;
|
|
2081
|
+
}
|
|
2082
|
+
function isDarkColor(hex) {
|
|
2083
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
2084
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
2085
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
2086
|
+
return .2126 * r + .7152 * g + .0722 * b <= .5;
|
|
2087
|
+
}
|
|
2088
|
+
//#endregion
|
|
2089
|
+
//#region packages/ansi/src/kitty-graphics-probe.ts
|
|
2090
|
+
const APC = "\x1B_G";
|
|
2091
|
+
const ST = "\x1B\\";
|
|
2092
|
+
/**
|
|
2093
|
+
* The image id used for the support query. Chosen well above the runtime image
|
|
2094
|
+
* id range (1..255) so a probe ack can never be confused with a real image's
|
|
2095
|
+
* response.
|
|
2096
|
+
*/
|
|
2097
|
+
const KITTY_PROBE_ID = 7777;
|
|
2098
|
+
/**
|
|
2099
|
+
* Parse a Kitty graphics query response for {@link KITTY_PROBE_ID}.
|
|
2100
|
+
*
|
|
2101
|
+
* - `{ result: true }` — `\x1b_Gi=<id>;OK\x1b\` → graphics supported.
|
|
2102
|
+
* - `{ result: false }` — a well-formed response for our id that is NOT `OK`
|
|
2103
|
+
* (the terminal parsed the APC but rejected the query).
|
|
2104
|
+
* - `null` — no complete response for our id yet (the probe keeps
|
|
2105
|
+
* waiting; on timeout the caller treats this as
|
|
2106
|
+
* unsupported).
|
|
2107
|
+
*/
|
|
2108
|
+
function parseKittyGraphicsResponse(acc, id) {
|
|
2109
|
+
const prefix = `${APC}i=${id};`;
|
|
2110
|
+
const start = acc.indexOf(prefix);
|
|
2111
|
+
if (start === -1) return null;
|
|
2112
|
+
const bodyStart = start + prefix.length;
|
|
2113
|
+
const end = acc.indexOf(ST, bodyStart);
|
|
2114
|
+
if (end === -1) return null;
|
|
2115
|
+
return {
|
|
2116
|
+
result: acc.slice(bodyStart, end).trim() === "OK",
|
|
2117
|
+
consumed: end + 2
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Probe the terminal at runtime for Kitty graphics support. Resolves:
|
|
2122
|
+
* - `true` — the terminal acknowledged it can paint Kitty graphics.
|
|
2123
|
+
* - `false` — a non-`OK` response (the protocol is understood but refused).
|
|
2124
|
+
* - `undefined` — no response within `timeoutMs` (the terminal does not speak
|
|
2125
|
+
* the protocol, or a proxy/capture that cannot paint it).
|
|
2126
|
+
*
|
|
2127
|
+
* The query is a 1×1 RGB query-only command, harmless to a terminal that ignores
|
|
2128
|
+
* unknown APC sequences. Routed through the session's {@link ProbeInputOwner}
|
|
2129
|
+
* (never `process.stdin` directly) so it is safe inside a running TUI.
|
|
2130
|
+
*/
|
|
2131
|
+
async function probeKittyGraphics(input, timeoutMs = 150) {
|
|
2132
|
+
const query = `${APC}i=${KITTY_PROBE_ID},s=1,v=1,a=q,t=d,f=24;AAAA${ST}`;
|
|
2133
|
+
return await input.probe({
|
|
2134
|
+
query,
|
|
2135
|
+
timeoutMs,
|
|
2136
|
+
parse: (acc) => parseKittyGraphicsResponse(acc, 7777)
|
|
2137
|
+
}) ?? void 0;
|
|
2138
|
+
}
|
|
2139
|
+
//#endregion
|
|
2140
|
+
//#region packages/ansi/src/color-maps.ts
|
|
2141
|
+
/** Modifier SGR codes: open -> close */
|
|
2142
|
+
const MODIFIERS = {
|
|
2143
|
+
reset: [0, 0],
|
|
2144
|
+
bold: [1, 22],
|
|
2145
|
+
dim: [2, 22],
|
|
2146
|
+
italic: [3, 23],
|
|
2147
|
+
underline: [4, 24],
|
|
2148
|
+
inverse: [7, 27],
|
|
2149
|
+
hidden: [8, 28],
|
|
2150
|
+
strikethrough: [9, 29],
|
|
2151
|
+
overline: [53, 55]
|
|
2152
|
+
};
|
|
2153
|
+
/** Foreground color name -> ANSI SGR code */
|
|
2154
|
+
const FG_COLORS = {
|
|
2155
|
+
black: 30,
|
|
2156
|
+
red: 31,
|
|
2157
|
+
green: 32,
|
|
2158
|
+
yellow: 33,
|
|
2159
|
+
blue: 34,
|
|
2160
|
+
magenta: 35,
|
|
2161
|
+
cyan: 36,
|
|
2162
|
+
white: 37,
|
|
2163
|
+
blackBright: 90,
|
|
2164
|
+
gray: 90,
|
|
2165
|
+
grey: 90,
|
|
2166
|
+
redBright: 91,
|
|
2167
|
+
greenBright: 92,
|
|
2168
|
+
yellowBright: 93,
|
|
2169
|
+
blueBright: 94,
|
|
2170
|
+
magentaBright: 95,
|
|
2171
|
+
cyanBright: 96,
|
|
2172
|
+
whiteBright: 97
|
|
2173
|
+
};
|
|
2174
|
+
/** Background color name -> ANSI SGR code */
|
|
2175
|
+
const BG_COLORS = {
|
|
2176
|
+
bgBlack: 40,
|
|
2177
|
+
bgRed: 41,
|
|
2178
|
+
bgGreen: 42,
|
|
2179
|
+
bgYellow: 43,
|
|
2180
|
+
bgBlue: 44,
|
|
2181
|
+
bgMagenta: 45,
|
|
2182
|
+
bgCyan: 46,
|
|
2183
|
+
bgWhite: 47,
|
|
2184
|
+
bgBlackBright: 100,
|
|
2185
|
+
bgGray: 100,
|
|
2186
|
+
bgGrey: 100,
|
|
2187
|
+
bgRedBright: 101,
|
|
2188
|
+
bgGreenBright: 102,
|
|
2189
|
+
bgYellowBright: 103,
|
|
2190
|
+
bgBlueBright: 104,
|
|
2191
|
+
bgMagentaBright: 105,
|
|
2192
|
+
bgCyanBright: 106,
|
|
2193
|
+
bgWhiteBright: 107
|
|
2194
|
+
};
|
|
2195
|
+
/** Standard ANSI 16 color RGB values for nearest-color matching.
|
|
2196
|
+
* Uses xterm-256 standard values for consistent mapping (matches chalk/ansi-styles). */
|
|
2197
|
+
const ANSI_16_COLORS = [
|
|
2198
|
+
[
|
|
2199
|
+
0,
|
|
2200
|
+
0,
|
|
2201
|
+
0
|
|
2202
|
+
],
|
|
2203
|
+
[
|
|
2204
|
+
128,
|
|
2205
|
+
0,
|
|
2206
|
+
0
|
|
2207
|
+
],
|
|
2208
|
+
[
|
|
2209
|
+
0,
|
|
2210
|
+
128,
|
|
2211
|
+
0
|
|
2212
|
+
],
|
|
2213
|
+
[
|
|
2214
|
+
128,
|
|
2215
|
+
128,
|
|
2216
|
+
0
|
|
2217
|
+
],
|
|
2218
|
+
[
|
|
2219
|
+
0,
|
|
2220
|
+
0,
|
|
2221
|
+
128
|
|
2222
|
+
],
|
|
2223
|
+
[
|
|
2224
|
+
128,
|
|
2225
|
+
0,
|
|
2226
|
+
128
|
|
2227
|
+
],
|
|
2228
|
+
[
|
|
2229
|
+
0,
|
|
2230
|
+
128,
|
|
2231
|
+
128
|
|
2232
|
+
],
|
|
2233
|
+
[
|
|
2234
|
+
192,
|
|
2235
|
+
192,
|
|
2236
|
+
192
|
|
2237
|
+
],
|
|
2238
|
+
[
|
|
2239
|
+
128,
|
|
2240
|
+
128,
|
|
2241
|
+
128
|
|
2242
|
+
],
|
|
2243
|
+
[
|
|
2244
|
+
255,
|
|
2245
|
+
0,
|
|
2246
|
+
0
|
|
2247
|
+
],
|
|
2248
|
+
[
|
|
2249
|
+
0,
|
|
2250
|
+
255,
|
|
2251
|
+
0
|
|
2252
|
+
],
|
|
2253
|
+
[
|
|
2254
|
+
255,
|
|
2255
|
+
255,
|
|
2256
|
+
0
|
|
2257
|
+
],
|
|
2258
|
+
[
|
|
2259
|
+
0,
|
|
2260
|
+
0,
|
|
2261
|
+
255
|
|
2262
|
+
],
|
|
2263
|
+
[
|
|
2264
|
+
255,
|
|
2265
|
+
0,
|
|
2266
|
+
255
|
|
2267
|
+
],
|
|
2268
|
+
[
|
|
2269
|
+
0,
|
|
2270
|
+
255,
|
|
2271
|
+
255
|
|
2272
|
+
],
|
|
2273
|
+
[
|
|
2274
|
+
255,
|
|
2275
|
+
255,
|
|
2276
|
+
255
|
|
2277
|
+
]
|
|
2278
|
+
];
|
|
2279
|
+
/**
|
|
2280
|
+
* Canonical hex values for the 16 standard ANSI color slots.
|
|
2281
|
+
*
|
|
2282
|
+
* These are the xterm-256 reference values — the same RGB values used by
|
|
2283
|
+
* `nearestAnsi16` for quantization. Used to convert ANSI16 slot-name themes
|
|
2284
|
+
* (e.g., "yellow") into hex-valued themes (e.g., "#808000") so Theme objects
|
|
2285
|
+
* are pure hex across all tiers.
|
|
2286
|
+
*
|
|
2287
|
+
* Terminal-rendering behavior is unchanged: the output phase reads `colorLevel`
|
|
2288
|
+
* and emits 4-bit ANSI codes when `colorLevel === "basic"` — the hex value is
|
|
2289
|
+
* only carried in the Theme object itself, not emitted verbatim.
|
|
2290
|
+
*/
|
|
2291
|
+
const ANSI16_SLOT_HEX = {
|
|
2292
|
+
black: "#000000",
|
|
2293
|
+
red: "#800000",
|
|
2294
|
+
green: "#008000",
|
|
2295
|
+
yellow: "#808000",
|
|
2296
|
+
blue: "#000080",
|
|
2297
|
+
magenta: "#800080",
|
|
2298
|
+
cyan: "#008080",
|
|
2299
|
+
white: "#c0c0c0",
|
|
2300
|
+
blackBright: "#808080",
|
|
2301
|
+
gray: "#808080",
|
|
2302
|
+
grey: "#808080",
|
|
2303
|
+
redBright: "#ff0000",
|
|
2304
|
+
greenBright: "#00ff00",
|
|
2305
|
+
yellowBright: "#ffff00",
|
|
2306
|
+
blueBright: "#0000ff",
|
|
2307
|
+
magentaBright: "#ff00ff",
|
|
2308
|
+
cyanBright: "#00ffff",
|
|
2309
|
+
whiteBright: "#ffffff"
|
|
2310
|
+
};
|
|
2311
|
+
/** Find nearest ANSI 16 color index for an RGB value. */
|
|
2312
|
+
function nearestAnsi16(r, g, b) {
|
|
2313
|
+
let bestIdx = 0;
|
|
2314
|
+
let bestDist = Infinity;
|
|
2315
|
+
for (let i = 0; i < 16; i++) {
|
|
2316
|
+
const [cr, cg, cb] = ANSI_16_COLORS[i];
|
|
2317
|
+
const dist = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
|
|
2318
|
+
if (dist < bestDist) {
|
|
2319
|
+
bestDist = dist;
|
|
2320
|
+
bestIdx = i;
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
return bestIdx;
|
|
2324
|
+
}
|
|
2325
|
+
/** Convert RGB to 256-color index (using the 6x6x6 color cube). */
|
|
2326
|
+
function rgbToAnsi256(r, g, b) {
|
|
2327
|
+
if (r === g && g === b) {
|
|
2328
|
+
if (r < 8) return 16;
|
|
2329
|
+
if (r > 248) return 231;
|
|
2330
|
+
return Math.round((r - 8) / 247 * 24) + 232;
|
|
2331
|
+
}
|
|
2332
|
+
const ri = Math.round(r / 255 * 5);
|
|
2333
|
+
const gi = Math.round(g / 255 * 5);
|
|
2334
|
+
const bi = Math.round(b / 255 * 5);
|
|
2335
|
+
return 16 + 36 * ri + 6 * gi + bi;
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Generate SGR foreground code for an RGB color at the given color tier.
|
|
2339
|
+
* Returns the SGR parameter string (e.g., "31" or "38;5;196" or "38;2;255;0;0").
|
|
2340
|
+
*
|
|
2341
|
+
* Tiers `"truecolor"`, `"256"`, and `"ansi16"` emit color codes. `"mono"`
|
|
2342
|
+
* is handled by the caller (no SGR code is emitted for color at the mono tier)
|
|
2343
|
+
* — this function coerces `"mono"` to the `"ansi16"` code path rather than
|
|
2344
|
+
* throwing, since callers that get here with `"mono"` have already bypassed
|
|
2345
|
+
* the mono short-circuit.
|
|
2346
|
+
*/
|
|
2347
|
+
function fgFromRgb(r, g, b, tier) {
|
|
2348
|
+
if (tier === "truecolor") return `38;2;${r};${g};${b}`;
|
|
2349
|
+
if (tier === "256") return `38;5;${rgbToAnsi256(r, g, b)}`;
|
|
2350
|
+
const idx = nearestAnsi16(r, g, b);
|
|
2351
|
+
return idx < 8 ? `${30 + idx}` : `${82 + idx}`;
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* Generate SGR background code for an RGB color at the given color tier.
|
|
2355
|
+
* See {@link fgFromRgb} for tier handling.
|
|
2356
|
+
*/
|
|
2357
|
+
function bgFromRgb(r, g, b, tier) {
|
|
2358
|
+
if (tier === "truecolor") return `48;2;${r};${g};${b}`;
|
|
2359
|
+
if (tier === "256") return `48;5;${rgbToAnsi256(r, g, b)}`;
|
|
2360
|
+
const idx = nearestAnsi16(r, g, b);
|
|
2361
|
+
return idx < 8 ? `${40 + idx}` : `${92 + idx}`;
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Convert a 256-palette index back to its canonical xterm hex value.
|
|
2365
|
+
*
|
|
2366
|
+
* Mirrors `rgbToAnsi256`:
|
|
2367
|
+
* - 16–231: 6×6×6 color cube. Index = 16 + 36·r + 6·g + b (each channel 0..5).
|
|
2368
|
+
* - 232–255: 24-step grayscale ramp.
|
|
2369
|
+
* - 0–15: ANSI 16 slots (reuses ANSI16_SLOT_HEX for exact parity with
|
|
2370
|
+
* `nearestAnsi16`).
|
|
2371
|
+
*/
|
|
2372
|
+
function ansi256ToHex(idx) {
|
|
2373
|
+
if (idx < 0 || idx > 255 || !Number.isInteger(idx)) return "#000000";
|
|
2374
|
+
if (idx < 16) {
|
|
2375
|
+
const [r, g, b] = ANSI_16_COLORS[idx];
|
|
2376
|
+
return rgbToHexHash(r, g, b);
|
|
2377
|
+
}
|
|
2378
|
+
if (idx < 232) {
|
|
2379
|
+
const levels = [
|
|
2380
|
+
0,
|
|
2381
|
+
95,
|
|
2382
|
+
135,
|
|
2383
|
+
175,
|
|
2384
|
+
215,
|
|
2385
|
+
255
|
|
2386
|
+
];
|
|
2387
|
+
const i = idx - 16;
|
|
2388
|
+
const r = levels[Math.floor(i / 36)];
|
|
2389
|
+
const g = levels[Math.floor(i % 36 / 6)];
|
|
2390
|
+
const b = levels[i % 6];
|
|
2391
|
+
return rgbToHexHash(r, g, b);
|
|
2392
|
+
}
|
|
2393
|
+
const gray = 8 + (idx - 232) * 10;
|
|
2394
|
+
return rgbToHexHash(gray, gray, gray);
|
|
2395
|
+
}
|
|
2396
|
+
function rgbToHexHash(r, g, b) {
|
|
2397
|
+
const h = (n) => n.toString(16).padStart(2, "0");
|
|
2398
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
2399
|
+
}
|
|
2400
|
+
function parseHexLocal(hex) {
|
|
2401
|
+
if (typeof hex !== "string") return null;
|
|
2402
|
+
let s = hex.trim();
|
|
2403
|
+
if (s.startsWith("#")) s = s.slice(1);
|
|
2404
|
+
if (s.length === 3) s = s.split("").map((c) => c + c).join("");
|
|
2405
|
+
if (s.length !== 6) return null;
|
|
2406
|
+
const r = parseInt(s.slice(0, 2), 16);
|
|
2407
|
+
const g = parseInt(s.slice(2, 4), 16);
|
|
2408
|
+
const b = parseInt(s.slice(4, 6), 16);
|
|
2409
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
|
|
2410
|
+
return [
|
|
2411
|
+
r,
|
|
2412
|
+
g,
|
|
2413
|
+
b
|
|
2414
|
+
];
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Hex-in / hex-out quantization for previews.
|
|
2418
|
+
*
|
|
2419
|
+
* Takes any hex color and returns the hex a real terminal at that tier would
|
|
2420
|
+
* actually emit. Used by the Sterling storybook to make the `1/2/3/4` tier
|
|
2421
|
+
* toggle visibly different in-process — the output phase already does this
|
|
2422
|
+
* when writing to a real TTY, but preview surfaces (theme swatches, rendered
|
|
2423
|
+
* components inside a storybook app) bypass output-phase quantization. Apply
|
|
2424
|
+
* `quantizeHex` at render time to mimic tier-specific terminal output.
|
|
2425
|
+
*
|
|
2426
|
+
* - `truecolor`: returns the input unchanged (normalized to `#rrggbb`).
|
|
2427
|
+
* - `256`: snaps to the nearest xterm-256 slot, then returns that slot's hex.
|
|
2428
|
+
* - `ansi16`: snaps to one of the 16 standard slots (canonical xterm RGB).
|
|
2429
|
+
* - `mono`: luminance threshold (>= 0.5 → `#ffffff`, else `#000000`).
|
|
2430
|
+
*
|
|
2431
|
+
* Returns the input unchanged if it cannot be parsed as a hex color.
|
|
2432
|
+
*/
|
|
2433
|
+
function quantizeHex(hex, tier) {
|
|
2434
|
+
const rgb = parseHexLocal(hex);
|
|
2435
|
+
if (!rgb) return hex;
|
|
2436
|
+
const [r, g, b] = rgb;
|
|
2437
|
+
if (tier === "truecolor") return rgbToHexHash(r, g, b);
|
|
2438
|
+
if (tier === "256") return ansi256ToHex(rgbToAnsi256(r, g, b));
|
|
2439
|
+
if (tier === "ansi16") {
|
|
2440
|
+
const [cr, cg, cb] = ANSI_16_COLORS[nearestAnsi16(r, g, b)];
|
|
2441
|
+
return rgbToHexHash(cr, cg, cb);
|
|
2442
|
+
}
|
|
2443
|
+
return (.2126 * r + .7152 * g + .0722 * b) / 255 >= .5 ? "#ffffff" : "#000000";
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Hex-regex used to detect hex leaves during tier quantization walks.
|
|
2447
|
+
* Matches `#rgb` and `#rrggbb` (case-insensitive).
|
|
2448
|
+
*/
|
|
2449
|
+
const HEX_LEAF_RE$1 = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
|
|
2450
|
+
function isHexLeaf$1(value) {
|
|
2451
|
+
return typeof value === "string" && HEX_LEAF_RE$1.test(value);
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Pre-quantize every hex leaf in a Theme (or any object tree) to the
|
|
2455
|
+
* requested color tier.
|
|
2456
|
+
*
|
|
2457
|
+
* Walks the input recursively — each string leaf matching `#rgb` / `#rrggbb`
|
|
2458
|
+
* is passed through {@link quantizeHex}; all other values (numbers, booleans,
|
|
2459
|
+
* non-hex strings like `"Nord"`, null/undefined, arrays of non-hex values)
|
|
2460
|
+
* pass through unchanged. Arrays and nested objects are rebuilt with
|
|
2461
|
+
* quantized leaves.
|
|
2462
|
+
*
|
|
2463
|
+
* Works on both the legacy ANSI Theme (flat hex tokens + `palette` array)
|
|
2464
|
+
* and the Sterling Theme (nested roles + flat tokens) — the structural rule
|
|
2465
|
+
* "any leaf that looks like a hex is a color value" holds for both.
|
|
2466
|
+
*
|
|
2467
|
+
* @example Pre-cache tier variants
|
|
2468
|
+
* ```ts
|
|
2469
|
+
* import { pickColorLevel } from "silvery"
|
|
2470
|
+
*
|
|
2471
|
+
* const themes = {
|
|
2472
|
+
* truecolor: theme,
|
|
2473
|
+
* ansi16: pickColorLevel(theme, "ansi16"),
|
|
2474
|
+
* mono: pickColorLevel(theme, "mono"),
|
|
2475
|
+
* }
|
|
2476
|
+
* ```
|
|
2477
|
+
*
|
|
2478
|
+
* @example Storybook — show multiple tiers simultaneously
|
|
2479
|
+
* ```tsx
|
|
2480
|
+
* <ThemeProvider theme={pickColorLevel(theme, "ansi16")}>
|
|
2481
|
+
* <AlertPreview />
|
|
2482
|
+
* </ThemeProvider>
|
|
2483
|
+
* ```
|
|
2484
|
+
*
|
|
2485
|
+
* Notes:
|
|
2486
|
+
* - `truecolor` is a no-op — returns the input unchanged (identity).
|
|
2487
|
+
* - The result is structurally identical to the input (same keys, same
|
|
2488
|
+
* nesting); only hex leaves are remapped.
|
|
2489
|
+
* - Idempotent per tier: `pickColorLevel(pickColorLevel(t, "ansi16"), "ansi16")`
|
|
2490
|
+
* yields the same hex values as `pickColorLevel(t, "ansi16")`.
|
|
2491
|
+
* - Does not freeze the returned object. Callers that want immutability
|
|
2492
|
+
* should `Object.freeze()` (or deep-freeze) the result themselves.
|
|
2493
|
+
*/
|
|
2494
|
+
function pickColorLevel(theme, tier) {
|
|
2495
|
+
if (tier === "truecolor") return theme;
|
|
2496
|
+
return pickColorLevelWalk(theme, tier);
|
|
2497
|
+
}
|
|
2498
|
+
function pickColorLevelWalk(obj, tier) {
|
|
2499
|
+
if (obj == null) return obj;
|
|
2500
|
+
if (isHexLeaf$1(obj)) return quantizeHex(obj, tier);
|
|
2501
|
+
if (Array.isArray(obj)) return obj.map((v) => pickColorLevelWalk(v, tier));
|
|
2502
|
+
if (typeof obj === "object") {
|
|
2503
|
+
const out = {};
|
|
2504
|
+
for (const [k, v] of Object.entries(obj)) out[k] = pickColorLevelWalk(v, tier);
|
|
2505
|
+
return out;
|
|
2506
|
+
}
|
|
2507
|
+
return obj;
|
|
2508
|
+
}
|
|
2509
|
+
//#endregion
|
|
2510
|
+
//#region packages/ansi/src/profile.ts
|
|
2511
|
+
/**
|
|
2512
|
+
* Terminal profile — single source of truth for terminal detection.
|
|
2513
|
+
*
|
|
2514
|
+
* One function, one profile. Collapses the previously redundant trio of
|
|
2515
|
+
* `detectColor()`, `detectTerminalCaps()`, and `resolveColorTier()` into a
|
|
2516
|
+
* single entry point: {@link createTerminalProfile}.
|
|
2517
|
+
*
|
|
2518
|
+
* Phase 3 of `km-silvery.terminal-profile-plateau`. Phase 4 (unify entry
|
|
2519
|
+
* points) will thread the profile through `run()`, `createApp().run()`, and
|
|
2520
|
+
* `render()` so every Term instance is populated from one detection pass.
|
|
2521
|
+
*
|
|
2522
|
+
* Post km-silvery.plateau-naming-polish (2026-04-23): 2-layer shape —
|
|
2523
|
+
* `profile.caps` (protocol flags + `maybe*` heuristics) and `profile.emulator`
|
|
2524
|
+
* (program/version/TERM). The former `profile.heuristics` namespace was
|
|
2525
|
+
* absorbed into caps with a `maybe` prefix per-field.
|
|
2526
|
+
*
|
|
2527
|
+
* Phase 7 of `km-silvery.caps-restructure` (Pro verdict 2026-04-23) originally
|
|
2528
|
+
* split the flat `TerminalCaps` into three layers; the heuristics layer proved
|
|
2529
|
+
* too small to earn its own namespace and was collapsed in the naming polish.
|
|
2530
|
+
*/
|
|
2531
|
+
/**
|
|
2532
|
+
* Build a {@link TerminalProfile} from the current environment.
|
|
2533
|
+
*
|
|
2534
|
+
* Priority for the final `colorLevel` (highest wins):
|
|
2535
|
+
* 1. `NO_COLOR` env var → `"mono"`
|
|
2536
|
+
* 2. `FORCE_COLOR` env var → `0/false → mono, 1 → ansi16, 2 → 256, 3 → truecolor`
|
|
2537
|
+
* 3. `options.colorLevel` (caller-supplied explicit tier)
|
|
2538
|
+
* 4. `options.caps.colorLevel` (base caps' pre-detected tier)
|
|
2539
|
+
* 5. Auto-detected tier from env (TERM, COLORTERM, TERM_PROGRAM, …)
|
|
2540
|
+
*
|
|
2541
|
+
* The env-var precedence (1 & 2) matches the existing `detectColor()` semantics
|
|
2542
|
+
* and is observed on every silvery entry point — tests pass with explicit
|
|
2543
|
+
* env vars even when a caller forces a tier via `colorLevel`.
|
|
2544
|
+
*
|
|
2545
|
+
* When `options.caps` is provided, the profile treats those as the base
|
|
2546
|
+
* capabilities and skips the env-based caps detection — only the color tier
|
|
2547
|
+
* is resolved through the precedence chain above. When `options.caps` is
|
|
2548
|
+
* absent, the full `detectTerminalProfileFromEnv` pass runs.
|
|
2549
|
+
*
|
|
2550
|
+
* No I/O beyond whatever `detectTerminalCaps()` already does (a `defaults read`
|
|
2551
|
+
* call on macOS for Apple Terminal dark-mode heuristics — cached).
|
|
2552
|
+
*
|
|
2553
|
+
* @example
|
|
2554
|
+
* ```ts
|
|
2555
|
+
* // Auto-detect from process.env + process.stdout
|
|
2556
|
+
* const profile = createTerminalProfile()
|
|
2557
|
+
* console.log(profile.colorLevel) // "truecolor" on Ghostty
|
|
2558
|
+
*
|
|
2559
|
+
* // Force a tier (still honors NO_COLOR / FORCE_COLOR env precedence)
|
|
2560
|
+
* const forced = createTerminalProfile({ colorLevel: "256" })
|
|
2561
|
+
*
|
|
2562
|
+
* // Term path — base caps already detected, just resolve color tier.
|
|
2563
|
+
* const termProfile = createTerminalProfile({
|
|
2564
|
+
* colorLevel: userColorLevel,
|
|
2565
|
+
* caps: term.caps,
|
|
2566
|
+
* })
|
|
2567
|
+
*
|
|
2568
|
+
* // Headless/test fixture — zero env influence
|
|
2569
|
+
* const fake = createTerminalProfile({
|
|
2570
|
+
* env: {},
|
|
2571
|
+
* stdout: { isTTY: true },
|
|
2572
|
+
* colorLevel: "truecolor",
|
|
2573
|
+
* })
|
|
2574
|
+
* ```
|
|
2575
|
+
*/
|
|
2576
|
+
function createTerminalProfile(options = {}) {
|
|
2577
|
+
const env = options.env ?? process.env;
|
|
2578
|
+
const stdout = options.stdout ?? process.stdout;
|
|
2579
|
+
const stdin = "stdin" in options ? options.stdin : process.stdin;
|
|
2580
|
+
const envTier = envColorTier(env);
|
|
2581
|
+
const overrideTier = options.colorLevel === null ? "mono" : options.colorLevel ?? void 0;
|
|
2582
|
+
const baseCapsTier = options.caps?.colorLevel;
|
|
2583
|
+
let resolvedTier;
|
|
2584
|
+
let colorProvenance;
|
|
2585
|
+
if (envTier !== void 0) {
|
|
2586
|
+
resolvedTier = envTier;
|
|
2587
|
+
colorProvenance = "env";
|
|
2588
|
+
} else if (overrideTier !== void 0) {
|
|
2589
|
+
resolvedTier = overrideTier;
|
|
2590
|
+
colorProvenance = "override";
|
|
2591
|
+
} else if (baseCapsTier !== void 0) {
|
|
2592
|
+
resolvedTier = baseCapsTier;
|
|
2593
|
+
colorProvenance = "caller-caps";
|
|
2594
|
+
} else {
|
|
2595
|
+
resolvedTier = detectColorFromEnv(env, stdout);
|
|
2596
|
+
colorProvenance = "auto";
|
|
2597
|
+
}
|
|
2598
|
+
const detected = options.caps ? void 0 : detectTerminalProfileFromEnv(env, stdout);
|
|
2599
|
+
const baseCaps = options.caps ? {
|
|
2600
|
+
...defaultCaps(),
|
|
2601
|
+
...options.caps
|
|
2602
|
+
} : detected.caps;
|
|
2603
|
+
const baseEmulator = options.emulator ? {
|
|
2604
|
+
...defaultEmulator(),
|
|
2605
|
+
...options.emulator
|
|
2606
|
+
} : detected?.emulator ?? defaultEmulator();
|
|
2607
|
+
const inputResolved = options.caps?.input ?? (stdin?.isTTY === true && typeof stdin.setRawMode === "function");
|
|
2608
|
+
return freezeProfileInDev({
|
|
2609
|
+
emulator: baseEmulator,
|
|
2610
|
+
caps: {
|
|
2611
|
+
...baseCaps,
|
|
2612
|
+
colorLevel: resolvedTier,
|
|
2613
|
+
colorForced: colorProvenance === "env" || colorProvenance === "override",
|
|
2614
|
+
colorProvenance,
|
|
2615
|
+
input: inputResolved
|
|
2616
|
+
},
|
|
2617
|
+
colorLevel: resolvedTier
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Freeze a profile (plus its nested caps / emulator) in dev builds so
|
|
2622
|
+
* `profile.colorLevel === profile.caps.colorLevel` and every other invariant
|
|
2623
|
+
* can't silently drift via direct mutation. Production builds skip the
|
|
2624
|
+
* freeze to keep the allocation cheap; the type-level `readonly` fields
|
|
2625
|
+
* already block TS-side writes.
|
|
2626
|
+
*
|
|
2627
|
+
* Per km-silvery.profile-immutable (/pro review 2026-04-23): profiles are
|
|
2628
|
+
* snapshot values by contract. Any caller that needs to "change" a profile
|
|
2629
|
+
* must build a new one — the plateau-era single-source-of-truth guarantee
|
|
2630
|
+
* leans on this.
|
|
2631
|
+
*/
|
|
2632
|
+
function freezeProfileInDev(profile) {
|
|
2633
|
+
if (process.env.NODE_ENV === "production") return profile;
|
|
2634
|
+
Object.freeze(profile.caps);
|
|
2635
|
+
Object.freeze(profile.emulator);
|
|
2636
|
+
Object.freeze(profile);
|
|
2637
|
+
return profile;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* Build a {@link TerminalProfile} with an OSC-detected `theme` bundled in.
|
|
2641
|
+
*
|
|
2642
|
+
* Async because the theme probe writes OSC queries to stdout and waits for
|
|
2643
|
+
* responses on stdin. This is the Phase-H2 variant of
|
|
2644
|
+
* {@link createTerminalProfile} — everything the sync factory does, plus:
|
|
2645
|
+
*
|
|
2646
|
+
* 1. Run `detectTheme` (OSC 4/10/11 probe with fallback) once.
|
|
2647
|
+
* 2. Pre-quantize the resulting theme via {@link pickColorLevel} when the
|
|
2648
|
+
* tier was forced ({@link TerminalCaps.colorForced} is `true`) so
|
|
2649
|
+
* token hex values match what the pipeline will actually emit.
|
|
2650
|
+
* 3. Return the profile with `theme` populated — one detection, one profile
|
|
2651
|
+
* flowing end-to-end through run() / createApp().
|
|
2652
|
+
*
|
|
2653
|
+
* Call sites previously ran `createTerminalProfile(...)` + `detectTheme(...)`
|
|
2654
|
+
* + `pickColorLevel(...)` as three separate steps on both the Term-path and
|
|
2655
|
+
* options-path branches. Collapsing that into one function removes the
|
|
2656
|
+
* duplication and the possibility of the three views disagreeing about
|
|
2657
|
+
* what was forced.
|
|
2658
|
+
*
|
|
2659
|
+
* When `probeTheme` is `false`, behaves like the sync {@link createTerminalProfile}
|
|
2660
|
+
* but wrapped in a Promise — useful for call sites that want uniform async
|
|
2661
|
+
* treatment regardless of whether a probe is needed.
|
|
2662
|
+
*
|
|
2663
|
+
* @example
|
|
2664
|
+
* ```ts
|
|
2665
|
+
* // Node entry point with TUI-safe probing.
|
|
2666
|
+
* const profile = await probeTerminalProfile({
|
|
2667
|
+
* colorLevel: options.colorLevel,
|
|
2668
|
+
* caps: term.profile.caps,
|
|
2669
|
+
* fallbackDark: nord,
|
|
2670
|
+
* fallbackLight: catppuccinLatte,
|
|
2671
|
+
* input: probeOwner, // structural InputOwner from @silvery/ag-term
|
|
2672
|
+
* })
|
|
2673
|
+
* // profile.caps, profile.colorLevel, profile.caps.colorForced, profile.theme
|
|
2674
|
+
* ```
|
|
2675
|
+
*
|
|
2676
|
+
* @see createTerminalProfile — sync variant, no theme probe
|
|
2677
|
+
* @see DetectThemeOptions — the underlying probe options this wraps
|
|
2678
|
+
*/
|
|
2679
|
+
async function probeTerminalProfile(options = {}) {
|
|
2680
|
+
const profile = createTerminalProfile(options);
|
|
2681
|
+
if (options.probeTheme === false) return profile;
|
|
2682
|
+
let kittyGraphics = profile.caps.kittyGraphics;
|
|
2683
|
+
if (kittyGraphics && options.input) kittyGraphics = await probeKittyGraphics(options.input, options.timeoutMs ?? 150) === true;
|
|
2684
|
+
const theme = await detectTheme({
|
|
2685
|
+
caps: profile.caps,
|
|
2686
|
+
fallbackDark: options.fallbackDark,
|
|
2687
|
+
fallbackLight: options.fallbackLight,
|
|
2688
|
+
timeoutMs: options.timeoutMs,
|
|
2689
|
+
input: options.input
|
|
2690
|
+
});
|
|
2691
|
+
const resolvedTheme = profile.caps.colorForced ? pickColorLevel(theme, profile.colorLevel) : theme;
|
|
2692
|
+
const caps = kittyGraphics === profile.caps.kittyGraphics ? profile.caps : {
|
|
2693
|
+
...profile.caps,
|
|
2694
|
+
kittyGraphics
|
|
2695
|
+
};
|
|
2696
|
+
return freezeProfileInDev({
|
|
2697
|
+
...profile,
|
|
2698
|
+
caps,
|
|
2699
|
+
theme: resolvedTheme
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Deterministic variant of {@link detectColor} that takes env+stdout as args.
|
|
2704
|
+
* Exported-internal so the shim `detectColor()` can delegate without reading
|
|
2705
|
+
* `process.env` twice.
|
|
2706
|
+
*/
|
|
2707
|
+
function detectColorFromEnv(env, stdout) {
|
|
2708
|
+
if (env.NO_COLOR !== void 0) return "mono";
|
|
2709
|
+
const forceColor = env.FORCE_COLOR;
|
|
2710
|
+
if (forceColor !== void 0) {
|
|
2711
|
+
if (forceColor === "0" || forceColor === "false") return "mono";
|
|
2712
|
+
if (forceColor === "1") return "ansi16";
|
|
2713
|
+
if (forceColor === "2") return "256";
|
|
2714
|
+
if (forceColor === "3") return "truecolor";
|
|
2715
|
+
return "ansi16";
|
|
2716
|
+
}
|
|
2717
|
+
if (!stdout.isTTY) return "mono";
|
|
2718
|
+
if (env.TERM === "dumb") return "mono";
|
|
2719
|
+
const colorTerm = env.COLORTERM;
|
|
2720
|
+
if (colorTerm === "truecolor" || colorTerm === "24bit") return "truecolor";
|
|
2721
|
+
const term = env.TERM ?? "";
|
|
2722
|
+
if (term.includes("truecolor") || term.includes("24bit") || term.includes("xterm-ghostty") || term.includes("xterm-kitty") || term.includes("wezterm")) return "truecolor";
|
|
2723
|
+
if (term.includes("256color") || term.includes("256")) return "256";
|
|
2724
|
+
const termProgram = env.TERM_PROGRAM;
|
|
2725
|
+
if (termProgram === "iTerm.app" || termProgram === "Apple_Terminal") return termProgram === "iTerm.app" ? "truecolor" : "256";
|
|
2726
|
+
if (termProgram === "Ghostty" || termProgram === "WezTerm") return "truecolor";
|
|
2727
|
+
if (env.KITTY_WINDOW_ID) return "truecolor";
|
|
2728
|
+
if (term.includes("xterm") || term.includes("color") || term.includes("ansi")) return "ansi16";
|
|
2729
|
+
if (CI_ENVS.some((name) => env[name] !== void 0)) return "ansi16";
|
|
2730
|
+
if (env.WT_SESSION) return "truecolor";
|
|
2731
|
+
return "ansi16";
|
|
2732
|
+
}
|
|
2733
|
+
/**
|
|
2734
|
+
* Env-only FORCE_COLOR / NO_COLOR tier probe. Returns `undefined` when no
|
|
2735
|
+
* env override applies. Used by {@link createTerminalProfile} to enforce that
|
|
2736
|
+
* env always beats caller-supplied overrides.
|
|
2737
|
+
*/
|
|
2738
|
+
function envColorTier(env) {
|
|
2739
|
+
if (env.NO_COLOR !== void 0) return "mono";
|
|
2740
|
+
const force = env.FORCE_COLOR;
|
|
2741
|
+
if (force !== void 0) {
|
|
2742
|
+
if (force === "0" || force === "false") return "mono";
|
|
2743
|
+
if (force === "1") return "ansi16";
|
|
2744
|
+
if (force === "2") return "256";
|
|
2745
|
+
if (force === "3") return "truecolor";
|
|
2746
|
+
return "ansi16";
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Deterministic env-based detection of the full two-layer profile
|
|
2751
|
+
* ({@link TerminalCaps} + {@link TerminalEmulator}). Reads env explicitly
|
|
2752
|
+
* (no `process.env` access) so callers can inject custom environments in
|
|
2753
|
+
* tests. Color tier is derived via {@link detectColorFromEnv} and therefore
|
|
2754
|
+
* honors FORCE_COLOR / NO_COLOR.
|
|
2755
|
+
*/
|
|
2756
|
+
function detectTerminalProfileFromEnv(env, stdout) {
|
|
2757
|
+
const program = env.TERM_PROGRAM ?? "";
|
|
2758
|
+
const programLower = program.toLowerCase();
|
|
2759
|
+
const version = env.TERM_PROGRAM_VERSION ?? "";
|
|
2760
|
+
const TERM = env.TERM ?? "";
|
|
2761
|
+
const noColor = env.NO_COLOR !== void 0;
|
|
2762
|
+
const isAppleTerminal = programLower === "apple_terminal";
|
|
2763
|
+
const colorLevel = noColor ? "mono" : detectColorFromEnv(env, stdout);
|
|
2764
|
+
const isKitty = TERM === "xterm-kitty";
|
|
2765
|
+
const isITerm = programLower === "iterm.app";
|
|
2766
|
+
const isGhostty = programLower === "ghostty";
|
|
2767
|
+
const isWezTerm = programLower === "wezterm";
|
|
2768
|
+
const isAlacritty = programLower === "alacritty";
|
|
2769
|
+
const isFoot = TERM === "foot" || TERM === "foot-extra";
|
|
2770
|
+
const isDumb = TERM === "dumb";
|
|
2771
|
+
const isModern = !isDumb && (isKitty || isITerm || isGhostty || isWezTerm || isFoot);
|
|
2772
|
+
let isKittyWithTextSizing = false;
|
|
2773
|
+
if (isKitty) {
|
|
2774
|
+
const parts = version.split(".");
|
|
2775
|
+
const major = Number(parts[0]) || 0;
|
|
2776
|
+
const minor = Number(parts[1]) || 0;
|
|
2777
|
+
isKittyWithTextSizing = major > 0 || major === 0 && minor >= 40;
|
|
2778
|
+
}
|
|
2779
|
+
let maybeDarkBackground = !isAppleTerminal;
|
|
2780
|
+
const colorFgBg = env.COLORFGBG;
|
|
2781
|
+
if (colorFgBg) {
|
|
2782
|
+
const parts = colorFgBg.split(";");
|
|
2783
|
+
const bg = parseInt(parts[parts.length - 1] ?? "", 10);
|
|
2784
|
+
if (!isNaN(bg)) maybeDarkBackground = bg < 7;
|
|
2785
|
+
} else if (isAppleTerminal) maybeDarkBackground = detectMacOSDarkMode();
|
|
2786
|
+
let maybeNerdFont = isModern || isAlacritty;
|
|
2787
|
+
const nfEnv = env.NERDFONT;
|
|
2788
|
+
if (nfEnv === "0" || nfEnv === "false") maybeNerdFont = false;
|
|
2789
|
+
else if (nfEnv === "1" || nfEnv === "true") maybeNerdFont = true;
|
|
2790
|
+
const underlineExtensions = isModern || !isDumb && isAlacritty;
|
|
2791
|
+
const underlineStyles = underlineExtensions ? [
|
|
2792
|
+
"double",
|
|
2793
|
+
"curly",
|
|
2794
|
+
"dotted",
|
|
2795
|
+
"dashed"
|
|
2796
|
+
] : [];
|
|
2797
|
+
const unicode = isModern || !isDumb && env.WT_SESSION !== void 0 || env.KITTY_WINDOW_ID !== void 0 || utf8Locale(env) || !isDumb && termImpliesUnicode(TERM) || env.CI !== void 0 && env.GITHUB_ACTIONS !== void 0;
|
|
2798
|
+
const cursor = stdout.isTTY === true && TERM !== "dumb";
|
|
2799
|
+
return {
|
|
2800
|
+
emulator: {
|
|
2801
|
+
program,
|
|
2802
|
+
version,
|
|
2803
|
+
TERM
|
|
2804
|
+
},
|
|
2805
|
+
caps: {
|
|
2806
|
+
cursor,
|
|
2807
|
+
input: false,
|
|
2808
|
+
colorLevel,
|
|
2809
|
+
colorForced: noColor || env.FORCE_COLOR !== void 0,
|
|
2810
|
+
colorProvenance: noColor || env.FORCE_COLOR !== void 0 ? "env" : "auto",
|
|
2811
|
+
unicode,
|
|
2812
|
+
underlineStyles,
|
|
2813
|
+
underlineColor: underlineExtensions,
|
|
2814
|
+
overline: underlineExtensions,
|
|
2815
|
+
textSizing: isKittyWithTextSizing,
|
|
2816
|
+
kittyKeyboard: !isDumb && (isKitty || isGhostty || isWezTerm || isFoot),
|
|
2817
|
+
bracketedPaste: true,
|
|
2818
|
+
mouse: true,
|
|
2819
|
+
kittyGraphics: !isDumb && (isKitty || isGhostty),
|
|
2820
|
+
sixel: !isDumb && (isFoot || isWezTerm),
|
|
2821
|
+
osc52: isModern || !isDumb && isAlacritty,
|
|
2822
|
+
hyperlinks: isModern || !isDumb && isAlacritty,
|
|
2823
|
+
notifications: isITerm || isKitty,
|
|
2824
|
+
syncOutput: isModern || !isDumb && isAlacritty,
|
|
2825
|
+
maybeDarkBackground,
|
|
2826
|
+
maybeNerdFont,
|
|
2827
|
+
maybeWideEmojis: !isAppleTerminal
|
|
2828
|
+
},
|
|
2829
|
+
colorLevel
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Does `env.LANG` / `LC_ALL` / `LC_CTYPE` name a UTF-8 locale? Absorbed from
|
|
2834
|
+
* the retired `detectUnicode()` helper.
|
|
2835
|
+
*/
|
|
2836
|
+
function utf8Locale(env) {
|
|
2837
|
+
const lang = (env.LANG ?? env.LC_ALL ?? env.LC_CTYPE ?? "").toLowerCase();
|
|
2838
|
+
return lang.includes("utf-8") || lang.includes("utf8");
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Does the `TERM` value imply a multiplexer / terminal family we know renders
|
|
2842
|
+
* unicode correctly? Absorbed from the retired `detectUnicode()` helper.
|
|
2843
|
+
*/
|
|
2844
|
+
function termImpliesUnicode(term) {
|
|
2845
|
+
return term.includes("xterm") || term.includes("rxvt") || term.includes("screen") || term.includes("tmux");
|
|
2846
|
+
}
|
|
2847
|
+
const CI_ENVS = [
|
|
2848
|
+
"CI",
|
|
2849
|
+
"GITHUB_ACTIONS",
|
|
2850
|
+
"GITLAB_CI",
|
|
2851
|
+
"JENKINS_URL",
|
|
2852
|
+
"BUILDKITE",
|
|
2853
|
+
"CIRCLECI",
|
|
2854
|
+
"TRAVIS"
|
|
2855
|
+
];
|
|
2856
|
+
let cachedMacOSDarkMode;
|
|
2857
|
+
function detectMacOSDarkMode() {
|
|
2858
|
+
if (cachedMacOSDarkMode !== void 0) return cachedMacOSDarkMode;
|
|
2859
|
+
try {
|
|
2860
|
+
const { spawnSync } = process.getBuiltinModule("node:child_process");
|
|
2861
|
+
cachedMacOSDarkMode = spawnSync("defaults", [
|
|
2862
|
+
"read",
|
|
2863
|
+
"-g",
|
|
2864
|
+
"AppleInterfaceStyle"
|
|
2865
|
+
], {
|
|
2866
|
+
encoding: "utf-8",
|
|
2867
|
+
timeout: 500
|
|
2868
|
+
}).stdout?.trim() === "Dark";
|
|
2869
|
+
} catch {
|
|
2870
|
+
cachedMacOSDarkMode = false;
|
|
2871
|
+
}
|
|
2872
|
+
return cachedMacOSDarkMode;
|
|
2873
|
+
}
|
|
2874
|
+
//#endregion
|
|
2875
|
+
//#region packages/ansi/src/sgr-codes.ts
|
|
2876
|
+
/**
|
|
2877
|
+
* Marker bit for the PACKED numeric truecolor form: `0x1000000 | r<<16 | g<<8 | b`.
|
|
2878
|
+
* ag-term's ANSI parser (unicode.ts) tracks inline truecolor SGR in this compact
|
|
2879
|
+
* form, and those numbers reach fg/bgColorCode via styleToAnsiCodes when wrapped
|
|
2880
|
+
* text re-emits carried styles (fixSgrAcrossWrappedLines).
|
|
2881
|
+
*/
|
|
2882
|
+
const PACKED_TRUECOLOR = 16777216;
|
|
2883
|
+
/** The palette slot for a color, or `undefined` for a genuine truecolor. */
|
|
2884
|
+
function paletteSlot(color) {
|
|
2885
|
+
const idx = typeof color === "number" ? color : color.index;
|
|
2886
|
+
return typeof idx === "number" && idx >= 0 && idx <= 255 ? idx : void 0;
|
|
2887
|
+
}
|
|
2888
|
+
/** Resolve a non-palette color to truecolor components, unpacking the packed numeric form. */
|
|
2889
|
+
function truecolorRgb(color) {
|
|
2890
|
+
if (typeof color === "number") {
|
|
2891
|
+
if (color >= PACKED_TRUECOLOR && color <= 33554431) return {
|
|
2892
|
+
r: color >> 16 & 255,
|
|
2893
|
+
g: color >> 8 & 255,
|
|
2894
|
+
b: color & 255
|
|
2895
|
+
};
|
|
2896
|
+
throw new Error(`sgr-codes: number ${color} is neither a palette slot (0-255) nor a packed truecolor (0x1000000|rgb)`);
|
|
2897
|
+
}
|
|
2898
|
+
return color;
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* Emit the shortest SGR code string for a foreground color.
|
|
2902
|
+
* - Basic 0-7: 4-bit code (30+N)
|
|
2903
|
+
* - Extended 8-255: 256-color (38;5;N)
|
|
2904
|
+
* - RGB (no palette index): true color (38;2;R;G;B)
|
|
2905
|
+
*/
|
|
2906
|
+
function fgColorCode(color) {
|
|
2907
|
+
const slot = paletteSlot(color);
|
|
2908
|
+
if (slot !== void 0) {
|
|
2909
|
+
if (slot <= 7) return `${30 + slot}`;
|
|
2910
|
+
return `38;5;${slot}`;
|
|
2911
|
+
}
|
|
2912
|
+
const { r, g, b } = truecolorRgb(color);
|
|
2913
|
+
return `38;2;${r};${g};${b}`;
|
|
2914
|
+
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Emit the shortest SGR code string for a background color.
|
|
2917
|
+
* - Basic 0-7: 4-bit code (40+N)
|
|
2918
|
+
* - Extended 8-255: 256-color (48;5;N)
|
|
2919
|
+
* - RGB (no palette index): true color (48;2;R;G;B)
|
|
2920
|
+
*/
|
|
2921
|
+
function bgColorCode(color) {
|
|
2922
|
+
const slot = paletteSlot(color);
|
|
2923
|
+
if (slot !== void 0) {
|
|
2924
|
+
if (slot <= 7) return `${40 + slot}`;
|
|
2925
|
+
return `48;5;${slot}`;
|
|
2926
|
+
}
|
|
2927
|
+
const { r, g, b } = truecolorRgb(color);
|
|
2928
|
+
return `48;2;${r};${g};${b}`;
|
|
2929
|
+
}
|
|
2930
|
+
//#endregion
|
|
2931
|
+
//#region packages/ansi/src/utils.ts
|
|
2932
|
+
/**
|
|
2933
|
+
* Process-lifetime set of warning IDs that have already fired. Used by
|
|
2934
|
+
* {@link warnOnce} to avoid console spam on every re-render / every paste /
|
|
2935
|
+
* every parse. Shared across packages — one latch per warning ID, regardless
|
|
2936
|
+
* of which module emits it.
|
|
2937
|
+
*
|
|
2938
|
+
* Intentionally process-global (not scoped per {@link Term}) because the
|
|
2939
|
+
* warnings gated here describe developer-mistake conditions that are
|
|
2940
|
+
* semantically "once per process": spam is worse than missed repeats.
|
|
2941
|
+
*/
|
|
2942
|
+
const _firedWarnings = /* @__PURE__ */ new Set();
|
|
2943
|
+
/**
|
|
2944
|
+
* Emit a warning exactly once per process, keyed by `id`.
|
|
2945
|
+
*
|
|
2946
|
+
* The first call with a given `id` invokes `emit(message)`; subsequent calls
|
|
2947
|
+
* with the same `id` are no-ops. Use for dev-mode checks that would otherwise
|
|
2948
|
+
* spam the console on every render pass / every keystroke / every reconcile.
|
|
2949
|
+
*
|
|
2950
|
+
* Consolidates what used to be three parallel `let hasWarned*` latches
|
|
2951
|
+
* scattered across silvery packages (`test/index.tsx`,
|
|
2952
|
+
* `ag-react/reconciler/host-config.ts`, `ag/keys.ts`). See
|
|
2953
|
+
* km-silvery.latch-consolidation.
|
|
2954
|
+
*
|
|
2955
|
+
* @param id - Unique warning identifier (stable across restarts). Convention:
|
|
2956
|
+
* `<package>:<short-slug>`, e.g. `"silvery/test:termless-leak"`,
|
|
2957
|
+
* `"silvery/ag-react:box-in-text"`.
|
|
2958
|
+
* @param emit - Callback that actually produces the warning. Called once.
|
|
2959
|
+
* Omit to use `console.warn` with no message (rarely useful — prefer an
|
|
2960
|
+
* explicit emit).
|
|
2961
|
+
*
|
|
2962
|
+
* @example
|
|
2963
|
+
* ```ts
|
|
2964
|
+
* import { warnOnce } from "@silvery/ansi"
|
|
2965
|
+
*
|
|
2966
|
+
* function validateBoxInText() {
|
|
2967
|
+
* if (!isValid) {
|
|
2968
|
+
* warnOnce("silvery/ag-react:box-in-text", () =>
|
|
2969
|
+
* console.warn("<Box> cannot be nested inside <Text>.")
|
|
2970
|
+
* )
|
|
2971
|
+
* }
|
|
2972
|
+
* }
|
|
2973
|
+
* ```
|
|
2974
|
+
*/
|
|
2975
|
+
function warnOnce(id, emit) {
|
|
2976
|
+
if (_firedWarnings.has(id)) return;
|
|
2977
|
+
_firedWarnings.add(id);
|
|
2978
|
+
emit();
|
|
2979
|
+
}
|
|
2980
|
+
//#endregion
|
|
2981
|
+
//#region packages/ansi/src/flatten.ts
|
|
2982
|
+
/**
|
|
2983
|
+
* Channel-role-state default rule. Matches Sterling / Primer / CSS-var
|
|
2984
|
+
* conventions: `{kind}-{role}[-{state}]`, with `fg-on-{role}` for `fgOn`,
|
|
2985
|
+
* and implicit-kind collapse for the `surface` / `border` roles.
|
|
2986
|
+
*
|
|
2987
|
+
* Mapping examples:
|
|
2988
|
+
* | Path | Flat key |
|
|
2989
|
+
* | ---------------------------- | --------------------------- |
|
|
2990
|
+
* | `accent.fg` | `fg-accent` |
|
|
2991
|
+
* | `accent.bg` | `bg-accent` |
|
|
2992
|
+
* | `accent.fgOn` | `fg-on-accent` |
|
|
2993
|
+
* | `accent.border` | `border-accent` |
|
|
2994
|
+
* | `accent.hover.bg` | `bg-accent-hover` |
|
|
2995
|
+
* | `accent.active.fg` | `fg-accent-active` |
|
|
2996
|
+
* | `info.hover.bg` | `bg-info-hover` |
|
|
2997
|
+
* | `cursor.fg` | `fg-cursor` |
|
|
2998
|
+
* | `muted.bg` | `bg-muted` |
|
|
2999
|
+
* | `surface.default` | `bg-surface-default` |
|
|
3000
|
+
* | `surface.subtle` | `bg-surface-subtle` |
|
|
3001
|
+
* | `surface.hover` | `bg-surface-hover` |
|
|
3002
|
+
* | `border.default` | `border-default` |
|
|
3003
|
+
* | `border.focus` | `border-focus` |
|
|
3004
|
+
* | `border.muted` | `border-muted` |
|
|
3005
|
+
*
|
|
3006
|
+
* Returns `null` for depth-1 leaves (e.g. `mode`, `name` — not role-scoped)
|
|
3007
|
+
* so metadata doesn't get flattened. The caller (`bakeFlat`) already filters
|
|
3008
|
+
* non-hex leaves, but the rule also guards paths shorter than 2 segments.
|
|
3009
|
+
*/
|
|
3010
|
+
const defaultFlattenRule = (path) => {
|
|
3011
|
+
if (path.length < 2) return null;
|
|
3012
|
+
const role = path[0];
|
|
3013
|
+
const last = path[path.length - 1];
|
|
3014
|
+
const mid = path.slice(1, -1);
|
|
3015
|
+
if (last === "fgOn") return `fg-on-${role}`;
|
|
3016
|
+
if (last === "fg" || last === "bg" || last === "border") {
|
|
3017
|
+
const state = mid.length > 0 ? mid.join("-") : void 0;
|
|
3018
|
+
return state ? `${last}-${role}-${state}` : `${last}-${role}`;
|
|
3019
|
+
}
|
|
3020
|
+
if (role === "surface") return `bg-surface-${last}`;
|
|
3021
|
+
if (role === "border") return `border-${last}`;
|
|
3022
|
+
return null;
|
|
3023
|
+
};
|
|
3024
|
+
/** Matches `#rgb`, `#rrggbb`, and `#rrggbbaa` (case-insensitive). */
|
|
3025
|
+
const HEX_LEAF_RE = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?([0-9a-fA-F]{2})?$/;
|
|
3026
|
+
function isHexLeaf(value) {
|
|
3027
|
+
return typeof value === "string" && HEX_LEAF_RE.test(value);
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Populate flat hyphen-keys onto `theme` in-place by walking hex leaves and
|
|
3031
|
+
* asking `rule` where each leaf should also live at the root.
|
|
3032
|
+
*
|
|
3033
|
+
* Both the nested and flat forms reference the SAME string (not copies) —
|
|
3034
|
+
* `bakeFlat({...}).accent.bg === bakeFlat({...})["bg-accent"]`.
|
|
3035
|
+
*
|
|
3036
|
+
* `rule` defaults to {@link defaultFlattenRule} (channel-role-state).
|
|
3037
|
+
* Rules returning `null` for a path skip that leaf — useful for suppressing
|
|
3038
|
+
* metadata or implementing partial projections.
|
|
3039
|
+
*
|
|
3040
|
+
* The returned object is deep-frozen. The input object is mutated in place
|
|
3041
|
+
* and returned; callers that want an unfrozen copy should `structuredClone`
|
|
3042
|
+
* before calling.
|
|
3043
|
+
*
|
|
3044
|
+
* @param theme nested POJO of hex-string leaves (plus optional metadata)
|
|
3045
|
+
* @param rule how to compute flat keys from nested paths
|
|
3046
|
+
* @returns the same object, with flat keys added and frozen
|
|
3047
|
+
*/
|
|
3048
|
+
function bakeFlat(theme, rule = defaultFlattenRule) {
|
|
3049
|
+
const root = theme;
|
|
3050
|
+
if (Object.isFrozen(root)) return theme;
|
|
3051
|
+
walk(root, [], root, rule);
|
|
3052
|
+
freezeDeep(root);
|
|
3053
|
+
return theme;
|
|
3054
|
+
}
|
|
3055
|
+
function walk(node, path, root, rule) {
|
|
3056
|
+
for (const key of Object.keys(node)) {
|
|
3057
|
+
if (node === root && key.includes("-")) continue;
|
|
3058
|
+
const value = node[key];
|
|
3059
|
+
const subpath = [...path, key];
|
|
3060
|
+
if (isHexLeaf(value)) {
|
|
3061
|
+
const flatKey = rule(subpath);
|
|
3062
|
+
if (flatKey !== null) root[flatKey] = value;
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
if (value && typeof value === "object" && !Array.isArray(value)) walk(value, subpath, root, rule);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
function freezeDeep(o) {
|
|
3069
|
+
if (o === null || typeof o !== "object") return;
|
|
3070
|
+
if (Object.isFrozen(o)) return;
|
|
3071
|
+
Object.freeze(o);
|
|
3072
|
+
for (const k of Object.keys(o)) freezeDeep(o[k]);
|
|
3073
|
+
}
|
|
3074
|
+
//#endregion
|
|
3075
|
+
//#region packages/ansi/src/terminal-control.ts
|
|
3076
|
+
/**
|
|
3077
|
+
* ANSI terminal control helpers.
|
|
3078
|
+
*
|
|
3079
|
+
* Pure string-returning functions for terminal control sequences.
|
|
3080
|
+
* No side effects, no stdout writes -- consumers compose and write.
|
|
3081
|
+
*
|
|
3082
|
+
* Covers: screen management, cursor control, scroll regions,
|
|
3083
|
+
* mouse tracking, keyboard protocols, and bracketed paste.
|
|
3084
|
+
*
|
|
3085
|
+
* @see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
|
3086
|
+
* @see https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
3087
|
+
*/
|
|
3088
|
+
/** Escape character (0x1B) */
|
|
3089
|
+
const ESC$1 = "\x1B";
|
|
3090
|
+
/** Control Sequence Introducer: ESC [ */
|
|
3091
|
+
const CSI$1 = `${ESC$1}[`;
|
|
3092
|
+
`${ESC$1}`;
|
|
3093
|
+
function enableMouse(options = {}) {
|
|
3094
|
+
return `${CSI$1}?1003h${CSI$1}?1006h${options.pixels ? `${CSI$1}?1016h` : ""}`;
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Disable mouse tracking. Disables in reverse order of enabling.
|
|
3098
|
+
*/
|
|
3099
|
+
function disableMouse() {
|
|
3100
|
+
return `${CSI$1}?1016l${CSI$1}?1006l${CSI$1}?1003l`;
|
|
3101
|
+
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Enable bracketed paste mode (DEC private mode 2004).
|
|
3104
|
+
* Terminal wraps pasted text with markers so the app can distinguish
|
|
3105
|
+
* paste from typed input.
|
|
3106
|
+
*/
|
|
3107
|
+
function enableBracketedPaste() {
|
|
3108
|
+
return `${CSI$1}?2004h`;
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Disable bracketed paste mode.
|
|
3112
|
+
*/
|
|
3113
|
+
function disableBracketedPaste() {
|
|
3114
|
+
return `${CSI$1}?2004l`;
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3117
|
+
* Enable the Kitty keyboard protocol (push mode).
|
|
3118
|
+
*
|
|
3119
|
+
* Sends CSI > flags u to opt into the specified modes.
|
|
3120
|
+
* Supported by: Ghostty, Kitty, WezTerm, foot. Ignored by unsupported terminals.
|
|
3121
|
+
*
|
|
3122
|
+
* Flags are a bitfield:
|
|
3123
|
+
*
|
|
3124
|
+
* | Flag | Bit | Description |
|
|
3125
|
+
* | ---- | --- | ----------------------------------------- |
|
|
3126
|
+
* | 1 | 0 | Disambiguate escape codes |
|
|
3127
|
+
* | 2 | 1 | Report event types (press/repeat/release) |
|
|
3128
|
+
* | 4 | 2 | Report alternate keys |
|
|
3129
|
+
* | 8 | 3 | Report all keys as escape codes |
|
|
3130
|
+
* | 16 | 4 | Report associated text |
|
|
3131
|
+
*
|
|
3132
|
+
* @param flags Bitfield of Kitty keyboard flags
|
|
3133
|
+
*/
|
|
3134
|
+
function enableKittyKeyboard(flags = 1) {
|
|
3135
|
+
return `${CSI$1}>${flags}u`;
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Disable the Kitty keyboard protocol (pop mode stack).
|
|
3139
|
+
* Sends CSI < u to restore the previous keyboard mode.
|
|
3140
|
+
*/
|
|
3141
|
+
function disableKittyKeyboard() {
|
|
3142
|
+
return `${CSI$1}<u`;
|
|
3143
|
+
}
|
|
3144
|
+
//#endregion
|
|
3145
|
+
//#region packages/ansi/src/kitty-graphics.ts
|
|
3146
|
+
/**
|
|
3147
|
+
* Kitty graphics protocol — minimal encoder for cell-sized overlay placements.
|
|
3148
|
+
*
|
|
3149
|
+
* Used by the backdrop-fade pass to fade emoji/wide-char glyphs (SGR 2 "dim"
|
|
3150
|
+
* is a no-op on bitmap emoji in most terminals). We upload a single tiny
|
|
3151
|
+
* translucent RGBA image once, then emit `a=p` placements over specific cells
|
|
3152
|
+
* with `z=1` so the overlay sits on top of the already-rendered emoji glyph.
|
|
3153
|
+
*
|
|
3154
|
+
* ## Protocol summary
|
|
3155
|
+
*
|
|
3156
|
+
* The Kitty graphics protocol uses APC escapes: `\x1b_G<control>;<payload>\x1b\\`.
|
|
3157
|
+
* Control parameters are key=value pairs separated by commas. Payload is
|
|
3158
|
+
* base64-encoded image data (only on upload).
|
|
3159
|
+
*
|
|
3160
|
+
* Key commands we use:
|
|
3161
|
+
*
|
|
3162
|
+
* | Command | Meaning |
|
|
3163
|
+
* | ----------- | ------------------------------------------------------------- |
|
|
3164
|
+
* | `a=t` | Transmit (upload) image data |
|
|
3165
|
+
* | `a=p` | Place a previously-uploaded image |
|
|
3166
|
+
* | `a=d` | Delete (remove placements and/or free images) |
|
|
3167
|
+
* | `f=32` | RGBA pixel format (4 bytes per pixel) |
|
|
3168
|
+
* | `s=W,v=H` | Source image dimensions in pixels |
|
|
3169
|
+
* | `i=<id>` | Image ID (stable across frames) |
|
|
3170
|
+
* | `p=<id>` | Placement ID (stable per cell) |
|
|
3171
|
+
* | `C=1` | Disable cursor movement after placement (crucial for overlay) |
|
|
3172
|
+
* | `c=<cols>` | Cell column extent |
|
|
3173
|
+
* | `r=<rows>` | Cell row extent |
|
|
3174
|
+
* | `z=<zidx>` | Z-index (0 = below text, 1 = above text) |
|
|
3175
|
+
* | `q=2` | Quiet mode (suppress OK/error responses from terminal) |
|
|
3176
|
+
*
|
|
3177
|
+
* Because the image is tiled into a single cell, `c=1,r=1,C=1` keeps the
|
|
3178
|
+
* placement local — it doesn't shift the rendered text layout.
|
|
3179
|
+
*
|
|
3180
|
+
* @see https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
|
3181
|
+
*/
|
|
3182
|
+
/**
|
|
3183
|
+
* Stable image ID for the backdrop scrim overlay. Uploaded once per terminal
|
|
3184
|
+
* session; placements reuse this ID. Value is arbitrary — just needs to be
|
|
3185
|
+
* unique within the process. 0xBEEF picked for grep-ability.
|
|
3186
|
+
*/
|
|
3187
|
+
const BACKDROP_SCRIM_IMAGE_ID = 48879;
|
|
3188
|
+
/**
|
|
3189
|
+
* Placement ID base for backdrop scrim placements. Each cell gets a unique
|
|
3190
|
+
* placement ID derived from `x * OFFSET + y`. This lets us target individual
|
|
3191
|
+
* placements for deletion while leaving others alive. `i=<id>,p=<pid>` refers
|
|
3192
|
+
* to a single placement.
|
|
3193
|
+
*/
|
|
3194
|
+
const BACKDROP_PLACEMENT_X_STRIDE = 1e4;
|
|
3195
|
+
/**
|
|
3196
|
+
* Derive a stable placement ID for a given (x, y) cell. Max column = 9999,
|
|
3197
|
+
* which comfortably exceeds any realistic terminal width.
|
|
3198
|
+
*/
|
|
3199
|
+
function backdropPlacementId(x, y) {
|
|
3200
|
+
return x * BACKDROP_PLACEMENT_X_STRIDE + y + 1;
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Encode a Uint8Array as base64. Kitty expects standard base64 (with `+`/`/`
|
|
3204
|
+
* and `=` padding). We use Buffer when available (Node/Bun), fall back to
|
|
3205
|
+
* btoa for browser/canvas adapters (the canvas target may never actually
|
|
3206
|
+
* need to emit Kitty escapes — this is just defensive).
|
|
3207
|
+
*/
|
|
3208
|
+
function base64Encode(bytes) {
|
|
3209
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
3210
|
+
let binary = "";
|
|
3211
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
3212
|
+
const g = globalThis;
|
|
3213
|
+
if (typeof g.btoa === "function") return g.btoa(binary);
|
|
3214
|
+
throw new Error("base64 encoding unavailable in this environment");
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Build a tiny RGBA pixel grid for the scrim overlay.
|
|
3218
|
+
*
|
|
3219
|
+
* Kitty's graphics protocol paints images at native pixel resolution scaled
|
|
3220
|
+
* to `c` x `r` cells. A 2x2 RGBA image scaled to a single cell (~10x20 px
|
|
3221
|
+
* depending on font) gives us a smooth fill. We intentionally keep the
|
|
3222
|
+
* image tiny to minimize base64 payload size on the upload frame.
|
|
3223
|
+
*
|
|
3224
|
+
* Pixel color: `(r, g, b, a)` where `r/g/b` is the scrim tint and `a` is the
|
|
3225
|
+
* alpha (0-255). For a dark backdrop we use near-black at ~50% alpha, which
|
|
3226
|
+
* darkens the emoji underneath without completely hiding it.
|
|
3227
|
+
*
|
|
3228
|
+
* Width/height = 2 pixels — 16 bytes total, ~24 bytes base64. Upload is ~60
|
|
3229
|
+
* bytes including control chars. One-time cost per modal session.
|
|
3230
|
+
*/
|
|
3231
|
+
function buildScrimPixels(tint, alpha) {
|
|
3232
|
+
const a = Math.max(0, Math.min(255, Math.round(alpha)));
|
|
3233
|
+
const r = Math.max(0, Math.min(255, Math.round(tint.r)));
|
|
3234
|
+
const g = Math.max(0, Math.min(255, Math.round(tint.g)));
|
|
3235
|
+
const b = Math.max(0, Math.min(255, Math.round(tint.b)));
|
|
3236
|
+
const bytes = new Uint8Array(16);
|
|
3237
|
+
for (let i = 0; i < 4; i++) {
|
|
3238
|
+
bytes[i * 4 + 0] = r;
|
|
3239
|
+
bytes[i * 4 + 1] = g;
|
|
3240
|
+
bytes[i * 4 + 2] = b;
|
|
3241
|
+
bytes[i * 4 + 3] = a;
|
|
3242
|
+
}
|
|
3243
|
+
return bytes;
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* APC wrapper: `\x1b_G<control>[;<payload>]\x1b\\`.
|
|
3247
|
+
*
|
|
3248
|
+
* The protocol allows chunking large payloads via `m=1` but our scrim is
|
|
3249
|
+
* tiny — always fits in one chunk.
|
|
3250
|
+
*/
|
|
3251
|
+
function apc(control, payload) {
|
|
3252
|
+
if (payload === void 0 || payload === "") return `\x1b_G${control}\x1b\\`;
|
|
3253
|
+
return `\x1b_G${control};${payload}\x1b\\`;
|
|
3254
|
+
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Emit a one-shot image upload. Terminal stores the RGBA pixels under
|
|
3257
|
+
* `i=<imageId>` and keeps them until explicitly freed. Subsequent placements
|
|
3258
|
+
* reference the image by ID without re-sending pixel data.
|
|
3259
|
+
*
|
|
3260
|
+
* `q=2` suppresses the terminal's OK/error reply — otherwise we'd see stray
|
|
3261
|
+
* APC sequences back on stdin.
|
|
3262
|
+
*/
|
|
3263
|
+
function kittyUploadScrimImage(pixels, width, height, imageId = BACKDROP_SCRIM_IMAGE_ID) {
|
|
3264
|
+
const payload = base64Encode(pixels);
|
|
3265
|
+
return apc(`a=t,f=32,s=${width},v=${height},i=${imageId},q=2`, payload);
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Emit a cell placement. Places `imageId` at the current cursor position
|
|
3269
|
+
* covering `c` cols and `r` rows with z-index `z`. `C=1` prevents the cursor
|
|
3270
|
+
* from advancing after placement (critical — otherwise every placement
|
|
3271
|
+
* shifts the cursor, breaking the caller's positioning).
|
|
3272
|
+
*
|
|
3273
|
+
* Placement ID (`p=<pid>`) is stable per cell so incremental frames can
|
|
3274
|
+
* replace placements without accumulating duplicates.
|
|
3275
|
+
*/
|
|
3276
|
+
function kittyPlaceAt(opts) {
|
|
3277
|
+
const imageId = opts.imageId ?? 48879;
|
|
3278
|
+
const cols = opts.cols ?? 1;
|
|
3279
|
+
const rows = opts.rows ?? 1;
|
|
3280
|
+
const z = opts.z ?? 1;
|
|
3281
|
+
return apc(`a=p,i=${imageId},p=${opts.placementId},c=${cols},r=${rows},z=${z},C=1,q=2`);
|
|
3282
|
+
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Delete ALL placements of our scrim image without freeing the image.
|
|
3285
|
+
* Used when the modal closes — we leave the image cached in case another
|
|
3286
|
+
* modal opens, but remove every overlay cell at once.
|
|
3287
|
+
*/
|
|
3288
|
+
function kittyDeleteAllScrimPlacements(imageId = BACKDROP_SCRIM_IMAGE_ID) {
|
|
3289
|
+
return apc(`a=d,d=i,i=${imageId},q=2`);
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* Absolute cursor position (CUP). 1-based row/col per VT100.
|
|
3293
|
+
*
|
|
3294
|
+
* Used to position the cursor before emitting a placement so the placement
|
|
3295
|
+
* lands in the right cell. Kept small and local — the rest of the pipeline
|
|
3296
|
+
* uses more elaborate cursor tracking, but for out-of-band overlay emission
|
|
3297
|
+
* we just want a deterministic "jump here, place, done."
|
|
3298
|
+
*/
|
|
3299
|
+
function cupTo(col, row) {
|
|
3300
|
+
return `\x1b[${row + 1};${col + 1}H`;
|
|
3301
|
+
}
|
|
3302
|
+
//#endregion
|
|
3303
|
+
//#region packages/ansi/src/style/colors.ts
|
|
3304
|
+
/** Default ANSI codes for theme tokens when no theme object is given. */
|
|
3305
|
+
const THEME_TOKEN_DEFAULTS = {
|
|
3306
|
+
primary: 33,
|
|
3307
|
+
secondary: 36,
|
|
3308
|
+
accent: 35,
|
|
3309
|
+
error: 31,
|
|
3310
|
+
warning: 33,
|
|
3311
|
+
success: 32,
|
|
3312
|
+
info: 36,
|
|
3313
|
+
muted: 2,
|
|
3314
|
+
link: 34,
|
|
3315
|
+
border: 90,
|
|
3316
|
+
surface: 37
|
|
3317
|
+
};
|
|
3318
|
+
//#endregion
|
|
3319
|
+
//#region packages/ansi/src/style/style.ts
|
|
3320
|
+
/**
|
|
3321
|
+
* createStyle() — theme-aware chalk replacement.
|
|
3322
|
+
*
|
|
3323
|
+
* Returns a chainable Proxy-based style object. Access properties to
|
|
3324
|
+
* accumulate styles, call with a string to apply them.
|
|
3325
|
+
*
|
|
3326
|
+
* @example
|
|
3327
|
+
* ```ts
|
|
3328
|
+
* const s = createStyle()
|
|
3329
|
+
* s.bold.red("error") // "\x1b[1;31merror\x1b[22;39m"
|
|
3330
|
+
* s.hex("#ff0000")("text") // truecolor foreground
|
|
3331
|
+
*
|
|
3332
|
+
* const s = createStyle({ theme })
|
|
3333
|
+
* s.primary("deploy") // resolves $primary from theme
|
|
3334
|
+
* ```
|
|
3335
|
+
*/
|
|
3336
|
+
/**
|
|
3337
|
+
* Resolve a color value against a theme — the canonical token resolver.
|
|
3338
|
+
*
|
|
3339
|
+
* If the color starts with `$`, looks up the token in the theme.
|
|
3340
|
+
* Supports `$primary`, `$surface-bg` (hyphens stripped), `$color0`–`$color15` (palette).
|
|
3341
|
+
* Non-`$` strings pass through unchanged. Returns undefined if no theme or unknown token.
|
|
3342
|
+
*
|
|
3343
|
+
* Compatible with @silvery/theme's Theme type (or any object with string properties).
|
|
3344
|
+
*/
|
|
3345
|
+
function resolveThemeColor(name, theme) {
|
|
3346
|
+
if (!name) return void 0;
|
|
3347
|
+
if (!name.startsWith("$")) return name;
|
|
3348
|
+
if (!theme) return void 0;
|
|
3349
|
+
return resolveToken(name, theme);
|
|
3350
|
+
}
|
|
3351
|
+
/** Internal: resolve a token name (with or without $ prefix) against a theme.
|
|
3352
|
+
*
|
|
3353
|
+
* Resolution order:
|
|
3354
|
+
* 1. Direct key lookup — finds Sterling flat keys (`bg-accent`,
|
|
3355
|
+
* `fg-on-error`, `border-focus`, …) and legacy kebab keys
|
|
3356
|
+
* (`primary-hover`, `fg-hover`, `bg-surface-hover`) and plain names
|
|
3357
|
+
* (`bg`, `primary`, `muted`).
|
|
3358
|
+
* 2. No-hyphen fallback — `$surface-bg` → `theme.surfacebg`,
|
|
3359
|
+
* `$focus-border` → `theme.focusborder`.
|
|
3360
|
+
*
|
|
3361
|
+
* The old `LEGACY_ALIASES` table (e.g. `fgmuted` → `muted`, `bgsurface` →
|
|
3362
|
+
* `surfacebg`) was removed in 0.18.1 once every shipped default Theme ships
|
|
3363
|
+
* with Sterling flat tokens baked in — `theme["fg-muted"]` and
|
|
3364
|
+
* `theme["bg-surface-subtle"]` are direct fields now, so no alias fallback
|
|
3365
|
+
* is required for canonical Sterling tokens. Tokens that existed only as
|
|
3366
|
+
* aliases (e.g. `$bg-surface`, `$fg-on-primary`, `$border-input`,
|
|
3367
|
+
* `$fg-disabled`) no longer resolve — callers should use the canonical
|
|
3368
|
+
* Sterling equivalents (`$bg-surface-default`, `$fg-on-accent`,
|
|
3369
|
+
* `$border-default`, `$fg-muted`).
|
|
3370
|
+
*/
|
|
3371
|
+
function resolveToken(name, theme) {
|
|
3372
|
+
if (!theme) return void 0;
|
|
3373
|
+
const token = name.startsWith("$") ? name.slice(1) : name;
|
|
3374
|
+
if (token.startsWith("color")) {
|
|
3375
|
+
const idx = parseInt(token.slice(5), 10);
|
|
3376
|
+
if (idx >= 0 && idx < 16 && theme.palette && idx < theme.palette.length) return theme.palette[idx];
|
|
3377
|
+
}
|
|
3378
|
+
const themeObj = theme;
|
|
3379
|
+
const direct = themeObj[token];
|
|
3380
|
+
if (typeof direct === "string") return direct;
|
|
3381
|
+
const noHyphen = token.replace(/-/g, "");
|
|
3382
|
+
if (noHyphen !== token) {
|
|
3383
|
+
const stripped = themeObj[noHyphen];
|
|
3384
|
+
if (typeof stripped === "string") return stripped;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
const ESC = "\x1B[";
|
|
3388
|
+
const KNOWN_METHODS = new Set([
|
|
3389
|
+
"hex",
|
|
3390
|
+
"rgb",
|
|
3391
|
+
"bgHex",
|
|
3392
|
+
"bgRgb",
|
|
3393
|
+
"ansi256",
|
|
3394
|
+
"bgAnsi256",
|
|
3395
|
+
"resolve",
|
|
3396
|
+
"curlyUnderline",
|
|
3397
|
+
"dottedUnderline",
|
|
3398
|
+
"dashedUnderline",
|
|
3399
|
+
"doubleUnderline",
|
|
3400
|
+
"underlineColor",
|
|
3401
|
+
"styledUnderline"
|
|
3402
|
+
]);
|
|
3403
|
+
const THEME_TOKENS = new Set([
|
|
3404
|
+
"primary",
|
|
3405
|
+
"secondary",
|
|
3406
|
+
"accent",
|
|
3407
|
+
"error",
|
|
3408
|
+
"warning",
|
|
3409
|
+
"success",
|
|
3410
|
+
"info",
|
|
3411
|
+
"muted",
|
|
3412
|
+
"link",
|
|
3413
|
+
"border",
|
|
3414
|
+
"surface"
|
|
3415
|
+
]);
|
|
3416
|
+
/** Convert chalk numeric level (0-3) to {@link ColorLevel}. */
|
|
3417
|
+
function fromChalkLevel(n) {
|
|
3418
|
+
if (n <= 0) return "mono";
|
|
3419
|
+
if (n === 1) return "ansi16";
|
|
3420
|
+
if (n === 2) return "256";
|
|
3421
|
+
return "truecolor";
|
|
3422
|
+
}
|
|
3423
|
+
/** Convert {@link ColorLevel} to chalk numeric level (0-3). */
|
|
3424
|
+
function toChalkLevel(cl) {
|
|
3425
|
+
if (cl === "mono") return 0;
|
|
3426
|
+
if (cl === "ansi16") return 1;
|
|
3427
|
+
if (cl === "256") return 2;
|
|
3428
|
+
return 3;
|
|
3429
|
+
}
|
|
3430
|
+
/**
|
|
3431
|
+
* Create a style object for terminal output.
|
|
3432
|
+
*
|
|
3433
|
+
* @param options - Color level and optional theme
|
|
3434
|
+
* @returns A chainable style object (chalk-compatible API)
|
|
3435
|
+
*
|
|
3436
|
+
* @example
|
|
3437
|
+
* ```ts
|
|
3438
|
+
* import { createStyle } from "@silvery/ansi"
|
|
3439
|
+
*
|
|
3440
|
+
* const s = createStyle()
|
|
3441
|
+
* console.log(s.bold.red("Error!"))
|
|
3442
|
+
* console.log(s.hex("#818cf8")("Indigo"))
|
|
3443
|
+
*
|
|
3444
|
+
* // With theme
|
|
3445
|
+
* const s = createStyle({ theme })
|
|
3446
|
+
* console.log(s.primary("Deploy"))
|
|
3447
|
+
* console.log(s.success("Done"))
|
|
3448
|
+
* ```
|
|
3449
|
+
*/
|
|
3450
|
+
function createStyle(options) {
|
|
3451
|
+
const ref = {
|
|
3452
|
+
level: "mono",
|
|
3453
|
+
theme: options?.theme,
|
|
3454
|
+
caps: {
|
|
3455
|
+
underlineStyles: false,
|
|
3456
|
+
underlineColor: false
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
if (options?.level !== void 0 && options.level !== null) {
|
|
3460
|
+
ref.level = options.level;
|
|
3461
|
+
ref.caps = options.caps ?? ref.caps;
|
|
3462
|
+
} else if (options?.level === null) {
|
|
3463
|
+
ref.level = "mono";
|
|
3464
|
+
ref.caps = options.caps ?? ref.caps;
|
|
3465
|
+
} else try {
|
|
3466
|
+
const profile = createTerminalProfile({ stdout: process.stdout });
|
|
3467
|
+
ref.level = profile.colorLevel;
|
|
3468
|
+
ref.caps = options?.caps ?? {
|
|
3469
|
+
underlineStyles: profile.caps.underlineStyles.length > 0,
|
|
3470
|
+
underlineColor: profile.caps.underlineColor
|
|
3471
|
+
};
|
|
3472
|
+
} catch {
|
|
3473
|
+
ref.level = "mono";
|
|
3474
|
+
ref.caps = options?.caps ?? ref.caps;
|
|
3475
|
+
}
|
|
3476
|
+
return createChainWithRef({
|
|
3477
|
+
opens: [],
|
|
3478
|
+
closes: []
|
|
3479
|
+
}, ref);
|
|
3480
|
+
}
|
|
3481
|
+
createStyle();
|
|
3482
|
+
/**
|
|
3483
|
+
* Create a chain that reads level from a mutable ref.
|
|
3484
|
+
* This allows `style.level = 3` to affect all subsequent calls.
|
|
3485
|
+
*/
|
|
3486
|
+
function createChainWithRef(state, ref) {
|
|
3487
|
+
const proxyRef = { proxy: null };
|
|
3488
|
+
const handler = {
|
|
3489
|
+
apply(_target, _thisArg, args) {
|
|
3490
|
+
const level = ref.level;
|
|
3491
|
+
if (state.visible && level === "mono") return "";
|
|
3492
|
+
let text;
|
|
3493
|
+
if (args.length === 0) text = "";
|
|
3494
|
+
else if (Array.isArray(args[0]) && "raw" in args[0]) text = String.raw(args[0], ...args.slice(1));
|
|
3495
|
+
else if (args.length > 1) text = args.map((a) => String(a ?? "")).join(" ");
|
|
3496
|
+
else text = String(args[0] ?? "");
|
|
3497
|
+
if (text === "") return "";
|
|
3498
|
+
if (level === "mono" || state.opens.length === 0) return text;
|
|
3499
|
+
const open = `${ESC}${state.opens.join(";")}m`;
|
|
3500
|
+
const close = `${ESC}${state.closes.join(";")}m`;
|
|
3501
|
+
for (const closeCode of state.closes) {
|
|
3502
|
+
const closeSeq = `${ESC}${closeCode}m`;
|
|
3503
|
+
const parts = text.split(closeSeq);
|
|
3504
|
+
if (parts.length > 1) text = parts.join(`${closeSeq}${open}`);
|
|
3505
|
+
}
|
|
3506
|
+
if (text.includes("\n")) text = text.replace(/\r?\n/g, `${close}$&${open}`);
|
|
3507
|
+
return `${open}${text}${close}`;
|
|
3508
|
+
},
|
|
3509
|
+
get(_target, prop) {
|
|
3510
|
+
if (typeof prop === "symbol") return void 0;
|
|
3511
|
+
if (prop === "level") return toChalkLevel(ref.level);
|
|
3512
|
+
if (prop === "resolve") return (token) => resolveToken(token, ref.theme);
|
|
3513
|
+
if (prop === "visible") return createChainWithRef({
|
|
3514
|
+
...state,
|
|
3515
|
+
visible: true
|
|
3516
|
+
}, ref);
|
|
3517
|
+
if (prop === "call" || prop === "apply" || prop === "bind") return Function.prototype[prop].bind(proxyRef.proxy);
|
|
3518
|
+
const level = ref.level;
|
|
3519
|
+
if (prop === "hex" || prop === "bgHex") return (color) => {
|
|
3520
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3521
|
+
const rgb = hexToRgb$1(color);
|
|
3522
|
+
if (!rgb) return createChainWithRef(state, ref);
|
|
3523
|
+
const code = prop === "hex" ? fgFromRgb(rgb[0], rgb[1], rgb[2], level) : bgFromRgb(rgb[0], rgb[1], rgb[2], level);
|
|
3524
|
+
const close = prop === "hex" ? "39" : "49";
|
|
3525
|
+
return createChainWithRef({
|
|
3526
|
+
opens: [...state.opens, code],
|
|
3527
|
+
closes: [...state.closes, close]
|
|
3528
|
+
}, ref);
|
|
3529
|
+
};
|
|
3530
|
+
if (prop === "rgb" || prop === "bgRgb") return (r, g, b) => {
|
|
3531
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3532
|
+
const code = prop === "rgb" ? fgFromRgb(r, g, b, level) : bgFromRgb(r, g, b, level);
|
|
3533
|
+
const close = prop === "rgb" ? "39" : "49";
|
|
3534
|
+
return createChainWithRef({
|
|
3535
|
+
opens: [...state.opens, code],
|
|
3536
|
+
closes: [...state.closes, close]
|
|
3537
|
+
}, ref);
|
|
3538
|
+
};
|
|
3539
|
+
if (prop === "ansi256") return (code) => {
|
|
3540
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3541
|
+
return createChainWithRef({
|
|
3542
|
+
opens: [...state.opens, `38;5;${code}`],
|
|
3543
|
+
closes: [...state.closes, "39"]
|
|
3544
|
+
}, ref);
|
|
3545
|
+
};
|
|
3546
|
+
if (prop === "bgAnsi256") return (code) => {
|
|
3547
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3548
|
+
return createChainWithRef({
|
|
3549
|
+
opens: [...state.opens, `48;5;${code}`],
|
|
3550
|
+
closes: [...state.closes, "49"]
|
|
3551
|
+
}, ref);
|
|
3552
|
+
};
|
|
3553
|
+
if (prop === "curlyUnderline" || prop === "dottedUnderline" || prop === "dashedUnderline" || prop === "doubleUnderline") {
|
|
3554
|
+
const styleName = prop.slice(0, prop.length - 9);
|
|
3555
|
+
return (text) => applyExtendedUnderline(text, styleName, ref);
|
|
3556
|
+
}
|
|
3557
|
+
if (prop === "underlineColor") return (r, g, b, text) => applyUnderlineColor(text, r, g, b, ref);
|
|
3558
|
+
if (prop === "styledUnderline") return (styleName, rgb, text) => applyStyledUnderline(text, styleName, rgb, ref);
|
|
3559
|
+
if (prop in MODIFIERS) {
|
|
3560
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3561
|
+
const [open, close] = MODIFIERS[prop];
|
|
3562
|
+
return createChainWithRef({
|
|
3563
|
+
opens: [...state.opens, String(open)],
|
|
3564
|
+
closes: [...state.closes, String(close)]
|
|
3565
|
+
}, ref);
|
|
3566
|
+
}
|
|
3567
|
+
if (prop in FG_COLORS) {
|
|
3568
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3569
|
+
return createChainWithRef({
|
|
3570
|
+
opens: [...state.opens, String(FG_COLORS[prop])],
|
|
3571
|
+
closes: [...state.closes, "39"]
|
|
3572
|
+
}, ref);
|
|
3573
|
+
}
|
|
3574
|
+
if (prop in BG_COLORS) {
|
|
3575
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3576
|
+
return createChainWithRef({
|
|
3577
|
+
opens: [...state.opens, String(BG_COLORS[prop])],
|
|
3578
|
+
closes: [...state.closes, "49"]
|
|
3579
|
+
}, ref);
|
|
3580
|
+
}
|
|
3581
|
+
if (THEME_TOKENS.has(prop)) {
|
|
3582
|
+
if (level === "mono") return createChainWithRef(state, ref);
|
|
3583
|
+
const hex = resolveToken(prop, ref.theme);
|
|
3584
|
+
if (hex) {
|
|
3585
|
+
const rgb = hexToRgb$1(hex);
|
|
3586
|
+
if (rgb) {
|
|
3587
|
+
const code = fgFromRgb(rgb[0], rgb[1], rgb[2], level);
|
|
3588
|
+
if (prop === "link") return createChainWithRef({
|
|
3589
|
+
opens: [
|
|
3590
|
+
...state.opens,
|
|
3591
|
+
code,
|
|
3592
|
+
"4"
|
|
3593
|
+
],
|
|
3594
|
+
closes: [
|
|
3595
|
+
...state.closes,
|
|
3596
|
+
"39",
|
|
3597
|
+
"24"
|
|
3598
|
+
]
|
|
3599
|
+
}, ref);
|
|
3600
|
+
return createChainWithRef({
|
|
3601
|
+
opens: [...state.opens, code],
|
|
3602
|
+
closes: [...state.closes, "39"]
|
|
3603
|
+
}, ref);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
const fallback = THEME_TOKEN_DEFAULTS[prop];
|
|
3607
|
+
if (fallback !== void 0) {
|
|
3608
|
+
if (prop === "muted") return createChainWithRef({
|
|
3609
|
+
opens: [...state.opens, String(fallback)],
|
|
3610
|
+
closes: [...state.closes, "22"]
|
|
3611
|
+
}, ref);
|
|
3612
|
+
if (prop === "link") return createChainWithRef({
|
|
3613
|
+
opens: [
|
|
3614
|
+
...state.opens,
|
|
3615
|
+
String(fallback),
|
|
3616
|
+
"4"
|
|
3617
|
+
],
|
|
3618
|
+
closes: [
|
|
3619
|
+
...state.closes,
|
|
3620
|
+
"39",
|
|
3621
|
+
"24"
|
|
3622
|
+
]
|
|
3623
|
+
}, ref);
|
|
3624
|
+
return createChainWithRef({
|
|
3625
|
+
opens: [...state.opens, String(fallback)],
|
|
3626
|
+
closes: [...state.closes, "39"]
|
|
3627
|
+
}, ref);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
},
|
|
3631
|
+
set(_target, prop, value) {
|
|
3632
|
+
if (prop === "level") {
|
|
3633
|
+
ref.level = fromChalkLevel(value);
|
|
3634
|
+
return true;
|
|
3635
|
+
}
|
|
3636
|
+
return false;
|
|
3637
|
+
},
|
|
3638
|
+
has(_target, prop) {
|
|
3639
|
+
if (prop === "level") return true;
|
|
3640
|
+
if (typeof prop === "symbol") return false;
|
|
3641
|
+
return prop in MODIFIERS || prop in FG_COLORS || prop in BG_COLORS || THEME_TOKENS.has(prop) || KNOWN_METHODS.has(prop);
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
const target = function() {};
|
|
3645
|
+
const proxy = new Proxy(target, handler);
|
|
3646
|
+
proxyRef.proxy = proxy;
|
|
3647
|
+
return proxy;
|
|
3648
|
+
}
|
|
3649
|
+
const UNDERLINE_OPEN = "\x1B[4m";
|
|
3650
|
+
const UNDERLINE_CLOSE = "\x1B[24m";
|
|
3651
|
+
/** `style.curlyUnderline(text)` / `.dotted` / `.dashed` / `.double`. */
|
|
3652
|
+
function applyExtendedUnderline(text, name, ref) {
|
|
3653
|
+
if (ref.level === "mono") return text;
|
|
3654
|
+
if (!ref.caps.underlineStyles) return `${UNDERLINE_OPEN}${text}${UNDERLINE_CLOSE}`;
|
|
3655
|
+
return `${UNDERLINE_CODES[name]}${text}${UNDERLINE_CODES.reset}`;
|
|
3656
|
+
}
|
|
3657
|
+
/** `style.underlineColor(r, g, b, text)`. */
|
|
3658
|
+
function applyUnderlineColor(text, r, g, b, ref) {
|
|
3659
|
+
if (ref.level === "mono") return text;
|
|
3660
|
+
if (!ref.caps.underlineColor) return `${UNDERLINE_OPEN}${text}${UNDERLINE_CLOSE}`;
|
|
3661
|
+
return `${UNDERLINE_STANDARD}${buildUnderlineColorCode(r, g, b)}${text}${UNDERLINE_COLOR_RESET}${UNDERLINE_RESET_STANDARD}`;
|
|
3662
|
+
}
|
|
3663
|
+
/** `style.styledUnderline(name, [r,g,b], text)`. */
|
|
3664
|
+
function applyStyledUnderline(text, name, rgb, ref) {
|
|
3665
|
+
if (ref.level === "mono") return text;
|
|
3666
|
+
if (!ref.caps.underlineStyles) return `${UNDERLINE_OPEN}${text}${UNDERLINE_CLOSE}`;
|
|
3667
|
+
const [r, g, b] = rgb;
|
|
3668
|
+
const styleCode = UNDERLINE_CODES[name];
|
|
3669
|
+
if (!ref.caps.underlineColor) return `${styleCode}${text}${UNDERLINE_CODES.reset}`;
|
|
3670
|
+
return `${styleCode}${buildUnderlineColorCode(r, g, b)}${text}${UNDERLINE_CODES.reset}${UNDERLINE_COLOR_RESET}`;
|
|
3671
|
+
}
|
|
3672
|
+
//#endregion
|
|
3673
|
+
//#region packages/ansi/src/style/mixed-proxy.ts
|
|
3674
|
+
/** Methods on Style that take arguments and return a new Style chain. */
|
|
3675
|
+
const STYLE_METHODS = new Set([
|
|
3676
|
+
"hex",
|
|
3677
|
+
"bgHex",
|
|
3678
|
+
"rgb",
|
|
3679
|
+
"bgRgb",
|
|
3680
|
+
"ansi256",
|
|
3681
|
+
"bgAnsi256"
|
|
3682
|
+
]);
|
|
3683
|
+
/**
|
|
3684
|
+
* Create a proxy that wraps a style instance with additional properties.
|
|
3685
|
+
*
|
|
3686
|
+
* The proxy makes the result:
|
|
3687
|
+
* - Callable: result('text') applies current styles
|
|
3688
|
+
* - Chainable: result.bold.red('text') chains styles
|
|
3689
|
+
* - Extended: result.anyExtraProp accesses extra properties
|
|
3690
|
+
*
|
|
3691
|
+
* Extra properties take priority over style properties on name collision.
|
|
3692
|
+
*/
|
|
3693
|
+
function createMixedStyle(style, extra) {
|
|
3694
|
+
return createChainProxy(style, extra);
|
|
3695
|
+
}
|
|
3696
|
+
/**
|
|
3697
|
+
* Internal recursive proxy builder for style chain + extra properties.
|
|
3698
|
+
*/
|
|
3699
|
+
function createChainProxy(currentStyle, extra) {
|
|
3700
|
+
const handler = {
|
|
3701
|
+
apply(_target, _thisArg, args) {
|
|
3702
|
+
return currentStyle(...args);
|
|
3703
|
+
},
|
|
3704
|
+
get(_target, prop) {
|
|
3705
|
+
if (prop in extra) {
|
|
3706
|
+
const value = extra[prop];
|
|
3707
|
+
if (typeof value === "function") return value;
|
|
3708
|
+
return value;
|
|
3709
|
+
}
|
|
3710
|
+
if (typeof prop === "symbol") return extra[prop];
|
|
3711
|
+
if (STYLE_METHODS.has(prop)) {
|
|
3712
|
+
const method = currentStyle[prop];
|
|
3713
|
+
if (typeof method === "function") return (...args) => {
|
|
3714
|
+
return createChainProxy(method.apply(currentStyle, args), extra);
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
const styleProp = currentStyle[prop];
|
|
3718
|
+
if (styleProp !== void 0) {
|
|
3719
|
+
if (typeof styleProp === "function" || typeof styleProp === "object") return createChainProxy(styleProp, extra);
|
|
3720
|
+
return styleProp;
|
|
3721
|
+
}
|
|
3722
|
+
},
|
|
3723
|
+
set(_target, prop, value) {
|
|
3724
|
+
extra[prop] = value;
|
|
3725
|
+
return true;
|
|
3726
|
+
},
|
|
3727
|
+
defineProperty(_target, prop, descriptor) {
|
|
3728
|
+
Object.defineProperty(extra, prop, descriptor);
|
|
3729
|
+
return true;
|
|
3730
|
+
},
|
|
3731
|
+
has(_target, prop) {
|
|
3732
|
+
if (prop in extra) return true;
|
|
3733
|
+
if (typeof prop === "string" && prop in currentStyle) return true;
|
|
3734
|
+
return false;
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
3737
|
+
const proxyTarget = function() {};
|
|
3738
|
+
return new Proxy(proxyTarget, handler);
|
|
3739
|
+
}
|
|
3740
|
+
//#endregion
|
|
3741
|
+
//#region packages/ansi/src/theme/monochrome.ts
|
|
3742
|
+
/**
|
|
3743
|
+
* Default monochrome attrs — Polaris-aligned mapping from the design spec.
|
|
3744
|
+
*
|
|
3745
|
+
* The philosophy: every *semantic* token that would normally carry color gets an
|
|
3746
|
+
* attrs set that preserves its hierarchy rank and state semantics. Example:
|
|
3747
|
+
* error (danger) is bold+inverse so it *grabs* attention even without red;
|
|
3748
|
+
* warning is bold to stand out but not as aggressively; info is italic to
|
|
3749
|
+
* indicate auxiliary information.
|
|
3750
|
+
*
|
|
3751
|
+
* Structural surfaces (bg/mutedbg/surfacebg/popoverbg) have no attrs — they
|
|
3752
|
+
* represent background planes that monochrome terminals can't vary anyway.
|
|
3753
|
+
*/
|
|
3754
|
+
const DEFAULT_MONO_ATTRS = {
|
|
3755
|
+
bg: [],
|
|
3756
|
+
mutedbg: [],
|
|
3757
|
+
surfacebg: [],
|
|
3758
|
+
popoverbg: [],
|
|
3759
|
+
border: [],
|
|
3760
|
+
cursorbg: [],
|
|
3761
|
+
fg: [],
|
|
3762
|
+
muted: ["dim"],
|
|
3763
|
+
disabledfg: ["dim"],
|
|
3764
|
+
surface: [],
|
|
3765
|
+
popover: [],
|
|
3766
|
+
primary: ["bold"],
|
|
3767
|
+
secondary: ["bold"],
|
|
3768
|
+
accent: ["italic", "bold"],
|
|
3769
|
+
error: ["bold", "inverse"],
|
|
3770
|
+
warning: ["bold"],
|
|
3771
|
+
success: ["bold"],
|
|
3772
|
+
info: ["italic"],
|
|
3773
|
+
primaryfg: [],
|
|
3774
|
+
secondaryfg: [],
|
|
3775
|
+
accentfg: [],
|
|
3776
|
+
errorfg: ["inverse"],
|
|
3777
|
+
warningfg: [],
|
|
3778
|
+
successfg: [],
|
|
3779
|
+
infofg: [],
|
|
3780
|
+
focusborder: ["bold"],
|
|
3781
|
+
inputborder: [],
|
|
3782
|
+
cursor: [],
|
|
3783
|
+
"fg-muted": ["dim"],
|
|
3784
|
+
"bg-muted": [],
|
|
3785
|
+
"fg-accent": ["italic", "bold"],
|
|
3786
|
+
"bg-accent": [],
|
|
3787
|
+
"fg-on-accent": [],
|
|
3788
|
+
"border-accent": [],
|
|
3789
|
+
"fg-accent-hover": ["italic", "bold"],
|
|
3790
|
+
"bg-accent-hover": [],
|
|
3791
|
+
"fg-accent-active": ["italic", "bold"],
|
|
3792
|
+
"bg-accent-active": [],
|
|
3793
|
+
"fg-info": ["italic"],
|
|
3794
|
+
"bg-info": [],
|
|
3795
|
+
"fg-on-info": [],
|
|
3796
|
+
"bg-info-hover": [],
|
|
3797
|
+
"bg-info-active": [],
|
|
3798
|
+
"fg-success": ["bold"],
|
|
3799
|
+
"bg-success": [],
|
|
3800
|
+
"fg-on-success": [],
|
|
3801
|
+
"bg-success-hover": [],
|
|
3802
|
+
"bg-success-active": [],
|
|
3803
|
+
"fg-warning": ["bold"],
|
|
3804
|
+
"bg-warning": [],
|
|
3805
|
+
"fg-on-warning": [],
|
|
3806
|
+
"bg-warning-hover": [],
|
|
3807
|
+
"bg-warning-active": [],
|
|
3808
|
+
"fg-error": ["bold", "inverse"],
|
|
3809
|
+
"bg-error": [],
|
|
3810
|
+
"fg-on-error": ["inverse"],
|
|
3811
|
+
"bg-error-hover": [],
|
|
3812
|
+
"bg-error-active": [],
|
|
3813
|
+
"bg-surface-default": [],
|
|
3814
|
+
"bg-surface-subtle": [],
|
|
3815
|
+
"bg-surface-raised": [],
|
|
3816
|
+
"bg-surface-overlay": [],
|
|
3817
|
+
"bg-surface-hover": [],
|
|
3818
|
+
"border-default": [],
|
|
3819
|
+
"border-focus": ["bold"],
|
|
3820
|
+
"border-muted": [],
|
|
3821
|
+
"fg-cursor": [],
|
|
3822
|
+
"bg-cursor": [],
|
|
3823
|
+
"bg-selected": ["inverse"],
|
|
3824
|
+
"fg-on-selected": [],
|
|
3825
|
+
"bg-selected-hover": ["inverse"],
|
|
3826
|
+
"bg-inverse": ["inverse"],
|
|
3827
|
+
"fg-on-inverse": [],
|
|
3828
|
+
"fg-link": ["underline"]
|
|
3829
|
+
};
|
|
3830
|
+
/**
|
|
3831
|
+
* Produce per-token monochrome attrs from a base Theme.
|
|
3832
|
+
*
|
|
3833
|
+
* Currently returns `DEFAULT_MONO_ATTRS` — a canonical mapping. Passed the
|
|
3834
|
+
* theme to allow per-theme overrides in the future (e.g., a palette that
|
|
3835
|
+
* prefers `underline` for accents over `italic`). The argument is reserved.
|
|
3836
|
+
*/
|
|
3837
|
+
function deriveMonochromeTheme(theme) {
|
|
3838
|
+
return DEFAULT_MONO_ATTRS;
|
|
3839
|
+
}
|
|
3840
|
+
/**
|
|
3841
|
+
* Resolve mono-attrs from a color *string* — the high-level entry point
|
|
3842
|
+
* consumed by the render pipeline.
|
|
3843
|
+
*
|
|
3844
|
+
* Accepts strings like `"$primary"`, `"$fg-muted"`, `"$border-focus"`. Strips
|
|
3845
|
+
* the `$` prefix and looks the name up directly against `DEFAULT_MONO_ATTRS`,
|
|
3846
|
+
* which carries both legacy keys (`muted`, `surfacebg`, `focusborder`, …) AND
|
|
3847
|
+
* Sterling flat tokens (`fg-muted`, `bg-surface-default`, `border-focus`, …)
|
|
3848
|
+
* as first-class entries. Returns `undefined` for non-token strings (hex,
|
|
3849
|
+
* rgb(), named ANSI colors) — callers should treat this as "no attrs".
|
|
3850
|
+
*
|
|
3851
|
+
* A secondary no-hyphen fallback (`$surface-bg` → `surfacebg`) keeps the
|
|
3852
|
+
* legacy hyphenated-compound form working for callers that still emit that
|
|
3853
|
+
* shape.
|
|
3854
|
+
*
|
|
3855
|
+
* @param color The color string (e.g. `"$primary"`, `"#ff0000"`, `"red"`)
|
|
3856
|
+
* @param theme Active theme (reserved for per-theme overrides)
|
|
3857
|
+
* @returns Array of mono-attrs for the token, or `undefined` if not a
|
|
3858
|
+
* recognized token.
|
|
3859
|
+
*/
|
|
3860
|
+
function monoAttrsForColorString(color, theme) {
|
|
3861
|
+
if (!color.startsWith("$")) return void 0;
|
|
3862
|
+
const name = color.slice(1);
|
|
3863
|
+
const attrs = deriveMonochromeTheme(theme);
|
|
3864
|
+
const direct = attrs[name];
|
|
3865
|
+
if (direct !== void 0) return direct;
|
|
3866
|
+
const noHyphen = name.replace(/-/g, "");
|
|
3867
|
+
if (noHyphen !== name) {
|
|
3868
|
+
const stripped = attrs[noHyphen];
|
|
3869
|
+
if (stripped !== void 0) return stripped;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
//#endregion
|
|
3873
|
+
//#region packages/ansi/src/theme/fingerprint.ts
|
|
3874
|
+
/** Fields that are always probed and used for fingerprinting. */
|
|
3875
|
+
const FINGERPRINT_FIELDS = [
|
|
3876
|
+
"foreground",
|
|
3877
|
+
"background",
|
|
3878
|
+
"black",
|
|
3879
|
+
"red",
|
|
3880
|
+
"green",
|
|
3881
|
+
"yellow",
|
|
3882
|
+
"blue",
|
|
3883
|
+
"magenta",
|
|
3884
|
+
"cyan",
|
|
3885
|
+
"white",
|
|
3886
|
+
"brightBlack",
|
|
3887
|
+
"brightRed",
|
|
3888
|
+
"brightGreen",
|
|
3889
|
+
"brightYellow",
|
|
3890
|
+
"brightBlue",
|
|
3891
|
+
"brightMagenta",
|
|
3892
|
+
"brightCyan",
|
|
3893
|
+
"brightWhite"
|
|
3894
|
+
];
|
|
3895
|
+
/**
|
|
3896
|
+
* Map ΔE thresholds into a 0–1 confidence.
|
|
3897
|
+
*
|
|
3898
|
+
* sumΔE=0 + maxΔE=0 → 1.0 (perfect match)
|
|
3899
|
+
* sumΔE=30 (threshold) → ~0.5
|
|
3900
|
+
* sumΔE≥60 → ~0.0
|
|
3901
|
+
*/
|
|
3902
|
+
function computeConfidence(sumDE, maxDE, sumThreshold) {
|
|
3903
|
+
const sumScore = Math.max(0, 1 - sumDE / (sumThreshold * 2));
|
|
3904
|
+
const maxScore = Math.max(0, 1 - maxDE / 16);
|
|
3905
|
+
return Math.max(0, Math.min(1, .7 * sumScore + .3 * maxScore));
|
|
3906
|
+
}
|
|
3907
|
+
/**
|
|
3908
|
+
* Match probed slots against a catalog, returning the best candidate if it
|
|
3909
|
+
* satisfies both sum and per-slot thresholds. Returns `null` if nothing matches.
|
|
3910
|
+
*
|
|
3911
|
+
* `probed` is a partial ColorScheme — whatever slots OSC queries returned. Missing
|
|
3912
|
+
* slots are skipped (still counted as "not compared"). Non-hex values are
|
|
3913
|
+
* ignored (ΔE can't be computed).
|
|
3914
|
+
*/
|
|
3915
|
+
function fingerprintMatch(probed, catalog, opts = {}) {
|
|
3916
|
+
const sumThreshold = opts.sumThreshold ?? 30;
|
|
3917
|
+
const perSlotThreshold = opts.perSlotThreshold ?? 8;
|
|
3918
|
+
let best = null;
|
|
3919
|
+
for (const scheme of catalog) {
|
|
3920
|
+
let sumDE = 0;
|
|
3921
|
+
let maxDE = 0;
|
|
3922
|
+
let slotsCompared = 0;
|
|
3923
|
+
for (const field of FINGERPRINT_FIELDS) {
|
|
3924
|
+
const probedVal = probed[field];
|
|
3925
|
+
const catalogVal = scheme[field];
|
|
3926
|
+
if (typeof probedVal !== "string" || typeof catalogVal !== "string") continue;
|
|
3927
|
+
const de = colorDistance(probedVal, catalogVal);
|
|
3928
|
+
if (de === null) continue;
|
|
3929
|
+
const scaled = de * 100;
|
|
3930
|
+
sumDE += scaled;
|
|
3931
|
+
if (scaled > maxDE) maxDE = scaled;
|
|
3932
|
+
slotsCompared++;
|
|
3933
|
+
}
|
|
3934
|
+
if (slotsCompared === 0) continue;
|
|
3935
|
+
if (maxDE > perSlotThreshold) continue;
|
|
3936
|
+
if (sumDE > sumThreshold) continue;
|
|
3937
|
+
if (best === null || sumDE < best.sumDeltaE) best = {
|
|
3938
|
+
scheme,
|
|
3939
|
+
sumDeltaE: sumDE,
|
|
3940
|
+
maxDeltaE: maxDE,
|
|
3941
|
+
slotsCompared,
|
|
3942
|
+
confidence: computeConfidence(sumDE, maxDE, sumThreshold)
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
return best;
|
|
3946
|
+
}
|
|
3947
|
+
//#endregion
|
|
3948
|
+
//#region packages/ansi/src/theme/types.ts
|
|
3949
|
+
const COLOR_SCHEME_FIELDS = [
|
|
3950
|
+
"black",
|
|
3951
|
+
"red",
|
|
3952
|
+
"green",
|
|
3953
|
+
"yellow",
|
|
3954
|
+
"blue",
|
|
3955
|
+
"magenta",
|
|
3956
|
+
"cyan",
|
|
3957
|
+
"white",
|
|
3958
|
+
"brightBlack",
|
|
3959
|
+
"brightRed",
|
|
3960
|
+
"brightGreen",
|
|
3961
|
+
"brightYellow",
|
|
3962
|
+
"brightBlue",
|
|
3963
|
+
"brightMagenta",
|
|
3964
|
+
"brightCyan",
|
|
3965
|
+
"brightWhite",
|
|
3966
|
+
"foreground",
|
|
3967
|
+
"background",
|
|
3968
|
+
"cursorColor",
|
|
3969
|
+
"cursorText",
|
|
3970
|
+
"selectionBackground",
|
|
3971
|
+
"selectionForeground"
|
|
3972
|
+
];
|
|
3973
|
+
//#endregion
|
|
3974
|
+
//#region packages/ansi/src/theme/orchestrator.ts
|
|
3975
|
+
function envOverride() {
|
|
3976
|
+
const v = process.env.SILVERY_COLOR;
|
|
3977
|
+
if (!v) return null;
|
|
3978
|
+
if (v === "truecolor" || v === "256" || v === "ansi16" || v === "scheme" || v === "mono" || v === "auto") return v;
|
|
3979
|
+
return null;
|
|
3980
|
+
}
|
|
3981
|
+
/**
|
|
3982
|
+
* Detect the terminal's color scheme + derive a theme in one call.
|
|
3983
|
+
*
|
|
3984
|
+
* Runs the 4-layer detection cascade (override → probe → fingerprint →
|
|
3985
|
+
* fallback) and returns a fully-resolved Theme along with provenance metadata
|
|
3986
|
+
* so callers can log how the scheme was determined.
|
|
3987
|
+
*
|
|
3988
|
+
* This is the recommended entry point for apps — it handles all the gotchas
|
|
3989
|
+
* (non-TTY environments, failed probes, partial OSC responses, catalog matches,
|
|
3990
|
+
* bg-mode inference) and returns something you can hand to `ThemeProvider`.
|
|
3991
|
+
*
|
|
3992
|
+
* @example
|
|
3993
|
+
* ```ts
|
|
3994
|
+
* import { detectScheme } from "@silvery/ansi"
|
|
3995
|
+
* import { builtinPalettes } from "@silvery/theme/schemes"
|
|
3996
|
+
*
|
|
3997
|
+
* const { scheme, theme, source, matchedName, confidence } = await detectScheme({
|
|
3998
|
+
* catalog: Object.values(builtinPalettes),
|
|
3999
|
+
* enforce: "lenient",
|
|
4000
|
+
* })
|
|
4001
|
+
* console.log(`${source === "fingerprint" ? `detected ${matchedName}` : source} (${(confidence * 100).toFixed(0)}%)`)
|
|
4002
|
+
* ```
|
|
4003
|
+
*/
|
|
4004
|
+
async function detectScheme(opts = {}) {
|
|
4005
|
+
const enforce = opts.enforce ?? "lenient";
|
|
4006
|
+
const wcag = opts.wcag ?? false;
|
|
4007
|
+
if (opts.override) {
|
|
4008
|
+
const theme = loadTheme(opts.override, {
|
|
4009
|
+
enforce,
|
|
4010
|
+
wcag
|
|
4011
|
+
});
|
|
4012
|
+
return {
|
|
4013
|
+
scheme: opts.override,
|
|
4014
|
+
theme,
|
|
4015
|
+
source: "override",
|
|
4016
|
+
confidence: 1,
|
|
4017
|
+
slotSources: allSlotsFrom("fallback"),
|
|
4018
|
+
matchedName: opts.override.name
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
const envMode = envOverride();
|
|
4022
|
+
if (envMode === "mono" || envMode === "ansi16") {
|
|
4023
|
+
const fallback = opts.darkFallback !== false ? defaultDarkScheme : defaultLightScheme;
|
|
4024
|
+
return {
|
|
4025
|
+
scheme: fallback,
|
|
4026
|
+
theme: loadTheme(fallback, {
|
|
4027
|
+
enforce,
|
|
4028
|
+
wcag
|
|
4029
|
+
}),
|
|
4030
|
+
source: "override",
|
|
4031
|
+
confidence: 1,
|
|
4032
|
+
slotSources: allSlotsFrom("fallback"),
|
|
4033
|
+
matchedName: fallback.name
|
|
4034
|
+
};
|
|
4035
|
+
}
|
|
4036
|
+
const detected = await probeColors({
|
|
4037
|
+
timeoutMs: opts.timeoutMs,
|
|
4038
|
+
input: opts.input
|
|
4039
|
+
});
|
|
4040
|
+
if (!detected) {
|
|
4041
|
+
const fallback = opts.darkFallback !== false ? defaultDarkScheme : defaultLightScheme;
|
|
4042
|
+
return {
|
|
4043
|
+
scheme: fallback,
|
|
4044
|
+
theme: loadTheme(fallback, {
|
|
4045
|
+
enforce,
|
|
4046
|
+
wcag
|
|
4047
|
+
}),
|
|
4048
|
+
source: "fallback",
|
|
4049
|
+
confidence: 0,
|
|
4050
|
+
slotSources: allSlotsFrom("fallback"),
|
|
4051
|
+
matchedName: fallback.name
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
const catalog = opts.catalog ?? [];
|
|
4055
|
+
if (catalog.length > 0) {
|
|
4056
|
+
const match = fingerprintMatch(detected.palette, catalog);
|
|
4057
|
+
if (match) {
|
|
4058
|
+
const theme = loadTheme(match.scheme, {
|
|
4059
|
+
enforce,
|
|
4060
|
+
wcag
|
|
4061
|
+
});
|
|
4062
|
+
return {
|
|
4063
|
+
scheme: match.scheme,
|
|
4064
|
+
theme,
|
|
4065
|
+
source: "fingerprint",
|
|
4066
|
+
confidence: match.confidence,
|
|
4067
|
+
slotSources: allSlotsFrom("catalog"),
|
|
4068
|
+
matchedName: match.scheme.name
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
const fallback = detected.dark ? defaultDarkScheme : defaultLightScheme;
|
|
4073
|
+
const probedSlots = stripNulls(detected.palette);
|
|
4074
|
+
const derivedSlots = deriveMissingSlotsFromProbe(probedSlots);
|
|
4075
|
+
const merged = {
|
|
4076
|
+
...fallback,
|
|
4077
|
+
...derivedSlots,
|
|
4078
|
+
...probedSlots
|
|
4079
|
+
};
|
|
4080
|
+
const theme = loadTheme(merged, {
|
|
4081
|
+
enforce,
|
|
4082
|
+
wcag
|
|
4083
|
+
});
|
|
4084
|
+
const slotSources = {};
|
|
4085
|
+
for (const field of COLOR_SCHEME_FIELDS) slotSources[field] = typeof detected.palette[field] === "string" ? "probed" : field in derivedSlots ? "derived" : "fallback";
|
|
4086
|
+
const probedCount = Object.values(slotSources).filter((s) => s === "probed").length;
|
|
4087
|
+
return {
|
|
4088
|
+
scheme: merged,
|
|
4089
|
+
theme,
|
|
4090
|
+
source: "probed",
|
|
4091
|
+
confidence: Math.min(1, probedCount / 18),
|
|
4092
|
+
slotSources,
|
|
4093
|
+
matchedName: void 0
|
|
4094
|
+
};
|
|
4095
|
+
}
|
|
4096
|
+
function stripNulls(partial) {
|
|
4097
|
+
const result = {};
|
|
4098
|
+
for (const [k, v] of Object.entries(partial)) if (v != null) result[k] = v;
|
|
4099
|
+
return result;
|
|
4100
|
+
}
|
|
4101
|
+
function deriveMissingSlotsFromProbe(probed) {
|
|
4102
|
+
const out = {};
|
|
4103
|
+
if (typeof probed.background === "string" && typeof probed.foreground === "string" && typeof probed.selectionBackground !== "string") out.selectionBackground = blend(probed.background, probed.foreground, .16);
|
|
4104
|
+
if (typeof probed.foreground === "string" && typeof probed.selectionForeground !== "string") out.selectionForeground = probed.foreground;
|
|
4105
|
+
return out;
|
|
4106
|
+
}
|
|
4107
|
+
function allSlotsFrom(src) {
|
|
4108
|
+
const out = {};
|
|
4109
|
+
for (const field of COLOR_SCHEME_FIELDS) out[field] = src;
|
|
4110
|
+
return out;
|
|
4111
|
+
}
|
|
4112
|
+
/**
|
|
4113
|
+
* Shortcut: detect scheme + return the Theme only. For apps that don't care
|
|
4114
|
+
* about provenance. Same defaults as `detectScheme`.
|
|
4115
|
+
*
|
|
4116
|
+
* @example
|
|
4117
|
+
* ```ts
|
|
4118
|
+
* const theme = await detectSchemeTheme({ catalog: Object.values(builtinPalettes) })
|
|
4119
|
+
* render(<ThemeProvider theme={theme}>…</ThemeProvider>)
|
|
4120
|
+
* ```
|
|
4121
|
+
*/
|
|
4122
|
+
async function detectSchemeTheme(opts = {}) {
|
|
4123
|
+
const { theme } = await detectScheme(opts);
|
|
4124
|
+
return theme;
|
|
4125
|
+
}
|
|
4126
|
+
//#endregion
|
|
4127
|
+
//#region packages/ansi/src/theme/tokens.ts
|
|
4128
|
+
/**
|
|
4129
|
+
* Runtime constant — the 12 built-in variant names shipped by silvery.
|
|
4130
|
+
*
|
|
4131
|
+
* Used in dev warnings when an unknown variant is looked up in Text.tsx:
|
|
4132
|
+
* ```
|
|
4133
|
+
* Warning: Unknown variant "h11". Known variants: h1, h2, h3, …
|
|
4134
|
+
* ```
|
|
4135
|
+
*
|
|
4136
|
+
* Mirrors `VariantName` exactly — update both when variants change.
|
|
4137
|
+
*/
|
|
4138
|
+
const KNOWN_VARIANTS = [
|
|
4139
|
+
"h1",
|
|
4140
|
+
"h2",
|
|
4141
|
+
"h3",
|
|
4142
|
+
"body",
|
|
4143
|
+
"body-muted",
|
|
4144
|
+
"fine-print",
|
|
4145
|
+
"strong",
|
|
4146
|
+
"em",
|
|
4147
|
+
"link",
|
|
4148
|
+
"key",
|
|
4149
|
+
"code",
|
|
4150
|
+
"kbd"
|
|
4151
|
+
];
|
|
4152
|
+
//#endregion
|
|
4153
|
+
//#region packages/ansi/src/color-scheme.ts
|
|
4154
|
+
const CSI = `[`;
|
|
4155
|
+
`${CSI}`;
|
|
4156
|
+
`${CSI}`;
|
|
4157
|
+
//#endregion
|
|
4158
|
+
//#region packages/ansi/src/theme/generate.ts
|
|
4159
|
+
/**
|
|
4160
|
+
* Generate a complete ANSI 16 theme from a primary color + dark/light preference.
|
|
4161
|
+
*
|
|
4162
|
+
* All token values are ANSI color names (e.g. "yellow", "blueBright").
|
|
4163
|
+
*/
|
|
4164
|
+
function generateTheme(primary, dark) {
|
|
4165
|
+
const fg = dark ? "whiteBright" : "black";
|
|
4166
|
+
const accent = primary;
|
|
4167
|
+
const selectionbg = primary;
|
|
4168
|
+
const surfacebg = dark ? "black" : "white";
|
|
4169
|
+
const derived = deriveFields({
|
|
4170
|
+
primary,
|
|
4171
|
+
accent,
|
|
4172
|
+
fg,
|
|
4173
|
+
selectionbg,
|
|
4174
|
+
surfacebg,
|
|
4175
|
+
ring: {
|
|
4176
|
+
red: dark ? "redBright" : "red",
|
|
4177
|
+
orange: dark ? "redBright" : "red",
|
|
4178
|
+
yellow: "yellow",
|
|
4179
|
+
green: dark ? "greenBright" : "green",
|
|
4180
|
+
teal: "cyan",
|
|
4181
|
+
blue: dark ? "blueBright" : "blue",
|
|
4182
|
+
purple: "magenta",
|
|
4183
|
+
pink: dark ? "magentaBright" : "magenta"
|
|
4184
|
+
}
|
|
4185
|
+
});
|
|
4186
|
+
return {
|
|
4187
|
+
name: `${dark ? "dark" : "light"}-${primary}`,
|
|
4188
|
+
bg: "",
|
|
4189
|
+
fg,
|
|
4190
|
+
muted: dark ? "white" : "blackBright",
|
|
4191
|
+
mutedbg: dark ? "black" : "white",
|
|
4192
|
+
surface: dark ? "whiteBright" : "black",
|
|
4193
|
+
surfacebg,
|
|
4194
|
+
popover: dark ? "whiteBright" : "black",
|
|
4195
|
+
popoverbg: dark ? "blackBright" : "white",
|
|
4196
|
+
inverse: dark ? "black" : "whiteBright",
|
|
4197
|
+
inversebg: dark ? "whiteBright" : "black",
|
|
4198
|
+
cursor: "black",
|
|
4199
|
+
cursorbg: primary,
|
|
4200
|
+
selection: "black",
|
|
4201
|
+
selectionbg: primary,
|
|
4202
|
+
primary,
|
|
4203
|
+
primaryfg: "black",
|
|
4204
|
+
secondary: primary,
|
|
4205
|
+
secondaryfg: "black",
|
|
4206
|
+
accent: primary,
|
|
4207
|
+
accentfg: "black",
|
|
4208
|
+
error: dark ? "redBright" : "red",
|
|
4209
|
+
errorfg: "black",
|
|
4210
|
+
warning: primary,
|
|
4211
|
+
warningfg: "black",
|
|
4212
|
+
success: dark ? "greenBright" : "green",
|
|
4213
|
+
successfg: "black",
|
|
4214
|
+
info: dark ? "cyanBright" : "cyan",
|
|
4215
|
+
infofg: "black",
|
|
4216
|
+
border: "gray",
|
|
4217
|
+
inputborder: "gray",
|
|
4218
|
+
focusborder: dark ? "blueBright" : "blue",
|
|
4219
|
+
link: "blueBright",
|
|
4220
|
+
disabledfg: "gray",
|
|
4221
|
+
palette: [
|
|
4222
|
+
"black",
|
|
4223
|
+
"red",
|
|
4224
|
+
"green",
|
|
4225
|
+
"yellow",
|
|
4226
|
+
"blue",
|
|
4227
|
+
"magenta",
|
|
4228
|
+
"cyan",
|
|
4229
|
+
"white",
|
|
4230
|
+
"blackBright",
|
|
4231
|
+
"redBright",
|
|
4232
|
+
"greenBright",
|
|
4233
|
+
"yellowBright",
|
|
4234
|
+
"blueBright",
|
|
4235
|
+
"magentaBright",
|
|
4236
|
+
"cyanBright",
|
|
4237
|
+
"whiteBright"
|
|
4238
|
+
],
|
|
4239
|
+
...derived
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
//#endregion
|
|
4243
|
+
//#region packages/ansi/src/sterling/flat-tokens.ts
|
|
4244
|
+
const STERLING_FLAT_TOKENS = [
|
|
4245
|
+
"bg-surface-default",
|
|
4246
|
+
"bg-surface-subtle",
|
|
4247
|
+
"bg-surface-raised",
|
|
4248
|
+
"bg-surface-overlay",
|
|
4249
|
+
"bg-surface-hover",
|
|
4250
|
+
"border-default",
|
|
4251
|
+
"border-focus",
|
|
4252
|
+
"border-muted",
|
|
4253
|
+
"fg-cursor",
|
|
4254
|
+
"bg-cursor",
|
|
4255
|
+
"fg-muted",
|
|
4256
|
+
"bg-muted",
|
|
4257
|
+
"fg-accent",
|
|
4258
|
+
"bg-accent",
|
|
4259
|
+
"fg-on-accent",
|
|
4260
|
+
"fg-accent-hover",
|
|
4261
|
+
"bg-accent-hover",
|
|
4262
|
+
"fg-accent-active",
|
|
4263
|
+
"bg-accent-active",
|
|
4264
|
+
"border-accent",
|
|
4265
|
+
"fg-info",
|
|
4266
|
+
"bg-info",
|
|
4267
|
+
"fg-on-info",
|
|
4268
|
+
"bg-info-hover",
|
|
4269
|
+
"bg-info-active",
|
|
4270
|
+
"fg-success",
|
|
4271
|
+
"bg-success",
|
|
4272
|
+
"fg-on-success",
|
|
4273
|
+
"bg-success-hover",
|
|
4274
|
+
"bg-success-active",
|
|
4275
|
+
"fg-warning",
|
|
4276
|
+
"bg-warning",
|
|
4277
|
+
"fg-on-warning",
|
|
4278
|
+
"bg-warning-hover",
|
|
4279
|
+
"bg-warning-active",
|
|
4280
|
+
"fg-error",
|
|
4281
|
+
"bg-error",
|
|
4282
|
+
"fg-on-error",
|
|
4283
|
+
"bg-error-hover",
|
|
4284
|
+
"bg-error-active",
|
|
4285
|
+
"bg-selected",
|
|
4286
|
+
"fg-on-selected",
|
|
4287
|
+
"bg-selected-hover",
|
|
4288
|
+
"bg-inverse",
|
|
4289
|
+
"fg-on-inverse",
|
|
4290
|
+
"fg-link",
|
|
4291
|
+
"fg-disabled",
|
|
4292
|
+
"bg-disabled",
|
|
4293
|
+
"border-disabled",
|
|
4294
|
+
"bg-backdrop",
|
|
4295
|
+
"fg-default",
|
|
4296
|
+
"bg-default"
|
|
4297
|
+
];
|
|
4298
|
+
//#endregion
|
|
4299
|
+
//#region packages/ansi/src/sterling/defaults.ts
|
|
4300
|
+
/**
|
|
4301
|
+
* A hand-tuned neutral dark scheme — not a copy of any catalog palette, but
|
|
4302
|
+
* close to Nord/Dracula territory. Used only when the caller asks for a
|
|
4303
|
+
* "raw default" (no scheme at all).
|
|
4304
|
+
*/
|
|
4305
|
+
const darkBaseline = {
|
|
4306
|
+
name: "sterling-dark",
|
|
4307
|
+
dark: true,
|
|
4308
|
+
primary: "#7FB4CA",
|
|
4309
|
+
black: "#1E1E2E",
|
|
4310
|
+
red: "#E06C75",
|
|
4311
|
+
green: "#98C379",
|
|
4312
|
+
yellow: "#E5C07B",
|
|
4313
|
+
blue: "#61AFEF",
|
|
4314
|
+
magenta: "#C678DD",
|
|
4315
|
+
cyan: "#56B6C2",
|
|
4316
|
+
white: "#ABB2BF",
|
|
4317
|
+
brightBlack: "#5C6370",
|
|
4318
|
+
brightRed: "#E06C75",
|
|
4319
|
+
brightGreen: "#98C379",
|
|
4320
|
+
brightYellow: "#E5C07B",
|
|
4321
|
+
brightBlue: "#61AFEF",
|
|
4322
|
+
brightMagenta: "#C678DD",
|
|
4323
|
+
brightCyan: "#56B6C2",
|
|
4324
|
+
brightWhite: "#FFFFFF",
|
|
4325
|
+
foreground: "#E4E4E7",
|
|
4326
|
+
background: "#16181D",
|
|
4327
|
+
cursorColor: "#E4E4E7",
|
|
4328
|
+
cursorText: "#16181D",
|
|
4329
|
+
selectionBackground: "#3E4452",
|
|
4330
|
+
selectionForeground: "#E4E4E7"
|
|
4331
|
+
};
|
|
4332
|
+
const lightBaseline = {
|
|
4333
|
+
name: "sterling-light",
|
|
4334
|
+
dark: false,
|
|
4335
|
+
primary: "#1F6FEB",
|
|
4336
|
+
black: "#24292F",
|
|
4337
|
+
red: "#CF222E",
|
|
4338
|
+
green: "#1A7F37",
|
|
4339
|
+
yellow: "#9A6700",
|
|
4340
|
+
blue: "#0969DA",
|
|
4341
|
+
magenta: "#8250DF",
|
|
4342
|
+
cyan: "#1B7C83",
|
|
4343
|
+
white: "#6E7781",
|
|
4344
|
+
brightBlack: "#57606A",
|
|
4345
|
+
brightRed: "#A40E26",
|
|
4346
|
+
brightGreen: "#2DA44E",
|
|
4347
|
+
brightYellow: "#BF8700",
|
|
4348
|
+
brightBlue: "#218BFF",
|
|
4349
|
+
brightMagenta: "#A475F9",
|
|
4350
|
+
brightCyan: "#3192AA",
|
|
4351
|
+
brightWhite: "#8C959F",
|
|
4352
|
+
foreground: "#1F2328",
|
|
4353
|
+
background: "#FFFFFF",
|
|
4354
|
+
cursorColor: "#1F2328",
|
|
4355
|
+
cursorText: "#FFFFFF",
|
|
4356
|
+
selectionBackground: "#DDF4FF",
|
|
4357
|
+
selectionForeground: "#1F2328"
|
|
4358
|
+
};
|
|
4359
|
+
function defaultScheme(mode = "dark") {
|
|
4360
|
+
return mode === "dark" ? darkBaseline : lightBaseline;
|
|
4361
|
+
}
|
|
4362
|
+
//#endregion
|
|
4363
|
+
//#region packages/ansi/src/sterling/define.ts
|
|
4364
|
+
/**
|
|
4365
|
+
* `defineDesignSystem` — wrap a DesignSystem so its derivations auto-apply
|
|
4366
|
+
* `bakeFlat` per the `flatten` flag.
|
|
4367
|
+
*
|
|
4368
|
+
* This makes flat-projection-on-same-object a FRAMEWORK feature, not a
|
|
4369
|
+
* Sterling-specific one. Any `DesignSystem` whose Theme is a nested POJO of
|
|
4370
|
+
* hex-string leaves gets `theme.accent.bg` AND `theme["bg-accent"]` access
|
|
4371
|
+
* without reimplementing the walk.
|
|
4372
|
+
*
|
|
4373
|
+
* ```ts
|
|
4374
|
+
* export const sterling = defineDesignSystem({
|
|
4375
|
+
* name: "sterling",
|
|
4376
|
+
* shape: STERLING_SHAPE,
|
|
4377
|
+
* flatten: true, // ← opt in to default channel-role-state rule
|
|
4378
|
+
* defaults(mode) { return derive(...) },
|
|
4379
|
+
* theme(partial) { return derive(...) },
|
|
4380
|
+
* deriveFromScheme(scheme) { return derive(...) },
|
|
4381
|
+
* // … etc. Return VALUES without flat keys; defineDesignSystem bakes them.
|
|
4382
|
+
* })
|
|
4383
|
+
* ```
|
|
4384
|
+
*
|
|
4385
|
+
* Contract:
|
|
4386
|
+
* - `flatten: true` → apply `bakeFlat(theme)` with `defaultFlattenRule`
|
|
4387
|
+
* - `flatten: <fn>` → apply `bakeFlat(theme, <fn>)`
|
|
4388
|
+
* - `flatten: false` / omitted → pass-through (identity)
|
|
4389
|
+
*
|
|
4390
|
+
* All derivation methods (`defaults`, `theme`, `deriveFromScheme`,
|
|
4391
|
+
* `deriveFromColor`, `deriveFromPair`, `deriveFromSchemeWithBrand`) are
|
|
4392
|
+
* wrapped; their return values go through the flatten filter before
|
|
4393
|
+
* reaching the caller. `deriveFromPair` returns `{ light, dark }` —
|
|
4394
|
+
* both are flattened.
|
|
4395
|
+
*/
|
|
4396
|
+
function resolveFlatten(flatten) {
|
|
4397
|
+
if (flatten === false || flatten === void 0) return (t) => t;
|
|
4398
|
+
if (typeof flatten === "function") {
|
|
4399
|
+
const rule = flatten;
|
|
4400
|
+
return (t) => bakeFlat(t, rule);
|
|
4401
|
+
}
|
|
4402
|
+
return (t) => bakeFlat(t);
|
|
4403
|
+
}
|
|
4404
|
+
/**
|
|
4405
|
+
* Wrap a DesignSystem so every derivation method auto-applies `bakeFlat`
|
|
4406
|
+
* per the `flatten` flag. Pass your raw system (one that returns nested
|
|
4407
|
+
* themes) and this returns a user-facing system whose outputs have flat
|
|
4408
|
+
* keys populated.
|
|
4409
|
+
*/
|
|
4410
|
+
function defineDesignSystem(def) {
|
|
4411
|
+
const flatten = resolveFlatten(def.flatten);
|
|
4412
|
+
return {
|
|
4413
|
+
name: def.name,
|
|
4414
|
+
shape: def.shape,
|
|
4415
|
+
flatten: def.flatten,
|
|
4416
|
+
defaults: (mode) => flatten(def.defaults(mode)),
|
|
4417
|
+
theme: (partial, opts) => flatten(def.theme(partial, opts)),
|
|
4418
|
+
deriveFromScheme: (scheme, opts) => flatten(def.deriveFromScheme(scheme, opts)),
|
|
4419
|
+
deriveFromColor: (color, opts) => flatten(def.deriveFromColor(color, opts)),
|
|
4420
|
+
deriveFromPair: (light, dark, opts) => {
|
|
4421
|
+
const pair = def.deriveFromPair(light, dark, opts);
|
|
4422
|
+
return {
|
|
4423
|
+
light: flatten(pair.light),
|
|
4424
|
+
dark: flatten(pair.dark)
|
|
4425
|
+
};
|
|
4426
|
+
},
|
|
4427
|
+
deriveFromSchemeWithBrand: (scheme, brand, opts) => flatten(def.deriveFromSchemeWithBrand(scheme, brand, opts))
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
//#endregion
|
|
4431
|
+
//#region packages/ansi/src/sterling/sterling.ts
|
|
4432
|
+
/**
|
|
4433
|
+
* Sterling — silvery's canonical DesignSystem.
|
|
4434
|
+
*
|
|
4435
|
+
* This is the default system shipped from `@silvery/theme`. It implements
|
|
4436
|
+
* the `DesignSystem` contract from `types.ts` and serves as the reference
|
|
4437
|
+
* for alternative systems (`@silvery/design-material`, `-primer`, etc.).
|
|
4438
|
+
*
|
|
4439
|
+
* The flat-projection (`theme["bg-accent"]` as a sibling of `theme.accent.bg`
|
|
4440
|
+
* on the same object) is NOT Sterling-specific — it's a framework feature.
|
|
4441
|
+
* Sterling opts in via `flatten: true` in {@link defineDesignSystem}, which
|
|
4442
|
+
* auto-applies `bakeFlat` (from `@silvery/ansi`) to every derivation's
|
|
4443
|
+
* output. The default rule is channel-role-state (`fg-accent`, `bg-accent-hover`,
|
|
4444
|
+
* `fg-on-error`, `bg-surface-subtle`, `border-focus`, …) — exactly what
|
|
4445
|
+
* Sterling's pre-generalization `populateFlat` produced.
|
|
4446
|
+
*
|
|
4447
|
+
* All derivation functions return a frozen Theme with both nested roles
|
|
4448
|
+
* AND flat hyphen keys populated — the user-facing `$fg-accent` syntax
|
|
4449
|
+
* resolves against the flat keys, while programmatic access uses nested.
|
|
4450
|
+
*/
|
|
4451
|
+
const STERLING_SHAPE = {
|
|
4452
|
+
flatTokens: STERLING_FLAT_TOKENS,
|
|
4453
|
+
roles: [
|
|
4454
|
+
"accent",
|
|
4455
|
+
"info",
|
|
4456
|
+
"success",
|
|
4457
|
+
"warning",
|
|
4458
|
+
"error",
|
|
4459
|
+
"muted",
|
|
4460
|
+
"surface",
|
|
4461
|
+
"border",
|
|
4462
|
+
"cursor",
|
|
4463
|
+
"selected",
|
|
4464
|
+
"inverse",
|
|
4465
|
+
"link",
|
|
4466
|
+
"disabled"
|
|
4467
|
+
],
|
|
4468
|
+
states: ["hover", "active"]
|
|
4469
|
+
};
|
|
4470
|
+
/**
|
|
4471
|
+
* Internal: build a nested Theme (no flat keys). `defineDesignSystem` applies
|
|
4472
|
+
* `bakeFlat` afterwards — the inner derivation stays flat-agnostic.
|
|
4473
|
+
*
|
|
4474
|
+
* Also pre-populates the standalone flat tokens that don't come from a role
|
|
4475
|
+
* walk: `bg-backdrop` (modal scrim), and `fg-default`/`bg-default` (explicit
|
|
4476
|
+
* aliases for canvas fg/bg). bakeFlat preserves pre-existing root-level
|
|
4477
|
+
* hyphen keys, so writing these here is the simplest seam.
|
|
4478
|
+
*/
|
|
4479
|
+
function buildRawTheme(scheme, opts = {}) {
|
|
4480
|
+
const base = deriveTheme$1(scheme, opts);
|
|
4481
|
+
const out = base;
|
|
4482
|
+
if (typeof out["bg-backdrop"] !== "string") out["bg-backdrop"] = blend(scheme.background, "#000000", .4);
|
|
4483
|
+
if (typeof out["fg-default"] !== "string") out["fg-default"] = scheme.foreground;
|
|
4484
|
+
if (typeof out["bg-default"] !== "string") out["bg-default"] = scheme.background;
|
|
4485
|
+
return base;
|
|
4486
|
+
}
|
|
4487
|
+
/**
|
|
4488
|
+
* Apply a brand overlay to a ColorScheme — overrides `primary` and relevant
|
|
4489
|
+
* ANSI hue slots with the brand color. Keeps the rest of the scheme intact.
|
|
4490
|
+
* Per Appendix F: brand is a theme INPUT, not a public token sibling of accent.
|
|
4491
|
+
*/
|
|
4492
|
+
function applyBrand(scheme, brand) {
|
|
4493
|
+
return {
|
|
4494
|
+
...scheme,
|
|
4495
|
+
primary: brand
|
|
4496
|
+
};
|
|
4497
|
+
}
|
|
4498
|
+
/**
|
|
4499
|
+
* Sterling — the user-facing DesignSystem. `defineDesignSystem` wraps
|
|
4500
|
+
* `rawSterling` with auto-`bakeFlat` (per `flatten: true`), so every
|
|
4501
|
+
* returned Theme has both nested roles AND flat hyphen keys populated.
|
|
4502
|
+
*/
|
|
4503
|
+
const sterling = defineDesignSystem({
|
|
4504
|
+
name: "sterling",
|
|
4505
|
+
shape: STERLING_SHAPE,
|
|
4506
|
+
flatten: true,
|
|
4507
|
+
defaults(mode = "dark") {
|
|
4508
|
+
return buildRawTheme(defaultScheme(mode), { contrast: "auto-lift" });
|
|
4509
|
+
},
|
|
4510
|
+
theme(partial, opts = {}) {
|
|
4511
|
+
const base = buildRawTheme(defaultScheme(opts.mode ?? "dark"), {
|
|
4512
|
+
...opts,
|
|
4513
|
+
contrast: opts.contrast ?? "auto-lift"
|
|
4514
|
+
});
|
|
4515
|
+
if (!partial) return base;
|
|
4516
|
+
return mergePartial(base, partial);
|
|
4517
|
+
},
|
|
4518
|
+
deriveFromScheme(scheme, opts = {}) {
|
|
4519
|
+
return buildRawTheme(scheme, opts);
|
|
4520
|
+
},
|
|
4521
|
+
deriveFromColor(color, opts = {}) {
|
|
4522
|
+
return buildRawTheme({
|
|
4523
|
+
...defaultScheme(opts.mode ?? "dark"),
|
|
4524
|
+
name: `seed:${color}`,
|
|
4525
|
+
primary: color,
|
|
4526
|
+
blue: color,
|
|
4527
|
+
brightBlue: blend(color, "#ffffff", .15)
|
|
4528
|
+
}, opts);
|
|
4529
|
+
},
|
|
4530
|
+
deriveFromPair(light, dark, opts = {}) {
|
|
4531
|
+
return {
|
|
4532
|
+
light: buildRawTheme(light, {
|
|
4533
|
+
...opts,
|
|
4534
|
+
mode: "light"
|
|
4535
|
+
}),
|
|
4536
|
+
dark: buildRawTheme(dark, {
|
|
4537
|
+
...opts,
|
|
4538
|
+
mode: "dark"
|
|
4539
|
+
})
|
|
4540
|
+
};
|
|
4541
|
+
},
|
|
4542
|
+
deriveFromSchemeWithBrand(scheme, brand, opts = {}) {
|
|
4543
|
+
return buildRawTheme(applyBrand(scheme, brand), opts);
|
|
4544
|
+
}
|
|
4545
|
+
});
|
|
4546
|
+
//#endregion
|
|
4547
|
+
//#region packages/ansi/src/sterling/token-manifest.ts
|
|
4548
|
+
const AA = "AA 4.5:1";
|
|
4549
|
+
const NA = "—";
|
|
4550
|
+
[...[
|
|
4551
|
+
"info",
|
|
4552
|
+
"success",
|
|
4553
|
+
"warning",
|
|
4554
|
+
"error"
|
|
4555
|
+
].flatMap((role) => [
|
|
4556
|
+
{
|
|
4557
|
+
flat: `fg-${role}`,
|
|
4558
|
+
path: `${role}.fg`,
|
|
4559
|
+
family: role,
|
|
4560
|
+
axis: "fg",
|
|
4561
|
+
purpose: `${capitalize(role)} status text.`,
|
|
4562
|
+
derivation: seedRule(role),
|
|
4563
|
+
contrast: AA,
|
|
4564
|
+
tierNotes: "Seeds from the matching ANSI palette slot."
|
|
4565
|
+
},
|
|
4566
|
+
{
|
|
4567
|
+
flat: `bg-${role}`,
|
|
4568
|
+
path: `${role}.bg`,
|
|
4569
|
+
family: role,
|
|
4570
|
+
axis: "bg",
|
|
4571
|
+
purpose: `${capitalize(role)} fill — alerts, badges.`,
|
|
4572
|
+
derivation: seedRule(role),
|
|
4573
|
+
contrast: NA,
|
|
4574
|
+
tierNotes: "Distinct from siblings in ALL 84 palettes (collision test)."
|
|
4575
|
+
},
|
|
4576
|
+
{
|
|
4577
|
+
flat: `fg-on-${role}`,
|
|
4578
|
+
path: `${role}.fgOn`,
|
|
4579
|
+
family: role,
|
|
4580
|
+
axis: "fg-on",
|
|
4581
|
+
purpose: `Foreground when drawing text ON \`bg-${role}\`.`,
|
|
4582
|
+
derivation: "contrast-pick(scheme.fg / scheme.bg / black / white) for AA on bg.",
|
|
4583
|
+
contrast: AA,
|
|
4584
|
+
tierNotes: "Pre-quantization pick."
|
|
4585
|
+
},
|
|
4586
|
+
{
|
|
4587
|
+
flat: `bg-${role}-hover`,
|
|
4588
|
+
path: `${role}.hover.bg`,
|
|
4589
|
+
family: role,
|
|
4590
|
+
axis: "bg-hover",
|
|
4591
|
+
purpose: `Hover fill for ${role} surfaces.`,
|
|
4592
|
+
derivation: `OKLCH ±0.04L on bg-${role}.`,
|
|
4593
|
+
contrast: NA,
|
|
4594
|
+
tierNotes: "Often collapses with bg in ansi16."
|
|
4595
|
+
},
|
|
4596
|
+
{
|
|
4597
|
+
flat: `bg-${role}-active`,
|
|
4598
|
+
path: `${role}.active.bg`,
|
|
4599
|
+
family: role,
|
|
4600
|
+
axis: "bg-active",
|
|
4601
|
+
purpose: `Pressed/active fill for ${role} surfaces.`,
|
|
4602
|
+
derivation: `OKLCH ±0.08L on bg-${role}.`,
|
|
4603
|
+
contrast: NA,
|
|
4604
|
+
tierNotes: "Collapses to bg in low tiers."
|
|
4605
|
+
}
|
|
4606
|
+
])];
|
|
4607
|
+
function capitalize(s) {
|
|
4608
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
4609
|
+
}
|
|
4610
|
+
function seedRule(role) {
|
|
4611
|
+
switch (role) {
|
|
4612
|
+
case "info": return "scheme.primary (info mirrors accent's seed).";
|
|
4613
|
+
case "success": return "scheme.green.";
|
|
4614
|
+
case "warning": return "scheme.yellow.";
|
|
4615
|
+
case "error": return "scheme.red.";
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
//#endregion
|
|
4619
|
+
export { queryMultiplePaletteColors as $, fgColorCode as A, detectColorScheme as B, enableBracketedPaste as C, defaultFlattenRule as D, bakeFlat as E, pickColorLevel as F, resetCursorColor as G, queryCursorColor as H, quantizeHex as I, setCursorColor as J, resetForegroundColor as K, detectTerminalScheme as L, detectColorFromEnv as M, probeTerminalProfile as N, warnOnce as O, ANSI16_SLOT_HEX as P, parsePaletteResponse as Q, detectTheme as R, disableMouse as S, enableMouse as T, queryForegroundColor as U, queryBackgroundColor as V, resetBackgroundColor as W, ProtocolError as X, setForegroundColor as Y, isProtocolError as Z, kittyDeleteAllScrimPlacements as _, generateTheme as a, deriveRoles as at, disableBracketedPaste as b, detectSchemeTheme as c, WCAG_AA as ct, createMixedStyle as d, deriveFields as dt, queryPaletteColor as et, createStyle as f, defaultCaps as ft, cupTo as g, buildScrimPixels as h, STERLING_FLAT_TOKENS as i, deriveTheme as it, createTerminalProfile as j, bgColorCode as k, COLOR_SCHEME_FIELDS as l, autoLift as lt, backdropPlacementId as m, defineDesignSystem as n, ansi16DarkTheme as nt, KNOWN_VARIANTS as o, deriveTheme$1 as ot, resolveThemeColor as p, setBackgroundColor as q, defaultScheme as r, ansi16LightTheme as rt, detectScheme as s, ContrastError as st, sterling as t, setPaletteColor as tt, monoAttrsForColorString as u, checkAA as ut, kittyPlaceAt as v, enableKittyKeyboard as w, disableKittyKeyboard as x, kittyUploadScrimImage as y, probeColors as z };
|
|
4620
|
+
|
|
4621
|
+
//# sourceMappingURL=src-Oe6x5PrS.mjs.map
|