@silvery/theme 0.3.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.
Files changed (47) hide show
  1. package/package.json +36 -0
  2. package/src/ThemeContext.tsx +62 -0
  3. package/src/alias.ts +94 -0
  4. package/src/auto-generate.ts +126 -0
  5. package/src/builder.ts +218 -0
  6. package/src/cli.ts +275 -0
  7. package/src/color.ts +142 -0
  8. package/src/contrast.ts +75 -0
  9. package/src/css.ts +51 -0
  10. package/src/derive.ts +167 -0
  11. package/src/detect.ts +263 -0
  12. package/src/export/base16.ts +64 -0
  13. package/src/generate.ts +79 -0
  14. package/src/generators.ts +255 -0
  15. package/src/import/base16.ts +150 -0
  16. package/src/import/types.ts +47 -0
  17. package/src/index.ts +2 -0
  18. package/src/palettes/ayu.ts +92 -0
  19. package/src/palettes/catppuccin.ts +118 -0
  20. package/src/palettes/dracula.ts +34 -0
  21. package/src/palettes/edge.ts +63 -0
  22. package/src/palettes/everforest.ts +63 -0
  23. package/src/palettes/gruvbox.ts +62 -0
  24. package/src/palettes/horizon.ts +35 -0
  25. package/src/palettes/index.ts +293 -0
  26. package/src/palettes/kanagawa.ts +91 -0
  27. package/src/palettes/material.ts +64 -0
  28. package/src/palettes/modus.ts +63 -0
  29. package/src/palettes/monokai.ts +64 -0
  30. package/src/palettes/moonfly.ts +35 -0
  31. package/src/palettes/nightfly.ts +35 -0
  32. package/src/palettes/nightfox.ts +63 -0
  33. package/src/palettes/nord.ts +34 -0
  34. package/src/palettes/one-dark.ts +34 -0
  35. package/src/palettes/oxocarbon.ts +63 -0
  36. package/src/palettes/palenight.ts +36 -0
  37. package/src/palettes/rose-pine.ts +90 -0
  38. package/src/palettes/snazzy.ts +35 -0
  39. package/src/palettes/solarized.ts +62 -0
  40. package/src/palettes/sonokai.ts +35 -0
  41. package/src/palettes/tokyo-night.ts +90 -0
  42. package/src/resolve.ts +44 -0
  43. package/src/state.ts +58 -0
  44. package/src/theme.ts +148 -0
  45. package/src/types.ts +223 -0
  46. package/src/validate-theme.ts +100 -0
  47. package/src/validate.ts +56 -0
package/src/detect.ts ADDED
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Terminal palette auto-detection via OSC queries.
3
+ *
4
+ * Detects the terminal's actual colors by querying:
5
+ * - OSC 10: foreground (text) color
6
+ * - OSC 11: background color
7
+ * - OSC 4;0–15: ANSI 16 palette colors
8
+ *
9
+ * Then maps the detected colors to a partial ColorPalette for
10
+ * theme generation via createTheme() or deriveTheme().
11
+ *
12
+ * Supported by: Ghostty, Kitty, WezTerm, iTerm2, foot, Alacritty, xterm
13
+ * NOT supported by: tmux (blocks OSC), basic xterm, CI environments
14
+ */
15
+
16
+ import type { ColorPalette, Theme } from "./types"
17
+ import { deriveTheme } from "./derive"
18
+ import { nord } from "./palettes/nord"
19
+ import { catppuccinLatte } from "./palettes/catppuccin"
20
+
21
+ // silvery is an optional peer dependency — lazy-import to avoid breaking
22
+ // standalone consumers that don't have silvery installed.
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ let _silvery: any = null
25
+ async function getSilvery() {
26
+ if (!_silvery) {
27
+ try {
28
+ const mod = "@silvery/react"
29
+ _silvery = await import(mod)
30
+ } catch {
31
+ throw new Error("Terminal palette detection requires '@silvery/term' to be installed")
32
+ }
33
+ }
34
+ return _silvery as {
35
+ queryBackgroundColor: (
36
+ write: (s: string) => void,
37
+ read: (ms: number) => Promise<string | null>,
38
+ timeout: number,
39
+ ) => Promise<string | null>
40
+ queryForegroundColor: (
41
+ write: (s: string) => void,
42
+ read: (ms: number) => Promise<string | null>,
43
+ timeout: number,
44
+ ) => Promise<string | null>
45
+ queryMultiplePaletteColors: (indices: number[], write: (s: string) => void) => void
46
+ parsePaletteResponse: (chunk: string) => { index: number; color: string } | null
47
+ }
48
+ }
49
+
50
+ /** Result of terminal palette detection. */
51
+ export interface DetectedPalette {
52
+ /** Terminal foreground color (#RRGGBB), or null if undetected */
53
+ fg: string | null
54
+ /** Terminal background color (#RRGGBB), or null if undetected */
55
+ bg: string | null
56
+ /** ANSI 16 palette colors (index 0-15), null entries = undetected */
57
+ ansi: (string | null)[]
58
+ /** Whether the terminal appears to be dark mode (from bg luminance) */
59
+ dark: boolean
60
+ /** Partial ColorPalette derived from detected colors */
61
+ palette: Partial<ColorPalette>
62
+ }
63
+
64
+ /**
65
+ * Detect the terminal's color palette via OSC queries.
66
+ *
67
+ * Must be called BEFORE entering alternate screen / fullscreen mode,
68
+ * as some terminals don't respond to OSC in alternate screen.
69
+ *
70
+ * @param timeoutMs How long to wait for each response (default 150ms)
71
+ * @returns Detected palette, or null if terminal doesn't support OSC queries
72
+ */
73
+ export async function detectTerminalPalette(timeoutMs = 150): Promise<DetectedPalette | null> {
74
+ const stdin = process.stdin
75
+ const stdout = process.stdout
76
+
77
+ if (!stdin.isTTY || !stdout.isTTY) return null
78
+
79
+ const wasRaw = stdin.isRaw
80
+ if (!wasRaw) stdin.setRawMode(true)
81
+
82
+ // Buffer for collecting responses
83
+ let buffer = ""
84
+ const onData = (chunk: Buffer) => {
85
+ buffer += chunk.toString()
86
+ }
87
+ stdin.on("data", onData)
88
+
89
+ try {
90
+ const write = (s: string) => {
91
+ stdout.write(s)
92
+ }
93
+
94
+ const read = (ms: number): Promise<string | null> =>
95
+ new Promise((resolve) => {
96
+ if (buffer.length > 0) {
97
+ const result = buffer
98
+ buffer = ""
99
+ resolve(result)
100
+ return
101
+ }
102
+
103
+ const timer = setTimeout(() => {
104
+ resolve(buffer.length > 0 ? buffer : null)
105
+ buffer = ""
106
+ }, ms)
107
+
108
+ const check = (_chunk: Buffer) => {
109
+ clearTimeout(timer)
110
+ stdin.removeListener("data", check)
111
+ const result = buffer
112
+ buffer = ""
113
+ resolve(result)
114
+ }
115
+ stdin.on("data", check)
116
+ })
117
+
118
+ const silvery = await getSilvery()
119
+
120
+ // Query bg and fg first
121
+ const bg = await silvery.queryBackgroundColor(write, read, timeoutMs)
122
+ const fg = await silvery.queryForegroundColor(write, read, timeoutMs)
123
+
124
+ // Query ANSI 16 palette
125
+ const ansi: (string | null)[] = new Array(16).fill(null)
126
+ silvery.queryMultiplePaletteColors(
127
+ Array.from({ length: 16 }, (_, i) => i),
128
+ write,
129
+ )
130
+
131
+ // Wait for responses
132
+ await new Promise((resolve) => setTimeout(resolve, timeoutMs))
133
+
134
+ // Parse any buffered palette responses
135
+ const remaining = buffer
136
+ buffer = ""
137
+ if (remaining) {
138
+ const oscPrefix = "\x1b]4;"
139
+ let pos = 0
140
+ while (pos < remaining.length) {
141
+ const nextOsc = remaining.indexOf(oscPrefix, pos)
142
+ if (nextOsc === -1) break
143
+
144
+ let end = remaining.indexOf("\x07", nextOsc)
145
+ if (end === -1) end = remaining.indexOf("\x1b\\", nextOsc)
146
+ if (end === -1) break
147
+
148
+ const chunk = remaining.slice(nextOsc, end + 1)
149
+ const parsed = silvery.parsePaletteResponse(chunk)
150
+ if (parsed && parsed.index >= 0 && parsed.index < 16) {
151
+ ansi[parsed.index] = parsed.color
152
+ }
153
+ pos = end + 1
154
+ }
155
+ }
156
+
157
+ // Determine dark/light from bg
158
+ const dark = bg ? isDarkColor(bg) : true
159
+
160
+ // Build partial ColorPalette from detected colors
161
+ const palette: Partial<ColorPalette> = { dark }
162
+
163
+ if (bg) palette.background = bg
164
+ if (fg) palette.foreground = fg
165
+
166
+ // Map ANSI 16 indices to ColorPalette fields
167
+ const ansiFields: (keyof ColorPalette)[] = [
168
+ "black",
169
+ "red",
170
+ "green",
171
+ "yellow",
172
+ "blue",
173
+ "magenta",
174
+ "cyan",
175
+ "white",
176
+ "brightBlack",
177
+ "brightRed",
178
+ "brightGreen",
179
+ "brightYellow",
180
+ "brightBlue",
181
+ "brightMagenta",
182
+ "brightCyan",
183
+ "brightWhite",
184
+ ]
185
+ for (let i = 0; i < 16; i++) {
186
+ if (ansi[i]) {
187
+ ;(palette as Record<string, string>)[ansiFields[i]!] = ansi[i]!
188
+ }
189
+ }
190
+
191
+ // Derive special colors from detected values
192
+ if (fg) {
193
+ palette.cursorColor = fg
194
+ palette.selectionForeground = fg
195
+ }
196
+ if (bg) {
197
+ palette.cursorText = bg
198
+ }
199
+ if (ansi[4]) {
200
+ // Selection background from blue at 30% on bg
201
+ palette.selectionBackground = ansi[4]
202
+ }
203
+
204
+ return { fg, bg, ansi, dark, palette }
205
+ } finally {
206
+ stdin.removeListener("data", onData)
207
+ if (!wasRaw) stdin.setRawMode(false)
208
+ }
209
+ }
210
+
211
+ // ============================================================================
212
+ // detectTheme — high-level: detect terminal palette, fill gaps, derive theme
213
+ // ============================================================================
214
+
215
+ export interface DetectThemeOptions {
216
+ /** Fallback ColorPalette when detection fails or returns partial data.
217
+ * Detected colors override matching fallback fields. */
218
+ fallback?: ColorPalette
219
+ /** Timeout per OSC query in ms (default 150). */
220
+ timeoutMs?: number
221
+ }
222
+
223
+ /**
224
+ * Detect the terminal's color palette and derive a Theme.
225
+ *
226
+ * Queries the terminal via OSC, fills gaps from `fallback` palette,
227
+ * and runs `deriveTheme()` to produce a complete 33-token Theme.
228
+ *
229
+ * Falls back entirely to the fallback palette (or Nord dark) if
230
+ * detection fails (e.g., not a TTY, tmux, CI).
231
+ */
232
+ export async function detectTheme(opts: DetectThemeOptions = {}): Promise<Theme> {
233
+ const detected = await detectTerminalPalette(opts.timeoutMs)
234
+ const isDark = detected?.dark ?? true
235
+ const fallback = opts.fallback ?? (isDark ? nord : catppuccinLatte)
236
+
237
+ if (!detected) {
238
+ // Detection failed entirely — use fallback
239
+ return deriveTheme(fallback)
240
+ }
241
+
242
+ // Merge: detected colors override fallback
243
+ const merged: ColorPalette = { ...fallback, ...stripNulls(detected.palette) }
244
+ return deriveTheme(merged)
245
+ }
246
+
247
+ /** Strip null/undefined values from a partial palette so they don't override fallback. */
248
+ function stripNulls(partial: Partial<ColorPalette>): Partial<ColorPalette> {
249
+ const result: Record<string, unknown> = {}
250
+ for (const [k, v] of Object.entries(partial)) {
251
+ if (v != null) result[k] = v
252
+ }
253
+ return result as Partial<ColorPalette>
254
+ }
255
+
256
+ /** Check if a #RRGGBB color is dark (luminance <= 0.5). */
257
+ function isDarkColor(hex: string): boolean {
258
+ const r = parseInt(hex.slice(1, 3), 16) / 255
259
+ const g = parseInt(hex.slice(3, 5), 16) / 255
260
+ const b = parseInt(hex.slice(5, 7), 16) / 255
261
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
262
+ return luminance <= 0.5
263
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Export ColorPalette to Base16 YAML format.
3
+ *
4
+ * Maps ColorPalette fields to base00–base0F. For fields that
5
+ * ColorPalette doesn't store directly (base04/base06/base07),
6
+ * we interpolate between neighboring values.
7
+ *
8
+ * @see https://github.com/chriskempson/base16
9
+ */
10
+
11
+ import { blend } from "../color"
12
+ import type { ColorPalette } from "../types"
13
+
14
+ /** Strip `#` prefix from a hex color string and uppercase. */
15
+ function stripHash(hex: string): string {
16
+ const bare = hex.startsWith("#") ? hex.slice(1) : hex
17
+ return bare.toUpperCase()
18
+ }
19
+
20
+ /**
21
+ * Export a ColorPalette to Base16 YAML format.
22
+ *
23
+ * Mapping:
24
+ * background → base00, brightBlack → base01, selectionBackground → base02,
25
+ * white → base03, (interpolated) → base04, foreground → base05,
26
+ * (interpolated) → base06, (interpolated) → base07,
27
+ * red → base08, brightRed → base09, yellow → base0A, green → base0B,
28
+ * cyan → base0C, blue → base0D, magenta → base0E, brightMagenta → base0F.
29
+ */
30
+ export function exportBase16(palette: ColorPalette): string {
31
+ const dark = palette.dark ?? true
32
+
33
+ // base04: between white (base03/muted fg) and foreground (base05)
34
+ const base04 = blend(palette.white, palette.foreground, 0.33)
35
+
36
+ // base06: light foreground — between fg and the inverse extreme
37
+ const base06 = dark ? blend(palette.foreground, "#FFFFFF", 0.15) : blend(palette.foreground, "#000000", 0.15)
38
+
39
+ // base07: lightest background — use black (the deepest bg extreme)
40
+ const base07 = palette.black
41
+
42
+ const lines = [
43
+ `scheme: "${palette.name ?? "exported"}"`,
44
+ `author: ""`,
45
+ `base00: "${stripHash(palette.background)}"`,
46
+ `base01: "${stripHash(palette.brightBlack)}"`,
47
+ `base02: "${stripHash(palette.selectionBackground)}"`,
48
+ `base03: "${stripHash(palette.white)}"`,
49
+ `base04: "${stripHash(base04)}"`,
50
+ `base05: "${stripHash(palette.foreground)}"`,
51
+ `base06: "${stripHash(base06)}"`,
52
+ `base07: "${stripHash(base07)}"`,
53
+ `base08: "${stripHash(palette.red)}"`,
54
+ `base09: "${stripHash(palette.brightRed)}"`,
55
+ `base0A: "${stripHash(palette.yellow)}"`,
56
+ `base0B: "${stripHash(palette.green)}"`,
57
+ `base0C: "${stripHash(palette.cyan)}"`,
58
+ `base0D: "${stripHash(palette.blue)}"`,
59
+ `base0E: "${stripHash(palette.magenta)}"`,
60
+ `base0F: "${stripHash(palette.brightMagenta)}"`,
61
+ ]
62
+
63
+ return lines.join("\n") + "\n"
64
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * ANSI 16 theme generation — derives a complete Theme from a primary color + dark/light.
3
+ *
4
+ * Uses ANSI color names (not hex) so it works on any terminal without truecolor support.
5
+ */
6
+
7
+ import type { AnsiPrimary, Theme } from "./types"
8
+
9
+ /**
10
+ * Generate a complete ANSI 16 theme from a primary color + dark/light preference.
11
+ *
12
+ * All token values are ANSI color names (e.g. "yellow", "blueBright").
13
+ */
14
+ export function generateTheme(primary: AnsiPrimary, dark: boolean): Theme {
15
+ return {
16
+ name: `${dark ? "dark" : "light"}-${primary}`,
17
+
18
+ // ── Root pair ─────────────────────────────────────────────────
19
+ bg: "",
20
+ fg: dark ? "whiteBright" : "black",
21
+
22
+ // ── Surface pairs (base = text, *bg = background) ──────────
23
+ muted: dark ? "white" : "blackBright",
24
+ mutedbg: dark ? "black" : "white",
25
+ surface: dark ? "whiteBright" : "black",
26
+ surfacebg: dark ? "black" : "white",
27
+ popover: dark ? "whiteBright" : "black",
28
+ popoverbg: dark ? "black" : "white",
29
+ inverse: dark ? "black" : "whiteBright",
30
+ inversebg: dark ? "whiteBright" : "black",
31
+ cursor: "black",
32
+ cursorbg: primary,
33
+ selection: "black",
34
+ selectionbg: primary,
35
+
36
+ // ── Accent pairs (base = area bg, *fg = text on area) ──────
37
+ primary,
38
+ primaryfg: "black",
39
+ secondary: primary,
40
+ secondaryfg: "black",
41
+ accent: primary,
42
+ accentfg: "black",
43
+ error: dark ? "redBright" : "red",
44
+ errorfg: "black",
45
+ warning: primary,
46
+ warningfg: "black",
47
+ success: dark ? "greenBright" : "green",
48
+ successfg: "black",
49
+ info: dark ? "cyanBright" : "cyan",
50
+ infofg: "black",
51
+
52
+ // ── Standalone ───────────────────────────────────────────────
53
+ border: "gray",
54
+ inputborder: "gray",
55
+ focusborder: dark ? "blueBright" : "blue",
56
+ link: "blueBright",
57
+ disabledfg: "gray",
58
+
59
+ // ── Palette ──────────────────────────────────────────────────
60
+ palette: [
61
+ "black",
62
+ "red",
63
+ "green",
64
+ "yellow",
65
+ "blue",
66
+ "magenta",
67
+ "cyan",
68
+ "white",
69
+ "blackBright",
70
+ "redBright",
71
+ "greenBright",
72
+ "yellowBright",
73
+ "blueBright",
74
+ "magentaBright",
75
+ "cyanBright",
76
+ "whiteBright",
77
+ ],
78
+ }
79
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Palette generators — produce a ColorPalette from various inputs.
3
+ *
4
+ * All generators return a complete ColorPalette (22 fields).
5
+ */
6
+
7
+ import { hexToRgb, hexToHsl, hslToHex, brighten, darken, blend } from "./color"
8
+ import { importBase16 as importBase16Internal } from "./import/base16"
9
+ import { getPaletteByName } from "./palettes/index"
10
+ import type { ColorPalette, HueName } from "./types"
11
+
12
+ // ============================================================================
13
+ // Luminance
14
+ // ============================================================================
15
+
16
+ function luminance(hex: string): number {
17
+ const rgb = hexToRgb(hex)
18
+ if (!rgb) return 0.5
19
+ return (rgb[0] + rgb[1] + rgb[2]) / (255 * 3)
20
+ }
21
+
22
+ function isDarkColor(hex: string): boolean {
23
+ return luminance(hex) < 0.5
24
+ }
25
+
26
+ // ============================================================================
27
+ // Accent Generation
28
+ // ============================================================================
29
+
30
+ /** Target hues for each accent slot. */
31
+ const targetHues: Record<HueName, number> = {
32
+ red: 0,
33
+ orange: 30,
34
+ yellow: 60,
35
+ green: 120,
36
+ teal: 180,
37
+ blue: 220,
38
+ purple: 280,
39
+ pink: 330,
40
+ }
41
+
42
+ /** Find which hue slot the primary color best matches by hue angle proximity. */
43
+ export function assignPrimaryToSlot(primary: string): HueName {
44
+ const hsl = hexToHsl(primary)
45
+ if (!hsl) return "blue"
46
+ const h = hsl[0]
47
+ const slots: [number, number, HueName][] = [
48
+ [0, 15, "red"],
49
+ [15, 45, "orange"],
50
+ [45, 75, "yellow"],
51
+ [75, 165, "green"],
52
+ [165, 200, "teal"],
53
+ [200, 260, "blue"],
54
+ [260, 310, "purple"],
55
+ [310, 345, "pink"],
56
+ [345, 360, "red"],
57
+ ]
58
+ for (const [lo, hi, name] of slots) {
59
+ if (h >= lo && h < hi) return name
60
+ }
61
+ return "blue"
62
+ }
63
+
64
+ /** Generate 8 accent hues from a primary, placing it in its natural slot. */
65
+ function generateAccentsFromPrimary(primary: string): Record<HueName, string> {
66
+ const hsl = hexToHsl(primary)
67
+ if (!hsl) {
68
+ return {
69
+ red: "#BF616A",
70
+ orange: "#D08770",
71
+ yellow: "#EBCB8B",
72
+ green: "#A3BE8C",
73
+ teal: "#88C0D0",
74
+ blue: "#5E81AC",
75
+ purple: "#B48EAD",
76
+ pink: "#D4879C",
77
+ }
78
+ }
79
+ const [, s, l] = hsl
80
+ const slot = assignPrimaryToSlot(primary)
81
+ const result = {} as Record<HueName, string>
82
+ for (const [name, targetH] of Object.entries(targetHues) as [HueName, number][]) {
83
+ result[name] = name === slot ? primary : hslToHex(targetH, s, l)
84
+ }
85
+ return result
86
+ }
87
+
88
+ // ============================================================================
89
+ // fromBase16 — Base16 YAML → ColorPalette
90
+ // ============================================================================
91
+
92
+ /**
93
+ * Generate a ColorPalette from a Base16 YAML scheme.
94
+ *
95
+ * Maps base00–base0F to ANSI palette colors, derives special colors.
96
+ */
97
+ export function fromBase16(yamlOrJson: string): ColorPalette {
98
+ return importBase16Internal(yamlOrJson)
99
+ }
100
+
101
+ // ============================================================================
102
+ // fromColors — Generate full palette from 1-3 hex colors
103
+ // ============================================================================
104
+
105
+ interface FromColorsOptions {
106
+ /** Background color (infers dark/light). */
107
+ background?: string
108
+ /** Foreground/text color. Generated if omitted. */
109
+ foreground?: string
110
+ /** Primary accent color. Generated if omitted. */
111
+ primary?: string
112
+ /** Force dark mode. */
113
+ dark?: boolean
114
+ /** Theme name. */
115
+ name?: string
116
+ }
117
+
118
+ /**
119
+ * Generate a full ColorPalette from 1-3 hex colors.
120
+ *
121
+ * At minimum, provide `background` or `primary`. Missing colors are
122
+ * generated via surface ramp (from bg) and hue rotation (from primary).
123
+ */
124
+ export function fromColors(opts: FromColorsOptions): ColorPalette {
125
+ const dark = opts.dark ?? (opts.background ? isDarkColor(opts.background) : true)
126
+ const step = dark ? brighten : darken
127
+
128
+ // Generate background if not provided
129
+ const bg = opts.background ?? (dark ? "#2E3440" : "#FFFFFF")
130
+ const fg = opts.foreground ?? step(bg, 0.85)
131
+
132
+ // Generate accents from primary or defaults
133
+ const accents = opts.primary
134
+ ? generateAccentsFromPrimary(opts.primary)
135
+ : {
136
+ red: "#BF616A",
137
+ orange: "#D08770",
138
+ yellow: "#EBCB8B",
139
+ green: "#A3BE8C",
140
+ teal: "#88C0D0",
141
+ blue: "#5E81AC",
142
+ purple: "#B48EAD",
143
+ pink: "#D4879C",
144
+ }
145
+
146
+ // Surface ramp for grayscale ANSI colors
147
+ const black = dark ? darken(bg, 0.05) : darken(bg, 0.1)
148
+ const white = dark ? blend(fg, bg, 0.3) : blend(bg, fg, 0.3)
149
+ const brightBlack = step(bg, 0.15)
150
+ const brightWhite = dark ? fg : brighten(fg, 0.1)
151
+
152
+ return {
153
+ name: opts.name ?? (dark ? "generated-dark" : "generated-light"),
154
+ dark,
155
+ black,
156
+ red: accents.red,
157
+ green: accents.green,
158
+ yellow: accents.yellow,
159
+ blue: accents.blue,
160
+ magenta: accents.purple,
161
+ cyan: accents.teal,
162
+ white,
163
+ brightBlack,
164
+ brightRed: accents.orange,
165
+ brightGreen: brighten(accents.green, 0.15),
166
+ brightYellow: brighten(accents.yellow, 0.15),
167
+ brightBlue: brighten(accents.blue, 0.15),
168
+ brightMagenta: accents.pink,
169
+ brightCyan: brighten(accents.teal, 0.15),
170
+ brightWhite,
171
+ foreground: fg,
172
+ background: bg,
173
+ cursorColor: fg,
174
+ cursorText: bg,
175
+ selectionBackground: blend(bg, accents.blue, 0.3),
176
+ selectionForeground: fg,
177
+ }
178
+ }
179
+
180
+ // ============================================================================
181
+ // fromPreset — Look up a built-in ColorPalette by name
182
+ // ============================================================================
183
+
184
+ /**
185
+ * Look up a built-in palette by name.
186
+ *
187
+ * @returns The ColorPalette, or undefined if not found.
188
+ */
189
+ export function fromPreset(name: string): ColorPalette | undefined {
190
+ return getPaletteByName(name)
191
+ }
192
+
193
+ // ============================================================================
194
+ // ThemePalette → ColorPalette conversion (migration helper)
195
+ // ============================================================================
196
+
197
+ /** Old ThemePalette shape for migration. */
198
+ interface OldThemePalette {
199
+ name: string
200
+ dark: boolean
201
+ crust: string
202
+ base: string
203
+ surface: string
204
+ overlay: string
205
+ subtext: string
206
+ text: string
207
+ red: string
208
+ orange: string
209
+ yellow: string
210
+ green: string
211
+ teal: string
212
+ blue: string
213
+ purple: string
214
+ pink: string
215
+ }
216
+
217
+ /**
218
+ * Convert an old ThemePalette to a ColorPalette.
219
+ *
220
+ * Mapping:
221
+ * black = crust, red/green/yellow/blue = direct, magenta = purple,
222
+ * cyan = teal, white = subtext, brightBlack = surface,
223
+ * brightRed = orange, bright{green,yellow,blue,cyan} = brighten(normal),
224
+ * brightMagenta = pink, brightWhite = text,
225
+ * foreground = text, background = base,
226
+ * cursor = text/base, selection = overlay/text.
227
+ */
228
+ export function themePaletteToColorPalette(p: OldThemePalette): ColorPalette {
229
+ return {
230
+ name: p.name,
231
+ dark: p.dark,
232
+ black: p.crust,
233
+ red: p.red,
234
+ green: p.green,
235
+ yellow: p.yellow,
236
+ blue: p.blue,
237
+ magenta: p.purple,
238
+ cyan: p.teal,
239
+ white: p.subtext,
240
+ brightBlack: p.surface,
241
+ brightRed: p.orange,
242
+ brightGreen: brighten(p.green, 0.15),
243
+ brightYellow: brighten(p.yellow, 0.15),
244
+ brightBlue: brighten(p.blue, 0.15),
245
+ brightMagenta: p.pink,
246
+ brightCyan: brighten(p.teal, 0.15),
247
+ brightWhite: p.text,
248
+ foreground: p.text,
249
+ background: p.base,
250
+ cursorColor: p.text,
251
+ cursorText: p.base,
252
+ selectionBackground: p.overlay,
253
+ selectionForeground: p.text,
254
+ }
255
+ }