@adea-ai/themes 0.1.2 → 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/src/sources.ts CHANGED
@@ -37,6 +37,7 @@
37
37
  */
38
38
 
39
39
  import type { Base24Slot } from './adapters/base24'
40
+ import { CONTRAST_FLOORS } from './normalize'
40
41
  import type { ThemeAppearance, ThemeProvenance } from './schema'
41
42
 
42
43
  /** The dataset the Base24 schemes are reproduced from. */
@@ -58,6 +59,258 @@ export const ADEA_PROVENANCE: ThemeProvenance = Object.freeze({
58
59
  license: 'Apache-2.0',
59
60
  })
60
61
 
62
+ /**
63
+ * A theme composed from **two** upstream palettes.
64
+ *
65
+ * Adea's own dark theme is not a copy of any published palette, and it is not
66
+ * hand-authored either. It is a deliberate composition: the canvas, the greys, the
67
+ * cursor and the selection come from a palette chosen for exactly those — Aardvark
68
+ * Ink, whose near-black navy and muted blue-grey foreground are why it was picked —
69
+ * and the sixteen ANSI hues come from a second palette chosen for its saturation,
70
+ * GitHub's dark scheme.
71
+ *
72
+ * The split exists because the two properties are in tension inside one palette. A
73
+ * palette tuned for a comfortable canvas mutes its hues to agree with it; a palette
74
+ * tuned for vivid syntax colours tends to sit on a canvas that is very dark or very
75
+ * flat. Taking one of each yields a calm ground with legible colour on it.
76
+ *
77
+ * ## Why it is composed rather than merged by hand
78
+ *
79
+ * The alternative was to read both palettes and paste the merged hex into
80
+ * `palettes/adea-dark.json`. That works once and is wrong afterwards: nothing then
81
+ * records which donor each value came from, so neither donor could ever be
82
+ * refreshed. Composing in the vendor step keeps both parents named, keeps the values
83
+ * upstream's own bytes rather than a transcription, and makes "GitHub changed its
84
+ * blue" a one-line diff plus a rebuild.
85
+ */
86
+ export interface ComposedSource {
87
+ id: string
88
+ name: string
89
+ family: 'adea'
90
+ familyLabel: string
91
+ label: string
92
+ description: string
93
+ appearance: ThemeAppearance
94
+ tags: readonly string[]
95
+ /** The two donors' slugs in the dataset, and which half each supplies. */
96
+ donors: Readonly<Record<'structure' | 'hues', string>>
97
+ /**
98
+ * The Base24 slot map.
99
+ *
100
+ * Each entry is `[donor, key]`, where the donor is `structure` or `hues` and the
101
+ * key is a role name in that donor's published palette — the dataset's own names,
102
+ * so `purple` is Base24's magenta slot and `brightPurple` its bright magenta.
103
+ */
104
+ slots: Readonly<Partial<Record<Base24Slot, readonly ['structure' | 'hues', string]>>>
105
+ /**
106
+ * Slots neither donor supplies, derived from one of them.
107
+ *
108
+ * `slots` and `synthesise` are jointly total: between them they must cover every
109
+ * Base24 slot, and the vendor step asserts that rather than trusting it. The two
110
+ * are separate maps rather than one so that "this value came from a donor" and
111
+ * "this value was computed" stay distinguishable in review.
112
+ */
113
+ synthesise: Readonly<
114
+ Partial<
115
+ Record<
116
+ Base24Slot,
117
+ readonly ['structure' | 'hues', string, { rotate?: number; lighten?: number }]
118
+ >
119
+ >
120
+ >
121
+ /**
122
+ * ANSI roles pinned to the structure donor's own values.
123
+ *
124
+ * The greyscale ramp is normally *derived* by ranking a scheme's grey slots by
125
+ * measured contrast, because Base24 does not name `black`, `white` or
126
+ * `brightBlack` and light schemes invert them. That derivation is right for an
127
+ * imported palette and wrong here: Aardvark Ink publishes a greyscale it chose,
128
+ * the greyscale is the reason it was chosen as the structure donor, and
129
+ * re-deriving it gives back something close but not equal. So the four roles are
130
+ * pinned to the donor's own values instead.
131
+ */
132
+ ansiFromStructure: readonly ('black' | 'brightBlack' | 'white' | 'brightWhite')[]
133
+ /** The role supplying the accent, when the family's identity demands one. */
134
+ accentSlot?: 'blue' | 'magenta' | 'cyan' | 'green'
135
+ palette?: Partial<Record<Base24Slot, string>>
136
+ /**
137
+ * Transpose this composition's hues onto the borrowed canvas.
138
+ *
139
+ * Set when a composition takes its hues from a palette authored against a different
140
+ * kind of canvas — which is what borrowing a *dark* palette's hues for a *light*
141
+ * canvas is. See `transposeHues` in `src/normalize.ts` for why the whole hue set
142
+ * moves by one common step rather than each hue being repaired on its own.
143
+ */
144
+ hueTranspose?: { floor: number }
145
+ provenance: ThemeProvenance
146
+ }
147
+
148
+ /**
149
+ * Adea's two default themes.
150
+ *
151
+ * Both are compositions and both were previously authored here — the light one as a
152
+ * neutral grey ramp on white with a monochrome accent, the dark one as a neutral grey
153
+ * ramp on `#252525`. They replaced those entirely while keeping their names, because
154
+ * they are the defaults: the system's own appearance, not a pair of options beside it.
155
+ *
156
+ * ## Why they are a *pair* rather than two themes
157
+ *
158
+ * They share one hue donor, so a red is the same red and a blue the same blue in both,
159
+ * and each borrows its canvas from a palette chosen for the *canvas* rather than for
160
+ * its colours:
161
+ *
162
+ * | | canvas | canvas hue | hues |
163
+ * | --- | --- | --- | --- |
164
+ * | Adea Dark | Aardvark Ink `#0f141f` | 265° | GitHub Dark Default |
165
+ * | Adea Light | Nord Light `#e3e9f4` | 262° | GitHub Dark Default |
166
+ *
167
+ * Three degrees apart, and both canvases are *tinted* rather than neutral — 0.024 and
168
+ * 0.016 of chroma — which is the property that makes the pair feel like one theme seen
169
+ * at two exposures instead of two themes that happen to ship together. Switching
170
+ * appearance changes lightness and nothing else about the theme's identity; that claim
171
+ * is asserted in `tests/provenance.test.ts` rather than left as intent.
172
+ *
173
+ * The light theme is the reason `hueTranspose` exists: GitHub's dark hues measure
174
+ * between 2.5:1 and 4.1:1 on a light canvas, so the whole hue set is transposed down in
175
+ * lightness by one common step — hue and chroma intact, relative brightness ordering
176
+ * intact — rather than repaired hue by hue, which would have collapsed every bright
177
+ * variant onto its normal sibling.
178
+ */
179
+ export const COMPOSED_SOURCES: readonly ComposedSource[] = Object.freeze([
180
+ {
181
+ id: 'adea-dark',
182
+ name: 'Adea Dark',
183
+ family: 'adea',
184
+ familyLabel: 'Adea',
185
+ label: 'Dark',
186
+ description:
187
+ "The default dark theme. Aardvark Ink's quiet canvas, with GitHub's vivid hues on it.",
188
+ appearance: 'dark',
189
+ tags: ['dark', 'default', 'vivid'],
190
+ donors: { structure: 'aardvark-ink', hues: 'github-dark-default' },
191
+ slots: {
192
+ // The structure donor. `base01` receives its `black` rather than a background
193
+ // step: an ANSI black equal to the background is invisible as foreground text,
194
+ // which is the defect the normalizer's ramp notes describe.
195
+ base00: ['structure', 'background'],
196
+ base01: ['structure', 'black'],
197
+ base02: ['structure', 'selection'],
198
+ base03: ['structure', 'brightBlack'],
199
+ base04: ['structure', 'white'],
200
+ base05: ['structure', 'foreground'],
201
+ base06: ['structure', 'brightWhite'],
202
+ base07: ['structure', 'brightWhite'],
203
+ // The hue donor. Base24's bright slots run red, yellow, green, cyan, blue,
204
+ // magenta — not the ANSI order of yellow before green.
205
+ base08: ['hues', 'red'],
206
+ base0A: ['hues', 'yellow'],
207
+ base0B: ['hues', 'green'],
208
+ base0C: ['hues', 'cyan'],
209
+ base0D: ['hues', 'blue'],
210
+ base0E: ['hues', 'purple'],
211
+ base12: ['hues', 'brightRed'],
212
+ base13: ['hues', 'brightYellow'],
213
+ base14: ['hues', 'brightGreen'],
214
+ base15: ['hues', 'brightCyan'],
215
+ base16: ['hues', 'brightBlue'],
216
+ base17: ['hues', 'brightPurple'],
217
+ },
218
+ synthesise: {
219
+ // Neither donor's ANSI set has an orange or a brown, and Base24 defines both.
220
+ // A hue rotation off red and yellow is how the export adapter fills them too,
221
+ // so a composed scheme and an exported one are synthesised identically.
222
+ base09: ['hues', 'red', { rotate: 26 }],
223
+ base0F: ['hues', 'yellow', { rotate: -34 }],
224
+ // The two rungs below the canvas, which the specification defines as "dark
225
+ // black" and "darker than that". Lightness slices of the structure donor's own
226
+ // canvas, so the theme keeps its hue as it darkens.
227
+ base10: ['structure', 'background', { lighten: -0.03 }],
228
+ base11: ['structure', 'background', { lighten: -0.06 }],
229
+ },
230
+ ansiFromStructure: ['black', 'brightBlack', 'white', 'brightWhite'],
231
+ provenance: {
232
+ project: 'Adea',
233
+ url: 'https://github.com/adea-ai/themes',
234
+ license: 'Apache-2.0',
235
+ bootstrappedFrom: [
236
+ 'Aardvark Ink (iTerm2-Color-Schemes)',
237
+ 'GitHub Dark Default (iTerm2-Color-Schemes)',
238
+ ],
239
+ },
240
+ },
241
+ {
242
+ id: 'adea-light',
243
+ name: 'Adea Light',
244
+ family: 'adea',
245
+ familyLabel: 'Adea',
246
+ label: 'Light',
247
+ description:
248
+ "The default light theme. Nord Light's cool canvas, with the same hues as Adea Dark.",
249
+ appearance: 'light',
250
+ tags: ['light', 'default', 'cool'],
251
+ donors: { structure: 'nord-light', hues: 'github-dark-default' },
252
+ slots: {
253
+ // The structure donor. Same shape as the dark theme's, with one difference worth
254
+ // noting: Nord Light's `black` (`#3b4252`) is *darker* than its foreground, which
255
+ // is what an ANSI black should be on a light canvas — the text colour and the
256
+ // darkest neutral are the same role there, and Base24's `base01` is where Nord
257
+ // keeps it.
258
+ base00: ['structure', 'background'],
259
+ base01: ['structure', 'black'],
260
+ base02: ['structure', 'selection'],
261
+ base03: ['structure', 'brightBlack'],
262
+ base04: ['structure', 'white'],
263
+ base05: ['structure', 'foreground'],
264
+ base06: ['structure', 'brightWhite'],
265
+ base07: ['structure', 'brightWhite'],
266
+ base08: ['hues', 'red'],
267
+ base0A: ['hues', 'yellow'],
268
+ base0B: ['hues', 'green'],
269
+ base0C: ['hues', 'cyan'],
270
+ base0D: ['hues', 'blue'],
271
+ base0E: ['hues', 'purple'],
272
+ base12: ['hues', 'brightRed'],
273
+ base13: ['hues', 'brightYellow'],
274
+ base14: ['hues', 'brightGreen'],
275
+ base15: ['hues', 'brightCyan'],
276
+ base16: ['hues', 'brightBlue'],
277
+ base17: ['hues', 'brightPurple'],
278
+ },
279
+ synthesise: {
280
+ base09: ['hues', 'red', { rotate: 26 }],
281
+ base0F: ['hues', 'yellow', { rotate: -34 }],
282
+ // Beyond the canvas rather than below it, which is the convention for a light
283
+ // scheme: these two slots are the light end of the ramp.
284
+ base10: ['structure', 'background', { lighten: 0.03 }],
285
+ base11: ['structure', 'background', { lighten: 0.06 }],
286
+ },
287
+ /**
288
+ * The one deliberate deviation from the donor, and it is aesthetic rather than
289
+ * factual — unlike the One Dark correction above, nothing here is wrong.
290
+ *
291
+ * Nord Light's canvas carries 0.010 of chroma against Aardvark Ink's 0.024, so
292
+ * rendering the same hue at half the tint makes the light theme read as neutral grey
293
+ * beside a partner that is clearly navy. Deepening it to 0.016 — two thirds of the
294
+ * dark theme's — is what makes the pair look like siblings. Lightness and hue are
295
+ * untouched, so no contrast pairing moves: body text measures 7.5:1 either way.
296
+ */
297
+ palette: { base00: 'oklch(0.933 0.016 261.79)' },
298
+ ansiFromStructure: ['black', 'brightBlack', 'white', 'brightWhite'],
299
+ // GitHub's hues were drawn for a `#0d1117` canvas. On Nord Light's they measure
300
+ // between 2.5:1 and 4.1:1, which is legible as an accent and not as terminal text.
301
+ hueTranspose: { floor: CONTRAST_FLOORS.status },
302
+ provenance: {
303
+ project: 'Adea',
304
+ url: 'https://github.com/adea-ai/themes',
305
+ license: 'Apache-2.0',
306
+ bootstrappedFrom: [
307
+ 'Nord Light (iTerm2-Color-Schemes)',
308
+ 'GitHub Dark Default (iTerm2-Color-Schemes)',
309
+ ],
310
+ },
311
+ },
312
+ ])
313
+
61
314
  /**
62
315
  * Per-family provenance.
63
316
  *
@@ -505,9 +758,17 @@ export const VENDORED_SOURCES: readonly VendoredSource[] = Object.freeze([
505
758
  * accent — the design system's default has to work as a default for everyone, which
506
759
  * means it cannot be anybody's favourite colour.
507
760
  *
508
- * Both appearances are generated from the accent-and-grey ramp below, so the two
509
- * variants cannot drift apart: a change to the ramp changes both, and the contrast
510
- * suite measures both.
761
+ * Intentionally empty, and worth leaving that way until something needs it.
762
+ *
763
+ * This was where both default themes lived. They are now
764
+ * {@link COMPOSED_SOURCES} entries instead, which is a better home for them: a
765
+ * composition names the upstream palettes it came from and can be rebuilt when either
766
+ * moves, while an authored entry is a table of values nobody can refresh.
767
+ *
768
+ * The route stays because it is the only way to specify *semantic* roles directly
769
+ * rather than deriving them through Base24 slots — which is what a brand theme with
770
+ * values handed over by a designer would need. Nothing uses it today; if nothing has by
771
+ * the time someone reads this, delete it.
511
772
  */
512
773
  export interface AuthoredSource {
513
774
  id: string
@@ -575,108 +836,4 @@ export interface AuthoredSource {
575
836
  selection: string
576
837
  }
577
838
 
578
- export const AUTHORED_SOURCES: readonly AuthoredSource[] = Object.freeze([
579
- {
580
- id: 'adea-light',
581
- family: 'adea',
582
- familyLabel: 'Adea',
583
- label: 'Light',
584
- name: 'Adea Light',
585
- description: "The default light theme. A neutral ladder with a monochrome accent.",
586
- appearance: 'light',
587
- tags: ['light', 'neutral', 'default'],
588
- ramp: {
589
- background: '#ffffff',
590
- foreground: '#252525',
591
- surface: '#ffffff',
592
- surfaceElevated: '#ffffff',
593
- surfaceHover: '#f7f7f7',
594
- surfaceActive: '#f0f0f0',
595
- border: '#ebebeb',
596
- borderMuted: '#f2f2f2',
597
- textMuted: '#6f6f6f',
598
- textSubtle: '#767676',
599
- },
600
- accent: '#343434',
601
- accentForeground: '#fcfcfc',
602
- status: {
603
- success: '#1a7f37',
604
- warning: '#a16207',
605
- error: '#c53c2b',
606
- info: '#0e7490',
607
- },
608
- ansi: {
609
- black: '#1b1f24',
610
- red: '#b91c1c',
611
- green: '#116a2e',
612
- yellow: '#8a5a1b',
613
- blue: '#0b57d0',
614
- magenta: '#a0186f',
615
- cyan: '#0e7490',
616
- white: '#57606a',
617
- brightBlack: '#57606a',
618
- brightRed: '#c94d4d',
619
- brightGreen: '#1f9d4f',
620
- brightYellow: '#a9752c',
621
- brightBlue: '#3b82f6',
622
- brightMagenta: '#c04a92',
623
- brightCyan: '#0891b2',
624
- brightWhite: '#24292f',
625
- },
626
- cursor: '#24292f',
627
- selection: '#b6c7ff',
628
- },
629
- {
630
- id: 'adea-dark',
631
- family: 'adea',
632
- familyLabel: 'Adea',
633
- label: 'Dark',
634
- name: 'Adea Dark',
635
- description: "The default dark theme. A soft grey, never a near-black.",
636
- appearance: 'dark',
637
- tags: ['dark', 'neutral', 'default'],
638
- ramp: {
639
- background: '#252525',
640
- foreground: '#fcfcfc',
641
- surface: '#2d2d2d',
642
- surfaceElevated: '#343434',
643
- surfaceHover: '#444444',
644
- surfaceActive: '#4d4d4d',
645
- border: '#3d3d3d',
646
- borderMuted: '#333333',
647
- textMuted: '#a3a3a3',
648
- textSubtle: '#8a8a8a',
649
- },
650
- accent: '#ebebeb',
651
- accentForeground: '#343434',
652
- status: {
653
- success: '#3fb950',
654
- warning: '#e3b341',
655
- // Lighter than the value the product shipped: `#e07060` measures 4.37:1 on
656
- // this theme's raised surface and 4.09:1 on a popover, which is where an
657
- // alert is most likely to sit. Same hue, same chroma, four steps up.
658
- error: '#ee7c6c',
659
- info: '#39c5cf',
660
- },
661
- ansi: {
662
- black: '#2f3742',
663
- red: '#ff8183',
664
- green: '#56d364',
665
- yellow: '#e3b341',
666
- blue: '#6ca4f8',
667
- magenta: '#db61a2',
668
- cyan: '#39c5cf',
669
- white: '#d5dde5',
670
- brightBlack: '#57606a',
671
- brightRed: '#ff9494',
672
- brightGreen: '#79dd8a',
673
- brightYellow: '#f0c264',
674
- brightBlue: '#8db9ff',
675
- brightMagenta: '#e87cb4',
676
- brightCyan: '#66d3dc',
677
- brightWhite: '#eef2f6',
678
- },
679
- cursor: '#e6edf3',
680
- selection: '#264f78',
681
- },
682
- ])
839
+ export const AUTHORED_SOURCES: readonly AuthoredSource[] = Object.freeze([])