theme-vir 28.24.0 → 28.25.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.
|
@@ -43,6 +43,17 @@ export type ArrayOrSelectParam<T extends PropertyKey> = ReadonlyArray<T> | Reado
|
|
|
43
43
|
export declare const defaultLightThemePair: RequiredAndNotNull<NoRefColorInit>;
|
|
44
44
|
/** @category Internal */
|
|
45
45
|
export declare const defaultContrastLevels: Readonly<ArrayOrSelectParam<ContrastLevelName>>;
|
|
46
|
+
/**
|
|
47
|
+
* Extra contrast levels generated by {@link buildColorTheme} beyond the standard `ContrastLevelName`
|
|
48
|
+
* values. `highest-contrast` always picks the palette color with the most contrast against the
|
|
49
|
+
* fixed color; `lowest-contrast` always picks the closest.
|
|
50
|
+
*
|
|
51
|
+
* @category Internal
|
|
52
|
+
*/
|
|
53
|
+
export declare enum ExtremeContrastLevel {
|
|
54
|
+
HighestContrast = "highest-contrast",
|
|
55
|
+
LowestContrast = "lowest-contrast"
|
|
56
|
+
}
|
|
46
57
|
/**
|
|
47
58
|
* Options for {@link buildColorTheme}.
|
|
48
59
|
*
|
|
@@ -90,9 +90,56 @@ export const defaultLightThemePair = {
|
|
|
90
90
|
};
|
|
91
91
|
/** @category Internal */
|
|
92
92
|
export const defaultContrastLevels = getEnumValues(ContrastLevelName);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Extra contrast levels generated by {@link buildColorTheme} beyond the standard `ContrastLevelName`
|
|
95
|
+
* values. `highest-contrast` always picks the palette color with the most contrast against the
|
|
96
|
+
* fixed color; `lowest-contrast` always picks the closest.
|
|
97
|
+
*
|
|
98
|
+
* @category Internal
|
|
99
|
+
*/
|
|
100
|
+
export var ExtremeContrastLevel;
|
|
101
|
+
(function (ExtremeContrastLevel) {
|
|
102
|
+
ExtremeContrastLevel["HighestContrast"] = "highest-contrast";
|
|
103
|
+
ExtremeContrastLevel["LowestContrast"] = "lowest-contrast";
|
|
104
|
+
})(ExtremeContrastLevel || (ExtremeContrastLevel = {}));
|
|
105
|
+
/**
|
|
106
|
+
* Picks the absolute lightest or darkest palette color based on which extreme produces the most (or
|
|
107
|
+
* least) contrast against the fixed color.
|
|
108
|
+
*/
|
|
109
|
+
function resolveExtremeContrastColor({ comparison, isHighestContrast, lightestColorString, darkestColorString, }) {
|
|
110
|
+
const paletteIsBackground = check.isArray(comparison.background);
|
|
111
|
+
const fixedColor = paletteIsBackground
|
|
112
|
+
? comparison.foreground
|
|
113
|
+
: comparison.background;
|
|
114
|
+
const lightContrastParams = paletteIsBackground
|
|
115
|
+
? {
|
|
116
|
+
foreground: fixedColor,
|
|
117
|
+
background: lightestColorString,
|
|
118
|
+
}
|
|
119
|
+
: {
|
|
120
|
+
foreground: lightestColorString,
|
|
121
|
+
background: fixedColor,
|
|
122
|
+
};
|
|
123
|
+
const darkContrastParams = paletteIsBackground
|
|
124
|
+
? {
|
|
125
|
+
foreground: fixedColor,
|
|
126
|
+
background: darkestColorString,
|
|
127
|
+
}
|
|
128
|
+
: {
|
|
129
|
+
foreground: darkestColorString,
|
|
130
|
+
background: fixedColor,
|
|
131
|
+
};
|
|
132
|
+
const lightestContrast = Math.abs(calculateContrast(lightContrastParams).contrast);
|
|
133
|
+
const darkestContrast = Math.abs(calculateContrast(darkContrastParams).contrast);
|
|
134
|
+
return isHighestContrast
|
|
135
|
+
? lightestContrast > darkestContrast
|
|
136
|
+
? lightestColorString
|
|
137
|
+
: darkestColorString
|
|
138
|
+
: lightestContrast < darkestContrast
|
|
139
|
+
? lightestColorString
|
|
140
|
+
: darkestColorString;
|
|
141
|
+
}
|
|
142
|
+
function findColorWithPreference({ colors, desiredContrastLevel, preference, lightnessProxies, }) {
|
|
96
143
|
const minContrast = contrastLevelNameMap[desiredContrastLevel].min;
|
|
97
144
|
const candidateColors = check.isArray(colors.foreground)
|
|
98
145
|
? colors.foreground
|
|
@@ -140,6 +187,11 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
|
|
|
140
187
|
const lightThemeColors = {};
|
|
141
188
|
const darkThemeOverrides = {};
|
|
142
189
|
// Compute these once outside the loop since they don't change
|
|
190
|
+
const allContrastLevels = [
|
|
191
|
+
ExtremeContrastLevel.HighestContrast,
|
|
192
|
+
...contrastLevels,
|
|
193
|
+
ExtremeContrastLevel.LowestContrast,
|
|
194
|
+
];
|
|
143
195
|
const allCrosses = crossProduct({
|
|
144
196
|
crossWith: [
|
|
145
197
|
'color-in-foreground-light-mode',
|
|
@@ -151,7 +203,7 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
|
|
|
151
203
|
'color-on-self-light-mode',
|
|
152
204
|
'color-on-self-dark-mode',
|
|
153
205
|
],
|
|
154
|
-
contrast:
|
|
206
|
+
contrast: allContrastLevels,
|
|
155
207
|
});
|
|
156
208
|
const defaultForegroundString = noRefColorInitToString(defaultTheme.foreground);
|
|
157
209
|
const defaultBackgroundString = noRefColorInitToString(defaultTheme.background);
|
|
@@ -160,10 +212,12 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
|
|
|
160
212
|
const colorStrings = colors.map((color) => color.definition.default);
|
|
161
213
|
const firstColor = colors[0];
|
|
162
214
|
// Create an object for O(1) color lookup instead of O(n) find()
|
|
163
|
-
const colorByDefault = arrayToObject(colors, (color) =>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
215
|
+
const colorByDefault = arrayToObject(colors, (color) => {
|
|
216
|
+
return {
|
|
217
|
+
key: color.definition.default,
|
|
218
|
+
value: color,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
167
221
|
/** Pre-computed contrast-against-white per color. Higher value = darker shade. */
|
|
168
222
|
const lightnessProxies = arrayToObject(colorStrings, (colorString) => {
|
|
169
223
|
return {
|
|
@@ -185,17 +239,36 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
|
|
|
185
239
|
* bg. Fixed across all on-self contrast levels.
|
|
186
240
|
*/
|
|
187
241
|
const lightSelfFgString = assertWrap.isTruthy(findColorWithPreference({
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
242
|
+
colors: {
|
|
243
|
+
foreground: colorStrings,
|
|
244
|
+
background: lightestColorString,
|
|
245
|
+
},
|
|
246
|
+
desiredContrastLevel: ContrastLevelName.SmallBodyText,
|
|
247
|
+
preference: 'lightest',
|
|
248
|
+
lightnessProxies,
|
|
249
|
+
}), `Failed to find light mode on-self foreground color for ${firstColor.colorName}`);
|
|
191
250
|
/**
|
|
192
251
|
* On-self dark mode: darkest fg achieving small-body contrast on the darkest palette
|
|
193
252
|
* bg. Fixed across all on-self contrast levels.
|
|
194
253
|
*/
|
|
195
254
|
const darkSelfFgString = assertWrap.isTruthy(findColorWithPreference({
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
255
|
+
colors: {
|
|
256
|
+
foreground: colorStrings,
|
|
257
|
+
background: darkestColorString,
|
|
258
|
+
},
|
|
259
|
+
desiredContrastLevel: ContrastLevelName.SmallBodyText,
|
|
260
|
+
preference: 'darkest',
|
|
261
|
+
lightnessProxies,
|
|
262
|
+
}), `Failed to find dark mode on-self foreground color for ${firstColor.colorName}`);
|
|
263
|
+
/**
|
|
264
|
+
* Reversed palette order for dark mode on-self backgrounds. `findColorAtContrastLevel`
|
|
265
|
+
* picks the last qualifying color in array order. With the natural lightest-to-darkest
|
|
266
|
+
* order, high-contrast backgrounds end up as the darkest palette colors, which are
|
|
267
|
+
* indistinguishable from the dark page background. Reversing the order makes it select
|
|
268
|
+
* the lightest palette color that still achieves the required contrast level, producing
|
|
269
|
+
* backgrounds that are visible against the dark page.
|
|
270
|
+
*/
|
|
271
|
+
const reversedColorStrings = colorStrings.toReversed();
|
|
199
272
|
// Pre-compute base name parts that don't change per cross
|
|
200
273
|
const baseNameParts = [
|
|
201
274
|
prefix,
|
|
@@ -215,7 +288,7 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
|
|
|
215
288
|
: cross.crossWith === 'color-on-self-dark-mode'
|
|
216
289
|
? {
|
|
217
290
|
foreground: darkSelfFgString,
|
|
218
|
-
background:
|
|
291
|
+
background: reversedColorStrings,
|
|
219
292
|
}
|
|
220
293
|
: cross.crossWith === 'color-on-self-light-mode'
|
|
221
294
|
? {
|
|
@@ -285,7 +358,15 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
|
|
|
285
358
|
if (!comparison) {
|
|
286
359
|
throw new Error(`Forgot to handle crossWith: '${cross.crossWith}'`);
|
|
287
360
|
}
|
|
288
|
-
const matchedColorString =
|
|
361
|
+
const matchedColorString = cross.contrast === ExtremeContrastLevel.HighestContrast ||
|
|
362
|
+
cross.contrast === ExtremeContrastLevel.LowestContrast
|
|
363
|
+
? resolveExtremeContrastColor({
|
|
364
|
+
comparison,
|
|
365
|
+
isHighestContrast: cross.contrast === ExtremeContrastLevel.HighestContrast,
|
|
366
|
+
lightestColorString,
|
|
367
|
+
darkestColorString,
|
|
368
|
+
})
|
|
369
|
+
: findColorAtContrastLevel(comparison, cross.contrast);
|
|
289
370
|
const matchedColor = matchedColorString
|
|
290
371
|
? colorByDefault[matchedColorString]
|
|
291
372
|
: undefined;
|
|
@@ -8,8 +8,18 @@ import { CSSResult } from 'element-vir';
|
|
|
8
8
|
*/
|
|
9
9
|
export function generateThemeCode(theme, options) {
|
|
10
10
|
const paletteVarName = options?.paletteVarName;
|
|
11
|
-
const defaultInitCode = colorInitToCode(
|
|
12
|
-
|
|
11
|
+
const defaultInitCode = colorInitToCode({
|
|
12
|
+
colorInit: theme.init.default,
|
|
13
|
+
indentLevel: 1,
|
|
14
|
+
defaultInit: undefined,
|
|
15
|
+
paletteVarName,
|
|
16
|
+
});
|
|
17
|
+
const colorsInitCode = colorThemeInitToCode({
|
|
18
|
+
colorsInit: theme.init.colors,
|
|
19
|
+
indentLevel: 1,
|
|
20
|
+
defaultInit: theme.init.default,
|
|
21
|
+
paletteVarName,
|
|
22
|
+
});
|
|
13
23
|
const themeCode = `export const theme = defineColorTheme(\n${defaultInitCode},\n${colorsInitCode},\n);`;
|
|
14
24
|
const overridesCodes = (options?.overrides || []).map((override) => {
|
|
15
25
|
return generateOverrideCode(override, paletteVarName);
|
|
@@ -63,6 +73,7 @@ function generateOverrideCode(override, paletteVarName) {
|
|
|
63
73
|
function tab(level) {
|
|
64
74
|
return ' '.repeat(level);
|
|
65
75
|
}
|
|
76
|
+
// eslint-disable-next-line @virmator/prefer-params-object
|
|
66
77
|
function colorInitValuesEqual(a, b) {
|
|
67
78
|
if (typeof a !== typeof b) {
|
|
68
79
|
return false;
|
|
@@ -116,7 +127,7 @@ function colorInitValueToCode(value, indentLevel, paletteVarName) {
|
|
|
116
127
|
return `'${value.default}'`;
|
|
117
128
|
}
|
|
118
129
|
}
|
|
119
|
-
function colorInitToCode(colorInit, indentLevel, defaultInit, paletteVarName) {
|
|
130
|
+
function colorInitToCode({ colorInit, indentLevel, defaultInit, paletteVarName, }) {
|
|
120
131
|
const entries = [];
|
|
121
132
|
if ('foreground' in colorInit &&
|
|
122
133
|
(!defaultInit || !colorInitValuesEqual(colorInit.foreground, defaultInit.foreground)) &&
|
|
@@ -142,9 +153,14 @@ function colorInitToCode(colorInit, indentLevel, defaultInit, paletteVarName) {
|
|
|
142
153
|
}
|
|
143
154
|
return `${tab(indentLevel)}{\n${entries.join('\n')}\n${tab(indentLevel)}}`;
|
|
144
155
|
}
|
|
145
|
-
function colorThemeInitToCode(colorsInit, indentLevel, defaultInit, paletteVarName) {
|
|
156
|
+
function colorThemeInitToCode({ colorsInit, indentLevel, defaultInit, paletteVarName, }) {
|
|
146
157
|
const entries = getObjectTypedEntries(colorsInit).map(([colorName, colorInit,]) => {
|
|
147
|
-
return `${tab(indentLevel + 1)}'${colorName}': ${colorInitToCode(
|
|
158
|
+
return `${tab(indentLevel + 1)}'${colorName}': ${colorInitToCode({
|
|
159
|
+
colorInit,
|
|
160
|
+
indentLevel: indentLevel + 1,
|
|
161
|
+
defaultInit,
|
|
162
|
+
paletteVarName,
|
|
163
|
+
}).trimStart()},`;
|
|
148
164
|
});
|
|
149
165
|
return `${tab(indentLevel)}{\n${entries.join('\n')}\n${tab(indentLevel)}}`;
|
|
150
166
|
}
|
|
@@ -16,6 +16,7 @@ export function noRefColorInitToString(init) {
|
|
|
16
16
|
*
|
|
17
17
|
* @category Internal
|
|
18
18
|
*/
|
|
19
|
+
// eslint-disable-next-line @virmator/prefer-params-object
|
|
19
20
|
export function createColorCssVarDefault(fromName, init, defaultInit, colorsInit) {
|
|
20
21
|
const defaultForegroundKey = `${defaultInit.prefix}-default-fg`;
|
|
21
22
|
const defaultBackgroundKey = `${defaultInit.prefix}-default-bg`;
|
|
@@ -77,7 +77,13 @@ function defineHeadingElement(options) {
|
|
|
77
77
|
const headingSelectors = unsafeCSS(getEnumValues(HeadingLevel).join(', '));
|
|
78
78
|
return defineElement()({
|
|
79
79
|
tagName: headingTag,
|
|
80
|
-
hostClasses: typedObjectFromEntries(
|
|
80
|
+
hostClasses: typedObjectFromEntries(
|
|
81
|
+
/**
|
|
82
|
+
* `mapEnumToObject` cannot be used here: host class keys must be prefixed with the
|
|
83
|
+
* element's tag name, not the bare enum values.
|
|
84
|
+
*/
|
|
85
|
+
// eslint-disable-next-line @virmator/prefer-map-enum-to-object
|
|
86
|
+
getEnumValues(HeadingLevel).map((headingLevel) => {
|
|
81
87
|
return [
|
|
82
88
|
`${headingTag}-${headingLevel}`,
|
|
83
89
|
({ inputs }) => inputs.headingLevel === headingLevel,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "theme-vir",
|
|
3
|
-
"version": "28.
|
|
3
|
+
"version": "28.25.1",
|
|
4
4
|
"description": "Create an entire web theme.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"type": "git",
|
|
23
23
|
"url": "git+https://github.com/electrovir/theme-vir.git"
|
|
24
24
|
},
|
|
25
|
-
"license": "(MIT
|
|
25
|
+
"license": "(MIT OR CC0-1.0)",
|
|
26
26
|
"author": {
|
|
27
27
|
"name": "electrovir",
|
|
28
28
|
"url": "https://github.com/electrovir"
|
|
@@ -43,30 +43,30 @@
|
|
|
43
43
|
"test:update": "virmator test web update"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@augment-vir/assert": "^
|
|
47
|
-
"@augment-vir/common": "^
|
|
48
|
-
"@electrovir/color": "^1.7.
|
|
46
|
+
"@augment-vir/assert": "^32.2.1",
|
|
47
|
+
"@augment-vir/common": "^32.2.1",
|
|
48
|
+
"@electrovir/color": "^1.7.11",
|
|
49
49
|
"apca-w3": "^0.1.9",
|
|
50
|
-
"lit-css-vars": "^3.
|
|
51
|
-
"type-fest": "^5.
|
|
50
|
+
"lit-css-vars": "^3.6.3",
|
|
51
|
+
"type-fest": "^5.8.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@augment-vir/test": "^
|
|
54
|
+
"@augment-vir/test": "^32.2.1",
|
|
55
55
|
"@types/apca-w3": "^0.1.3",
|
|
56
|
-
"@web/dev-server-esbuild": "^
|
|
57
|
-
"@web/test-runner": "^0.
|
|
58
|
-
"@web/test-runner-commands": "^0.
|
|
59
|
-
"@web/test-runner-playwright": "^0.
|
|
60
|
-
"@web/test-runner-visual-regression": "^0.
|
|
61
|
-
"element-book": "^26.17.
|
|
62
|
-
"element-vir": "^26.
|
|
63
|
-
"esbuild": "^0.
|
|
56
|
+
"@web/dev-server-esbuild": "^2.0.0",
|
|
57
|
+
"@web/test-runner": "^1.0.0",
|
|
58
|
+
"@web/test-runner-commands": "^1.0.1",
|
|
59
|
+
"@web/test-runner-playwright": "^1.0.0",
|
|
60
|
+
"@web/test-runner-visual-regression": "^1.0.1",
|
|
61
|
+
"element-book": "^26.17.3",
|
|
62
|
+
"element-vir": "^26.17.1",
|
|
63
|
+
"esbuild": "^0.28.1",
|
|
64
64
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
65
|
-
"markdown-code-example-inserter": "^3.0.
|
|
66
|
-
"typedoc": "^0.28.
|
|
65
|
+
"markdown-code-example-inserter": "^3.0.6",
|
|
66
|
+
"typedoc": "^0.28.20",
|
|
67
67
|
"typescript": "5.9.3",
|
|
68
|
-
"vira": "^
|
|
69
|
-
"vite": "^
|
|
68
|
+
"vira": "^31.28.1",
|
|
69
|
+
"vite": "^8.1.5",
|
|
70
70
|
"vite-tsconfig-paths": "^6.1.1"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|