theme-vir 28.21.2 → 28.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { assert, assertWrap, check } from '@augment-vir/assert';
2
2
  import { arrayToObject, crossProduct, filterMap, getEnumValues, getOrSet, log, mapObjectValues, removeDuplicates, stringify, } from '@augment-vir/common';
3
- import { ContrastLevelName, contrastLevelLabel, findClosestColor, findColorAtContrastLevel, } from '@electrovir/color';
3
+ import { ContrastLevelName, calculateContrast, contrastLevelLabel, contrastLevelNameMap, findColorAtContrastLevel, } from '@electrovir/color';
4
4
  import { defineColorThemeOverride } from './color-theme-override.js';
5
5
  import { defineColorTheme, noRefColorInitToString, } from './color-theme.js';
6
6
  /**
@@ -90,6 +90,38 @@ export const defaultLightThemePair = {
90
90
  };
91
91
  /** @category Internal */
92
92
  export const defaultContrastLevels = getEnumValues(ContrastLevelName);
93
+ function findColorWithPreference(colors, desiredContrastLevel, preference,
94
+ /** Pre-computed contrast-against-white values per color string. Higher = darker. */
95
+ lightnessProxies) {
96
+ const minContrast = contrastLevelNameMap[desiredContrastLevel].min;
97
+ const candidateColors = check.isArray(colors.foreground)
98
+ ? colors.foreground
99
+ : check.isArray(colors.background)
100
+ ? colors.background
101
+ : [];
102
+ const qualifying = candidateColors.filter((candidate) => {
103
+ const foreground = check.isArray(colors.foreground) ? candidate : colors.foreground;
104
+ const background = check.isArray(colors.foreground) ? colors.background : candidate;
105
+ return (Math.abs(calculateContrast({
106
+ foreground,
107
+ background: background,
108
+ }).contrast) >= minContrast);
109
+ });
110
+ if (qualifying.length === 0) {
111
+ return undefined;
112
+ }
113
+ return qualifying.reduce((best, color) => {
114
+ const bestProxy = lightnessProxies[best] ?? 0;
115
+ const colorProxy = lightnessProxies[color] ?? 0;
116
+ return preference === 'lightest'
117
+ ? colorProxy < bestProxy
118
+ ? color
119
+ : best
120
+ : colorProxy > bestProxy
121
+ ? color
122
+ : best;
123
+ });
124
+ }
93
125
  /**
94
126
  * Creates a color theme from a color palette.
95
127
  *
@@ -132,10 +164,38 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
132
164
  key: color.definition.default,
133
165
  value: color,
134
166
  }));
135
- const lightestSelfString = findClosestColor('white', colorStrings);
136
- const darkestSelfString = findClosestColor('black', colorStrings);
137
- const lightestSelf = colorByDefault[lightestSelfString];
138
- const darkestSelf = colorByDefault[darkestSelfString];
167
+ /** Pre-computed contrast-against-white per color. Higher value = darker shade. */
168
+ const lightnessProxies = arrayToObject(colorStrings, (colorString) => {
169
+ return {
170
+ key: colorString,
171
+ value: Math.abs(calculateContrast({
172
+ foreground: colorString,
173
+ background: '#ffffff',
174
+ }).contrast),
175
+ };
176
+ });
177
+ /** Lightest palette color (least contrast against white). */
178
+ const lightestColorString = colorStrings.reduce((lightest, color) => (lightnessProxies[color] ?? 0) < (lightnessProxies[lightest] ?? 0)
179
+ ? color
180
+ : lightest);
181
+ /** Darkest palette color (most contrast against white). */
182
+ const darkestColorString = colorStrings.reduce((darkest, color) => (lightnessProxies[color] ?? 0) > (lightnessProxies[darkest] ?? 0) ? color : darkest);
183
+ /**
184
+ * On-self light mode: lightest fg achieving small-body contrast on the lightest palette
185
+ * bg. Fixed across all on-self contrast levels.
186
+ */
187
+ const lightSelfFgString = assertWrap.isTruthy(findColorWithPreference({
188
+ foreground: colorStrings,
189
+ background: lightestColorString,
190
+ }, ContrastLevelName.SmallBodyText, 'lightest', lightnessProxies), `Failed to find light mode on-self foreground color for ${firstColor.colorName}`);
191
+ /**
192
+ * On-self dark mode: darkest fg achieving small-body contrast on the darkest palette
193
+ * bg. Fixed across all on-self contrast levels.
194
+ */
195
+ const darkSelfFgString = assertWrap.isTruthy(findColorWithPreference({
196
+ foreground: colorStrings,
197
+ background: darkestColorString,
198
+ }, ContrastLevelName.SmallBodyText, 'darkest', lightnessProxies), `Failed to find dark mode on-self foreground color for ${firstColor.colorName}`);
139
199
  // Pre-compute base name parts that don't change per cross
140
200
  const baseNameParts = [
141
201
  prefix,
@@ -154,13 +214,13 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
154
214
  }
155
215
  : cross.crossWith === 'color-on-self-dark-mode'
156
216
  ? {
157
- foreground: colorStrings,
158
- background: darkestSelfString,
217
+ foreground: darkSelfFgString,
218
+ background: colorStrings,
159
219
  }
160
220
  : cross.crossWith === 'color-on-self-light-mode'
161
221
  ? {
162
- foreground: colorStrings,
163
- background: lightestSelfString,
222
+ foreground: lightSelfFgString,
223
+ background: colorStrings,
164
224
  }
165
225
  : cross.crossWith === 'color-behind-bg-light-mode'
166
226
  ? {
@@ -241,12 +301,16 @@ export function buildColorTheme(colorPalette, { omittedColorValues = defaultOmit
241
301
  cross.crossWith === 'color-behind-fg-dark-mode';
242
302
  const colorValue = mapObjectValues(comparison, (key, value) => {
243
303
  if (check.isString(value)) {
244
- // For self-contrast modes, use the CSS var reference for the background
245
- if (isSelfContrast && key === 'background') {
246
- const selfColor = cross.crossWith === 'color-on-self-light-mode'
247
- ? lightestSelf
248
- : darkestSelf;
249
- return selfColor?.definition.value || value;
304
+ /**
305
+ * For self-contrast modes, the foreground is the fixed string side —
306
+ * use its CSS var reference
307
+ */
308
+ if (isSelfContrast && key === 'foreground') {
309
+ const selfFg = cross.crossWith === 'color-on-self-light-mode'
310
+ ? lightSelfFgString
311
+ : darkSelfFgString;
312
+ const selfFgColor = selfFg ? colorByDefault[selfFg] : undefined;
313
+ return selfFgColor?.definition.value || value;
250
314
  }
251
315
  const referenceToDefault = referencesToDefault &&
252
316
  check.isKeyOf(key, referencesToDefault) &&
@@ -10,8 +10,12 @@ export var HeadingLevel;
10
10
  export function createDefaultThemeOptions() {
11
11
  const defaultFont = {
12
12
  family: 'sans-serif',
13
- lineHeight: { ratio: 1.1 },
14
- size: { pixels: 14 },
13
+ lineHeight: {
14
+ ratio: 1.1,
15
+ },
16
+ size: {
17
+ pixels: 14,
18
+ },
15
19
  weight: 400,
16
20
  };
17
21
  const bold = {
@@ -29,31 +33,43 @@ export function createDefaultThemeOptions() {
29
33
  monospace: {
30
34
  ...defaultFont,
31
35
  family: 'monospace',
32
- size: { ratio: 1.2 },
36
+ size: {
37
+ ratio: 1.2,
38
+ },
33
39
  },
34
40
  headings: {
35
41
  h1: {
36
42
  ...bold,
37
- size: { ratio: 2 },
43
+ size: {
44
+ ratio: 2,
45
+ },
38
46
  },
39
47
  h2: {
40
48
  ...bold,
41
- size: { ratio: 1.5 },
49
+ size: {
50
+ ratio: 1.5,
51
+ },
42
52
  },
43
53
  h3: {
44
54
  ...bold,
45
- size: { ratio: 1.17 },
55
+ size: {
56
+ ratio: 1.17,
57
+ },
46
58
  },
47
59
  h4: {
48
60
  ...bold,
49
61
  },
50
62
  h5: {
51
63
  ...bold,
52
- size: { ratio: 0.83 },
64
+ size: {
65
+ ratio: 0.83,
66
+ },
53
67
  },
54
68
  h6: {
55
69
  ...bold,
56
- size: { ratio: 0.67 },
70
+ size: {
71
+ ratio: 0.67,
72
+ },
57
73
  },
58
74
  },
59
75
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theme-vir",
3
- "version": "28.21.2",
3
+ "version": "28.23.0",
4
4
  "description": "Create an entire web theme.",
5
5
  "keywords": [
6
6
  "design",
@@ -43,31 +43,31 @@
43
43
  "test:update": "virmator test web update"
44
44
  },
45
45
  "dependencies": {
46
- "@augment-vir/assert": "^31.59.3",
47
- "@augment-vir/common": "^31.59.3",
46
+ "@augment-vir/assert": "^31.67.1",
47
+ "@augment-vir/common": "^31.67.1",
48
48
  "@electrovir/color": "^1.7.8",
49
49
  "apca-w3": "^0.1.9",
50
50
  "lit-css-vars": "^3.5.0",
51
51
  "type-fest": "^5.4.4"
52
52
  },
53
53
  "devDependencies": {
54
- "@augment-vir/test": "^31.59.3",
54
+ "@augment-vir/test": "^31.67.1",
55
55
  "@types/apca-w3": "^0.1.3",
56
56
  "@web/dev-server-esbuild": "^1.0.5",
57
57
  "@web/test-runner": "^0.20.2",
58
58
  "@web/test-runner-commands": "^0.9.0",
59
59
  "@web/test-runner-playwright": "^0.11.1",
60
60
  "@web/test-runner-visual-regression": "^0.10.0",
61
- "element-book": "^26.16.0",
62
- "element-vir": "^26.14.3",
61
+ "element-book": "^26.17.0",
62
+ "element-vir": "^26.14.5",
63
63
  "esbuild": "^0.27.3",
64
64
  "istanbul-smart-text-reporter": "^1.1.5",
65
65
  "markdown-code-example-inserter": "^3.0.3",
66
- "typedoc": "^0.28.16",
66
+ "typedoc": "^0.28.17",
67
67
  "typescript": "5.9.3",
68
- "vira": "^29.3.1",
68
+ "vira": "^30.6.0",
69
69
  "vite": "^7.3.1",
70
- "vite-tsconfig-paths": "^6.1.0"
70
+ "vite-tsconfig-paths": "^6.1.1"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "element-book": ">=17",