@a4ui/core 0.37.1 → 0.38.1

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 (105) hide show
  1. package/README.md +8 -7
  2. package/dist/Collapse-Cr5cDqAl.js +375 -0
  3. package/dist/charts/AreaChart.d.ts +38 -0
  4. package/dist/charts/BarChart.d.ts +17 -1
  5. package/dist/charts/PieChart.d.ts +29 -0
  6. package/dist/charts/RadialBarChart.d.ts +33 -0
  7. package/dist/charts/index.d.ts +5 -0
  8. package/dist/charts/shared.d.ts +111 -0
  9. package/dist/charts.js +1493 -780
  10. package/dist/commerce.js +625 -634
  11. package/dist/elements.css +1076 -8
  12. package/dist/elements.iife.js +1 -6
  13. package/dist/elements.js +7774 -9133
  14. package/dist/full.css +1076 -8
  15. package/dist/index.d.ts +30 -3
  16. package/dist/index.js +17502 -15126
  17. package/dist/layout/Aurora.d.ts +16 -0
  18. package/dist/layout/EffectsToggle.d.ts +1 -1
  19. package/dist/layout/SpaceBackground.d.ts +1 -1
  20. package/dist/motion-CTIBwMCA.js +104 -0
  21. package/dist/styles.css +110 -3
  22. package/dist/ui/AspectRatio.d.ts +19 -0
  23. package/dist/ui/Attachment.d.ts +70 -0
  24. package/dist/ui/ButtonGroup.d.ts +37 -0
  25. package/dist/ui/ChatMarker.d.ts +20 -0
  26. package/dist/ui/ChatThread.d.ts +32 -8
  27. package/dist/ui/ContactSection.d.ts +49 -0
  28. package/dist/ui/CtaBanner.d.ts +28 -0
  29. package/dist/ui/Drawer.d.ts +5 -2
  30. package/dist/ui/Faq.d.ts +29 -0
  31. package/dist/ui/FeatureGrid.d.ts +35 -0
  32. package/dist/ui/Field.d.ts +148 -0
  33. package/dist/ui/Hero.d.ts +48 -0
  34. package/dist/ui/InputGroup.d.ts +93 -0
  35. package/dist/ui/Item.d.ts +63 -0
  36. package/dist/ui/Label.d.ts +23 -0
  37. package/dist/ui/Menubar.d.ts +83 -0
  38. package/dist/ui/Message.d.ts +51 -5
  39. package/dist/ui/Modal.d.ts +8 -2
  40. package/dist/ui/NativeSelect.d.ts +26 -0
  41. package/dist/ui/NavigationMenu.d.ts +85 -0
  42. package/dist/ui/PromptComposer.d.ts +5 -0
  43. package/dist/ui/Prose.d.ts +21 -0
  44. package/dist/ui/Questionnaire.d.ts +71 -0
  45. package/dist/ui/ScrollArea.d.ts +30 -0
  46. package/dist/ui/Sidebar.d.ts +229 -0
  47. package/dist/ui/SiteFooter.d.ts +39 -0
  48. package/dist/ui/SiteNav.d.ts +35 -0
  49. package/dist/ui/TeamGrid.d.ts +33 -0
  50. package/dist/ui/Testimonials.d.ts +37 -0
  51. package/dist/ui/internal/sectionKit.d.ts +22 -0
  52. package/dist/ui/internal/socialIcons.d.ts +9 -0
  53. package/package.json +38 -26
  54. package/preset.d.ts +23 -0
  55. package/src/charts/AreaChart.tsx +206 -0
  56. package/src/charts/BarChart.tsx +241 -5
  57. package/src/charts/DonutChart.tsx +8 -11
  58. package/src/charts/LineChart.tsx +2 -3
  59. package/src/charts/PieChart.tsx +145 -0
  60. package/src/charts/RadialBarChart.tsx +141 -0
  61. package/src/charts/index.ts +14 -0
  62. package/src/charts/shared.tsx +304 -0
  63. package/src/index.ts +192 -3
  64. package/src/layout/Aurora.tsx +36 -7
  65. package/src/layout/sceneEffects.ts +22 -3
  66. package/src/ui/AlertDialog.tsx +6 -3
  67. package/src/ui/AspectRatio.tsx +35 -0
  68. package/src/ui/Attachment.tsx +189 -0
  69. package/src/ui/ButtonGroup.tsx +89 -0
  70. package/src/ui/ChatMarker.tsx +58 -0
  71. package/src/ui/ChatThread.tsx +115 -23
  72. package/src/ui/Command.tsx +6 -3
  73. package/src/ui/ContactSection.tsx +199 -0
  74. package/src/ui/CtaBanner.tsx +62 -0
  75. package/src/ui/DateField.tsx +2 -1
  76. package/src/ui/DateRangePicker.tsx +2 -1
  77. package/src/ui/Drawer.tsx +43 -7
  78. package/src/ui/Faq.tsx +57 -0
  79. package/src/ui/FeatureGrid.tsx +77 -0
  80. package/src/ui/Field.tsx +374 -0
  81. package/src/ui/Hero.tsx +146 -0
  82. package/src/ui/Image.tsx +6 -3
  83. package/src/ui/InputGroup.tsx +219 -0
  84. package/src/ui/Item.tsx +185 -0
  85. package/src/ui/Label.tsx +50 -0
  86. package/src/ui/Menubar.tsx +212 -0
  87. package/src/ui/Message.tsx +139 -13
  88. package/src/ui/Modal.tsx +47 -8
  89. package/src/ui/MultiSelect.tsx +2 -1
  90. package/src/ui/NativeSelect.tsx +66 -0
  91. package/src/ui/NavigationMenu.tsx +262 -0
  92. package/src/ui/PromptComposer.tsx +11 -0
  93. package/src/ui/Prose.tsx +63 -0
  94. package/src/ui/Questionnaire.tsx +331 -0
  95. package/src/ui/ScrollArea.tsx +70 -0
  96. package/src/ui/Sidebar.tsx +753 -0
  97. package/src/ui/SiteFooter.tsx +112 -0
  98. package/src/ui/SiteNav.tsx +146 -0
  99. package/src/ui/TeamGrid.tsx +81 -0
  100. package/src/ui/Testimonials.tsx +103 -0
  101. package/src/ui/TimeField.tsx +2 -1
  102. package/src/ui/internal/sectionKit.tsx +72 -0
  103. package/src/ui/internal/socialIcons.tsx +41 -0
  104. package/dist/NumberInput-Bnh2u7pp.js +0 -381
  105. package/dist/motion-jYMWmZqB.js +0 -119
@@ -0,0 +1,145 @@
1
+ // PieChart — SVG pie (solid wedges, no charting library). Shares its arc math
2
+ // with DonutChart / RadialBarChart (`sliceAngles`, `arcPath`). Each wedge
3
+ // exposes `data-value` and `data-angle` (degrees) so proportions are testable.
4
+ import { createMemo, For, Show, type JSX } from 'solid-js'
5
+
6
+ import { cn } from '../lib/cn'
7
+ import {
8
+ ChartLegend,
9
+ ChartTooltip,
10
+ FOCUS_CLASS,
11
+ arcPath,
12
+ createChartNav,
13
+ createHiddenSet,
14
+ polar,
15
+ seriesColor,
16
+ sliceAngles,
17
+ type ChartColor,
18
+ } from './shared'
19
+
20
+ export interface PieDatum {
21
+ label: string
22
+ value: number
23
+ /** Token name; defaults to cycling `chart-1..5`. */
24
+ color?: ChartColor
25
+ }
26
+
27
+ export interface PieChartProps {
28
+ data: PieDatum[]
29
+ /** SVG width/height in px. Default 200. */
30
+ size?: number
31
+ /** Hover / keyboard tooltip. Default true. */
32
+ showTooltip?: boolean
33
+ /** Toggleable legend. Default true. */
34
+ showLegend?: boolean
35
+ class?: string
36
+ }
37
+
38
+ /**
39
+ * Pie chart whose wedge angles are proportional to the values. Left / Right
40
+ * arrows move between wedges and show the tooltip; legend entries toggle
41
+ * wedges (the rest re-share the circle).
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * <PieChart data={[{ label: 'Direct', value: 42 }, { label: 'Social', value: 28, color: 'accent' }]} />
46
+ * ```
47
+ */
48
+ export function PieChart(props: PieChartProps): JSX.Element {
49
+ const size = () => props.size ?? 200
50
+ const r = () => size() / 2 - 4
51
+ const { hidden, toggle } = createHiddenSet()
52
+ const colorOf = (i: number) => seriesColor(props.data[i].color, i)
53
+ const visible = () => props.data.map((d, i) => ({ d, i })).filter((x) => !hidden().has(x.d.label))
54
+ const total = () => visible().reduce((s, x) => s + x.d.value, 0)
55
+
56
+ const slices = createMemo(() => {
57
+ const vis = visible()
58
+ return sliceAngles(vis.map((x) => x.d.value)).map((a) => ({
59
+ ...a,
60
+ datum: vis[a.index].d,
61
+ orig: vis[a.index].i,
62
+ }))
63
+ })
64
+ const nav = createChartNav(() => slices().length)
65
+ const active = () => {
66
+ const i = nav.active()
67
+ return i === null ? undefined : slices()[i]
68
+ }
69
+
70
+ const summary = () => {
71
+ const sum = props.data.reduce((s, x) => s + x.value, 0) || 1
72
+ return `Pie chart. ${props.data
73
+ .map((d) => `${d.label} ${d.value} (${Math.round((d.value / sum) * 100)}%)`)
74
+ .join(', ')}`
75
+ }
76
+
77
+ const tipPos = () => {
78
+ const s = active()
79
+ if (!s) return { left: '50%', top: '0', anchor: 0.5 }
80
+ const p = polar(size() / 2, size() / 2, r() * 0.6, s.start + s.angle / 2)
81
+ return { left: `${(p.x / size()) * 100}%`, top: `${(p.y / size()) * 100}%`, anchor: p.x / size() }
82
+ }
83
+
84
+ return (
85
+ <div class={cn('inline-block', props.class)}>
86
+ <div
87
+ class={cn('relative', FOCUS_CLASS)}
88
+ style={{ width: `${size()}px`, height: `${size()}px` }}
89
+ tabindex="0"
90
+ role="img"
91
+ aria-label={summary()}
92
+ data-chart="pie"
93
+ onKeyDown={nav.onKeyDown}
94
+ onFocus={nav.onFocus}
95
+ onBlur={nav.onBlur}
96
+ onPointerLeave={() => nav.setActive(null)}
97
+ >
98
+ <svg width={size()} height={size()} viewBox={`0 0 ${size()} ${size()}`} aria-hidden="true">
99
+ <For each={slices()}>
100
+ {(s, i) => (
101
+ <path
102
+ data-slice
103
+ data-label={s.datum.label}
104
+ data-value={s.value}
105
+ data-angle={s.angle}
106
+ d={arcPath(size() / 2, size() / 2, r(), 0, s.start, s.angle)}
107
+ fill={colorOf(s.orig)}
108
+ stroke="hsl(var(--background))"
109
+ stroke-width={1.5}
110
+ opacity={nav.active() === null || nav.active() === i() ? 1 : 0.6}
111
+ onPointerEnter={() => nav.setActive(i())}
112
+ />
113
+ )}
114
+ </For>
115
+ </svg>
116
+ <Show when={(props.showTooltip ?? true) && active()}>
117
+ {(s) => (
118
+ <ChartTooltip
119
+ title={s().datum.label}
120
+ left={tipPos().left}
121
+ top={tipPos().top}
122
+ anchor={tipPos().anchor}
123
+ rows={[
124
+ { label: 'Value', value: s().value, color: colorOf(s().orig) },
125
+ {
126
+ label: 'Share',
127
+ value: `${Math.round((s().value / (total() || 1)) * 100)}%`,
128
+ color: colorOf(s().orig),
129
+ },
130
+ ]}
131
+ />
132
+ )}
133
+ </Show>
134
+ </div>
135
+ <Show when={props.showLegend ?? true}>
136
+ <ChartLegend
137
+ class="mt-2"
138
+ hidden={hidden()}
139
+ onToggle={toggle}
140
+ items={props.data.map((d, i) => ({ key: d.label, label: d.label, color: colorOf(i) }))}
141
+ />
142
+ </Show>
143
+ </div>
144
+ )
145
+ }
@@ -0,0 +1,141 @@
1
+ // RadialBarChart — concentric progress rings, one per datum (no charting
2
+ // library). The first datum is the outermost ring; each ring sweeps
3
+ // `value / max` of a full turn from 12 o'clock. Rings expose `data-value` and
4
+ // `data-angle` (degrees).
5
+ import { createMemo, For, Show, type JSX } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+ import {
9
+ ChartLegend,
10
+ ChartTooltip,
11
+ FOCUS_CLASS,
12
+ arcPath,
13
+ createChartNav,
14
+ createHiddenSet,
15
+ seriesColor,
16
+ type ChartColor,
17
+ } from './shared'
18
+
19
+ export interface RadialBarDatum {
20
+ label: string
21
+ value: number
22
+ /** Token name; defaults to cycling `chart-1..5`. */
23
+ color?: ChartColor
24
+ }
25
+
26
+ export interface RadialBarChartProps {
27
+ data: RadialBarDatum[]
28
+ /** Value that fills a whole ring. Default: the largest value. */
29
+ max?: number
30
+ /** SVG width/height in px. Default 220. */
31
+ size?: number
32
+ /** Ring thickness in px. Default 14. */
33
+ thickness?: number
34
+ /** Hover / keyboard tooltip. Default true. */
35
+ showTooltip?: boolean
36
+ /** Toggleable legend. Default true. */
37
+ showLegend?: boolean
38
+ class?: string
39
+ }
40
+
41
+ /**
42
+ * Radial bar chart: one concentric ring per datum, each filled to `value /
43
+ * max`. Arrow keys move between rings and show the tooltip; legend entries
44
+ * toggle rings.
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * <RadialBarChart max={100} data={[{ label: 'Move', value: 72 }, { label: 'Stand', value: 40, color: 'accent' }]} />
49
+ * ```
50
+ */
51
+ export function RadialBarChart(props: RadialBarChartProps): JSX.Element {
52
+ const size = () => props.size ?? 220
53
+ const thickness = () => props.thickness ?? 14
54
+ const gap = 4
55
+ const { hidden, toggle } = createHiddenSet()
56
+ const colorOf = (i: number) => seriesColor(props.data[i].color, i)
57
+ const visible = () => props.data.map((d, i) => ({ d, i })).filter((x) => !hidden().has(x.d.label))
58
+ const max = () => props.max ?? (Math.max(...props.data.map((d) => d.value), 0) || 1)
59
+
60
+ const rings = createMemo(() =>
61
+ visible().map((x, n) => {
62
+ const outer = size() / 2 - 2 - n * (thickness() + gap)
63
+ const angle = Math.min(Math.max(x.d.value / max(), 0), 1) * 360
64
+ return { datum: x.d, orig: x.i, outer, inner: outer - thickness(), angle }
65
+ }),
66
+ )
67
+ const nav = createChartNav(() => rings().length)
68
+ const active = () => {
69
+ const i = nav.active()
70
+ return i === null ? undefined : rings()[i]
71
+ }
72
+
73
+ const summary = () =>
74
+ `Radial bar chart. ${props.data.map((d) => `${d.label} ${d.value} of ${max()}`).join(', ')}`
75
+
76
+ return (
77
+ <div class={cn('inline-block', props.class)}>
78
+ <div
79
+ class={cn('relative', FOCUS_CLASS)}
80
+ style={{ width: `${size()}px`, height: `${size()}px` }}
81
+ tabindex="0"
82
+ role="img"
83
+ aria-label={summary()}
84
+ data-chart="radial-bar"
85
+ onKeyDown={nav.onKeyDown}
86
+ onFocus={nav.onFocus}
87
+ onBlur={nav.onBlur}
88
+ onPointerLeave={() => nav.setActive(null)}
89
+ >
90
+ <svg width={size()} height={size()} viewBox={`0 0 ${size()} ${size()}`} aria-hidden="true">
91
+ <For each={rings()}>
92
+ {(ring, i) => (
93
+ <g
94
+ onPointerEnter={() => nav.setActive(i())}
95
+ opacity={nav.active() === null || nav.active() === i() ? 1 : 0.6}
96
+ >
97
+ <path
98
+ d={arcPath(size() / 2, size() / 2, ring.outer, ring.inner, 0, 360)}
99
+ fill="hsl(var(--muted))"
100
+ fill-rule="evenodd"
101
+ />
102
+ <Show when={ring.angle > 0}>
103
+ <path
104
+ data-ring
105
+ data-label={ring.datum.label}
106
+ data-value={ring.datum.value}
107
+ data-angle={ring.angle}
108
+ d={arcPath(size() / 2, size() / 2, ring.outer, ring.inner, 0, ring.angle)}
109
+ fill={colorOf(ring.orig)}
110
+ fill-rule="evenodd"
111
+ />
112
+ </Show>
113
+ </g>
114
+ )}
115
+ </For>
116
+ </svg>
117
+ <Show when={(props.showTooltip ?? true) && active()}>
118
+ {(ring) => (
119
+ <ChartTooltip
120
+ title={ring().datum.label}
121
+ left="50%"
122
+ top="0"
123
+ rows={[
124
+ { label: 'Value', value: ring().datum.value, color: colorOf(ring().orig) },
125
+ { label: 'Of', value: max(), color: colorOf(ring().orig) },
126
+ ]}
127
+ />
128
+ )}
129
+ </Show>
130
+ </div>
131
+ <Show when={props.showLegend ?? true}>
132
+ <ChartLegend
133
+ class="mt-2"
134
+ hidden={hidden()}
135
+ onToggle={toggle}
136
+ items={props.data.map((d, i) => ({ key: d.label, label: d.label, color: colorOf(i) }))}
137
+ />
138
+ </Show>
139
+ </div>
140
+ )
141
+ }
@@ -9,3 +9,17 @@ export { RadarChart, type RadarSeries, type RadarChartProps } from './RadarChart
9
9
  export { BarList, type BarListDatum, type BarListProps } from './BarList'
10
10
  export { StatusTracker, type StatusSegment, type StatusTrackerProps } from './StatusTracker'
11
11
  export { CategoryBar, type CategoryBarTone, type CategoryBarProps } from './CategoryBar'
12
+ export { AreaChart, type AreaDatum, type AreaChartProps } from './AreaChart'
13
+ export { PieChart, type PieDatum, type PieChartProps } from './PieChart'
14
+ export { RadialBarChart, type RadialBarDatum, type RadialBarChartProps } from './RadialBarChart'
15
+ export {
16
+ ChartTooltip,
17
+ ChartLegend,
18
+ type ChartSeries,
19
+ type ChartColor,
20
+ type ChartTooltipProps,
21
+ type ChartTooltipRow,
22
+ type ChartLegendProps,
23
+ type ChartLegendItem,
24
+ } from './shared'
25
+ export type { BarChartMode } from './BarChart'
@@ -0,0 +1,304 @@
1
+ // Shared building blocks for the multi-series charts: the series config type,
2
+ // a single token -> color helper, ChartTooltip, ChartLegend, keyboard / pointer
3
+ // category navigation, and the arc math used by DonutChart, PieChart and
4
+ // RadialBarChart. Colors always resolve to theme CSS variables.
5
+ import { createSignal, For, Show, type JSX } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+
9
+ /** Token names a series can be tinted with. `chart-1..5` cycle the theme's data hues. */
10
+ export type ChartColor =
11
+ 'primary' | 'accent' | 'destructive' | 'muted' | 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5'
12
+
13
+ /** Describes one series of a multi-series chart. */
14
+ export interface ChartSeries {
15
+ /** Key of the series' value inside each datum's `values` record. */
16
+ key: string
17
+ /** Human label shown in the legend and tooltip. */
18
+ label: string
19
+ /** Token name; defaults to cycling `chart-1..5` by index. */
20
+ color?: ChartColor
21
+ }
22
+
23
+ const COLOR_VARS: Record<ChartColor, string> = {
24
+ primary: '--primary',
25
+ accent: '--accent',
26
+ destructive: '--destructive',
27
+ muted: '--muted-foreground',
28
+ 'chart-1': '--primary',
29
+ 'chart-2': '--accent',
30
+ 'chart-3': '--data-emit',
31
+ 'chart-4': '--data-received',
32
+ 'chart-5': '--data-net',
33
+ }
34
+
35
+ const COLOR_CYCLE: ChartColor[] = ['chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5']
36
+
37
+ /** The CSS custom property (e.g. `--primary`) for a series color or index. */
38
+ export function colorVar(color: ChartColor | undefined, index: number): string {
39
+ return COLOR_VARS[color ?? COLOR_CYCLE[index % COLOR_CYCLE.length]]
40
+ }
41
+
42
+ /** `hsl(var(--token) / alpha)` for a series color. */
43
+ export function seriesColor(color: ChartColor | undefined, index: number, alpha?: number): string {
44
+ const v = colorVar(color, index)
45
+ return alpha === undefined ? `hsl(var(${v}))` : `hsl(var(${v}) / ${alpha})`
46
+ }
47
+
48
+ /** Value lookup that fails loudly when a datum lacks a declared series key. */
49
+ export function seriesValue(values: Record<string, number> | undefined, key: string, label: string): number {
50
+ const v = values?.[key]
51
+ if (v === undefined) throw new Error(`Chart datum "${label}" has no value for series "${key}"`)
52
+ return v
53
+ }
54
+
55
+ export interface ChartTooltipRow {
56
+ label: string
57
+ value: string | number
58
+ color: string
59
+ }
60
+
61
+ export interface ChartTooltipProps {
62
+ title?: string
63
+ rows: ChartTooltipRow[]
64
+ /** CSS `left` inside the (relative) chart wrapper, e.g. `42%`. */
65
+ left: string
66
+ /** CSS `top` inside the wrapper. Default `0`. */
67
+ top?: string
68
+ /** Horizontal anchor 0..1 used to keep the tooltip inside the chart. Default 0.5. */
69
+ anchor?: number
70
+ }
71
+
72
+ /**
73
+ * Floating tooltip listing every series value at the active x. Position it
74
+ * inside a `relative` wrapper; it ignores pointer events and is announced
75
+ * politely to assistive tech.
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * <ChartTooltip title="Mon" left="20%" rows={[{ label: 'Sales', value: 12, color: 'hsl(var(--primary))' }]} />
80
+ * ```
81
+ */
82
+ export function ChartTooltip(props: ChartTooltipProps): JSX.Element {
83
+ const shift = () => {
84
+ const a = props.anchor ?? 0.5
85
+ return a > 0.8 ? '-100%' : a < 0.2 ? '0%' : '-50%'
86
+ }
87
+ return (
88
+ <div
89
+ data-chart-tooltip
90
+ aria-live="polite"
91
+ class="pointer-events-none absolute z-10 whitespace-nowrap rounded-md border border-border bg-popover px-2 py-1 text-xs text-popover-foreground shadow-md"
92
+ style={{ left: props.left, top: props.top ?? '0', transform: `translate(${shift()}, -100%)` }}
93
+ >
94
+ <Show when={props.title}>
95
+ <div class="mb-0.5 font-medium text-foreground">{props.title}</div>
96
+ </Show>
97
+ <For each={props.rows}>
98
+ {(row) => (
99
+ <div class="flex items-center gap-1.5" data-chart-tooltip-row>
100
+ <span class="inline-block size-1.5 rounded-full" style={{ 'background-color': row.color }} />
101
+ <span>{row.label}:</span>
102
+ <span class="font-medium text-foreground">{row.value}</span>
103
+ </div>
104
+ )}
105
+ </For>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ export interface ChartLegendItem {
111
+ key: string
112
+ label: string
113
+ color: string
114
+ }
115
+
116
+ export interface ChartLegendProps {
117
+ items: ChartLegendItem[]
118
+ /** Keys currently hidden. */
119
+ hidden?: ReadonlySet<string>
120
+ /** When set, items become toggle buttons. */
121
+ onToggle?: (key: string) => void
122
+ class?: string
123
+ }
124
+
125
+ /**
126
+ * Legend with colored swatches. With `onToggle` each entry is a toggle button
127
+ * (`aria-pressed`) that hides / shows its series.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * <ChartLegend items={[{ key: 'a', label: 'Sales', color: 'hsl(var(--primary))' }]} onToggle={toggle} />
132
+ * ```
133
+ */
134
+ export function ChartLegend(props: ChartLegendProps): JSX.Element {
135
+ return (
136
+ <ul class={cn('flex flex-wrap gap-x-3 gap-y-1', props.class)} data-chart-legend>
137
+ <For each={props.items}>
138
+ {(item) => {
139
+ const off = () => props.hidden?.has(item.key) ?? false
140
+ const inner = (
141
+ <>
142
+ <span
143
+ class="inline-block size-2 rounded-full"
144
+ style={{
145
+ 'background-color': off() ? 'transparent' : item.color,
146
+ 'box-shadow': `inset 0 0 0 1px ${item.color}`,
147
+ }}
148
+ />
149
+ <span class={off() ? 'line-through opacity-60' : undefined}>{item.label}</span>
150
+ </>
151
+ )
152
+ return (
153
+ <li>
154
+ <Show
155
+ when={props.onToggle}
156
+ fallback={
157
+ <span class="inline-flex items-center gap-1.5 text-xs text-muted-foreground">{inner}</span>
158
+ }
159
+ >
160
+ <button
161
+ type="button"
162
+ aria-pressed={!off()}
163
+ data-series={item.key}
164
+ class="inline-flex items-center gap-1.5 rounded text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
165
+ onClick={() => props.onToggle?.(item.key)}
166
+ >
167
+ {inner}
168
+ </button>
169
+ </Show>
170
+ </li>
171
+ )
172
+ }}
173
+ </For>
174
+ </ul>
175
+ )
176
+ }
177
+
178
+ /** Signal-backed set of hidden series keys with a toggle. */
179
+ export function createHiddenSet() {
180
+ const [hidden, setHidden] = createSignal<ReadonlySet<string>>(new Set())
181
+ const toggle = (key: string) =>
182
+ setHidden((prev) => {
183
+ const next = new Set(prev)
184
+ if (next.has(key)) next.delete(key)
185
+ else next.add(key)
186
+ return next
187
+ })
188
+ return { hidden, toggle }
189
+ }
190
+
191
+ /**
192
+ * Pointer + keyboard navigation over `count` categories. Arrow keys / Home /
193
+ * End move the active index, Escape clears it; keyboard focus (focus-visible)
194
+ * activates the first one.
195
+ */
196
+ export function createChartNav(count: () => number) {
197
+ const [active, setActive] = createSignal<number | null>(null)
198
+ const onKeyDown = (e: KeyboardEvent) => {
199
+ const n = count()
200
+ if (n === 0) return
201
+ const cur = active()
202
+ let next: number | null | undefined
203
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = cur === null ? 0 : Math.min(cur + 1, n - 1)
204
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = cur === null ? n - 1 : Math.max(cur - 1, 0)
205
+ else if (e.key === 'Home') next = 0
206
+ else if (e.key === 'End') next = n - 1
207
+ else if (e.key === 'Escape' && cur !== null) next = null
208
+ if (next === undefined) return
209
+ e.preventDefault()
210
+ setActive(next)
211
+ }
212
+ const onFocus = (e: FocusEvent) => {
213
+ if (active() === null && count() > 0 && (e.currentTarget as HTMLElement).matches(':focus-visible'))
214
+ setActive(0)
215
+ }
216
+ const onBlur = () => setActive(null)
217
+ return { active, setActive, onKeyDown, onFocus, onBlur }
218
+ }
219
+
220
+ /** Common focusable-wrapper classes (visible focus ring). */
221
+ export const FOCUS_CLASS = 'rounded outline-none focus-visible:ring-2 focus-visible:ring-ring'
222
+
223
+ export interface SliceAngle {
224
+ index: number
225
+ value: number
226
+ /** Start angle in degrees, clockwise from 12 o'clock. */
227
+ start: number
228
+ /** Sweep in degrees; always `value / total * 360`. */
229
+ angle: number
230
+ }
231
+
232
+ /** Proportional angles (degrees) for a list of values; shared by Donut, Pie and RadialBar. */
233
+ export function sliceAngles(values: number[]): SliceAngle[] {
234
+ const total = values.reduce((s, v) => s + v, 0) || 1
235
+ let start = 0
236
+ return values.map((value, index) => {
237
+ const angle = (value / total) * 360
238
+ const out = { index, value, start, angle }
239
+ start += angle
240
+ return out
241
+ })
242
+ }
243
+
244
+ /** Point on a circle, angle in degrees clockwise from 12 o'clock. */
245
+ export function polar(cx: number, cy: number, r: number, deg: number): { x: number; y: number } {
246
+ const rad = ((deg - 90) * Math.PI) / 180
247
+ return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
248
+ }
249
+
250
+ /** SVG path for a pie wedge (`inner` = 0) or ring segment; handles full circles. */
251
+ export function arcPath(
252
+ cx: number,
253
+ cy: number,
254
+ outer: number,
255
+ inner: number,
256
+ start: number,
257
+ angle: number,
258
+ ): string {
259
+ const f = (n: number) => +n.toFixed(3)
260
+ if (angle >= 359.999) {
261
+ const half = (r: number, rev: boolean) => {
262
+ const a = polar(cx, cy, r, 0)
263
+ const b = polar(cx, cy, r, 180)
264
+ return `M ${f(a.x)} ${f(a.y)} A ${r} ${r} 0 1 ${rev ? 0 : 1} ${f(b.x)} ${f(b.y)} A ${r} ${r} 0 1 ${rev ? 0 : 1} ${f(a.x)} ${f(a.y)}`
265
+ }
266
+ return inner > 0 ? `${half(outer, false)} ${half(inner, true)} Z` : `${half(outer, false)} Z`
267
+ }
268
+ const end = start + angle
269
+ const large = angle > 180 ? 1 : 0
270
+ const o1 = polar(cx, cy, outer, start)
271
+ const o2 = polar(cx, cy, outer, end)
272
+ if (inner <= 0) {
273
+ return `M ${f(cx)} ${f(cy)} L ${f(o1.x)} ${f(o1.y)} A ${outer} ${outer} 0 ${large} 1 ${f(o2.x)} ${f(o2.y)} Z`
274
+ }
275
+ const i1 = polar(cx, cy, inner, start)
276
+ const i2 = polar(cx, cy, inner, end)
277
+ return `M ${f(o1.x)} ${f(o1.y)} A ${outer} ${outer} 0 ${large} 1 ${f(o2.x)} ${f(o2.y)} L ${f(i2.x)} ${f(i2.y)} A ${inner} ${inner} 0 ${large} 0 ${f(i1.x)} ${f(i1.y)} Z`
278
+ }
279
+
280
+ /**
281
+ * X-axis label row aligned with points placed edge to edge (`i / (n - 1)` of the
282
+ * width). Each label sits on its point; the first and last are anchored to the
283
+ * edges so they are not clipped.
284
+ */
285
+ export function ChartXLabels(props: { labels: string[] }): JSX.Element {
286
+ const frac = (i: number): number => (props.labels.length <= 1 ? 0.5 : i / (props.labels.length - 1))
287
+ const shift = (i: number): string =>
288
+ props.labels.length <= 1 ? '-50%' : i === 0 ? '0%' : i === props.labels.length - 1 ? '-100%' : '-50%'
289
+ return (
290
+ <div class="relative mt-1 h-4 w-full text-xs text-muted-foreground" aria-hidden="true">
291
+ <For each={props.labels}>
292
+ {(label, i) => (
293
+ <span
294
+ data-x-label
295
+ class="absolute top-0 whitespace-nowrap"
296
+ style={{ left: `${frac(i()) * 100}%`, transform: `translateX(${shift(i())})` }}
297
+ >
298
+ {label}
299
+ </span>
300
+ )}
301
+ </For>
302
+ </div>
303
+ )
304
+ }