@adea-ai/themes 0.1.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/LICENSE +201 -0
- package/NOTICE +110 -0
- package/README.md +157 -0
- package/dist/adapters/base24.d.ts +117 -0
- package/dist/adapters/base24.d.ts.map +1 -0
- package/dist/adapters/base24.js +313 -0
- package/dist/adapters/base24.js.map +1 -0
- package/dist/adapters/css.d.ts +68 -0
- package/dist/adapters/css.d.ts.map +1 -0
- package/dist/adapters/css.js +107 -0
- package/dist/adapters/css.js.map +1 -0
- package/dist/adapters/shadcn.d.ts +43 -0
- package/dist/adapters/shadcn.d.ts.map +1 -0
- package/dist/adapters/shadcn.js +89 -0
- package/dist/adapters/shadcn.js.map +1 -0
- package/dist/adapters/shiki.d.ts +60 -0
- package/dist/adapters/shiki.d.ts.map +1 -0
- package/dist/adapters/shiki.js +135 -0
- package/dist/adapters/shiki.js.map +1 -0
- package/dist/adapters/tailwind.d.ts +35 -0
- package/dist/adapters/tailwind.d.ts.map +1 -0
- package/dist/adapters/tailwind.js +58 -0
- package/dist/adapters/tailwind.js.map +1 -0
- package/dist/adapters/xterm.d.ts +64 -0
- package/dist/adapters/xterm.d.ts.map +1 -0
- package/dist/adapters/xterm.js +112 -0
- package/dist/adapters/xterm.js.map +1 -0
- package/dist/catalogue.d.ts +66 -0
- package/dist/catalogue.d.ts.map +1 -0
- package/dist/catalogue.js +110 -0
- package/dist/catalogue.js.map +1 -0
- package/dist/derive.d.ts +89 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +165 -0
- package/dist/derive.js.map +1 -0
- package/dist/generated/schemes.d.ts +16 -0
- package/dist/generated/schemes.d.ts.map +1 -0
- package/dist/generated/schemes.js +880 -0
- package/dist/generated/schemes.js.map +1 -0
- package/dist/generated/themes.d.ts +11 -0
- package/dist/generated/themes.d.ts.map +1 -0
- package/dist/generated/themes.js +1650 -0
- package/dist/generated/themes.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/normalize.d.ts +160 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +795 -0
- package/dist/normalize.js.map +1 -0
- package/dist/oklch.d.ts +141 -0
- package/dist/oklch.d.ts.map +1 -0
- package/dist/oklch.js +306 -0
- package/dist/oklch.js.map +1 -0
- package/dist/schema.d.ts +178 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +69 -0
- package/dist/schema.js.map +1 -0
- package/dist/sources.d.ts +171 -0
- package/dist/sources.d.ts.map +1 -0
- package/dist/sources.js +559 -0
- package/dist/sources.js.map +1 -0
- package/dist/validate.d.ts +121 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +255 -0
- package/dist/validate.js.map +1 -0
- package/package.json +106 -0
- package/palettes/ayu-light.json +38 -0
- package/palettes/ayu-mirage.json +38 -0
- package/palettes/ayu.json +38 -0
- package/palettes/catppuccin-frappe.json +38 -0
- package/palettes/catppuccin-latte.json +38 -0
- package/palettes/catppuccin-macchiato.json +38 -0
- package/palettes/catppuccin-mocha.json +38 -0
- package/palettes/dracula.json +38 -0
- package/palettes/everforest-dark.json +38 -0
- package/palettes/everforest-light.json +38 -0
- package/palettes/gruvbox-dark.json +38 -0
- package/palettes/gruvbox-light.json +38 -0
- package/palettes/kanagawa.json +38 -0
- package/palettes/monokai.json +38 -0
- package/palettes/nord.json +38 -0
- package/palettes/one-dark.json +38 -0
- package/palettes/rosepine-dawn.json +38 -0
- package/palettes/rosepine-moon.json +38 -0
- package/palettes/rosepine.json +38 -0
- package/palettes/solarized-dark.json +38 -0
- package/palettes/solarized-light.json +38 -0
- package/palettes/tokyonight-day.json +38 -0
- package/palettes/tokyonight-night.json +38 -0
- package/palettes/tokyonight-storm.json +38 -0
- package/palettes/vesper.json +38 -0
- package/src/adapters/base24.ts +355 -0
- package/src/adapters/css.ts +149 -0
- package/src/adapters/shadcn.ts +99 -0
- package/src/adapters/shiki.ts +168 -0
- package/src/adapters/tailwind.ts +79 -0
- package/src/adapters/xterm.ts +159 -0
- package/src/catalogue.ts +129 -0
- package/src/derive.ts +203 -0
- package/src/generated/schemes.ts +882 -0
- package/src/generated/themes.ts +1652 -0
- package/src/index.ts +146 -0
- package/src/normalize.ts +1010 -0
- package/src/oklch.ts +366 -0
- package/src/schema.ts +222 -0
- package/src/sources.ts +682 -0
- package/src/validate.ts +325 -0
package/src/derive.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Colours a theme does not store, derived from the ones it does.
|
|
3
|
+
*
|
|
4
|
+
* The canonical schema is deliberately small — seventeen surface roles, sixteen
|
|
5
|
+
* ANSI colours, a cursor and a selection — and three things an application needs
|
|
6
|
+
* are deliberately absent from it: the **syntax roles** a code view wants, the
|
|
7
|
+
* **chart series** a graph wants, and the **fills and foregrounds** a status chip
|
|
8
|
+
* wants.
|
|
9
|
+
*
|
|
10
|
+
* They are absent because each is a *function of* the roles that are present, and a
|
|
11
|
+
* schema that stored them would be a schema with three ways to say the same thing.
|
|
12
|
+
* Deriving them here, once, means every consumer derives them identically — the
|
|
13
|
+
* alternative, which this replaces, is each application inventing its own mapping
|
|
14
|
+
* and two applications disagreeing about what colour a type is.
|
|
15
|
+
*
|
|
16
|
+
* ## Why syntax roles come from ANSI
|
|
17
|
+
*
|
|
18
|
+
* A theme carries no syntax palette, and Base24's editor-oriented slots are not
|
|
19
|
+
* one: `base08` is documented as "variables" and `base0D` as "functions", but every
|
|
20
|
+
* palette's author filled those slots for a terminal, where the question is "what
|
|
21
|
+
* colour is `ls` output". Importing them as syntax roles would colour a diff by
|
|
22
|
+
* accident.
|
|
23
|
+
*
|
|
24
|
+
* Reading the roles off the sixteen ANSI colours instead has a property worth more
|
|
25
|
+
* than per-theme tuning: the ANSI set is the part of a palette its author *did*
|
|
26
|
+
* choose carefully, every theme in the catalogue has one, and it is already tuned
|
|
27
|
+
* for legibility against that theme's background — the same requirement a code view
|
|
28
|
+
* has. It is the reasoning that makes `ls --color` readable in each of these
|
|
29
|
+
* palettes today.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { AdeaTheme } from './schema'
|
|
33
|
+
import type { Oklch } from './oklch'
|
|
34
|
+
import { contrastRatio, formatOklch, mix, oklchToHex, parseColor, repairContrast } from './oklch'
|
|
35
|
+
|
|
36
|
+
/** The syntax roles, matching the names Shiki's theme contract expects. */
|
|
37
|
+
export type SyntaxRole =
|
|
38
|
+
| 'keyword'
|
|
39
|
+
| 'string'
|
|
40
|
+
| 'number'
|
|
41
|
+
| 'comment'
|
|
42
|
+
| 'function'
|
|
43
|
+
| 'variable'
|
|
44
|
+
| 'type'
|
|
45
|
+
| 'tag'
|
|
46
|
+
| 'attribute'
|
|
47
|
+
| 'operator'
|
|
48
|
+
| 'heading'
|
|
49
|
+
| 'link'
|
|
50
|
+
| 'constant'
|
|
51
|
+
| 'punctuation'
|
|
52
|
+
|
|
53
|
+
/** Which ANSI role each syntax role is read from, and why. */
|
|
54
|
+
const SYNTAX_SOURCE: Readonly<Record<SyntaxRole, keyof AdeaTheme['ansi']>> = Object.freeze({
|
|
55
|
+
// Magenta is the slot palettes spend on the most distinctive hue they have, and
|
|
56
|
+
// a keyword is the token most worth making distinctive.
|
|
57
|
+
keyword: 'magenta',
|
|
58
|
+
string: 'green',
|
|
59
|
+
number: 'yellow',
|
|
60
|
+
// The dim grey every palette defines specifically for text it wants present but
|
|
61
|
+
// quiet. Comments are the one role where "quiet" is the requirement.
|
|
62
|
+
comment: 'brightBlack',
|
|
63
|
+
function: 'blue',
|
|
64
|
+
variable: 'white',
|
|
65
|
+
type: 'cyan',
|
|
66
|
+
tag: 'red',
|
|
67
|
+
attribute: 'yellow',
|
|
68
|
+
operator: 'white',
|
|
69
|
+
heading: 'magenta',
|
|
70
|
+
link: 'blue',
|
|
71
|
+
constant: 'brightMagenta',
|
|
72
|
+
punctuation: 'brightBlack',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The syntax palette for a theme, as OKLCH strings.
|
|
77
|
+
*
|
|
78
|
+
* Comment and punctuation are additionally measured against the canvas: a
|
|
79
|
+
* palette's dim grey is chosen to be read against its *terminal* background, which
|
|
80
|
+
* is the same colour as the canvas here, so the measurement normally passes — but
|
|
81
|
+
* when it does not, the same minimal lightness repair the semantic roles use is
|
|
82
|
+
* applied rather than leaving unreadable comments.
|
|
83
|
+
*/
|
|
84
|
+
export function syntaxRoles(
|
|
85
|
+
theme: AdeaTheme,
|
|
86
|
+
options: { commentFloor?: number } = {}
|
|
87
|
+
): Record<SyntaxRole, string> {
|
|
88
|
+
const background = parseColor(theme.colors.background)
|
|
89
|
+
/**
|
|
90
|
+
* Comments and punctuation are held to a *visibility* floor rather than a
|
|
91
|
+
* legibility one.
|
|
92
|
+
*
|
|
93
|
+
* Their whole purpose is to be the faintest thing on screen, and every palette
|
|
94
|
+
* here defines its comment colour that way on purpose: Everforest Light's is
|
|
95
|
+
* 1.9:1 against its own canvas. Holding them to 3:1 could only be satisfied by
|
|
96
|
+
* promoting comments to the body text's colour, which inverts the role — and the
|
|
97
|
+
* floors that *do* carry text are measured separately and are not relaxed.
|
|
98
|
+
*/
|
|
99
|
+
const floor = options.commentFloor ?? 2
|
|
100
|
+
const roles = {} as Record<SyntaxRole, string>
|
|
101
|
+
|
|
102
|
+
for (const [role, source] of Object.entries(SYNTAX_SOURCE) as [SyntaxRole, keyof AdeaTheme['ansi']][]) {
|
|
103
|
+
const value: Oklch | undefined = parseColor(theme.ansi[source])
|
|
104
|
+
if (!value) continue
|
|
105
|
+
|
|
106
|
+
if (role === 'comment' || role === 'punctuation') {
|
|
107
|
+
const repaired = background
|
|
108
|
+
? repairContrast(value, background, floor, 0.3)
|
|
109
|
+
: { color: value }
|
|
110
|
+
roles[role] = formatOklch(repaired.color)
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
roles[role] = formatOklch(value)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return roles
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The syntax palette as hex, for engines that cannot evaluate `oklch()`. */
|
|
121
|
+
export function syntaxRolesHex(theme: AdeaTheme): Record<SyntaxRole, string> {
|
|
122
|
+
const roles = syntaxRoles(theme)
|
|
123
|
+
return Object.fromEntries(
|
|
124
|
+
Object.entries(roles).map(([role, value]) => {
|
|
125
|
+
const parsed = parseColor(value)
|
|
126
|
+
return [role, parsed ? oklchToHex(parsed) : value]
|
|
127
|
+
})
|
|
128
|
+
) as Record<SyntaxRole, string>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The categorical chart series, derived from the ANSI hues.
|
|
133
|
+
*
|
|
134
|
+
* Six series, taken in the order that keeps adjacent ones furthest apart on the
|
|
135
|
+
* hue circle: blue, magenta, cyan, green, yellow, red. A palette's own ordering
|
|
136
|
+
* would put red next to green, which is the pair a pie chart most needs to
|
|
137
|
+
* separate. Constant across themes by construction — every theme has these six
|
|
138
|
+
* slots — so a chart's series colours mean the same thing in every theme.
|
|
139
|
+
*/
|
|
140
|
+
export const CHART_SERIES: readonly (keyof AdeaTheme['ansi'])[] = Object.freeze([
|
|
141
|
+
'blue',
|
|
142
|
+
'magenta',
|
|
143
|
+
'cyan',
|
|
144
|
+
'green',
|
|
145
|
+
'yellow',
|
|
146
|
+
'red',
|
|
147
|
+
] as const)
|
|
148
|
+
|
|
149
|
+
/** The chart series for a theme, as OKLCH strings. */
|
|
150
|
+
export function chartSeries(theme: AdeaTheme): readonly string[] {
|
|
151
|
+
return CHART_SERIES.map((role) => formatOklch(parseColor(theme.ansi[role]) as Oklch))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A tint of a role for use as a background behind text of that role.
|
|
156
|
+
*
|
|
157
|
+
* Status chips need a fill that is the role's colour at low strength, and doing
|
|
158
|
+
* that with alpha would put a translucent colour into a token whose contrast was
|
|
159
|
+
* measured as opaque — and a translucent fill's real contrast depends on whatever
|
|
160
|
+
* happens to be behind it. Blending toward the canvas in OKLCH keeps the result
|
|
161
|
+
* opaque, so the chip's own text pairing can be asserted like any other.
|
|
162
|
+
*/
|
|
163
|
+
export function tint(color: string, background: string, amount = 0.14): string {
|
|
164
|
+
const foreground = parseColor(color)
|
|
165
|
+
const canvas = parseColor(background)
|
|
166
|
+
if (!foreground || !canvas) return color
|
|
167
|
+
return formatOklch(mix(canvas, foreground, amount))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The four status roles, in the order an alert stack shows them. */
|
|
171
|
+
export const STATUS_ROLES = ['success', 'warning', 'error', 'info'] as const
|
|
172
|
+
export type StatusRole = (typeof STATUS_ROLES)[number]
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The text colour to draw on a **solid** fill of a status role.
|
|
176
|
+
*
|
|
177
|
+
* Not the appearance's `foreground`, which is the shortcut and is wrong half the
|
|
178
|
+
* time: a dark theme's foreground is near-white, and near-white on AdEA Dark's
|
|
179
|
+
* `success` at `#3fb950` measures 2.6:1 — illegible. The correct answer is
|
|
180
|
+
* whichever of the theme's two extremes measures better against the fill, which
|
|
181
|
+
* for a bright green is black and for a deep red is white.
|
|
182
|
+
*
|
|
183
|
+
* Where neither clears the floor — a mid-tone fill, which some palettes have —
|
|
184
|
+
* the better of the two is returned and {@link validateTheme} is what reports the
|
|
185
|
+
* pairing as failing, rather than a silent blend being substituted here.
|
|
186
|
+
*/
|
|
187
|
+
export function statusForeground(theme: AdeaTheme, role: StatusRole): string {
|
|
188
|
+
const fill = parseColor(theme.colors[role])
|
|
189
|
+
const background = parseColor(theme.colors.background)
|
|
190
|
+
const foreground = parseColor(theme.colors.foreground)
|
|
191
|
+
if (!fill || !background || !foreground) return theme.colors.foreground
|
|
192
|
+
|
|
193
|
+
const best =
|
|
194
|
+
contrastRatio(foreground, fill) >= contrastRatio(background, fill) ? foreground : background
|
|
195
|
+
return formatOklch(best)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** {@link statusForeground} as hex, for engines that cannot evaluate `oklch()`. */
|
|
199
|
+
export function statusForegroundHex(theme: AdeaTheme, role: StatusRole): string {
|
|
200
|
+
const parsed = parseColor(statusForeground(theme, role))
|
|
201
|
+
return parsed ? oklchToHex(parsed) : theme.colors.foreground
|
|
202
|
+
}
|
|
203
|
+
|