@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.
- package/package.json +36 -0
- package/src/ThemeContext.tsx +62 -0
- package/src/alias.ts +94 -0
- package/src/auto-generate.ts +126 -0
- package/src/builder.ts +218 -0
- package/src/cli.ts +275 -0
- package/src/color.ts +142 -0
- package/src/contrast.ts +75 -0
- package/src/css.ts +51 -0
- package/src/derive.ts +167 -0
- package/src/detect.ts +263 -0
- package/src/export/base16.ts +64 -0
- package/src/generate.ts +79 -0
- package/src/generators.ts +255 -0
- package/src/import/base16.ts +150 -0
- package/src/import/types.ts +47 -0
- package/src/index.ts +2 -0
- package/src/palettes/ayu.ts +92 -0
- package/src/palettes/catppuccin.ts +118 -0
- package/src/palettes/dracula.ts +34 -0
- package/src/palettes/edge.ts +63 -0
- package/src/palettes/everforest.ts +63 -0
- package/src/palettes/gruvbox.ts +62 -0
- package/src/palettes/horizon.ts +35 -0
- package/src/palettes/index.ts +293 -0
- package/src/palettes/kanagawa.ts +91 -0
- package/src/palettes/material.ts +64 -0
- package/src/palettes/modus.ts +63 -0
- package/src/palettes/monokai.ts +64 -0
- package/src/palettes/moonfly.ts +35 -0
- package/src/palettes/nightfly.ts +35 -0
- package/src/palettes/nightfox.ts +63 -0
- package/src/palettes/nord.ts +34 -0
- package/src/palettes/one-dark.ts +34 -0
- package/src/palettes/oxocarbon.ts +63 -0
- package/src/palettes/palenight.ts +36 -0
- package/src/palettes/rose-pine.ts +90 -0
- package/src/palettes/snazzy.ts +35 -0
- package/src/palettes/solarized.ts +62 -0
- package/src/palettes/sonokai.ts +35 -0
- package/src/palettes/tokyo-night.ts +90 -0
- package/src/resolve.ts +44 -0
- package/src/state.ts +58 -0
- package/src/theme.ts +148 -0
- package/src/types.ts +223 -0
- package/src/validate-theme.ts +100 -0
- package/src/validate.ts +56 -0
package/src/theme.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @silvery/theme — Universal color themes for any platform.
|
|
3
|
+
*
|
|
4
|
+
* Two-layer architecture:
|
|
5
|
+
* Layer 1: ColorPalette (22 terminal colors — universal pivot format)
|
|
6
|
+
* Layer 2: Theme (33 semantic tokens — what UI apps consume)
|
|
7
|
+
*
|
|
8
|
+
* Pipeline: Palette generators → ColorPalette → deriveTheme() → Theme
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createTheme, catppuccinMocha, resolveThemeColor } from "@silvery/theme"
|
|
13
|
+
*
|
|
14
|
+
* const theme = createTheme().preset('catppuccin-mocha').build()
|
|
15
|
+
* const color = resolveThemeColor("$primary", theme) // → "#F9E2AF"
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// React integration
|
|
22
|
+
export { ThemeProvider, useTheme } from "./ThemeContext"
|
|
23
|
+
export type { ThemeProviderProps } from "./ThemeContext"
|
|
24
|
+
|
|
25
|
+
// Core types
|
|
26
|
+
export type { Theme, ColorPalette, HueName, AnsiPrimary, AnsiColorName } from "./types"
|
|
27
|
+
export { COLOR_PALETTE_FIELDS } from "./types"
|
|
28
|
+
|
|
29
|
+
// Derivation
|
|
30
|
+
export { deriveTheme } from "./derive"
|
|
31
|
+
|
|
32
|
+
// Color utilities
|
|
33
|
+
export {
|
|
34
|
+
blend,
|
|
35
|
+
brighten,
|
|
36
|
+
darken,
|
|
37
|
+
contrastFg,
|
|
38
|
+
desaturate,
|
|
39
|
+
complement,
|
|
40
|
+
hexToRgb,
|
|
41
|
+
rgbToHex,
|
|
42
|
+
hexToHsl,
|
|
43
|
+
hslToHex,
|
|
44
|
+
rgbToHsl,
|
|
45
|
+
} from "./color"
|
|
46
|
+
export type { HSL } from "./color"
|
|
47
|
+
|
|
48
|
+
// Token resolution
|
|
49
|
+
export { resolveThemeColor } from "./resolve"
|
|
50
|
+
|
|
51
|
+
// ANSI 16 theme generation
|
|
52
|
+
export { generateTheme } from "./generate"
|
|
53
|
+
|
|
54
|
+
// Builder API
|
|
55
|
+
export { createTheme, quickTheme, presetTheme } from "./builder"
|
|
56
|
+
|
|
57
|
+
// Palette generators
|
|
58
|
+
export { fromBase16, fromColors, fromPreset } from "./generators"
|
|
59
|
+
|
|
60
|
+
// Active theme state (side-effectful)
|
|
61
|
+
export { setActiveTheme, getActiveTheme, pushContextTheme, popContextTheme } from "./state"
|
|
62
|
+
|
|
63
|
+
// Validation
|
|
64
|
+
export { validateColorPalette } from "./validate"
|
|
65
|
+
export type { ValidationResult } from "./validate"
|
|
66
|
+
export { validateTheme, THEME_TOKEN_KEYS } from "./validate-theme"
|
|
67
|
+
export type { ThemeValidationResult } from "./validate-theme"
|
|
68
|
+
|
|
69
|
+
// Contrast checking
|
|
70
|
+
export { checkContrast } from "./contrast"
|
|
71
|
+
export type { ContrastResult } from "./contrast"
|
|
72
|
+
|
|
73
|
+
// Token aliasing
|
|
74
|
+
export { resolveAliases, resolveTokenAlias } from "./alias"
|
|
75
|
+
|
|
76
|
+
// CSS variables export
|
|
77
|
+
export { themeToCSSVars } from "./css"
|
|
78
|
+
|
|
79
|
+
// Auto-generate themes from a single color
|
|
80
|
+
export { autoGenerateTheme } from "./auto-generate"
|
|
81
|
+
|
|
82
|
+
// Base16 import/export
|
|
83
|
+
export { importBase16 } from "./import/base16"
|
|
84
|
+
export { exportBase16 } from "./export/base16"
|
|
85
|
+
export type { Base16Scheme } from "./import/types"
|
|
86
|
+
|
|
87
|
+
// Terminal detection
|
|
88
|
+
export { detectTerminalPalette, detectTheme } from "./detect"
|
|
89
|
+
export type { DetectedPalette, DetectThemeOptions } from "./detect"
|
|
90
|
+
|
|
91
|
+
// Built-in themes (pre-derived)
|
|
92
|
+
export {
|
|
93
|
+
ansi16DarkTheme,
|
|
94
|
+
ansi16LightTheme,
|
|
95
|
+
defaultDarkTheme,
|
|
96
|
+
defaultLightTheme,
|
|
97
|
+
builtinThemes,
|
|
98
|
+
getThemeByName,
|
|
99
|
+
} from "./palettes/index"
|
|
100
|
+
|
|
101
|
+
// Built-in palettes (45 palettes from 15 theme families)
|
|
102
|
+
export {
|
|
103
|
+
builtinPalettes,
|
|
104
|
+
getPaletteByName,
|
|
105
|
+
catppuccinMocha,
|
|
106
|
+
catppuccinFrappe,
|
|
107
|
+
catppuccinMacchiato,
|
|
108
|
+
catppuccinLatte,
|
|
109
|
+
nord,
|
|
110
|
+
dracula,
|
|
111
|
+
oneDark,
|
|
112
|
+
solarizedDark,
|
|
113
|
+
solarizedLight,
|
|
114
|
+
gruvboxDark,
|
|
115
|
+
gruvboxLight,
|
|
116
|
+
tokyoNight,
|
|
117
|
+
tokyoNightStorm,
|
|
118
|
+
tokyoNightDay,
|
|
119
|
+
rosePine,
|
|
120
|
+
rosePineMoon,
|
|
121
|
+
rosePineDawn,
|
|
122
|
+
kanagawaWave,
|
|
123
|
+
kanagawaDragon,
|
|
124
|
+
kanagawaLotus,
|
|
125
|
+
everforestDark,
|
|
126
|
+
everforestLight,
|
|
127
|
+
nightfox,
|
|
128
|
+
dawnfox,
|
|
129
|
+
monokai,
|
|
130
|
+
monokaiPro,
|
|
131
|
+
snazzy,
|
|
132
|
+
materialDark,
|
|
133
|
+
materialLight,
|
|
134
|
+
palenight,
|
|
135
|
+
ayuDark,
|
|
136
|
+
ayuMirage,
|
|
137
|
+
ayuLight,
|
|
138
|
+
horizon,
|
|
139
|
+
moonfly,
|
|
140
|
+
nightfly,
|
|
141
|
+
oxocarbonDark,
|
|
142
|
+
oxocarbonLight,
|
|
143
|
+
sonokai,
|
|
144
|
+
edgeDark,
|
|
145
|
+
edgeLight,
|
|
146
|
+
modusVivendi,
|
|
147
|
+
modusOperandi,
|
|
148
|
+
} from "./palettes/index"
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core type definitions for the swatch theme system.
|
|
3
|
+
*
|
|
4
|
+
* Two-layer architecture:
|
|
5
|
+
* Layer 1: ColorPalette — 22 terminal colors (what palette generators produce)
|
|
6
|
+
* Layer 2: Theme — 33 semantic tokens (what UI apps consume)
|
|
7
|
+
*
|
|
8
|
+
* Pipeline: Palette generators → ColorPalette (22) → deriveTheme() → Theme (33)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// ColorPalette — The 22-Color Terminal Standard
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The 22-color format every modern terminal emulator uses
|
|
17
|
+
* (Ghostty, Kitty, Alacritty, iTerm2, WezTerm).
|
|
18
|
+
*
|
|
19
|
+
* 16 ANSI palette colors + 6 special colors = universal pivot format.
|
|
20
|
+
* All fields are required hex strings (#RRGGBB).
|
|
21
|
+
*/
|
|
22
|
+
export interface ColorPalette {
|
|
23
|
+
name?: string
|
|
24
|
+
dark?: boolean
|
|
25
|
+
|
|
26
|
+
// ── 16 ANSI palette ────────────────────────────────────────────
|
|
27
|
+
/** ANSI 0 — normal black */
|
|
28
|
+
black: string
|
|
29
|
+
/** ANSI 1 — normal red */
|
|
30
|
+
red: string
|
|
31
|
+
/** ANSI 2 — normal green */
|
|
32
|
+
green: string
|
|
33
|
+
/** ANSI 3 — normal yellow */
|
|
34
|
+
yellow: string
|
|
35
|
+
/** ANSI 4 — normal blue */
|
|
36
|
+
blue: string
|
|
37
|
+
/** ANSI 5 — normal magenta */
|
|
38
|
+
magenta: string
|
|
39
|
+
/** ANSI 6 — normal cyan */
|
|
40
|
+
cyan: string
|
|
41
|
+
/** ANSI 7 — normal white */
|
|
42
|
+
white: string
|
|
43
|
+
/** ANSI 8 — bright black */
|
|
44
|
+
brightBlack: string
|
|
45
|
+
/** ANSI 9 — bright red */
|
|
46
|
+
brightRed: string
|
|
47
|
+
/** ANSI 10 — bright green */
|
|
48
|
+
brightGreen: string
|
|
49
|
+
/** ANSI 11 — bright yellow */
|
|
50
|
+
brightYellow: string
|
|
51
|
+
/** ANSI 12 — bright blue */
|
|
52
|
+
brightBlue: string
|
|
53
|
+
/** ANSI 13 — bright magenta */
|
|
54
|
+
brightMagenta: string
|
|
55
|
+
/** ANSI 14 — bright cyan */
|
|
56
|
+
brightCyan: string
|
|
57
|
+
/** ANSI 15 — bright white */
|
|
58
|
+
brightWhite: string
|
|
59
|
+
|
|
60
|
+
// ── 6 special colors ────────────────────────────────────────────
|
|
61
|
+
/** Default text color */
|
|
62
|
+
foreground: string
|
|
63
|
+
/** Default background color */
|
|
64
|
+
background: string
|
|
65
|
+
/** Cursor block/line color */
|
|
66
|
+
cursorColor: string
|
|
67
|
+
/** Text rendered under the cursor */
|
|
68
|
+
cursorText: string
|
|
69
|
+
/** Background color of selected text */
|
|
70
|
+
selectionBackground: string
|
|
71
|
+
/** Text color of selected text */
|
|
72
|
+
selectionForeground: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** All 22 color field names on ColorPalette. */
|
|
76
|
+
export const COLOR_PALETTE_FIELDS = [
|
|
77
|
+
"black",
|
|
78
|
+
"red",
|
|
79
|
+
"green",
|
|
80
|
+
"yellow",
|
|
81
|
+
"blue",
|
|
82
|
+
"magenta",
|
|
83
|
+
"cyan",
|
|
84
|
+
"white",
|
|
85
|
+
"brightBlack",
|
|
86
|
+
"brightRed",
|
|
87
|
+
"brightGreen",
|
|
88
|
+
"brightYellow",
|
|
89
|
+
"brightBlue",
|
|
90
|
+
"brightMagenta",
|
|
91
|
+
"brightCyan",
|
|
92
|
+
"brightWhite",
|
|
93
|
+
"foreground",
|
|
94
|
+
"background",
|
|
95
|
+
"cursorColor",
|
|
96
|
+
"cursorText",
|
|
97
|
+
"selectionBackground",
|
|
98
|
+
"selectionForeground",
|
|
99
|
+
] as const
|
|
100
|
+
|
|
101
|
+
/** Name of one of the 16 ANSI palette colors. */
|
|
102
|
+
export type AnsiColorName =
|
|
103
|
+
| "black"
|
|
104
|
+
| "red"
|
|
105
|
+
| "green"
|
|
106
|
+
| "yellow"
|
|
107
|
+
| "blue"
|
|
108
|
+
| "magenta"
|
|
109
|
+
| "cyan"
|
|
110
|
+
| "white"
|
|
111
|
+
| "brightBlack"
|
|
112
|
+
| "brightRed"
|
|
113
|
+
| "brightGreen"
|
|
114
|
+
| "brightYellow"
|
|
115
|
+
| "brightBlue"
|
|
116
|
+
| "brightMagenta"
|
|
117
|
+
| "brightCyan"
|
|
118
|
+
| "brightWhite"
|
|
119
|
+
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Theme — 33 Semantic Tokens for UI Consumption
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Semantic color token map (33 tokens + palette).
|
|
126
|
+
*
|
|
127
|
+
* Two pairing conventions:
|
|
128
|
+
* Surface pairs: `$name` = text, `$name-bg` = background
|
|
129
|
+
* (muted, surface, popover, inverse, cursor, selection)
|
|
130
|
+
* Accent pairs: `$name` = area bg, `$name-fg` = text on area
|
|
131
|
+
* (primary, secondary, accent, error, warning, success, info)
|
|
132
|
+
*
|
|
133
|
+
* Components reference tokens with a `$` prefix (e.g. `color="$primary"`).
|
|
134
|
+
* All property names are lowercase, no hyphens, no camelCase.
|
|
135
|
+
*/
|
|
136
|
+
export interface Theme {
|
|
137
|
+
/** Human-readable theme name */
|
|
138
|
+
name: string
|
|
139
|
+
|
|
140
|
+
// ── Root pair ───────────────────────────────────────────────────
|
|
141
|
+
/** Default background */
|
|
142
|
+
bg: string
|
|
143
|
+
/** Default text */
|
|
144
|
+
fg: string
|
|
145
|
+
|
|
146
|
+
// ── 6 surface pairs (base = text, *bg = background) ─────────────
|
|
147
|
+
/** Secondary/muted text (~70% contrast) */
|
|
148
|
+
muted: string
|
|
149
|
+
/** Muted area background (hover state) */
|
|
150
|
+
mutedbg: string
|
|
151
|
+
/** Text on elevated surface */
|
|
152
|
+
surface: string
|
|
153
|
+
/** Elevated content area background */
|
|
154
|
+
surfacebg: string
|
|
155
|
+
/** Text on floating content */
|
|
156
|
+
popover: string
|
|
157
|
+
/** Floating content background (popover, dropdown) */
|
|
158
|
+
popoverbg: string
|
|
159
|
+
/** Text on chrome area */
|
|
160
|
+
inverse: string
|
|
161
|
+
/** Chrome area (status/title bar) */
|
|
162
|
+
inversebg: string
|
|
163
|
+
/** Text under cursor */
|
|
164
|
+
cursor: string
|
|
165
|
+
/** Cursor color */
|
|
166
|
+
cursorbg: string
|
|
167
|
+
/** Text on selected items */
|
|
168
|
+
selection: string
|
|
169
|
+
/** Selected items background */
|
|
170
|
+
selectionbg: string
|
|
171
|
+
|
|
172
|
+
// ── 7 accent pairs (base = area bg, *fg = text on area) ─────────
|
|
173
|
+
/** Brand accent area */
|
|
174
|
+
primary: string
|
|
175
|
+
/** Text on primary accent area */
|
|
176
|
+
primaryfg: string
|
|
177
|
+
/** Alternate accent area */
|
|
178
|
+
secondary: string
|
|
179
|
+
/** Text on secondary accent area */
|
|
180
|
+
secondaryfg: string
|
|
181
|
+
/** Attention/pop accent area */
|
|
182
|
+
accent: string
|
|
183
|
+
/** Text on accent area */
|
|
184
|
+
accentfg: string
|
|
185
|
+
/** Error/destructive area */
|
|
186
|
+
error: string
|
|
187
|
+
/** Text on error area */
|
|
188
|
+
errorfg: string
|
|
189
|
+
/** Warning/caution area */
|
|
190
|
+
warning: string
|
|
191
|
+
/** Text on warning area */
|
|
192
|
+
warningfg: string
|
|
193
|
+
/** Success/positive area */
|
|
194
|
+
success: string
|
|
195
|
+
/** Text on success area */
|
|
196
|
+
successfg: string
|
|
197
|
+
/** Neutral info area */
|
|
198
|
+
info: string
|
|
199
|
+
/** Text on info area */
|
|
200
|
+
infofg: string
|
|
201
|
+
|
|
202
|
+
// ── 5 standalone tokens ─────────────────────────────────────────
|
|
203
|
+
/** Structural dividers, borders */
|
|
204
|
+
border: string
|
|
205
|
+
/** Interactive control borders (inputs, buttons) */
|
|
206
|
+
inputborder: string
|
|
207
|
+
/** Focus border (always blue) */
|
|
208
|
+
focusborder: string
|
|
209
|
+
/** Hyperlinks */
|
|
210
|
+
link: string
|
|
211
|
+
/** Disabled/placeholder text (~50% contrast) */
|
|
212
|
+
disabledfg: string
|
|
213
|
+
|
|
214
|
+
// ── 16 palette passthrough ──────────────────────────────────────
|
|
215
|
+
/** 16 ANSI colors ($color0–$color15) */
|
|
216
|
+
palette: string[]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Supported primary colors for ANSI 16 theme generation. */
|
|
220
|
+
export type AnsiPrimary = "yellow" | "cyan" | "magenta" | "green" | "red" | "blue" | "white"
|
|
221
|
+
|
|
222
|
+
/** Accent hue name — the 8 hue names for palette generators. */
|
|
223
|
+
export type HueName = "red" | "orange" | "yellow" | "green" | "teal" | "blue" | "purple" | "pink"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme validation — checks that all required semantic tokens are present.
|
|
3
|
+
*
|
|
4
|
+
* Complements validateColorPalette() which validates the lower-level
|
|
5
|
+
* ColorPalette. This validates the derived Theme object.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** All 33 required semantic token keys on Theme (excludes `name` and `palette`). */
|
|
9
|
+
export const THEME_TOKEN_KEYS: readonly string[] = [
|
|
10
|
+
// Root pair
|
|
11
|
+
"bg",
|
|
12
|
+
"fg",
|
|
13
|
+
// 6 surface pairs (base = text, *bg = background)
|
|
14
|
+
"muted",
|
|
15
|
+
"mutedbg",
|
|
16
|
+
"surface",
|
|
17
|
+
"surfacebg",
|
|
18
|
+
"popover",
|
|
19
|
+
"popoverbg",
|
|
20
|
+
"inverse",
|
|
21
|
+
"inversebg",
|
|
22
|
+
"cursor",
|
|
23
|
+
"cursorbg",
|
|
24
|
+
"selection",
|
|
25
|
+
"selectionbg",
|
|
26
|
+
// 7 accent pairs (base = area bg, *fg = text on area)
|
|
27
|
+
"primary",
|
|
28
|
+
"primaryfg",
|
|
29
|
+
"secondary",
|
|
30
|
+
"secondaryfg",
|
|
31
|
+
"accent",
|
|
32
|
+
"accentfg",
|
|
33
|
+
"error",
|
|
34
|
+
"errorfg",
|
|
35
|
+
"warning",
|
|
36
|
+
"warningfg",
|
|
37
|
+
"success",
|
|
38
|
+
"successfg",
|
|
39
|
+
"info",
|
|
40
|
+
"infofg",
|
|
41
|
+
// 5 standalone tokens
|
|
42
|
+
"border",
|
|
43
|
+
"inputborder",
|
|
44
|
+
"focusborder",
|
|
45
|
+
"link",
|
|
46
|
+
"disabledfg",
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
/** Result of theme validation. */
|
|
50
|
+
export interface ThemeValidationResult {
|
|
51
|
+
/** Whether the theme has all required tokens. */
|
|
52
|
+
valid: boolean
|
|
53
|
+
/** Token keys that are required but missing or empty. */
|
|
54
|
+
missing: string[]
|
|
55
|
+
/** Token keys that exist on the object but are not recognized theme tokens. */
|
|
56
|
+
extra: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** All recognized keys on Theme (tokens + metadata). */
|
|
60
|
+
const ALL_KNOWN_KEYS = new Set([...THEME_TOKEN_KEYS, "name", "palette"])
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate a Theme object — check that all required tokens are present.
|
|
64
|
+
*
|
|
65
|
+
* @param theme - The theme object to validate
|
|
66
|
+
* @returns Validation result with missing and extra token lists
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* const result = validateTheme(myTheme)
|
|
71
|
+
* if (!result.valid) {
|
|
72
|
+
* console.log("Missing tokens:", result.missing)
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function validateTheme(theme: Record<string, unknown>): ThemeValidationResult {
|
|
77
|
+
const missing: string[] = []
|
|
78
|
+
const extra: string[] = []
|
|
79
|
+
|
|
80
|
+
// Check for missing or empty required tokens
|
|
81
|
+
for (const key of THEME_TOKEN_KEYS) {
|
|
82
|
+
const val = theme[key]
|
|
83
|
+
if (val === undefined || val === null || val === "") {
|
|
84
|
+
missing.push(key)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for unrecognized keys (exclude prototype properties)
|
|
89
|
+
for (const key of Object.keys(theme)) {
|
|
90
|
+
if (!ALL_KNOWN_KEYS.has(key)) {
|
|
91
|
+
extra.push(key)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
valid: missing.length === 0,
|
|
97
|
+
missing,
|
|
98
|
+
extra,
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Palette validation — checks ColorPalette fields and contrast.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { hexToRgb } from "./color"
|
|
6
|
+
import { COLOR_PALETTE_FIELDS, type ColorPalette } from "./types"
|
|
7
|
+
|
|
8
|
+
/** Validation result from validateColorPalette(). */
|
|
9
|
+
export interface ValidationResult {
|
|
10
|
+
valid: boolean
|
|
11
|
+
errors: string[]
|
|
12
|
+
warnings: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate a ColorPalette.
|
|
17
|
+
*
|
|
18
|
+
* Checks:
|
|
19
|
+
* - All 22 color fields are present and non-empty hex strings
|
|
20
|
+
* - Warns on low-contrast foreground/background combinations
|
|
21
|
+
*/
|
|
22
|
+
export function validateColorPalette(p: ColorPalette): ValidationResult {
|
|
23
|
+
const errors: string[] = []
|
|
24
|
+
const warnings: string[] = []
|
|
25
|
+
|
|
26
|
+
// Required color fields
|
|
27
|
+
for (const field of COLOR_PALETTE_FIELDS) {
|
|
28
|
+
const val = p[field]
|
|
29
|
+
if (!val || typeof val !== "string") {
|
|
30
|
+
errors.push(`${field} is required and must be a non-empty string`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Contrast warnings (only for hex colors)
|
|
35
|
+
if (p.foreground && p.background) {
|
|
36
|
+
const fgRgb = hexToRgb(p.foreground)
|
|
37
|
+
const bgRgb = hexToRgb(p.background)
|
|
38
|
+
if (fgRgb && bgRgb) {
|
|
39
|
+
const fgSum = fgRgb[0] + fgRgb[1] + fgRgb[2]
|
|
40
|
+
const bgSum = bgRgb[0] + bgRgb[1] + bgRgb[2]
|
|
41
|
+
const fgIsLight = fgSum > 384
|
|
42
|
+
const bgIsLight = bgSum > 384
|
|
43
|
+
if (fgIsLight === bgIsLight) {
|
|
44
|
+
warnings.push(
|
|
45
|
+
`Low contrast: foreground (${p.foreground}) and background (${p.background}) have similar lightness`,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
valid: errors.length === 0,
|
|
53
|
+
errors,
|
|
54
|
+
warnings,
|
|
55
|
+
}
|
|
56
|
+
}
|