@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,209 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface DualAxisSeries {
4
+ label: string
5
+ color: string
6
+ values: number[]
7
+ }
8
+
9
+ export interface DualAxisChartProps {
10
+ bars: DualAxisSeries
11
+ line: DualAxisSeries
12
+ labels?: string[]
13
+ height?: number
14
+ ariaLabel?: string
15
+ }
16
+
17
+ const VW = 400
18
+ const PAD = { t: 16, r: 44, b: 28, l: 44 }
19
+
20
+ function fmtTick(n: number): string {
21
+ if (n >= 1000) return `${(n / 1000).toFixed(0)}k`
22
+ return String(Math.round(n))
23
+ }
24
+
25
+ export function DualAxisChart({
26
+ bars,
27
+ line,
28
+ labels = [],
29
+ height = 160,
30
+ ariaLabel = 'Dual axis chart',
31
+ }: DualAxisChartProps) {
32
+ const { barRects, linePath, lineArea, gridLines, leftTicks, rightTicks } = useMemo(() => {
33
+ const n = bars.values.length
34
+ if (!n)
35
+ return {
36
+ barRects: [],
37
+ linePath: '',
38
+ lineArea: '',
39
+ gridLines: [],
40
+ leftTicks: [],
41
+ rightTicks: [],
42
+ }
43
+
44
+ const chartW = VW - PAD.l - PAD.r
45
+ const chartH = height - PAD.t - PAD.b
46
+ const bot = PAD.t + chartH
47
+ const gw = chartW / n
48
+ const barW = gw * 0.55
49
+
50
+ const barMax = Math.max(...bars.values, 1)
51
+ const lineMin = Math.min(...line.values)
52
+ const lineMax = Math.max(...line.values, lineMin + 1)
53
+ const lineRange = lineMax - lineMin || 1
54
+
55
+ const toBarH = (v: number) => (v / barMax) * chartH
56
+ const toLineX = (i: number) => PAD.l + (n > 1 ? (i / (n - 1)) * chartW : chartW / 2)
57
+ const toLineY = (v: number) => PAD.t + chartH - ((v - lineMin) / lineRange) * chartH
58
+
59
+ const gridLines = [0, 0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH)
60
+
61
+ const barRects = bars.values.map((v, i) => ({
62
+ key: i,
63
+ x: (PAD.l + i * gw + gw / 2 - barW / 2).toFixed(1),
64
+ y: (bot - toBarH(v)).toFixed(1),
65
+ w: barW.toFixed(1),
66
+ h: toBarH(v).toFixed(1),
67
+ }))
68
+
69
+ const linePts = line.values.map((v, i) => `${toLineX(i).toFixed(1)},${toLineY(v).toFixed(1)}`)
70
+ const linePath = `M ${linePts.join(' L ')}`
71
+ const lineArea = `M ${toLineX(0).toFixed(1)},${bot} L ${linePts.join(' L ')} L ${toLineX(n - 1).toFixed(1)},${bot} Z`
72
+
73
+ const leftTicks = [0, 0.5, 1].map((f) => ({
74
+ y: PAD.t + (1 - f) * chartH,
75
+ label: fmtTick(barMax * f),
76
+ }))
77
+ const rightTicks = [0, 0.5, 1].map((f) => ({
78
+ y: PAD.t + (1 - f) * chartH,
79
+ label: fmtTick(lineMin + lineRange * f),
80
+ }))
81
+
82
+ return { barRects, linePath, lineArea, gridLines, leftTicks, rightTicks }
83
+ }, [bars, line, height])
84
+
85
+ const chartW = VW - PAD.l - PAD.r
86
+ const n = bars.values.length
87
+ const gw = n > 0 ? chartW / n : chartW
88
+
89
+ return (
90
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
91
+ <svg
92
+ viewBox={`0 0 ${VW} ${height}`}
93
+ style={{ width: '100%', height: 'auto', display: 'block' }}
94
+ aria-hidden="true"
95
+ focusable="false"
96
+ >
97
+ {gridLines.map((y, i) => (
98
+ <line
99
+ key={i}
100
+ x1={PAD.l}
101
+ y1={y}
102
+ x2={VW - PAD.r}
103
+ y2={y}
104
+ stroke="rgba(255,255,255,0.06)"
105
+ strokeWidth={1}
106
+ />
107
+ ))}
108
+ {leftTicks.map((t, i) => (
109
+ <text
110
+ key={i}
111
+ x={PAD.l - 4}
112
+ y={t.y + 4}
113
+ textAnchor="end"
114
+ fill={bars.color}
115
+ fontSize={9}
116
+ fontFamily="inherit"
117
+ fillOpacity={0.8}
118
+ >
119
+ {t.label}
120
+ </text>
121
+ ))}
122
+ {rightTicks.map((t, i) => (
123
+ <text
124
+ key={i}
125
+ x={VW - PAD.r + 4}
126
+ y={t.y + 4}
127
+ textAnchor="start"
128
+ fill={line.color}
129
+ fontSize={9}
130
+ fontFamily="inherit"
131
+ fillOpacity={0.8}
132
+ >
133
+ {t.label}
134
+ </text>
135
+ ))}
136
+ {barRects.map((b) => (
137
+ <rect
138
+ key={b.key}
139
+ x={b.x}
140
+ y={b.y}
141
+ width={b.w}
142
+ height={b.h}
143
+ fill={bars.color}
144
+ fillOpacity={0.75}
145
+ rx={2}
146
+ />
147
+ ))}
148
+ <path d={lineArea} fill={line.color} fillOpacity={0.12} />
149
+ <path
150
+ d={linePath}
151
+ fill="none"
152
+ stroke={line.color}
153
+ strokeWidth={2}
154
+ strokeLinejoin="round"
155
+ strokeLinecap="round"
156
+ />
157
+ {labels.map((lbl, i) => (
158
+ <text
159
+ key={i}
160
+ x={(PAD.l + (i + 0.5) * gw).toFixed(1)}
161
+ y={height - 4}
162
+ textAnchor="middle"
163
+ fill="#6E7079"
164
+ fontSize={9}
165
+ fontFamily="inherit"
166
+ >
167
+ {lbl}
168
+ </text>
169
+ ))}
170
+ </svg>
171
+ <div
172
+ style={{
173
+ display: 'flex',
174
+ gap: 16,
175
+ justifyContent: 'center',
176
+ fontSize: 11,
177
+ color: '#A4A7B5',
178
+ marginTop: 4,
179
+ }}
180
+ >
181
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
182
+ <span
183
+ style={{
184
+ display: 'inline-block',
185
+ width: 10,
186
+ height: 10,
187
+ background: bars.color,
188
+ borderRadius: 2,
189
+ opacity: 0.75,
190
+ }}
191
+ />
192
+ {bars.label}
193
+ </span>
194
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
195
+ <span
196
+ style={{
197
+ display: 'inline-block',
198
+ width: 16,
199
+ height: 2,
200
+ background: line.color,
201
+ borderRadius: 1,
202
+ }}
203
+ />
204
+ {line.label}
205
+ </span>
206
+ </div>
207
+ </div>
208
+ )
209
+ }
@@ -0,0 +1,102 @@
1
+ export interface DumbbellItem {
2
+ label: string
3
+ start: number
4
+ end: number
5
+ color?: string
6
+ }
7
+
8
+ export interface DumbbellChartProps {
9
+ items: DumbbellItem[]
10
+ unit?: string
11
+ color?: string
12
+ ariaLabel?: string
13
+ }
14
+
15
+ const VW = 340
16
+ const LABEL_W = 110
17
+ const PAD_R = 8
18
+ const TRACK_W = VW - LABEL_W - PAD_R
19
+ const DOT_R = 5
20
+ const ROW_H = 26
21
+ const PAD_T = 6
22
+ const AXIS_H = 18
23
+ const TICKS = 4
24
+
25
+ export function DumbbellChart({
26
+ items,
27
+ unit = '',
28
+ color = '#006FFF',
29
+ ariaLabel = 'Dumbbell chart',
30
+ }: DumbbellChartProps) {
31
+ const allValues = items.flatMap((it) => [it.start, it.end])
32
+ const minVal = allValues.length ? Math.min(...allValues) : 0
33
+ const maxVal = allValues.length ? Math.max(...allValues) : 1
34
+ const range = maxVal - minVal || 1
35
+ const svgH = PAD_T + items.length * ROW_H + AXIS_H
36
+
37
+ const tx = (v: number) => LABEL_W + ((v - minVal) / range) * TRACK_W
38
+
39
+ const ticks = Array.from({ length: TICKS + 1 }, (_, i) =>
40
+ Math.round(minVal + (i / TICKS) * range),
41
+ )
42
+
43
+ return (
44
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
45
+ <svg
46
+ viewBox={`0 0 ${VW} ${svgH}`}
47
+ style={{ width: '100%', height: 'auto', display: 'block' }}
48
+ aria-hidden="true"
49
+ focusable="false"
50
+ >
51
+ {ticks.map((t, i) => (
52
+ <g key={i}>
53
+ <line
54
+ x1={tx(t)}
55
+ y1={PAD_T}
56
+ x2={tx(t)}
57
+ y2={PAD_T + items.length * ROW_H}
58
+ stroke="rgba(255,255,255,0.06)"
59
+ strokeWidth={1}
60
+ />
61
+ <text
62
+ x={tx(t)}
63
+ y={PAD_T + items.length * ROW_H + 13}
64
+ fill="#6E7079"
65
+ fontSize={9}
66
+ textAnchor="middle"
67
+ fontFamily="inherit"
68
+ >
69
+ {t}
70
+ {unit}
71
+ </text>
72
+ </g>
73
+ ))}
74
+
75
+ {items.map((item, i) => {
76
+ const cy = PAD_T + i * ROW_H + ROW_H / 2
77
+ const cx1 = tx(item.start)
78
+ const cx2 = tx(item.end)
79
+ const c = item.color ?? color
80
+
81
+ return (
82
+ <g key={i}>
83
+ <text
84
+ x={LABEL_W - 6}
85
+ y={cy + 4}
86
+ fill="#A4A7B5"
87
+ fontSize={10}
88
+ textAnchor="end"
89
+ fontFamily="inherit"
90
+ >
91
+ {item.label}
92
+ </text>
93
+ <line x1={cx1} y1={cy} x2={cx2} y2={cy} stroke={c} strokeWidth={2} opacity={0.4} />
94
+ <circle cx={cx1} cy={cy} r={DOT_R} fill="#13131A" stroke={c} strokeWidth={1.5} />
95
+ <circle cx={cx2} cy={cy} r={DOT_R} fill={c} />
96
+ </g>
97
+ )
98
+ })}
99
+ </svg>
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,144 @@
1
+ import { useId, useRef, useState } from 'react'
2
+ import type { KeyboardEvent, ChangeEvent } from 'react'
3
+
4
+ export interface DurationInputProps {
5
+ label?: string
6
+ value?: number
7
+ defaultValue?: number
8
+ maxHours?: number
9
+ disabled?: boolean
10
+ onChange?: (seconds: number) => void
11
+ id?: string
12
+ className?: string
13
+ }
14
+
15
+ function pad(n: number): string {
16
+ return String(n).padStart(2, '0')
17
+ }
18
+
19
+ function toHMS(secs: number): [number, number, number] {
20
+ const t = Math.max(0, secs)
21
+ return [Math.floor(t / 3600), Math.floor((t % 3600) / 60), t % 60]
22
+ }
23
+
24
+ function fromHMS(h: number, m: number, s: number): number {
25
+ return h * 3600 + m * 60 + s
26
+ }
27
+
28
+ export function DurationInput({
29
+ label,
30
+ value: valueProp,
31
+ defaultValue = 0,
32
+ maxHours = 99,
33
+ disabled = false,
34
+ onChange,
35
+ id,
36
+ className = '',
37
+ }: DurationInputProps) {
38
+ const gid = useId()
39
+ const inputId = id ?? gid
40
+ const [internal, setInternal] = useState<number>(valueProp ?? defaultValue)
41
+ const refs = useRef<[HTMLInputElement | null, HTMLInputElement | null, HTMLInputElement | null]>([
42
+ null,
43
+ null,
44
+ null,
45
+ ])
46
+
47
+ const [hours, minutes, seconds] = toHMS(valueProp !== undefined ? valueProp : internal)
48
+
49
+ function commit(h: number, m: number, s: number) {
50
+ const total = fromHMS(Math.min(maxHours, h), m, s)
51
+ if (valueProp === undefined) setInternal(total)
52
+ onChange?.(total)
53
+ }
54
+
55
+ function handleKey(field: 0 | 1 | 2, e: KeyboardEvent<HTMLInputElement>) {
56
+ const vals: [number, number, number] = [hours, minutes, seconds]
57
+ const maxes = [maxHours, 59, 59]
58
+ if (e.key === 'ArrowUp') {
59
+ e.preventDefault()
60
+ const next = vals[field] >= maxes[field] ? 0 : vals[field] + 1
61
+ vals[field] = next
62
+ commit(...vals)
63
+ } else if (e.key === 'ArrowDown') {
64
+ e.preventDefault()
65
+ const next = vals[field] <= 0 ? maxes[field] : vals[field] - 1
66
+ vals[field] = next
67
+ commit(...vals)
68
+ } else if ((e.key === ':' || e.key === 'ArrowRight') && field < 2) {
69
+ e.preventDefault()
70
+ refs.current[field + 1]?.focus()
71
+ } else if (e.key === 'ArrowLeft' && field > 0) {
72
+ e.preventDefault()
73
+ refs.current[field - 1]?.focus()
74
+ }
75
+ }
76
+
77
+ function handleChange(field: 0 | 1 | 2, e: ChangeEvent<HTMLInputElement>) {
78
+ const raw = e.target.value
79
+ if (!/^\d{0,2}$/.test(raw)) return
80
+ const n = parseInt(raw, 10)
81
+ if (isNaN(n)) return
82
+ const maxes = [maxHours, 59, 59]
83
+ const updated: [number, number, number] = [hours, minutes, seconds]
84
+ updated[field] = Math.min(maxes[field], n)
85
+ commit(...updated)
86
+ if (raw.length === 2 && field < 2) refs.current[field + 1]?.focus()
87
+ }
88
+
89
+ const FIELDS = [
90
+ { idx: 0 as const, val: hours, ariaLabel: 'Hours', ariaMax: maxHours },
91
+ { idx: 1 as const, val: minutes, ariaLabel: 'Minutes', ariaMax: 59 },
92
+ { idx: 2 as const, val: seconds, ariaLabel: 'Seconds', ariaMax: 59 },
93
+ ]
94
+
95
+ return (
96
+ <div className={`dur-input-wrapper${className ? ` ${className}` : ''}`}>
97
+ {label && (
98
+ <label htmlFor={`${inputId}-h`} className="dur-input__label">
99
+ {label}
100
+ </label>
101
+ )}
102
+ <div
103
+ className={`dur-input${disabled ? ' dur-input--disabled' : ''}`}
104
+ role="group"
105
+ aria-label={label ?? 'Duration'}
106
+ >
107
+ {FIELDS.flatMap(({ idx, val, ariaLabel, ariaMax }, i) => {
108
+ const nodes = [
109
+ <input
110
+ key={`f${idx}`}
111
+ ref={(el) => {
112
+ refs.current[idx] = el
113
+ }}
114
+ id={idx === 0 ? `${inputId}-h` : undefined}
115
+ type="text"
116
+ inputMode="numeric"
117
+ pattern="[0-9]*"
118
+ role="spinbutton"
119
+ aria-label={ariaLabel}
120
+ aria-valuenow={val}
121
+ aria-valuemin={0}
122
+ aria-valuemax={ariaMax}
123
+ aria-valuetext={pad(val)}
124
+ value={pad(val)}
125
+ disabled={disabled}
126
+ maxLength={2}
127
+ className="dur-input__field"
128
+ onChange={(e) => handleChange(idx, e)}
129
+ onKeyDown={(e) => handleKey(idx, e)}
130
+ onFocus={(e) => e.currentTarget.select()}
131
+ />,
132
+ ]
133
+ if (i < 2)
134
+ nodes.push(
135
+ <span key={`c${idx}`} className="dur-input__sep" aria-hidden="true">
136
+ :
137
+ </span>,
138
+ )
139
+ return nodes
140
+ })}
141
+ </div>
142
+ </div>
143
+ )
144
+ }
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export interface EmptyStateProps {
4
+ title: string
5
+ description?: string
6
+ action?: ReactNode
7
+ className?: string
8
+ }
9
+
10
+ export function EmptyState({ title, description, action, className = '' }: EmptyStateProps) {
11
+ return (
12
+ <div role="status" aria-live="polite" className={`empty-state ${className}`.trim()}>
13
+ <svg
14
+ className="empty-state__icon"
15
+ viewBox="0 0 48 48"
16
+ fill="none"
17
+ aria-hidden="true"
18
+ focusable={false}
19
+ >
20
+ <rect x="6" y="11" width="36" height="28" rx="4" stroke="currentColor" strokeWidth="1.5" />
21
+ <path
22
+ d="M6 30h10l3 5h10l3-5h10"
23
+ stroke="currentColor"
24
+ strokeWidth="1.5"
25
+ strokeLinejoin="round"
26
+ />
27
+ <path
28
+ d="M15 20h18M15 25h12"
29
+ stroke="currentColor"
30
+ strokeWidth="1.5"
31
+ strokeLinecap="round"
32
+ opacity=".4"
33
+ />
34
+ </svg>
35
+ <p className="empty-state__title">{title}</p>
36
+ {description && <p className="empty-state__desc">{description}</p>}
37
+ {action && <div className="empty-state__action">{action}</div>}
38
+ </div>
39
+ )
40
+ }
@@ -0,0 +1,124 @@
1
+ export interface ErrorBandSeries {
2
+ label: string
3
+ color: string
4
+ mean: number[]
5
+ lower: number[]
6
+ upper: number[]
7
+ }
8
+
9
+ export interface ErrorBandChartProps {
10
+ series: ErrorBandSeries[]
11
+ xLabels?: string[]
12
+ yRange?: [number, number]
13
+ height?: number
14
+ unit?: string
15
+ ariaLabel?: string
16
+ }
17
+
18
+ const VW = 340
19
+ const PAD_L = 28
20
+ const PAD_R = 8
21
+ const PAD_T = 8
22
+ const PAD_B = 20
23
+ const PLOT_W = VW - PAD_L - PAD_R
24
+
25
+ export function ErrorBandChart({
26
+ series,
27
+ xLabels = [],
28
+ yRange,
29
+ height = 180,
30
+ unit = '',
31
+ ariaLabel = 'Error band chart',
32
+ }: ErrorBandChartProps) {
33
+ if (!series.length || !series[0].mean.length) return null
34
+
35
+ const n = series[0].mean.length
36
+ const PLOT_H = height - PAD_T - PAD_B
37
+
38
+ const allVals = series.flatMap((s) => [...s.lower, ...s.upper])
39
+ const minV = yRange ? yRange[0] : Math.min(...allVals)
40
+ const maxV = yRange ? yRange[1] : Math.max(...allVals)
41
+ const range = maxV - minV || 1
42
+
43
+ const tx = (i: number) => PAD_L + (i / (n - 1 || 1)) * PLOT_W
44
+ const ty = (v: number) => PAD_T + (1 - (v - minV) / range) * PLOT_H
45
+
46
+ const bandPath = (lower: number[], upper: number[]) => {
47
+ const top = upper
48
+ .map((v, i) => `${i === 0 ? 'M' : 'L'}${tx(i).toFixed(1)},${ty(v).toFixed(1)}`)
49
+ .join(' ')
50
+ const bot = [...lower]
51
+ .reverse()
52
+ .map((v, i) => `L${tx(n - 1 - i).toFixed(1)},${ty(v).toFixed(1)}`)
53
+ .join(' ')
54
+ return `${top} ${bot} Z`
55
+ }
56
+
57
+ const linePath = (mean: number[]) =>
58
+ mean.map((v, i) => `${i === 0 ? 'M' : 'L'}${tx(i).toFixed(1)},${ty(v).toFixed(1)}`).join(' ')
59
+
60
+ const yTicks = [minV, (minV + maxV) / 2, maxV]
61
+
62
+ return (
63
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
64
+ <svg
65
+ viewBox={`0 0 ${VW} ${height}`}
66
+ style={{ width: '100%', height: 'auto', display: 'block' }}
67
+ aria-hidden="true"
68
+ focusable="false"
69
+ >
70
+ {yTicks.map((v, i) => (
71
+ <g key={i}>
72
+ <line
73
+ x1={PAD_L}
74
+ y1={ty(v)}
75
+ x2={PAD_L + PLOT_W}
76
+ y2={ty(v)}
77
+ stroke="rgba(255,255,255,0.06)"
78
+ strokeWidth={1}
79
+ />
80
+ <text
81
+ x={PAD_L - 4}
82
+ y={ty(v) + 4}
83
+ fill="#6E7079"
84
+ fontSize={8}
85
+ textAnchor="end"
86
+ fontFamily="inherit"
87
+ >
88
+ {Math.round(v)}
89
+ {unit}
90
+ </text>
91
+ </g>
92
+ ))}
93
+
94
+ {series.map((s, si) => (
95
+ <g key={si}>
96
+ <path d={bandPath(s.lower, s.upper)} fill={s.color} opacity={0.12} />
97
+ <path
98
+ d={linePath(s.mean)}
99
+ fill="none"
100
+ stroke={s.color}
101
+ strokeWidth={1.5}
102
+ strokeLinejoin="round"
103
+ strokeLinecap="round"
104
+ />
105
+ </g>
106
+ ))}
107
+
108
+ {xLabels.map((lbl, i) => (
109
+ <text
110
+ key={i}
111
+ x={PAD_L + (i / (xLabels.length - 1 || 1)) * PLOT_W}
112
+ y={height - 4}
113
+ fill="#6E7079"
114
+ fontSize={9}
115
+ textAnchor="middle"
116
+ fontFamily="inherit"
117
+ >
118
+ {lbl}
119
+ </text>
120
+ ))}
121
+ </svg>
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,58 @@
1
+ import { useId, useState, type ReactNode } from 'react'
2
+
3
+ export interface ExpandableRowProps {
4
+ row: ReactNode
5
+ detail: ReactNode
6
+ colSpan: number
7
+ defaultExpanded?: boolean
8
+ }
9
+
10
+ export function ExpandableRow({
11
+ row,
12
+ detail,
13
+ colSpan,
14
+ defaultExpanded = false,
15
+ }: ExpandableRowProps) {
16
+ const [open, setOpen] = useState(defaultExpanded)
17
+ const detailId = useId()
18
+ return (
19
+ <tbody className="exp-tbody">
20
+ <tr className="exp-row">
21
+ <td className="exp-toggle-cell">
22
+ <button
23
+ type="button"
24
+ className="exp-toggle"
25
+ aria-expanded={open}
26
+ aria-controls={detailId}
27
+ onClick={() => setOpen((o) => !o)}
28
+ >
29
+ <svg
30
+ className={`exp-chevron${open ? ' exp-chevron--open' : ''}`}
31
+ viewBox="0 0 10 6"
32
+ fill="none"
33
+ aria-hidden="true"
34
+ >
35
+ <path
36
+ d="M1 1l4 4 4-4"
37
+ stroke="currentColor"
38
+ strokeWidth="1.5"
39
+ strokeLinecap="round"
40
+ strokeLinejoin="round"
41
+ />
42
+ </svg>
43
+ </button>
44
+ </td>
45
+ {row}
46
+ </tr>
47
+ <tr
48
+ id={detailId}
49
+ className={`exp-detail${open ? ' exp-detail--open' : ''}`}
50
+ aria-hidden={!open}
51
+ >
52
+ <td colSpan={colSpan + 1} className="exp-detail-cell">
53
+ {detail}
54
+ </td>
55
+ </tr>
56
+ </tbody>
57
+ )
58
+ }