@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.
- package/dist/accents.d.ts +150 -0
- package/dist/accents.d.ts.map +1 -0
- package/dist/accents.js +205 -0
- package/dist/accents.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/accents.ts +249 -0
- package/src/index.ts +15 -0
|
@@ -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"}
|
package/dist/accents.js
ADDED
|
@@ -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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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
|
+
"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,
|