vscode-bauhaus-theme 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.vscodeignore CHANGED
@@ -9,3 +9,4 @@ palettes.json
9
9
  .npmignore
10
10
  icon.svg
11
11
  screenshot.*
12
+ terminal
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4
4
 
5
+ # [1.2.0](https://github.com/dmnsgn/vscode-bauhaus-theme/compare/v1.1.0...v1.2.0) (2026-10-26)
6
+
7
+ ### Features
8
+
9
+ * use a dedicated caret color fitted to WCAG 2.1 non-text contrast ([9353c25](https://github.com/dmnsgn/vscode-bauhaus-theme/commit/9353c25d172f46f6a2f67e9bd9a627fd2dd511a4))
10
+
11
+ # [1.1.0](https://github.com/dmnsgn/vscode-bauhaus-theme/compare/v1.0.1...v1.1.0) (2026-10-01)
12
+
13
+ ### Features
14
+
15
+ * update terminal colors ([7e64545](https://github.com/dmnsgn/vscode-bauhaus-theme/commit/7e64545df5ad05aae0e25ea694c6804cabe3302e))
16
+
5
17
  ## [1.0.1](https://github.com/dmnsgn/vscode-bauhaus-theme/compare/v1.0.0...v1.0.1) (2026-09-25)
6
18
 
7
19
  # 1.0.0 (2026-09-25)
package/README.md CHANGED
@@ -141,9 +141,16 @@ To switch, add the matching snippet to your `settings.json`.
141
141
 
142
142
  </details>
143
143
 
144
+ ## Terminal themes
145
+
146
+ The same colors for the macOS Terminal and iTerm2, generated from the VS Code themes.
147
+
148
+ - **Terminal**: download [Bauhaus Dark](terminal/macos/Bauhaus%20Dark.terminal) or [Bauhaus Light](terminal/macos/Bauhaus%20Light.terminal), then in `Settings… > Profiles`, click <kbd>…</kbd> > `Import…`.
149
+ - **iTerm2**: download [Bauhaus Dark](terminal/iterm2/Bauhaus%20Dark.itermcolors) or [Bauhaus Light](terminal/iterm2/Bauhaus%20Light.itermcolors), then in `Settings > Profiles > Colors`, pick `Color Presets… > Import…`.
150
+
144
151
  ## Palettes
145
152
 
146
- Contrast is WCAG 2.0 against the editor background.
153
+ Contrast is WCAG 2.0 against the editor background. Terminal hues reach AA (bright: AAA) against the terminal background.
147
154
 
148
155
  <details>
149
156
  <summary>Colors and contrast ratios</summary>
@@ -160,15 +167,15 @@ Contrast is WCAG 2.0 against the editor background.
160
167
  | fg.default | `#f2f2f2` |
161
168
  | fg.muted | `#9a9a9a` |
162
169
 
163
- | Hue | Ink | Bright (terminal) |
164
- | ------ | --------- | ----------------- |
165
- | red | `#ff6963` | `#ffaba3` |
166
- | orange | `#ef7a04` | `#feaf7b` |
167
- | yellow | `#c59208` | `#f6b606` |
168
- | green | `#8aa441` | `#afcc69` |
169
- | cyan | `#15acaa` | `#52d3d1` |
170
- | blue | `#769ae2` | `#a2c1fe` |
171
- | violet | `#d873e1` | `#f8a2ff` |
170
+ | Hue | Ink | Terminal | Terminal bright |
171
+ | ------ | --------- | --------- | --------------- |
172
+ | red | `#ff6963` | `#ff3e42` | `#ff8d84` |
173
+ | orange | `#ef7a04` | `#d66c03` | `#ff9240` |
174
+ | yellow | `#c59208` | `#b18201` | `#dea301` |
175
+ | green | `#8aa441` | `#79932d` | `#9cb855` |
176
+ | cyan | `#15acaa` | `#099997` | `#38bfbc` |
177
+ | blue | `#769ae2` | `#6689cf` | `#88adf6` |
178
+ | violet | `#d873e1` | `#c661cf` | `#ed86f6` |
172
179
 
173
180
  | Syntax role | Color | Contrast on editor | WCAG 2.0 |
174
181
  | ----------- | --------- | ------------------ | -------- |
@@ -200,15 +207,15 @@ Contrast is WCAG 2.0 against the editor background.
200
207
  | fg.default | `#2b2119` |
201
208
  | fg.muted | `#616161` |
202
209
 
203
- | Hue | Ink | Bright (terminal) |
204
- | ------ | --------- | ----------------- |
205
- | red | `#c5031e` | `#8f0113` |
206
- | orange | `#9a4c00` | `#703500` |
207
- | yellow | `#7e5c05` | `#5a4100` |
208
- | green | `#546903` | `#3b4b01` |
209
- | cyan | `#036d6c` | `#034d4c` |
210
- | blue | `#4060a3` | `#254283` |
211
- | violet | `#9935a3` | `#790d83` |
210
+ | Hue | Ink | Terminal | Terminal bright |
211
+ | ------ | --------- | --------- | --------------- |
212
+ | red | `#c5031e` | `#c5031f` | `#8f0113` |
213
+ | orange | `#9a4c00` | `#9a4c00` | `#703500` |
214
+ | yellow | `#7e5c05` | `#7e5c05` | `#5a4100` |
215
+ | green | `#546903` | `#546903` | `#3b4b01` |
216
+ | cyan | `#036d6c` | `#036d6c` | `#034d4d` |
217
+ | blue | `#4060a3` | `#4060a3` | `#254283` |
218
+ | violet | `#9935a3` | `#9935a2` | `#790d83` |
212
219
 
213
220
  | Syntax role | Color | Contrast on editor | WCAG 2.0 |
214
221
  | ----------- | --------- | ------------------ | -------- |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vscode-bauhaus-theme",
3
3
  "displayName": "Bauhaus Theme",
4
- "version": "1.0.1",
4
+ "version": "1.2.0",
5
5
  "description": "Bauhaus primary colors on warm black and paper. Dark and light themes, every syntax color WCAG AA.",
6
6
  "categories": [
7
7
  "Themes"
@@ -39,10 +39,12 @@
39
39
  "publisher": "dmnsgn",
40
40
  "type": "module",
41
41
  "scripts": {
42
- "build": "node scripts/build.js",
42
+ "build": "node scripts/build.js && node scripts/terminal.js",
43
+ "dev": "code --extensionDevelopmentPath=\"$PWD\" samples",
43
44
  "icon": "node scripts/icon.js",
44
45
  "reference": "node scripts/reference.js",
45
- "screenshot": "node scripts/screenshot.js"
46
+ "screenshot": "node scripts/screenshot.js",
47
+ "terminal": "node scripts/terminal.js"
46
48
  },
47
49
  "contributes": {
48
50
  "themes": [
@@ -60,10 +62,11 @@
60
62
  },
61
63
  "devDependencies": {
62
64
  "@resvg/resvg-js": "^2.6.2",
63
- "pex-color": "^2.3.0"
65
+ "pex-color": "^2.3.0",
66
+ "value-format-table": "^1.0.0"
64
67
  },
65
68
  "engines": {
66
- "node": ">=22.0.0",
69
+ "node": ">=24.0.0",
67
70
  "npm": ">=10.5.1",
68
71
  "snowdev": ">=3.0.0-alpha.3",
69
72
  "vscode": "^1.90.0"
package/palettes.json CHANGED
@@ -20,14 +20,23 @@
20
20
  "blue": "#769ae2",
21
21
  "violet": "#d873e1"
22
22
  },
23
+ "ansi": {
24
+ "red": "#ff3e42",
25
+ "orange": "#d66c03",
26
+ "yellow": "#b18201",
27
+ "green": "#79932d",
28
+ "cyan": "#099997",
29
+ "blue": "#6689cf",
30
+ "violet": "#c661cf"
31
+ },
23
32
  "bright": {
24
- "red": "#ffaba3",
25
- "orange": "#feaf7b",
26
- "yellow": "#f6b606",
27
- "green": "#afcc69",
28
- "cyan": "#52d3d1",
29
- "blue": "#a2c1fe",
30
- "violet": "#f8a2ff"
33
+ "red": "#ff8d84",
34
+ "orange": "#ff9240",
35
+ "yellow": "#dea301",
36
+ "green": "#9cb855",
37
+ "cyan": "#38bfbc",
38
+ "blue": "#88adf6",
39
+ "violet": "#ed86f6"
31
40
  },
32
41
  "fill": {
33
42
  "red": "#95171d",
@@ -155,12 +164,21 @@
155
164
  "blue": "#4060a3",
156
165
  "violet": "#9935a3"
157
166
  },
167
+ "ansi": {
168
+ "red": "#c5031f",
169
+ "orange": "#9a4c00",
170
+ "yellow": "#7e5c05",
171
+ "green": "#546903",
172
+ "cyan": "#036d6c",
173
+ "blue": "#4060a3",
174
+ "violet": "#9935a2"
175
+ },
158
176
  "bright": {
159
177
  "red": "#8f0113",
160
178
  "orange": "#703500",
161
179
  "yellow": "#5a4100",
162
180
  "green": "#3b4b01",
163
- "cyan": "#034d4c",
181
+ "cyan": "#034d4d",
164
182
  "blue": "#254283",
165
183
  "violet": "#790d83"
166
184
  },
package/scripts/build.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
 
3
+ import { valueFormatTableAsMarkdown } from "value-format-table";
3
4
  import reference from "../reference/theme-colors.json" with { type: "json" };
4
5
  import { accents } from "../src/accents.js";
5
6
  import { contrast } from "../src/color.js";
@@ -92,29 +93,56 @@ const report = palettes.map((palette) => {
92
93
  });
93
94
  await writeJson("palettes.json", report);
94
95
 
96
+ const table = (rows) => valueFormatTableAsMarkdown(rows).trim();
97
+ const code = (color) => `\`${color}\``;
98
+
95
99
  const markdown = report
96
100
  .map(
97
- ({ type, bg, fg, ink, bright, syntax, fills }) => `### ${capitalize(type)}
98
-
99
- | Surface | Color |
100
- | --- | --- |
101
- ${Object.entries({ ...bg, "fg.default": fg.default, "fg.muted": fg.muted })
102
- .map(([key, color]) => `| ${key} | \`${color}\` |`)
103
- .join("\n")}
104
-
105
- | Hue | Ink | Bright (terminal) |
106
- | --- | --- | --- |
107
- ${Object.keys(ink)
108
- .map((hue) => `| ${hue} | \`${ink[hue]}\` | \`${bright[hue]}\` |`)
109
- .join("\n")}
110
-
111
- | Syntax role | Color | Contrast on editor | WCAG 2.0 |
112
- | --- | --- | --- | --- |
113
- ${syntax.map(({ role, color, ratio, grade }) => `| ${role} | \`${color}\` | ${ratio}:1 | ${grade} |`).join("\n")}
114
-
115
- | Fill | Text | Contrast | WCAG 2.0 |
116
- | --- | --- | --- | --- |
117
- ${fills.map(({ hue, color, text, ratio, grade }) => `| ${hue} \`${color}\` | \`${text}\` | ${ratio}:1 | ${grade} |`).join("\n")}
101
+ ({
102
+ type,
103
+ bg,
104
+ fg,
105
+ ink,
106
+ ansi,
107
+ bright,
108
+ syntax,
109
+ fills,
110
+ }) => `### ${capitalize(type)}
111
+
112
+ ${table(
113
+ Object.entries({
114
+ ...bg,
115
+ "fg.default": fg.default,
116
+ "fg.muted": fg.muted,
117
+ }).map(([surface, color]) => ({ Surface: surface, Color: code(color) })),
118
+ )}
119
+
120
+ ${table(
121
+ Object.keys(ink).map((hue) => ({
122
+ Hue: hue,
123
+ Ink: code(ink[hue]),
124
+ Terminal: code(ansi[hue]),
125
+ "Terminal bright": code(bright[hue]),
126
+ })),
127
+ )}
128
+
129
+ ${table(
130
+ syntax.map(({ role, color, ratio, grade }) => ({
131
+ "Syntax role": role,
132
+ Color: code(color),
133
+ "Contrast on editor": `${ratio}:1`,
134
+ "WCAG 2.0": grade,
135
+ })),
136
+ )}
137
+
138
+ ${table(
139
+ fills.map(({ hue, color, text, ratio, grade }) => ({
140
+ Fill: `${hue} ${code(color)}`,
141
+ Text: code(text),
142
+ Contrast: `${ratio}:1`,
143
+ "WCAG 2.0": grade,
144
+ })),
145
+ )}
118
146
  `,
119
147
  )
120
148
  .join("\n");
@@ -0,0 +1,131 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+
3
+ import { flatten, toRgb } from "../src/color.js";
4
+ import { plist } from "../src/plist.js";
5
+
6
+ const root = new URL("../", import.meta.url);
7
+ const ANSI = [
8
+ "Black",
9
+ "Red",
10
+ "Green",
11
+ "Yellow",
12
+ "Blue",
13
+ "Magenta",
14
+ "Cyan",
15
+ "White",
16
+ ];
17
+
18
+ // The VS Code themes are the source: terminal colors are read back from the build output.
19
+ function scheme(colors) {
20
+ const background = colors["terminal.background"];
21
+ const foreground = colors["terminal.foreground"];
22
+ return {
23
+ background,
24
+ foreground,
25
+ cursor: colors["terminalCursor.foreground"],
26
+ cursorText: background,
27
+ // Neither terminal blends a translucent selection like VS Code does.
28
+ selection: flatten(colors["terminal.selectionBackground"], background),
29
+ link: colors["textLink.foreground"],
30
+ ansi: [
31
+ ...ANSI.map((name) => colors[`terminal.ansi${name}`]),
32
+ ...ANSI.map((name) => colors[`terminal.ansiBright${name}`]),
33
+ ],
34
+ };
35
+ }
36
+
37
+ // iTerm2
38
+ function itermColor(hex) {
39
+ const [r, g, b] = toRgb(hex);
40
+ return {
41
+ "Alpha Component": 1,
42
+ "Blue Component": b,
43
+ "Color Space": "sRGB",
44
+ "Green Component": g,
45
+ "Red Component": r,
46
+ };
47
+ }
48
+
49
+ const iterm = (s) => ({
50
+ ...Object.fromEntries(
51
+ s.ansi.map((hex, i) => [`Ansi ${i} Color`, itermColor(hex)]),
52
+ ),
53
+ "Background Color": itermColor(s.background),
54
+ "Bold Color": itermColor(s.foreground),
55
+ "Cursor Color": itermColor(s.cursor),
56
+ "Cursor Text Color": itermColor(s.cursorText),
57
+ "Foreground Color": itermColor(s.foreground),
58
+ "Link Color": itermColor(s.link),
59
+ "Selected Text Color": itermColor(s.foreground),
60
+ "Selection Color": itermColor(s.selection),
61
+ });
62
+
63
+ // Terminal.app stores each color as an NSKeyedArchiver NSColor. The XML archive format is
64
+ // accepted, and NSID 7 resolves to sRGB without embedding its ICC profile. NSRGB is the
65
+ // fallback for readers ignoring NSCustomColorSpace.
66
+ const uid = (id) => ({ CF$UID: BigInt(id) });
67
+ const classDef = (name) => ({ $classes: [name, "NSObject"], $classname: name });
68
+
69
+ function archive(hex) {
70
+ const rgb = toRgb(hex).join(" ");
71
+ return Buffer.from(
72
+ plist({
73
+ $archiver: "NSKeyedArchiver",
74
+ $objects: [
75
+ "$null",
76
+ {
77
+ $class: uid(4),
78
+ NSColorSpace: 1n,
79
+ NSComponents: Buffer.from(`${rgb} 1`),
80
+ NSCustomColorSpace: uid(2),
81
+ NSRGB: Buffer.from(`${rgb}\0`),
82
+ },
83
+ { $class: uid(3), NSID: 7n },
84
+ classDef("NSColorSpace"),
85
+ classDef("NSColor"),
86
+ ],
87
+ $top: { root: uid(1) },
88
+ $version: 100_000n,
89
+ }),
90
+ );
91
+ }
92
+
93
+ const terminal = (s, name) => ({
94
+ name,
95
+ type: "Window Settings",
96
+ ProfileCurrentVersion: 2.09,
97
+ BackgroundColor: archive(s.background),
98
+ TextColor: archive(s.foreground),
99
+ TextBoldColor: archive(s.foreground),
100
+ CursorColor: archive(s.cursor),
101
+ SelectionColor: archive(s.selection),
102
+ ...Object.fromEntries(
103
+ s.ansi.map((hex, i) => [
104
+ `ANSI${i < 8 ? "" : "Bright"}${ANSI[i % 8]}Color`,
105
+ archive(hex),
106
+ ]),
107
+ ),
108
+ });
109
+
110
+ const TARGETS = [
111
+ { dir: "terminal/iterm2", extension: "itermcolors", format: iterm },
112
+ { dir: "terminal/macos", extension: "terminal", format: terminal },
113
+ ];
114
+
115
+ await rm(new URL("terminal", root), { recursive: true, force: true });
116
+ for (const { dir } of TARGETS)
117
+ await mkdir(new URL(dir, root), { recursive: true });
118
+
119
+ const pkg = JSON.parse(await readFile(new URL("package.json", root)));
120
+ for (const { label, path } of pkg.contributes.themes) {
121
+ const { colors } = JSON.parse(await readFile(new URL(path, root)));
122
+ const s = scheme(colors);
123
+ for (const { dir, extension, format } of TARGETS)
124
+ await writeFile(
125
+ new URL(`${dir}/${label}.${extension}`, root),
126
+ plist(format(s, label)),
127
+ );
128
+ }
129
+ console.log(
130
+ `${pkg.contributes.themes.length * TARGETS.length} terminal themes written`,
131
+ );
package/src/accents.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { fit } from "./color.js";
2
+ import { CHROMA } from "./palettes.js";
3
+
1
4
  /** @typedef {import("./palettes.js").Palette} Palette */
2
5
 
3
6
  /**
@@ -15,7 +18,8 @@
15
18
 
16
19
  /**
17
20
  * @typedef {object} Accent
18
- * @property {string} focus Focus border, cursor, progress, links.
21
+ * @property {string} focus Focus border, progress, links.
22
+ * @property {string} caret Editor and terminal cursors.
19
23
  * @property {ActiveItems} active
20
24
  * @property {Pair} statusBar
21
25
  * @property {Pair} debugging
@@ -29,6 +33,9 @@ const fill = (p, hue) => ({
29
33
  background: p.fill[hue],
30
34
  foreground: p.onFill[hue],
31
35
  });
36
+ // WCAG 2.1 non-text contrast, on both surfaces a cursor can blink on.
37
+ const caret = (p, hue) =>
38
+ fit(p.fill[hue], [p.bg.editor, p.bg.chrome], 3, { chroma: CHROMA });
32
39
  const chrome = (p) => ({ background: p.bg.chrome, foreground: p.fg.muted });
33
40
  const active = (p, activity, panel = activity) => ({
34
41
  tab: p.ink.red,
@@ -42,12 +49,13 @@ const active = (p, activity, panel = activity) => ({
42
49
  * @property {function(Palette): Accent} accent
43
50
  */
44
51
 
45
- /** @type {Object<string, AccentStyle>} */
52
+ /** @type {Record<string, AccentStyle>} */
46
53
  export const accents = {
47
54
  blocks: {
48
55
  name: "Blocks",
49
56
  accent: (p) => ({
50
57
  focus: p.ink.red,
58
+ caret: caret(p, "red"),
51
59
  active: active(p, "red"),
52
60
  statusBar: fill(p, "red"),
53
61
  debugging: fill(p, "yellow"),
@@ -61,6 +69,7 @@ export const accents = {
61
69
  name: "Restrained",
62
70
  accent: (p) => ({
63
71
  focus: p.ink.red,
72
+ caret: caret(p, "red"),
64
73
  active: active(p, "red"),
65
74
  statusBar: chrome(p),
66
75
  debugging: fill(p, "red"),
@@ -74,6 +83,7 @@ export const accents = {
74
83
  name: "Tricolor",
75
84
  accent: (p) => ({
76
85
  focus: p.ink.red,
86
+ caret: caret(p, "red"),
77
87
  active: active(p, "yellow", "blue"),
78
88
  statusBar: fill(p, "blue"),
79
89
  debugging: fill(p, "red"),
package/src/color.js CHANGED
@@ -9,8 +9,7 @@ import {
9
9
  toOklch as colorToOklch,
10
10
  } from "pex-color";
11
11
 
12
- const parse = (hex) => fromHex(create(), hex);
13
- const format = (color) => toHex(color).toLowerCase();
12
+ const toLowerCaseHex = (color) => toHex(color).toLowerCase();
14
13
  const lerp = (a, b, t) => a + (b - a) * t;
15
14
 
16
15
  /**
@@ -21,7 +20,7 @@ const lerp = (a, b, t) => a + (b - a) * t;
21
20
  */
22
21
  export function luminance(hex) {
23
22
  // WCAG 2.0's 0.03928 linearization threshold and sRGB's 0.04045 give the same result on 8-bit channels.
24
- const [r, g, b] = toLinear(parse(hex));
23
+ const [r, g, b] = toLinear(fromHex([], hex));
25
24
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
26
25
  }
27
26
 
@@ -37,19 +36,25 @@ export function contrast(a, b) {
37
36
  return (hi + 0.05) / (lo + 0.05);
38
37
  }
39
38
 
39
+ /**
40
+ * Convert to sRGB channels.
41
+ *
42
+ * @param {string} hex
43
+ * @returns {[number, number, number]} Red, green, blue from 0 to 1.
44
+ */
45
+ export const toRgb = (hex) => fromHex([], hex);
46
+
40
47
  /**
41
48
  * Convert to OKLCH.
42
49
  *
43
50
  * @param {string} hex
44
51
  * @returns {[number, number, number]} Lightness, chroma, hue in turns.
45
52
  */
46
- export const toOklch = (hex) => colorToOklch(parse(hex)).slice(0, 3);
53
+ export const toOklch = (hex) => colorToOklch(fromHex([], hex));
47
54
 
48
55
  // Tolerance applies to linear values: gamma encoding would make it ~13x stricter near 0.
49
56
  const inGamut = (color) =>
50
- toLinear(color)
51
- .slice(0, 3)
52
- .every((v) => v >= -1e-4 && v <= 1 + 1e-4);
57
+ toLinear(color).every((v) => v >= -1e-4 && v <= 1 + 1e-4);
53
58
 
54
59
  /**
55
60
  * Convert from OKLCH, reducing chroma until the color fits in sRGB.
@@ -58,9 +63,9 @@ const inGamut = (color) =>
58
63
  * @returns {string}
59
64
  */
60
65
  export function fromOklch([L, C, H]) {
61
- const color = create();
66
+ const color = [];
62
67
  while (!inGamut(colorFromOklch(color, L, C, H))) C = Math.max(0, C - 0.002);
63
- return format(color);
68
+ return toLowerCaseHex(color);
64
69
  }
65
70
 
66
71
  /**
@@ -72,10 +77,10 @@ export function fromOklch([L, C, H]) {
72
77
  * @returns {string}
73
78
  */
74
79
  export function mix(a, b, t = 0.5) {
75
- const [La, Aa, Ba] = toOklab(parse(a));
76
- const [Lb, Ab, Bb] = toOklab(parse(b));
77
- return format(
78
- fromOklab(create(), lerp(La, Lb, t), lerp(Aa, Ab, t), lerp(Ba, Bb, t)),
80
+ const [La, Aa, Ba] = toOklab(fromHex([], a));
81
+ const [Lb, Ab, Bb] = toOklab(fromHex([], b));
82
+ return toLowerCaseHex(
83
+ fromOklab([], lerp(La, Lb, t), lerp(Aa, Ab, t), lerp(Ba, Bb, t)),
79
84
  );
80
85
  }
81
86
 
@@ -156,7 +161,21 @@ export const mostContrasted = (bg, candidates) =>
156
161
  * @returns {string} #RRGGBBAA, or #RRGGBB when opaque.
157
162
  */
158
163
  export function alpha(hex, opacity) {
159
- const color = parse(hex);
164
+ const color = fromHex([], hex);
160
165
  color[3] = opacity;
161
- return format(color);
166
+ return toLowerCaseHex(color);
167
+ }
168
+
169
+ /**
170
+ * Composite a translucent color over an opaque background, in sRGB as browsers
171
+ * do.
172
+ *
173
+ * @param {string} hex #RRGGBBAA or #RRGGBB.
174
+ * @param {string} bg
175
+ * @returns {string} #RRGGBB.
176
+ */
177
+ export function flatten(hex, bg) {
178
+ const [r, g, b, a] = fromHex(create(), hex);
179
+ const [R, G, B] = fromHex([], bg);
180
+ return toLowerCaseHex([lerp(R, r, a), lerp(G, g, a), lerp(B, b, a), 1]);
162
181
  }
package/src/palettes.js CHANGED
@@ -34,7 +34,7 @@ const mapValues = (object, fn) =>
34
34
  // keeps the tint.
35
35
  // Light surfaces have no source match: a paper tint at the same hue.
36
36
  // Lines (borders, guides, selected rows) keep the grey-40 on grey-22 step: 1.53:1.
37
- const [, , WARM_HUE] = toOklch(black);
37
+ const WARM_HUE = toOklch(black)[2];
38
38
  const darkEditor = fit(black, black, 1.23);
39
39
  const lightEditor = fromOklch([0.945, 0.015, WARM_HUE]);
40
40
 
@@ -58,7 +58,8 @@ const SURFACES = {
58
58
  const AA = 4.5;
59
59
  const AAA = 7;
60
60
  // Source blue, green and cyan are greyish (OKLCH chroma ~0.07): too close to the text color once dark.
61
- const INK = { exact: true, chroma: 1.6 };
61
+ export const CHROMA = 1.6;
62
+ const INK = { exact: true, chroma: CHROMA };
62
63
 
63
64
  /**
64
65
  * @typedef {object} Palette
@@ -66,7 +67,8 @@ const INK = { exact: true, chroma: 1.6 };
66
67
  * @property {"dark" | "light"} type
67
68
  * @property {Backgrounds} bg
68
69
  * @property {Foregrounds} fg
69
- * @property {Hues} ink Hues for text and marks on backgrounds.
70
+ * @property {Hues} ink Hues for text on backgrounds.
71
+ * @property {Hues} ansi Terminal hues.
70
72
  * @property {Hues} bright Terminal bright variants.
71
73
  * @property {Fills} fill Hues as solid surfaces.
72
74
  * @property {Fills} onFill Text on each fill.
@@ -118,6 +120,7 @@ function source(type) {
118
120
  bg: { editor, chrome, line },
119
121
  fg: { default: text, muted },
120
122
  ink: HUES,
123
+ ansi: HUES,
121
124
  bright: mapValues(HUES, (hue) => mix(hue, text, 0.3)),
122
125
  fill: FILLS,
123
126
  onFill: mapValues(FILLS, (fill) => mostContrasted(fill, [white, black])),
@@ -127,7 +130,8 @@ function source(type) {
127
130
  /**
128
131
  * Source hues, saturated, at the lightness closest to the backgrounds that
129
132
  * reaches WCAG 2.0 AA against editor and chrome. Equal contrast keeps hues
130
- * apart from the text color on both sides.
133
+ * apart from the text color on both sides. Terminal hues reach AA (bright: AAA)
134
+ * against the terminal background only.
131
135
  *
132
136
  * @param {"dark" | "light"} type
133
137
  * @returns {Palette}
@@ -139,7 +143,8 @@ function wcag(type) {
139
143
  ...base,
140
144
  fg: { ...base.fg, muted: fit(base.fg.muted, backgrounds, AA) },
141
145
  ink: mapValues(HUES, (hue) => fit(hue, backgrounds, AA, INK)),
142
- bright: mapValues(HUES, (hue) => fit(hue, backgrounds, AAA, INK)),
146
+ ansi: mapValues(HUES, (hue) => fit(hue, base.bg.chrome, AA, INK)),
147
+ bright: mapValues(HUES, (hue) => fit(hue, base.bg.chrome, AAA, INK)),
143
148
  };
144
149
  }
145
150
 
package/src/plist.js ADDED
@@ -0,0 +1,42 @@
1
+ const escape = (text) =>
2
+ text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3
+
4
+ function node(value, depth) {
5
+ const pad = "\t".repeat(depth);
6
+ if (Buffer.isBuffer(value))
7
+ return `${pad}<data>${value.toString("base64")}</data>`;
8
+ if (Array.isArray(value))
9
+ return [
10
+ `${pad}<array>`,
11
+ ...value.map((item) => node(item, depth + 1)),
12
+ `${pad}</array>`,
13
+ ].join("\n");
14
+ if (typeof value === "object")
15
+ return [
16
+ `${pad}<dict>`,
17
+ ...Object.entries(value).flatMap(([key, item]) => [
18
+ `${pad}\t<key>${escape(key)}</key>`,
19
+ node(item, depth + 1),
20
+ ]),
21
+ `${pad}</dict>`,
22
+ ].join("\n");
23
+ if (typeof value === "boolean") return `${pad}<${value}/>`;
24
+ if (typeof value === "bigint") return `${pad}<integer>${value}</integer>`;
25
+ return typeof value === "number"
26
+ ? `${pad}<real>${value}</real>`
27
+ : `${pad}<string>${escape(String(value))}</string>`;
28
+ }
29
+
30
+ /**
31
+ * Serialize to an XML property list.
32
+ *
33
+ * @param {unknown} value Objects become dicts, Buffers become data, BigInts
34
+ * become integers and numbers become reals.
35
+ * @returns {string}
36
+ */
37
+ export const plist = (value) => `<?xml version="1.0" encoding="UTF-8"?>
38
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
39
+ <plist version="1.0">
40
+ ${node(value, 0)}
41
+ </plist>
42
+ `;
package/src/syntax.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * tags, constants) → violet, other hues keep their role.
7
7
  *
8
8
  * @param {Palette} p
9
+ * @returns {Record<string, string>}
9
10
  */
10
11
  export const roles = (p) => ({
11
12
  text: p.fg.default,
@@ -43,6 +44,7 @@ const jsonKeys = (colors) =>
43
44
  * TextMate rules. Later rules win on equal specificity.
44
45
  *
45
46
  * @param {Palette} p
47
+ * @returns {object[]}
46
48
  */
47
49
  export function tokenColors(p) {
48
50
  const r = roles(p);
@@ -251,6 +253,7 @@ export function tokenColors(p) {
251
253
  * mapping.
252
254
  *
253
255
  * @param {Palette} p
256
+ * @returns {Record<string, string | object>}
254
257
  */
255
258
  export function semanticTokenColors(p) {
256
259
  const r = roles(p);