@adea-ai/themes 0.3.2 → 0.4.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.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The accent axis.
3
+ *
4
+ * A theme has one interactive colour — its primary — and the accent axis lets a
5
+ * user replace it without changing the theme. That is a different question from
6
+ * "which palette do you want": a user may like Catppuccin's surfaces and still want
7
+ * a violet primary, and a product may ship a brand colour that has to survive every
8
+ * theme in the catalogue.
9
+ *
10
+ * ## Why the presets live here
11
+ *
12
+ * They are colour data with a light/dark pair per preset — structurally the same
13
+ * thing as a theme's primary, and the last colour a consumer should have to author
14
+ * itself. Keeping them in a consumer meant two things that must agree lived in two
15
+ * places: the preset list, and the CSS blocks generated from it. They were written
16
+ * by hand in both, and they drifted — the generated default moved its hover *toward*
17
+ * the canvas while every hand-written accent moved *away*, so the default primary
18
+ * button hovered backwards and picking any accent silently fixed it.
19
+ *
20
+ * Here the values and the derivation are one module, so a consumer emits both from
21
+ * the same rule and cannot produce that disagreement.
22
+ *
23
+ * ## The hover rule
24
+ *
25
+ * A hovered primary moves **away from the canvas**, which is what makes it read as
26
+ * hovered rather than as a second, slightly different colour.
27
+ *
28
+ * Two rules were tried here before this one, and both are worth recording because
29
+ * each looks right until it is measured:
30
+ *
31
+ * - **Toward `--background`** — what the consumer used to do. Inverted in both
32
+ * appearances: it moves the button toward the page it sits on, so a hovered button
33
+ * recedes.
34
+ * - **Toward `--foreground`** — correct in principle, since the foreground is
35
+ * normally away from the canvas, and wrong in practice: a dark theme's foreground
36
+ * is mid-lightness (`0.79` in the default), so an accent lighter than that — the
37
+ * dark green is `0.80` — moves *toward* the canvas by a hair.
38
+ * - **Toward white or black** — always the right direction, but a *proportional*
39
+ * step: a mid-lightness light accent moves ~0.10 and a pale dark accent ~0.03, so
40
+ * the same rule gives one appearance a hover twice as strong as the other's.
41
+ *
42
+ * A uniform step needs a resolved value, which is why {@link primaryHover} returns
43
+ * one and why the tint below can stay an expression. `shiftLightness` is what keeps
44
+ * the step uniform *and* the hue intact — a mix toward a neutral cannot do both.
45
+ */
46
+ import type { ThemeAppearance } from './schema';
47
+ /** One accent preset: a named primary, per appearance. */
48
+ export type AccentPreset = {
49
+ /** The `data-accent` value. */
50
+ id: string;
51
+ label: string;
52
+ /** What the preset is for, in a gallery or a picker. */
53
+ description: string;
54
+ /** The accent as it appears on a light theme. */
55
+ light: string;
56
+ /** The accent as it appears on a dark theme. */
57
+ dark: string;
58
+ };
59
+ /**
60
+ * The presets.
61
+ *
62
+ * Each is a pair rather than one colour, because the same accent cannot be one
63
+ * value across both appearances: a violet that reads as vivid on a dark canvas is
64
+ * muddy on a light one, and the deep form that works on light is dull on dark. The
65
+ * pairs below are chosen so each appearance gets the vibrancy the other's value
66
+ * would lose.
67
+ */
68
+ export declare const ACCENTS: readonly AccentPreset[];
69
+ /** The accent ids, for validating a stored preference without loading the data. */
70
+ export declare const ACCENT_IDS: readonly string[];
71
+ /** Look a preset up by id. */
72
+ export declare function getAccent(id: string): AccentPreset | undefined;
73
+ /** The accent's value for one appearance. */
74
+ export declare function accentValue(preset: AccentPreset, appearance: ThemeAppearance): string;
75
+ /**
76
+ * The label to draw on an accent fill: black or white, whichever wins on contrast.
77
+ *
78
+ * Measured rather than conventional. The usual convention is "white on a coloured
79
+ * fill", which is wrong for the bright half of this set — white on `#a78bfa` is
80
+ * about 2.1:1, and black on it is about 9.9:1. That is why the same accent carries a
81
+ * black label in dark mode and a white one in light: the pair is designed so the
82
+ * polarity flips, and the label has to flip with it.
83
+ */
84
+ export declare function accentForeground(accent: string): string;
85
+ /** The contrast the chosen label achieves, for validation and reporting. */
86
+ export declare function accentForegroundContrast(accent: string): number;
87
+ /**
88
+ * How far a hovered primary moves along lightness.
89
+ *
90
+ * A **uniform step**, not a proportional one, and the difference matters. Mixing a
91
+ * fixed share toward white or black moves an accent by a share of its headroom, so
92
+ * the same rule gives a mid-lightness light accent a step of ~0.10 and a pale dark
93
+ * accent one of ~0.03 — a hover that feels twice as strong in one appearance as the
94
+ * other, and nearly invisible on the palest accent.
95
+ *
96
+ * 0.05 is the step this system's accents were already hand-written with, so the
97
+ * accents keep the appearance they had while the rule becomes one value.
98
+ */
99
+ export declare const ACCENT_HOVER_STEP = 0.05;
100
+ /**
101
+ * The hovered primary, as a resolved colour.
102
+ *
103
+ * Resolved rather than expressed as a `color-mix()`, because a uniform step cannot
104
+ * be written as a mix — see {@link ACCENT_HOVER_STEP}. That means it does **not**
105
+ * follow `--primary` on its own, so a consumer that applies themes at runtime has to
106
+ * write this token too; the alternative is a hover left over from the previous
107
+ * theme's primary. `themeCssVariables` includes it for that reason.
108
+ *
109
+ * The direction is away from the canvas: darker on a light theme, lighter on a dark
110
+ * one. `shiftLightness` keeps chroma and hue and damps chroma near the extremes, so a
111
+ * hovered accent stays the same colour rather than washing out toward grey.
112
+ */
113
+ export declare function primaryHover(primary: string, appearance: ThemeAppearance): string;
114
+ /**
115
+ * How much of the primary a tint carries, per appearance.
116
+ *
117
+ * Not one value, because a tint's visibility depends on what it is tinted onto: the
118
+ * same alpha over a near-black surface reads as nothing, and over a near-white one
119
+ * reads as a wash. The dark value is the larger for exactly that reason.
120
+ *
121
+ * This one *is* a `color-mix()`, because a tint is a share of the primary by nature
122
+ * and follows `--primary` correctly at runtime.
123
+ */
124
+ export declare const ACCENT_SUBTLE_ALPHA: Readonly<Record<ThemeAppearance, number>>;
125
+ /**
126
+ * The tint expression, for any primary.
127
+ *
128
+ * Takes the appearance because the alpha is appearance-dependent — see
129
+ * {@link ACCENT_SUBTLE_ALPHA}.
130
+ */
131
+ export declare function primarySubtleCss(appearance: ThemeAppearance, primaryVariable?: string): string;
132
+ /**
133
+ * The five declarations an accent sets, as CSS values.
134
+ *
135
+ * `primary`, `primaryForeground` and `ring` are resolved colours; `primaryHover` and
136
+ * `primarySubtle` are expressions, for the reason in the module comment. A consumer
137
+ * writes them as custom properties and the rules hold for every theme and every
138
+ * accent at once.
139
+ */
140
+ export type AccentRoles = {
141
+ primary: string;
142
+ primaryForeground: string;
143
+ primaryHover: string;
144
+ primarySubtle: string;
145
+ ring: string;
146
+ };
147
+ export declare function accentRoles(preset: AccentPreset, appearance: ThemeAppearance,
148
+ /** The property the subtle tint is built from. A consumer may pass its own alias. */
149
+ primaryVariable?: string): AccentRoles;
150
+ //# sourceMappingURL=accents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accents.d.ts","sourceRoot":"","sources":["../src/accents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAI/C,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG;IACzB,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,EAAE,SAAS,YAAY,EA2CzC,CAAA;AAEF,mFAAmF;AACnF,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EAAsD,CAAA;AAE9F,8BAA8B;AAC9B,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAE9D;AAED,6CAA6C;AAC7C,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,GAAG,MAAM,CAErF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAQvD;AAED,4EAA4E;AAC5E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAA;AAErC;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,GAAG,MAAM,CAKjF;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAGxE,CAAA;AAEF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,eAAe,EAAE,eAAe,SAAc,GAAG,MAAM,CAEnG;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,iBAAiB,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,eAAe;AAC3B,qFAAqF;AACrF,eAAe,SAAc,GAC5B,WAAW,CAYb"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * The accent axis.
3
+ *
4
+ * A theme has one interactive colour — its primary — and the accent axis lets a
5
+ * user replace it without changing the theme. That is a different question from
6
+ * "which palette do you want": a user may like Catppuccin's surfaces and still want
7
+ * a violet primary, and a product may ship a brand colour that has to survive every
8
+ * theme in the catalogue.
9
+ *
10
+ * ## Why the presets live here
11
+ *
12
+ * They are colour data with a light/dark pair per preset — structurally the same
13
+ * thing as a theme's primary, and the last colour a consumer should have to author
14
+ * itself. Keeping them in a consumer meant two things that must agree lived in two
15
+ * places: the preset list, and the CSS blocks generated from it. They were written
16
+ * by hand in both, and they drifted — the generated default moved its hover *toward*
17
+ * the canvas while every hand-written accent moved *away*, so the default primary
18
+ * button hovered backwards and picking any accent silently fixed it.
19
+ *
20
+ * Here the values and the derivation are one module, so a consumer emits both from
21
+ * the same rule and cannot produce that disagreement.
22
+ *
23
+ * ## The hover rule
24
+ *
25
+ * A hovered primary moves **away from the canvas**, which is what makes it read as
26
+ * hovered rather than as a second, slightly different colour.
27
+ *
28
+ * Two rules were tried here before this one, and both are worth recording because
29
+ * each looks right until it is measured:
30
+ *
31
+ * - **Toward `--background`** — what the consumer used to do. Inverted in both
32
+ * appearances: it moves the button toward the page it sits on, so a hovered button
33
+ * recedes.
34
+ * - **Toward `--foreground`** — correct in principle, since the foreground is
35
+ * normally away from the canvas, and wrong in practice: a dark theme's foreground
36
+ * is mid-lightness (`0.79` in the default), so an accent lighter than that — the
37
+ * dark green is `0.80` — moves *toward* the canvas by a hair.
38
+ * - **Toward white or black** — always the right direction, but a *proportional*
39
+ * step: a mid-lightness light accent moves ~0.10 and a pale dark accent ~0.03, so
40
+ * the same rule gives one appearance a hover twice as strong as the other's.
41
+ *
42
+ * A uniform step needs a resolved value, which is why {@link primaryHover} returns
43
+ * one and why the tint below can stay an expression. `shiftLightness` is what keeps
44
+ * the step uniform *and* the hue intact — a mix toward a neutral cannot do both.
45
+ */
46
+ import { contrastRatio, formatOklch, parseColor, shiftLightness } from './oklch';
47
+ /**
48
+ * The presets.
49
+ *
50
+ * Each is a pair rather than one colour, because the same accent cannot be one
51
+ * value across both appearances: a violet that reads as vivid on a dark canvas is
52
+ * muddy on a light one, and the deep form that works on light is dull on dark. The
53
+ * pairs below are chosen so each appearance gets the vibrancy the other's value
54
+ * would lose.
55
+ */
56
+ export const ACCENTS = Object.freeze([
57
+ {
58
+ id: 'violet',
59
+ label: 'Violet',
60
+ description: 'The default brand accent.',
61
+ light: '#6d28d9',
62
+ dark: '#a78bfa',
63
+ },
64
+ {
65
+ id: 'blue',
66
+ label: 'Blue',
67
+ description: 'Cool and conventional. Reads as informational.',
68
+ light: '#2563eb',
69
+ dark: '#60a5fa',
70
+ },
71
+ {
72
+ id: 'green',
73
+ label: 'Green',
74
+ description: 'Reads as confirmatory, which competes with the success status.',
75
+ light: '#15803d',
76
+ dark: '#4ade80',
77
+ },
78
+ {
79
+ id: 'amber',
80
+ label: 'Amber',
81
+ description: 'Warm and attention-drawing. Competes with the warning status.',
82
+ light: '#b45309',
83
+ dark: '#fbbf24',
84
+ },
85
+ {
86
+ id: 'cyan',
87
+ label: 'Cyan',
88
+ description: 'Quiet and technical. The least saturated of the set.',
89
+ light: '#0e7490',
90
+ dark: '#22d3ee',
91
+ },
92
+ {
93
+ id: 'pink',
94
+ label: 'Pink',
95
+ description: 'The loudest of the set. Use where the accent is decorative.',
96
+ light: '#be185d',
97
+ dark: '#f472b6',
98
+ },
99
+ ]);
100
+ /** The accent ids, for validating a stored preference without loading the data. */
101
+ export const ACCENT_IDS = Object.freeze(ACCENTS.map((accent) => accent.id));
102
+ /** Look a preset up by id. */
103
+ export function getAccent(id) {
104
+ return ACCENTS.find((accent) => accent.id === id);
105
+ }
106
+ /** The accent's value for one appearance. */
107
+ export function accentValue(preset, appearance) {
108
+ return appearance === 'light' ? preset.light : preset.dark;
109
+ }
110
+ /**
111
+ * The label to draw on an accent fill: black or white, whichever wins on contrast.
112
+ *
113
+ * Measured rather than conventional. The usual convention is "white on a coloured
114
+ * fill", which is wrong for the bright half of this set — white on `#a78bfa` is
115
+ * about 2.1:1, and black on it is about 9.9:1. That is why the same accent carries a
116
+ * black label in dark mode and a white one in light: the pair is designed so the
117
+ * polarity flips, and the label has to flip with it.
118
+ */
119
+ export function accentForeground(accent) {
120
+ const fill = parseColor(accent);
121
+ if (!fill)
122
+ return 'oklch(1 0 0)';
123
+ const black = { l: 0, c: 0, h: 0 };
124
+ const white = { l: 1, c: 0, h: 0 };
125
+ const best = contrastRatio(black, fill) >= contrastRatio(white, fill) ? black : white;
126
+ return formatOklch(best);
127
+ }
128
+ /** The contrast the chosen label achieves, for validation and reporting. */
129
+ export function accentForegroundContrast(accent) {
130
+ const fill = parseColor(accent);
131
+ if (!fill)
132
+ return 0;
133
+ return contrastRatio(parseColor(accentForeground(accent)), fill);
134
+ }
135
+ /**
136
+ * How far a hovered primary moves along lightness.
137
+ *
138
+ * A **uniform step**, not a proportional one, and the difference matters. Mixing a
139
+ * fixed share toward white or black moves an accent by a share of its headroom, so
140
+ * the same rule gives a mid-lightness light accent a step of ~0.10 and a pale dark
141
+ * accent one of ~0.03 — a hover that feels twice as strong in one appearance as the
142
+ * other, and nearly invisible on the palest accent.
143
+ *
144
+ * 0.05 is the step this system's accents were already hand-written with, so the
145
+ * accents keep the appearance they had while the rule becomes one value.
146
+ */
147
+ export const ACCENT_HOVER_STEP = 0.05;
148
+ /**
149
+ * The hovered primary, as a resolved colour.
150
+ *
151
+ * Resolved rather than expressed as a `color-mix()`, because a uniform step cannot
152
+ * be written as a mix — see {@link ACCENT_HOVER_STEP}. That means it does **not**
153
+ * follow `--primary` on its own, so a consumer that applies themes at runtime has to
154
+ * write this token too; the alternative is a hover left over from the previous
155
+ * theme's primary. `themeCssVariables` includes it for that reason.
156
+ *
157
+ * The direction is away from the canvas: darker on a light theme, lighter on a dark
158
+ * one. `shiftLightness` keeps chroma and hue and damps chroma near the extremes, so a
159
+ * hovered accent stays the same colour rather than washing out toward grey.
160
+ */
161
+ export function primaryHover(primary, appearance) {
162
+ const parsed = parseColor(primary);
163
+ if (!parsed)
164
+ return primary;
165
+ const delta = appearance === 'light' ? -ACCENT_HOVER_STEP : ACCENT_HOVER_STEP;
166
+ return formatOklch(shiftLightness(parsed, delta));
167
+ }
168
+ /**
169
+ * How much of the primary a tint carries, per appearance.
170
+ *
171
+ * Not one value, because a tint's visibility depends on what it is tinted onto: the
172
+ * same alpha over a near-black surface reads as nothing, and over a near-white one
173
+ * reads as a wash. The dark value is the larger for exactly that reason.
174
+ *
175
+ * This one *is* a `color-mix()`, because a tint is a share of the primary by nature
176
+ * and follows `--primary` correctly at runtime.
177
+ */
178
+ export const ACCENT_SUBTLE_ALPHA = Object.freeze({
179
+ light: 10,
180
+ dark: 16,
181
+ });
182
+ /**
183
+ * The tint expression, for any primary.
184
+ *
185
+ * Takes the appearance because the alpha is appearance-dependent — see
186
+ * {@link ACCENT_SUBTLE_ALPHA}.
187
+ */
188
+ export function primarySubtleCss(appearance, primaryVariable = '--primary') {
189
+ return `color-mix(in oklch, var(${primaryVariable}) ${ACCENT_SUBTLE_ALPHA[appearance]}%, transparent)`;
190
+ }
191
+ export function accentRoles(preset, appearance,
192
+ /** The property the subtle tint is built from. A consumer may pass its own alias. */
193
+ primaryVariable = '--primary') {
194
+ const value = accentValue(preset, appearance);
195
+ const parsed = parseColor(value);
196
+ const primary = parsed ? formatOklch(parsed) : value;
197
+ return {
198
+ primary,
199
+ primaryForeground: accentForeground(value),
200
+ primaryHover: primaryHover(value, appearance),
201
+ primarySubtle: primarySubtleCss(appearance, primaryVariable),
202
+ ring: primary,
203
+ };
204
+ }
205
+ //# sourceMappingURL=accents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accents.js","sourceRoot":"","sources":["../src/accents.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -44,6 +44,8 @@
44
44
  */
45
45
  export type { AdeaAnsi, AdeaTheme, AdeaThemeColors, AdeaThemeRecord, AnsiKey, ThemeAppearance, ThemeColorKey, ThemeFamily, ThemeProvenance, } from './schema';
46
46
  export { ANSI_KEYS, THEME_COLOR_KEYS } from './schema';
47
+ export type { AccentPreset, AccentRoles } from './accents';
48
+ export { ACCENTS, ACCENT_HOVER_STEP, ACCENT_IDS, ACCENT_SUBTLE_ALPHA, accentForeground, accentForegroundContrast, accentRoles, accentValue, getAccent, primaryHover, primarySubtleCss, } from './accents';
47
49
  export { DEFAULT_THEME_IDS, getBase24Scheme, getTheme, hasTheme, resolveTheme, themeCount, themeFamilies, themeIds, themes, themesByAppearance, toTheme, } from './catalogue';
48
50
  export type { ContrastRepair, Oklch } from './oklch';
49
51
  export { contrastRatio, deltaEok, formatOklch, gamutMap, hexToOklch, inGamut, mix, oklchToHex, parseColor, parseOklch, relativeLuminance, repairContrast, shiftLightness, } from './oklch';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAGH,YAAY,EACV,QAAQ,EACR,SAAS,EACT,eAAe,EACf,eAAe,EACf,OAAO,EACP,eAAe,EACf,aAAa,EACb,WAAW,EACX,eAAe,GAChB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAGtD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,aAAa,EACb,QAAQ,EACR,MAAM,EACN,kBAAkB,EAClB,OAAO,GACR,MAAM,aAAa,CAAA;AAGpB,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,EACL,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,UAAU,EACV,OAAO,EACP,GAAG,EACH,UAAU,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,cAAc,GACf,MAAM,SAAS,CAAA;AAGhB,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACzE,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAA;AAGnB,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACzF,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAG7D,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,IAAI,GACL,MAAM,UAAU,CAAA;AAGjB,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAChF,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,GACT,MAAM,mBAAmB,CAAA;AAE1B,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAE3F,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACjF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAE9D,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAGH,YAAY,EACV,QAAQ,EACR,SAAS,EACT,eAAe,EACf,eAAe,EACf,OAAO,EACP,eAAe,EACf,aAAa,EACb,WAAW,EACX,eAAe,GAChB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAEtD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,EACL,OAAO,EACP,iBAAiB,EACjB,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,WAAW,EACX,SAAS,EACT,YAAY,EACZ,gBAAgB,GACjB,MAAM,WAAW,CAAA;AAGlB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,aAAa,EACb,QAAQ,EACR,MAAM,EACN,kBAAkB,EAClB,OAAO,GACR,MAAM,aAAa,CAAA;AAGpB,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,EACL,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,UAAU,EACV,OAAO,EACP,GAAG,EACH,UAAU,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,cAAc,GACf,MAAM,SAAS,CAAA;AAGhB,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACzE,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAA;AAGnB,YAAY,EAAE,oBAAoB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACzF,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAG7D,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,IAAI,GACL,MAAM,UAAU,CAAA;AAGjB,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAChF,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,GACT,MAAM,mBAAmB,CAAA;AAE1B,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAE3F,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAErD,YAAY,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACjF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAE9D,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA"}
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@
43
43
  * ```
44
44
  */
45
45
  export { ANSI_KEYS, THEME_COLOR_KEYS } from './schema';
46
+ export { ACCENTS, ACCENT_HOVER_STEP, ACCENT_IDS, ACCENT_SUBTLE_ALPHA, accentForeground, accentForegroundContrast, accentRoles, accentValue, getAccent, primaryHover, primarySubtleCss, } from './accents';
46
47
  /* --- The catalogue ------------------------------------------------------ */
47
48
  export { DEFAULT_THEME_IDS, getBase24Scheme, getTheme, hasTheme, resolveTheme, themeCount, themeFamilies, themeIds, themes, themesByAppearance, toTheme, } from './catalogue';
48
49
  export { contrastRatio, deltaEok, formatOklch, gamutMap, hexToOklch, inGamut, mix, oklchToHex, parseColor, parseOklch, relativeLuminance, repairContrast, shiftLightness, } from './oklch';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAcH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAEtD,8EAA8E;AAC9E,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,aAAa,EACb,QAAQ,EACR,MAAM,EACN,kBAAkB,EAClB,OAAO,GACR,MAAM,aAAa,CAAA;AAIpB,OAAO,EACL,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,UAAU,EACV,OAAO,EACP,GAAG,EACH,UAAU,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,cAAc,GACf,MAAM,SAAS,CAAA;AAIhB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAA;AAInB,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAI7D,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,IAAI,GACL,MAAM,UAAU,CAAA;AAIjB,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,GACT,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAG/C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAG3F,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAGrD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAE9D,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAcH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAGtD,OAAO,EACL,OAAO,EACP,iBAAiB,EACjB,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,WAAW,EACX,SAAS,EACT,YAAY,EACZ,gBAAgB,GACjB,MAAM,WAAW,CAAA;AAElB,8EAA8E;AAC9E,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,aAAa,EACb,QAAQ,EACR,MAAM,EACN,kBAAkB,EAClB,OAAO,GACR,MAAM,aAAa,CAAA;AAIpB,OAAO,EACL,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,UAAU,EACV,OAAO,EACP,GAAG,EACH,UAAU,EACV,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,cAAc,GACf,MAAM,SAAS,CAAA;AAIhB,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAA;AAInB,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAI7D,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,mBAAmB,EACnB,WAAW,EACX,cAAc,EACd,IAAI,GACL,MAAM,UAAU,CAAA;AAIjB,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,QAAQ,GACT,MAAM,mBAAmB,CAAA;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAG/C,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAG3F,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAGrD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAE9D,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/themes",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "The Adea theme catalogue: semantic OKLCH themes normalized from upstream palettes, with Base24, terminal, CSS and syntax adapters.",
5
5
  "keywords": [
6
6
  "adea",
@@ -84,6 +84,7 @@
84
84
  },
85
85
  "scripts": {
86
86
  "build": "tsc -p tsconfig.build.json",
87
+ "prepublishOnly": "bun run build",
87
88
  "typecheck": "tsc -p tsconfig.json --noEmit",
88
89
  "lint": "oxlint",
89
90
  "test": "bun test tests",
package/src/accents.ts ADDED
@@ -0,0 +1,249 @@
1
+ /**
2
+ * The accent axis.
3
+ *
4
+ * A theme has one interactive colour — its primary — and the accent axis lets a
5
+ * user replace it without changing the theme. That is a different question from
6
+ * "which palette do you want": a user may like Catppuccin's surfaces and still want
7
+ * a violet primary, and a product may ship a brand colour that has to survive every
8
+ * theme in the catalogue.
9
+ *
10
+ * ## Why the presets live here
11
+ *
12
+ * They are colour data with a light/dark pair per preset — structurally the same
13
+ * thing as a theme's primary, and the last colour a consumer should have to author
14
+ * itself. Keeping them in a consumer meant two things that must agree lived in two
15
+ * places: the preset list, and the CSS blocks generated from it. They were written
16
+ * by hand in both, and they drifted — the generated default moved its hover *toward*
17
+ * the canvas while every hand-written accent moved *away*, so the default primary
18
+ * button hovered backwards and picking any accent silently fixed it.
19
+ *
20
+ * Here the values and the derivation are one module, so a consumer emits both from
21
+ * the same rule and cannot produce that disagreement.
22
+ *
23
+ * ## The hover rule
24
+ *
25
+ * A hovered primary moves **away from the canvas**, which is what makes it read as
26
+ * hovered rather than as a second, slightly different colour.
27
+ *
28
+ * Two rules were tried here before this one, and both are worth recording because
29
+ * each looks right until it is measured:
30
+ *
31
+ * - **Toward `--background`** — what the consumer used to do. Inverted in both
32
+ * appearances: it moves the button toward the page it sits on, so a hovered button
33
+ * recedes.
34
+ * - **Toward `--foreground`** — correct in principle, since the foreground is
35
+ * normally away from the canvas, and wrong in practice: a dark theme's foreground
36
+ * is mid-lightness (`0.79` in the default), so an accent lighter than that — the
37
+ * dark green is `0.80` — moves *toward* the canvas by a hair.
38
+ * - **Toward white or black** — always the right direction, but a *proportional*
39
+ * step: a mid-lightness light accent moves ~0.10 and a pale dark accent ~0.03, so
40
+ * the same rule gives one appearance a hover twice as strong as the other's.
41
+ *
42
+ * A uniform step needs a resolved value, which is why {@link primaryHover} returns
43
+ * one and why the tint below can stay an expression. `shiftLightness` is what keeps
44
+ * the step uniform *and* the hue intact — a mix toward a neutral cannot do both.
45
+ */
46
+
47
+ import type { ThemeAppearance } from './schema'
48
+ import type { Oklch } from './oklch'
49
+ import { contrastRatio, formatOklch, parseColor, shiftLightness } from './oklch'
50
+
51
+ /** One accent preset: a named primary, per appearance. */
52
+ export type AccentPreset = {
53
+ /** The `data-accent` value. */
54
+ id: string
55
+ label: string
56
+ /** What the preset is for, in a gallery or a picker. */
57
+ description: string
58
+ /** The accent as it appears on a light theme. */
59
+ light: string
60
+ /** The accent as it appears on a dark theme. */
61
+ dark: string
62
+ }
63
+
64
+ /**
65
+ * The presets.
66
+ *
67
+ * Each is a pair rather than one colour, because the same accent cannot be one
68
+ * value across both appearances: a violet that reads as vivid on a dark canvas is
69
+ * muddy on a light one, and the deep form that works on light is dull on dark. The
70
+ * pairs below are chosen so each appearance gets the vibrancy the other's value
71
+ * would lose.
72
+ */
73
+ export const ACCENTS: readonly AccentPreset[] = Object.freeze([
74
+ {
75
+ id: 'violet',
76
+ label: 'Violet',
77
+ description: 'The default brand accent.',
78
+ light: '#6d28d9',
79
+ dark: '#a78bfa',
80
+ },
81
+ {
82
+ id: 'blue',
83
+ label: 'Blue',
84
+ description: 'Cool and conventional. Reads as informational.',
85
+ light: '#2563eb',
86
+ dark: '#60a5fa',
87
+ },
88
+ {
89
+ id: 'green',
90
+ label: 'Green',
91
+ description: 'Reads as confirmatory, which competes with the success status.',
92
+ light: '#15803d',
93
+ dark: '#4ade80',
94
+ },
95
+ {
96
+ id: 'amber',
97
+ label: 'Amber',
98
+ description: 'Warm and attention-drawing. Competes with the warning status.',
99
+ light: '#b45309',
100
+ dark: '#fbbf24',
101
+ },
102
+ {
103
+ id: 'cyan',
104
+ label: 'Cyan',
105
+ description: 'Quiet and technical. The least saturated of the set.',
106
+ light: '#0e7490',
107
+ dark: '#22d3ee',
108
+ },
109
+ {
110
+ id: 'pink',
111
+ label: 'Pink',
112
+ description: 'The loudest of the set. Use where the accent is decorative.',
113
+ light: '#be185d',
114
+ dark: '#f472b6',
115
+ },
116
+ ])
117
+
118
+ /** The accent ids, for validating a stored preference without loading the data. */
119
+ export const ACCENT_IDS: readonly string[] = Object.freeze(ACCENTS.map((accent) => accent.id))
120
+
121
+ /** Look a preset up by id. */
122
+ export function getAccent(id: string): AccentPreset | undefined {
123
+ return ACCENTS.find((accent) => accent.id === id)
124
+ }
125
+
126
+ /** The accent's value for one appearance. */
127
+ export function accentValue(preset: AccentPreset, appearance: ThemeAppearance): string {
128
+ return appearance === 'light' ? preset.light : preset.dark
129
+ }
130
+
131
+ /**
132
+ * The label to draw on an accent fill: black or white, whichever wins on contrast.
133
+ *
134
+ * Measured rather than conventional. The usual convention is "white on a coloured
135
+ * fill", which is wrong for the bright half of this set — white on `#a78bfa` is
136
+ * about 2.1:1, and black on it is about 9.9:1. That is why the same accent carries a
137
+ * black label in dark mode and a white one in light: the pair is designed so the
138
+ * polarity flips, and the label has to flip with it.
139
+ */
140
+ export function accentForeground(accent: string): string {
141
+ const fill = parseColor(accent)
142
+ if (!fill) return 'oklch(1 0 0)'
143
+
144
+ const black: Oklch = { l: 0, c: 0, h: 0 }
145
+ const white: Oklch = { l: 1, c: 0, h: 0 }
146
+ const best = contrastRatio(black, fill) >= contrastRatio(white, fill) ? black : white
147
+ return formatOklch(best)
148
+ }
149
+
150
+ /** The contrast the chosen label achieves, for validation and reporting. */
151
+ export function accentForegroundContrast(accent: string): number {
152
+ const fill = parseColor(accent)
153
+ if (!fill) return 0
154
+ return contrastRatio(parseColor(accentForeground(accent))!, fill)
155
+ }
156
+
157
+ /**
158
+ * How far a hovered primary moves along lightness.
159
+ *
160
+ * A **uniform step**, not a proportional one, and the difference matters. Mixing a
161
+ * fixed share toward white or black moves an accent by a share of its headroom, so
162
+ * the same rule gives a mid-lightness light accent a step of ~0.10 and a pale dark
163
+ * accent one of ~0.03 — a hover that feels twice as strong in one appearance as the
164
+ * other, and nearly invisible on the palest accent.
165
+ *
166
+ * 0.05 is the step this system's accents were already hand-written with, so the
167
+ * accents keep the appearance they had while the rule becomes one value.
168
+ */
169
+ export const ACCENT_HOVER_STEP = 0.05
170
+
171
+ /**
172
+ * The hovered primary, as a resolved colour.
173
+ *
174
+ * Resolved rather than expressed as a `color-mix()`, because a uniform step cannot
175
+ * be written as a mix — see {@link ACCENT_HOVER_STEP}. That means it does **not**
176
+ * follow `--primary` on its own, so a consumer that applies themes at runtime has to
177
+ * write this token too; the alternative is a hover left over from the previous
178
+ * theme's primary. `themeCssVariables` includes it for that reason.
179
+ *
180
+ * The direction is away from the canvas: darker on a light theme, lighter on a dark
181
+ * one. `shiftLightness` keeps chroma and hue and damps chroma near the extremes, so a
182
+ * hovered accent stays the same colour rather than washing out toward grey.
183
+ */
184
+ export function primaryHover(primary: string, appearance: ThemeAppearance): string {
185
+ const parsed = parseColor(primary)
186
+ if (!parsed) return primary
187
+ const delta = appearance === 'light' ? -ACCENT_HOVER_STEP : ACCENT_HOVER_STEP
188
+ return formatOklch(shiftLightness(parsed, delta))
189
+ }
190
+
191
+ /**
192
+ * How much of the primary a tint carries, per appearance.
193
+ *
194
+ * Not one value, because a tint's visibility depends on what it is tinted onto: the
195
+ * same alpha over a near-black surface reads as nothing, and over a near-white one
196
+ * reads as a wash. The dark value is the larger for exactly that reason.
197
+ *
198
+ * This one *is* a `color-mix()`, because a tint is a share of the primary by nature
199
+ * and follows `--primary` correctly at runtime.
200
+ */
201
+ export const ACCENT_SUBTLE_ALPHA: Readonly<Record<ThemeAppearance, number>> = Object.freeze({
202
+ light: 10,
203
+ dark: 16,
204
+ })
205
+
206
+ /**
207
+ * The tint expression, for any primary.
208
+ *
209
+ * Takes the appearance because the alpha is appearance-dependent — see
210
+ * {@link ACCENT_SUBTLE_ALPHA}.
211
+ */
212
+ export function primarySubtleCss(appearance: ThemeAppearance, primaryVariable = '--primary'): string {
213
+ return `color-mix(in oklch, var(${primaryVariable}) ${ACCENT_SUBTLE_ALPHA[appearance]}%, transparent)`
214
+ }
215
+
216
+ /**
217
+ * The five declarations an accent sets, as CSS values.
218
+ *
219
+ * `primary`, `primaryForeground` and `ring` are resolved colours; `primaryHover` and
220
+ * `primarySubtle` are expressions, for the reason in the module comment. A consumer
221
+ * writes them as custom properties and the rules hold for every theme and every
222
+ * accent at once.
223
+ */
224
+ export type AccentRoles = {
225
+ primary: string
226
+ primaryForeground: string
227
+ primaryHover: string
228
+ primarySubtle: string
229
+ ring: string
230
+ }
231
+
232
+ export function accentRoles(
233
+ preset: AccentPreset,
234
+ appearance: ThemeAppearance,
235
+ /** The property the subtle tint is built from. A consumer may pass its own alias. */
236
+ primaryVariable = '--primary'
237
+ ): AccentRoles {
238
+ const value = accentValue(preset, appearance)
239
+ const parsed = parseColor(value)
240
+ const primary = parsed ? formatOklch(parsed) : value
241
+
242
+ return {
243
+ primary,
244
+ primaryForeground: accentForeground(value),
245
+ primaryHover: primaryHover(value, appearance),
246
+ primarySubtle: primarySubtleCss(appearance, primaryVariable),
247
+ ring: primary,
248
+ }
249
+ }
package/src/index.ts CHANGED
@@ -57,6 +57,21 @@ export type {
57
57
  } from './schema'
58
58
  export { ANSI_KEYS, THEME_COLOR_KEYS } from './schema'
59
59
 
60
+ export type { AccentPreset, AccentRoles } from './accents'
61
+ export {
62
+ ACCENTS,
63
+ ACCENT_HOVER_STEP,
64
+ ACCENT_IDS,
65
+ ACCENT_SUBTLE_ALPHA,
66
+ accentForeground,
67
+ accentForegroundContrast,
68
+ accentRoles,
69
+ accentValue,
70
+ getAccent,
71
+ primaryHover,
72
+ primarySubtleCss,
73
+ } from './accents'
74
+
60
75
  /* --- The catalogue ------------------------------------------------------ */
61
76
  export {
62
77
  DEFAULT_THEME_IDS,