@eocrm/design-system 0.6.22 → 0.6.24

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 (34) hide show
  1. package/AI-PRIMER.md +167 -288
  2. package/README.md +11 -1
  3. package/docs/accessible-names.md +22 -0
  4. package/docs/components/Chart.md +1 -0
  5. package/docs/components/CircularProgress.md +4 -0
  6. package/docs/components/CurrencySelect.md +1 -1
  7. package/docs/components/FunnelChart.md +96 -0
  8. package/docs/components/Kbd.md +1 -1
  9. package/docs/components/MoneyInput.md +1 -1
  10. package/docs/components/PhoneInput.md +1 -1
  11. package/docs/components/Progress.md +7 -0
  12. package/docs/components/Text.md +8 -0
  13. package/docs/components/Title.md +4 -0
  14. package/docs/components/index.json +657 -0
  15. package/docs/i18n.md +74 -0
  16. package/docs/typescript.md +9 -0
  17. package/package.json +2 -2
  18. package/src/_meta/manifest.ts +1 -0
  19. package/src/components/Chart/Chart.tsx +1 -22
  20. package/src/components/Chart/ChartTooltip.tsx +5 -3
  21. package/src/components/FunnelChart/FunnelChart.module.scss +265 -0
  22. package/src/components/FunnelChart/FunnelChart.tokens.scss +72 -0
  23. package/src/components/FunnelChart/FunnelChart.tsx +607 -0
  24. package/src/components/FunnelChart/funnelModel.ts +124 -0
  25. package/src/components/FunnelChart/index.ts +2 -0
  26. package/src/components/LiquidEditor/useLiquidAutocomplete.ts +1 -1
  27. package/src/components/RichText/RichTextAttachment.tsx +1 -1
  28. package/src/components/_internal/useSize.ts +23 -0
  29. package/src/components.manifest.json +18 -2
  30. package/src/i18n/I18nProvider.tsx +1 -1
  31. package/src/i18n/en.ts +10 -0
  32. package/src/i18n/messages.ts +18 -0
  33. package/src/i18n/ru.ts +11 -0
  34. package/src/index.ts +7 -0
package/docs/i18n.md ADDED
@@ -0,0 +1,74 @@
1
+ # Localization (i18n)
2
+
3
+ How library strings are translated, and how to pair `I18nProvider` with `LocaleProvider`.
4
+
5
+ Every user-facing string the library renders — visible text, aria-labels, placeholders, default empty/loading copy — flows through a single React Context. Wrap your app once; every component picks up the right copy. There are NO `labels` / `cancelLabel` / `searchPlaceholder` props on any component.
6
+
7
+ ```tsx
8
+ import { I18nProvider } from '@eocrm/design-system';
9
+
10
+ <I18nProvider locale="ru">
11
+ <App />
12
+ </I18nProvider>;
13
+ ```
14
+
15
+ **Overrides** are a deep-partial of the messages tree:
16
+
17
+ ```tsx
18
+ <I18nProvider
19
+ locale="ru"
20
+ overrides={{
21
+ pagination: { next: 'Дальше' },
22
+ badge: { modified: 'Изменено!' },
23
+ }}
24
+ >
25
+ <App />
26
+ </I18nProvider>
27
+ ```
28
+
29
+ - Missing override keys fall back to the locale defaults.
30
+ - Missing locale keys do NOT fall back to `en` — `I18nProvider` uses the one locale catalogue and `useTranslation` warns and returns the literal key string. The real guard is the type: `en` and `ru` are both annotated `: Messages` (total, not partial), so a missing key is a `tsc` error and cannot ship.
31
+ - No provider at all = `en` defaults.
32
+
33
+ **Available locales:** `'en'` (default), `'ru'`. v1.
34
+
35
+ **`I18nProvider` vs `LocaleProvider` — pair them.** `I18nProvider` carries the message catalog (translated strings). `LocaleProvider` carries the BCP-47 tag used by `Intl.*` formatters inside Calendar / DatePicker / DateRangePicker (month names, weekday order, date-range formatting). They are independent — a Russian app should wrap with BOTH so visible strings AND Intl-rendered dates speak the right language:
36
+
37
+ ```tsx
38
+ <I18nProvider locale="ru">
39
+ <LocaleProvider locale="ru-RU">
40
+ <App />
41
+ </LocaleProvider>
42
+ </I18nProvider>
43
+ ```
44
+
45
+ **Adding a new string in a library component**:
46
+
47
+ 1. Add the key to `src/i18n/messages.ts` (`Messages` interface).
48
+ 2. Add the English value to `src/i18n/en.ts`.
49
+ 3. Add the Russian value to `src/i18n/ru.ts` (use `ruPlural()` from `i18n/format.ts` for count-varying strings).
50
+ 4. In the component, `const t = useTranslation();` then `aria-label={t('component.key')}`. For array messages (months / weekdays), use `useTranslationArray()`.
51
+ 5. Never inline an English string — `aria-label="Close"` is a Hard rule 9 violation.
52
+
53
+ **Drag-and-drop announcements** are localized too. Every drag surface (`Sortable`,
54
+ `SortableGroup`, `Kanban`, `DataTable` column reorder, `RichTextEditor` block
55
+ gutter) overrides dnd-kit's English defaults with the `drag.*` messages, and
56
+ names what it drags by its **rendered text** rather than its id — "Amount due,
57
+ position 3 of 7", not "col_amount was moved over droppable area col_name". Text
58
+ is read from the DOM, so a closed `DropdownMenu` / `Tooltip` / `Popover` inside
59
+ a card contributes nothing, and `aria-hidden` decoration is skipped.
60
+
61
+ Two things are worth doing yourself:
62
+
63
+ ```tsx
64
+ {/* Name the CONTAINER — the library can't invent one. Either attribute works. */}
65
+ <Kanban.Column id="qualified" aria-label="Qualified"> {/* → "…in Qualified" */}
66
+ <Kanban.Column id="qualified" aria-labelledby="qualified-h"> {/* same, from the heading */}
67
+ <SortableGroup.Container id="review" items={ids} aria-label="In review" />
68
+
69
+ {/* Override a chatty ITEM. Without this the whole card is read out. */}
70
+ <Kanban.Card id="d-1" aria-label="Acme renewal">…title, owner, due date, badges…</Kanban.Card>
71
+ <Sortable.Item id="f-1" aria-label="Company name">…</Sortable.Item>
72
+ ```
73
+
74
+ An unnamed container falls back to its position ("column 2 of 3").
@@ -0,0 +1,9 @@
1
+ # TypeScript
2
+
3
+ Getting prop types to resolve in your editor.
4
+
5
+ Every prop and variant type is typed. If your editor shows no types:
6
+
7
+ - Confirm `moduleResolution: "bundler"` (or `"node16"`) in your `tsconfig.json`.
8
+ - Confirm your IDE's TS server is running.
9
+ - The package's `types` field points at `./src/index.ts` — source distribution means the consumer's bundler compiles `.tsx`. Vite, Next.js, Webpack 5+ all support this; older configurations may need `transpilePackages: ['@eocrm/design-system']` (Next.js) or equivalent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eocrm/design-system",
3
- "version": "0.6.22",
3
+ "version": "0.6.24",
4
4
  "description": "Design system for the EOCRM product — React 19 + SCSS modules + CSS custom properties.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -50,7 +50,7 @@
50
50
  "@dnd-kit/core": "^6.3.1",
51
51
  "@dnd-kit/sortable": "^10.0.0",
52
52
  "@dnd-kit/utilities": "^3.2.2",
53
- "@eocrm/design-tokens": "0.6.22",
53
+ "@eocrm/design-tokens": "0.6.24",
54
54
  "@floating-ui/react-dom": "^2.1.8",
55
55
  "@types/d3-scale": "^4.0.9",
56
56
  "@types/d3-shape": "^3.2.0",
@@ -127,6 +127,7 @@ const CLUSTERS: Record<string, string> = {
127
127
  DashboardWidget: 'Display',
128
128
  StatGroup: 'Display',
129
129
  Chart: 'Display',
130
+ FunnelChart: 'Display',
130
131
  Sparkline: 'Display',
131
132
  TreeView: 'Display',
132
133
  DataTable: 'Display',
@@ -2,7 +2,6 @@ import {
2
2
  forwardRef,
3
3
  useEffect,
4
4
  useId,
5
- useLayoutEffect,
6
5
  useMemo,
7
6
  useState,
8
7
  type CSSProperties,
@@ -16,6 +15,7 @@ import { EmptyState } from '../EmptyState';
16
15
  import { useTranslation } from '../../i18n/useTranslation';
17
16
  import { useControllableState } from '../_internal/useControllableState';
18
17
  import { mergeRefs } from '../_internal/refs';
18
+ import { useSize } from '../_internal/useSize';
19
19
  import { ChartLegend } from './ChartLegend';
20
20
  import { ChartTable } from './ChartTable';
21
21
  import { ChartTooltip } from './ChartTooltip';
@@ -131,27 +131,6 @@ function useDevWarnings(dropped: string[], overflowCount: number, droppedMarkers
131
131
  }, [overflowCount]);
132
132
  }
133
133
 
134
- function useSize<T extends HTMLElement>() {
135
- const [el, setEl] = useState<T | null>(null);
136
- const [size, setSize] = useState({ width: 0, height: 0 });
137
- useLayoutEffect(() => {
138
- if (!el || typeof ResizeObserver === 'undefined') return;
139
- const measure = () => {
140
- const { width, height } = el.getBoundingClientRect();
141
- setSize((prev) =>
142
- prev.width === Math.floor(width) && prev.height === Math.floor(height)
143
- ? prev
144
- : { width: Math.floor(width), height: Math.floor(height) },
145
- );
146
- };
147
- measure();
148
- const ro = new ResizeObserver(measure);
149
- ro.observe(el);
150
- return () => ro.disconnect();
151
- }, [el]);
152
- return [setEl, size, el] as const;
153
- }
154
-
155
134
  /**
156
135
  * Time-series chart (line, bar, stacked bar, area) that fills a `DashboardWidget variant="chart"` body.
157
136
  * @see docs/components/Chart.md
@@ -9,12 +9,14 @@ export interface ChartTooltipRow {
9
9
  slot: number;
10
10
  comparison: boolean;
11
11
  value: string;
12
+ /** CSS colour for the swatch; overrides `slot` (FunnelChart's ramp colours). */
13
+ color?: string;
12
14
  }
13
15
 
14
- const swatchStyle = (slot: number) =>
16
+ const swatchStyle = (slot: number, color?: string) =>
15
17
  ({
16
18
  '--chart-series-color':
17
- slot === 0 ? 'var(--chart-total-comparison)' : `var(--chart-series-${slot})`,
19
+ color ?? (slot === 0 ? 'var(--chart-total-comparison)' : `var(--chart-series-${slot})`),
18
20
  }) as CSSProperties;
19
21
 
20
22
  /**
@@ -72,7 +74,7 @@ export function ChartTooltip({
72
74
  <div key={r.key} className={styles.tooltipRow} data-nested={r.comparison || undefined}>
73
75
  <span
74
76
  className={styles.swatch}
75
- style={swatchStyle(r.slot)}
77
+ style={swatchStyle(r.slot, r.color)}
76
78
  data-comparison={r.comparison || undefined}
77
79
  />
78
80
  <span className={styles.tooltipLabel}>{r.label}</span>
@@ -0,0 +1,265 @@
1
+ @use './FunnelChart.tokens';
2
+ @use '../../styles/mixins' as *;
3
+
4
+ .root {
5
+ display: grid;
6
+ grid-template-rows: minmax(0, 1fr);
7
+ gap: var(--funnel-chart-gap);
8
+ width: 100%;
9
+ height: 100%;
10
+ min-width: 0;
11
+ min-height: 0;
12
+ }
13
+
14
+ .root[data-empty] {
15
+ place-items: center;
16
+ }
17
+
18
+ // The footnote takes its own row; the plot (or EmptyState) fills the rest.
19
+ .root[data-footnote] {
20
+ grid-template-rows: minmax(0, 1fr) auto;
21
+ }
22
+
23
+ .footnote {
24
+ color: var(--chart-footnote-fg);
25
+ font-size: var(--chart-footnote-size);
26
+ line-height: var(--chart-footnote-line-height);
27
+ }
28
+
29
+ .plot {
30
+ display: grid;
31
+ grid-template-rows: minmax(var(--funnel-chart-min-height), 1fr) auto;
32
+ gap: var(--funnel-chart-gap);
33
+ min-height: 0;
34
+ border-radius: var(--chart-plot-radius);
35
+ outline: none;
36
+ touch-action: pan-y;
37
+
38
+ &:focus-visible {
39
+ @include focus-ring(var(--funnel-chart-ring));
40
+ }
41
+ }
42
+
43
+ .shapes {
44
+ min-height: 0;
45
+ overflow: hidden;
46
+
47
+ // The SVG's explicit height must not feed back into the row size, or the strip only ever grows
48
+ // in an auto-height parent. Size containment makes the row fall back to min-height / parent height.
49
+ contain: size;
50
+ }
51
+
52
+ .plot[data-clickable] .shapes {
53
+ cursor: pointer;
54
+ }
55
+
56
+ .svg {
57
+ display: block;
58
+ }
59
+
60
+ .stage {
61
+ transition: opacity var(--funnel-chart-transition);
62
+
63
+ &[data-dimmed] {
64
+ opacity: var(--funnel-chart-dim-opacity);
65
+ }
66
+ }
67
+
68
+ .shape {
69
+ fill: var(--chart-series-color, currentColor);
70
+
71
+ // ponytail: CSS `d` transitions don't animate in Safari (it snaps). Use a JS tween only if Safari users notice.
72
+ transition: d var(--funnel-chart-transition);
73
+ }
74
+
75
+ .pill {
76
+ fill: var(--funnel-chart-pill-bg);
77
+ }
78
+
79
+ .heroValue,
80
+ .heroLabel {
81
+ fill: var(--funnel-chart-hero-fg);
82
+ }
83
+
84
+ .heroValue {
85
+ font-size: var(--funnel-chart-hero-value-size);
86
+ font-weight: var(--font-weight-semibold);
87
+ font-variant-numeric: tabular-nums;
88
+ }
89
+
90
+ .heroLabel {
91
+ font-size: var(--funnel-chart-hero-label-size);
92
+ font-weight: var(--funnel-chart-hero-label-weight);
93
+ }
94
+
95
+ .pillText {
96
+ fill: var(--funnel-chart-pill-fg);
97
+ font-size: var(--funnel-chart-pill-size);
98
+ font-weight: var(--font-weight-semibold);
99
+ font-variant-numeric: tabular-nums;
100
+ }
101
+
102
+ // One column per stage, aligned with the shapes above; dashed rules between columns.
103
+ .footer {
104
+ display: grid;
105
+ grid-auto-columns: minmax(0, 1fr);
106
+ grid-auto-flow: column;
107
+ }
108
+
109
+ .footer > .cell + .cell {
110
+ border-inline-start: var(--border-width) dashed var(--funnel-chart-separator);
111
+ }
112
+
113
+ .cell {
114
+ display: grid;
115
+ align-content: start;
116
+ justify-items: center;
117
+ padding-inline: var(--space-1);
118
+ text-align: center;
119
+ overflow-wrap: anywhere;
120
+ transition: opacity var(--funnel-chart-transition);
121
+
122
+ &[data-dimmed] {
123
+ opacity: var(--funnel-chart-dim-opacity);
124
+ }
125
+
126
+ // A focused stage hovered away from keeps its focus ring readable (WCAG 2.4.7).
127
+ &[data-dimmed]:focus-visible {
128
+ opacity: initial;
129
+ }
130
+
131
+ // Hero shown: the first cell keeps its value and label, invisible, so it is a normal footer-height
132
+ // box (and a full-size focus ring when it is a button) under the band that shows them.
133
+ &[data-hero-cell] > * {
134
+ visibility: hidden;
135
+ }
136
+
137
+ // Clickable stages render the cell as a <button> (reset.scss strips its chrome).
138
+ &:is(button) {
139
+ border-radius: var(--chart-plot-radius);
140
+ cursor: pointer;
141
+ outline: none;
142
+ }
143
+
144
+ &:is(button):focus-visible {
145
+ @include focus-ring(var(--funnel-chart-ring));
146
+ }
147
+ }
148
+
149
+ .value {
150
+ // Only labels break anywhere; numbers stay whole.
151
+ overflow-wrap: normal;
152
+ font-size: var(--funnel-chart-value-size);
153
+ font-weight: var(--funnel-chart-value-weight);
154
+ font-variant-numeric: tabular-nums;
155
+ }
156
+
157
+ .label {
158
+ color: var(--funnel-chart-label-fg);
159
+ font-size: var(--funnel-chart-label-size);
160
+ }
161
+
162
+ // Vertical layout: a slice of the funnel per row (same `shape`, turned on its side, thin gaps
163
+ // between slices) beside a table-like grid; the rows scroll as a last resort.
164
+ .root[data-orientation='vertical'] .plot {
165
+ grid-template-rows: none;
166
+ grid-auto-rows: min-content;
167
+
168
+ // One column template shared by the header and every row (subgrid): band track (flexible share, so
169
+ // band widths stay proportional), swatch, label (wraps when tight), value, step pill.
170
+ grid-template-columns:
171
+ minmax(var(--funnel-chart-row-track-min), 1fr) auto minmax(0, max-content)
172
+ max-content max-content;
173
+ align-content: start;
174
+ gap: var(--funnel-chart-slice-gap) var(--funnel-chart-gap);
175
+ overflow-y: auto;
176
+
177
+ // Room for the stage buttons' focus rings, which the scroll container would clip.
178
+ padding: calc(var(--ring-width) + var(--ring-offset));
179
+ }
180
+
181
+ // Header and rows span the plot's columns and inherit them (subgrid).
182
+ .header,
183
+ .root[data-orientation='vertical'] .cell {
184
+ // stylelint-disable-next-line property-disallowed-list -- internal row spanning the plot's own columns (subgrid), not consumer layout
185
+ grid-column: 1 / -1;
186
+ display: grid;
187
+ grid-template-columns: subgrid;
188
+ }
189
+
190
+ .header {
191
+ padding-block-end: var(--space-1);
192
+ color: var(--funnel-chart-header-fg);
193
+ font-size: var(--funnel-chart-header-size);
194
+ }
195
+
196
+ .headerValue {
197
+ text-align: end;
198
+ }
199
+
200
+ // Items stretch to the row (the band needs the full height); text items centre themselves.
201
+ .root[data-orientation='vertical'] .cell {
202
+ align-content: stretch;
203
+ align-items: stretch;
204
+ justify-items: stretch;
205
+ min-block-size: var(--funnel-chart-row-min-height);
206
+ padding-inline: 0;
207
+ text-align: start;
208
+
209
+ .label,
210
+ .value,
211
+ .swatch,
212
+ .step {
213
+ display: grid;
214
+ align-content: center;
215
+ }
216
+
217
+ .value {
218
+ font-size: var(--funnel-chart-label-size);
219
+ text-align: end;
220
+ }
221
+ }
222
+
223
+ // The slice stretches to the row; size containment keeps the SVG's aspect ratio out of the row
224
+ // height, which comes from the text (or the min height).
225
+ .bandTrack {
226
+ display: block;
227
+ contain: size;
228
+
229
+ .svg {
230
+ inline-size: 100%;
231
+ block-size: 100%;
232
+ }
233
+ }
234
+
235
+ .swatch::before {
236
+ content: '';
237
+ inline-size: var(--chart-swatch);
238
+ block-size: var(--chart-swatch);
239
+ border-radius: var(--radius-sm);
240
+ background: var(--chart-series-color, currentColor);
241
+ }
242
+
243
+ .step {
244
+ justify-items: start;
245
+ color: var(--funnel-chart-label-fg);
246
+ font-size: var(--funnel-chart-pill-size);
247
+ }
248
+
249
+ .rowPill {
250
+ padding-inline: var(--space-2);
251
+ border-radius: var(--radius-full);
252
+ background: var(--funnel-chart-row-pill-bg);
253
+ color: var(--funnel-chart-pill-fg);
254
+ font-size: var(--funnel-chart-pill-size);
255
+ font-weight: var(--font-weight-semibold);
256
+ font-variant-numeric: tabular-nums;
257
+ }
258
+
259
+ @media (prefers-reduced-motion: reduce) {
260
+ .stage,
261
+ .shape,
262
+ .cell {
263
+ transition: none;
264
+ }
265
+ }
@@ -0,0 +1,72 @@
1
+ // FunnelChart.tokens.scss — component tokens for <FunnelChart>. Stages are a one-hue ramp from
2
+ // --chart-series-1 toward the surface (a stage's `color` slot uses --chart-series-N instead);
3
+ // everything else defaults to primitives or chart tokens.
4
+ @use '../Chart/Chart.tokens';
5
+
6
+ :root {
7
+ --funnel-chart-gap: var(--space-2);
8
+
9
+ // Ramp: stage i of n is ramp-color mixed (oklab) toward ramp-target by max-mix * i / (n - 1). Light:
10
+ // target is the surface, 48% keeps the lightest step >= 2:1 against it. Dark (below): target is
11
+ // --color-fg so later stages get paler, not muddier; 65% passes validate_palette.js --ordinal
12
+ // --mode dark for 3-4 stages (6 stages: ΔL ~0.04), palest 4-stage step #a6c5eb is still clearly blue.
13
+ // ponytail: from 5 stages on, adjacent steps fall under the 0.06 OKLCH ΔL ideal (~0.03 at 8); labels,
14
+ // the vertical table and the tooltip carry identity. A wider ramp needs a darker first stage than series-1.
15
+ --funnel-chart-ramp-color: var(--chart-series-1);
16
+ --funnel-chart-surface: var(--color-bg);
17
+ --funnel-chart-ramp-target: var(--funnel-chart-surface);
18
+ --funnel-chart-ramp-max-mix: 48%;
19
+
20
+ // Hero: the first stage's value and label inside its band (horizontal). Paired with the band
21
+ // (--funnel-chart-ramp-color, i.e. --chart-series-1), not the accent: the surface colour knocked out
22
+ // of the band, the same pair the chart series are gated on (3:1). It misses 4.5:1 (only near-black,
23
+ // no neutral token, clears that in light), so both lines are WCAG large text (24px; 20px bold), 3:1:
24
+ // @contrast --color-bg on --color-chart-series-1 = 4.42:1 light
25
+ // @contrast --color-bg on --color-chart-series-1 = 4.45:1 dark
26
+ // Override --funnel-chart-ramp-color and this pair is yours to recheck; a stage-0 `color` slot hides the hero.
27
+ --funnel-chart-hero-fg: var(--color-bg);
28
+ --funnel-chart-hero-value-size: var(--font-size-2xl);
29
+ --funnel-chart-hero-label-size: var(--font-size-xl);
30
+ --funnel-chart-hero-label-weight: var(--font-weight-bold);
31
+
32
+ // Dashed rule between horizontal footer columns, from stage 2 on.
33
+ --funnel-chart-separator: var(--color-border);
34
+
35
+ // Intrinsic height of the shapes strip when the parent gives no height.
36
+ --funnel-chart-min-height: calc(var(--space-16) * 2);
37
+ --funnel-chart-pill-bg: var(--color-bg);
38
+ --funnel-chart-pill-fg: var(--color-fg);
39
+ --funnel-chart-pill-size: var(--font-size-xs);
40
+ --funnel-chart-value-size: var(--font-size-lg);
41
+ --funnel-chart-value-weight: var(--font-weight-semibold);
42
+ --funnel-chart-label-fg: var(--color-fg-muted);
43
+ --funnel-chart-label-size: var(--font-size-sm);
44
+ --funnel-chart-dim-opacity: var(--chart-dim-opacity);
45
+ --funnel-chart-ring: var(--chart-ring);
46
+ --funnel-chart-transition: var(--transition-slow);
47
+
48
+ // Vertical layout (narrow containers): rows of a funnel slice beside a table-like grid (swatch,
49
+ // stage, value, step pill). The min height keeps a short row's taper readable; the slice gap
50
+ // separates the slices with a thin line of surface.
51
+ --funnel-chart-row-min-height: var(--space-10);
52
+ --funnel-chart-slice-gap: var(--space-05);
53
+ --funnel-chart-header-fg: var(--color-fg-muted);
54
+ --funnel-chart-header-size: var(--font-size-xs);
55
+
56
+ // Narrowest the band track gets beside the table, so the band never vanishes.
57
+ --funnel-chart-row-track-min: var(--space-16);
58
+ --funnel-chart-row-pill-bg: var(--color-bg-muted);
59
+ }
60
+
61
+ // Dark ramp: same scopes as the generated dark.scss (forced dark, and OS dark unless forced light).
62
+ :root[data-theme='dark'] {
63
+ --funnel-chart-ramp-target: var(--color-fg);
64
+ --funnel-chart-ramp-max-mix: 65%;
65
+ }
66
+
67
+ @media (prefers-color-scheme: dark) {
68
+ :root:not([data-theme='light']) {
69
+ --funnel-chart-ramp-target: var(--color-fg);
70
+ --funnel-chart-ramp-max-mix: 65%;
71
+ }
72
+ }