@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,156 +0,0 @@
1
- /* GENERATED FILE — do not edit. Source: packages/core/src/styles/token-definitions.ts. Regenerate: bun run generate:tokens */
2
-
3
- @layer oc.tokens {
4
- /* ---------------------------------------------------------------------------
5
- * Custom properties (light mode defaults)
6
- *
7
- * These are FALLBACK defaults. At runtime, the JS theme engine stamps the
8
- * resolved theme as inline --oc-* custom properties on each .oc-root
9
- * container (see packages/vanilla/src/theme-tokens.ts). These CSS values
10
- * serve as fallbacks for contexts where JS hasn't mounted yet (SSR, static
11
- * HTML examples).
12
- * --------------------------------------------------------------------------- */
13
-
14
- .oc-root,
15
- .oc-chart-root,
16
- .oc-table-wrapper,
17
- .oc-table-root,
18
- .oc-graph-wrapper,
19
- .oc-graph-root,
20
- .oc-sankey-root,
21
- .oc-tilemap-root,
22
- .oc-barlist-root,
23
- .oc-map-root {
24
- -webkit-font-smoothing: antialiased;
25
- -moz-osx-font-smoothing: grayscale;
26
-
27
- /* Opt out of iOS text auto-inflation so measured widths match rendered text. */
28
- -webkit-text-size-adjust: 100%;
29
- text-size-adjust: 100%;
30
-
31
- --oc-font-family:
32
- "Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
33
- --oc-font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
34
-
35
- /* Animation easing presets via CSS linear() */
36
- --oc-ease-smooth: linear(
37
- 0,
38
- 0.157,
39
- 0.438,
40
- 0.64,
41
- 0.766,
42
- 0.85,
43
- 0.906,
44
- 0.941,
45
- 0.964,
46
- 0.978,
47
- 0.988,
48
- 0.994,
49
- 0.998,
50
- 1
51
- );
52
- --oc-ease-snappy: linear(
53
- 0,
54
- 0.012,
55
- 0.048,
56
- 0.108,
57
- 0.194,
58
- 0.302,
59
- 0.426,
60
- 0.559,
61
- 0.69,
62
- 0.808,
63
- 0.905,
64
- 0.973,
65
- 1.013,
66
- 1.028,
67
- 1.023,
68
- 1.006,
69
- 0.984,
70
- 0.966,
71
- 0.957,
72
- 0.957,
73
- 0.964,
74
- 0.975,
75
- 0.986,
76
- 0.995,
77
- 1,
78
- 1.003,
79
- 1.002,
80
- 1,
81
- 0.998,
82
- 0.998,
83
- 0.999,
84
- 1
85
- );
86
-
87
- /* Animation timing defaults */
88
- --oc-animation-duration: 500ms;
89
- --oc-animation-stagger: 80ms;
90
- --oc-annotation-delay: 200ms;
91
-
92
- /* Typography scale (editorial design system) */
93
- --oc-title-size: 26px;
94
- --oc-title-weight: 590;
95
- --oc-title-tracking: -0.022em;
96
- --oc-subtitle-size: 14px;
97
- --oc-subtitle-weight: 450;
98
- --oc-source-size: 11px;
99
- --oc-source-weight: 450;
100
- --oc-body-size: 13px;
101
- --oc-eyebrow-size: 11px;
102
- --oc-eyebrow-weight: 550;
103
- --oc-eyebrow-tracking: 0.08em;
104
-
105
- /* Surfaces (light mode defaults) */
106
- --oc-bg: #ffffff;
107
- --oc-card: #ffffff;
108
- --oc-secondary: #f4f4f5; /* zinc-100, raised surface */
109
-
110
- /*
111
- * Text levels. Names invert across modes: in light mode "muted" sits at
112
- * a lighter zinc step than "subtle" because both are picked relative to
113
- * the active background, not from a fixed lightness ladder. The intent
114
- * is "muted = first step away from primary text"; "subtle = next step
115
- * down"; etc.
116
- *
117
- * Light mode: text=zinc-950, secondary=zinc-700, muted=zinc-500,
118
- * subtle=zinc-400, faint=zinc-300
119
- * Dark mode (dark.css): inverts the surface tokens but keeps the same
120
- * muted -> subtle -> faint progression away from primary.
121
- */
122
- --oc-text: #09090b;
123
- --oc-text-secondary: #3f3f46;
124
- --oc-text-muted: #71717a;
125
- --oc-text-faint: #d4d4d8;
126
-
127
- /* Lines */
128
- --oc-gridline: rgba(0, 0, 0, 0.1);
129
- --oc-axis: rgba(0, 0, 0, 0.1);
130
- --oc-border: rgba(0, 0, 0, 0.08);
131
- --oc-border-radius: 2px;
132
-
133
- /* Brand and semantic */
134
- --oc-accent: #06b6d4;
135
- --oc-accent-strong: #0891b2; /* darker cyan for line strokes on light bg */
136
- --oc-positive: #10b981;
137
- --oc-negative: #e11d48;
138
- --oc-focus: #3b82f6;
139
- /* static, intentionally NOT derived from --oc-focus and NOT flipped in dark mode */
140
- --oc-focus-ring: rgba(59, 130, 246, 0.1);
141
- --oc-focus-ring-strong: rgba(59, 130, 246, 0.25);
142
- --oc-editable-hover: rgba(79, 70, 229, 0.35); /* edit-mode hover outline, indigo one-off */
143
-
144
- /* Spacing scale (4px base) — only --oc-space-2/4 are consumed (JS-stamped) */
145
- --oc-space-2: 8px;
146
- --oc-space-4: 16px;
147
-
148
- /* Interactive states */
149
- --oc-hover-bg: rgba(0, 0, 0, 0.025);
150
- --oc-tooltip-bg: rgba(255, 255, 255, 0.88);
151
- --oc-tooltip-border: rgba(0, 0, 0, 0.08);
152
- --oc-tooltip-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.12);
153
- --oc-tooltip-text: #09090b;
154
- --oc-legend-text: #3f3f46;
155
- }
156
- }
@@ -1,126 +0,0 @@
1
- @layer oc.components {
2
- /* ---------------------------------------------------------------------------
3
- * Tooltip
4
- * Editorial card matching the new design system: zinc surface, cyan accent,
5
- * Inter Variable, mono numerics. Sized for a slice tooltip that lists every
6
- * series at the snapped x.
7
- * --------------------------------------------------------------------------- */
8
-
9
- .oc-tooltip {
10
- position: absolute;
11
- display: none;
12
- pointer-events: none;
13
- z-index: 1000;
14
- background: var(--oc-tooltip-bg);
15
- backdrop-filter: blur(14px);
16
- border: 1px solid var(--oc-tooltip-border);
17
- border-radius: var(--oc-border-radius, 2px);
18
- box-shadow: var(--oc-tooltip-shadow);
19
- color: var(--oc-tooltip-text);
20
- font-family: var(--oc-font-family);
21
- font-size: 12px;
22
- padding: 0;
23
- max-width: 280px;
24
- min-width: 160px;
25
- line-height: 1.4;
26
- animation: oc-tooltip-in 120ms ease-out;
27
- transition:
28
- left 100ms ease-in-out,
29
- top 100ms ease-in-out;
30
-
31
- & .oc-tooltip-header {
32
- display: flex;
33
- align-items: center;
34
- gap: 8px;
35
- padding: 8px 12px 6px;
36
- }
37
-
38
- & .oc-tooltip-dot {
39
- width: 8px;
40
- height: 8px;
41
- border-radius: 50%;
42
- flex-shrink: 0;
43
- }
44
-
45
- & .oc-tooltip-title {
46
- font-weight: 590;
47
- font-size: 11px;
48
- letter-spacing: 0.04em;
49
- text-transform: uppercase;
50
- color: var(--oc-text-muted);
51
- white-space: nowrap;
52
- overflow: hidden;
53
- text-overflow: ellipsis;
54
- }
55
-
56
- & .oc-tooltip-body {
57
- padding: 6px 12px 10px;
58
- border-top: 1px solid var(--oc-tooltip-border);
59
- }
60
-
61
- /* No separator when body is the only child */
62
- & .oc-tooltip-body:first-child {
63
- border-top: none;
64
- padding-top: 10px;
65
- }
66
-
67
- & .oc-tooltip-row {
68
- display: flex;
69
- align-items: baseline;
70
- justify-content: space-between;
71
- gap: 16px;
72
- padding: 2px 0;
73
- }
74
-
75
- & .oc-tooltip-row-swatch {
76
- display: inline-block;
77
- width: 8px;
78
- height: 8px;
79
- border-radius: 50%;
80
- margin-right: 6px;
81
- flex-shrink: 0;
82
- transform: translateY(-1px);
83
- }
84
-
85
- & .oc-tooltip-label {
86
- color: var(--oc-text-secondary);
87
- font-size: 12px;
88
- font-weight: 400;
89
- white-space: nowrap;
90
- flex-shrink: 0;
91
- display: inline-flex;
92
- align-items: center;
93
- }
94
-
95
- & .oc-tooltip-value {
96
- font-weight: 510;
97
- font-size: 12px;
98
- font-variant-numeric: tabular-nums;
99
- color: var(--oc-tooltip-text);
100
- text-align: right;
101
- overflow: hidden;
102
- text-overflow: ellipsis;
103
- white-space: nowrap;
104
- }
105
- }
106
-
107
- /* ---------------------------------------------------------------------------
108
- * Crosshair guideline + per-series snap dots (line/area charts)
109
- * Animate the snap between adjacent points so the indicator slides instead
110
- * of jumping. ~50ms ease-in-out keeps the motion crisp without lagging.
111
- * --------------------------------------------------------------------------- */
112
-
113
- .oc-crosshair {
114
- pointer-events: none;
115
- transition:
116
- x1 50ms ease-in-out,
117
- x2 50ms ease-in-out;
118
- }
119
-
120
- .oc-snap-dots circle {
121
- pointer-events: none;
122
- transition:
123
- cx 50ms ease-in-out,
124
- cy 50ms ease-in-out;
125
- }
126
- }
@@ -1,126 +0,0 @@
1
- @layer oc.components {
2
- /* ---------------------------------------------------------------------------
3
- * You draw it (youDrawIt)
4
- *
5
- * NYT-style draw-then-reveal. SVG-space elements (hatched region, guess path,
6
- * reveal clip) render inside the chart SVG; the prompt + skip-to-reveal button
7
- * are an absolutely-positioned HTML overlay the vanilla adapter sizes over the
8
- * drawing region (inline left/top/width/height). All colors come from --oc-*
9
- * tokens so they flip in .oc-dark.
10
- * --------------------------------------------------------------------------- */
11
-
12
- /* Hatched "draw here" region. Fill comes from an SVG <pattern> (line color =
13
- * the target series color); this only governs overall opacity + fade-out. */
14
- .oc-ydi-region {
15
- opacity: 0.35;
16
- transition: opacity 0.4s ease;
17
- }
18
-
19
- .oc-ydi-region[data-ydi-hidden="true"] {
20
- opacity: 0;
21
- }
22
-
23
- /* The vertical rule at `from` where drawing begins. */
24
- .oc-ydi-boundary {
25
- stroke: var(--oc-text-muted);
26
- stroke-width: 1;
27
- stroke-dasharray: 3 3;
28
- stroke-opacity: 0.6;
29
- }
30
-
31
- /* Reader's guess: a distinct pen-like stroke that reads as "yours" against the
32
- * solid real line. Dashed + focus-blue so it stays legible in both modes. */
33
- .oc-ydi-guess {
34
- stroke: var(--oc-focus);
35
- stroke-width: 2.5;
36
- stroke-linecap: round;
37
- stroke-linejoin: round;
38
- stroke-dasharray: 1 6;
39
- }
40
-
41
- /* Host-supplied comparison line ("what everyone else guessed"). Muted so it
42
- * sits behind the reader's guess and the real line. */
43
- .oc-ydi-comparison {
44
- stroke: var(--oc-text-muted);
45
- stroke-width: 1.5;
46
- stroke-opacity: 0.5;
47
- stroke-dasharray: 4 4;
48
- }
49
-
50
- /* Reveal clip rect: the width animates from the `from` boundary to the full
51
- * area on reveal, wiping the real line in left-to-right. The vanilla adapter
52
- * adds .oc-ydi-clip-animate right before setting the final width; reduced
53
- * motion skips the class so the width snaps. */
54
- .oc-ydi-clip-rect.oc-ydi-clip-animate {
55
- transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);
56
- }
57
-
58
- /* HTML controls overlay: prompt (top) + reveal button (bottom-right). */
59
- .oc-ydi-controls {
60
- display: flex;
61
- flex-direction: column;
62
- justify-content: space-between;
63
- align-items: flex-end;
64
- pointer-events: none;
65
- font-family: var(--oc-font-family);
66
- padding: 8px;
67
- box-sizing: border-box;
68
- z-index: 10;
69
- }
70
-
71
- .oc-ydi-prompt {
72
- align-self: center;
73
- max-width: 100%;
74
- padding: 6px 12px;
75
- border-radius: 6px;
76
- background: var(--oc-card);
77
- border: 1px solid var(--oc-border);
78
- color: var(--oc-text-secondary);
79
- font-size: 13px;
80
- font-weight: 500;
81
- line-height: 16px;
82
- text-align: center;
83
- box-shadow: var(--oc-tooltip-shadow);
84
- }
85
-
86
- .oc-ydi-controls.oc-ydi-revealed .oc-ydi-prompt {
87
- display: none;
88
- }
89
-
90
- /* Skip-to-reveal button. pointer-events re-enabled (the overlay disables it)
91
- * so it stays clickable and keyboard-reachable. Min height enforced inline by
92
- * the adapter (>= 24px effective touch target). */
93
- .oc-ydi-reveal-button {
94
- pointer-events: auto;
95
- display: inline-flex;
96
- align-items: center;
97
- justify-content: center;
98
- padding: 6px 14px;
99
- border: 1px solid var(--oc-border);
100
- border-radius: 6px;
101
- background: var(--oc-bg);
102
- color: var(--oc-text);
103
- font-family: inherit;
104
- font-size: 13px;
105
- font-weight: 500;
106
- cursor: pointer;
107
- box-shadow: var(--oc-tooltip-shadow);
108
- transition:
109
- background-color 0.15s,
110
- border-color 0.15s;
111
- }
112
-
113
- .oc-ydi-reveal-button:hover {
114
- background: var(--oc-hover-bg);
115
- }
116
-
117
- .oc-ydi-reveal-button:focus-visible {
118
- outline: 2px solid var(--oc-focus);
119
- outline-offset: 2px;
120
- }
121
-
122
- .oc-ydi-reveal-button:disabled {
123
- opacity: 0.5;
124
- cursor: default;
125
- }
126
- }
@@ -1,232 +0,0 @@
1
- import { hsl } from 'd3-color';
2
- import { describe, expect, it } from 'vitest';
3
- import { contrastRatio } from '../../colors/contrast';
4
- import { adaptColorForDarkMode, adaptForLightLineStroke, adaptTheme } from '../dark-mode';
5
- import { resolveTheme } from '../resolve';
6
-
7
- describe('adaptColorForDarkMode', () => {
8
- const lightBg = '#ffffff';
9
- const darkBg = '#09090b';
10
-
11
- it('adapted color has similar contrast on dark bg as original on light bg', () => {
12
- const original = '#06b6d4'; // cyan, primary accent
13
- const adapted = adaptColorForDarkMode(original, lightBg, darkBg);
14
-
15
- const originalRatio = contrastRatio(original, lightBg);
16
- const adaptedRatio = contrastRatio(adapted, darkBg);
17
-
18
- // Should be within 30% of the original ratio
19
- const tolerance = originalRatio * 0.3;
20
- expect(Math.abs(adaptedRatio - originalRatio)).toBeLessThan(tolerance);
21
- });
22
-
23
- it('returns unchanged input for unparseable colors (e.g. raw oklch)', () => {
24
- const result = adaptColorForDarkMode('oklch(70% 0.15 200)', lightBg, darkBg);
25
- expect(result).toBe('oklch(70% 0.15 200)');
26
- });
27
-
28
- it('returns a valid hex color', () => {
29
- const result = adaptColorForDarkMode('#e15759', lightBg, darkBg);
30
- expect(result).toMatch(/^#[0-9a-f]{6}$/i);
31
- });
32
-
33
- it('handles pure black gracefully', () => {
34
- const result = adaptColorForDarkMode('#000000', lightBg, darkBg);
35
- expect(result).toMatch(/^#[0-9a-f]{6}$/i);
36
- });
37
- });
38
-
39
- describe('adaptForLightLineStroke', () => {
40
- it('darkens a saturated cyan by ~12% lightness', () => {
41
- const original = '#06b6d4';
42
- const darkened = adaptForLightLineStroke(original);
43
- expect(darkened).not.toBe(original);
44
- const c = hsl(darkened);
45
- const o = hsl(original);
46
- expect(c).not.toBeNull();
47
- expect(c!.l).toBeCloseTo(o.l - 0.12, 2);
48
- });
49
-
50
- it('passes saturated red and blue through with reduced lightness', () => {
51
- for (const color of ['#ef4444', '#3b82f6']) {
52
- const out = adaptForLightLineStroke(color);
53
- const before = hsl(color);
54
- const after = hsl(out);
55
- expect(after!.l).toBeLessThan(before.l);
56
- }
57
- });
58
-
59
- it('passes pure gray through unchanged (saturation below threshold)', () => {
60
- // zinc-400 has near-zero saturation; reducing lightness on a gray would
61
- // shift it toward black, which isn't desired for achromatic palettes.
62
- expect(adaptForLightLineStroke('#a1a1aa')).toBe('#a1a1aa');
63
- });
64
-
65
- it('passes already-dark colors through unchanged (l <= 0.4)', () => {
66
- // Indigo-900 sits at l ≈ 0.30 — already meets contrast on white.
67
- const dark = '#312e81';
68
- expect(adaptForLightLineStroke(dark)).toBe(dark);
69
- });
70
-
71
- it('passes invalid input through unchanged', () => {
72
- expect(adaptForLightLineStroke('not-a-color')).toBe('not-a-color');
73
- expect(adaptForLightLineStroke('')).toBe('');
74
- });
75
- });
76
-
77
- describe('adaptTheme', () => {
78
- it('sets isDark to true', () => {
79
- const light = resolveTheme();
80
- const dark = adaptTheme(light);
81
- expect(dark.isDark).toBe(true);
82
- });
83
-
84
- it('preserves transparent background in dark mode', () => {
85
- const light = resolveTheme();
86
- // Default background is transparent; adaptTheme preserves it so the
87
- // host page's surface shows through in both light and dark contexts.
88
- const dark = adaptTheme(light);
89
- expect(dark.colors.background).toBe('transparent');
90
- });
91
-
92
- it('swaps to dark background when starting from an explicit light color', () => {
93
- const light = resolveTheme({ colors: { background: '#ffffff' } });
94
- const dark = adaptTheme(light);
95
- expect(dark.colors.background).toBe('#09090b');
96
- });
97
-
98
- it('updates text color for dark mode', () => {
99
- // Use an explicit light background so the contrast ratio check has a
100
- // concrete background to measure against.
101
- const light = resolveTheme({ colors: { background: '#ffffff' } });
102
- const dark = adaptTheme(light);
103
- expect(dark.colors.text).not.toBe(light.colors.text);
104
- // Dark text should be light — measure against the dark canvas color.
105
- const ratio = contrastRatio(dark.colors.text, '#09090b');
106
- expect(ratio).toBeGreaterThan(4);
107
- });
108
-
109
- it('preserves categorical palette across modes', () => {
110
- const light = resolveTheme();
111
- const dark = adaptTheme(light);
112
- // Design-system tokens are mode-agnostic: the same vibrant cyan-led
113
- // palette renders in both modes. Contrast-equivalence adaptation
114
- // dulls cyan into teal, which is not what the spec calls for.
115
- expect(dark.colors.categorical).toEqual(light.colors.categorical);
116
- });
117
-
118
- it('updates chrome text colors', () => {
119
- const light = resolveTheme();
120
- const dark = adaptTheme(light);
121
- expect(dark.chrome.title.color).not.toBe(light.chrome.title.color);
122
- });
123
-
124
- it('preserves an explicit axis color override in dark mode', () => {
125
- const light = resolveTheme({ colors: { background: '#ffffff', axis: '#d0d6e0' } });
126
- const dark = adaptTheme(light);
127
- expect(dark.colors.axis).toBe('#d0d6e0');
128
- });
129
-
130
- it('applies the dark axis default when axis is left unset', () => {
131
- const light = resolveTheme({ colors: { background: '#ffffff' } });
132
- const dark = adaptTheme(light);
133
- expect(dark.colors.axis).toBe('#a1a1aa');
134
- });
135
-
136
- it('preserves explicit chrome color overrides in dark mode', () => {
137
- const light = resolveTheme({
138
- colors: { background: '#ffffff' },
139
- chrome: { subtitle: '#d0d6e0', source: '#d0d6e0' },
140
- });
141
- const dark = adaptTheme(light);
142
- expect(dark.chrome.subtitle.color).toBe('#d0d6e0');
143
- expect(dark.chrome.source.color).toBe('#d0d6e0');
144
- });
145
-
146
- it('applies dark chrome defaults when chrome colors are left unset', () => {
147
- const light = resolveTheme({ colors: { background: '#ffffff' } });
148
- const dark = adaptTheme(light);
149
- // Unset chrome colors fall to the muted dark default, not the light one.
150
- expect(dark.chrome.subtitle.color).not.toBe('#71717a');
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
- });
232
- });