@opendata-ai/openchart-core 8.0.0-rc.9 → 8.1.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.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +403 -45
  3. package/dist/index.js +50 -18
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.css.map +1 -1
  7. package/package.json +1 -2
  8. package/schema/chart.schema.json +375 -69
  9. package/schema/table.schema.json +35 -1
  10. package/schema/vizspec.schema.json +992 -171
  11. package/src/accessibility/__tests__/alt-text.test.ts +0 -110
  12. package/src/accessibility/__tests__/aria.test.ts +0 -125
  13. package/src/accessibility/alt-text.ts +0 -155
  14. package/src/accessibility/aria.ts +0 -75
  15. package/src/accessibility/index.ts +0 -6
  16. package/src/colors/__tests__/colorblind.test.ts +0 -75
  17. package/src/colors/__tests__/contrast.test.ts +0 -119
  18. package/src/colors/__tests__/palettes.test.ts +0 -74
  19. package/src/colors/colorblind.ts +0 -131
  20. package/src/colors/contrast.ts +0 -143
  21. package/src/colors/index.ts +0 -36
  22. package/src/colors/palettes.ts +0 -206
  23. package/src/helpers/__tests__/deep-merge.test.ts +0 -54
  24. package/src/helpers/__tests__/spec-builders.test.ts +0 -338
  25. package/src/helpers/deep-merge.ts +0 -29
  26. package/src/helpers/spec-builders.ts +0 -479
  27. package/src/index.ts +0 -215
  28. package/src/labels/__tests__/collision.test.ts +0 -310
  29. package/src/labels/collision.ts +0 -249
  30. package/src/labels/index.ts +0 -13
  31. package/src/layout/__tests__/chrome.test.ts +0 -208
  32. package/src/layout/__tests__/text-measure.test.ts +0 -57
  33. package/src/layout/chrome.ts +0 -470
  34. package/src/layout/index.ts +0 -18
  35. package/src/layout/text-measure.ts +0 -404
  36. package/src/layout/text-wrap.ts +0 -93
  37. package/src/locale/__tests__/format.test.ts +0 -606
  38. package/src/locale/format.ts +0 -459
  39. package/src/locale/index.ts +0 -21
  40. package/src/responsive/__tests__/breakpoints.test.ts +0 -146
  41. package/src/responsive/__tests__/metrics.test.ts +0 -52
  42. package/src/responsive/breakpoints.ts +0 -177
  43. package/src/responsive/index.ts +0 -53
  44. package/src/responsive/metrics.ts +0 -299
  45. package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
  46. package/src/schema/__tests__/schema.test.ts +0 -179
  47. package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
  48. package/src/styles/animation.css +0 -213
  49. package/src/styles/base.css +0 -102
  50. package/src/styles/chrome.css +0 -174
  51. package/src/styles/dark.css +0 -44
  52. package/src/styles/graph.css +0 -135
  53. package/src/styles/index.css +0 -28
  54. package/src/styles/keyframes.css +0 -151
  55. package/src/styles/legend.css +0 -18
  56. package/src/styles/reduced-motion.css +0 -67
  57. package/src/styles/series-search.css +0 -153
  58. package/src/styles/sparkline.css +0 -16
  59. package/src/styles/table-animation.css +0 -93
  60. package/src/styles/table.css +0 -398
  61. package/src/styles/token-definitions.ts +0 -199
  62. package/src/styles/tokens.css +0 -156
  63. package/src/styles/tooltip.css +0 -126
  64. package/src/styles/you-draw-it.css +0 -126
  65. package/src/theme/__tests__/dark-mode.test.ts +0 -232
  66. package/src/theme/__tests__/defaults.test.ts +0 -73
  67. package/src/theme/__tests__/resolve.test.ts +0 -277
  68. package/src/theme/dark-mode.ts +0 -258
  69. package/src/theme/defaults.ts +0 -110
  70. package/src/theme/index.ts +0 -8
  71. package/src/theme/presets.ts +0 -64
  72. package/src/theme/resolve.ts +0 -312
  73. package/src/types/__tests__/encoding.test.ts +0 -368
  74. package/src/types/__tests__/gradient.test.ts +0 -95
  75. package/src/types/__tests__/spec.test.ts +0 -499
  76. package/src/types/encoding.ts +0 -280
  77. package/src/types/events.ts +0 -236
  78. package/src/types/index.ts +0 -290
  79. package/src/types/layout.ts +0 -2024
  80. package/src/types/schema-roots.ts +0 -32
  81. package/src/types/spec.ts +0 -2925
  82. package/src/types/table.ts +0 -108
  83. package/src/types/theme.ts +0 -209
@@ -1,110 +0,0 @@
1
- /**
2
- * Default theme definition.
3
- *
4
- * Editorial design system with cyan-led categorical palette, Inter
5
- * Variable typography (weights 450/550/590/700), and zinc-based achromatic
6
- * surfaces. Light is the default surface; dark adaptation lives in
7
- * dark-mode.ts.
8
- *
9
- * resolveTheme() deep-merges user overrides onto this base.
10
- */
11
-
12
- import { CATEGORICAL_PALETTE, DIVERGING_PALETTES, SEQUENTIAL_PALETTES } from '../colors/palettes';
13
- import { X_AXIS_BAND_HEIGHT } from '../responsive/metrics';
14
- import type { Theme } from '../types/theme';
15
-
16
- /**
17
- * The default theme. All fields are required and fully specified.
18
- */
19
- export const DEFAULT_THEME: Theme = {
20
- colors: {
21
- categorical: [...CATEGORICAL_PALETTE],
22
- sequential: SEQUENTIAL_PALETTES,
23
- diverging: DIVERGING_PALETTES,
24
- background: 'transparent',
25
- text: '#09090b',
26
- gridline: 'rgba(0,0,0,0.1)',
27
- // Used for axis lines/ticks AND axis tick label fill. Must clear WCAG AA
28
- // contrast (4.5:1) on white because tick labels are rendered with this
29
- // color. Zinc-500 hits ~5.7:1.
30
- axis: '#71717a',
31
- annotationFill: 'rgba(0,0,0,0.04)',
32
- annotationText: '#71717a',
33
- positive: '#16a34a',
34
- negative: '#dc2626',
35
- },
36
- fonts: {
37
- family:
38
- '"Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
39
- mono: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
40
- sizes: {
41
- title: 26,
42
- subtitle: 14,
43
- body: 13,
44
- small: 11,
45
- axisTick: 11,
46
- metricLabel: 10,
47
- metricValue: 22,
48
- },
49
- weights: {
50
- normal: 450,
51
- medium: 550,
52
- semibold: 590,
53
- bold: 700,
54
- },
55
- },
56
- spacing: {
57
- padding: 20,
58
- chromeGap: 4,
59
- chromeToChart: 8,
60
- chartToFooter: 8,
61
- axisMargin: 6,
62
- xAxisHeight: X_AXIS_BAND_HEIGHT,
63
- // Gap (px) between the x-axis line and the TOP of the tick labels. The
64
- // renderer treats this as a literal top gap and then shifts the label down
65
- // by textAscent() to land it on the alphabetic baseline (rather than
66
- // dominant-baseline:hanging, which WebKit positions from different metrics),
67
- // so the gap holds regardless of font size.
68
- xAxisLabelPadding: 4,
69
- },
70
- borderRadius: 2,
71
- chrome: {
72
- eyebrow: {
73
- fontSize: 11,
74
- fontWeight: 510,
75
- color: '#06b6d4',
76
- lineHeight: 1.4,
77
- },
78
- title: {
79
- fontSize: 26,
80
- fontWeight: 590,
81
- color: '#09090b',
82
- lineHeight: 1.15,
83
- },
84
- subtitle: {
85
- fontSize: 14,
86
- fontWeight: 400,
87
- color: '#71717a',
88
- lineHeight: 1.45,
89
- },
90
- source: {
91
- fontSize: 11,
92
- fontWeight: 400,
93
- color: '#71717a',
94
- lineHeight: 1.4,
95
- },
96
- byline: {
97
- fontSize: 11,
98
- fontWeight: 400,
99
- color: '#71717a',
100
- lineHeight: 1.4,
101
- },
102
- footer: {
103
- fontSize: 11,
104
- fontWeight: 400,
105
- color: '#71717a',
106
- lineHeight: 1.4,
107
- },
108
- },
109
- seriesStrategy: 'palette',
110
- };
@@ -1,8 +0,0 @@
1
- /**
2
- * Theme module barrel export.
3
- */
4
-
5
- export { adaptColorForDarkMode, adaptForLightLineStroke, adaptTheme } from './dark-mode';
6
- export { DEFAULT_THEME } from './defaults';
7
- export { editorial, essay, wire } from './presets';
8
- export { resolveTheme } from './resolve';
@@ -1,64 +0,0 @@
1
- /**
2
- * Named theme presets built on the design token system.
3
- *
4
- * Each preset is a ThemeConfig object that can be passed to createChart()
5
- * or the React/Svelte/Vue Chart component. The engine deep-merges them
6
- * onto DEFAULT_THEME, so only the deltas are specified here.
7
- */
8
-
9
- import type { ThemeConfig } from '../types/spec';
10
-
11
- /** Editorial preset: the current default look. Exported for explicit selection. */
12
- export const editorial: ThemeConfig = {};
13
-
14
- /** Essay preset: serif display titles, warmer background, generous spacing. */
15
- export const essay: ThemeConfig = {
16
- colors: {
17
- background: { light: '#faf8f5', dark: '#1a1816' },
18
- text: { light: '#1c1917', dark: '#e7e5e4' },
19
- gridline: { light: 'rgba(0,0,0,0.08)', dark: 'rgba(255,255,255,0.06)' },
20
- axis: { light: '#78716c', dark: '#a8a29e' },
21
- },
22
- fonts: {
23
- family: '"Source Serif 4", "Source Serif Pro", Georgia, "Times New Roman", serif',
24
- sizes: { title: 28, subtitle: 15, body: 14, small: 12 },
25
- weights: { normal: 400, medium: 500, semibold: 600, bold: 700 },
26
- },
27
- spacing: {
28
- padding: 24,
29
- chromeGap: 6,
30
- chromeToChart: 12,
31
- chartToFooter: 12,
32
- },
33
- chrome: {
34
- title: { fontWeight: 600, lineHeight: 1.2 },
35
- subtitle: { fontWeight: 400, lineHeight: 1.5 },
36
- },
37
- borderRadius: 0,
38
- };
39
-
40
- /** Wire preset: dense, mono numerals, tighter chrome. Dashboard/agency feel. */
41
- export const wire: ThemeConfig = {
42
- colors: {
43
- background: { light: '#ffffff', dark: '#0a0a0a' },
44
- text: { light: '#171717', dark: '#ededed' },
45
- gridline: { light: 'rgba(0,0,0,0.06)', dark: 'rgba(255,255,255,0.06)' },
46
- axis: { light: '#737373', dark: '#a3a3a3' },
47
- },
48
- fonts: {
49
- family: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
50
- sizes: { title: 16, subtitle: 12, body: 11, small: 10, axisTick: 10 },
51
- weights: { normal: 400, medium: 500, semibold: 600, bold: 700 },
52
- },
53
- spacing: {
54
- padding: 12,
55
- chromeGap: 2,
56
- chromeToChart: 4,
57
- chartToFooter: 4,
58
- },
59
- chrome: {
60
- title: { fontWeight: 600, lineHeight: 1.25 },
61
- subtitle: { fontWeight: 400, lineHeight: 1.3 },
62
- },
63
- borderRadius: 2,
64
- };
@@ -1,312 +0,0 @@
1
- /**
2
- * Theme resolver: deep-merges user overrides onto default theme.
3
- *
4
- * Produces a ResolvedTheme where every property is guaranteed to have
5
- * a value. The engine uses ResolvedTheme internally so it never needs
6
- * null checks on theme properties.
7
- *
8
- * Auto-detects dark backgrounds and adapts chrome text colors so
9
- * custom themes with dark canvases are readable without explicitly
10
- * enabling darkMode.
11
- */
12
-
13
- import type { ChromeThemeOverride, ThemeConfig } from '../types/spec';
14
- import type { ChromeDefaults, ResolvedTheme, Theme, TokenValue } from '../types/theme';
15
- import { DEFAULT_THEME } from './defaults';
16
-
17
- /**
18
- * Deep merge source into target, creating a new object.
19
- * Only merges plain objects; arrays and primitives replace directly.
20
- */
21
- // biome-ignore lint/suspicious/noExplicitAny: recursive object merge requires dynamic typing
22
- function deepMerge(target: any, source: any): any {
23
- const result = { ...target };
24
-
25
- for (const key of Object.keys(source)) {
26
- const sourceVal = source[key];
27
- const targetVal = target[key];
28
-
29
- if (
30
- sourceVal !== undefined &&
31
- sourceVal !== null &&
32
- typeof sourceVal === 'object' &&
33
- !Array.isArray(sourceVal) &&
34
- typeof targetVal === 'object' &&
35
- targetVal !== null &&
36
- !Array.isArray(targetVal)
37
- ) {
38
- result[key] = deepMerge(targetVal, sourceVal);
39
- } else if (sourceVal !== undefined) {
40
- result[key] = sourceVal;
41
- }
42
- }
43
-
44
- return result;
45
- }
46
-
47
- /** Extract the light-mode string from a TokenValue. */
48
- function resolveTokenLight(v: TokenValue): string {
49
- return typeof v === 'string' ? v : v.light;
50
- }
51
-
52
- /**
53
- * Convert a user-facing ThemeConfig (partial) into the full Theme shape
54
- * that deepMerge can work with.
55
- */
56
- function themeConfigToPartial(config: ThemeConfig): Partial<Theme> {
57
- const partial: Partial<Theme> = {};
58
-
59
- if (config.colors) {
60
- const colors: Partial<Theme['colors']> = {};
61
- // Shorthand: flat string[] is treated as categorical palette
62
- if (Array.isArray(config.colors)) {
63
- colors.categorical = config.colors;
64
- } else {
65
- if (config.colors.categorical) colors.categorical = config.colors.categorical;
66
- if (config.colors.sequential) colors.sequential = config.colors.sequential;
67
- if (config.colors.diverging) colors.diverging = config.colors.diverging;
68
- if (config.colors.background) colors.background = resolveTokenLight(config.colors.background);
69
- if (config.colors.text) colors.text = resolveTokenLight(config.colors.text);
70
- if (config.colors.gridline) colors.gridline = resolveTokenLight(config.colors.gridline);
71
- if (config.colors.axis) colors.axis = resolveTokenLight(config.colors.axis);
72
- if (config.colors.annotationFill)
73
- colors.annotationFill = resolveTokenLight(config.colors.annotationFill);
74
- if (config.colors.annotationText)
75
- colors.annotationText = resolveTokenLight(config.colors.annotationText);
76
- if (config.colors.positive) colors.positive = resolveTokenLight(config.colors.positive);
77
- if (config.colors.negative) colors.negative = resolveTokenLight(config.colors.negative);
78
- }
79
- partial.colors = colors as Theme['colors'];
80
- }
81
-
82
- if (config.fonts) {
83
- const fonts: Partial<Theme['fonts']> = {};
84
- if (config.fonts.family) fonts.family = config.fonts.family;
85
- if (config.fonts.mono) fonts.mono = config.fonts.mono;
86
- if (config.fonts.sizes) fonts.sizes = config.fonts.sizes as Theme['fonts']['sizes'];
87
- if (config.fonts.weights) fonts.weights = config.fonts.weights as Theme['fonts']['weights'];
88
- partial.fonts = fonts as Theme['fonts'];
89
- }
90
-
91
- if (config.spacing) {
92
- const spacing: Partial<Theme['spacing']> = {};
93
- if (config.spacing.padding !== undefined) spacing.padding = config.spacing.padding;
94
- if (config.spacing.chromeGap !== undefined) spacing.chromeGap = config.spacing.chromeGap;
95
- if (config.spacing.chromeToChart !== undefined)
96
- spacing.chromeToChart = config.spacing.chromeToChart;
97
- if (config.spacing.chartToFooter !== undefined)
98
- spacing.chartToFooter = config.spacing.chartToFooter;
99
- if (config.spacing.axisMargin !== undefined) spacing.axisMargin = config.spacing.axisMargin;
100
- if (config.spacing.xAxisHeight !== undefined) spacing.xAxisHeight = config.spacing.xAxisHeight;
101
- if (config.spacing.xAxisLabelPadding !== undefined)
102
- spacing.xAxisLabelPadding = config.spacing.xAxisLabelPadding;
103
- partial.spacing = spacing as Theme['spacing'];
104
- }
105
-
106
- if (config.borderRadius !== undefined) {
107
- partial.borderRadius = config.borderRadius;
108
- }
109
-
110
- if (config.chrome) {
111
- const chrome: Record<string, Partial<ChromeDefaults>> = {};
112
- for (const [element, value] of Object.entries(config.chrome)) {
113
- if (value === undefined) continue;
114
- if (typeof value === 'string') {
115
- chrome[element] = { color: value };
116
- } else {
117
- const override: Partial<ChromeDefaults> = {};
118
- const obj = value as ChromeThemeOverride;
119
- if (obj.color !== undefined) override.color = resolveTokenLight(obj.color);
120
- if (obj.fontSize !== undefined) override.fontSize = obj.fontSize;
121
- if (obj.fontWeight !== undefined) override.fontWeight = obj.fontWeight;
122
- if (obj.lineHeight !== undefined) override.lineHeight = obj.lineHeight;
123
- chrome[element] = override;
124
- }
125
- }
126
- partial.chrome = chrome as unknown as Theme['chrome'];
127
- }
128
-
129
- if (config.seriesStrategy) {
130
- partial.seriesStrategy = config.seriesStrategy;
131
- }
132
-
133
- return partial;
134
- }
135
-
136
- /**
137
- * Collect TokenValue pairs from the user's ThemeConfig so adaptTheme()
138
- * can use explicit dark values instead of algorithmic adaptation.
139
- */
140
- function collectTokenPairs(
141
- config: ThemeConfig,
142
- ): Record<string, { light: string; dark: string }> | undefined {
143
- const pairs: Record<string, { light: string; dark: string }> = {};
144
- if (config.colors && !Array.isArray(config.colors)) {
145
- const colorFields = [
146
- 'background',
147
- 'text',
148
- 'gridline',
149
- 'axis',
150
- 'annotationFill',
151
- 'annotationText',
152
- 'positive',
153
- 'negative',
154
- ] as const;
155
- for (const field of colorFields) {
156
- const v = config.colors[field];
157
- if (v && typeof v === 'object' && 'light' in v) {
158
- pairs[`colors.${field}`] = v;
159
- }
160
- }
161
- }
162
- if (config.chrome) {
163
- for (const [element, value] of Object.entries(config.chrome)) {
164
- if (
165
- value &&
166
- typeof value === 'object' &&
167
- 'color' in value &&
168
- value.color &&
169
- typeof value.color === 'object' &&
170
- 'light' in value.color
171
- ) {
172
- pairs[`chrome.${element}.color`] = value.color;
173
- }
174
- }
175
- }
176
- return Object.keys(pairs).length > 0 ? pairs : undefined;
177
- }
178
-
179
- /**
180
- * Parse a hex color to sRGB relative luminance (WCAG 2.1).
181
- * Returns 0 for invalid/unparseable colors.
182
- */
183
- function relativeLuminance(hex: string): number {
184
- const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
185
- if (!m) return 0;
186
- const r = parseInt(m[1].slice(0, 2), 16) / 255;
187
- const g = parseInt(m[1].slice(2, 4), 16) / 255;
188
- const b = parseInt(m[1].slice(4, 6), 16) / 255;
189
- const toLinear = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
190
- return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
191
- }
192
-
193
- /** Returns true if the hex color is perceptually dark (luminance < 0.2). */
194
- function isDarkBackground(hex: string): boolean {
195
- // Transparent has no intrinsic luminance — don't treat it as dark so
196
- // light-mode chrome text defaults don't get inverted.
197
- if (hex === 'transparent') return false;
198
- return relativeLuminance(hex) < 0.2;
199
- }
200
-
201
- /**
202
- * Adapt chrome text colors for a dark background.
203
- * Only overrides values that still match the light-mode defaults,
204
- * so explicit user overrides are preserved.
205
- */
206
- function adaptChromeForDarkBg(theme: Theme, textColor: string): Theme {
207
- const light = DEFAULT_THEME.chrome;
208
- return {
209
- ...theme,
210
- chrome: {
211
- // Eyebrow keeps its accent tint regardless of surface mode.
212
- eyebrow: theme.chrome.eyebrow,
213
- title: {
214
- ...theme.chrome.title,
215
- color:
216
- theme.chrome.title.color === light.title.color ? textColor : theme.chrome.title.color,
217
- },
218
- subtitle: {
219
- ...theme.chrome.subtitle,
220
- color:
221
- theme.chrome.subtitle.color === light.subtitle.color
222
- ? adjustOpacity(textColor, 0.7)
223
- : theme.chrome.subtitle.color,
224
- },
225
- source: {
226
- ...theme.chrome.source,
227
- color:
228
- theme.chrome.source.color === light.source.color
229
- ? adjustOpacity(textColor, 0.5)
230
- : theme.chrome.source.color,
231
- },
232
- byline: {
233
- ...theme.chrome.byline,
234
- color:
235
- theme.chrome.byline.color === light.byline.color
236
- ? adjustOpacity(textColor, 0.5)
237
- : theme.chrome.byline.color,
238
- },
239
- footer: {
240
- ...theme.chrome.footer,
241
- color:
242
- theme.chrome.footer.color === light.footer.color
243
- ? adjustOpacity(textColor, 0.5)
244
- : theme.chrome.footer.color,
245
- },
246
- },
247
- };
248
- }
249
-
250
- /** Blend a hex color toward white/black by a factor to simulate opacity. */
251
- function adjustOpacity(hex: string, opacity: number): string {
252
- const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
253
- if (!m) return hex;
254
- const r = parseInt(m[1].slice(0, 2), 16);
255
- const g = parseInt(m[1].slice(2, 4), 16);
256
- const b = parseInt(m[1].slice(4, 6), 16);
257
- // Blend toward mid-gray for muted secondary text
258
- const mix = (c: number) => Math.round(c * opacity + 128 * (1 - opacity));
259
- const toHex = (n: number) => n.toString(16).padStart(2, '0');
260
- return `#${toHex(mix(r))}${toHex(mix(g))}${toHex(mix(b))}`;
261
- }
262
-
263
- /**
264
- * Resolve a theme by deep-merging user overrides onto a base theme.
265
- *
266
- * Auto-detects dark backgrounds: if the resolved background color is
267
- * perceptually dark and chrome text colors are still the light-mode
268
- * defaults, they're adapted for readability.
269
- *
270
- * @param userTheme - Optional partial theme overrides from the spec.
271
- * @param base - Base theme to merge onto. Defaults to DEFAULT_THEME.
272
- * @returns A ResolvedTheme with all properties guaranteed.
273
- */
274
- export function resolveTheme(userTheme?: ThemeConfig, base: Theme = DEFAULT_THEME): ResolvedTheme {
275
- let merged: Theme = userTheme ? deepMerge(base, themeConfigToPartial(userTheme)) : { ...base };
276
-
277
- // Propagate fonts.sizes overrides onto the chrome element fontSizes.
278
- // The chrome defaults hard-code fontSize independently from fonts.sizes,
279
- // so we sync them here after merging so a single sizes override applies everywhere.
280
- if (userTheme?.fonts?.sizes) {
281
- const s = userTheme.fonts.sizes;
282
- merged = {
283
- ...merged,
284
- chrome: {
285
- ...merged.chrome,
286
- ...(s.title !== undefined && {
287
- title: { ...merged.chrome.title, fontSize: s.title },
288
- }),
289
- ...(s.subtitle !== undefined && {
290
- subtitle: { ...merged.chrome.subtitle, fontSize: s.subtitle },
291
- }),
292
- ...(s.small !== undefined && {
293
- source: { ...merged.chrome.source, fontSize: s.small },
294
- byline: { ...merged.chrome.byline, fontSize: s.small },
295
- footer: { ...merged.chrome.footer, fontSize: s.small },
296
- }),
297
- },
298
- };
299
- }
300
-
301
- // Auto-adapt chrome for dark backgrounds
302
- const dark = isDarkBackground(merged.colors.background);
303
- if (dark) {
304
- merged = adaptChromeForDarkBg(merged, merged.colors.text);
305
- }
306
-
307
- return {
308
- ...merged,
309
- isDark: dark,
310
- _tokenPairs: userTheme ? collectTokenPairs(userTheme) : undefined,
311
- } as ResolvedTheme;
312
- }