@glyphui/ui 0.2.0 → 0.4.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 (116) hide show
  1. package/README.md +65 -12
  2. package/dist/chart.d.ts +202 -0
  3. package/dist/chart.js +466 -0
  4. package/dist/color-mode-script-Bf7LYkOj.js +13 -0
  5. package/dist/color-mode-script-KdwItKN2.d.ts +16 -0
  6. package/dist/data-table.js +3 -2
  7. package/dist/{field-CCecIzdq.js → field-CVbywksE.js} +21 -5
  8. package/dist/{field-context-B6WOcK5m.js → field-context-7I-cWvvS.js} +3 -57
  9. package/dist/form.d.ts +11 -4
  10. package/dist/form.js +34 -8
  11. package/dist/glyph-context-DxGJrdvr.d.ts +50 -0
  12. package/dist/{index-BWK4czIG.d.ts → index-Bi7rZ6ba.d.ts} +11 -0
  13. package/dist/index.d.ts +2334 -283
  14. package/dist/index.js +2952 -326
  15. package/dist/react-router.d.ts +18 -0
  16. package/dist/react-router.js +44 -0
  17. package/dist/{states-Rp8wZ_jH.js → states-IvhjBgna.js} +2 -1
  18. package/dist/tv-DLbT66dC.js +70 -0
  19. package/dist/vite.d.ts +27 -0
  20. package/dist/vite.js +29 -0
  21. package/fonts.css +10 -0
  22. package/package.json +47 -5
  23. package/src/components/accordion/accordion.styles.ts +4 -4
  24. package/src/components/badge/badge.styles.ts +11 -16
  25. package/src/components/banner/banner.styles.ts +4 -4
  26. package/src/components/button/button.styles.ts +3 -1
  27. package/src/components/calendar/calendar.styles.ts +51 -0
  28. package/src/components/calendar/calendar.tsx +84 -0
  29. package/src/components/calendar/index.ts +3 -0
  30. package/src/components/carousel/carousel.styles.ts +43 -0
  31. package/src/components/carousel/carousel.tsx +278 -0
  32. package/src/components/carousel/index.ts +13 -0
  33. package/src/components/collapsible/collapsible.tsx +66 -0
  34. package/src/components/collapsible/index.ts +6 -0
  35. package/src/components/context-menu/context-menu.styles.ts +19 -0
  36. package/src/components/context-menu/context-menu.tsx +226 -0
  37. package/src/components/context-menu/index.ts +19 -0
  38. package/src/components/copy-button/copy-button.tsx +117 -0
  39. package/src/components/copy-button/index.ts +1 -0
  40. package/src/components/date-picker/date-picker.styles.ts +42 -0
  41. package/src/components/date-picker/date-picker.tsx +322 -0
  42. package/src/components/date-picker/index.ts +8 -0
  43. package/src/components/dropdown-menu/dropdown-menu.styles.ts +2 -2
  44. package/src/components/eyebrow/eyebrow.styles.ts +1 -1
  45. package/src/components/field/field-context.ts +2 -0
  46. package/src/components/field/field.styles.ts +4 -2
  47. package/src/components/field/field.tsx +25 -4
  48. package/src/components/file-upload/file-upload.styles.ts +43 -0
  49. package/src/components/file-upload/file-upload.tsx +343 -0
  50. package/src/components/file-upload/index.ts +10 -0
  51. package/src/components/hover-card/hover-card.tsx +41 -0
  52. package/src/components/hover-card/index.ts +1 -0
  53. package/src/components/input-otp/index.ts +10 -0
  54. package/src/components/input-otp/input-otp.styles.ts +38 -0
  55. package/src/components/input-otp/input-otp.tsx +116 -0
  56. package/src/components/link/link.styles.ts +1 -1
  57. package/src/components/modal/index.ts +2 -0
  58. package/src/components/modal/modal.styles.ts +20 -0
  59. package/src/components/modal/modal.tsx +111 -0
  60. package/src/components/navigation-menu/index.ts +14 -0
  61. package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
  62. package/src/components/navigation-menu/navigation-menu.tsx +208 -0
  63. package/src/components/number-input/index.ts +2 -0
  64. package/src/components/number-input/number-input.styles.ts +41 -0
  65. package/src/components/number-input/number-input.tsx +211 -0
  66. package/src/components/password-input/index.ts +7 -0
  67. package/src/components/password-input/password-input.styles.ts +30 -0
  68. package/src/components/password-input/password-input.tsx +127 -0
  69. package/src/components/prose/prose.tsx +1 -1
  70. package/src/components/resizable/index.ts +9 -0
  71. package/src/components/resizable/resizable.styles.ts +28 -0
  72. package/src/components/resizable/resizable.tsx +69 -0
  73. package/src/components/scroll-area/scroll-area.tsx +23 -1
  74. package/src/components/segmented-control/segmented-control.tsx +17 -2
  75. package/src/components/sheet/sheet.styles.ts +7 -7
  76. package/src/components/slider/slider.tsx +9 -2
  77. package/src/components/stepper/index.ts +2 -0
  78. package/src/components/stepper/stepper.styles.ts +74 -0
  79. package/src/components/stepper/stepper.tsx +138 -0
  80. package/src/components/tag-input/index.ts +2 -0
  81. package/src/components/tag-input/tag-input.styles.ts +39 -0
  82. package/src/components/tag-input/tag-input.tsx +198 -0
  83. package/src/components/text/text.styles.ts +4 -4
  84. package/src/components/timeline/index.ts +8 -0
  85. package/src/components/timeline/timeline.styles.ts +56 -0
  86. package/src/components/timeline/timeline.tsx +91 -0
  87. package/src/components/toggle/index.ts +9 -0
  88. package/src/components/toggle/toggle.styles.ts +39 -0
  89. package/src/components/toggle/toggle.tsx +61 -0
  90. package/src/components/tree-view/index.ts +2 -0
  91. package/src/components/tree-view/tree-view.styles.ts +42 -0
  92. package/src/components/tree-view/tree-view.tsx +274 -0
  93. package/src/components/visually-hidden/index.ts +1 -0
  94. package/src/components/visually-hidden/visually-hidden.tsx +10 -0
  95. package/src/index.ts +20 -0
  96. package/src/integrations/react-router.tsx +39 -0
  97. package/src/integrations/vite.test-d.ts +6 -0
  98. package/src/integrations/vite.test.ts +28 -0
  99. package/src/integrations/vite.ts +33 -0
  100. package/src/internal/dev.ts +12 -0
  101. package/src/patterns/app-shell/app-shell.styles.ts +1 -1
  102. package/src/patterns/app-shell/navbar.tsx +34 -16
  103. package/src/patterns/app-shell/sidebar.tsx +7 -2
  104. package/src/patterns/chart/chart-parts.tsx +179 -0
  105. package/src/patterns/chart/chart.styles.ts +35 -0
  106. package/src/patterns/chart/chart.tsx +386 -0
  107. package/src/patterns/chart/index.ts +13 -0
  108. package/src/patterns/form/form.tsx +48 -6
  109. package/src/patterns/form/index.ts +1 -0
  110. package/src/provider/color-mode-script.ts +28 -0
  111. package/src/provider/color-mode.tsx +14 -20
  112. package/src/provider/glyph-context.ts +38 -0
  113. package/src/provider/glyph-provider.tsx +16 -4
  114. package/src/provider/index.ts +4 -0
  115. package/src/provider/theme-scope.tsx +7 -3
  116. package/styles.css +88 -0
@@ -0,0 +1,179 @@
1
+ import { type ReactNode, useId } from 'react'
2
+ import { useComponentConfig } from '../../hooks/use-component-config'
3
+ import { chartStyles } from './chart.styles'
4
+
5
+ /** Theme colors a series can use by name. Any CSS color also works. */
6
+ export type ChartColor =
7
+ | 'chart-1'
8
+ | 'chart-2'
9
+ | 'chart-3'
10
+ | 'chart-4'
11
+ | 'chart-5'
12
+ | 'primary'
13
+ | 'info'
14
+ | 'success'
15
+ | 'warning'
16
+ | 'danger'
17
+ | 'foreground'
18
+ | 'muted-foreground'
19
+
20
+ /** Series without a color use the theme's chart palette, in order. */
21
+ const palette: ChartColor[] = ['chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5']
22
+
23
+ export function resolveColor(color: string | undefined, index: number): string {
24
+ const value = color ?? palette[index % palette.length]!
25
+ return /^[a-z][a-z\d-]*$/.test(value) ? `var(--color-${value})` : value
26
+ }
27
+
28
+ export const defaultFormatter = (value: number) =>
29
+ typeof value === 'number' ? value.toLocaleString() : String(value)
30
+
31
+ export function useChartParts() {
32
+ const [, slots] = useComponentConfig('Chart', {})
33
+ return { s: chartStyles(), slots }
34
+ }
35
+
36
+ export interface LegendEntry {
37
+ label: ReactNode
38
+ color: string
39
+ }
40
+
41
+ export function ChartLegend({ items }: { items: LegendEntry[] }) {
42
+ const { s, slots } = useChartParts()
43
+ return (
44
+ <ul data-slot="chart-legend" className={s.legend({ class: slots.legend })} aria-hidden="true">
45
+ {items.map((item, i) => (
46
+ <li key={i} className={s.legendItem({ class: slots.legendItem })}>
47
+ <span className={s.swatch({ class: slots.swatch })} style={{ background: item.color }} />
48
+ {item.label}
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ )
53
+ }
54
+
55
+ interface TooltipPayloadItem {
56
+ name?: string | number
57
+ value?: unknown
58
+ color?: string
59
+ dataKey?: unknown
60
+ payload?: Record<string, unknown> & { fill?: string }
61
+ }
62
+
63
+ export function ChartTooltipCard({
64
+ active,
65
+ payload,
66
+ label,
67
+ formatter,
68
+ labelFormatter,
69
+ }: {
70
+ active?: boolean
71
+ payload?: readonly TooltipPayloadItem[]
72
+ label?: ReactNode
73
+ formatter: (value: number) => string
74
+ labelFormatter?: ((label: ReactNode) => ReactNode) | undefined
75
+ }) {
76
+ const { s, slots } = useChartParts()
77
+ if (!active || !payload?.length) return null
78
+ return (
79
+ <div data-slot="chart-tooltip" className={s.tooltip({ class: slots.tooltip })}>
80
+ {label !== undefined && label !== '' && (
81
+ <p className={s.tooltipLabel({ class: slots.tooltipLabel })}>
82
+ {labelFormatter ? labelFormatter(label) : label}
83
+ </p>
84
+ )}
85
+ {payload.map((item, i) => (
86
+ <div key={i} className={s.tooltipRow({ class: slots.tooltipRow })}>
87
+ <span
88
+ className={s.swatch({ class: slots.swatch })}
89
+ style={{ background: item.color ?? item.payload?.fill }}
90
+ />
91
+ <span className={s.tooltipName({ class: slots.tooltipName })}>
92
+ {String(item.name ?? '')}
93
+ </span>
94
+ <span className={s.tooltipValue({ class: slots.tooltipValue })}>
95
+ {formatter(Number(item.value))}
96
+ </span>
97
+ </div>
98
+ ))}
99
+ </div>
100
+ )
101
+ }
102
+
103
+ export interface ChartFrameProps {
104
+ /** Accessible name of the chart. Shown above it unless `hideTitle` is set. */
105
+ title: string
106
+ description?: ReactNode
107
+ hideTitle?: boolean
108
+ className?: string
109
+ legend?: LegendEntry[] | undefined
110
+ /** Columns and rows for the screen-reader table that mirrors the chart. */
111
+ table: { columns: string[]; rows: (string | number)[][] }
112
+ children: ReactNode
113
+ }
114
+
115
+ /**
116
+ * Wraps every chart in a `<figure>` with a caption and a visually hidden data table,
117
+ * so screen-reader users get the numbers the chart shows.
118
+ */
119
+ export function ChartFrame({
120
+ title,
121
+ description,
122
+ hideTitle,
123
+ className,
124
+ legend,
125
+ table,
126
+ children,
127
+ }: ChartFrameProps) {
128
+ const { s, slots } = useChartParts()
129
+ const titleId = useId()
130
+ return (
131
+ <figure
132
+ data-slot="chart"
133
+ aria-labelledby={titleId}
134
+ className={s.root({ class: [slots.root, className] })}
135
+ >
136
+ <figcaption className={hideTitle ? s.srOnly() : s.caption({ class: slots.caption })}>
137
+ <span id={titleId} className={s.title({ class: slots.title })}>
138
+ {title}
139
+ </span>
140
+ {description && (
141
+ <span className={s.description({ class: slots.description })}>{description}</span>
142
+ )}
143
+ </figcaption>
144
+ {/* Screen readers get the data table below. Charts pass `accessibilityLayer={false}`
145
+ so recharts adds no focusable nodes inside this hidden plot. */}
146
+ <div className={s.plot({ class: slots.plot })} aria-hidden="true">
147
+ {children}
148
+ </div>
149
+ {legend && legend.length > 1 && <ChartLegend items={legend} />}
150
+ <table className={s.srOnly()}>
151
+ <caption>{title}</caption>
152
+ <thead>
153
+ <tr>
154
+ {table.columns.map((column) => (
155
+ <th key={column} scope="col">
156
+ {column}
157
+ </th>
158
+ ))}
159
+ </tr>
160
+ </thead>
161
+ <tbody>
162
+ {table.rows.map((row, i) => (
163
+ <tr key={i}>
164
+ {row.map((cell, j) =>
165
+ j === 0 ? (
166
+ <th key={j} scope="row">
167
+ {cell}
168
+ </th>
169
+ ) : (
170
+ <td key={j}>{cell}</td>
171
+ ),
172
+ )}
173
+ </tr>
174
+ ))}
175
+ </tbody>
176
+ </table>
177
+ </figure>
178
+ )
179
+ }
@@ -0,0 +1,35 @@
1
+ import type { StylesEntry } from '../../utils/registry'
2
+ import { themedBorder } from '../../utils/styles'
3
+ import { tv, type VariantProps } from '../../utils/tv'
4
+
5
+ export const chartStyles = tv({
6
+ slots: {
7
+ root: 'm-0 grid min-w-0 gap-3',
8
+ caption: 'grid gap-0.5',
9
+ title: 'text-small font-medium text-foreground',
10
+ description: 'text-caption text-muted-foreground',
11
+ plot: 'min-w-0 [&_.recharts-surface]:overflow-visible [&_.recharts-text]:fill-muted-foreground',
12
+ legend: 'flex flex-wrap items-center gap-x-4 gap-y-1 text-caption text-muted-foreground',
13
+ legendItem: 'inline-flex items-center gap-1.5',
14
+ swatch: 'size-2.5 shrink-0 rounded-[2px]',
15
+ tooltip: [
16
+ 'grid min-w-36 gap-1.5 rounded-md border-border bg-raised px-3 py-2 text-caption text-foreground shadow-lg',
17
+ themedBorder,
18
+ ],
19
+ tooltipLabel: 'font-medium text-foreground',
20
+ tooltipRow: 'flex items-center gap-2',
21
+ tooltipName: 'flex-1 text-muted-foreground',
22
+ tooltipValue: 'font-mono font-medium tabular-nums text-foreground',
23
+ center:
24
+ 'pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center',
25
+ srOnly: 'sr-only',
26
+ },
27
+ })
28
+
29
+ export type ChartVariants = VariantProps<typeof chartStyles>
30
+
31
+ declare module '@glyphui/tokens' {
32
+ interface GlyphComponentRegistry {
33
+ Chart: StylesEntry<typeof chartStyles>
34
+ }
35
+ }
@@ -0,0 +1,386 @@
1
+ import { type ReactNode, useId } from 'react'
2
+ import * as R from 'recharts'
3
+ import {
4
+ type ChartColor,
5
+ ChartFrame,
6
+ ChartTooltipCard,
7
+ defaultFormatter,
8
+ resolveColor,
9
+ useChartParts,
10
+ } from './chart-parts'
11
+
12
+ export type { ChartColor }
13
+
14
+ type Key<T> = Extract<keyof T, string>
15
+
16
+ export interface ChartSeries<T> {
17
+ /** Field of each data row holding this series' value. */
18
+ key: Key<T>
19
+ /** Name shown in the legend, tooltip and accessible table. Defaults to `key`. */
20
+ label?: string
21
+ /** A theme color name (`primary`, `info`, ...) or any CSS color. Defaults follow the theme palette. */
22
+ color?: ChartColor | (string & {})
23
+ }
24
+
25
+ export interface CartesianChartProps<T extends object> {
26
+ data: readonly T[]
27
+ /** Field used for the x axis, e.g. `"month"`. */
28
+ index: Key<T>
29
+ series: readonly ChartSeries<T>[]
30
+ /** Accessible name; also shown as the caption unless `hideTitle` is set. */
31
+ title: string
32
+ description?: ReactNode
33
+ hideTitle?: boolean
34
+ /** Formats values on the y axis, in tooltips and in the accessible table. */
35
+ valueFormatter?: (value: number) => string
36
+ /** Plot height in pixels. Default 240. */
37
+ height?: number
38
+ showGrid?: boolean
39
+ /** Defaults to true when there is more than one series. */
40
+ showLegend?: boolean
41
+ showYAxis?: boolean
42
+ className?: string
43
+ }
44
+
45
+ const axisTick = { fill: 'var(--color-muted-foreground)', fontSize: 12 }
46
+
47
+ function useCartesian<T extends object>(props: CartesianChartProps<T>) {
48
+ const { data, index, series, title, valueFormatter = defaultFormatter } = props
49
+ const resolved = series.map((item, i) => ({
50
+ ...item,
51
+ label: item.label ?? item.key,
52
+ stroke: resolveColor(item.color, i),
53
+ }))
54
+ const table = {
55
+ columns: [String(index), ...resolved.map((item) => item.label)],
56
+ rows: data.map((row) => [
57
+ String((row as Record<string, unknown>)[index]),
58
+ ...resolved.map((item) => valueFormatter(Number((row as Record<string, unknown>)[item.key]))),
59
+ ]),
60
+ }
61
+ const legend =
62
+ (props.showLegend ?? series.length > 1)
63
+ ? resolved.map((item) => ({ label: item.label, color: item.stroke }))
64
+ : undefined
65
+ return { resolved, table, legend, valueFormatter, title }
66
+ }
67
+
68
+ function frameProps<T extends object>(props: CartesianChartProps<T>) {
69
+ return {
70
+ title: props.title,
71
+ description: props.description,
72
+ hideTitle: props.hideTitle,
73
+ className: props.className,
74
+ }
75
+ }
76
+
77
+ function Axes({
78
+ index,
79
+ showGrid = true,
80
+ showYAxis = true,
81
+ valueFormatter,
82
+ }: {
83
+ index: string
84
+ showGrid?: boolean | undefined
85
+ showYAxis?: boolean | undefined
86
+ valueFormatter: (value: number) => string
87
+ }) {
88
+ return (
89
+ <>
90
+ {showGrid && (
91
+ <R.CartesianGrid vertical={false} stroke="var(--color-border)" strokeDasharray="3 3" />
92
+ )}
93
+ <R.XAxis
94
+ dataKey={index}
95
+ tick={axisTick}
96
+ tickLine={false}
97
+ axisLine={false}
98
+ tickMargin={8}
99
+ minTickGap={16}
100
+ />
101
+ {showYAxis && (
102
+ <R.YAxis
103
+ tick={axisTick}
104
+ tickLine={false}
105
+ axisLine={false}
106
+ width={48}
107
+ tickFormatter={valueFormatter}
108
+ />
109
+ )}
110
+ </>
111
+ )
112
+ }
113
+
114
+ function tooltip(valueFormatter: (value: number) => string, cursor: object | boolean) {
115
+ return (
116
+ <R.Tooltip
117
+ cursor={cursor}
118
+ content={(p) => (
119
+ <ChartTooltipCard
120
+ active={p.active}
121
+ payload={p.payload as never}
122
+ label={p.label as ReactNode}
123
+ formatter={valueFormatter}
124
+ />
125
+ )}
126
+ />
127
+ )
128
+ }
129
+
130
+ /** Trends over time, one line per series. */
131
+ export function LineChart<T extends object>(
132
+ props: CartesianChartProps<T> & { curve?: 'monotone' | 'linear' },
133
+ ) {
134
+ const { resolved, table, legend, valueFormatter } = useCartesian(props)
135
+ return (
136
+ <ChartFrame {...frameProps(props)} legend={legend} table={table}>
137
+ <R.ResponsiveContainer width="100%" height={props.height ?? 240}>
138
+ <R.LineChart
139
+ accessibilityLayer={false}
140
+ data={props.data as T[]}
141
+ margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
142
+ >
143
+ <Axes
144
+ index={props.index}
145
+ showGrid={props.showGrid}
146
+ showYAxis={props.showYAxis}
147
+ valueFormatter={valueFormatter}
148
+ />
149
+ {tooltip(valueFormatter, { stroke: 'var(--color-border-strong)' })}
150
+ {resolved.map((item) => (
151
+ <R.Line
152
+ key={item.key}
153
+ dataKey={item.key}
154
+ name={item.label}
155
+ type={props.curve ?? 'monotone'}
156
+ stroke={item.stroke}
157
+ strokeWidth={2}
158
+ dot={false}
159
+ activeDot={{ r: 4, strokeWidth: 0 }}
160
+ isAnimationActive={false}
161
+ />
162
+ ))}
163
+ </R.LineChart>
164
+ </R.ResponsiveContainer>
165
+ </ChartFrame>
166
+ )
167
+ }
168
+
169
+ /** Trends with filled areas. `stacked` shows how series add up to a total. */
170
+ export function AreaChart<T extends object>(props: CartesianChartProps<T> & { stacked?: boolean }) {
171
+ const { resolved, table, legend, valueFormatter } = useCartesian(props)
172
+ const id = useId().replace(/:/g, '')
173
+ return (
174
+ <ChartFrame {...frameProps(props)} legend={legend} table={table}>
175
+ <R.ResponsiveContainer width="100%" height={props.height ?? 240}>
176
+ <R.AreaChart
177
+ accessibilityLayer={false}
178
+ data={props.data as T[]}
179
+ margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
180
+ >
181
+ <defs>
182
+ {resolved.map((item, i) => (
183
+ <linearGradient key={item.key} id={`${id}-${i}`} x1="0" x2="0" y1="0" y2="1">
184
+ <stop offset="0%" stopColor={item.stroke} stopOpacity={0.3} />
185
+ <stop offset="100%" stopColor={item.stroke} stopOpacity={0} />
186
+ </linearGradient>
187
+ ))}
188
+ </defs>
189
+ <Axes
190
+ index={props.index}
191
+ showGrid={props.showGrid}
192
+ showYAxis={props.showYAxis}
193
+ valueFormatter={valueFormatter}
194
+ />
195
+ {tooltip(valueFormatter, { stroke: 'var(--color-border-strong)' })}
196
+ {resolved.map((item, i) => (
197
+ <R.Area
198
+ key={item.key}
199
+ dataKey={item.key}
200
+ name={item.label}
201
+ type="monotone"
202
+ stroke={item.stroke}
203
+ strokeWidth={2}
204
+ fill={`url(#${id}-${i})`}
205
+ {...(props.stacked && { stackId: 'stack' })}
206
+ isAnimationActive={false}
207
+ />
208
+ ))}
209
+ </R.AreaChart>
210
+ </R.ResponsiveContainer>
211
+ </ChartFrame>
212
+ )
213
+ }
214
+
215
+ /** Compare values across categories. `stacked` puts series on top of each other. */
216
+ export function BarChart<T extends object>(
217
+ props: CartesianChartProps<T> & {
218
+ stacked?: boolean /** Corner radius of bar tops. Default 3. */
219
+ barRadius?: number
220
+ },
221
+ ) {
222
+ const { resolved, table, legend, valueFormatter } = useCartesian(props)
223
+ const radius = props.barRadius ?? 3
224
+ return (
225
+ <ChartFrame {...frameProps(props)} legend={legend} table={table}>
226
+ <R.ResponsiveContainer width="100%" height={props.height ?? 240}>
227
+ <R.BarChart
228
+ accessibilityLayer={false}
229
+ data={props.data as T[]}
230
+ margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
231
+ >
232
+ <Axes
233
+ index={props.index}
234
+ showGrid={props.showGrid}
235
+ showYAxis={props.showYAxis}
236
+ valueFormatter={valueFormatter}
237
+ />
238
+ {tooltip(valueFormatter, { fill: 'var(--color-muted)', opacity: 0.6 })}
239
+ {resolved.map((item, i) => (
240
+ <R.Bar
241
+ key={item.key}
242
+ dataKey={item.key}
243
+ name={item.label}
244
+ fill={item.stroke}
245
+ radius={props.stacked && i < resolved.length - 1 ? 0 : [radius, radius, 0, 0]}
246
+ {...(props.stacked && { stackId: 'stack' })}
247
+ isAnimationActive={false}
248
+ />
249
+ ))}
250
+ </R.BarChart>
251
+ </R.ResponsiveContainer>
252
+ </ChartFrame>
253
+ )
254
+ }
255
+
256
+ export interface DonutChartProps<T extends object> {
257
+ data: readonly T[]
258
+ /** Field naming each slice. */
259
+ category: Key<T>
260
+ /** Field holding each slice's value. */
261
+ value: Key<T>
262
+ title: string
263
+ description?: ReactNode
264
+ hideTitle?: boolean
265
+ /** One color per slice, in order. Defaults follow the theme palette. */
266
+ colors?: readonly (ChartColor | (string & {}))[]
267
+ /** Content in the hole, e.g. the total. */
268
+ centerLabel?: ReactNode
269
+ valueFormatter?: (value: number) => string
270
+ /** Diameter in pixels. Default 200. */
271
+ size?: number
272
+ showLegend?: boolean
273
+ className?: string
274
+ }
275
+
276
+ /** Parts of a whole. Best with five or fewer slices. */
277
+ export function DonutChart<T extends object>(props: DonutChartProps<T>) {
278
+ const { data, category, value, valueFormatter = defaultFormatter, size = 200 } = props
279
+ const { s, slots } = useChartParts()
280
+ const rows = data.map((row) => row as Record<string, unknown>)
281
+ const colors = rows.map((_, i) => resolveColor(props.colors?.[i], i))
282
+ const table = {
283
+ columns: [String(category), String(value)],
284
+ rows: rows.map((row) => [String(row[category]), valueFormatter(Number(row[value]))]),
285
+ }
286
+ const legend =
287
+ (props.showLegend ?? true)
288
+ ? rows.map((row, i) => ({ label: String(row[category]), color: colors[i]! }))
289
+ : undefined
290
+ return (
291
+ <ChartFrame
292
+ title={props.title}
293
+ description={props.description}
294
+ hideTitle={props.hideTitle}
295
+ className={props.className}
296
+ legend={legend}
297
+ table={table}
298
+ >
299
+ <div className="relative mx-auto" style={{ width: size, height: size }}>
300
+ <R.ResponsiveContainer width="100%" height="100%">
301
+ <R.PieChart accessibilityLayer={false}>
302
+ {tooltip(valueFormatter, false)}
303
+ <R.Pie
304
+ rootTabIndex={-1}
305
+ data={rows}
306
+ dataKey={value}
307
+ nameKey={category}
308
+ innerRadius="68%"
309
+ outerRadius="100%"
310
+ paddingAngle={2}
311
+ stroke="var(--color-surface)"
312
+ strokeWidth={2}
313
+ isAnimationActive={false}
314
+ >
315
+ {rows.map((_, i) => (
316
+ <R.Cell key={i} fill={colors[i]} />
317
+ ))}
318
+ </R.Pie>
319
+ </R.PieChart>
320
+ </R.ResponsiveContainer>
321
+ {props.centerLabel && (
322
+ <div className={s.center({ class: slots.center })}>{props.centerLabel}</div>
323
+ )}
324
+ </div>
325
+ </ChartFrame>
326
+ )
327
+ }
328
+
329
+ export interface SparklineProps {
330
+ values: readonly number[]
331
+ /** Accessible description, e.g. "Revenue over the last 12 months, rising". */
332
+ title: string
333
+ color?: ChartColor | (string & {})
334
+ /** Height in pixels. Default 36. */
335
+ height?: number
336
+ /** Fill the area under the line. */
337
+ area?: boolean
338
+ className?: string
339
+ }
340
+
341
+ /** A tiny trend line without axes, for stat cards and table cells. */
342
+ export function Sparkline({
343
+ values,
344
+ title,
345
+ color,
346
+ height = 36,
347
+ area = true,
348
+ className,
349
+ }: SparklineProps) {
350
+ const id = useId().replace(/:/g, '')
351
+ const stroke = resolveColor(color, 0)
352
+ const data = values.map((v, i) => ({ i, v }))
353
+ return (
354
+ <div
355
+ role="img"
356
+ aria-label={title}
357
+ data-slot="sparkline"
358
+ className={className}
359
+ style={{ height }}
360
+ >
361
+ <R.ResponsiveContainer width="100%" height="100%">
362
+ <R.AreaChart
363
+ accessibilityLayer={false}
364
+ data={data}
365
+ margin={{ top: 2, right: 2, bottom: 2, left: 2 }}
366
+ >
367
+ <defs>
368
+ <linearGradient id={id} x1="0" x2="0" y1="0" y2="1">
369
+ <stop offset="0%" stopColor={stroke} stopOpacity={area ? 0.25 : 0} />
370
+ <stop offset="100%" stopColor={stroke} stopOpacity={0} />
371
+ </linearGradient>
372
+ </defs>
373
+ <R.Area
374
+ dataKey="v"
375
+ type="monotone"
376
+ stroke={stroke}
377
+ strokeWidth={1.75}
378
+ fill={`url(#${id})`}
379
+ isAnimationActive={false}
380
+ dot={false}
381
+ />
382
+ </R.AreaChart>
383
+ </R.ResponsiveContainer>
384
+ </div>
385
+ )
386
+ }
@@ -0,0 +1,13 @@
1
+ export {
2
+ AreaChart,
3
+ BarChart,
4
+ type CartesianChartProps,
5
+ type ChartColor,
6
+ type ChartSeries,
7
+ DonutChart,
8
+ type DonutChartProps,
9
+ LineChart,
10
+ Sparkline,
11
+ type SparklineProps,
12
+ } from './chart'
13
+ export { type ChartVariants, chartStyles } from './chart.styles'