@w5-ui/react 0.1.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 (164) hide show
  1. package/README.md +9 -0
  2. package/package.json +59 -0
  3. package/src/components/Accordion.tsx +65 -0
  4. package/src/components/ActionMenu.tsx +131 -0
  5. package/src/components/ActivityFeed.tsx +77 -0
  6. package/src/components/Alert.tsx +33 -0
  7. package/src/components/AnnotatedTimeSeries.tsx +148 -0
  8. package/src/components/AppShell.tsx +158 -0
  9. package/src/components/ArcDiagram.tsx +114 -0
  10. package/src/components/AreaChart.tsx +101 -0
  11. package/src/components/Avatar.tsx +37 -0
  12. package/src/components/AvatarGroup.tsx +36 -0
  13. package/src/components/Badge.tsx +36 -0
  14. package/src/components/Banner.tsx +49 -0
  15. package/src/components/BarChart.tsx +106 -0
  16. package/src/components/BeeswarmChart.tsx +142 -0
  17. package/src/components/BoxPlot.tsx +142 -0
  18. package/src/components/Breadcrumb.tsx +44 -0
  19. package/src/components/BubbleChart.tsx +115 -0
  20. package/src/components/BulletChart.tsx +121 -0
  21. package/src/components/BumpChart.tsx +127 -0
  22. package/src/components/Button.tsx +61 -0
  23. package/src/components/CIDRInput.tsx +191 -0
  24. package/src/components/CalendarHeatmap.tsx +117 -0
  25. package/src/components/Callout.tsx +82 -0
  26. package/src/components/CandlestickChart.tsx +113 -0
  27. package/src/components/Card.tsx +27 -0
  28. package/src/components/Carousel.tsx +78 -0
  29. package/src/components/Checkbox.tsx +42 -0
  30. package/src/components/ChordDiagram.tsx +133 -0
  31. package/src/components/CirclePacking.tsx +189 -0
  32. package/src/components/CodeBlock.tsx +27 -0
  33. package/src/components/ColorPicker.tsx +120 -0
  34. package/src/components/ColumnToggle.tsx +106 -0
  35. package/src/components/Combobox.tsx +211 -0
  36. package/src/components/CommandPalette.tsx +168 -0
  37. package/src/components/ConfirmDialog.tsx +114 -0
  38. package/src/components/ContextMenu.tsx +127 -0
  39. package/src/components/ContextualHelp.tsx +69 -0
  40. package/src/components/ContourPlot.tsx +255 -0
  41. package/src/components/CopyButton.tsx +47 -0
  42. package/src/components/CorrelationMatrix.tsx +107 -0
  43. package/src/components/CountUp.tsx +70 -0
  44. package/src/components/CumulativeDistribution.tsx +155 -0
  45. package/src/components/DatePicker.tsx +315 -0
  46. package/src/components/DateRangePicker.tsx +327 -0
  47. package/src/components/Dendrogram.tsx +150 -0
  48. package/src/components/Donut.tsx +101 -0
  49. package/src/components/DotPlot.tsx +195 -0
  50. package/src/components/Drawer.tsx +81 -0
  51. package/src/components/DualAxisChart.tsx +209 -0
  52. package/src/components/DumbbellChart.tsx +102 -0
  53. package/src/components/DurationInput.tsx +144 -0
  54. package/src/components/EmptyState.tsx +40 -0
  55. package/src/components/ErrorBandChart.tsx +124 -0
  56. package/src/components/ExpandableRow.tsx +58 -0
  57. package/src/components/FileUpload.tsx +129 -0
  58. package/src/components/FilterBuilder.tsx +217 -0
  59. package/src/components/FlameGraph.tsx +119 -0
  60. package/src/components/ForceGraph.tsx +149 -0
  61. package/src/components/FunnelChart.tsx +79 -0
  62. package/src/components/GanttChart.tsx +96 -0
  63. package/src/components/Gauge.tsx +71 -0
  64. package/src/components/GroupedList.tsx +125 -0
  65. package/src/components/HealthBar.tsx +25 -0
  66. package/src/components/HeatMap.tsx +106 -0
  67. package/src/components/HexbinChart.tsx +152 -0
  68. package/src/components/Histogram.tsx +121 -0
  69. package/src/components/HorizonChart.tsx +110 -0
  70. package/src/components/HoverCard.tsx +52 -0
  71. package/src/components/IPInput.tsx +147 -0
  72. package/src/components/IcicleChart.tsx +118 -0
  73. package/src/components/InlineEdit.tsx +119 -0
  74. package/src/components/Input.tsx +94 -0
  75. package/src/components/InputGroup.tsx +21 -0
  76. package/src/components/JsonViewer.tsx +116 -0
  77. package/src/components/KVTable.tsx +29 -0
  78. package/src/components/KanbanBoard.tsx +171 -0
  79. package/src/components/Kbd.tsx +22 -0
  80. package/src/components/LineChart.tsx +112 -0
  81. package/src/components/LogViewer.tsx +102 -0
  82. package/src/components/LollipopChart.tsx +110 -0
  83. package/src/components/MACInput.tsx +164 -0
  84. package/src/components/MarimekkoChart.tsx +143 -0
  85. package/src/components/MatrixChart.tsx +118 -0
  86. package/src/components/Menubar.tsx +194 -0
  87. package/src/components/MirroredBarChart.tsx +148 -0
  88. package/src/components/Modal.tsx +87 -0
  89. package/src/components/MultiSelect.tsx +232 -0
  90. package/src/components/NightingaleChart.tsx +101 -0
  91. package/src/components/NotificationPanel.tsx +187 -0
  92. package/src/components/NumberInput.tsx +129 -0
  93. package/src/components/OTPInput.tsx +135 -0
  94. package/src/components/Pagination.tsx +73 -0
  95. package/src/components/ParallelCoordinates.tsx +149 -0
  96. package/src/components/ParetoChart.tsx +159 -0
  97. package/src/components/PasswordInput.tsx +112 -0
  98. package/src/components/PieChart.tsx +109 -0
  99. package/src/components/Pill.tsx +26 -0
  100. package/src/components/PolarHeatmap.tsx +118 -0
  101. package/src/components/Popover.tsx +103 -0
  102. package/src/components/ProgressBar.tsx +33 -0
  103. package/src/components/PunchCard.tsx +93 -0
  104. package/src/components/QuadrantChart.tsx +256 -0
  105. package/src/components/RadarChart.tsx +119 -0
  106. package/src/components/RadialBarChart.tsx +103 -0
  107. package/src/components/RadioGroup.tsx +69 -0
  108. package/src/components/RangeSlider.tsx +116 -0
  109. package/src/components/RankedList.tsx +61 -0
  110. package/src/components/ResizablePanel.tsx +122 -0
  111. package/src/components/RidgelinePlot.tsx +155 -0
  112. package/src/components/SankeyDiagram.tsx +156 -0
  113. package/src/components/ScatterPlot.tsx +96 -0
  114. package/src/components/SegmentedControl.tsx +78 -0
  115. package/src/components/Select.tsx +158 -0
  116. package/src/components/SelectionToolbar.tsx +55 -0
  117. package/src/components/Signal.tsx +19 -0
  118. package/src/components/Skeleton.tsx +37 -0
  119. package/src/components/SkipLink.tsx +19 -0
  120. package/src/components/Slider.tsx +74 -0
  121. package/src/components/SlopeChart.tsx +205 -0
  122. package/src/components/SortableList.tsx +130 -0
  123. package/src/components/SortableTable.tsx +46 -0
  124. package/src/components/Sparkline.tsx +50 -0
  125. package/src/components/SparklineMatrix.tsx +117 -0
  126. package/src/components/Spinner.tsx +8 -0
  127. package/src/components/SplitButton.tsx +151 -0
  128. package/src/components/Spoiler.tsx +47 -0
  129. package/src/components/StackedBarChart.tsx +145 -0
  130. package/src/components/StackedProgress.tsx +47 -0
  131. package/src/components/StarRating.tsx +103 -0
  132. package/src/components/Stat.tsx +48 -0
  133. package/src/components/StatusIndicator.tsx +26 -0
  134. package/src/components/StepChart.tsx +130 -0
  135. package/src/components/Stepper.tsx +51 -0
  136. package/src/components/StreamGraph.tsx +97 -0
  137. package/src/components/StripeChart.tsx +76 -0
  138. package/src/components/SunburstChart.tsx +142 -0
  139. package/src/components/SwitchPortGrid.tsx +59 -0
  140. package/src/components/Tabs.tsx +91 -0
  141. package/src/components/Tag.tsx +37 -0
  142. package/src/components/TagInput.tsx +99 -0
  143. package/src/components/Textarea.tsx +9 -0
  144. package/src/components/ThresholdAreaChart.tsx +113 -0
  145. package/src/components/TimePicker.tsx +144 -0
  146. package/src/components/TimeRange.tsx +152 -0
  147. package/src/components/Timeline.tsx +31 -0
  148. package/src/components/Toast.tsx +72 -0
  149. package/src/components/Toggle.tsx +68 -0
  150. package/src/components/ToggleGroup.tsx +57 -0
  151. package/src/components/Tooltip.tsx +25 -0
  152. package/src/components/TransferList.tsx +227 -0
  153. package/src/components/TreeMap.tsx +126 -0
  154. package/src/components/TreeView.tsx +221 -0
  155. package/src/components/UptimeTimeline.tsx +138 -0
  156. package/src/components/VennDiagram.tsx +151 -0
  157. package/src/components/ViolinPlot.tsx +158 -0
  158. package/src/components/VirtualList.tsx +59 -0
  159. package/src/components/WaffleChart.tsx +79 -0
  160. package/src/components/WaterfallChart.tsx +164 -0
  161. package/src/components/WordCloud.tsx +107 -0
  162. package/src/icons.tsx +241 -0
  163. package/src/index.ts +160 -0
  164. package/src/styles.css +2 -0
@@ -0,0 +1,93 @@
1
+ export interface PunchCardProps {
2
+ data: number[][]
3
+ rowLabels?: string[]
4
+ colLabels?: string[]
5
+ color?: string
6
+ height?: number
7
+ ariaLabel?: string
8
+ }
9
+
10
+ const VW = 400
11
+ const PAD = { t: 16, r: 8, b: 28, l: 28 }
12
+ const DEFAULT_ROWS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
13
+
14
+ export function PunchCard({
15
+ data,
16
+ rowLabels,
17
+ colLabels,
18
+ color = '#006FFF',
19
+ height = 180,
20
+ ariaLabel = 'Punch card chart',
21
+ }: PunchCardProps) {
22
+ if (!data.length || !data[0]?.length) return null
23
+
24
+ const rows = data.length
25
+ const cols = data[0].length
26
+ const chartW = VW - PAD.l - PAD.r
27
+ const chartH = height - PAD.t - PAD.b
28
+ const cellW = chartW / cols
29
+ const cellH = chartH / rows
30
+ const maxR = (Math.min(cellW, cellH) / 2) * 0.85
31
+ const maxVal = Math.max(...data.flat(), 1)
32
+
33
+ const rLabels = rowLabels ?? DEFAULT_ROWS.slice(0, rows)
34
+ const cLabels =
35
+ colLabels ??
36
+ Array.from({ length: cols }, (_, i) => (i % 6 === 0 ? String(i).padStart(2, '0') : ''))
37
+
38
+ return (
39
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
40
+ <svg
41
+ viewBox={`0 0 ${VW} ${height}`}
42
+ style={{ width: '100%', height: 'auto', display: 'block' }}
43
+ aria-hidden="true"
44
+ focusable="false"
45
+ >
46
+ {rLabels.map((lbl, ri) => (
47
+ <text
48
+ key={ri}
49
+ x={PAD.l - 4}
50
+ y={PAD.t + (ri + 0.5) * cellH + 3}
51
+ fontSize={8}
52
+ fill="#6E7079"
53
+ textAnchor="end"
54
+ fontFamily="inherit"
55
+ >
56
+ {lbl}
57
+ </text>
58
+ ))}
59
+ {cLabels.map((lbl, ci) =>
60
+ lbl ? (
61
+ <text
62
+ key={ci}
63
+ x={PAD.l + (ci + 0.5) * cellW}
64
+ y={height - 6}
65
+ fontSize={7}
66
+ fill="#6E7079"
67
+ textAnchor="middle"
68
+ fontFamily="inherit"
69
+ >
70
+ {lbl}
71
+ </text>
72
+ ) : null,
73
+ )}
74
+ {data.flatMap((row, ri) =>
75
+ row.map((val, ci) => {
76
+ const r = Math.sqrt(val / maxVal) * maxR
77
+ if (r < 0.4) return null
78
+ return (
79
+ <circle
80
+ key={`${ri}-${ci}`}
81
+ cx={PAD.l + (ci + 0.5) * cellW}
82
+ cy={PAD.t + (ri + 0.5) * cellH}
83
+ r={r}
84
+ fill={color}
85
+ fillOpacity={0.72}
86
+ />
87
+ )
88
+ }),
89
+ )}
90
+ </svg>
91
+ </div>
92
+ )
93
+ }
@@ -0,0 +1,256 @@
1
+ export interface QuadrantPoint {
2
+ x: number
3
+ y: number
4
+ label?: string
5
+ color?: string
6
+ }
7
+
8
+ export interface QuadrantChartProps {
9
+ points: QuadrantPoint[]
10
+ xThreshold: number
11
+ yThreshold: number
12
+ xRange?: [number, number]
13
+ yRange?: [number, number]
14
+ /** Labels: [top-left, top-right, bottom-left, bottom-right] */
15
+ quadrantLabels?: [string, string, string, string]
16
+ /** Fill colors for each quadrant zone */
17
+ quadrantColors?: [string, string, string, string]
18
+ xLabel?: string
19
+ yLabel?: string
20
+ height?: number
21
+ ariaLabel?: string
22
+ }
23
+
24
+ const VW = 360
25
+ const PAD = { t: 8, r: 8, b: 28, l: 36 }
26
+
27
+ const DEFAULT_LABELS: [string, string, string, string] = ['', '', '', '']
28
+ const DEFAULT_COLORS: [string, string, string, string] = [
29
+ 'rgba(0,200,117,0.06)',
30
+ 'rgba(245,166,35,0.07)',
31
+ 'rgba(0,111,255,0.06)',
32
+ 'rgba(255,123,123,0.09)',
33
+ ]
34
+
35
+ export function QuadrantChart({
36
+ points,
37
+ xThreshold,
38
+ yThreshold,
39
+ xRange,
40
+ yRange,
41
+ quadrantLabels = DEFAULT_LABELS,
42
+ quadrantColors = DEFAULT_COLORS,
43
+ xLabel,
44
+ yLabel,
45
+ height = 200,
46
+ ariaLabel = 'Quadrant chart',
47
+ }: QuadrantChartProps) {
48
+ if (!points.length) return null
49
+
50
+ const xs = points.map((p) => p.x)
51
+ const ys = points.map((p) => p.y)
52
+ const x0 = xRange?.[0] ?? Math.min(...xs, xThreshold - 1)
53
+ const x1 = xRange?.[1] ?? Math.max(...xs, xThreshold + 1)
54
+ const y0 = yRange?.[0] ?? Math.min(...ys, yThreshold - 1)
55
+ const y1 = yRange?.[1] ?? Math.max(...ys, yThreshold + 1)
56
+ const xSpan = x1 - x0 || 1
57
+ const ySpan = y1 - y0 || 1
58
+ const cw = VW - PAD.l - PAD.r
59
+ const ch = height - PAD.t - PAD.b
60
+
61
+ const toX = (x: number) => PAD.l + ((x - x0) / xSpan) * cw
62
+ const toY = (y: number) => PAD.t + ch - ((y - y0) / ySpan) * ch
63
+
64
+ const tx = toX(xThreshold)
65
+ const ty = toY(yThreshold)
66
+
67
+ const xTicks = [x0, xThreshold, x1]
68
+ const yTicks = [y0, yThreshold, y1]
69
+
70
+ return (
71
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
72
+ <svg
73
+ viewBox={`0 0 ${VW} ${height}`}
74
+ style={{ width: '100%', height: 'auto', display: 'block' }}
75
+ aria-hidden="true"
76
+ focusable="false"
77
+ >
78
+ {/* Quadrant zones */}
79
+ <rect x={PAD.l} y={PAD.t} width={tx - PAD.l} height={ty - PAD.t} fill={quadrantColors[0]} />
80
+ <rect
81
+ x={tx}
82
+ y={PAD.t}
83
+ width={PAD.l + cw - tx}
84
+ height={ty - PAD.t}
85
+ fill={quadrantColors[1]}
86
+ />
87
+ <rect
88
+ x={PAD.l}
89
+ y={ty}
90
+ width={tx - PAD.l}
91
+ height={PAD.t + ch - ty}
92
+ fill={quadrantColors[2]}
93
+ />
94
+ <rect
95
+ x={tx}
96
+ y={ty}
97
+ width={PAD.l + cw - tx}
98
+ height={PAD.t + ch - ty}
99
+ fill={quadrantColors[3]}
100
+ />
101
+
102
+ {/* Grid border */}
103
+ <rect
104
+ x={PAD.l}
105
+ y={PAD.t}
106
+ width={cw}
107
+ height={ch}
108
+ fill="none"
109
+ stroke="rgba(255,255,255,0.08)"
110
+ strokeWidth={1}
111
+ />
112
+
113
+ {/* Threshold lines */}
114
+ <line
115
+ x1={tx}
116
+ y1={PAD.t}
117
+ x2={tx}
118
+ y2={PAD.t + ch}
119
+ stroke="rgba(255,255,255,0.22)"
120
+ strokeWidth={1}
121
+ strokeDasharray="4 3"
122
+ />
123
+ <line
124
+ x1={PAD.l}
125
+ y1={ty}
126
+ x2={PAD.l + cw}
127
+ y2={ty}
128
+ stroke="rgba(255,255,255,0.22)"
129
+ strokeWidth={1}
130
+ strokeDasharray="4 3"
131
+ />
132
+
133
+ {/* Quadrant labels */}
134
+ {quadrantLabels[0] && (
135
+ <text
136
+ x={PAD.l + 5}
137
+ y={PAD.t + 11}
138
+ fill="rgba(255,255,255,0.22)"
139
+ fontSize={9}
140
+ fontFamily="inherit"
141
+ >
142
+ {quadrantLabels[0]}
143
+ </text>
144
+ )}
145
+ {quadrantLabels[1] && (
146
+ <text
147
+ x={PAD.l + cw - 5}
148
+ y={PAD.t + 11}
149
+ fill="rgba(255,255,255,0.22)"
150
+ fontSize={9}
151
+ fontFamily="inherit"
152
+ textAnchor="end"
153
+ >
154
+ {quadrantLabels[1]}
155
+ </text>
156
+ )}
157
+ {quadrantLabels[2] && (
158
+ <text
159
+ x={PAD.l + 5}
160
+ y={PAD.t + ch - 5}
161
+ fill="rgba(255,255,255,0.22)"
162
+ fontSize={9}
163
+ fontFamily="inherit"
164
+ >
165
+ {quadrantLabels[2]}
166
+ </text>
167
+ )}
168
+ {quadrantLabels[3] && (
169
+ <text
170
+ x={PAD.l + cw - 5}
171
+ y={PAD.t + ch - 5}
172
+ fill="rgba(255,255,255,0.22)"
173
+ fontSize={9}
174
+ fontFamily="inherit"
175
+ textAnchor="end"
176
+ >
177
+ {quadrantLabels[3]}
178
+ </text>
179
+ )}
180
+
181
+ {/* Y-axis ticks */}
182
+ {yTicks.map((v, i) => (
183
+ <text
184
+ key={i}
185
+ x={PAD.l - 4}
186
+ y={toY(v) + 3}
187
+ fill="#6E7079"
188
+ fontSize={8}
189
+ textAnchor="end"
190
+ fontFamily="inherit"
191
+ >
192
+ {Math.round(v)}
193
+ </text>
194
+ ))}
195
+
196
+ {/* X-axis ticks */}
197
+ {xTicks.map((v, i) => (
198
+ <text
199
+ key={i}
200
+ x={toX(v)}
201
+ y={height - 4}
202
+ fill="#6E7079"
203
+ fontSize={8}
204
+ textAnchor="middle"
205
+ fontFamily="inherit"
206
+ >
207
+ {Math.round(v)}
208
+ </text>
209
+ ))}
210
+
211
+ {/* Axis labels */}
212
+ {xLabel && (
213
+ <text
214
+ x={PAD.l + cw / 2}
215
+ y={height - 2}
216
+ fill="#6E7079"
217
+ fontSize={8}
218
+ textAnchor="middle"
219
+ fontFamily="inherit"
220
+ >
221
+ {xLabel}
222
+ </text>
223
+ )}
224
+ {yLabel && (
225
+ <text
226
+ x={8}
227
+ y={PAD.t + ch / 2}
228
+ fill="#6E7079"
229
+ fontSize={8}
230
+ textAnchor="middle"
231
+ fontFamily="inherit"
232
+ transform={`rotate(-90, 8, ${PAD.t + ch / 2})`}
233
+ >
234
+ {yLabel}
235
+ </text>
236
+ )}
237
+
238
+ {/* Points */}
239
+ {points.map((p, i) => {
240
+ const cx = toX(p.x)
241
+ const cy = toY(p.y)
242
+ return (
243
+ <g key={i}>
244
+ <circle cx={cx} cy={cy} r={5} fill={p.color ?? '#006FFF'} fillOpacity={0.85} />
245
+ {p.label && (
246
+ <text x={cx + 7} y={cy + 3} fill="#CDD0DB" fontSize={8} fontFamily="inherit">
247
+ {p.label}
248
+ </text>
249
+ )}
250
+ </g>
251
+ )
252
+ })}
253
+ </svg>
254
+ </div>
255
+ )
256
+ }
@@ -0,0 +1,119 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface RadarSeries {
4
+ label: string
5
+ color: string
6
+ values: number[] // each normalised 0–1
7
+ }
8
+
9
+ export interface RadarChartProps {
10
+ series: RadarSeries[]
11
+ axes: string[]
12
+ height?: number
13
+ ariaLabel?: string
14
+ }
15
+
16
+ const VW = 400
17
+ const PAD = 36
18
+
19
+ function pt(cx: number, cy: number, r: number, angle: number) {
20
+ return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)] as const
21
+ }
22
+
23
+ function pts(coords: readonly (readonly [number, number])[]) {
24
+ return coords.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(' ')
25
+ }
26
+
27
+ export function RadarChart({
28
+ series,
29
+ axes,
30
+ height = 200,
31
+ ariaLabel = 'Radar chart',
32
+ }: RadarChartProps) {
33
+ const { rings, axisLines, axisLabels, polygons } = useMemo(() => {
34
+ const n = axes.length
35
+ if (n < 3) return { rings: [], axisLines: [], axisLabels: [], polygons: [] }
36
+
37
+ const cx = VW / 2
38
+ const cy = height / 2
39
+ const R = Math.min(cx, cy) - PAD
40
+ const angles = axes.map((_, i) => (2 * Math.PI * i) / n - Math.PI / 2)
41
+
42
+ const rings = [0.25, 0.5, 0.75, 1.0].map((f) => pts(angles.map((a) => pt(cx, cy, R * f, a))))
43
+
44
+ const axisLines = angles.map((a) => {
45
+ const [x, y] = pt(cx, cy, R, a)
46
+ return { x: x.toFixed(1), y: y.toFixed(1) }
47
+ })
48
+
49
+ const axisLabels = axes.map((label, i) => {
50
+ const [x, y] = pt(cx, cy, R + 16, angles[i])
51
+ const cos = Math.cos(angles[i])
52
+ const anchor: 'middle' | 'end' | 'start' =
53
+ Math.abs(cos) < 0.3 ? 'middle' : cos < 0 ? 'end' : 'start'
54
+ return { x: x.toFixed(1), y: y.toFixed(1), label, anchor }
55
+ })
56
+
57
+ const polygons = series.map((s) => ({
58
+ pts: pts(angles.map((a, i) => pt(cx, cy, R * Math.max(0, Math.min(1, s.values[i] ?? 0)), a))),
59
+ color: s.color,
60
+ label: s.label,
61
+ }))
62
+
63
+ return { rings, axisLines, axisLabels, polygons }
64
+ }, [series, axes, height])
65
+
66
+ const cx = VW / 2
67
+ const cy = height / 2
68
+
69
+ return (
70
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
71
+ <svg
72
+ viewBox={`0 0 ${VW} ${height}`}
73
+ style={{ width: '100%', height: 'auto', display: 'block' }}
74
+ aria-hidden="true"
75
+ focusable="false"
76
+ >
77
+ {rings.map((p, i) => (
78
+ <polygon key={i} points={p} fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth={1} />
79
+ ))}
80
+ {axisLines.map((end, i) => (
81
+ <line
82
+ key={i}
83
+ x1={cx}
84
+ y1={cy}
85
+ x2={end.x}
86
+ y2={end.y}
87
+ stroke="rgba(255,255,255,0.08)"
88
+ strokeWidth={1}
89
+ />
90
+ ))}
91
+ {polygons.map((p) => (
92
+ <polygon
93
+ key={p.label}
94
+ points={p.pts}
95
+ fill={p.color}
96
+ fillOpacity={0.12}
97
+ stroke={p.color}
98
+ strokeWidth={1.5}
99
+ strokeLinejoin="round"
100
+ />
101
+ ))}
102
+ {axisLabels.map((l) => (
103
+ <text
104
+ key={l.label}
105
+ x={l.x}
106
+ y={l.y}
107
+ textAnchor={l.anchor}
108
+ fill="#6E7079"
109
+ fontSize={9}
110
+ fontFamily="inherit"
111
+ dominantBaseline="middle"
112
+ >
113
+ {l.label}
114
+ </text>
115
+ ))}
116
+ </svg>
117
+ </div>
118
+ )
119
+ }
@@ -0,0 +1,103 @@
1
+ export interface RadialBarItem {
2
+ label: string
3
+ value: number
4
+ max: number
5
+ color?: string
6
+ unit?: string
7
+ }
8
+
9
+ export interface RadialBarChartProps {
10
+ items: RadialBarItem[]
11
+ ariaLabel?: string
12
+ }
13
+
14
+ const W = 280
15
+ const H = 160
16
+ const CX = 80
17
+ const CY = 80
18
+ const RING_W = 9
19
+ const RING_GAP = 5
20
+ const MIN_R = 20
21
+ const LABEL_X = 170
22
+ const ITEM_H = 22
23
+
24
+ function arcPath(r: number, frac: number): string {
25
+ if (frac <= 0) return ''
26
+ const start = -Math.PI / 2
27
+ const clamped = Math.min(frac, 1)
28
+ const x1 = (CX + r * Math.cos(start)).toFixed(2)
29
+ const y1 = (CY + r * Math.sin(start)).toFixed(2)
30
+ if (clamped >= 1) {
31
+ const xm = (CX + r * Math.cos(start + Math.PI)).toFixed(2)
32
+ const ym = (CY + r * Math.sin(start + Math.PI)).toFixed(2)
33
+ return `M${x1},${y1} A${r},${r} 0 1 1 ${xm},${ym} A${r},${r} 0 1 1 ${x1},${y1}`
34
+ }
35
+ const sweep = clamped * 2 * Math.PI
36
+ const end = start + sweep
37
+ const x2 = (CX + r * Math.cos(end)).toFixed(2)
38
+ const y2 = (CY + r * Math.sin(end)).toFixed(2)
39
+ return `M${x1},${y1} A${r},${r} 0 ${sweep > Math.PI ? 1 : 0} 1 ${x2},${y2}`
40
+ }
41
+
42
+ export function RadialBarChart({ items, ariaLabel = 'Radial bar chart' }: RadialBarChartProps) {
43
+ if (!items.length) return null
44
+ const n = items.length
45
+ const yStart = CY - ((n - 1) * ITEM_H) / 2
46
+
47
+ return (
48
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
49
+ <svg
50
+ viewBox={`0 0 ${W} ${H}`}
51
+ style={{ width: '100%', height: 'auto', display: 'block' }}
52
+ aria-hidden="true"
53
+ focusable="false"
54
+ >
55
+ {items.map((item, i) => {
56
+ const r = MIN_R + (n - 1 - i) * (RING_W + RING_GAP)
57
+ const frac = Math.min(item.value / (item.max || 1), 1)
58
+ const color = item.color ?? '#006FFF'
59
+ const ly = yStart + i * ITEM_H
60
+ const d = arcPath(r, frac)
61
+ const pct = Math.round(frac * 100)
62
+ return (
63
+ <g key={i}>
64
+ <circle
65
+ cx={CX}
66
+ cy={CY}
67
+ r={r}
68
+ fill="none"
69
+ stroke="rgba(255,255,255,0.06)"
70
+ strokeWidth={RING_W}
71
+ />
72
+ {d && (
73
+ <path d={d} fill="none" stroke={color} strokeWidth={RING_W} strokeLinecap="round" />
74
+ )}
75
+ <circle cx={LABEL_X} cy={ly} r={3.5} fill={color} />
76
+ <text
77
+ x={LABEL_X + 10}
78
+ y={ly - 2}
79
+ fill="#C8C9D0"
80
+ fontSize={9}
81
+ fontFamily="inherit"
82
+ dominantBaseline="auto"
83
+ >
84
+ {item.label}
85
+ </text>
86
+ <text
87
+ x={LABEL_X + 10}
88
+ y={ly + 9}
89
+ fill="#6E7079"
90
+ fontSize={8}
91
+ fontFamily="inherit"
92
+ dominantBaseline="auto"
93
+ >
94
+ {item.value}
95
+ {item.unit ?? ''} · {pct}%
96
+ </text>
97
+ </g>
98
+ )
99
+ })}
100
+ </svg>
101
+ </div>
102
+ )
103
+ }
@@ -0,0 +1,69 @@
1
+ import { useId } from 'react'
2
+ import type { ReactNode } from 'react'
3
+
4
+ export interface RadioOption {
5
+ value: string
6
+ label: ReactNode
7
+ description?: string
8
+ disabled?: boolean
9
+ }
10
+
11
+ export interface RadioGroupProps {
12
+ legend: string
13
+ name?: string
14
+ options: RadioOption[]
15
+ value?: string
16
+ defaultValue?: string
17
+ onChange?: (value: string) => void
18
+ srOnlyLegend?: boolean
19
+ horizontal?: boolean
20
+ }
21
+
22
+ export function RadioGroup({
23
+ legend,
24
+ name,
25
+ options,
26
+ value,
27
+ defaultValue,
28
+ onChange,
29
+ srOnlyLegend = false,
30
+ horizontal = false,
31
+ }: RadioGroupProps) {
32
+ const groupId = useId()
33
+ const groupName = name ?? groupId
34
+ const cls = ['radio-group', horizontal && 'radio-group--h'].filter(Boolean).join(' ')
35
+
36
+ return (
37
+ <fieldset className={cls}>
38
+ <legend className={srOnlyLegend ? 'sr-only' : 'radio-group__legend'}>{legend}</legend>
39
+ {options.map((opt) => {
40
+ const optId = `${groupName}-${opt.value}`
41
+ return (
42
+ <label
43
+ key={opt.value}
44
+ htmlFor={optId}
45
+ className={['radio-option', opt.disabled && 'radio-option--disabled']
46
+ .filter(Boolean)
47
+ .join(' ')}
48
+ >
49
+ <input
50
+ type="radio"
51
+ id={optId}
52
+ name={groupName}
53
+ value={opt.value}
54
+ checked={value !== undefined ? value === opt.value : undefined}
55
+ defaultChecked={defaultValue !== undefined ? defaultValue === opt.value : undefined}
56
+ disabled={opt.disabled}
57
+ onChange={() => onChange?.(opt.value)}
58
+ className="radio"
59
+ />
60
+ <span className="radio-text">
61
+ <span>{opt.label}</span>
62
+ {opt.description && <span className="radio-description">{opt.description}</span>}
63
+ </span>
64
+ </label>
65
+ )
66
+ })}
67
+ </fieldset>
68
+ )
69
+ }