@cfasim-ui/docs 0.4.15 → 0.5.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.
@@ -124,6 +124,56 @@ e.g. `"percent:1"`), a printf-style format string, or a function
124
124
  </template>
125
125
  </ComponentDemo>
126
126
 
127
+ ### Wrapping long content
128
+
129
+ By default, cell content stays on one line and truncates with an ellipsis
130
+ when it's wider than the column — so a long string in one column can't
131
+ spill into the next. Set `wrap: true` on a column to let it grow
132
+ vertically instead.
133
+
134
+ <ComponentDemo>
135
+ <DataTable
136
+ :data="{
137
+ drug: ['Compound A', 'Compound B', 'Compound C'],
138
+ note: [
139
+ 'Well tolerated at all tested doses; no adverse events reported.',
140
+ 'Mild GI symptoms in 4% of participants; resolved without intervention.',
141
+ 'Discontinued at week 6 after liver-enzyme elevation in two participants.',
142
+ ],
143
+ }"
144
+ :column-config="{
145
+ drug: { width: 'small' },
146
+ note: { wrap: true, width: 'large' },
147
+ }"
148
+ />
149
+
150
+ <template #code>
151
+
152
+ ```vue
153
+ <DataTable
154
+ :data="{
155
+ drug: ['Compound A', 'Compound B', 'Compound C'],
156
+ note: [
157
+ 'Well tolerated at all tested doses; no adverse events reported.',
158
+ 'Mild GI symptoms in 4% of participants; resolved without intervention.',
159
+ 'Discontinued at week 6 after liver-enzyme elevation in two participants.',
160
+ ],
161
+ }"
162
+ :column-config="{
163
+ drug: { width: 'small' },
164
+ note: { wrap: true, width: 'large' },
165
+ }"
166
+ />
167
+ ```
168
+
169
+ </template>
170
+ </ComponentDemo>
171
+
172
+ Use `columnClass` to attach a class to both the header and every body
173
+ cell in a column — handy when whole-column styling (background, border,
174
+ font weight) should match across the header and data. `cellClass` keeps
175
+ its meaning (body cells only).
176
+
127
177
  ### Full width
128
178
 
129
179
  By default the table sizes to its content (columns default to a fixed
@@ -157,9 +207,8 @@ the available space equally.
157
207
 
158
208
  A `⋯` menu appears in the top-right corner of every table with a
159
209
  **Download** item that exports the data as CSV. Use `download-menu-link`
160
- to customize the menu item label, `filename` to control the downloaded
161
- filename, and `csv` to supply custom CSV content. Pass `:menu="false"`
162
- to hide the menu entirely.
210
+ to customize the menu item label and `filename` to control the
211
+ downloaded filename. Pass `:menu="false"` to hide the menu entirely.
163
212
 
164
213
  <ComponentDemo>
165
214
  <DataTable
@@ -184,6 +233,115 @@ to hide the menu entirely.
184
233
  </template>
185
234
  </ComponentDemo>
186
235
 
236
+ ### Custom CSV download
237
+
238
+ By default, the Download menu item exports the displayed table as CSV.
239
+ Use the `csv` prop to supply your own content — for example, to include
240
+ ISO dates, extra columns that aren't in the table, or values formatted
241
+ differently from the on-screen rendering. Accepts a raw string or a
242
+ function returning one (called lazily on click).
243
+
244
+ <ComponentDemo>
245
+ <DataTable
246
+ :data="{ day: [0, 1, 2, 3, 4], cases: [1, 21, 56, 101, 141] }"
247
+ filename="sir-cases"
248
+ :csv="'date,day,cases\n2024-01-01,0,1\n2024-01-02,1,21\n2024-01-03,2,56\n2024-01-04,3,101\n2024-01-05,4,141'"
249
+ />
250
+
251
+ <template #code>
252
+
253
+ ```vue
254
+ <DataTable
255
+ :data="{
256
+ day: [0, 1, 2, 3, 4],
257
+ cases: [1, 21, 56, 101, 141],
258
+ }"
259
+ filename="sir-cases"
260
+ :csv="`date,day,cases
261
+ 2024-01-01,0,1
262
+ 2024-01-02,1,21
263
+ 2024-01-03,2,56
264
+ 2024-01-04,3,101
265
+ 2024-01-05,4,141`"
266
+ />
267
+ ```
268
+
269
+ </template>
270
+ </ComponentDemo>
271
+
272
+ ### Download button
273
+
274
+ Pass `download-button` to render a visible, labeled button beneath the
275
+ table instead of exposing the download only via the top-right menu. The
276
+ button uses `download-menu-link` as its label, and the menu's Download
277
+ item is suppressed so the action isn't duplicated. The button has the
278
+ class `data-table-download-button` and its styles are unscoped, so it
279
+ can be targeted directly from custom CSS without specificity battles.
280
+
281
+ <ComponentDemo>
282
+ <DataTable
283
+ :data="{ day: [0, 1, 2, 3, 4], cases: [1, 21, 56, 101, 141] }"
284
+ filename="sir-cases"
285
+ download-menu-link="Download CSV"
286
+ download-button
287
+ :csv="'date,day,cases\n2024-01-01,0,1\n2024-01-02,1,21\n2024-01-03,2,56\n2024-01-04,3,101\n2024-01-05,4,141'"
288
+ />
289
+
290
+ <template #code>
291
+
292
+ ```vue
293
+ <DataTable
294
+ :data="{
295
+ day: [0, 1, 2, 3, 4],
296
+ cases: [1, 21, 56, 101, 141],
297
+ }"
298
+ filename="sir-cases"
299
+ download-menu-link="Download CSV"
300
+ download-button
301
+ :csv="`date,day,cases
302
+ 2024-01-01,0,1
303
+ 2024-01-02,1,21
304
+ 2024-01-03,2,56
305
+ 2024-01-04,3,101
306
+ 2024-01-05,4,141`"
307
+ />
308
+ ```
309
+
310
+ </template>
311
+ </ComponentDemo>
312
+
313
+ ### Download link
314
+
315
+ Pass `download-link` to render a plain text link beneath the table
316
+ instead of (or alongside) the menu. It's a real `<a href download>` —
317
+ right-click → Save As works. Pass `true` for the default "Download data
318
+ (CSV)" label, or a string to customize. The menu's Download item is
319
+ suppressed when this is set. If both `download-link` and
320
+ `download-button` are set, the button wins.
321
+
322
+ <ComponentDemo>
323
+ <DataTable
324
+ :data="{ day: [0, 1, 2, 3, 4], cases: [1, 21, 56, 101, 141] }"
325
+ filename="sir-cases"
326
+ download-link
327
+ />
328
+
329
+ <template #code>
330
+
331
+ ```vue
332
+ <DataTable
333
+ :data="{
334
+ day: [0, 1, 2, 3, 4],
335
+ cases: [1, 21, 56, 101, 141],
336
+ }"
337
+ filename="sir-cases"
338
+ download-link
339
+ />
340
+ ```
341
+
342
+ </template>
343
+ </ComponentDemo>
344
+
187
345
  ## Props
188
346
 
189
347
  | Prop | Type | Required | Default |
@@ -195,6 +353,8 @@ to hide the menu entirely.
195
353
  | `csv` | `string \| (() =&gt; string)` | No | — |
196
354
  | `filename` | `string` | No | — |
197
355
  | `downloadMenuLink` | `string` | No | `"Download"` |
356
+ | `downloadButton` | `boolean` | No | `false` |
357
+ | `downloadLink` | `boolean \| string` | No | — |
198
358
  | `fullWidth` | `boolean` | No | `false` |
199
359
 
200
360
 
@@ -205,7 +365,12 @@ interface ColumnConfig {
205
365
  label?: string;
206
366
  width?: "small" | "medium" | "large" | number;
207
367
  align?: "left" | "center" | "right";
368
+ /** Class applied to body `<td>` cells only. */
208
369
  cellClass?: string;
370
+ /** Class applied to both the header `<th>` and body `<td>` cells. */
371
+ columnClass?: string;
372
+ /** Allow cell contents to wrap. Default `false` (nowrap + ellipsis). */
373
+ wrap?: boolean;
209
374
  format?: NumberFormat | ((value: CellValue, row: number) => string);
210
375
  }
211
376
 
@@ -21,7 +21,22 @@ export interface ColumnConfig {
21
21
  label?: string;
22
22
  width?: ColumnWidth | number;
23
23
  align?: ColumnAlign;
24
+ /** Class applied to every body `<td>` in this column. */
24
25
  cellClass?: string;
26
+ /**
27
+ * Class applied to both the header `<th>` and every body `<td>` in
28
+ * this column. Use for styling that should span the whole column
29
+ * (borders, backgrounds, fonts). For body-only styling, use
30
+ * `cellClass`.
31
+ */
32
+ columnClass?: string;
33
+ /**
34
+ * Allow cell contents to wrap to multiple lines when wider than the
35
+ * column. Default `false` — cells stay on one line and truncate with
36
+ * an ellipsis. Set to `true` for text-heavy columns where the full
37
+ * value should remain visible.
38
+ */
39
+ wrap?: boolean;
25
40
  /**
26
41
  * Custom formatter for cell values in this column. Accepts a
27
42
  * {@link NumberFormat} (preset name, printf-style string, or
@@ -54,14 +69,34 @@ const props = withDefaults(
54
69
  /** Filename (without extension) for downloaded CSV files. */
55
70
  filename?: string;
56
71
  /**
57
- * Label for the Download item in the table's top-right menu.
58
- * Defaults to "Download".
72
+ * Label for the Download item in the table's top-right menu, and for
73
+ * the button rendered when `downloadButton` is true. Defaults to
74
+ * "Download".
59
75
  */
60
76
  downloadMenuLink?: string;
77
+ /**
78
+ * Render a visible "Download" button beneath the table instead of
79
+ * exposing the action only via the top-right menu. When enabled, the
80
+ * menu's Download item is suppressed to avoid duplicate controls.
81
+ */
82
+ downloadButton?: boolean;
83
+ /**
84
+ * Render a plain text link beneath the table for downloading CSV.
85
+ * Pass `true` for the default "Download data (CSV)" label, or a
86
+ * string to customize. When set, the menu's Download item is
87
+ * suppressed. Mutually exclusive with `downloadButton`; if both are
88
+ * set, `downloadButton` wins.
89
+ */
90
+ downloadLink?: boolean | string;
61
91
  /** Stretch the table to fill its container's width. */
62
92
  fullWidth?: boolean;
63
93
  }>(),
64
- { menu: true, fullWidth: false, downloadMenuLink: "Download" },
94
+ {
95
+ menu: true,
96
+ fullWidth: false,
97
+ downloadMenuLink: "Download",
98
+ downloadButton: false,
99
+ },
65
100
  );
66
101
 
67
102
  function columnLabel(name: string): string {
@@ -84,6 +119,20 @@ function columnAlignStyle(name: string): CSSProperties | undefined {
84
119
  return { textAlign: align };
85
120
  }
86
121
 
122
+ /** Classes shared by `<th>` and `<td>` for a column: `columnClass` + wrap flag. */
123
+ function columnSharedClass(name: string): (string | undefined)[] {
124
+ const cfg = props.columnConfig?.[name];
125
+ return [cfg?.columnClass, cfg?.wrap ? "cell-wrap" : undefined];
126
+ }
127
+
128
+ function headerCellClass(name: string): (string | undefined)[] {
129
+ return columnSharedClass(name);
130
+ }
131
+
132
+ function bodyCellClass(name: string): (string | undefined)[] {
133
+ return [...columnSharedClass(name), props.columnConfig?.[name]?.cellClass];
134
+ }
135
+
87
136
  function isModelOutput(d: TableData): d is ModelOutput {
88
137
  return typeof (d as ModelOutput).column === "function";
89
138
  }
@@ -165,14 +214,30 @@ function toCsv(): string {
165
214
  return lines.join("\n");
166
215
  }
167
216
 
168
- const menuItems = computed<ChartMenuItem[]>(() => [
169
- {
170
- label: props.downloadMenuLink,
171
- action: () => downloadCsv(toCsv(), menuFilename()),
172
- },
173
- ]);
217
+ function triggerDownload() {
218
+ downloadCsv(toCsv(), menuFilename());
219
+ }
220
+
221
+ const menuItems = computed<ChartMenuItem[]>(() => {
222
+ if (props.downloadButton || props.downloadLink) return [];
223
+ return [{ label: props.downloadMenuLink, action: triggerDownload }];
224
+ });
174
225
 
175
- const showMenu = computed(() => Boolean(props.menu));
226
+ const downloadLinkText = computed<string | null>(() => {
227
+ if (props.downloadButton) return null;
228
+ const v = props.downloadLink;
229
+ if (!v) return null;
230
+ return typeof v === "string" ? v : "Download data (CSV)";
231
+ });
232
+
233
+ const csvHref = computed<string | null>(() => {
234
+ if (!downloadLinkText.value) return null;
235
+ return `data:text/csv;charset=utf-8,${encodeURIComponent(toCsv())}`;
236
+ });
237
+
238
+ const showMenu = computed(
239
+ () => Boolean(props.menu) && menuItems.value.length > 0,
240
+ );
176
241
  </script>
177
242
 
178
243
  <template>
@@ -195,6 +260,7 @@ const showMenu = computed(() => Boolean(props.menu));
195
260
  <th
196
261
  v-for="col in columns"
197
262
  :key="col.name"
263
+ :class="headerCellClass(col.name)"
198
264
  :style="columnAlignStyle(col.name)"
199
265
  >
200
266
  {{ columnLabel(col.name) }}
@@ -206,7 +272,7 @@ const showMenu = computed(() => Boolean(props.menu));
206
272
  <td
207
273
  v-for="col in columns"
208
274
  :key="col.name"
209
- :class="columnConfig?.[col.name]?.cellClass"
275
+ :class="bodyCellClass(col.name)"
210
276
  :style="columnAlignStyle(col.name)"
211
277
  >
212
278
  {{ cellValue(col, row - 1) }}
@@ -215,6 +281,22 @@ const showMenu = computed(() => Boolean(props.menu));
215
281
  </tbody>
216
282
  </table>
217
283
  </div>
284
+ <button
285
+ v-if="downloadButton"
286
+ type="button"
287
+ class="data-table-download-button"
288
+ @click="triggerDownload"
289
+ >
290
+ {{ downloadMenuLink }}
291
+ </button>
292
+ <a
293
+ v-else-if="downloadLinkText"
294
+ class="data-table-download-link"
295
+ :href="csvHref!"
296
+ :download="`${menuFilename()}.csv`"
297
+ >
298
+ {{ downloadLinkText }}
299
+ </a>
218
300
  </div>
219
301
  </template>
220
302
 
@@ -257,9 +339,18 @@ const showMenu = computed(() => Boolean(props.menu));
257
339
  .Table td {
258
340
  padding: 0.75em 1.25em;
259
341
  white-space: nowrap;
342
+ overflow: hidden;
343
+ text-overflow: ellipsis;
260
344
  text-align: left;
261
345
  }
262
346
 
347
+ .Table th.cell-wrap,
348
+ .Table td.cell-wrap {
349
+ white-space: normal;
350
+ overflow: visible;
351
+ text-overflow: clip;
352
+ }
353
+
263
354
  .Table th {
264
355
  font-weight: 600;
265
356
  border-bottom: 1px solid var(--color-border-header);
@@ -288,3 +379,34 @@ const showMenu = computed(() => Boolean(props.menu));
288
379
  padding-right: 2.5em;
289
380
  }
290
381
  </style>
382
+
383
+ <style>
384
+ .data-table-download-button {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ margin-top: 0.75em;
388
+ padding: 0.5em 1em;
389
+ border: 1px solid var(--color-border);
390
+ border-radius: 0.25em;
391
+ background: var(--color-bg-0, #fff);
392
+ color: var(--color-text);
393
+ font-size: var(--font-size-sm);
394
+ cursor: pointer;
395
+ }
396
+
397
+ .data-table-download-button:hover {
398
+ background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
399
+ }
400
+
401
+ .data-table-download-button:focus-visible {
402
+ outline: 2px solid var(--color-primary);
403
+ outline-offset: 2px;
404
+ }
405
+
406
+ .data-table-download-link {
407
+ display: block;
408
+ text-align: right;
409
+ font-size: var(--font-size-sm);
410
+ margin-top: 0.25em;
411
+ }
412
+ </style>
@@ -941,7 +941,10 @@ dates, categorical labels, or extra columns that aren't plotted). Use
941
941
  `filename` to control the download filename (shared by SVG, PNG and CSV).
942
942
 
943
943
  Pass `download-link` to also render a plain text link below the chart — set
944
- it to `true` for the default label, or pass a string to customize it.
944
+ it to `true` for the default label, or pass a string to customize it. Use
945
+ `download-button` instead to render a styled `<button>` (with the class
946
+ `line-chart-download-button`, available for custom CSS) in place of the
947
+ link.
945
948
 
946
949
  <ComponentDemo>
947
950
  <LineChart
@@ -1004,6 +1007,7 @@ until the user clicks Download:
1004
1007
  | `csv` | `string \| (() =&gt; string)` | No | — |
1005
1008
  | `filename` | `string` | No | — |
1006
1009
  | `downloadLink` | `boolean \| string` | No | — |
1010
+ | `downloadButton` | `boolean \| string` | No | — |
1007
1011
  | `annotations` | `readonly ChartAnnotation[]` | No | — |
1008
1012
  | `chartPadding` | `ChartPadding` | No | — |
1009
1013
  | `y` | `LineChartData` | No | — |
@@ -929,6 +929,8 @@ const {
929
929
  menuItems,
930
930
  downloadLinkText,
931
931
  csvHref,
932
+ downloadButtonText,
933
+ triggerCsvDownload,
932
934
  menuFilename,
933
935
  isFullscreen,
934
936
  } = useChartFoundation({
@@ -944,6 +946,7 @@ const {
944
946
  tooltipClamp: () => props.tooltipClamp,
945
947
  filename: () => props.filename,
946
948
  downloadLink: () => props.downloadLink,
949
+ downloadButton: () => props.downloadButton,
947
950
  chartPadding: () => props.chartPadding,
948
951
  inlineLegendLabels: () => inlineLegendLabels.value,
949
952
  hasTooltipSlot: () => hasTooltipSlot.value,
@@ -1403,6 +1406,14 @@ const positionedLegendItems = computed(() => {
1403
1406
  >
1404
1407
  {{ downloadLinkText }}
1405
1408
  </a>
1409
+ <button
1410
+ v-if="downloadButtonText"
1411
+ type="button"
1412
+ class="line-chart-download-button"
1413
+ @click="triggerCsvDownload"
1414
+ >
1415
+ {{ downloadButtonText }}
1416
+ </button>
1406
1417
  </div>
1407
1418
  </template>
1408
1419
 
@@ -1438,3 +1449,27 @@ const positionedLegendItems = computed(() => {
1438
1449
  flex-shrink: 0;
1439
1450
  }
1440
1451
  </style>
1452
+
1453
+ <style>
1454
+ .line-chart-download-button {
1455
+ display: inline-flex;
1456
+ align-items: center;
1457
+ margin-top: 0.5em;
1458
+ padding: 0.5em 1em;
1459
+ border: 1px solid var(--color-border);
1460
+ border-radius: 0.25em;
1461
+ background: var(--color-bg-0, #fff);
1462
+ color: var(--color-text);
1463
+ font-size: var(--font-size-sm);
1464
+ cursor: pointer;
1465
+ }
1466
+
1467
+ .line-chart-download-button:hover {
1468
+ background: var(--color-bg-1, rgba(0, 0, 0, 0.05));
1469
+ }
1470
+
1471
+ .line-chart-download-button:focus-visible {
1472
+ outline: 2px solid var(--color-primary);
1473
+ outline-offset: 2px;
1474
+ }
1475
+ </style>
@@ -141,6 +141,13 @@ export interface ChartCommonProps {
141
141
  * for the default label or a string to customize.
142
142
  */
143
143
  downloadLink?: boolean | string;
144
+ /**
145
+ * Show a `<button>` below the chart to download CSV. Pass `true` for
146
+ * the default label or a string to customize. When set, the CSV menu
147
+ * item is suppressed. Mutually exclusive with `downloadLink`; if both
148
+ * are set, `downloadButton` wins.
149
+ */
150
+ downloadButton?: boolean | string;
144
151
  /** Annotations rendered as the top layer of the chart. */
145
152
  annotations?: readonly ChartAnnotation[];
146
153
  /**
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Color helpers for picking a readable text color against a bar fill.
3
+ * The luminance math is pure and unit-testable; resolving CSS values
4
+ * that aren't plain hex/rgb (named colors, `var(--x)`) needs the DOM and
5
+ * is handled separately by {@link resolveColorToRgb}.
6
+ */
7
+
8
+ export type Rgb = [number, number, number];
9
+
10
+ /**
11
+ * Parse a hex (`#rgb`, `#rrggbb`, `#rrggbbaa`) or `rgb()/rgba()` string to
12
+ * `[r, g, b]` in 0..255. Returns null for anything else (named colors,
13
+ * `var(...)`, `hsl(...)`) — resolve those via {@link resolveColorToRgb}.
14
+ */
15
+ export function parseRgb(color: string): Rgb | null {
16
+ const c = color.trim();
17
+ if (c.startsWith("#")) {
18
+ let hex = c.slice(1);
19
+ if (hex.length === 3 || hex.length === 4) {
20
+ hex = hex
21
+ .slice(0, 3)
22
+ .split("")
23
+ .map((ch) => ch + ch)
24
+ .join("");
25
+ } else if (hex.length === 6 || hex.length === 8) {
26
+ hex = hex.slice(0, 6);
27
+ } else {
28
+ return null;
29
+ }
30
+ const n = Number.parseInt(hex, 16);
31
+ if (Number.isNaN(n)) return null;
32
+ return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
33
+ }
34
+ const m = c.match(/^rgba?\(([^)]+)\)$/i);
35
+ if (m) {
36
+ const parts = m[1].split(/[,/\s]+/).filter(Boolean);
37
+ if (parts.length < 3) return null;
38
+ const rgb = parts.slice(0, 3).map((p) => {
39
+ if (p.endsWith("%")) return Math.round((parseFloat(p) / 100) * 255);
40
+ return Math.round(parseFloat(p));
41
+ });
42
+ if (rgb.some((v) => Number.isNaN(v))) return null;
43
+ return [rgb[0], rgb[1], rgb[2]];
44
+ }
45
+ return null;
46
+ }
47
+
48
+ /** WCAG relative luminance (0 = black, 1 = white) for an RGB triple. */
49
+ export function relativeLuminance([r, g, b]: Rgb): number {
50
+ const channel = (v: number) => {
51
+ const s = v / 255;
52
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
53
+ };
54
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
55
+ }
56
+
57
+ let probe: HTMLSpanElement | null = null;
58
+ const resolveCache = new Map<string, Rgb | null>();
59
+
60
+ /**
61
+ * Resolve any CSS color string (including named colors and `var(--x)`) to
62
+ * an RGB triple by letting the browser compute it on a hidden probe
63
+ * element. Cached per input. Returns null in non-DOM environments or when
64
+ * the value can't be resolved. Plain hex/rgb shortcut through
65
+ * {@link parseRgb} without touching the DOM.
66
+ */
67
+ export function resolveColorToRgb(color: string): Rgb | null {
68
+ const direct = parseRgb(color);
69
+ if (direct) return direct;
70
+ if (resolveCache.has(color)) return resolveCache.get(color) ?? null;
71
+ if (typeof document === "undefined") return null;
72
+ if (!probe) {
73
+ probe = document.createElement("span");
74
+ probe.style.cssText =
75
+ "position:absolute;width:0;height:0;visibility:hidden;pointer-events:none";
76
+ document.body.appendChild(probe);
77
+ }
78
+ probe.style.color = "";
79
+ probe.style.color = color;
80
+ const computed = getComputedStyle(probe).color;
81
+ const rgb = parseRgb(computed);
82
+ resolveCache.set(color, rgb);
83
+ return rgb;
84
+ }
85
+
86
+ /**
87
+ * Pick the more readable of `light`/`dark` text colors for a given fill.
88
+ * Uses the WCAG luminance threshold (0.179) that maximizes contrast
89
+ * against black-or-white text. When the fill can't be resolved (e.g. an
90
+ * unresolvable CSS var in a non-DOM context), falls back to `light`.
91
+ */
92
+ export function pickContrastColor(
93
+ fill: string,
94
+ light = "#ffffff",
95
+ dark = "#1a1a1a",
96
+ ): string {
97
+ const rgb = resolveColorToRgb(fill);
98
+ if (!rgb) return light;
99
+ return relativeLuminance(rgb) > 0.179 ? dark : light;
100
+ }
@@ -54,6 +54,13 @@ export type {
54
54
  BlendMode,
55
55
  LineMarkStyle,
56
56
  } from "./chartProps.js";
57
+ export {
58
+ parseRgb,
59
+ relativeLuminance,
60
+ resolveColorToRgb,
61
+ pickContrastColor,
62
+ type Rgb,
63
+ } from "./contrast.js";
57
64
  export {
58
65
  parseDate,
59
66
  isDateLike,
@@ -25,6 +25,7 @@ export interface ChartFoundationOptions {
25
25
  tooltipClamp: () => TooltipClamp | undefined;
26
26
  filename: () => string | undefined;
27
27
  downloadLink: () => boolean | string | undefined;
28
+ downloadButton: () => boolean | string | undefined;
28
29
  chartPadding: () => ChartPadding | undefined;
29
30
  // Chart-specific hooks that the composable can't infer.
30
31
  /**
@@ -61,6 +62,8 @@ export function useChartFoundation(opts: ChartFoundationOptions) {
61
62
  items: menuItems,
62
63
  downloadLinkText,
63
64
  csvHref,
65
+ downloadButtonText,
66
+ triggerCsvDownload,
64
67
  resolvedFilename: menuFilename,
65
68
  isFullscreen,
66
69
  } = useChartMenu({
@@ -68,6 +71,7 @@ export function useChartFoundation(opts: ChartFoundationOptions) {
68
71
  legacyMenuLabel: opts.menu,
69
72
  getCsv: opts.getCsv,
70
73
  downloadLink: opts.downloadLink,
74
+ downloadButton: opts.downloadButton,
71
75
  fullscreen: true,
72
76
  });
73
77
 
@@ -130,6 +134,8 @@ export function useChartFoundation(opts: ChartFoundationOptions) {
130
134
  menuItems,
131
135
  downloadLinkText,
132
136
  csvHref,
137
+ downloadButtonText,
138
+ triggerCsvDownload,
133
139
  menuFilename,
134
140
  isFullscreen,
135
141
  measuredHeight,