@surflane/ui 0.11.0 → 0.13.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 (137) hide show
  1. package/dist/alert-dialog.d.ts +25 -7
  2. package/dist/alert-dialog.js +9 -4
  3. package/dist/app-shell.d.ts +10 -0
  4. package/dist/app-shell.js +7 -1
  5. package/dist/autocomplete.d.ts +2 -6
  6. package/dist/avatar.d.ts +7 -7
  7. package/dist/avatar.js +2 -2
  8. package/dist/bar-list.d.ts +31 -0
  9. package/dist/bar-list.js +31 -0
  10. package/dist/bottom-navigation.d.ts +6 -2
  11. package/dist/bottom-navigation.js +2 -2
  12. package/dist/button.d.ts +6 -11
  13. package/dist/button.js +8 -8
  14. package/dist/card.d.ts +15 -10
  15. package/dist/card.js +7 -2
  16. package/dist/chart.d.ts +58 -3
  17. package/dist/chart.js +330 -48
  18. package/dist/choice.d.ts +2 -2
  19. package/dist/class-names.d.ts +0 -4
  20. package/dist/class-names.js +0 -14
  21. package/dist/code.d.ts +19 -7
  22. package/dist/code.js +32 -38
  23. package/dist/combobox.d.ts +2 -8
  24. package/dist/combobox.js +2 -2
  25. package/dist/context-menu.d.ts +2 -8
  26. package/dist/context-menu.js +2 -2
  27. package/dist/css/bar-list.css +9 -0
  28. package/dist/css/chart.css +2 -0
  29. package/dist/css/dashboard.css +20 -0
  30. package/dist/css/rules/alert-dialog.css +8 -2
  31. package/dist/css/rules/app-shell.css +57 -10
  32. package/dist/css/rules/bar-list.css +101 -0
  33. package/dist/css/rules/bar.css +5 -12
  34. package/dist/css/rules/bottom-navigation.css +21 -2
  35. package/dist/css/rules/breadcrumbs.css +0 -6
  36. package/dist/css/rules/button.css +4 -12
  37. package/dist/css/rules/card.css +19 -0
  38. package/dist/css/rules/chart.css +87 -8
  39. package/dist/css/rules/choice.css +26 -2
  40. package/dist/css/rules/combobox.css +17 -16
  41. package/dist/css/rules/dashboard.css +118 -0
  42. package/dist/css/rules/date-picker.css +50 -27
  43. package/dist/css/rules/dialog.css +47 -7
  44. package/dist/css/rules/disclosure.css +17 -3
  45. package/dist/css/rules/field-control.css +9 -6
  46. package/dist/css/rules/file-upload.css +9 -5
  47. package/dist/css/rules/filter-bar.css +32 -2
  48. package/dist/css/rules/gauge.css +2 -2
  49. package/dist/css/rules/icon.css +0 -5
  50. package/dist/css/rules/input-group.css +23 -8
  51. package/dist/css/rules/link.css +0 -6
  52. package/dist/css/rules/menu.css +5 -2
  53. package/dist/css/rules/menubar.css +1 -1
  54. package/dist/css/rules/navigation-menu.css +31 -26
  55. package/dist/css/rules/number-field.css +8 -1
  56. package/dist/css/rules/otp-field.css +13 -3
  57. package/dist/css/rules/page-header.css +10 -0
  58. package/dist/css/rules/panel.css +59 -10
  59. package/dist/css/rules/popup.css +50 -0
  60. package/dist/css/rules/preview-card.css +0 -21
  61. package/dist/css/rules/scroll-area.css +20 -1
  62. package/dist/css/rules/select.css +29 -2
  63. package/dist/css/rules/sizing.css +25 -3
  64. package/dist/css/rules/skeleton.css +1 -1
  65. package/dist/css/rules/slider.css +16 -8
  66. package/dist/css/rules/spinner.css +1 -1
  67. package/dist/css/rules/stat.css +66 -0
  68. package/dist/css/rules/tabs.css +14 -6
  69. package/dist/css/rules/textarea.css +28 -0
  70. package/dist/css/rules/time-range-picker.css +14 -0
  71. package/dist/css/rules/toast.css +3 -1
  72. package/dist/css/rules/toggle.css +10 -3
  73. package/dist/css/rules/toolbar.css +10 -3
  74. package/dist/css/rules/tree.css +29 -4
  75. package/dist/css/styles.css +3 -0
  76. package/dist/css/time-range-picker.css +14 -0
  77. package/dist/dashboard.d.ts +58 -0
  78. package/dist/dashboard.js +60 -0
  79. package/dist/date-picker.d.ts +32 -3
  80. package/dist/date-picker.js +43 -4
  81. package/dist/dialog.d.ts +25 -7
  82. package/dist/dialog.js +9 -4
  83. package/dist/drawer.d.ts +10 -2
  84. package/dist/drawer.js +6 -3
  85. package/dist/icon.d.ts +15 -5
  86. package/dist/icon.js +16 -3
  87. package/dist/index.d.ts +9 -6
  88. package/dist/index.js +7 -4
  89. package/dist/inline-message.d.ts +2 -2
  90. package/dist/inline-message.js +7 -2
  91. package/dist/input.d.ts +2 -2
  92. package/dist/layout.d.ts +1 -3
  93. package/dist/layout.js +2 -2
  94. package/dist/native-select.d.ts +2 -2
  95. package/dist/navigation-menu.d.ts +16 -0
  96. package/dist/navigation-menu.js +6 -1
  97. package/dist/page-header.d.ts +10 -2
  98. package/dist/page-header.js +3 -3
  99. package/dist/panel.d.ts +6 -3
  100. package/dist/panel.js +2 -2
  101. package/dist/parts.d.ts +7 -3
  102. package/dist/parts.js +12 -4
  103. package/dist/popover.d.ts +6 -6
  104. package/dist/popover.js +3 -2
  105. package/dist/preview-card.d.ts +2 -8
  106. package/dist/preview-card.js +2 -2
  107. package/dist/select.d.ts +38 -8
  108. package/dist/select.js +110 -22
  109. package/dist/sheet.d.ts +10 -2
  110. package/dist/sheet.js +6 -3
  111. package/dist/size.d.ts +8 -6
  112. package/dist/size.js +2 -7
  113. package/dist/sizing.d.ts +13 -6
  114. package/dist/sizing.js +13 -4
  115. package/dist/sparkline.d.ts +2 -2
  116. package/dist/sparkline.js +2 -2
  117. package/dist/spinner.d.ts +2 -2
  118. package/dist/spinner.js +2 -2
  119. package/dist/stat.d.ts +24 -0
  120. package/dist/stat.js +15 -1
  121. package/dist/table.d.ts +12 -4
  122. package/dist/table.js +8 -3
  123. package/dist/textarea.d.ts +6 -3
  124. package/dist/textarea.js +2 -2
  125. package/dist/time-range-picker.d.ts +42 -0
  126. package/dist/time-range-picker.js +69 -0
  127. package/dist/toggle.d.ts +2 -2
  128. package/dist/toggle.js +2 -2
  129. package/dist/toolbar.d.ts +11 -10
  130. package/dist/toolbar.js +23 -1
  131. package/dist/tooltip.d.ts +1 -3
  132. package/dist/tooltip.js +4 -2
  133. package/dist/tree.d.ts +1 -3
  134. package/dist/tree.js +2 -2
  135. package/dist/typography.d.ts +10 -3
  136. package/dist/typography.js +4 -3
  137. package/package.json +18 -3
package/dist/chart.js CHANGED
@@ -1,11 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from 'react';
4
- import { Area, Bar, CartesianGrid, ComposedChart, Line, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, useActiveTooltipCoordinate, useActiveTooltipLabel, useIsTooltipActive, usePlotArea, } from 'recharts';
4
+ import { Area, Bar, CartesianGrid, ComposedChart, Line, ReferenceArea, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, useActiveTooltipCoordinate, useActiveTooltipLabel, useIsTooltipActive, usePlotArea, } from 'recharts';
5
5
  import { createPortal } from 'react-dom';
6
6
  import { Table } from './table.js';
7
7
  import { Button } from './button.js';
8
+ import { Popover } from './popover.js';
8
9
  import { cx } from './parts.js';
10
+ import { useComponentDensity } from './sizing.js';
9
11
  const chartLabels = {
10
12
  missing: 'Not available',
11
13
  empty: 'No numeric measurements to display.',
@@ -20,6 +22,8 @@ const chartLabels = {
20
22
  noRows: 'No rows.',
21
23
  legend: (title) => `${title} series`,
22
24
  series: (label) => `${label} series`,
25
+ moreSeries: (count) => `${count} more series`,
26
+ total: 'Total',
23
27
  };
24
28
  /** The round step at or above a raw step: 1, 2, 2.5 or 5 times a power of ten. */
25
29
  function roundStep(raw) {
@@ -37,6 +41,112 @@ function axisStep(low, high) {
37
41
  return step;
38
42
  }
39
43
  const dashes = [undefined, '7 4', '2 4', '10 3 2 3', '12 5', '4 3'];
44
+ /** A series' color: the caller's, or the palette's for its place. */
45
+ function seriesColor(index, palette, color) {
46
+ return (color ??
47
+ (palette === 'categorical'
48
+ ? `var(--surf-chart-category-${(index % 12) + 1})`
49
+ : `var(--surf-chart-series-${(index % 3) + 1})`));
50
+ }
51
+ const toneColors = {
52
+ neutral: 'var(--surf-ink-muted)',
53
+ caution: 'var(--surf-status-caution-fg)',
54
+ danger: 'var(--surf-status-danger-fg)',
55
+ };
56
+ const MINUTE = 60_000, HOUR = 60 * MINUTE, DAY = 24 * HOUR;
57
+ const timeSteps = [
58
+ ['minute', 1, MINUTE],
59
+ ['minute', 5, 5 * MINUTE],
60
+ ['minute', 15, 15 * MINUTE],
61
+ ['minute', 30, 30 * MINUTE],
62
+ ['hour', 1, HOUR],
63
+ ['hour', 3, 3 * HOUR],
64
+ ['hour', 6, 6 * HOUR],
65
+ ['hour', 12, 12 * HOUR],
66
+ ['day', 1, DAY],
67
+ ['day', 2, 2 * DAY],
68
+ ['day', 7, 7 * DAY],
69
+ ['month', 1, 30 * DAY],
70
+ ['month', 3, 91 * DAY],
71
+ ['month', 6, 182 * DAY],
72
+ ['year', 1, 365 * DAY],
73
+ ];
74
+ /** Ticks on round local times, such as every 6 hours or the first of each month: at
75
+ * most six across the span, and no more than fit, so their steps stay even. */
76
+ function timeTicks(min, max, fits = () => true) {
77
+ const [unit, count] = timeSteps.find(([stepUnit, , ms]) => (max - min) / ms <= 6 && fits(stepUnit, ms)) ?? timeSteps[timeSteps.length - 1];
78
+ const date = new Date(min);
79
+ date.setSeconds(0, 0);
80
+ if (unit === 'minute')
81
+ date.setMinutes(Math.floor(date.getMinutes() / count) * count);
82
+ else
83
+ date.setMinutes(0);
84
+ if (unit === 'hour')
85
+ date.setHours(Math.floor(date.getHours() / count) * count);
86
+ else if (unit !== 'minute')
87
+ date.setHours(0);
88
+ if (unit === 'month')
89
+ date.setMonth(Math.floor(date.getMonth() / count) * count, 1);
90
+ if (unit === 'year')
91
+ date.setMonth(0, 1);
92
+ const ticks = [];
93
+ while (date.getTime() <= max && ticks.length < 100) {
94
+ if (date.getTime() >= min)
95
+ ticks.push(date.getTime());
96
+ if (unit === 'minute')
97
+ date.setMinutes(date.getMinutes() + count);
98
+ else if (unit === 'hour')
99
+ date.setHours(date.getHours() + count);
100
+ else if (unit === 'day')
101
+ date.setDate(date.getDate() + count);
102
+ else if (unit === 'month')
103
+ date.setMonth(date.getMonth() + count);
104
+ else
105
+ date.setFullYear(date.getFullYear() + count);
106
+ }
107
+ return { ticks, unit };
108
+ }
109
+ /** Powers of ten that span low to high, with 2 and 5 between them across one decade. */
110
+ function logTicks(low, high) {
111
+ const bottom = Math.floor(Math.log10(low)), top = Math.max(Math.ceil(Math.log10(high)), bottom + 1);
112
+ const ticks = [];
113
+ for (let power = bottom; power <= top; power++)
114
+ for (const factor of top - bottom === 1 ? [1, 2, 5] : [1])
115
+ if (power < top || factor === 1)
116
+ ticks.push(Number((factor * 10 ** power).toPrecision(12)));
117
+ return ticks;
118
+ }
119
+ const isNumber = (value) => typeof value === 'number' && Number.isFinite(value);
120
+ /** An element's content width, kept current as it resizes; 0 before it is measured. */
121
+ function useWidth() {
122
+ const [element, setElement] = React.useState(null);
123
+ const [width, setWidth] = React.useState(0);
124
+ React.useLayoutEffect(() => {
125
+ if (!element)
126
+ return;
127
+ setWidth(element.clientWidth);
128
+ if (typeof ResizeObserver === 'undefined')
129
+ return;
130
+ const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
131
+ observer.observe(element);
132
+ return () => observer.disconnect();
133
+ }, [element]);
134
+ return [setElement, width, element];
135
+ }
136
+ let measureCanvas;
137
+ /** How wide a tick label draws, in the font it draws in. */
138
+ function textWidth(text, font) {
139
+ measureCanvas ??= document.createElement('canvas');
140
+ const context = measureCanvas.getContext('2d');
141
+ if (!context)
142
+ return text.length * 7.5;
143
+ context.font = font;
144
+ return context.measureText(text).width;
145
+ }
146
+ /** The gap Recharts keeps between tick labels. */
147
+ const TICK_GAP = 24;
148
+ /** The value axis and the plot's right margin: what the plot gives up of the frame. */
149
+ const PLOT_INSET = 56 + 20;
40
150
  /** True once the font of the tick labels has loaded. Recharts measures a label to keep
41
151
  * it inside the chart and caches that width; measured in the fallback font, the last
42
152
  * label overflows by a few pixels once the real font arrives. */
@@ -79,6 +189,15 @@ function Pattern({ id, index, color, }) {
79
189
  function SeriesMark({ type, index, color, pattern, }) {
80
190
  return (_jsx("svg", { className: 'surf-chart-mark', viewBox: '0 0 32 20', "aria-hidden": 'true', focusable: 'false', children: type === 'bar' ? (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx(Pattern, { id: pattern, color: color, index: index }) }), _jsx("rect", { x: '4', y: '3', width: '24', height: '14', rx: '2', fill: `url(#${pattern})` })] })) : (_jsxs(_Fragment, { children: [_jsx("path", { d: 'M1 10h30', stroke: color, strokeWidth: '2', strokeDasharray: dashes[index % dashes.length] }), _jsx(Marker, { cx: 16, cy: 10, index: index, color: color })] })) }));
81
191
  }
192
+ function ReferenceMark({ reference }) {
193
+ const color = toneColors[reference.tone ?? 'neutral'];
194
+ return (_jsx("svg", { className: 'surf-chart-mark', viewBox: '0 0 32 20', "aria-hidden": 'true', focusable: 'false', children: 'y' in reference ? (_jsx("path", { d: 'M1 10h30', stroke: color, strokeWidth: '2', strokeDasharray: '4 3' })) : (_jsx("rect", { x: '4', y: '4', width: '24', height: '12', rx: '2', fill: color, fillOpacity: '0.2' })) }));
195
+ }
196
+ /** A series' swatch as Chart's legend draws it, for a legend or key of your own. */
197
+ export function ChartSeriesMark({ type = 'line', index, palette = 'neutral', color, }) {
198
+ const id = React.useId().replace(/[^a-zA-Z0-9_-]/g, '_');
199
+ return (_jsx(SeriesMark, { type: type, index: index, color: seriesColor(index, palette, color), pattern: `${id}-mark` }));
200
+ }
82
201
  /** The active point's values beside it, for pointer and touch users. The readout
83
202
  * below the plot announces the same values, so this copy stays out of the
84
203
  * accessibility tree. It sits on the side of the point with more room and stays
@@ -95,29 +214,54 @@ function FloatingValues({ host, hidden, render, }) {
95
214
  if (shown && box.current)
96
215
  setWidth(box.current.offsetWidth);
97
216
  }, [shown, label, plot?.width]);
98
- if (!shown || !point || !plot)
217
+ const content = shown ? render(String(label)) : null;
218
+ // A break in sampling has no values to show.
219
+ if (!shown || !point || !plot || content === null)
99
220
  return null;
100
221
  // The gap between the point and the box, in px: the --surf-space-3 step.
101
222
  const gap = 12;
102
223
  const start = point.x > plot.x + plot.width / 2;
103
224
  const left = Math.min(Math.max(start ? point.x - gap - width : point.x + gap, plot.x), plot.x + plot.width - width);
104
- return createPortal(_jsx("div", { ref: box, className: 'surf-chart-tooltip', "aria-hidden": 'true', style: { left, top: plot.y, visibility: width ? undefined : 'hidden' }, children: render(String(label)) }), host);
225
+ return createPortal(_jsx("div", { ref: box, className: 'surf-chart-tooltip', "aria-hidden": 'true', style: { left, top: plot.y, visibility: width ? undefined : 'hidden' }, children: content }), host);
105
226
  }
106
227
  /** Cartesian trends with a persistent, keyboard-readable tooltip region and a
107
228
  * full data-table alternative. Missing values are gaps, never fabricated zeros. */
108
- export const Chart = function Chart({ title, description, data, series, unit, xLabel, yLabel, type = 'line', height = 280, includeZero = true, palette = 'neutral', locale = 'en-US', formatValue, labels, hiddenSeries, defaultHiddenSeries = [], onHiddenSeriesChange, className, ref, ...props }) {
229
+ export const Chart = function Chart({ title, description, data, series, unit, xLabel, yLabel, type = 'line', height, includeZero = true, palette = 'neutral', xScale = 'category', yScale = 'linear', curve = 'linear', stacked = false, references = [], density: ownDensity, legendPosition = 'top', legendLimit, locale, formatValue, labels, hiddenSeries, defaultHiddenSeries = [], onHiddenSeriesChange, className, ref, ...props }) {
109
230
  const text = { ...chartLabels, ...labels };
110
231
  const id = React.useId().replace(/[^a-zA-Z0-9_-]/g, '_');
111
- const colorFor = (index) => palette === 'categorical'
112
- ? `var(--surf-chart-category-${(index % 6) + 1})`
113
- : `var(--surf-chart-series-${(index % 3) + 1})`;
232
+ const colorFor = (index) => seriesColor(index, palette, series[index]?.color);
233
+ const timeScale = xScale === 'time';
234
+ const logScale = yScale === 'log';
235
+ const stacks = stacked && type !== 'line';
236
+ const density = useComponentDensity(ownDensity);
237
+ const compact = density === 'compact';
114
238
  const [internalHidden, setInternalHidden] = React.useState(defaultHiddenSeries);
115
239
  const hidden = new Set(hiddenSeries ?? internalHidden);
116
240
  const visible = series.filter((s) => !hidden.has(s.key));
117
241
  const [host, setHost] = React.useState(null);
118
242
  const fontsReady = useFontReady(host, data.map((d) => d.label).join(''));
119
243
  const [floatHost, setFloatHost] = React.useState(null);
244
+ const [setFrame, frameWidth, frame] = useWidth();
245
+ // legendLimit='auto': lay every series out, count those on the first line, then
246
+ // fold the rest, keeping room for the "+N" button. A new width lays them out again.
247
+ const legendList = React.useRef(null);
248
+ const [legendFit, setLegendFit] = React.useState();
249
+ React.useLayoutEffect(() => {
250
+ if (legendLimit === 'auto')
251
+ setLegendFit(undefined);
252
+ }, [legendLimit, frameWidth, series.length]);
253
+ React.useLayoutEffect(() => {
254
+ if (legendLimit !== 'auto' || legendFit !== undefined)
255
+ return;
256
+ const items = Array.from(legendList.current?.querySelectorAll(':scope > [data-series]') ?? []);
257
+ if (!items.length)
258
+ return;
259
+ const line = items.filter((item) => item.offsetTop === items[0].offsetTop).length;
260
+ setLegendFit(line >= items.length ? items.length : Math.max(1, line - 1));
261
+ });
120
262
  const [dismissed, setDismissed] = React.useState(false);
263
+ // The table renders once it is first opened, so a page of charts stays light.
264
+ const [tableOpened, setTableOpened] = React.useState(false);
121
265
  const firstLegend = React.useRef(null);
122
266
  const [restoreRequested, setRestoreRequested] = React.useState(false);
123
267
  React.useLayoutEffect(() => {
@@ -128,13 +272,20 @@ export const Chart = function Chart({ title, description, data, series, unit, xL
128
272
  setRestoreRequested(false);
129
273
  }, [restoreRequested, visible.length]);
130
274
  const number = React.useMemo(() => new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }), [locale]);
131
- const compact = React.useMemo(() => new Intl.NumberFormat(locale, {
275
+ const compactNumber = React.useMemo(() => new Intl.NumberFormat(locale, {
132
276
  notation: 'compact',
133
277
  maximumFractionDigits: 1,
134
278
  }), [locale]);
135
- const format = (value) => typeof value === 'number' && Number.isFinite(value)
136
- ? `${formatValue ? formatValue(value) : number.format(value)} ${unit}`
137
- : text.missing;
279
+ const formatNumber = (value) => isNumber(value)
280
+ ? formatValue
281
+ ? formatValue(value)
282
+ : number.format(value)
283
+ : null;
284
+ // The readout names the unit after each value; the table names it once, in the header.
285
+ const format = (value) => {
286
+ const formatted = formatNumber(value);
287
+ return formatted === null ? text.missing : `${formatted} ${unit}`;
288
+ };
138
289
  const rows = React.useMemo(() => {
139
290
  const ids = new Set(), keys = new Set();
140
291
  for (const s of series) {
@@ -142,38 +293,144 @@ export const Chart = function Chart({ title, description, data, series, unit, xL
142
293
  throw new Error('Chart series keys must be non-empty and unique');
143
294
  keys.add(s.key);
144
295
  }
145
- return data.map((row) => {
296
+ const result = data.map((row) => {
146
297
  if (!row.id || ids.has(row.id))
147
298
  throw new Error('Chart datum IDs must be non-empty and unique');
148
299
  ids.add(row.id);
300
+ const time = row.time === undefined ? undefined : Number(row.time);
301
+ if (timeScale && !isNumber(time))
302
+ throw new Error('Chart rows need a time when xScale is "time"');
149
303
  return {
150
304
  ...row,
305
+ time,
151
306
  values: Object.fromEntries(series.map((s) => [
152
307
  s.key,
153
- typeof row.values[s.key] === 'number' &&
154
- Number.isFinite(row.values[s.key])
155
- ? row.values[s.key]
156
- : null,
308
+ isNumber(row.values[s.key]) ? row.values[s.key] : null,
157
309
  ])),
158
310
  };
159
311
  });
160
- }, [data, series]);
312
+ return timeScale ? result.sort((a, b) => a.time - b.time) : result;
313
+ }, [data, series, timeScale]);
161
314
  const byId = new Map(rows.map((row) => [row.id, row]));
315
+ const byTime = new Map(rows.map((row) => [row.time, row]));
316
+ const rowFor = (label) => timeScale ? byTime.get(Number(label)) : byId.get(String(label));
317
+ // On a time axis, a wait of more than twice the usual interval is a break in
318
+ // sampling: an empty row there keeps the line from bridging it.
319
+ const interval = React.useMemo(() => {
320
+ if (!timeScale || rows.length < 3)
321
+ return 0;
322
+ const steps = rows
323
+ .slice(1)
324
+ .map((row, index) => row.time - rows[index].time)
325
+ .sort((a, b) => a - b);
326
+ return steps[Math.floor(steps.length / 2)];
327
+ }, [rows, timeScale]);
328
+ const plotRows = React.useMemo(() => {
329
+ if (!interval)
330
+ return rows;
331
+ const empty = Object.fromEntries(series.map((s) => [s.key, null]));
332
+ return rows.flatMap((row, index) => {
333
+ const next = rows[index + 1];
334
+ return next && next.time - row.time > interval * 2
335
+ ? [
336
+ row,
337
+ {
338
+ ...row,
339
+ id: `${row.id}-gap`,
340
+ time: row.time + interval,
341
+ values: empty,
342
+ },
343
+ ]
344
+ : [row];
345
+ });
346
+ }, [rows, series, interval]);
347
+ const valueOf = (row, key) => {
348
+ const value = row.values[key];
349
+ return value === null || (logScale && value <= 0) ? null : value;
350
+ };
351
+ // The value axis spans the visible values (their sums when stacked) and the
352
+ // references, widened to round steps so its ticks read cleanly.
162
353
  const values = rows
163
- .flatMap((row) => visible.map((s) => row.values[s.key]))
164
- .filter((value) => typeof value === 'number');
165
- // Bars grow from zero, so they always keep it on the axis. Either way the axis
166
- // spans the values, widened to round steps so its ticks read cleanly.
167
- const keepZero = includeZero || type === 'bar';
168
- const start = keepZero ? 0 : (values[0] ?? 0);
169
- const low = values.reduce((a, b) => Math.min(a, b), start), high = values.reduce((a, b) => Math.max(a, b), start);
170
- const step = axisStep(low, high);
171
- const min = step ? Math.floor(low / step) * step : low, max = step ? Math.ceil(high / step) * step : high;
172
- const ticks = step
173
- ? Array.from({ length: Math.round((max - min) / step) + 1 },
174
- // Rounded, so float steps never print as 0.30000000000000004.
175
- (_, index) => Number((min + index * step).toPrecision(12)))
176
- : undefined;
354
+ .flatMap((row) => visible.map((s) => valueOf(row, s.key)))
355
+ .filter(isNumber);
356
+ const sums = stacks
357
+ ? rows.flatMap((row) => {
358
+ const parts = visible.map((s) => valueOf(row, s.key)).filter(isNumber);
359
+ return [
360
+ parts.filter((v) => v > 0).reduce((a, b) => a + b, 0),
361
+ parts.filter((v) => v < 0).reduce((a, b) => a + b, 0),
362
+ ];
363
+ })
364
+ : values;
365
+ const referenceValues = references
366
+ .flatMap((r) => ('y' in r ? [r.y] : [r.from, r.to]))
367
+ .filter((v) => isNumber(v) && (!logScale || v > 0));
368
+ const extent = [...sums, ...(values.length ? referenceValues : [])];
369
+ // Bars grow from zero, so they always keep it on a linear axis.
370
+ const keepZero = !logScale && (includeZero || type === 'bar');
371
+ const start = keepZero ? 0 : (extent[0] ?? 0);
372
+ const low = extent.reduce((a, b) => Math.min(a, b), start), high = extent.reduce((a, b) => Math.max(a, b), start);
373
+ let min = low, max = high, ticks;
374
+ if (logScale) {
375
+ ticks = logTicks(low > 0 ? low : 1, high > 0 ? high : 10);
376
+ min = ticks[0];
377
+ max = ticks[ticks.length - 1];
378
+ }
379
+ else {
380
+ const step = axisStep(low, high);
381
+ if (step) {
382
+ min = Math.floor(low / step) * step;
383
+ max = Math.ceil(high / step) * step;
384
+ ticks = Array.from({ length: Math.round((max - min) / step) + 1 },
385
+ // Rounded, so float steps never print as 0.30000000000000004.
386
+ (_, index) => Number((min + index * step).toPrecision(12)));
387
+ }
388
+ }
389
+ const time = React.useMemo(() => {
390
+ if (!timeScale || !rows.length)
391
+ return undefined;
392
+ const first = rows[0].time, last = rows[rows.length - 1].time;
393
+ const formatFor = (unit) => new Intl.DateTimeFormat(locale, unit === 'minute' || unit === 'hour'
394
+ ? { hour: 'numeric', minute: '2-digit' }
395
+ : unit === 'day'
396
+ ? { month: 'short', day: 'numeric' }
397
+ : unit === 'month'
398
+ ? { month: 'short', year: 'numeric' }
399
+ : { year: 'numeric' });
400
+ // Pick the step from the plot's width and the width its labels draw at, so a
401
+ // narrow chart steps every 2 or 3 hours instead of dropping uneven ticks.
402
+ const plotWidth = frameWidth - PLOT_INSET;
403
+ const font = frame && fontsReady
404
+ ? `12px ${getComputedStyle(frame).fontFamily}`
405
+ : undefined;
406
+ const fits = (unit, ms) => {
407
+ if (plotWidth <= 0 || !font)
408
+ return true;
409
+ const label = Math.max(textWidth(formatFor(unit).format(new Date(first)), font), textWidth(formatFor(unit).format(new Date(2026, 9, 24, 22, 30)), font));
410
+ const count = Math.floor((last - first) / ms) + 1;
411
+ return count * (label + TICK_GAP) - TICK_GAP <= plotWidth;
412
+ };
413
+ const { ticks: timeAxisTicks, unit: timeUnit } = timeTicks(first, last, fits);
414
+ // Bars sit on their time, so half an interval on each side keeps the end ones whole.
415
+ const pad = type === 'bar' ? (interval || DAY) / 2 : 0;
416
+ const subDay = timeUnit === 'minute' || timeUnit === 'hour';
417
+ const tickFormat = formatFor(timeUnit);
418
+ const dayFormat = new Intl.DateTimeFormat(locale, {
419
+ month: 'short',
420
+ day: 'numeric',
421
+ });
422
+ return {
423
+ domain: [first - pad, last + pad],
424
+ ticks: timeAxisTicks,
425
+ // Within a day the ticks name hours; one at midnight names the day instead.
426
+ format: (value) => {
427
+ const date = new Date(value);
428
+ return subDay && date.getHours() === 0 && date.getMinutes() === 0
429
+ ? dayFormat.format(date)
430
+ : tickFormat.format(date);
431
+ },
432
+ };
433
+ }, [rows, timeScale, type, interval, locale, frameWidth, frame, fontsReady]);
177
434
  const hasValues = values.length > 0;
178
435
  const changeHidden = (next) => {
179
436
  if (hiddenSeries === undefined)
@@ -183,14 +440,35 @@ export const Chart = function Chart({ title, description, data, series, unit, xL
183
440
  const toggle = (key) => changeHidden(series
184
441
  .filter((s) => (s.key === key ? !hidden.has(key) : hidden.has(s.key)))
185
442
  .map((s) => s.key));
186
- const chartHeight = Number.isFinite(height) ? Math.max(200, height) : 280;
187
- return (_jsxs("figure", { ...props, ref: ref, className: cx('surf-chart', className), "aria-labelledby": `${id}-title`, children: [_jsxs("figcaption", { className: 'surf-chart-caption', children: [_jsx("strong", { id: `${id}-title`, children: title }), _jsx("p", { id: `${id}-summary`, children: description })] }), _jsxs("div", { className: 'surf-chart-frame', children: [_jsxs("div", { className: 'surf-chart-key', children: [_jsxs("p", { className: 'surf-chart-axis-label', children: [yLabel, " (", unit, ")"] }), _jsx("ul", { className: 'surf-chart-legend', "aria-label": text.legend(title), children: series.map((s, index) => (_jsx("li", { children: _jsxs("button", { ref: index === 0 ? firstLegend : undefined, type: 'button', className: 'surf-chart-legend-button', "aria-label": text.series(s.label), "aria-pressed": !hidden.has(s.key), onClick: () => toggle(s.key), children: [_jsx(SeriesMark, { type: type, index: index, color: colorFor(index), pattern: `${id}-legend-${index}` }), _jsx("span", { children: s.label })] }) }, s.key))) })] }), hasValues ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: 'surf-chart-plot', onKeyDownCapture: (event) => {
443
+ const chartHeight = height !== undefined && Number.isFinite(height)
444
+ ? Math.max(compact ? 120 : 200, height)
445
+ : compact
446
+ ? 180
447
+ : 280;
448
+ const total = (row) => visible
449
+ .map((s) => row.values[s.key])
450
+ .filter(isNumber)
451
+ .reduce((a, b) => a + b, 0);
452
+ const referenceText = (r) => 'y' in r
453
+ ? `${r.label} ${format(r.y)}`
454
+ : `${r.label} ${formatNumber(r.from) ?? text.missing}–${format(r.to)}`;
455
+ const legendButton = (s, index) => (_jsxs("button", { ref: index === 0 ? firstLegend : undefined, type: 'button', className: 'surf-chart-legend-button', "aria-label": text.series(s.label), "aria-pressed": !hidden.has(s.key), onClick: () => toggle(s.key), children: [_jsx(SeriesMark, { type: type, index: index, color: colorFor(index), pattern: `${id}-legend-${index}` }), _jsx("span", { children: s.label })] }));
456
+ const limit = legendLimit === 'auto' ? legendFit : legendLimit;
457
+ const shown = limit !== undefined && limit < series.length
458
+ ? series.slice(0, Math.max(0, limit))
459
+ : series;
460
+ const folded = series.slice(shown.length);
461
+ const legend = (_jsxs("ul", { ref: legendList, className: 'surf-chart-legend', "aria-label": text.legend(title), children: [shown.map((s, index) => (_jsx("li", { "data-series": '', children: legendButton(s, index) }, s.key))), folded.length > 0 && (_jsx("li", { children: _jsxs(Popover.Root, { children: [_jsxs(Popover.Trigger, { className: 'surf-chart-legend-button surf-chart-legend-more', "aria-label": text.moreSeries(folded.length), children: ["+", folded.length] }), _jsx(Popover.Content, { size: 'small', children: _jsx("ul", { className: 'surf-chart-legend-list', children: folded.map((s, index) => (_jsx("li", { children: legendButton(s, shown.length + index) }, s.key))) }) })] }) })), references.map((reference, index) => (_jsxs("li", { className: 'surf-chart-legend-reference', children: [_jsx(ReferenceMark, { reference: reference }), _jsx("span", { children: referenceText(reference) })] }, index)))] }));
462
+ return (_jsxs("figure", { ...props, ref: ref, "data-density": density, className: cx('surf-chart', className), "aria-labelledby": `${id}-title`, children: [_jsxs("figcaption", { className: 'surf-chart-caption', children: [_jsx("strong", { id: `${id}-title`, children: title }), _jsx("p", { id: `${id}-summary`, children: description })] }), _jsxs("div", { className: 'surf-chart-frame', ref: setFrame, children: [_jsxs("div", { className: 'surf-chart-key', children: [_jsxs("p", { className: 'surf-chart-axis-label', children: [yLabel, " (", unit, ")"] }), legendPosition === 'top' && legend] }), hasValues ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: 'surf-chart-plot', onKeyDownCapture: (event) => {
188
463
  if (event.key === 'Escape')
189
464
  setDismissed(true);
190
465
  else if (event.key === 'ArrowLeft' ||
191
466
  event.key === 'ArrowRight')
192
467
  setDismissed(false);
193
- }, onMouseMoveCapture: () => setDismissed(false), onTouchStartCapture: () => setDismissed(false), onFocusCapture: () => setDismissed(false), children: [_jsx(ResponsiveContainer, { width: '100%', height: chartHeight, minWidth: 0, initialDimension: { width: 320, height: chartHeight }, children: _jsxs(ComposedChart, { data: rows, accessibilityLayer: true, role: 'application', "aria-label": title, "aria-describedby": `${id}-summary ${id}-keys`, margin: { top: 16, right: 20, bottom: 8, left: 0 }, children: [_jsx("defs", { children: series.map((s, index) => (_jsx(Pattern, { id: `${id}-pattern-${index}`, index: index, color: colorFor(index) }, s.key))) }), _jsx(CartesianGrid, { vertical: false, stroke: 'var(--surf-line)' }), _jsx(XAxis, { dataKey: 'id', tickFormatter: (value) => {
468
+ }, onMouseMoveCapture: () => setDismissed(false), onTouchStartCapture: () => setDismissed(false), onFocusCapture: () => setDismissed(false), children: [_jsx(ResponsiveContainer, { width: '100%', height: chartHeight, minWidth: 0, initialDimension: { width: 320, height: chartHeight }, children: _jsxs(ComposedChart, { data: plotRows, accessibilityLayer: true, role: 'application', "aria-label": title, "aria-describedby": `${id}-summary ${id}-keys`, margin: { top: 16, right: 20, bottom: 8, left: 0 }, baseValue: logScale ? min : undefined, children: [_jsx("defs", { children: series.map((s, index) => (_jsx(Pattern, { id: `${id}-pattern-${index}`, index: index, color: colorFor(index) }, s.key))) }), _jsx(CartesianGrid, { vertical: false, stroke: 'var(--surf-line)' }), references.map((reference, index) => 'y' in reference ? null : (_jsx(ReferenceArea, { y1: reference.from, y2: reference.to, fill: toneColors[reference.tone ?? 'neutral'], fillOpacity: 0.12, stroke: 'none' }, index))), time ? (_jsx(XAxis, { dataKey: 'time', type: 'number', scale: 'time', domain: time.domain, ticks: time.ticks, tickFormatter: (value) => time.format(Number(value)), tick: fontsReady && {
469
+ fill: 'var(--surf-ink-muted)',
470
+ fontSize: 12,
471
+ }, tickLine: false, axisLine: { stroke: 'var(--surf-line)' }, minTickGap: 24, interval: 'preserveStartEnd' })) : (_jsx(XAxis, { dataKey: 'id', tickFormatter: (value) => {
194
472
  const label = byId.get(String(value))?.label ?? String(value);
195
473
  // minTickGap already drops ticks that would overlap; the cap
196
474
  // only keeps one very long label from crowding the axis.
@@ -200,9 +478,9 @@ export const Chart = function Chart({ title, description, data, series, unit, xL
200
478
  }, tick: fontsReady && {
201
479
  fill: 'var(--surf-ink-muted)',
202
480
  fontSize: 12,
203
- }, tickLine: false, axisLine: { stroke: 'var(--surf-line)' }, minTickGap: 24, interval: 'preserveStartEnd' }), _jsx(YAxis, { width: 56, tickFormatter: (value) => formatValue
481
+ }, tickLine: false, axisLine: { stroke: 'var(--surf-line)' }, minTickGap: 24, interval: 'preserveStartEnd' })), _jsx(YAxis, { width: 56, scale: logScale ? 'log' : 'auto', tickFormatter: (value) => formatValue
204
482
  ? formatValue(Number(value))
205
- : compact.format(Number(value)), tick: { fill: 'var(--surf-ink-muted)', fontSize: 12 }, tickLine: false, axisLine: false, domain: [min, max === min ? max + 1 : max], ticks: ticks }), _jsx(ReferenceLine, { y: 0, stroke: 'var(--surf-ink-muted)' }), host && (_jsx(Tooltip, { portal: host, isAnimationActive: false, active: dismissed ? false : undefined, filterNull: false, cursor: dismissed
483
+ : compactNumber.format(Number(value)), tick: { fill: 'var(--surf-ink-muted)', fontSize: 12 }, tickLine: false, axisLine: false, domain: [min, max === min ? max + 1 : max], ticks: ticks, allowDataOverflow: logScale }), !logScale && (_jsx(ReferenceLine, { y: 0, stroke: 'var(--surf-ink-muted)' })), host && (_jsx(Tooltip, { portal: host, isAnimationActive: false, active: dismissed ? false : undefined, filterNull: false, cursor: dismissed
206
484
  ? false
207
485
  : {
208
486
  stroke: 'var(--surf-control-border)',
@@ -213,29 +491,33 @@ export const Chart = function Chart({ title, description, data, series, unit, xL
213
491
  pointerEvents: 'auto',
214
492
  }, content: (tooltip) => {
215
493
  const row = tooltip.active && !dismissed
216
- ? byId.get(String(tooltip.label))
494
+ ? rowFor(tooltip.label)
217
495
  : undefined;
218
- return (_jsx("div", { className: 'surf-chart-readout', "data-chart-readout": row?.id, children: row ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: row.label }), _jsx("ul", { children: visible.map((s) => (_jsxs("li", { children: [s.label, ": ", format(row.values[s.key])] }, s.key))) })] })) : (_jsx("span", { children: text.inspectHint })) }));
496
+ return (_jsx("div", { className: 'surf-chart-readout', "data-chart-readout": row?.id, children: row ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: row.label }), _jsxs("ul", { children: [visible.map((s) => (_jsxs("li", { children: [s.label, ": ", format(row.values[s.key])] }, s.key))), stacks && (_jsxs("li", { children: [text.total, ": ", format(total(row))] }))] })] })) : (_jsx("span", { children: text.inspectHint })) }));
219
497
  } })), series.map((s, index) => {
220
498
  const common = {
221
499
  name: s.label,
222
- dataKey: (row) => row.values[s.key] ?? null,
500
+ dataKey: (row) => valueOf(row, s.key),
223
501
  hide: hidden.has(s.key),
224
502
  isAnimationActive: false,
225
503
  stroke: colorFor(index),
504
+ stackId: stacks ? 'total' : undefined,
226
505
  };
227
- return type === 'bar' ? (_jsx(Bar, { ...common, fill: `url(#${id}-pattern-${index})`, maxBarSize: 48 }, s.key)) : type === 'area' ? (_jsx(Area, { ...common, type: 'linear', strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], fill: `url(#${id}-pattern-${index})`, fillOpacity: 0.12, connectNulls: false, dot: rows.length > 40
228
- ? false
229
- : (dot) => (_jsx(Marker, { ...dot, color: colorFor(index), index: index })), activeDot: false }, s.key)) : (_jsx(Line, { ...common, type: 'linear', strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], connectNulls: false, dot: rows.length > 40
230
- ? false
231
- : (dot) => (_jsx(Marker, { ...dot, color: colorFor(index), index: index })), activeDot: false }, s.key));
232
- }), _jsx(FloatingValues, { host: floatHost, hidden: dismissed, render: (label) => {
233
- const row = byId.get(label);
506
+ const dot = rows.length > 40
507
+ ? false
508
+ : (dot) => (_jsx(Marker, { cx: dot.cx, cy: dot.cy, color: colorFor(index), index: index }));
509
+ const shape = curve === 'step' ? 'stepAfter' : 'linear';
510
+ return type === 'bar' ? (_jsx(Bar, { ...common, fill: `url(#${id}-pattern-${index})`, maxBarSize: 48 }, s.key)) : type === 'area' ? (_jsx(Area, { ...common, type: shape, strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], fill: `url(#${id}-pattern-${index})`, fillOpacity: stacks ? 0.32 : 0.12, connectNulls: false, dot: dot, activeDot: false }, s.key)) : (_jsx(Line, { ...common, type: shape, strokeWidth: 2, strokeDasharray: dashes[index % dashes.length], connectNulls: false, dot: dot, activeDot: false }, s.key));
511
+ }), references.map((reference, index) => 'y' in reference ? (_jsx(ReferenceLine, { y: reference.y, stroke: toneColors[reference.tone ?? 'neutral'], strokeWidth: 1.5, strokeDasharray: '4 3' }, index)) : null), _jsx(FloatingValues, { host: floatHost, hidden: dismissed, render: (label) => {
512
+ const row = rowFor(label);
234
513
  if (!row)
235
514
  return null;
236
- return (_jsxs(_Fragment, { children: [_jsx("strong", { children: row.label }), _jsx("ul", { children: series.map((s, index) => hidden.has(s.key) ? null : (_jsxs("li", { children: [_jsx(SeriesMark, { type: type, index: index, color: colorFor(index), pattern: `${id}-float-${index}` }), _jsx("span", { children: s.label }), _jsx("span", { className: 'surf-chart-tooltip-value', children: format(row.values[s.key]) })] }, s.key))) })] }));
515
+ return (_jsxs(_Fragment, { children: [_jsx("strong", { children: row.label }), _jsxs("ul", { children: [series.map((s, index) => hidden.has(s.key) ? null : (_jsxs("li", { children: [_jsx(SeriesMark, { type: type, index: index, color: colorFor(index), pattern: `${id}-float-${index}` }), _jsx("span", { children: s.label }), _jsx("span", { className: 'surf-chart-tooltip-value', children: format(row.values[s.key]) })] }, s.key))), stacks && (_jsxs("li", { className: 'surf-chart-tooltip-total', children: [_jsx("span", { children: text.total }), _jsx("span", { className: 'surf-chart-tooltip-value', children: format(total(row)) })] }))] })] }));
237
516
  } })] }) }), _jsx("div", { className: 'surf-chart-float', ref: setFloatHost })] }), _jsx("p", { className: 'surf-chart-axis-label surf-chart-x-label', children: xLabel })] })) : (_jsx("div", { className: 'surf-chart-empty', role: 'status', children: visible.length === 0 && series.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("p", { children: text.noSeries }), _jsx(Button, { variant: 'outline', onClick: () => {
238
517
  setRestoreRequested(true);
239
518
  changeHidden([]);
240
- }, children: text.showAll })] })) : (_jsx("p", { children: text.empty })) }))] }), hasValues && (_jsx("p", { className: 'surf-chart-help', id: `${id}-keys`, children: text.keyboardHelp })), _jsx("div", { className: 'surf-chart-inspector', ref: setHost, role: 'status', "aria-live": 'polite', "aria-atomic": 'true', "aria-label": text.values(title), hidden: !hasValues }), _jsxs("details", { className: 'surf-chart-table', children: [_jsx("summary", { children: text.viewTable }), _jsx(Table.Container, { label: text.table(title), orientation: 'both', scrollChain: 'contain', children: _jsxs(Table.Root, { children: [_jsx(Table.Caption, { children: text.tableCaption(unit) }), _jsx(Table.Head, { children: _jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'col', children: xLabel }), series.map((s) => (_jsxs(Table.HeaderCell, { scope: 'col', children: [s.label, " (", unit, ")"] }, s.key)))] }) }), _jsx(Table.Body, { children: rows.length ? (rows.map((row) => (_jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'row', children: row.label }), series.map((s) => (_jsx(Table.Cell, { children: format(row.values[s.key]) }, s.key)))] }, row.id)))) : (_jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: series.length + 1, children: text.noRows }) })) })] }) })] })] }));
519
+ }, children: text.showAll })] })) : (_jsx("p", { children: text.empty })) })), legendPosition === 'bottom' && (_jsx("div", { className: 'surf-chart-key', "data-position": 'bottom', children: legend }))] }), hasValues && (_jsx("p", { className: 'surf-chart-help', id: `${id}-keys`, children: text.keyboardHelp })), _jsx("div", { className: 'surf-chart-inspector', ref: setHost, role: 'status', "aria-live": 'polite', "aria-atomic": 'true', "aria-label": text.values(title), hidden: !hasValues }), _jsxs("details", { className: 'surf-chart-table', onToggle: (event) => {
520
+ if (event.currentTarget.open)
521
+ setTableOpened(true);
522
+ }, children: [_jsx("summary", { children: text.viewTable }), tableOpened && (_jsx(Table.Container, { label: text.table(title), orientation: 'both', scrollChain: 'contain', children: _jsxs(Table.Root, { density: density, children: [_jsx(Table.Caption, { children: text.tableCaption(unit) }), _jsx(Table.Head, { children: _jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'col', children: xLabel }), series.map((s) => (_jsxs(Table.HeaderCell, { scope: 'col', numeric: true, children: [s.label, " (", unit, ")"] }, s.key)))] }) }), _jsx(Table.Body, { children: rows.length ? (rows.map((row) => (_jsxs(Table.Row, { children: [_jsx(Table.HeaderCell, { scope: 'row', children: row.label }), series.map((s) => (_jsx(Table.Cell, { numeric: true, children: formatNumber(row.values[s.key]) ?? text.missing }, s.key)))] }, row.id)))) : (_jsx(Table.Row, { children: _jsx(Table.Cell, { colSpan: series.length + 1, children: text.noRows }) })) })] }) }))] })] }));
241
523
  };
package/dist/choice.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
- import { type ComponentSizeAlias } from './sizing.js';
2
+ import { type ComponentSize } from './sizing.js';
3
3
  export declare const ChoiceResetPaint: React.Context<number>;
4
4
  /** Choice controls follow the surrounding size; `size` sets one control's scale. */
5
5
  export type ChoiceSize = {
6
- size?: ComponentSizeAlias;
6
+ size?: ComponentSize;
7
7
  };
@@ -1,6 +1,2 @@
1
1
  /** Joins class names, skipping empty ones. */
2
2
  export declare function cx(...classes: (string | false | null | undefined)[]): string;
3
- /** The data-size value: a deprecated short spelling (sm, md, lg, icon-sm, icon-xs,
4
- * icon-lg) renders as its full name, so both spellings meet the same CSS; other values
5
- * such as xs or icon pass through. */
6
- export declare function sizeName<Size extends string>(size?: Size): import("./size.js").ComponentSize | Size | "icon-small" | "icon-large" | undefined;
@@ -1,18 +1,4 @@
1
- // Class and data attribute helpers, with no React: server components use them too.
2
- import { normalizeSize } from './size.js';
3
1
  /** Joins class names, skipping empty ones. */
4
2
  export function cx(...classes) {
5
3
  return classes.filter(Boolean).join(' ');
6
4
  }
7
- /** The data-size value: a deprecated short spelling (sm, md, lg, icon-sm, icon-xs,
8
- * icon-lg) renders as its full name, so both spellings meet the same CSS; other values
9
- * such as xs or icon pass through. */
10
- export function sizeName(size) {
11
- return size === 'sm' || size === 'md' || size === 'lg'
12
- ? normalizeSize(size)
13
- : size === 'icon-sm' || size === 'icon-xs'
14
- ? 'icon-small'
15
- : size === 'icon-lg'
16
- ? 'icon-large'
17
- : size;
18
- }
package/dist/code.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as React from 'react';
2
+ import { Button } from './button.js';
2
3
  export type CodeBlockProps = Omit<React.ComponentPropsWithoutRef<'div'>, 'children' | 'onCopy'> & {
3
4
  code: string;
4
5
  label?: string;
@@ -6,12 +7,6 @@ export type CodeBlockProps = Omit<React.ComponentPropsWithoutRef<'div'>, 'childr
6
7
  wrap?: boolean;
7
8
  /** The copy button's text: the action, then the result. */
8
9
  labels?: Partial<CodeBlockLabels>;
9
- /** @deprecated Use `labels.copy`. Removed in a later minor. */
10
- copyLabel?: string;
11
- /** @deprecated Use `labels.copied`. Removed in a later minor. */
12
- copiedLabel?: string;
13
- /** @deprecated Use `labels.copyError`. Removed in a later minor. */
14
- copyErrorLabel?: string;
15
10
  onCopy?: (code: string) => void | Promise<void>;
16
11
  };
17
12
  export type CodeBlockLabels = {
@@ -19,6 +14,23 @@ export type CodeBlockLabels = {
19
14
  copied: string;
20
15
  copyError: string;
21
16
  };
22
- export declare const CodeBlock: ({ code, label, language, wrap, labels, copyLabel: legacyCopyLabel, copiedLabel: legacyCopiedLabel, copyErrorLabel: legacyCopyErrorLabel, onCopy, className, ref, ...props }: CodeBlockProps & {
17
+ export type CopyButtonLabels = CodeBlockLabels;
18
+ export type CopyButtonProps = Omit<React.ComponentPropsWithoutRef<typeof Button>, 'children' | 'onClick' | 'onCopy' | 'value'> & {
19
+ /** What to copy: the text, or a function that returns it when pressed. */
20
+ value: string | (() => string | Promise<string>);
21
+ /** The button's text: the action, then the result. */
22
+ labels?: Partial<CopyButtonLabels>;
23
+ /** Copies instead of the clipboard write; throw to report a failure. */
24
+ onCopy?: (text: string) => void | Promise<void>;
25
+ /** Hears each outcome, such as to select the source after a failure. */
26
+ onResult?: (result: 'copied' | 'failed') => void;
27
+ };
28
+ /** Copies text and says what happened in its own label: the result replaces the
29
+ * action in place, as a polite live region, then the action returns after 2s.
30
+ * The button is as wide as its longest label, so nothing around it moves. */
31
+ export declare const CopyButton: ({ value, labels, onCopy, onResult, variant, size, className, ref, ...props }: CopyButtonProps & {
32
+ ref?: React.Ref<HTMLButtonElement>;
33
+ }) => React.JSX.Element;
34
+ export declare const CodeBlock: ({ code, label, language, wrap, labels, onCopy, className, ref, ...props }: CodeBlockProps & {
23
35
  ref?: React.Ref<HTMLDivElement>;
24
36
  }) => React.JSX.Element;