@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,142 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface BoxSeries {
4
+ label: string
5
+ q0: number
6
+ q1: number
7
+ q2: number
8
+ q3: number
9
+ q4: number
10
+ color?: string
11
+ }
12
+
13
+ export interface BoxPlotProps {
14
+ series: BoxSeries[]
15
+ yRange?: [number, number]
16
+ height?: number
17
+ ariaLabel?: string
18
+ }
19
+
20
+ const VW = 400
21
+ const PAD = { t: 12, r: 8, b: 28, l: 8 }
22
+ const DEFAULT_COLOR = '#006FFF'
23
+
24
+ export function BoxPlot({ series, yRange, height = 160, ariaLabel = 'Box plot' }: BoxPlotProps) {
25
+ const { boxes, gridLines } = useMemo(() => {
26
+ if (!series.length) return { boxes: [], gridLines: [] }
27
+ const allVals = series.flatMap((s) => [s.q0, s.q1, s.q2, s.q3, s.q4])
28
+ const y0 = yRange?.[0] ?? Math.min(...allVals)
29
+ const y1 = yRange?.[1] ?? Math.max(...allVals)
30
+ const ySpan = y1 - y0 || 1
31
+ const chartW = VW - PAD.l - PAD.r
32
+ const chartH = height - PAD.t - PAD.b
33
+ const n = series.length
34
+ const slotW = chartW / n
35
+ const bw = slotW * 0.4
36
+ const toY = (v: number) => PAD.t + chartH - ((v - y0) / ySpan) * chartH
37
+ const gridLines = [0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH)
38
+ const boxes = series.map((s, i) => {
39
+ const cx = PAD.l + (i + 0.5) * slotW
40
+ return {
41
+ key: String(i),
42
+ cx: cx.toFixed(1),
43
+ color: s.color ?? DEFAULT_COLOR,
44
+ label: s.label,
45
+ whiskerY0: toY(s.q0).toFixed(1),
46
+ whiskerY4: toY(s.q4).toFixed(1),
47
+ capX0: (cx - bw / 2).toFixed(1),
48
+ capX1: (cx + bw / 2).toFixed(1),
49
+ boxX: (cx - bw / 2).toFixed(1),
50
+ boxW: bw.toFixed(1),
51
+ boxY: toY(s.q3).toFixed(1),
52
+ boxH: (toY(s.q1) - toY(s.q3)).toFixed(1),
53
+ medY: toY(s.q2).toFixed(1),
54
+ }
55
+ })
56
+ return { boxes, gridLines }
57
+ }, [series, yRange, height])
58
+
59
+ return (
60
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
61
+ <svg
62
+ viewBox={`0 0 ${VW} ${height}`}
63
+ style={{ width: '100%', height: 'auto', display: 'block' }}
64
+ aria-hidden="true"
65
+ focusable="false"
66
+ >
67
+ {gridLines.map((y, i) => (
68
+ <line
69
+ key={i}
70
+ x1={PAD.l}
71
+ y1={y}
72
+ x2={VW - PAD.r}
73
+ y2={y}
74
+ stroke="rgba(255,255,255,0.06)"
75
+ strokeWidth={1}
76
+ />
77
+ ))}
78
+ {boxes.map((b) => (
79
+ <g key={b.key}>
80
+ <line
81
+ x1={b.cx}
82
+ y1={b.whiskerY4}
83
+ x2={b.cx}
84
+ y2={b.whiskerY0}
85
+ stroke={b.color}
86
+ strokeWidth={1.5}
87
+ strokeOpacity={0.6}
88
+ />
89
+ <line
90
+ x1={b.capX0}
91
+ y1={b.whiskerY0}
92
+ x2={b.capX1}
93
+ y2={b.whiskerY0}
94
+ stroke={b.color}
95
+ strokeWidth={1.5}
96
+ strokeOpacity={0.6}
97
+ />
98
+ <line
99
+ x1={b.capX0}
100
+ y1={b.whiskerY4}
101
+ x2={b.capX1}
102
+ y2={b.whiskerY4}
103
+ stroke={b.color}
104
+ strokeWidth={1.5}
105
+ strokeOpacity={0.6}
106
+ />
107
+ <rect
108
+ x={b.boxX}
109
+ y={b.boxY}
110
+ width={b.boxW}
111
+ height={b.boxH}
112
+ fill={b.color}
113
+ fillOpacity={0.2}
114
+ stroke={b.color}
115
+ strokeWidth={1.5}
116
+ strokeOpacity={0.8}
117
+ rx={2}
118
+ />
119
+ <line
120
+ x1={b.capX0}
121
+ y1={b.medY}
122
+ x2={b.capX1}
123
+ y2={b.medY}
124
+ stroke={b.color}
125
+ strokeWidth={2.5}
126
+ />
127
+ <text
128
+ x={b.cx}
129
+ y={height - 4}
130
+ textAnchor="middle"
131
+ fill="#6E7079"
132
+ fontSize={9}
133
+ fontFamily="inherit"
134
+ >
135
+ {b.label}
136
+ </text>
137
+ </g>
138
+ ))}
139
+ </svg>
140
+ </div>
141
+ )
142
+ }
@@ -0,0 +1,44 @@
1
+ export interface BreadcrumbItem {
2
+ label: string
3
+ href?: string
4
+ }
5
+
6
+ export interface BreadcrumbProps {
7
+ items: BreadcrumbItem[]
8
+ onNavigate?: (index: number) => void
9
+ className?: string
10
+ }
11
+
12
+ export function Breadcrumb({ items, onNavigate, className = '' }: BreadcrumbProps) {
13
+ return (
14
+ <nav aria-label="Breadcrumb" className={`breadcrumb ${className}`.trim()}>
15
+ <ol className="breadcrumb__list">
16
+ {items.map((item, i) => {
17
+ const isLast = i === items.length - 1
18
+ return (
19
+ <li key={i} className="breadcrumb__item">
20
+ {isLast ? (
21
+ <span aria-current="page" className="breadcrumb__current">
22
+ {item.label}
23
+ </span>
24
+ ) : item.href ? (
25
+ <a href={item.href} className="breadcrumb__link">
26
+ {item.label}
27
+ </a>
28
+ ) : (
29
+ <button type="button" className="breadcrumb__link" onClick={() => onNavigate?.(i)}>
30
+ {item.label}
31
+ </button>
32
+ )}
33
+ {!isLast && (
34
+ <span aria-hidden="true" className="breadcrumb__sep">
35
+ /
36
+ </span>
37
+ )}
38
+ </li>
39
+ )
40
+ })}
41
+ </ol>
42
+ </nav>
43
+ )
44
+ }
@@ -0,0 +1,115 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface BubblePoint {
4
+ x: number
5
+ y: number
6
+ size: number
7
+ color?: string
8
+ label?: string
9
+ key?: string
10
+ }
11
+
12
+ export interface BubbleChartProps {
13
+ points: BubblePoint[]
14
+ xRange?: [number, number]
15
+ yRange?: [number, number]
16
+ rRange?: [number, number]
17
+ height?: number
18
+ ariaLabel?: string
19
+ }
20
+
21
+ const VW = 400
22
+ const PAD = { t: 12, r: 12, b: 12, l: 12 }
23
+ const DEFAULT_COLOR = '#006FFF'
24
+
25
+ export function BubbleChart({
26
+ points,
27
+ xRange,
28
+ yRange,
29
+ rRange = [4, 20],
30
+ height = 160,
31
+ ariaLabel = 'Bubble chart',
32
+ }: BubbleChartProps) {
33
+ const { bubbles, gridLines } = useMemo(() => {
34
+ if (!points.length) return { bubbles: [], gridLines: [] }
35
+ const xs = points.map((p) => p.x)
36
+ const ys = points.map((p) => p.y)
37
+ const sizes = points.map((p) => p.size)
38
+ const x0 = xRange?.[0] ?? Math.min(...xs)
39
+ const x1 = xRange?.[1] ?? Math.max(...xs)
40
+ const y0 = yRange?.[0] ?? Math.min(...ys)
41
+ const y1 = yRange?.[1] ?? Math.max(...ys)
42
+ const s0 = Math.min(...sizes)
43
+ const s1 = Math.max(...sizes)
44
+ const xSpan = x1 - x0 || 1
45
+ const ySpan = y1 - y0 || 1
46
+ const sSpan = s1 - s0 || 1
47
+ const [rMin, rMax] = rRange
48
+ const chartW = VW - PAD.l - PAD.r
49
+ const chartH = height - PAD.t - PAD.b
50
+
51
+ const toX = (x: number) => PAD.l + ((x - x0) / xSpan) * chartW
52
+ const toY = (y: number) => PAD.t + chartH - ((y - y0) / ySpan) * chartH
53
+ const toR = (s: number) => rMin + ((s - s0) / sSpan) * (rMax - rMin)
54
+
55
+ const gridLines = [0.25, 0.5, 0.75].map((f) => ({
56
+ x: PAD.l + f * chartW,
57
+ y: PAD.t + (1 - f) * chartH,
58
+ }))
59
+
60
+ const bubbles = points.map((p, i) => ({
61
+ cx: toX(p.x).toFixed(1),
62
+ cy: toY(p.y).toFixed(1),
63
+ r: toR(p.size).toFixed(1),
64
+ color: p.color ?? DEFAULT_COLOR,
65
+ key: p.key ?? String(i),
66
+ }))
67
+
68
+ return { bubbles, gridLines }
69
+ }, [points, xRange, yRange, rRange, height])
70
+
71
+ return (
72
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
73
+ <svg
74
+ viewBox={`0 0 ${VW} ${height}`}
75
+ style={{ width: '100%', height: 'auto', display: 'block' }}
76
+ aria-hidden="true"
77
+ focusable="false"
78
+ >
79
+ {gridLines.map((g, i) => (
80
+ <g key={i}>
81
+ <line
82
+ x1={g.x}
83
+ y1={PAD.t}
84
+ x2={g.x}
85
+ y2={PAD.t + (height - PAD.t - PAD.b)}
86
+ stroke="rgba(255,255,255,0.06)"
87
+ strokeWidth={1}
88
+ />
89
+ <line
90
+ x1={PAD.l}
91
+ y1={g.y}
92
+ x2={VW - PAD.r}
93
+ y2={g.y}
94
+ stroke="rgba(255,255,255,0.06)"
95
+ strokeWidth={1}
96
+ />
97
+ </g>
98
+ ))}
99
+ {bubbles.map((b) => (
100
+ <circle
101
+ key={b.key}
102
+ cx={b.cx}
103
+ cy={b.cy}
104
+ r={b.r}
105
+ fill={b.color}
106
+ fillOpacity={0.6}
107
+ stroke={b.color}
108
+ strokeOpacity={0.9}
109
+ strokeWidth={1.5}
110
+ />
111
+ ))}
112
+ </svg>
113
+ </div>
114
+ )
115
+ }
@@ -0,0 +1,121 @@
1
+ export interface BulletItem {
2
+ label: string
3
+ value: number
4
+ target?: number
5
+ ranges?: [number, number]
6
+ unit?: string
7
+ color?: string
8
+ }
9
+
10
+ export interface BulletChartProps {
11
+ items: BulletItem[]
12
+ max?: number
13
+ ariaLabel?: string
14
+ }
15
+
16
+ const VW = 400
17
+ const LABEL_W = 110
18
+ const VAL_W = 44
19
+ const TRACK_H = 10
20
+ const RANGE_H = 18
21
+ const ROW_H = 34
22
+ const PAD = 8
23
+
24
+ export function BulletChart({ items, max = 100, ariaLabel = 'Bullet chart' }: BulletChartProps) {
25
+ const trackW = VW - LABEL_W - VAL_W
26
+ const totalH = items.length * ROW_H + PAD * 2
27
+
28
+ return (
29
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
30
+ <svg
31
+ viewBox={`0 0 ${VW} ${totalH}`}
32
+ style={{ width: '100%', height: 'auto', display: 'block' }}
33
+ aria-hidden="true"
34
+ focusable="false"
35
+ >
36
+ {items.map((item, i) => {
37
+ const y = PAD + i * ROW_H
38
+ const midY = y + ROW_H / 2
39
+ const rangeY = midY - RANGE_H / 2
40
+ const trackY = midY - TRACK_H / 2
41
+ const [r1, r2] = item.ranges ?? [50, 75]
42
+ const r1w = (r1 / max) * trackW
43
+ const r2w = (r2 / max) * trackW
44
+ const valW = Math.min(item.value / max, 1) * trackW
45
+ const targetX = item.target != null ? (Math.min(item.target, max) / max) * trackW : null
46
+ const color = item.color ?? '#006FFF'
47
+ const unit = item.unit ?? '%'
48
+
49
+ return (
50
+ <g key={i}>
51
+ <text
52
+ x={LABEL_W - 8}
53
+ y={midY + 4}
54
+ textAnchor="end"
55
+ fill="#A4A7B5"
56
+ fontSize={11}
57
+ fontFamily="inherit"
58
+ >
59
+ {item.label}
60
+ </text>
61
+ <rect
62
+ x={LABEL_W}
63
+ y={rangeY}
64
+ width={trackW}
65
+ height={RANGE_H}
66
+ fill="rgba(255,255,255,0.04)"
67
+ rx={2}
68
+ />
69
+ <rect
70
+ x={LABEL_W + r1w}
71
+ y={rangeY}
72
+ width={trackW - r1w}
73
+ height={RANGE_H}
74
+ fill="rgba(255,255,255,0.07)"
75
+ rx={2}
76
+ />
77
+ <rect
78
+ x={LABEL_W + r2w}
79
+ y={rangeY}
80
+ width={trackW - r2w}
81
+ height={RANGE_H}
82
+ fill="rgba(255,255,255,0.10)"
83
+ rx={2}
84
+ />
85
+ <rect
86
+ x={LABEL_W}
87
+ y={trackY}
88
+ width={valW}
89
+ height={TRACK_H}
90
+ fill={color}
91
+ fillOpacity={0.9}
92
+ rx={2}
93
+ />
94
+ {targetX != null && (
95
+ <rect
96
+ x={LABEL_W + targetX - 1.5}
97
+ y={rangeY}
98
+ width={3}
99
+ height={RANGE_H}
100
+ fill="rgba(255,255,255,0.85)"
101
+ rx={1}
102
+ />
103
+ )}
104
+ <text
105
+ x={LABEL_W + trackW + 6}
106
+ y={midY + 4}
107
+ fill="#fff"
108
+ fontSize={11}
109
+ fontFamily="inherit"
110
+ style={{ fontVariantNumeric: 'tabular-nums' }}
111
+ >
112
+ {item.value}
113
+ {unit}
114
+ </text>
115
+ </g>
116
+ )
117
+ })}
118
+ </svg>
119
+ </div>
120
+ )
121
+ }
@@ -0,0 +1,127 @@
1
+ export interface BumpSeries {
2
+ label: string
3
+ ranks: number[] // 1 = top rank at each time point
4
+ color?: string
5
+ }
6
+
7
+ export interface BumpChartProps {
8
+ labels: string[]
9
+ series: BumpSeries[]
10
+ ariaLabel?: string
11
+ }
12
+
13
+ const VW = 380
14
+ const PAD_L = 12
15
+ const PAD_R = 76
16
+ const PAD_T = 8
17
+ const PAD_B = 20
18
+ const ROW_H = 28
19
+ const DOT_R = 5
20
+ const COLORS = ['#006FFF', '#00C8C8', '#F5A623', '#7FB6FF', '#A878F5', '#F56342']
21
+
22
+ export function BumpChart({ labels, series, ariaLabel = 'Bump chart' }: BumpChartProps) {
23
+ const maxRank = Math.max(...series.flatMap((s) => s.ranks), 1)
24
+ const TRACK_W = VW - PAD_L - PAD_R
25
+ const svgH = PAD_T + maxRank * ROW_H + PAD_B
26
+
27
+ const xOf = (i: number) =>
28
+ labels.length > 1 ? PAD_L + (i / (labels.length - 1)) * TRACK_W : PAD_L + TRACK_W / 2
29
+
30
+ const yOf = (rank: number) => PAD_T + (rank - 1) * ROW_H + ROW_H / 2
31
+
32
+ function buildPath(ranks: number[]): string {
33
+ return ranks
34
+ .map((rank, i) => {
35
+ const x = xOf(i)
36
+ const y = yOf(rank)
37
+ if (i === 0) return `M${x},${y}`
38
+ const prevX = xOf(i - 1)
39
+ const prevY = yOf(ranks[i - 1])
40
+ const cpX = (prevX + x) / 2
41
+ return `C${cpX},${prevY} ${cpX},${y} ${x},${y}`
42
+ })
43
+ .join(' ')
44
+ }
45
+
46
+ return (
47
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
48
+ <svg
49
+ viewBox={`0 0 ${VW} ${svgH}`}
50
+ style={{ width: '100%', height: 'auto', display: 'block' }}
51
+ aria-hidden="true"
52
+ focusable="false"
53
+ >
54
+ {labels.map((label, i) => (
55
+ <g key={i}>
56
+ <line
57
+ x1={xOf(i)}
58
+ y1={PAD_T}
59
+ x2={xOf(i)}
60
+ y2={PAD_T + maxRank * ROW_H}
61
+ stroke="rgba(255,255,255,0.06)"
62
+ strokeWidth={1}
63
+ />
64
+ <text
65
+ x={xOf(i)}
66
+ y={svgH - 5}
67
+ fill="#6E7079"
68
+ fontSize={9}
69
+ textAnchor="middle"
70
+ fontFamily="inherit"
71
+ >
72
+ {label}
73
+ </text>
74
+ </g>
75
+ ))}
76
+
77
+ {Array.from({ length: maxRank }, (_, i) => (
78
+ <line
79
+ key={i}
80
+ x1={PAD_L}
81
+ y1={yOf(i + 1)}
82
+ x2={PAD_L + TRACK_W}
83
+ y2={yOf(i + 1)}
84
+ stroke="rgba(255,255,255,0.04)"
85
+ strokeWidth={1}
86
+ />
87
+ ))}
88
+
89
+ {series.map((s, si) => {
90
+ const color = s.color ?? COLORS[si % COLORS.length]
91
+ const lastRank = s.ranks[s.ranks.length - 1]
92
+ return (
93
+ <g key={si}>
94
+ <path
95
+ d={buildPath(s.ranks)}
96
+ fill="none"
97
+ stroke={color}
98
+ strokeWidth={2.5}
99
+ opacity={0.85}
100
+ />
101
+ {s.ranks.map((rank, i) => (
102
+ <circle
103
+ key={i}
104
+ cx={xOf(i)}
105
+ cy={yOf(rank)}
106
+ r={DOT_R}
107
+ fill={color}
108
+ stroke="#1A1F2E"
109
+ strokeWidth={1.5}
110
+ />
111
+ ))}
112
+ <text
113
+ x={xOf(labels.length - 1) + DOT_R + 6}
114
+ y={yOf(lastRank) + 4}
115
+ fill={color}
116
+ fontSize={10}
117
+ fontFamily="inherit"
118
+ >
119
+ {s.label}
120
+ </text>
121
+ </g>
122
+ )
123
+ })}
124
+ </svg>
125
+ </div>
126
+ )
127
+ }
@@ -0,0 +1,61 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react'
2
+ import { Spinner } from './Spinner.js'
3
+
4
+ export type ButtonVariant = 'primary' | 'ghost' | 'danger'
5
+
6
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7
+ variant?: ButtonVariant
8
+ iconOnly?: boolean
9
+ loading?: boolean
10
+ children?: ReactNode
11
+ }
12
+
13
+ export function Button({
14
+ variant = 'ghost',
15
+ iconOnly = false,
16
+ loading = false,
17
+ className = '',
18
+ type = 'button',
19
+ disabled,
20
+ children,
21
+ ...rest
22
+ }: ButtonProps) {
23
+ const cls = ['btn', `btn-${variant}`, iconOnly && 'btn-icon', className].filter(Boolean).join(' ')
24
+ return (
25
+ <button
26
+ type={type}
27
+ className={cls}
28
+ disabled={disabled || loading}
29
+ aria-busy={loading || undefined}
30
+ {...rest}
31
+ >
32
+ {loading && <Spinner size="sm" />}
33
+ {children}
34
+ </button>
35
+ )
36
+ }
37
+
38
+ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
39
+ children?: ReactNode
40
+ }
41
+
42
+ export function IconButton({
43
+ className = '',
44
+ type = 'button',
45
+ children,
46
+ title,
47
+ 'aria-label': ariaLabel,
48
+ ...rest
49
+ }: IconButtonProps) {
50
+ return (
51
+ <button
52
+ type={type}
53
+ className={`icon-btn ${className}`.trim()}
54
+ title={title}
55
+ aria-label={ariaLabel ?? title}
56
+ {...rest}
57
+ >
58
+ {children}
59
+ </button>
60
+ )
61
+ }