@a4ui/core 0.37.1 → 0.38.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 (102) 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/AspectRatio.tsx +35 -0
  67. package/src/ui/Attachment.tsx +189 -0
  68. package/src/ui/ButtonGroup.tsx +89 -0
  69. package/src/ui/ChatMarker.tsx +58 -0
  70. package/src/ui/ChatThread.tsx +115 -23
  71. package/src/ui/ContactSection.tsx +199 -0
  72. package/src/ui/CtaBanner.tsx +62 -0
  73. package/src/ui/DateField.tsx +2 -1
  74. package/src/ui/DateRangePicker.tsx +2 -1
  75. package/src/ui/Drawer.tsx +37 -4
  76. package/src/ui/Faq.tsx +57 -0
  77. package/src/ui/FeatureGrid.tsx +77 -0
  78. package/src/ui/Field.tsx +374 -0
  79. package/src/ui/Hero.tsx +146 -0
  80. package/src/ui/InputGroup.tsx +219 -0
  81. package/src/ui/Item.tsx +185 -0
  82. package/src/ui/Label.tsx +50 -0
  83. package/src/ui/Menubar.tsx +212 -0
  84. package/src/ui/Message.tsx +139 -13
  85. package/src/ui/Modal.tsx +41 -5
  86. package/src/ui/MultiSelect.tsx +2 -1
  87. package/src/ui/NativeSelect.tsx +66 -0
  88. package/src/ui/NavigationMenu.tsx +262 -0
  89. package/src/ui/PromptComposer.tsx +11 -0
  90. package/src/ui/Prose.tsx +63 -0
  91. package/src/ui/Questionnaire.tsx +331 -0
  92. package/src/ui/ScrollArea.tsx +70 -0
  93. package/src/ui/Sidebar.tsx +753 -0
  94. package/src/ui/SiteFooter.tsx +112 -0
  95. package/src/ui/SiteNav.tsx +146 -0
  96. package/src/ui/TeamGrid.tsx +81 -0
  97. package/src/ui/Testimonials.tsx +103 -0
  98. package/src/ui/TimeField.tsx +2 -1
  99. package/src/ui/internal/sectionKit.tsx +72 -0
  100. package/src/ui/internal/socialIcons.tsx +41 -0
  101. package/dist/NumberInput-Bnh2u7pp.js +0 -381
  102. package/dist/motion-jYMWmZqB.js +0 -119
package/preset.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Types for `@a4ui/core/preset` — the Spatial Glass Tailwind preset.
2
+ // Requires Tailwind CSS v3 (`tailwindcss@^3.4.0`): v4 removed `presets:`.
3
+ import type { Config } from 'tailwindcss'
4
+
5
+ /**
6
+ * Tailwind v3 preset: semantic color names bound to the A4ui CSS variables, the
7
+ * glass surface classes (`.card`, `.bg-glass`, `.tile-glass`, `.glow-edge`) and
8
+ * the `light:` variant.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import type { Config } from 'tailwindcss'
13
+ * import a4ui from '@a4ui/core/preset'
14
+ *
15
+ * export default {
16
+ * presets: [a4ui],
17
+ * content: ['./src/**\/*.{ts,tsx}', './node_modules/@a4ui/core/dist/**\/*.js'],
18
+ * } satisfies Config
19
+ * ```
20
+ */
21
+ declare const preset: Partial<Config>
22
+
23
+ export default preset
@@ -0,0 +1,206 @@
1
+ // AreaChart — multi-series SVG area chart (no charting library). Series either
2
+ // overlap or stack; each area is filled with an SVG linearGradient built from
3
+ // the series' theme token, with a solid line on top. Hover or arrow keys show
4
+ // a tooltip with every series' value at that x.
5
+ import { createMemo, createUniqueId, For, Show, type JSX } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+ import {
9
+ ChartLegend,
10
+ ChartTooltip,
11
+ FOCUS_CLASS,
12
+ createChartNav,
13
+ createHiddenSet,
14
+ seriesColor,
15
+ seriesValue,
16
+ type ChartSeries,
17
+ ChartXLabels,
18
+ } from './shared'
19
+
20
+ export interface AreaDatum {
21
+ label: string
22
+ /** Per-series values keyed by `ChartSeries.key`. */
23
+ values: Record<string, number>
24
+ }
25
+
26
+ export interface AreaChartProps {
27
+ data: AreaDatum[]
28
+ series: ChartSeries[]
29
+ /** `overlap` (default) draws each series from the baseline; `stacked` accumulates them. */
30
+ mode?: 'overlap' | 'stacked'
31
+ /** Height of the plot area in px. Default 200. */
32
+ height?: number
33
+ /** Hover / keyboard tooltip. Default true. */
34
+ showTooltip?: boolean
35
+ /** Toggleable legend. Default true. */
36
+ showLegend?: boolean
37
+ class?: string
38
+ }
39
+
40
+ const VIEW_W = 400
41
+
42
+ /**
43
+ * Multi-series area chart with gradient fills. Each series is tinted by a
44
+ * theme token (`color`), overlapping or stacked. The chart is focusable:
45
+ * Left / Right arrows (Home / End) move between points and show the tooltip.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <AreaChart
50
+ * mode="stacked"
51
+ * series={[{ key: 'web', label: 'Web' }, { key: 'app', label: 'App', color: 'accent' }]}
52
+ * data={[
53
+ * { label: 'Jan', values: { web: 4, app: 2 } },
54
+ * { label: 'Feb', values: { web: 6, app: 3 } },
55
+ * ]}
56
+ * />
57
+ * ```
58
+ */
59
+ export function AreaChart(props: AreaChartProps): JSX.Element {
60
+ const uid = createUniqueId()
61
+ const height = () => props.height ?? 200
62
+ const stacked = () => props.mode === 'stacked'
63
+ const { hidden, toggle } = createHiddenSet()
64
+ const nav = createChartNav(() => props.data.length)
65
+ const val = (d: AreaDatum, key: string) => seriesValue(d.values, key, d.label)
66
+ const colorOf = (key: string) => {
67
+ const i = props.series.findIndex((s) => s.key === key)
68
+ return seriesColor(props.series[i].color, i)
69
+ }
70
+ const visible = () => props.series.filter((s) => !hidden().has(s.key))
71
+ const xFor = (i: number) => (props.data.length <= 1 ? VIEW_W / 2 : (i / (props.data.length - 1)) * VIEW_W)
72
+
73
+ const max = createMemo(() => {
74
+ const vis = visible()
75
+ const tops = props.data.map((d) =>
76
+ stacked() ? vis.reduce((s, x) => s + val(d, x.key), 0) : Math.max(0, ...vis.map((x) => val(d, x.key))),
77
+ )
78
+ return Math.max(...tops, 0) || 1
79
+ })
80
+
81
+ const layers = createMemo(() => {
82
+ const acc = props.data.map(() => 0)
83
+ return visible().map((s) => {
84
+ const base = stacked() ? [...acc] : acc.map(() => 0)
85
+ const top = props.data.map((d, i) => base[i] + val(d, s.key))
86
+ if (stacked()) top.forEach((t, i) => (acc[i] = t))
87
+ const y = (v: number) => height() - (v / max()) * height()
88
+ const upper = top.map((t, i) => `${xFor(i)},${y(t)}`)
89
+ const lower = base.map((b, i) => `${xFor(i)},${y(b)}`).reverse()
90
+ return {
91
+ key: s.key,
92
+ color: colorOf(s.key),
93
+ line: `M ${upper.join(' L ')}`,
94
+ area: `M ${upper.join(' L ')} L ${lower.join(' L ')} Z`,
95
+ }
96
+ })
97
+ })
98
+
99
+ const summary = () =>
100
+ `${stacked() ? 'Stacked area' : 'Area'} chart. ` +
101
+ props.data
102
+ .map((d) => `${d.label}: ${props.series.map((s) => `${s.label} ${val(d, s.key)}`).join(', ')}`)
103
+ .join('; ')
104
+
105
+ let svgRef: SVGSVGElement | undefined
106
+ const onPointerMove = (e: PointerEvent) => {
107
+ if (!svgRef || props.data.length === 0) return
108
+ const rect = svgRef.getBoundingClientRect()
109
+ const f = rect.width === 0 ? 0 : (e.clientX - rect.left) / rect.width
110
+ nav.setActive(
111
+ props.data.length <= 1 ? 0 : Math.round(Math.min(1, Math.max(0, f)) * (props.data.length - 1)),
112
+ )
113
+ }
114
+ const frac = () => {
115
+ const i = nav.active() ?? 0
116
+ return props.data.length <= 1 ? 0.5 : i / (props.data.length - 1)
117
+ }
118
+ const activeDatum = () => {
119
+ const i = nav.active()
120
+ return i === null ? undefined : props.data[i]
121
+ }
122
+
123
+ return (
124
+ <div class={cn('w-full', props.class)}>
125
+ <Show when={props.showLegend ?? true}>
126
+ <ChartLegend
127
+ class="mb-2"
128
+ hidden={hidden()}
129
+ onToggle={toggle}
130
+ items={props.series.map((s, i) => ({ key: s.key, label: s.label, color: seriesColor(s.color, i) }))}
131
+ />
132
+ </Show>
133
+ <div
134
+ class={cn('relative w-full', FOCUS_CLASS)}
135
+ tabindex="0"
136
+ role="img"
137
+ aria-label={summary()}
138
+ data-chart="area"
139
+ data-mode={props.mode ?? 'overlap'}
140
+ onKeyDown={nav.onKeyDown}
141
+ onFocus={nav.onFocus}
142
+ onBlur={nav.onBlur}
143
+ >
144
+ <svg
145
+ ref={svgRef}
146
+ class="block w-full overflow-visible"
147
+ viewBox={`0 0 ${VIEW_W} ${height()}`}
148
+ preserveAspectRatio="none"
149
+ height={height()}
150
+ aria-hidden="true"
151
+ onPointerMove={onPointerMove}
152
+ onPointerLeave={() => nav.setActive(null)}
153
+ >
154
+ <defs>
155
+ <For each={props.series}>
156
+ {(s, i) => (
157
+ <linearGradient id={`${uid}-${s.key}`} x1="0" y1="0" x2="0" y2="1">
158
+ <stop offset="0%" stop-color={seriesColor(s.color, i())} stop-opacity="0.55" />
159
+ <stop offset="100%" stop-color={seriesColor(s.color, i())} stop-opacity="0.05" />
160
+ </linearGradient>
161
+ )}
162
+ </For>
163
+ </defs>
164
+ <For each={layers()}>
165
+ {(l) => (
166
+ <g data-series={l.key}>
167
+ <path d={l.area} fill={`url(#${uid}-${l.key})`} stroke="none" data-area />
168
+ <path
169
+ d={l.line}
170
+ fill="none"
171
+ stroke={l.color}
172
+ stroke-width={2}
173
+ stroke-linejoin="round"
174
+ vector-effect="non-scaling-stroke"
175
+ />
176
+ </g>
177
+ )}
178
+ </For>
179
+ <Show when={nav.active() !== null}>
180
+ <line
181
+ data-cursor
182
+ x1={xFor(nav.active() ?? 0)}
183
+ x2={xFor(nav.active() ?? 0)}
184
+ y1={0}
185
+ y2={height()}
186
+ stroke="hsl(var(--border))"
187
+ stroke-width={1}
188
+ vector-effect="non-scaling-stroke"
189
+ />
190
+ </Show>
191
+ </svg>
192
+ <Show when={(props.showTooltip ?? true) && activeDatum()}>
193
+ {(d) => (
194
+ <ChartTooltip
195
+ title={d().label}
196
+ left={`${frac() * 100}%`}
197
+ anchor={frac()}
198
+ rows={visible().map((s) => ({ label: s.label, value: val(d(), s.key), color: colorOf(s.key) }))}
199
+ />
200
+ )}
201
+ </Show>
202
+ </div>
203
+ <ChartXLabels labels={props.data.map((d) => d.label)} />
204
+ </div>
205
+ )
206
+ }
@@ -2,24 +2,52 @@
2
2
  // SVG, no charting library). Bar heights are percentages of the tallest
3
3
  // value so the whole chart reflows with its container, and height changes
4
4
  // animate via `transition-all`. Colors come from theme tokens only.
5
- import { For, type JSX } from 'solid-js'
5
+ import { createMemo, For, Show, type JSX } from 'solid-js'
6
6
 
7
7
  import { cn } from '../lib/cn'
8
+ import {
9
+ ChartLegend,
10
+ ChartTooltip,
11
+ FOCUS_CLASS,
12
+ createChartNav,
13
+ createHiddenSet,
14
+ seriesColor,
15
+ seriesValue,
16
+ type ChartSeries,
17
+ } from './shared'
8
18
 
9
19
  export interface BarDatum {
10
20
  label: string
11
- value: number
21
+ /** Value for `mode="single"` (required there). */
22
+ value?: number
23
+ /** Per-series values keyed by `ChartSeries.key`, for `stacked` / `grouped`. */
24
+ values?: Record<string, number>
12
25
  }
13
26
 
27
+ export type BarChartMode = 'single' | 'stacked' | 'grouped'
28
+
14
29
  export interface BarChartProps {
15
30
  data: BarDatum[]
16
31
  /** Height of the plot area in px. Default 160. */
17
32
  height?: number
18
33
  /** Fill color token. Default 'primary'. */
19
34
  tone?: 'primary' | 'accent'
35
+ /**
36
+ * `single` (default) draws one bar per datum from `value`; `stacked` and
37
+ * `grouped` draw one segment / bar per entry of `series` from `values`.
38
+ */
39
+ mode?: BarChartMode
40
+ /** Series definitions, required for `stacked` / `grouped`. */
41
+ series?: ChartSeries[]
42
+ /** Hover / keyboard tooltip in the multi-series modes. Default true. */
43
+ showTooltip?: boolean
44
+ /** Show a toggleable legend in the multi-series modes. Default true. */
45
+ showLegend?: boolean
20
46
  class?: string
21
47
  }
22
48
 
49
+ const VIEW_W = 400
50
+
23
51
  /**
24
52
  * Vertical bar chart rendered with flexbox: each bar's height is `value /
25
53
  * max` of the plot area, filled with a theme token, with its label truncated
@@ -31,9 +59,22 @@ export interface BarChartProps {
31
59
  * ```
32
60
  */
33
61
  export function BarChart(props: BarChartProps): JSX.Element {
62
+ return (
63
+ <Show when={(props.mode ?? 'single') !== 'single'} fallback={<SingleBars {...props} />}>
64
+ <MultiBars {...props} />
65
+ </Show>
66
+ )
67
+ }
68
+
69
+ function SingleBars(props: BarChartProps): JSX.Element {
70
+ const value = (d: BarDatum): number => {
71
+ if (d.value === undefined)
72
+ throw new Error(`BarChart datum "${d.label}" needs a numeric "value" in single mode`)
73
+ return d.value
74
+ }
34
75
  const height = () => props.height ?? 160
35
76
  const tone = () => props.tone ?? 'primary'
36
- const max = () => Math.max(...props.data.map((d) => d.value), 0) || 1
77
+ const max = () => Math.max(...props.data.map(value), 0) || 1
37
78
  const barClass = () => (tone() === 'accent' ? 'bg-accent' : 'bg-primary')
38
79
 
39
80
  return (
@@ -42,9 +83,9 @@ export function BarChart(props: BarChartProps): JSX.Element {
42
83
  {(datum) => (
43
84
  <div class="flex h-full min-w-0 flex-1 flex-col items-center justify-end gap-1">
44
85
  <div
45
- title={`${datum.label}: ${datum.value}`}
86
+ title={`${datum.label}: ${value(datum)}`}
46
87
  class={cn('w-full rounded-t transition-all', barClass())}
47
- style={{ height: `${Math.max((datum.value / max()) * 100, 0)}%` }}
88
+ style={{ height: `${Math.max((value(datum) / max()) * 100, 0)}%` }}
48
89
  />
49
90
  <span class="w-full truncate text-center text-xs text-muted-foreground">{datum.label}</span>
50
91
  </div>
@@ -53,3 +94,198 @@ export function BarChart(props: BarChartProps): JSX.Element {
53
94
  </div>
54
95
  )
55
96
  }
97
+
98
+ function MultiBars(props: BarChartProps): JSX.Element {
99
+ const height = () => props.height ?? 160
100
+ const stacked = () => props.mode === 'stacked'
101
+ const series = () => {
102
+ if (!props.series || props.series.length === 0)
103
+ throw new Error(`BarChart mode="${props.mode}" requires a non-empty "series" prop`)
104
+ return props.series
105
+ }
106
+ const { hidden, toggle } = createHiddenSet()
107
+ const nav = createChartNav(() => props.data.length)
108
+ const visible = () => series().filter((s) => !hidden().has(s.key))
109
+ const val = (d: BarDatum, key: string) => seriesValue(d.values, key, d.label)
110
+
111
+ const colorOf = (key: string) => {
112
+ const i = series().findIndex((s) => s.key === key)
113
+ return seriesColor(series()[i].color, i)
114
+ }
115
+
116
+ const max = createMemo(() => {
117
+ const vis = visible()
118
+ const perCat = props.data.map((d) =>
119
+ stacked()
120
+ ? vis.reduce((sum, s) => sum + val(d, s.key), 0)
121
+ : Math.max(0, ...vis.map((s) => val(d, s.key))),
122
+ )
123
+ return Math.max(...perCat, 0) || 1
124
+ })
125
+
126
+ const bars = createMemo(() => {
127
+ const n = props.data.length
128
+ const band = VIEW_W / Math.max(n, 1)
129
+ const vis = visible()
130
+ const m = vis.length
131
+ const out: {
132
+ cat: number
133
+ key: string
134
+ label: string
135
+ value: number
136
+ x: number
137
+ y: number
138
+ w: number
139
+ h: number
140
+ color: string
141
+ }[] = []
142
+ props.data.forEach((d, cat) => {
143
+ if (stacked()) {
144
+ const w = band * 0.6
145
+ const x = cat * band + (band - w) / 2
146
+ let acc = 0
147
+ vis.forEach((s) => {
148
+ const v = val(d, s.key)
149
+ const h = (v / max()) * height()
150
+ acc += h
151
+ out.push({
152
+ cat,
153
+ key: s.key,
154
+ label: s.label,
155
+ value: v,
156
+ x,
157
+ y: height() - acc,
158
+ w,
159
+ h,
160
+ color: colorOf(s.key),
161
+ })
162
+ })
163
+ } else {
164
+ const gap = 2
165
+ const groupW = band * 0.8
166
+ const w = m > 0 ? (groupW - gap * (m - 1)) / m : 0
167
+ vis.forEach((s, k) => {
168
+ const v = val(d, s.key)
169
+ const h = (v / max()) * height()
170
+ out.push({
171
+ cat,
172
+ key: s.key,
173
+ label: s.label,
174
+ value: v,
175
+ x: cat * band + band * 0.1 + k * (w + gap),
176
+ y: height() - h,
177
+ w,
178
+ h,
179
+ color: colorOf(s.key),
180
+ })
181
+ })
182
+ }
183
+ })
184
+ return out
185
+ })
186
+
187
+ const summary = () =>
188
+ `${props.mode === 'stacked' ? 'Stacked' : 'Grouped'} bar chart. ` +
189
+ props.data
190
+ .map(
191
+ (d) =>
192
+ `${d.label}: ${series()
193
+ .map((s) => `${s.label} ${val(d, s.key)}`)
194
+ .join(', ')}`,
195
+ )
196
+ .join('; ')
197
+
198
+ let svgRef: SVGSVGElement | undefined
199
+ const onPointerMove = (e: PointerEvent) => {
200
+ if (!svgRef || props.data.length === 0) return
201
+ const rect = svgRef.getBoundingClientRect()
202
+ const f = rect.width === 0 ? 0 : (e.clientX - rect.left) / rect.width
203
+ nav.setActive(Math.min(props.data.length - 1, Math.max(0, Math.floor(f * props.data.length))))
204
+ }
205
+ const activeDatum = () => {
206
+ const i = nav.active()
207
+ return i === null ? undefined : props.data[i]
208
+ }
209
+
210
+ return (
211
+ <div class={cn('w-full', props.class)}>
212
+ <Show when={props.showLegend ?? true}>
213
+ <ChartLegend
214
+ class="mb-2"
215
+ hidden={hidden()}
216
+ onToggle={toggle}
217
+ items={series().map((s, i) => ({ key: s.key, label: s.label, color: seriesColor(s.color, i) }))}
218
+ />
219
+ </Show>
220
+ <div
221
+ class={cn('relative w-full', FOCUS_CLASS)}
222
+ tabindex="0"
223
+ role="img"
224
+ aria-label={summary()}
225
+ data-chart="bar"
226
+ data-mode={props.mode}
227
+ onKeyDown={nav.onKeyDown}
228
+ onFocus={nav.onFocus}
229
+ onBlur={nav.onBlur}
230
+ >
231
+ <svg
232
+ ref={svgRef}
233
+ class="block w-full overflow-visible"
234
+ viewBox={`0 0 ${VIEW_W} ${height()}`}
235
+ preserveAspectRatio="none"
236
+ height={height()}
237
+ aria-hidden="true"
238
+ onPointerMove={onPointerMove}
239
+ onPointerLeave={() => nav.setActive(null)}
240
+ >
241
+ <Show when={nav.active() !== null}>
242
+ <rect
243
+ x={(nav.active() ?? 0) * (VIEW_W / props.data.length)}
244
+ y={0}
245
+ width={VIEW_W / props.data.length}
246
+ height={height()}
247
+ fill="hsl(var(--muted) / 0.4)"
248
+ />
249
+ </Show>
250
+ <For each={bars()}>
251
+ {(b) => (
252
+ <rect
253
+ data-bar
254
+ data-category={props.data[b.cat].label}
255
+ data-series={b.key}
256
+ data-value={b.value}
257
+ x={b.x}
258
+ y={b.y}
259
+ width={b.w}
260
+ height={b.h}
261
+ rx={1}
262
+ fill={b.color}
263
+ />
264
+ )}
265
+ </For>
266
+ </svg>
267
+ <Show when={props.showTooltip ?? true}>
268
+ <Show when={activeDatum()}>
269
+ {(d) => (
270
+ <ChartTooltip
271
+ title={d().label}
272
+ left={`${(((nav.active() ?? 0) + 0.5) / props.data.length) * 100}%`}
273
+ anchor={((nav.active() ?? 0) + 0.5) / props.data.length}
274
+ rows={visible().map((s) => ({
275
+ label: s.label,
276
+ value: val(d(), s.key),
277
+ color: colorOf(s.key),
278
+ }))}
279
+ />
280
+ )}
281
+ </Show>
282
+ </Show>
283
+ </div>
284
+ <div class="mt-1 flex w-full text-xs text-muted-foreground">
285
+ <For each={props.data}>
286
+ {(d) => <span class="min-w-0 flex-1 truncate text-center">{d.label}</span>}
287
+ </For>
288
+ </div>
289
+ </div>
290
+ )
291
+ }
@@ -7,6 +7,7 @@
7
7
  import { For, Show, type JSX } from 'solid-js'
8
8
 
9
9
  import { cn } from '../lib/cn'
10
+ import { sliceAngles } from './shared'
10
11
 
11
12
  export type DonutTone = 'primary' | 'accent' | 'destructive' | 'emit' | 'received' | 'net'
12
13
 
@@ -71,17 +72,13 @@ export function DonutChart(props: DonutChartProps): JSX.Element {
71
72
  const circumference = () => 2 * Math.PI * radius()
72
73
  const total = () => props.data.reduce((sum, d) => sum + d.value, 0)
73
74
 
74
- const arcs = () => {
75
- const grandTotal = total() || 1
76
- let cumulative = 0
77
- return props.data.map((segment, index) => {
78
- const fraction = segment.value / grandTotal
79
- const arcLength = fraction * circumference()
80
- const offset = cumulative
81
- cumulative += arcLength
82
- return { segment, index, arcLength, offset }
83
- })
84
- }
75
+ const arcs = () =>
76
+ sliceAngles(props.data.map((d) => d.value)).map((slice) => ({
77
+ segment: props.data[slice.index],
78
+ index: slice.index,
79
+ arcLength: (slice.angle / 360) * circumference(),
80
+ offset: (slice.start / 360) * circumference(),
81
+ }))
85
82
 
86
83
  return (
87
84
  <div
@@ -6,6 +6,7 @@
6
6
  import { createSignal, For, Show, type JSX } from 'solid-js'
7
7
 
8
8
  import { cn } from '../lib/cn'
9
+ import { ChartXLabels } from './shared'
9
10
 
10
11
  export interface LineSeries {
11
12
  name?: string
@@ -255,9 +256,7 @@ export function LineChart(props: LineChartProps): JSX.Element {
255
256
  </div>
256
257
 
257
258
  <Show when={props.labels && props.labels.length > 0}>
258
- <div class="mt-1 flex w-full text-xs text-muted-foreground">
259
- <For each={props.labels}>{(label) => <span class="flex-1 truncate text-center">{label}</span>}</For>
260
- </div>
259
+ <ChartXLabels labels={props.labels ?? []} />
261
260
  </Show>
262
261
  </div>
263
262
  )