@glyphui/ui 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chart.d.ts +202 -0
- package/dist/chart.js +457 -0
- package/dist/data-table.js +3 -2
- package/dist/{field-DXZD0l8w.js → field-CWZ_FdkD.js} +2 -1
- package/dist/{field-context-B8cYheXz.js → field-context-anGPYXbW.js} +89 -143
- package/dist/form.js +3 -2
- package/dist/index.d.ts +4960 -266
- package/dist/index.js +5336 -146
- package/dist/{states-BxPjCUxz.js → states-DR-OoDT-.js} +2 -1
- package/dist/tv-DBAGlgXJ.js +58 -0
- package/package.json +19 -2
- package/src/components/accordion/accordion.styles.ts +41 -0
- package/src/components/accordion/accordion.tsx +86 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/alert-icons.tsx +66 -0
- package/src/components/alert/alert.styles.ts +57 -0
- package/src/components/alert/alert.tsx +86 -0
- package/src/components/alert/index.ts +2 -0
- package/src/components/alert-dialog/alert-dialog.styles.ts +24 -0
- package/src/components/alert-dialog/alert-dialog.tsx +118 -0
- package/src/components/alert-dialog/confirm.tsx +111 -0
- package/src/components/alert-dialog/index.ts +15 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +40 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/badge/badge.styles.ts +1 -1
- package/src/components/banner/banner.styles.ts +40 -0
- package/src/components/banner/banner.tsx +101 -0
- package/src/components/banner/index.ts +2 -0
- package/src/components/blockquote/blockquote.tsx +59 -0
- package/src/components/blockquote/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.styles.ts +20 -0
- package/src/components/breadcrumb/breadcrumb.tsx +135 -0
- package/src/components/breadcrumb/index.ts +12 -0
- package/src/components/button/button.tsx +11 -8
- package/src/components/calendar/calendar.styles.ts +50 -0
- package/src/components/calendar/calendar.tsx +84 -0
- package/src/components/calendar/index.ts +3 -0
- package/src/components/carousel/carousel.styles.ts +43 -0
- package/src/components/carousel/carousel.tsx +278 -0
- package/src/components/carousel/index.ts +13 -0
- package/src/components/code/code.styles.ts +33 -0
- package/src/components/code/code.tsx +96 -0
- package/src/components/code/index.ts +2 -0
- package/src/components/collapsible/collapsible.tsx +66 -0
- package/src/components/collapsible/index.ts +6 -0
- package/src/components/combobox/combobox.styles.ts +50 -0
- package/src/components/combobox/combobox.tsx +203 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command-menu/command-menu.styles.ts +41 -0
- package/src/components/command-menu/command-menu.tsx +217 -0
- package/src/components/command-menu/index.ts +14 -0
- package/src/components/context-menu/context-menu.styles.ts +19 -0
- package/src/components/context-menu/context-menu.tsx +226 -0
- package/src/components/context-menu/index.ts +19 -0
- package/src/components/copy-button/copy-button.tsx +117 -0
- package/src/components/copy-button/index.ts +1 -0
- package/src/components/date-picker/date-picker.styles.ts +42 -0
- package/src/components/date-picker/date-picker.tsx +322 -0
- package/src/components/date-picker/index.ts +8 -0
- package/src/components/description-list/description-list.styles.ts +31 -0
- package/src/components/description-list/description-list.tsx +56 -0
- package/src/components/description-list/index.ts +7 -0
- package/src/components/dialog/close-icon.tsx +22 -0
- package/src/components/dialog/dialog.styles.ts +41 -0
- package/src/components/dialog/dialog.tsx +111 -0
- package/src/components/dialog/index.ts +13 -0
- package/src/components/dropdown-menu/dropdown-menu.styles.ts +45 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +230 -0
- package/src/components/dropdown-menu/index.ts +19 -0
- package/src/components/file-upload/file-upload.styles.ts +43 -0
- package/src/components/file-upload/file-upload.tsx +343 -0
- package/src/components/file-upload/index.ts +10 -0
- package/src/components/grid/grid.tsx +144 -0
- package/src/components/grid/index.ts +1 -0
- package/src/components/hover-card/hover-card.tsx +41 -0
- package/src/components/hover-card/index.ts +1 -0
- package/src/components/input-otp/index.ts +10 -0
- package/src/components/input-otp/input-otp.styles.ts +38 -0
- package/src/components/input-otp/input-otp.tsx +116 -0
- package/src/components/link/index.ts +2 -0
- package/src/components/link/link.styles.ts +39 -0
- package/src/components/link/link.tsx +62 -0
- package/src/components/modal/index.ts +2 -0
- package/src/components/modal/modal.styles.ts +18 -0
- package/src/components/modal/modal.tsx +86 -0
- package/src/components/navigation-menu/index.ts +14 -0
- package/src/components/navigation-menu/navigation-menu.styles.ts +48 -0
- package/src/components/navigation-menu/navigation-menu.tsx +198 -0
- package/src/components/number-input/index.ts +2 -0
- package/src/components/number-input/number-input.styles.ts +41 -0
- package/src/components/number-input/number-input.tsx +211 -0
- package/src/components/pagination/index.ts +3 -0
- package/src/components/pagination/pagination-range.ts +35 -0
- package/src/components/pagination/pagination.styles.ts +41 -0
- package/src/components/pagination/pagination.tsx +130 -0
- package/src/components/password-input/index.ts +7 -0
- package/src/components/password-input/password-input.styles.ts +30 -0
- package/src/components/password-input/password-input.tsx +127 -0
- package/src/components/popover/index.ts +9 -0
- package/src/components/popover/popover.tsx +66 -0
- package/src/components/progress/index.ts +7 -0
- package/src/components/progress/progress.styles.ts +68 -0
- package/src/components/progress/progress.tsx +158 -0
- package/src/components/prose/index.ts +1 -0
- package/src/components/prose/prose.tsx +82 -0
- package/src/components/radio-group/index.ts +7 -0
- package/src/components/radio-group/radio-group.styles.ts +64 -0
- package/src/components/radio-group/radio-group.tsx +128 -0
- package/src/components/resizable/index.ts +9 -0
- package/src/components/resizable/resizable.styles.ts +28 -0
- package/src/components/resizable/resizable.tsx +69 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +68 -0
- package/src/components/sheet/index.ts +13 -0
- package/src/components/sheet/sheet.styles.ts +60 -0
- package/src/components/sheet/sheet.tsx +108 -0
- package/src/components/slider/index.ts +2 -0
- package/src/components/slider/slider.styles.ts +32 -0
- package/src/components/slider/slider.tsx +131 -0
- package/src/components/stat-card/index.ts +2 -0
- package/src/components/stat-card/stat-card.styles.ts +24 -0
- package/src/components/stat-card/stat-card.tsx +108 -0
- package/src/components/stepper/index.ts +2 -0
- package/src/components/stepper/stepper.styles.ts +74 -0
- package/src/components/stepper/stepper.tsx +138 -0
- package/src/components/tag-input/index.ts +2 -0
- package/src/components/tag-input/tag-input.styles.ts +39 -0
- package/src/components/tag-input/tag-input.tsx +198 -0
- package/src/components/timeline/index.ts +8 -0
- package/src/components/timeline/timeline.styles.ts +56 -0
- package/src/components/timeline/timeline.tsx +91 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +91 -0
- package/src/components/toggle/index.ts +9 -0
- package/src/components/toggle/toggle.styles.ts +39 -0
- package/src/components/toggle/toggle.tsx +61 -0
- package/src/components/tree-view/index.ts +2 -0
- package/src/components/tree-view/tree-view.styles.ts +42 -0
- package/src/components/tree-view/tree-view.tsx +274 -0
- package/src/components/visually-hidden/index.ts +1 -0
- package/src/components/visually-hidden/visually-hidden.tsx +10 -0
- package/src/index.ts +45 -0
- package/src/patterns/chart/chart-parts.tsx +171 -0
- package/src/patterns/chart/chart.styles.ts +35 -0
- package/src/patterns/chart/chart.tsx +369 -0
- package/src/patterns/chart/index.ts +13 -0
- package/src/provider/glyph-provider.tsx +16 -2
- package/styles.css +16 -0
package/src/index.ts
CHANGED
|
@@ -16,31 +16,76 @@ export {
|
|
|
16
16
|
tokenVar,
|
|
17
17
|
} from '@glyphui/tokens'
|
|
18
18
|
// Components
|
|
19
|
+
export * from './components/accordion'
|
|
20
|
+
export * from './components/alert'
|
|
21
|
+
export * from './components/alert-dialog'
|
|
22
|
+
export * from './components/aspect-ratio'
|
|
19
23
|
export * from './components/avatar'
|
|
20
24
|
export * from './components/badge'
|
|
25
|
+
export * from './components/banner'
|
|
26
|
+
export * from './components/blockquote'
|
|
27
|
+
export * from './components/breadcrumb'
|
|
21
28
|
export * from './components/button'
|
|
29
|
+
export * from './components/calendar'
|
|
22
30
|
export * from './components/card'
|
|
31
|
+
export * from './components/carousel'
|
|
23
32
|
export * from './components/checkbox'
|
|
33
|
+
export * from './components/code'
|
|
34
|
+
export * from './components/collapsible'
|
|
35
|
+
export * from './components/combobox'
|
|
36
|
+
export * from './components/command-menu'
|
|
24
37
|
export * from './components/container'
|
|
38
|
+
export * from './components/context-menu'
|
|
39
|
+
export * from './components/copy-button'
|
|
40
|
+
export * from './components/date-picker'
|
|
41
|
+
export * from './components/description-list'
|
|
42
|
+
export * from './components/dialog'
|
|
43
|
+
export * from './components/dropdown-menu'
|
|
25
44
|
export * from './components/eyebrow'
|
|
26
45
|
export * from './components/field'
|
|
46
|
+
export * from './components/file-upload'
|
|
47
|
+
export * from './components/grid'
|
|
27
48
|
export * from './components/heading'
|
|
49
|
+
export * from './components/hover-card'
|
|
28
50
|
export * from './components/input'
|
|
51
|
+
export * from './components/input-otp'
|
|
29
52
|
export * from './components/kbd'
|
|
30
53
|
export * from './components/label'
|
|
54
|
+
export * from './components/link'
|
|
55
|
+
export * from './components/modal'
|
|
56
|
+
export * from './components/navigation-menu'
|
|
57
|
+
export * from './components/number-input'
|
|
58
|
+
export * from './components/pagination'
|
|
59
|
+
export * from './components/password-input'
|
|
60
|
+
export * from './components/popover'
|
|
61
|
+
export * from './components/progress'
|
|
62
|
+
export * from './components/prose'
|
|
63
|
+
export * from './components/radio-group'
|
|
64
|
+
export * from './components/resizable'
|
|
65
|
+
export * from './components/scroll-area'
|
|
31
66
|
export * from './components/section'
|
|
32
67
|
export * from './components/segmented-control'
|
|
33
68
|
export * from './components/select'
|
|
34
69
|
export * from './components/separator'
|
|
70
|
+
export * from './components/sheet'
|
|
35
71
|
export * from './components/skeleton'
|
|
72
|
+
export * from './components/slider'
|
|
36
73
|
export * from './components/spinner'
|
|
37
74
|
export * from './components/stack'
|
|
75
|
+
export * from './components/stat-card'
|
|
76
|
+
export * from './components/stepper'
|
|
38
77
|
export * from './components/switch'
|
|
39
78
|
export * from './components/table'
|
|
40
79
|
export * from './components/tabs'
|
|
80
|
+
export * from './components/tag-input'
|
|
41
81
|
export * from './components/text'
|
|
42
82
|
export * from './components/textarea'
|
|
83
|
+
export * from './components/timeline'
|
|
84
|
+
export * from './components/toast'
|
|
85
|
+
export * from './components/toggle'
|
|
43
86
|
export * from './components/tooltip'
|
|
87
|
+
export * from './components/tree-view'
|
|
88
|
+
export * from './components/visually-hidden'
|
|
44
89
|
export * from './hooks'
|
|
45
90
|
// Patterns
|
|
46
91
|
export * from './patterns/app-shell'
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
| 'primary'
|
|
8
|
+
| 'info'
|
|
9
|
+
| 'success'
|
|
10
|
+
| 'warning'
|
|
11
|
+
| 'danger'
|
|
12
|
+
| 'foreground'
|
|
13
|
+
| 'muted-foreground'
|
|
14
|
+
|
|
15
|
+
const palette: ChartColor[] = ['primary', 'info', 'success', 'warning', 'danger']
|
|
16
|
+
|
|
17
|
+
export function resolveColor(color: string | undefined, index: number): string {
|
|
18
|
+
const value = color ?? palette[index % palette.length]!
|
|
19
|
+
return /^[a-z-]+$/.test(value) ? `var(--color-${value})` : value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const defaultFormatter = (value: number) =>
|
|
23
|
+
typeof value === 'number' ? value.toLocaleString() : String(value)
|
|
24
|
+
|
|
25
|
+
export function useChartParts() {
|
|
26
|
+
const [, slots] = useComponentConfig('Chart', {})
|
|
27
|
+
return { s: chartStyles(), slots }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LegendEntry {
|
|
31
|
+
label: ReactNode
|
|
32
|
+
color: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ChartLegend({ items }: { items: LegendEntry[] }) {
|
|
36
|
+
const { s, slots } = useChartParts()
|
|
37
|
+
return (
|
|
38
|
+
<ul data-slot="chart-legend" className={s.legend({ class: slots.legend })} aria-hidden="true">
|
|
39
|
+
{items.map((item, i) => (
|
|
40
|
+
<li key={i} className={s.legendItem({ class: slots.legendItem })}>
|
|
41
|
+
<span className={s.swatch({ class: slots.swatch })} style={{ background: item.color }} />
|
|
42
|
+
{item.label}
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface TooltipPayloadItem {
|
|
50
|
+
name?: string | number
|
|
51
|
+
value?: unknown
|
|
52
|
+
color?: string
|
|
53
|
+
dataKey?: unknown
|
|
54
|
+
payload?: Record<string, unknown> & { fill?: string }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ChartTooltipCard({
|
|
58
|
+
active,
|
|
59
|
+
payload,
|
|
60
|
+
label,
|
|
61
|
+
formatter,
|
|
62
|
+
labelFormatter,
|
|
63
|
+
}: {
|
|
64
|
+
active?: boolean
|
|
65
|
+
payload?: readonly TooltipPayloadItem[]
|
|
66
|
+
label?: ReactNode
|
|
67
|
+
formatter: (value: number) => string
|
|
68
|
+
labelFormatter?: ((label: ReactNode) => ReactNode) | undefined
|
|
69
|
+
}) {
|
|
70
|
+
const { s, slots } = useChartParts()
|
|
71
|
+
if (!active || !payload?.length) return null
|
|
72
|
+
return (
|
|
73
|
+
<div data-slot="chart-tooltip" className={s.tooltip({ class: slots.tooltip })}>
|
|
74
|
+
{label !== undefined && label !== '' && (
|
|
75
|
+
<p className={s.tooltipLabel({ class: slots.tooltipLabel })}>
|
|
76
|
+
{labelFormatter ? labelFormatter(label) : label}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
{payload.map((item, i) => (
|
|
80
|
+
<div key={i} className={s.tooltipRow({ class: slots.tooltipRow })}>
|
|
81
|
+
<span
|
|
82
|
+
className={s.swatch({ class: slots.swatch })}
|
|
83
|
+
style={{ background: item.color ?? item.payload?.fill }}
|
|
84
|
+
/>
|
|
85
|
+
<span className={s.tooltipName({ class: slots.tooltipName })}>
|
|
86
|
+
{String(item.name ?? '')}
|
|
87
|
+
</span>
|
|
88
|
+
<span className={s.tooltipValue({ class: slots.tooltipValue })}>
|
|
89
|
+
{formatter(Number(item.value))}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ChartFrameProps {
|
|
98
|
+
/** Accessible name of the chart. Shown above it unless `hideTitle` is set. */
|
|
99
|
+
title: string
|
|
100
|
+
description?: ReactNode
|
|
101
|
+
hideTitle?: boolean
|
|
102
|
+
className?: string
|
|
103
|
+
legend?: LegendEntry[] | undefined
|
|
104
|
+
/** Columns and rows for the screen-reader table that mirrors the chart. */
|
|
105
|
+
table: { columns: string[]; rows: (string | number)[][] }
|
|
106
|
+
children: ReactNode
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Wraps every chart in a `<figure>` with a caption and a visually hidden data table,
|
|
111
|
+
* so screen-reader users get the numbers the chart shows.
|
|
112
|
+
*/
|
|
113
|
+
export function ChartFrame({
|
|
114
|
+
title,
|
|
115
|
+
description,
|
|
116
|
+
hideTitle,
|
|
117
|
+
className,
|
|
118
|
+
legend,
|
|
119
|
+
table,
|
|
120
|
+
children,
|
|
121
|
+
}: ChartFrameProps) {
|
|
122
|
+
const { s, slots } = useChartParts()
|
|
123
|
+
const titleId = useId()
|
|
124
|
+
return (
|
|
125
|
+
<figure
|
|
126
|
+
data-slot="chart"
|
|
127
|
+
aria-labelledby={titleId}
|
|
128
|
+
className={s.root({ class: [slots.root, className] })}
|
|
129
|
+
>
|
|
130
|
+
<figcaption className={hideTitle ? s.srOnly() : s.caption({ class: slots.caption })}>
|
|
131
|
+
<span id={titleId} className={s.title({ class: slots.title })}>
|
|
132
|
+
{title}
|
|
133
|
+
</span>
|
|
134
|
+
{description && (
|
|
135
|
+
<span className={s.description({ class: slots.description })}>{description}</span>
|
|
136
|
+
)}
|
|
137
|
+
</figcaption>
|
|
138
|
+
<div className={s.plot({ class: slots.plot })} aria-hidden="true">
|
|
139
|
+
{children}
|
|
140
|
+
</div>
|
|
141
|
+
{legend && legend.length > 1 && <ChartLegend items={legend} />}
|
|
142
|
+
<table className={s.srOnly()}>
|
|
143
|
+
<caption>{title}</caption>
|
|
144
|
+
<thead>
|
|
145
|
+
<tr>
|
|
146
|
+
{table.columns.map((column) => (
|
|
147
|
+
<th key={column} scope="col">
|
|
148
|
+
{column}
|
|
149
|
+
</th>
|
|
150
|
+
))}
|
|
151
|
+
</tr>
|
|
152
|
+
</thead>
|
|
153
|
+
<tbody>
|
|
154
|
+
{table.rows.map((row, i) => (
|
|
155
|
+
<tr key={i}>
|
|
156
|
+
{row.map((cell, j) =>
|
|
157
|
+
j === 0 ? (
|
|
158
|
+
<th key={j} scope="row">
|
|
159
|
+
{cell}
|
|
160
|
+
</th>
|
|
161
|
+
) : (
|
|
162
|
+
<td key={j}>{cell}</td>
|
|
163
|
+
),
|
|
164
|
+
)}
|
|
165
|
+
</tr>
|
|
166
|
+
))}
|
|
167
|
+
</tbody>
|
|
168
|
+
</table>
|
|
169
|
+
</figure>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -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,369 @@
|
|
|
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 data={props.data as T[]} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
|
|
139
|
+
<Axes
|
|
140
|
+
index={props.index}
|
|
141
|
+
showGrid={props.showGrid}
|
|
142
|
+
showYAxis={props.showYAxis}
|
|
143
|
+
valueFormatter={valueFormatter}
|
|
144
|
+
/>
|
|
145
|
+
{tooltip(valueFormatter, { stroke: 'var(--color-border-strong)' })}
|
|
146
|
+
{resolved.map((item) => (
|
|
147
|
+
<R.Line
|
|
148
|
+
key={item.key}
|
|
149
|
+
dataKey={item.key}
|
|
150
|
+
name={item.label}
|
|
151
|
+
type={props.curve ?? 'monotone'}
|
|
152
|
+
stroke={item.stroke}
|
|
153
|
+
strokeWidth={2}
|
|
154
|
+
dot={false}
|
|
155
|
+
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
156
|
+
isAnimationActive={false}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
</R.LineChart>
|
|
160
|
+
</R.ResponsiveContainer>
|
|
161
|
+
</ChartFrame>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Trends with filled areas. `stacked` shows how series add up to a total. */
|
|
166
|
+
export function AreaChart<T extends object>(props: CartesianChartProps<T> & { stacked?: boolean }) {
|
|
167
|
+
const { resolved, table, legend, valueFormatter } = useCartesian(props)
|
|
168
|
+
const id = useId().replace(/:/g, '')
|
|
169
|
+
return (
|
|
170
|
+
<ChartFrame {...frameProps(props)} legend={legend} table={table}>
|
|
171
|
+
<R.ResponsiveContainer width="100%" height={props.height ?? 240}>
|
|
172
|
+
<R.AreaChart data={props.data as T[]} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
|
|
173
|
+
<defs>
|
|
174
|
+
{resolved.map((item, i) => (
|
|
175
|
+
<linearGradient key={item.key} id={`${id}-${i}`} x1="0" x2="0" y1="0" y2="1">
|
|
176
|
+
<stop offset="0%" stopColor={item.stroke} stopOpacity={0.3} />
|
|
177
|
+
<stop offset="100%" stopColor={item.stroke} stopOpacity={0} />
|
|
178
|
+
</linearGradient>
|
|
179
|
+
))}
|
|
180
|
+
</defs>
|
|
181
|
+
<Axes
|
|
182
|
+
index={props.index}
|
|
183
|
+
showGrid={props.showGrid}
|
|
184
|
+
showYAxis={props.showYAxis}
|
|
185
|
+
valueFormatter={valueFormatter}
|
|
186
|
+
/>
|
|
187
|
+
{tooltip(valueFormatter, { stroke: 'var(--color-border-strong)' })}
|
|
188
|
+
{resolved.map((item, i) => (
|
|
189
|
+
<R.Area
|
|
190
|
+
key={item.key}
|
|
191
|
+
dataKey={item.key}
|
|
192
|
+
name={item.label}
|
|
193
|
+
type="monotone"
|
|
194
|
+
stroke={item.stroke}
|
|
195
|
+
strokeWidth={2}
|
|
196
|
+
fill={`url(#${id}-${i})`}
|
|
197
|
+
{...(props.stacked && { stackId: 'stack' })}
|
|
198
|
+
isAnimationActive={false}
|
|
199
|
+
/>
|
|
200
|
+
))}
|
|
201
|
+
</R.AreaChart>
|
|
202
|
+
</R.ResponsiveContainer>
|
|
203
|
+
</ChartFrame>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Compare values across categories. `stacked` puts series on top of each other. */
|
|
208
|
+
export function BarChart<T extends object>(
|
|
209
|
+
props: CartesianChartProps<T> & {
|
|
210
|
+
stacked?: boolean /** Corner radius of bar tops. Default 3. */
|
|
211
|
+
barRadius?: number
|
|
212
|
+
},
|
|
213
|
+
) {
|
|
214
|
+
const { resolved, table, legend, valueFormatter } = useCartesian(props)
|
|
215
|
+
const radius = props.barRadius ?? 3
|
|
216
|
+
return (
|
|
217
|
+
<ChartFrame {...frameProps(props)} legend={legend} table={table}>
|
|
218
|
+
<R.ResponsiveContainer width="100%" height={props.height ?? 240}>
|
|
219
|
+
<R.BarChart data={props.data as T[]} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
|
|
220
|
+
<Axes
|
|
221
|
+
index={props.index}
|
|
222
|
+
showGrid={props.showGrid}
|
|
223
|
+
showYAxis={props.showYAxis}
|
|
224
|
+
valueFormatter={valueFormatter}
|
|
225
|
+
/>
|
|
226
|
+
{tooltip(valueFormatter, { fill: 'var(--color-muted)', opacity: 0.6 })}
|
|
227
|
+
{resolved.map((item, i) => (
|
|
228
|
+
<R.Bar
|
|
229
|
+
key={item.key}
|
|
230
|
+
dataKey={item.key}
|
|
231
|
+
name={item.label}
|
|
232
|
+
fill={item.stroke}
|
|
233
|
+
radius={props.stacked && i < resolved.length - 1 ? 0 : [radius, radius, 0, 0]}
|
|
234
|
+
{...(props.stacked && { stackId: 'stack' })}
|
|
235
|
+
isAnimationActive={false}
|
|
236
|
+
/>
|
|
237
|
+
))}
|
|
238
|
+
</R.BarChart>
|
|
239
|
+
</R.ResponsiveContainer>
|
|
240
|
+
</ChartFrame>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface DonutChartProps<T extends object> {
|
|
245
|
+
data: readonly T[]
|
|
246
|
+
/** Field naming each slice. */
|
|
247
|
+
category: Key<T>
|
|
248
|
+
/** Field holding each slice's value. */
|
|
249
|
+
value: Key<T>
|
|
250
|
+
title: string
|
|
251
|
+
description?: ReactNode
|
|
252
|
+
hideTitle?: boolean
|
|
253
|
+
/** One color per slice, in order. Defaults follow the theme palette. */
|
|
254
|
+
colors?: readonly (ChartColor | (string & {}))[]
|
|
255
|
+
/** Content in the hole, e.g. the total. */
|
|
256
|
+
centerLabel?: ReactNode
|
|
257
|
+
valueFormatter?: (value: number) => string
|
|
258
|
+
/** Diameter in pixels. Default 200. */
|
|
259
|
+
size?: number
|
|
260
|
+
showLegend?: boolean
|
|
261
|
+
className?: string
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Parts of a whole. Best with five or fewer slices. */
|
|
265
|
+
export function DonutChart<T extends object>(props: DonutChartProps<T>) {
|
|
266
|
+
const { data, category, value, valueFormatter = defaultFormatter, size = 200 } = props
|
|
267
|
+
const { s, slots } = useChartParts()
|
|
268
|
+
const rows = data.map((row) => row as Record<string, unknown>)
|
|
269
|
+
const colors = rows.map((_, i) => resolveColor(props.colors?.[i], i))
|
|
270
|
+
const table = {
|
|
271
|
+
columns: [String(category), String(value)],
|
|
272
|
+
rows: rows.map((row) => [String(row[category]), valueFormatter(Number(row[value]))]),
|
|
273
|
+
}
|
|
274
|
+
const legend =
|
|
275
|
+
(props.showLegend ?? true)
|
|
276
|
+
? rows.map((row, i) => ({ label: String(row[category]), color: colors[i]! }))
|
|
277
|
+
: undefined
|
|
278
|
+
return (
|
|
279
|
+
<ChartFrame
|
|
280
|
+
title={props.title}
|
|
281
|
+
description={props.description}
|
|
282
|
+
hideTitle={props.hideTitle}
|
|
283
|
+
className={props.className}
|
|
284
|
+
legend={legend}
|
|
285
|
+
table={table}
|
|
286
|
+
>
|
|
287
|
+
<div className="relative mx-auto" style={{ width: size, height: size }}>
|
|
288
|
+
<R.ResponsiveContainer width="100%" height="100%">
|
|
289
|
+
<R.PieChart>
|
|
290
|
+
{tooltip(valueFormatter, false)}
|
|
291
|
+
<R.Pie
|
|
292
|
+
data={rows}
|
|
293
|
+
dataKey={value}
|
|
294
|
+
nameKey={category}
|
|
295
|
+
innerRadius="68%"
|
|
296
|
+
outerRadius="100%"
|
|
297
|
+
paddingAngle={2}
|
|
298
|
+
stroke="var(--color-surface)"
|
|
299
|
+
strokeWidth={2}
|
|
300
|
+
isAnimationActive={false}
|
|
301
|
+
>
|
|
302
|
+
{rows.map((_, i) => (
|
|
303
|
+
<R.Cell key={i} fill={colors[i]} />
|
|
304
|
+
))}
|
|
305
|
+
</R.Pie>
|
|
306
|
+
</R.PieChart>
|
|
307
|
+
</R.ResponsiveContainer>
|
|
308
|
+
{props.centerLabel && (
|
|
309
|
+
<div className={s.center({ class: slots.center })}>{props.centerLabel}</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</ChartFrame>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface SparklineProps {
|
|
317
|
+
values: readonly number[]
|
|
318
|
+
/** Accessible description, e.g. "Revenue over the last 12 months, rising". */
|
|
319
|
+
title: string
|
|
320
|
+
color?: ChartColor | (string & {})
|
|
321
|
+
/** Height in pixels. Default 36. */
|
|
322
|
+
height?: number
|
|
323
|
+
/** Fill the area under the line. */
|
|
324
|
+
area?: boolean
|
|
325
|
+
className?: string
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** A tiny trend line without axes, for stat cards and table cells. */
|
|
329
|
+
export function Sparkline({
|
|
330
|
+
values,
|
|
331
|
+
title,
|
|
332
|
+
color,
|
|
333
|
+
height = 36,
|
|
334
|
+
area = true,
|
|
335
|
+
className,
|
|
336
|
+
}: SparklineProps) {
|
|
337
|
+
const id = useId().replace(/:/g, '')
|
|
338
|
+
const stroke = resolveColor(color, 0)
|
|
339
|
+
const data = values.map((v, i) => ({ i, v }))
|
|
340
|
+
return (
|
|
341
|
+
<div
|
|
342
|
+
role="img"
|
|
343
|
+
aria-label={title}
|
|
344
|
+
data-slot="sparkline"
|
|
345
|
+
className={className}
|
|
346
|
+
style={{ height }}
|
|
347
|
+
>
|
|
348
|
+
<R.ResponsiveContainer width="100%" height="100%">
|
|
349
|
+
<R.AreaChart data={data} margin={{ top: 2, right: 2, bottom: 2, left: 2 }}>
|
|
350
|
+
<defs>
|
|
351
|
+
<linearGradient id={id} x1="0" x2="0" y1="0" y2="1">
|
|
352
|
+
<stop offset="0%" stopColor={stroke} stopOpacity={area ? 0.25 : 0} />
|
|
353
|
+
<stop offset="100%" stopColor={stroke} stopOpacity={0} />
|
|
354
|
+
</linearGradient>
|
|
355
|
+
</defs>
|
|
356
|
+
<R.Area
|
|
357
|
+
dataKey="v"
|
|
358
|
+
type="monotone"
|
|
359
|
+
stroke={stroke}
|
|
360
|
+
strokeWidth={1.75}
|
|
361
|
+
fill={`url(#${id})`}
|
|
362
|
+
isAnimationActive={false}
|
|
363
|
+
dot={false}
|
|
364
|
+
/>
|
|
365
|
+
</R.AreaChart>
|
|
366
|
+
</R.ResponsiveContainer>
|
|
367
|
+
</div>
|
|
368
|
+
)
|
|
369
|
+
}
|
|
@@ -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'
|