@opendata-ai/openchart-core 8.0.0-rc.8 → 8.0.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 +461 -56
- package/dist/index.js +62 -24
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -2
- package/schema/chart.schema.json +373 -67
- package/schema/table.schema.json +35 -1
- package/schema/vizspec.schema.json +950 -152
- package/src/accessibility/__tests__/alt-text.test.ts +0 -110
- package/src/accessibility/__tests__/aria.test.ts +0 -125
- package/src/accessibility/alt-text.ts +0 -155
- package/src/accessibility/aria.ts +0 -75
- package/src/accessibility/index.ts +0 -6
- package/src/colors/__tests__/colorblind.test.ts +0 -75
- package/src/colors/__tests__/contrast.test.ts +0 -119
- package/src/colors/__tests__/palettes.test.ts +0 -74
- package/src/colors/colorblind.ts +0 -131
- package/src/colors/contrast.ts +0 -143
- package/src/colors/index.ts +0 -36
- package/src/colors/palettes.ts +0 -206
- package/src/helpers/__tests__/deep-merge.test.ts +0 -54
- package/src/helpers/__tests__/spec-builders.test.ts +0 -338
- package/src/helpers/deep-merge.ts +0 -29
- package/src/helpers/spec-builders.ts +0 -479
- package/src/index.ts +0 -215
- package/src/labels/__tests__/collision.test.ts +0 -310
- package/src/labels/collision.ts +0 -249
- package/src/labels/index.ts +0 -13
- package/src/layout/__tests__/chrome.test.ts +0 -208
- package/src/layout/__tests__/text-measure.test.ts +0 -57
- package/src/layout/chrome.ts +0 -470
- package/src/layout/index.ts +0 -18
- package/src/layout/text-measure.ts +0 -404
- package/src/layout/text-wrap.ts +0 -93
- package/src/locale/__tests__/format.test.ts +0 -606
- package/src/locale/format.ts +0 -459
- package/src/locale/index.ts +0 -21
- package/src/responsive/__tests__/breakpoints.test.ts +0 -146
- package/src/responsive/__tests__/metrics.test.ts +0 -52
- package/src/responsive/breakpoints.ts +0 -172
- package/src/responsive/index.ts +0 -53
- package/src/responsive/metrics.ts +0 -299
- package/src/schema/__tests__/generated-artifacts.test.ts +0 -42
- package/src/schema/__tests__/schema.test.ts +0 -179
- package/src/styles/__tests__/reduced-motion-selectors.test.ts +0 -28
- package/src/styles/animation.css +0 -208
- package/src/styles/base.css +0 -102
- package/src/styles/chrome.css +0 -174
- package/src/styles/dark.css +0 -44
- package/src/styles/graph.css +0 -135
- package/src/styles/index.css +0 -28
- package/src/styles/keyframes.css +0 -151
- package/src/styles/legend.css +0 -18
- package/src/styles/reduced-motion.css +0 -67
- package/src/styles/series-search.css +0 -153
- package/src/styles/sparkline.css +0 -16
- package/src/styles/table-animation.css +0 -93
- package/src/styles/table.css +0 -398
- package/src/styles/token-definitions.ts +0 -199
- package/src/styles/tokens.css +0 -156
- package/src/styles/tooltip.css +0 -126
- package/src/styles/you-draw-it.css +0 -126
- package/src/theme/__tests__/dark-mode.test.ts +0 -232
- package/src/theme/__tests__/defaults.test.ts +0 -73
- package/src/theme/__tests__/resolve.test.ts +0 -277
- package/src/theme/dark-mode.ts +0 -258
- package/src/theme/defaults.ts +0 -110
- package/src/theme/index.ts +0 -8
- package/src/theme/presets.ts +0 -64
- package/src/theme/resolve.ts +0 -312
- package/src/types/__tests__/encoding.test.ts +0 -368
- package/src/types/__tests__/gradient.test.ts +0 -95
- package/src/types/__tests__/spec.test.ts +0 -499
- package/src/types/encoding.ts +0 -280
- package/src/types/events.ts +0 -154
- package/src/types/index.ts +0 -289
- package/src/types/layout.ts +0 -2001
- package/src/types/schema-roots.ts +0 -32
- package/src/types/spec.ts +0 -2906
- package/src/types/table.ts +0 -108
- package/src/types/theme.ts +0 -209
package/src/styles/tokens.css
DELETED
|
@@ -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
|
-
}
|
package/src/styles/tooltip.css
DELETED
|
@@ -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
|
-
});
|