@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.
Files changed (109) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +110 -0
  3. package/README.md +157 -0
  4. package/dist/adapters/base24.d.ts +117 -0
  5. package/dist/adapters/base24.d.ts.map +1 -0
  6. package/dist/adapters/base24.js +313 -0
  7. package/dist/adapters/base24.js.map +1 -0
  8. package/dist/adapters/css.d.ts +68 -0
  9. package/dist/adapters/css.d.ts.map +1 -0
  10. package/dist/adapters/css.js +107 -0
  11. package/dist/adapters/css.js.map +1 -0
  12. package/dist/adapters/shadcn.d.ts +43 -0
  13. package/dist/adapters/shadcn.d.ts.map +1 -0
  14. package/dist/adapters/shadcn.js +89 -0
  15. package/dist/adapters/shadcn.js.map +1 -0
  16. package/dist/adapters/shiki.d.ts +60 -0
  17. package/dist/adapters/shiki.d.ts.map +1 -0
  18. package/dist/adapters/shiki.js +135 -0
  19. package/dist/adapters/shiki.js.map +1 -0
  20. package/dist/adapters/tailwind.d.ts +35 -0
  21. package/dist/adapters/tailwind.d.ts.map +1 -0
  22. package/dist/adapters/tailwind.js +58 -0
  23. package/dist/adapters/tailwind.js.map +1 -0
  24. package/dist/adapters/xterm.d.ts +64 -0
  25. package/dist/adapters/xterm.d.ts.map +1 -0
  26. package/dist/adapters/xterm.js +112 -0
  27. package/dist/adapters/xterm.js.map +1 -0
  28. package/dist/catalogue.d.ts +66 -0
  29. package/dist/catalogue.d.ts.map +1 -0
  30. package/dist/catalogue.js +110 -0
  31. package/dist/catalogue.js.map +1 -0
  32. package/dist/derive.d.ts +89 -0
  33. package/dist/derive.d.ts.map +1 -0
  34. package/dist/derive.js +165 -0
  35. package/dist/derive.js.map +1 -0
  36. package/dist/generated/schemes.d.ts +16 -0
  37. package/dist/generated/schemes.d.ts.map +1 -0
  38. package/dist/generated/schemes.js +880 -0
  39. package/dist/generated/schemes.js.map +1 -0
  40. package/dist/generated/themes.d.ts +11 -0
  41. package/dist/generated/themes.d.ts.map +1 -0
  42. package/dist/generated/themes.js +1650 -0
  43. package/dist/generated/themes.js.map +1 -0
  44. package/dist/index.d.ts +67 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +58 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/normalize.d.ts +160 -0
  49. package/dist/normalize.d.ts.map +1 -0
  50. package/dist/normalize.js +795 -0
  51. package/dist/normalize.js.map +1 -0
  52. package/dist/oklch.d.ts +141 -0
  53. package/dist/oklch.d.ts.map +1 -0
  54. package/dist/oklch.js +306 -0
  55. package/dist/oklch.js.map +1 -0
  56. package/dist/schema.d.ts +178 -0
  57. package/dist/schema.d.ts.map +1 -0
  58. package/dist/schema.js +69 -0
  59. package/dist/schema.js.map +1 -0
  60. package/dist/sources.d.ts +171 -0
  61. package/dist/sources.d.ts.map +1 -0
  62. package/dist/sources.js +559 -0
  63. package/dist/sources.js.map +1 -0
  64. package/dist/validate.d.ts +121 -0
  65. package/dist/validate.d.ts.map +1 -0
  66. package/dist/validate.js +255 -0
  67. package/dist/validate.js.map +1 -0
  68. package/package.json +106 -0
  69. package/palettes/ayu-light.json +38 -0
  70. package/palettes/ayu-mirage.json +38 -0
  71. package/palettes/ayu.json +38 -0
  72. package/palettes/catppuccin-frappe.json +38 -0
  73. package/palettes/catppuccin-latte.json +38 -0
  74. package/palettes/catppuccin-macchiato.json +38 -0
  75. package/palettes/catppuccin-mocha.json +38 -0
  76. package/palettes/dracula.json +38 -0
  77. package/palettes/everforest-dark.json +38 -0
  78. package/palettes/everforest-light.json +38 -0
  79. package/palettes/gruvbox-dark.json +38 -0
  80. package/palettes/gruvbox-light.json +38 -0
  81. package/palettes/kanagawa.json +38 -0
  82. package/palettes/monokai.json +38 -0
  83. package/palettes/nord.json +38 -0
  84. package/palettes/one-dark.json +38 -0
  85. package/palettes/rosepine-dawn.json +38 -0
  86. package/palettes/rosepine-moon.json +38 -0
  87. package/palettes/rosepine.json +38 -0
  88. package/palettes/solarized-dark.json +38 -0
  89. package/palettes/solarized-light.json +38 -0
  90. package/palettes/tokyonight-day.json +38 -0
  91. package/palettes/tokyonight-night.json +38 -0
  92. package/palettes/tokyonight-storm.json +38 -0
  93. package/palettes/vesper.json +38 -0
  94. package/src/adapters/base24.ts +355 -0
  95. package/src/adapters/css.ts +149 -0
  96. package/src/adapters/shadcn.ts +99 -0
  97. package/src/adapters/shiki.ts +168 -0
  98. package/src/adapters/tailwind.ts +79 -0
  99. package/src/adapters/xterm.ts +159 -0
  100. package/src/catalogue.ts +129 -0
  101. package/src/derive.ts +203 -0
  102. package/src/generated/schemes.ts +882 -0
  103. package/src/generated/themes.ts +1652 -0
  104. package/src/index.ts +146 -0
  105. package/src/normalize.ts +1010 -0
  106. package/src/oklch.ts +366 -0
  107. package/src/schema.ts +222 -0
  108. package/src/sources.ts +682 -0
  109. 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
+