@opendata-ai/openchart-core 7.6.0 → 7.7.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.
- package/dist/index.d.ts +476 -179
- package/dist/index.js +155 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/colors/palettes.ts +3 -3
- package/src/helpers/spec-builders.ts +5 -2
- package/src/index.ts +5 -0
- package/src/styles/dark.css +5 -1
- package/src/styles/tokens.css +6 -0
- package/src/theme/__tests__/dark-mode.test.ts +80 -0
- package/src/theme/__tests__/resolve.test.ts +114 -0
- package/src/theme/dark-mode.ts +56 -26
- package/src/theme/defaults.ts +2 -1
- package/src/theme/index.ts +1 -0
- package/src/theme/presets.ts +64 -0
- package/src/theme/resolve.ts +85 -12
- package/src/types/index.ts +8 -0
- package/src/types/layout.ts +65 -1
- package/src/types/spec.ts +158 -29
- package/src/types/theme.ts +36 -0
package/package.json
CHANGED
package/src/colors/palettes.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* binary search. Source OKLCH values are documented inline so the ramp
|
|
9
9
|
* can be regenerated if needed.
|
|
10
10
|
*
|
|
11
|
-
* Convention for low-cardinality charts (1 / 2-4 / 5+ series) is
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Convention for low-cardinality charts (1 / 2-4 / 5+ series) is available
|
|
12
|
+
* as an opt-in via `theme.seriesStrategy`. The default strategy is 'palette'
|
|
13
|
+
* (full categorical ramp always). The accent-neutral convention is:
|
|
14
14
|
* - 1 series: cyan only
|
|
15
15
|
* - 2-4 series: cyan + zinc-400/500/600 (achromatic)
|
|
16
16
|
* - 5+ series: full multi-hue ramp below
|
|
@@ -425,8 +425,10 @@ export function dataTable(data: DataRow[], options?: TableBuilderOptions): Table
|
|
|
425
425
|
export interface TileMapBuilderOptions {
|
|
426
426
|
/** Encoding channels (required when data is DataRow[]). */
|
|
427
427
|
encoding?: TileMapEncoding;
|
|
428
|
-
/** Sequential color palette name. */
|
|
428
|
+
/** Sequential color palette name (quantitative mode only). */
|
|
429
429
|
palette?: TileMapPalette;
|
|
430
|
+
/** Custom category-to-color mapping. Forces categorical mode. */
|
|
431
|
+
colors?: Record<string, string>;
|
|
430
432
|
/** Editorial chrome. */
|
|
431
433
|
chrome?: Chrome;
|
|
432
434
|
/** Legend display configuration. */
|
|
@@ -457,7 +459,7 @@ export interface TileMapBuilderOptions {
|
|
|
457
459
|
* ```
|
|
458
460
|
*/
|
|
459
461
|
export function tileMap(
|
|
460
|
-
data: Record<string, number | null> | DataRow[],
|
|
462
|
+
data: Record<string, number | string | null> | DataRow[],
|
|
461
463
|
options?: TileMapBuilderOptions,
|
|
462
464
|
): TileMapSpec {
|
|
463
465
|
return {
|
|
@@ -465,6 +467,7 @@ export function tileMap(
|
|
|
465
467
|
data,
|
|
466
468
|
...(options?.encoding && { encoding: options.encoding }),
|
|
467
469
|
...(options?.palette && { palette: options.palette }),
|
|
470
|
+
...(options?.colors && { colors: options.colors }),
|
|
468
471
|
...(options?.chrome && { chrome: options.chrome }),
|
|
469
472
|
...(options?.legend && { legend: options.legend }),
|
|
470
473
|
...(options?.theme && { theme: options.theme }),
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type {
|
|
|
27
27
|
SequentialPalette,
|
|
28
28
|
} from './colors/index';
|
|
29
29
|
export {
|
|
30
|
+
ACHROMATIC_RAMP,
|
|
30
31
|
CATEGORICAL_PALETTE,
|
|
31
32
|
checkPaletteDistinguishability,
|
|
32
33
|
contrastRatio,
|
|
@@ -44,9 +45,13 @@ export {
|
|
|
44
45
|
|
|
45
46
|
export {
|
|
46
47
|
adaptColorForDarkMode,
|
|
48
|
+
adaptForLightLineStroke,
|
|
47
49
|
adaptTheme,
|
|
48
50
|
DEFAULT_THEME,
|
|
51
|
+
editorial,
|
|
52
|
+
essay,
|
|
49
53
|
resolveTheme,
|
|
54
|
+
wire,
|
|
50
55
|
} from './theme/index';
|
|
51
56
|
|
|
52
57
|
// ---------------------------------------------------------------------------
|
package/src/styles/dark.css
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/* ---------------------------------------------------------------------------
|
|
2
|
-
* Dark mode overrides
|
|
2
|
+
* Dark mode overrides (fallback defaults)
|
|
3
|
+
*
|
|
4
|
+
* At runtime, the JS theme engine stamps dark-adapted --oc-* custom
|
|
5
|
+
* properties on each .oc-root container. These CSS values serve as
|
|
6
|
+
* fallbacks for contexts where JS hasn't mounted yet.
|
|
3
7
|
* --------------------------------------------------------------------------- */
|
|
4
8
|
|
|
5
9
|
.oc-dark {
|
package/src/styles/tokens.css
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/* ---------------------------------------------------------------------------
|
|
2
2
|
* Custom properties (light mode defaults)
|
|
3
|
+
*
|
|
4
|
+
* These are FALLBACK defaults. At runtime, the JS theme engine stamps the
|
|
5
|
+
* resolved theme as inline --oc-* custom properties on each .oc-root
|
|
6
|
+
* container (see packages/vanilla/src/theme-tokens.ts). These CSS values
|
|
7
|
+
* serve as fallbacks for contexts where JS hasn't mounted yet (SSR, static
|
|
8
|
+
* HTML examples).
|
|
3
9
|
* --------------------------------------------------------------------------- */
|
|
4
10
|
|
|
5
11
|
.oc-root,
|
|
@@ -149,4 +149,84 @@ describe('adaptTheme', () => {
|
|
|
149
149
|
// Unset chrome colors fall to the muted dark default, not the light one.
|
|
150
150
|
expect(dark.chrome.subtitle.color).not.toBe('#71717a');
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
it('uses explicit dark pair value for background instead of algorithmic adaptation', () => {
|
|
154
|
+
const light = resolveTheme({
|
|
155
|
+
colors: { background: { light: '#fff1e5', dark: '#1a1311' } },
|
|
156
|
+
});
|
|
157
|
+
const dark = adaptTheme(light);
|
|
158
|
+
expect(dark.colors.background).toBe('#1a1311');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('uses explicit dark pair value for text instead of algorithmic adaptation', () => {
|
|
162
|
+
const light = resolveTheme({
|
|
163
|
+
colors: {
|
|
164
|
+
background: { light: '#ffffff', dark: '#111111' },
|
|
165
|
+
text: { light: '#1c1917', dark: '#e7e5e4' },
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const dark = adaptTheme(light);
|
|
169
|
+
expect(dark.colors.text).toBe('#e7e5e4');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('uses explicit dark pair for gridline and axis', () => {
|
|
173
|
+
const light = resolveTheme({
|
|
174
|
+
colors: {
|
|
175
|
+
background: { light: '#fff', dark: '#111' },
|
|
176
|
+
gridline: { light: 'rgba(0,0,0,0.08)', dark: 'rgba(255,255,255,0.06)' },
|
|
177
|
+
axis: { light: '#78716c', dark: '#a8a29e' },
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const dark = adaptTheme(light);
|
|
181
|
+
expect(dark.colors.gridline).toBe('rgba(255,255,255,0.06)');
|
|
182
|
+
expect(dark.colors.axis).toBe('#a8a29e');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('uses explicit dark pair for semantic colors', () => {
|
|
186
|
+
const light = resolveTheme({
|
|
187
|
+
colors: {
|
|
188
|
+
background: { light: '#fff', dark: '#111' },
|
|
189
|
+
positive: { light: '#10b981', dark: '#34d399' },
|
|
190
|
+
negative: { light: '#e11d48', dark: '#fb7185' },
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const dark = adaptTheme(light);
|
|
194
|
+
expect(dark.colors.positive).toBe('#34d399');
|
|
195
|
+
expect(dark.colors.negative).toBe('#fb7185');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('uses explicit dark pair for chrome element color', () => {
|
|
199
|
+
const light = resolveTheme({
|
|
200
|
+
colors: { background: { light: '#fff', dark: '#111' } },
|
|
201
|
+
chrome: {
|
|
202
|
+
title: { color: { light: '#33302e', dark: '#f2dfce' } },
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const dark = adaptTheme(light);
|
|
206
|
+
expect(dark.chrome.title.color).toBe('#f2dfce');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('uses explicit dark pair for eyebrow color (otherwise kept as-is)', () => {
|
|
210
|
+
const light = resolveTheme({
|
|
211
|
+
chrome: {
|
|
212
|
+
eyebrow: { color: { light: '#e3120b', dark: '#ff6b64' } },
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
const dark = adaptTheme(light);
|
|
216
|
+
expect(dark.chrome.eyebrow.color).toBe('#ff6b64');
|
|
217
|
+
|
|
218
|
+
// Without a pair, the eyebrow keeps its accent tint across modes.
|
|
219
|
+
const noPair = adaptTheme(resolveTheme({ chrome: { eyebrow: '#e3120b' } }));
|
|
220
|
+
expect(noPair.chrome.eyebrow.color).toBe('#e3120b');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('falls back to algorithmic adaptation when no explicit dark pair is given', () => {
|
|
224
|
+
const light = resolveTheme({
|
|
225
|
+
colors: { background: '#ffffff' },
|
|
226
|
+
});
|
|
227
|
+
const dark = adaptTheme(light);
|
|
228
|
+
// Text should be adapted to a light color for dark bg readability
|
|
229
|
+
expect(dark.colors.text).not.toBe(light.colors.text);
|
|
230
|
+
expect(dark.colors.text).toBeDefined();
|
|
231
|
+
});
|
|
152
232
|
});
|
|
@@ -161,3 +161,117 @@ describe('resolveTheme deep merge edge cases', () => {
|
|
|
161
161
|
expect(resolved.chrome.title.color).not.toBe(DEFAULT_THEME.chrome.title.color);
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// TokenValue resolution
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
describe('resolveTheme TokenValue handling', () => {
|
|
170
|
+
it('resolves plain string TokenValue to same string', () => {
|
|
171
|
+
const resolved = resolveTheme({ colors: { background: '#faf8f5' } });
|
|
172
|
+
expect(resolved.colors.background).toBe('#faf8f5');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('resolves light/dark TokenValue pair to the light value', () => {
|
|
176
|
+
const resolved = resolveTheme({
|
|
177
|
+
colors: { background: { light: '#faf8f5', dark: '#1a1816' } },
|
|
178
|
+
});
|
|
179
|
+
expect(resolved.colors.background).toBe('#faf8f5');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('stores token pairs on _tokenPairs for adaptTheme consumption', () => {
|
|
183
|
+
const resolved = resolveTheme({
|
|
184
|
+
colors: {
|
|
185
|
+
background: { light: '#fff', dark: '#111' },
|
|
186
|
+
text: { light: '#000', dark: '#eee' },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
expect(resolved._tokenPairs).toBeDefined();
|
|
190
|
+
expect(resolved._tokenPairs?.['colors.background']).toEqual({ light: '#fff', dark: '#111' });
|
|
191
|
+
expect(resolved._tokenPairs?.['colors.text']).toEqual({ light: '#000', dark: '#eee' });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does not create _tokenPairs when all colors are plain strings', () => {
|
|
195
|
+
const resolved = resolveTheme({
|
|
196
|
+
colors: { background: '#fff', text: '#000' },
|
|
197
|
+
});
|
|
198
|
+
expect(resolved._tokenPairs).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('resolves TokenValue on semantic color fields', () => {
|
|
202
|
+
const resolved = resolveTheme({
|
|
203
|
+
colors: {
|
|
204
|
+
positive: { light: '#10b981', dark: '#34d399' },
|
|
205
|
+
negative: { light: '#e11d48', dark: '#fb7185' },
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
expect(resolved.colors.positive).toBe('#10b981');
|
|
209
|
+
expect(resolved.colors.negative).toBe('#e11d48');
|
|
210
|
+
expect(resolved._tokenPairs?.['colors.positive']).toEqual({
|
|
211
|
+
light: '#10b981',
|
|
212
|
+
dark: '#34d399',
|
|
213
|
+
});
|
|
214
|
+
expect(resolved._tokenPairs?.['colors.negative']).toEqual({
|
|
215
|
+
light: '#e11d48',
|
|
216
|
+
dark: '#fb7185',
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// New ThemeConfig fields
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
describe('resolveTheme widened ThemeConfig', () => {
|
|
226
|
+
it('overrides fonts.weights partially', () => {
|
|
227
|
+
const resolved = resolveTheme({
|
|
228
|
+
fonts: { weights: { normal: 400, bold: 700 } },
|
|
229
|
+
});
|
|
230
|
+
expect(resolved.fonts.weights.normal).toBe(400);
|
|
231
|
+
expect(resolved.fonts.weights.bold).toBe(700);
|
|
232
|
+
expect(resolved.fonts.weights.medium).toBe(DEFAULT_THEME.fonts.weights.medium);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('overrides new spacing fields', () => {
|
|
236
|
+
const resolved = resolveTheme({
|
|
237
|
+
spacing: { chromeToChart: 16, chartToFooter: 20, axisMargin: 8 },
|
|
238
|
+
});
|
|
239
|
+
expect(resolved.spacing.chromeToChart).toBe(16);
|
|
240
|
+
expect(resolved.spacing.chartToFooter).toBe(20);
|
|
241
|
+
expect(resolved.spacing.axisMargin).toBe(8);
|
|
242
|
+
expect(resolved.spacing.padding).toBe(DEFAULT_THEME.spacing.padding);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('overrides chrome typography with ChromeThemeOverride object', () => {
|
|
246
|
+
const resolved = resolveTheme({
|
|
247
|
+
chrome: {
|
|
248
|
+
title: { fontWeight: 700, fontSize: 32, lineHeight: 1.1 },
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
expect(resolved.chrome.title.fontWeight).toBe(700);
|
|
252
|
+
expect(resolved.chrome.title.fontSize).toBe(32);
|
|
253
|
+
expect(resolved.chrome.title.lineHeight).toBe(1.1);
|
|
254
|
+
expect(resolved.chrome.subtitle.fontWeight).toBe(DEFAULT_THEME.chrome.subtitle.fontWeight);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('ChromeThemeOverride color accepts TokenValue', () => {
|
|
258
|
+
const resolved = resolveTheme({
|
|
259
|
+
chrome: {
|
|
260
|
+
title: { color: { light: '#111', dark: '#eee' }, fontWeight: 600 },
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
expect(resolved.chrome.title.color).toBe('#111');
|
|
264
|
+
expect(resolved.chrome.title.fontWeight).toBe(600);
|
|
265
|
+
expect(resolved._tokenPairs?.['chrome.title.color']).toEqual({ light: '#111', dark: '#eee' });
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('passes seriesStrategy through to resolved theme', () => {
|
|
269
|
+
const resolved = resolveTheme({ seriesStrategy: 'accent-neutral' });
|
|
270
|
+
expect(resolved.seriesStrategy).toBe('accent-neutral');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('defaults seriesStrategy to palette', () => {
|
|
274
|
+
const resolved = resolveTheme();
|
|
275
|
+
expect(resolved.seriesStrategy).toBe('palette');
|
|
276
|
+
});
|
|
277
|
+
});
|
package/src/theme/dark-mode.ts
CHANGED
|
@@ -106,6 +106,7 @@ function _luminanceFromHex(color: string): number {
|
|
|
106
106
|
* adjusts gridline and axis colors for the dark background.
|
|
107
107
|
*/
|
|
108
108
|
export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
|
|
109
|
+
const pairs = theme._tokenPairs;
|
|
109
110
|
const inputBg = theme.colors.background;
|
|
110
111
|
// "transparent" preserves the background token but must still adopt dark
|
|
111
112
|
// text/gridline/axis colors — the container is dark, not the chart canvas.
|
|
@@ -115,23 +116,33 @@ export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
|
|
|
115
116
|
// Preserve user-supplied background (including transparent) but always
|
|
116
117
|
// apply dark-mode text/axis/gridline colors so labels are readable on the
|
|
117
118
|
// dark host surface.
|
|
118
|
-
const darkBg =
|
|
119
|
+
const darkBg = pairs?.['colors.background']
|
|
120
|
+
? pairs['colors.background'].dark
|
|
121
|
+
: alreadyDark
|
|
122
|
+
? inputBg
|
|
123
|
+
: DARK_BG;
|
|
119
124
|
const darkMuted = ACHROMATIC_RAMP.fgMuted;
|
|
120
125
|
|
|
121
126
|
// Per the "sensible defaults, full override" convention (see spec-grammar),
|
|
122
127
|
// dark-mode adaptation only swaps in dark defaults for colors the spec
|
|
123
128
|
// left at their light-mode default. An explicit override is preserved.
|
|
124
|
-
//
|
|
129
|
+
// When a TokenValue pair has an explicit dark value, use it directly.
|
|
125
130
|
const light = DEFAULT_THEME.colors;
|
|
126
131
|
const lightChrome = DEFAULT_THEME.chrome;
|
|
127
132
|
const overridden = <T>(current: T, lightDefault: T, darkDefault: T): T =>
|
|
128
133
|
current === lightDefault ? darkDefault : current;
|
|
129
134
|
|
|
130
|
-
const darkText =
|
|
131
|
-
|
|
135
|
+
const darkText = pairs?.['colors.text']
|
|
136
|
+
? pairs['colors.text'].dark
|
|
137
|
+
: overridden(theme.colors.text, light.text, DARK_TEXT);
|
|
138
|
+
const darkGridline = pairs?.['colors.gridline']
|
|
139
|
+
? pairs['colors.gridline'].dark
|
|
140
|
+
: overridden(theme.colors.gridline, light.gridline, 'rgba(255,255,255,0.05)');
|
|
132
141
|
// axis is also tick-label fill — needs WCAG AA contrast on dark bg.
|
|
133
142
|
// Zinc-400 (`#a1a1aa`) hits ~6:1 against #09090b.
|
|
134
|
-
const darkAxis =
|
|
143
|
+
const darkAxis = pairs?.['colors.axis']
|
|
144
|
+
? pairs['colors.axis'].dark
|
|
145
|
+
: overridden(theme.colors.axis, light.axis, '#a1a1aa');
|
|
135
146
|
|
|
136
147
|
// Categorical palette is pinned to design-system tokens. The same vibrant
|
|
137
148
|
// hex values render in both light and dark modes — adapting them via
|
|
@@ -148,44 +159,63 @@ export function adaptTheme(theme: ResolvedTheme): ResolvedTheme {
|
|
|
148
159
|
text: darkText,
|
|
149
160
|
gridline: darkGridline,
|
|
150
161
|
axis: darkAxis,
|
|
151
|
-
annotationFill:
|
|
152
|
-
|
|
153
|
-
light.annotationFill,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
annotationFill: pairs?.['colors.annotationFill']
|
|
163
|
+
? pairs['colors.annotationFill'].dark
|
|
164
|
+
: overridden(theme.colors.annotationFill, light.annotationFill, 'rgba(255,255,255,0.06)'),
|
|
165
|
+
annotationText: pairs?.['colors.annotationText']
|
|
166
|
+
? pairs['colors.annotationText'].dark
|
|
167
|
+
: overridden(theme.colors.annotationText, light.annotationText, darkMuted),
|
|
157
168
|
categorical,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
positive: pairs?.['colors.positive']
|
|
170
|
+
? pairs['colors.positive'].dark
|
|
171
|
+
: theme.colors.positive !== '#16a34a'
|
|
172
|
+
? theme.colors.positive
|
|
173
|
+
: '#34d399',
|
|
174
|
+
negative: pairs?.['colors.negative']
|
|
175
|
+
? pairs['colors.negative'].dark
|
|
176
|
+
: theme.colors.negative !== '#dc2626'
|
|
177
|
+
? theme.colors.negative
|
|
178
|
+
: '#f87171',
|
|
163
179
|
},
|
|
164
180
|
chrome: {
|
|
165
|
-
// Eyebrow keeps its accent tint (cyan in both modes)
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// so explicit chrome color
|
|
169
|
-
|
|
181
|
+
// Eyebrow keeps its accent tint (cyan in both modes) unless the user
|
|
182
|
+
// gave an explicit dark value; the other chrome elements desaturate
|
|
183
|
+
// to a muted gray on the dark canvas. Each color only adapts if the
|
|
184
|
+
// spec left it at the light default, so explicit chrome color
|
|
185
|
+
// overrides survive dark-mode adaptation. When a TokenValue pair has
|
|
186
|
+
// an explicit dark value, use it directly.
|
|
187
|
+
eyebrow: pairs?.['chrome.eyebrow.color']
|
|
188
|
+
? { ...theme.chrome.eyebrow, color: pairs['chrome.eyebrow.color'].dark }
|
|
189
|
+
: theme.chrome.eyebrow,
|
|
170
190
|
title: {
|
|
171
191
|
...theme.chrome.title,
|
|
172
|
-
color:
|
|
192
|
+
color: pairs?.['chrome.title.color']
|
|
193
|
+
? pairs['chrome.title.color'].dark
|
|
194
|
+
: overridden(theme.chrome.title.color, lightChrome.title.color, darkText),
|
|
173
195
|
},
|
|
174
196
|
subtitle: {
|
|
175
197
|
...theme.chrome.subtitle,
|
|
176
|
-
color:
|
|
198
|
+
color: pairs?.['chrome.subtitle.color']
|
|
199
|
+
? pairs['chrome.subtitle.color'].dark
|
|
200
|
+
: overridden(theme.chrome.subtitle.color, lightChrome.subtitle.color, darkMuted),
|
|
177
201
|
},
|
|
178
202
|
source: {
|
|
179
203
|
...theme.chrome.source,
|
|
180
|
-
color:
|
|
204
|
+
color: pairs?.['chrome.source.color']
|
|
205
|
+
? pairs['chrome.source.color'].dark
|
|
206
|
+
: overridden(theme.chrome.source.color, lightChrome.source.color, darkMuted),
|
|
181
207
|
},
|
|
182
208
|
byline: {
|
|
183
209
|
...theme.chrome.byline,
|
|
184
|
-
color:
|
|
210
|
+
color: pairs?.['chrome.byline.color']
|
|
211
|
+
? pairs['chrome.byline.color'].dark
|
|
212
|
+
: overridden(theme.chrome.byline.color, lightChrome.byline.color, darkMuted),
|
|
185
213
|
},
|
|
186
214
|
footer: {
|
|
187
215
|
...theme.chrome.footer,
|
|
188
|
-
color:
|
|
216
|
+
color: pairs?.['chrome.footer.color']
|
|
217
|
+
? pairs['chrome.footer.color'].dark
|
|
218
|
+
: overridden(theme.chrome.footer.color, lightChrome.footer.color, darkMuted),
|
|
189
219
|
},
|
|
190
220
|
},
|
|
191
221
|
};
|
package/src/theme/defaults.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Default theme definition.
|
|
3
3
|
*
|
|
4
4
|
* Editorial design system with cyan-led categorical palette, Inter
|
|
5
|
-
* Variable typography (weights
|
|
5
|
+
* Variable typography (weights 450/550/590/700), and zinc-based achromatic
|
|
6
6
|
* surfaces. Light is the default surface; dark adaptation lives in
|
|
7
7
|
* dark-mode.ts.
|
|
8
8
|
*
|
|
@@ -106,4 +106,5 @@ export const DEFAULT_THEME: Theme = {
|
|
|
106
106
|
lineHeight: 1.4,
|
|
107
107
|
},
|
|
108
108
|
},
|
|
109
|
+
seriesStrategy: 'palette',
|
|
109
110
|
};
|
package/src/theme/index.ts
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
};
|