@adea-ai/themes 0.2.0 → 0.3.1

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. */
@@ -132,16 +133,50 @@ export interface ComposedSource {
132
133
  /** The role supplying the accent, when the family's identity demands one. */
133
134
  accentSlot?: 'blue' | 'magenta' | 'cyan' | 'green'
134
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
+ /** The body-text floor; the defaults are held to AAA rather than the catalogue's AA. */
146
+ textFloor?: number
135
147
  provenance: ThemeProvenance
136
148
  }
137
149
 
138
150
  /**
139
- * Adea's composed dark theme.
151
+ * Adea's two default themes.
152
+ *
153
+ * Both are compositions and both were previously authored here — the light one as a
154
+ * neutral grey ramp on white with a monochrome accent, the dark one as a neutral grey
155
+ * ramp on `#252525`. They replaced those entirely while keeping their names, because
156
+ * they are the defaults: the system's own appearance, not a pair of options beside it.
157
+ *
158
+ * ## Why they are a *pair* rather than two themes
140
159
  *
141
- * This was previously authored here as a neutral grey ramp with a monochrome accent.
142
- * It is now a composition, so the default dark theme is derived from named upstream
143
- * palettes and re-derivable, and it gains the saturated hues the earlier ramp did not
144
- * have.
160
+ * They share one hue donor, so a red is the same red and a blue the same blue in both,
161
+ * and each borrows its canvas from a palette chosen for the *canvas* rather than for
162
+ * its colours:
163
+ *
164
+ * | | canvas | canvas hue | hues |
165
+ * | --- | --- | --- | --- |
166
+ * | Adea Dark | Aardvark Ink `#0f141f` | 265° | GitHub Dark Default |
167
+ * | Adea Light | Nord Light `#e3e9f4` | 262° | GitHub Dark Default |
168
+ *
169
+ * Three degrees apart, and both canvases are *tinted* rather than neutral — 0.024 and
170
+ * 0.016 of chroma — which is the property that makes the pair feel like one theme seen
171
+ * at two exposures instead of two themes that happen to ship together. Switching
172
+ * appearance changes lightness and nothing else about the theme's identity; that claim
173
+ * is asserted in `tests/provenance.test.ts` rather than left as intent.
174
+ *
175
+ * The light theme is the reason `hueTranspose` exists: GitHub's dark hues measure
176
+ * between 2.5:1 and 4.1:1 on a light canvas, so the whole hue set is transposed down in
177
+ * lightness by one common step — hue and chroma intact, relative brightness ordering
178
+ * intact — rather than repaired hue by hue, which would have collapsed every bright
179
+ * variant onto its normal sibling.
145
180
  */
146
181
  export const COMPOSED_SOURCES: readonly ComposedSource[] = Object.freeze([
147
182
  {
@@ -195,6 +230,8 @@ export const COMPOSED_SOURCES: readonly ComposedSource[] = Object.freeze([
195
230
  base11: ['structure', 'background', { lighten: -0.06 }],
196
231
  },
197
232
  ansiFromStructure: ['black', 'brightBlack', 'white', 'brightWhite'],
233
+ // AAA rather than AA, because this is a default rather than an imported palette.
234
+ textFloor: 7,
198
235
  provenance: {
199
236
  project: 'Adea',
200
237
  url: 'https://github.com/adea-ai/themes',
@@ -205,6 +242,80 @@ export const COMPOSED_SOURCES: readonly ComposedSource[] = Object.freeze([
205
242
  ],
206
243
  },
207
244
  },
245
+ {
246
+ id: 'adea-light',
247
+ name: 'Adea Light',
248
+ family: 'adea',
249
+ familyLabel: 'Adea',
250
+ label: 'Light',
251
+ description:
252
+ "The default light theme. Nord Light's cool canvas, with the same hues as Adea Dark.",
253
+ appearance: 'light',
254
+ tags: ['light', 'default', 'cool'],
255
+ donors: { structure: 'nord-light', hues: 'github-dark-default' },
256
+ slots: {
257
+ // The structure donor. Same shape as the dark theme's, with one difference worth
258
+ // noting: Nord Light's `black` (`#3b4252`) is *darker* than its foreground, which
259
+ // is what an ANSI black should be on a light canvas — the text colour and the
260
+ // darkest neutral are the same role there, and Base24's `base01` is where Nord
261
+ // keeps it.
262
+ base00: ['structure', 'background'],
263
+ base01: ['structure', 'black'],
264
+ base02: ['structure', 'selection'],
265
+ base03: ['structure', 'brightBlack'],
266
+ base04: ['structure', 'white'],
267
+ base05: ['structure', 'foreground'],
268
+ base06: ['structure', 'brightWhite'],
269
+ base07: ['structure', 'brightWhite'],
270
+ base08: ['hues', 'red'],
271
+ base0A: ['hues', 'yellow'],
272
+ base0B: ['hues', 'green'],
273
+ base0C: ['hues', 'cyan'],
274
+ base0D: ['hues', 'blue'],
275
+ base0E: ['hues', 'purple'],
276
+ base12: ['hues', 'brightRed'],
277
+ base13: ['hues', 'brightYellow'],
278
+ base14: ['hues', 'brightGreen'],
279
+ base15: ['hues', 'brightCyan'],
280
+ base16: ['hues', 'brightBlue'],
281
+ base17: ['hues', 'brightPurple'],
282
+ },
283
+ synthesise: {
284
+ base09: ['hues', 'red', { rotate: 26 }],
285
+ base0F: ['hues', 'yellow', { rotate: -34 }],
286
+ // Beyond the canvas rather than below it, which is the convention for a light
287
+ // scheme: these two slots are the light end of the ramp.
288
+ base10: ['structure', 'background', { lighten: 0.03 }],
289
+ base11: ['structure', 'background', { lighten: 0.06 }],
290
+ },
291
+ /**
292
+ * The one deliberate deviation from the donor, and it is aesthetic rather than
293
+ * factual — unlike the One Dark correction above, nothing here is wrong.
294
+ *
295
+ * Nord Light's canvas carries 0.010 of chroma against Aardvark Ink's 0.024, so
296
+ * rendering the same hue at half the tint makes the light theme read as neutral grey
297
+ * beside a partner that is clearly navy. Deepening it to 0.016 — two thirds of the
298
+ * dark theme's — is what makes the pair look like siblings. Lightness and hue are
299
+ * untouched, so no contrast pairing moves: body text measures 7.5:1 either way.
300
+ */
301
+ palette: { base00: 'oklch(0.933 0.016 261.79)' },
302
+ ansiFromStructure: ['black', 'brightBlack', 'white', 'brightWhite'],
303
+ // GitHub's hues were drawn for a `#0d1117` canvas. On Nord Light's they measure
304
+ // between 2.5:1 and 4.1:1, which is legible as an accent and not as terminal text.
305
+ hueTranspose: { floor: CONTRAST_FLOORS.status },
306
+ // AAA rather than AA; see `textFloor`. The binding pair is text on a popover, since
307
+ // a light ladder descends away from its canvas.
308
+ textFloor: 7,
309
+ provenance: {
310
+ project: 'Adea',
311
+ url: 'https://github.com/adea-ai/themes',
312
+ license: 'Apache-2.0',
313
+ bootstrappedFrom: [
314
+ 'Nord Light (iTerm2-Color-Schemes)',
315
+ 'GitHub Dark Default (iTerm2-Color-Schemes)',
316
+ ],
317
+ },
318
+ },
208
319
  ])
209
320
 
210
321
  /**
@@ -654,9 +765,17 @@ export const VENDORED_SOURCES: readonly VendoredSource[] = Object.freeze([
654
765
  * accent — the design system's default has to work as a default for everyone, which
655
766
  * means it cannot be anybody's favourite colour.
656
767
  *
657
- * Only the light variant is authored here. Its dark counterpart was previously the
658
- * second entry in this list and is now a {@link ComposedSource} built from two
659
- * upstream palettes, which is where the default dark theme gets its saturated hues.
768
+ * Intentionally empty, and worth leaving that way until something needs it.
769
+ *
770
+ * This was where both default themes lived. They are now
771
+ * {@link COMPOSED_SOURCES} entries instead, which is a better home for them: a
772
+ * composition names the upstream palettes it came from and can be rebuilt when either
773
+ * moves, while an authored entry is a table of values nobody can refresh.
774
+ *
775
+ * The route stays because it is the only way to specify *semantic* roles directly
776
+ * rather than deriving them through Base24 slots — which is what a brand theme with
777
+ * values handed over by a designer would need. Nothing uses it today; if nothing has by
778
+ * the time someone reads this, delete it.
660
779
  */
661
780
  export interface AuthoredSource {
662
781
  id: string
@@ -724,55 +843,4 @@ export interface AuthoredSource {
724
843
  selection: string
725
844
  }
726
845
 
727
- export const AUTHORED_SOURCES: readonly AuthoredSource[] = Object.freeze([
728
- {
729
- id: 'adea-light',
730
- family: 'adea',
731
- familyLabel: 'Adea',
732
- label: 'Light',
733
- name: 'Adea Light',
734
- description: "The default light theme. A neutral ladder with a monochrome accent.",
735
- appearance: 'light',
736
- tags: ['light', 'neutral', 'default'],
737
- ramp: {
738
- background: '#ffffff',
739
- foreground: '#252525',
740
- surface: '#ffffff',
741
- surfaceElevated: '#ffffff',
742
- surfaceHover: '#f7f7f7',
743
- surfaceActive: '#f0f0f0',
744
- border: '#ebebeb',
745
- borderMuted: '#f2f2f2',
746
- textMuted: '#6f6f6f',
747
- textSubtle: '#767676',
748
- },
749
- accent: '#343434',
750
- accentForeground: '#fcfcfc',
751
- status: {
752
- success: '#1a7f37',
753
- warning: '#a16207',
754
- error: '#c53c2b',
755
- info: '#0e7490',
756
- },
757
- ansi: {
758
- black: '#1b1f24',
759
- red: '#b91c1c',
760
- green: '#116a2e',
761
- yellow: '#8a5a1b',
762
- blue: '#0b57d0',
763
- magenta: '#a0186f',
764
- cyan: '#0e7490',
765
- white: '#57606a',
766
- brightBlack: '#57606a',
767
- brightRed: '#c94d4d',
768
- brightGreen: '#1f9d4f',
769
- brightYellow: '#a9752c',
770
- brightBlue: '#3b82f6',
771
- brightMagenta: '#c04a92',
772
- brightCyan: '#0891b2',
773
- brightWhite: '#24292f',
774
- },
775
- cursor: '#24292f',
776
- selection: '#b6c7ff',
777
- },
778
- ])
846
+ export const AUTHORED_SOURCES: readonly AuthoredSource[] = Object.freeze([])