@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,159 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface ParetoItem {
4
+ label: string
5
+ value: number
6
+ color?: string
7
+ }
8
+
9
+ export interface ParetoChartProps {
10
+ items: ParetoItem[]
11
+ height?: number
12
+ ariaLabel?: string
13
+ barColor?: string
14
+ lineColor?: string
15
+ threshold?: number
16
+ }
17
+
18
+ const VW = 400
19
+ const PAD = { t: 12, r: 40, b: 28, l: 8 }
20
+ const DEF_BAR = '#006FFF'
21
+ const DEF_LINE = '#F5A623'
22
+
23
+ export function ParetoChart({
24
+ items,
25
+ height = 160,
26
+ ariaLabel = 'Pareto chart',
27
+ barColor = DEF_BAR,
28
+ lineColor = DEF_LINE,
29
+ threshold = 0.8,
30
+ }: ParetoChartProps) {
31
+ const { bars, linePoints, thresholdY, pctLabels, gridLines } = useMemo(() => {
32
+ if (!items.length)
33
+ return { bars: [], linePoints: '', thresholdY: null, pctLabels: [], gridLines: [] }
34
+
35
+ const sorted = [...items].sort((a, b) => b.value - a.value)
36
+ const total = sorted.reduce((s, it) => s + it.value, 0) || 1
37
+ const chartW = VW - PAD.l - PAD.r
38
+ const chartH = height - PAD.t - PAD.b
39
+ const bot = PAD.t + chartH
40
+ const maxV = sorted[0].value
41
+ const barW = chartW / sorted.length
42
+ const gap = barW * 0.15
43
+ const bw = Math.max(barW - gap * 2, 1)
44
+
45
+ const gridLines = [0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH)
46
+
47
+ const bars = sorted.map((item, i) => {
48
+ const bh = (item.value / maxV) * chartH
49
+ const x = PAD.l + i * barW + gap
50
+ return {
51
+ x: x.toFixed(1),
52
+ y: (bot - bh).toFixed(1),
53
+ w: bw.toFixed(1),
54
+ h: bh.toFixed(1),
55
+ label: item.label,
56
+ color: item.color ?? barColor,
57
+ key: `b${i}`,
58
+ }
59
+ })
60
+
61
+ let cumSum = 0
62
+ const pts: string[] = [`${PAD.l},${bot}`]
63
+ sorted.forEach((item, i) => {
64
+ cumSum += item.value
65
+ const pct = cumSum / total
66
+ const x = PAD.l + (i + 1) * barW
67
+ const y = PAD.t + (1 - pct) * chartH
68
+ pts.push(`${x.toFixed(1)},${y.toFixed(1)}`)
69
+ })
70
+ const linePoints = pts.join(' ')
71
+
72
+ const thresholdY = threshold > 0 ? (PAD.t + (1 - threshold) * chartH).toFixed(1) : null
73
+
74
+ const pctLabels = [0, 50, 100].map((pct) => ({
75
+ label: `${pct}%`,
76
+ y: (PAD.t + (1 - pct / 100) * chartH + 3).toFixed(1),
77
+ }))
78
+
79
+ return { bars, linePoints, thresholdY, pctLabels, gridLines }
80
+ }, [items, height, barColor, threshold])
81
+
82
+ return (
83
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
84
+ <svg
85
+ viewBox={`0 0 ${VW} ${height}`}
86
+ style={{ width: '100%', height: 'auto', display: 'block' }}
87
+ aria-hidden="true"
88
+ focusable="false"
89
+ >
90
+ {gridLines.map((y, i) => (
91
+ <line
92
+ key={i}
93
+ x1={PAD.l}
94
+ y1={y}
95
+ x2={VW - PAD.r}
96
+ y2={y}
97
+ stroke="rgba(255,255,255,0.06)"
98
+ strokeWidth={1}
99
+ />
100
+ ))}
101
+ {bars.map((b) => (
102
+ <rect
103
+ key={b.key}
104
+ x={b.x}
105
+ y={b.y}
106
+ width={b.w}
107
+ height={b.h}
108
+ fill={b.color}
109
+ fillOpacity={0.85}
110
+ rx={2}
111
+ />
112
+ ))}
113
+ {thresholdY && (
114
+ <line
115
+ x1={PAD.l}
116
+ y1={thresholdY}
117
+ x2={VW - PAD.r}
118
+ y2={thresholdY}
119
+ stroke="rgba(255,255,255,0.3)"
120
+ strokeWidth={1}
121
+ strokeDasharray="4 3"
122
+ />
123
+ )}
124
+ <polyline
125
+ points={linePoints}
126
+ fill="none"
127
+ stroke={lineColor}
128
+ strokeWidth={1.5}
129
+ strokeLinejoin="round"
130
+ />
131
+ {pctLabels.map((pl) => (
132
+ <text
133
+ key={pl.label}
134
+ x={VW - PAD.r + 4}
135
+ y={pl.y}
136
+ fill="#6E7079"
137
+ fontSize={8}
138
+ fontFamily="inherit"
139
+ >
140
+ {pl.label}
141
+ </text>
142
+ ))}
143
+ {bars.map((b, i) => (
144
+ <text
145
+ key={`l${i}`}
146
+ x={(parseFloat(b.x) + parseFloat(b.w) / 2).toFixed(1)}
147
+ y={height - 4}
148
+ textAnchor="middle"
149
+ fill="#6E7079"
150
+ fontSize={9}
151
+ fontFamily="inherit"
152
+ >
153
+ {b.label.length > 8 ? `${b.label.slice(0, 7)}…` : b.label}
154
+ </text>
155
+ ))}
156
+ </svg>
157
+ </div>
158
+ )
159
+ }
@@ -0,0 +1,112 @@
1
+ import { useId, useState } from 'react'
2
+ import type { ChangeEvent } from 'react'
3
+
4
+ export interface PasswordInputProps {
5
+ label?: string
6
+ value?: string
7
+ defaultValue?: string
8
+ placeholder?: string
9
+ disabled?: boolean
10
+ autocomplete?: 'current-password' | 'new-password' | 'off'
11
+ onChange?: (value: string) => void
12
+ id?: string
13
+ className?: string
14
+ }
15
+
16
+ function EyeIcon() {
17
+ return (
18
+ <svg
19
+ width="16"
20
+ height="16"
21
+ viewBox="0 0 16 16"
22
+ fill="none"
23
+ aria-hidden="true"
24
+ focusable="false"
25
+ >
26
+ <path
27
+ d="M1 8s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5z"
28
+ stroke="currentColor"
29
+ strokeWidth="1.25"
30
+ strokeLinejoin="round"
31
+ />
32
+ <circle cx="8" cy="8" r="2" stroke="currentColor" strokeWidth="1.25" />
33
+ </svg>
34
+ )
35
+ }
36
+
37
+ function EyeOffIcon() {
38
+ return (
39
+ <svg
40
+ width="16"
41
+ height="16"
42
+ viewBox="0 0 16 16"
43
+ fill="none"
44
+ aria-hidden="true"
45
+ focusable="false"
46
+ >
47
+ <path
48
+ d="M2 2l12 12M6.5 6.6A2 2 0 0 0 9.4 9.5M4.2 4.3C2.7 5.3 1.5 7 1.5 8s2.2 4.5 6.5 4.5c1.3 0 2.4-.3 3.3-.8M7 3.6C7.3 3.5 7.7 3.5 8 3.5c4.3 0 6.5 3.5 6.5 4.5 0 .6-.6 1.7-1.7 2.7"
49
+ stroke="currentColor"
50
+ strokeWidth="1.25"
51
+ strokeLinecap="round"
52
+ strokeLinejoin="round"
53
+ />
54
+ </svg>
55
+ )
56
+ }
57
+
58
+ export function PasswordInput({
59
+ label,
60
+ value: valueProp,
61
+ defaultValue = '',
62
+ placeholder,
63
+ disabled = false,
64
+ autocomplete = 'current-password',
65
+ onChange,
66
+ id,
67
+ className = '',
68
+ }: PasswordInputProps) {
69
+ const gid = useId()
70
+ const inputId = id ?? gid
71
+ const [shown, setShown] = useState(false)
72
+ const [internal, setInternal] = useState(defaultValue)
73
+
74
+ const value = valueProp !== undefined ? valueProp : internal
75
+
76
+ function handleChange(e: ChangeEvent<HTMLInputElement>) {
77
+ if (valueProp === undefined) setInternal(e.target.value)
78
+ onChange?.(e.target.value)
79
+ }
80
+
81
+ return (
82
+ <div className={`pwd-input-wrapper${className ? ` ${className}` : ''}`}>
83
+ {label && (
84
+ <label htmlFor={inputId} className="pwd-input__label">
85
+ {label}
86
+ </label>
87
+ )}
88
+ <div className={`pwd-input${disabled ? ' pwd-input--disabled' : ''}`}>
89
+ <input
90
+ id={inputId}
91
+ type={shown ? 'text' : 'password'}
92
+ className="pwd-input__field"
93
+ value={value}
94
+ placeholder={placeholder}
95
+ disabled={disabled}
96
+ autoComplete={autocomplete}
97
+ onChange={handleChange}
98
+ />
99
+ <button
100
+ type="button"
101
+ className="pwd-input__toggle"
102
+ aria-pressed={shown}
103
+ aria-label={shown ? 'Hide password' : 'Show password'}
104
+ disabled={disabled}
105
+ onClick={() => setShown((v) => !v)}
106
+ >
107
+ {shown ? <EyeOffIcon /> : <EyeIcon />}
108
+ </button>
109
+ </div>
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,109 @@
1
+ export interface PieSlice {
2
+ label: string
3
+ value: number
4
+ color?: string
5
+ }
6
+
7
+ export interface PieChartProps {
8
+ slices: PieSlice[]
9
+ size?: number
10
+ ariaLabel?: string
11
+ }
12
+
13
+ const COLORS = [
14
+ '#006FFF',
15
+ '#00C8C8',
16
+ '#F5A623',
17
+ '#A878F5',
18
+ '#FF6B6B',
19
+ '#00C875',
20
+ '#7FB6FF',
21
+ '#6E7079',
22
+ ]
23
+ const TAU = 2 * Math.PI
24
+ const START = -Math.PI / 2
25
+
26
+ function arc(cx: number, cy: number, r: number, a0: number, a1: number): string {
27
+ const x0 = cx + r * Math.cos(a0)
28
+ const y0 = cy + r * Math.sin(a0)
29
+ const x1 = cx + r * Math.cos(a1)
30
+ const y1 = cy + r * Math.sin(a1)
31
+ const large = a1 - a0 > Math.PI ? 1 : 0
32
+ return `M ${cx} ${cy} L ${x0.toFixed(2)} ${y0.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${x1.toFixed(2)} ${y1.toFixed(2)} Z`
33
+ }
34
+
35
+ export function PieChart({ slices, size = 140, ariaLabel = 'Pie chart' }: PieChartProps) {
36
+ const total = slices.reduce((s, sl) => s + sl.value, 0) || 1
37
+ const cx = size / 2
38
+ const cy = size / 2
39
+ const r = (size / 2) * 0.82
40
+ const LEGEND_ROW = 18
41
+ const legendH = Math.ceil(slices.length / 2) * LEGEND_ROW + 6
42
+ const VW = size
43
+ const VH = size + legendH
44
+
45
+ const segments: { path: string; color: string; mid: number }[] = []
46
+ let angle = START
47
+ for (const sl of slices) {
48
+ const span = (sl.value / total) * TAU
49
+ const color = sl.color ?? COLORS[segments.length % COLORS.length]
50
+ segments.push({ path: arc(cx, cy, r, angle, angle + span), color, mid: angle + span / 2 })
51
+ angle += span
52
+ }
53
+
54
+ return (
55
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
56
+ <svg
57
+ viewBox={`0 0 ${VW} ${VH}`}
58
+ style={{ width: '100%', height: 'auto', display: 'block' }}
59
+ aria-hidden="true"
60
+ focusable="false"
61
+ >
62
+ {segments.map((seg, i) => (
63
+ <path key={i} d={seg.path} fill={seg.color} stroke="#1a1d2b" strokeWidth={1.5} />
64
+ ))}
65
+
66
+ {slices.map((sl, i) => {
67
+ const pct = Math.round((sl.value / total) * 100)
68
+ if (pct < 5) return null
69
+ const mid = segments[i].mid
70
+ const lr = r * 0.6
71
+ const tx = cx + lr * Math.cos(mid)
72
+ const ty = cy + lr * Math.sin(mid)
73
+ return (
74
+ <text
75
+ key={i}
76
+ x={tx.toFixed(1)}
77
+ y={ty.toFixed(1)}
78
+ fill="#fff"
79
+ fontSize={9}
80
+ textAnchor="middle"
81
+ dominantBaseline="middle"
82
+ fontFamily="inherit"
83
+ fontWeight="600"
84
+ >
85
+ {pct}%
86
+ </text>
87
+ )
88
+ })}
89
+
90
+ {slices.map((sl, i) => {
91
+ const col = Math.floor(i / Math.ceil(slices.length / 2))
92
+ const row = i % Math.ceil(slices.length / 2)
93
+ const colW = VW / 2
94
+ const lx = col * colW + 10
95
+ const ly = size + 6 + row * LEGEND_ROW
96
+ const c = sl.color ?? COLORS[i % COLORS.length]
97
+ return (
98
+ <g key={i}>
99
+ <rect x={lx} y={ly + 2} width={8} height={8} rx={2} fill={c} />
100
+ <text x={lx + 12} y={ly + 9} fill="#A4A7B5" fontSize={9} fontFamily="inherit">
101
+ {sl.label} ({sl.value})
102
+ </text>
103
+ </g>
104
+ )
105
+ })}
106
+ </svg>
107
+ </div>
108
+ )
109
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode, CSSProperties } from 'react'
2
+
3
+ export type PillVariant = 'success' | 'warn' | 'danger' | 'info' | 'neutral'
4
+
5
+ export interface PillProps {
6
+ variant?: PillVariant
7
+ showDot?: boolean
8
+ children: ReactNode
9
+ style?: CSSProperties
10
+ className?: string
11
+ }
12
+
13
+ export function Pill({
14
+ variant = 'neutral',
15
+ showDot = true,
16
+ children,
17
+ style,
18
+ className = '',
19
+ }: PillProps) {
20
+ return (
21
+ <span className={`pill pill-${variant} ${className}`.trim()} style={style}>
22
+ {showDot && <span className="dot" aria-hidden="true" />}
23
+ {children}
24
+ </span>
25
+ )
26
+ }
@@ -0,0 +1,118 @@
1
+ export interface PolarCell {
2
+ row: number
3
+ col: number
4
+ value: number
5
+ }
6
+
7
+ export interface PolarHeatmapProps {
8
+ data: PolarCell[]
9
+ rows: number
10
+ cols: number
11
+ colLabels?: string[]
12
+ color?: string
13
+ ariaLabel?: string
14
+ }
15
+
16
+ const SIZE = 280
17
+ const CX = SIZE / 2
18
+ const CY = SIZE / 2
19
+ const MIN_R = 24
20
+ const MAX_R = 110
21
+ const BASE = -Math.PI / 2
22
+
23
+ function hexToRgb(hex: string): [number, number, number] {
24
+ const h = hex.replace('#', '')
25
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]
26
+ }
27
+
28
+ function annularSector(ir: number, or: number, sa: number, ea: number): string {
29
+ const { cos, sin } = Math
30
+ const f = (v: number) => v.toFixed(2)
31
+ return [
32
+ `M ${f(CX + ir * cos(sa))} ${f(CY + ir * sin(sa))}`,
33
+ `A ${f(ir)} ${f(ir)} 0 0 1 ${f(CX + ir * cos(ea))} ${f(CY + ir * sin(ea))}`,
34
+ `L ${f(CX + or * cos(ea))} ${f(CY + or * sin(ea))}`,
35
+ `A ${f(or)} ${f(or)} 0 0 0 ${f(CX + or * cos(sa))} ${f(CY + or * sin(sa))}`,
36
+ 'Z',
37
+ ].join(' ')
38
+ }
39
+
40
+ export function PolarHeatmap({
41
+ data,
42
+ rows,
43
+ cols,
44
+ colLabels = [],
45
+ color = '#006FFF',
46
+ ariaLabel = 'Polar heatmap',
47
+ }: PolarHeatmapProps) {
48
+ if (!data.length) return null
49
+ const maxVal = Math.max(...data.map((d) => d.value), 1)
50
+ const ringH = (MAX_R - MIN_R) / rows
51
+ const segA = (2 * Math.PI) / cols
52
+ const [r, g, b] = hexToRgb(color)
53
+ const LABEL_R = MAX_R + 14
54
+ const labelEvery = cols <= 12 ? 1 : cols <= 24 ? 3 : Math.round(cols / 8)
55
+
56
+ return (
57
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
58
+ <svg
59
+ viewBox={`0 0 ${SIZE} ${SIZE}`}
60
+ style={{ width: '100%', height: 'auto', display: 'block' }}
61
+ aria-hidden="true"
62
+ focusable="false"
63
+ >
64
+ {Array.from({ length: rows + 1 }, (_, i) => MIN_R + i * ringH).map((rr, i) => (
65
+ <circle
66
+ key={i}
67
+ cx={CX}
68
+ cy={CY}
69
+ r={rr.toFixed(2)}
70
+ fill="none"
71
+ stroke="rgba(255,255,255,0.05)"
72
+ strokeWidth={0.5}
73
+ />
74
+ ))}
75
+ {data.map((cell, i) => {
76
+ const ir = MIN_R + cell.row * ringH
77
+ const or = ir + ringH - 0.5
78
+ const sa = BASE + cell.col * segA + 0.01
79
+ const ea = BASE + (cell.col + 1) * segA - 0.01
80
+ const norm = cell.value / maxVal
81
+ return (
82
+ <path
83
+ key={i}
84
+ d={annularSector(ir, or, sa, ea)}
85
+ fill={`rgba(${r},${g},${b},${(0.08 + 0.92 * norm).toFixed(3)})`}
86
+ />
87
+ )
88
+ })}
89
+ {colLabels.map((label, i) => {
90
+ if (i % labelEvery !== 0) return null
91
+ const a = BASE + (i + 0.5) * segA
92
+ return (
93
+ <text
94
+ key={i}
95
+ x={(CX + LABEL_R * Math.cos(a)).toFixed(1)}
96
+ y={(CY + LABEL_R * Math.sin(a)).toFixed(1)}
97
+ fill="#6E7079"
98
+ fontSize={7}
99
+ textAnchor="middle"
100
+ dominantBaseline="middle"
101
+ fontFamily="inherit"
102
+ >
103
+ {label}
104
+ </text>
105
+ )
106
+ })}
107
+ <circle
108
+ cx={CX}
109
+ cy={CY}
110
+ r={MIN_R}
111
+ fill="#13131A"
112
+ stroke="rgba(255,255,255,0.08)"
113
+ strokeWidth={0.5}
114
+ />
115
+ </svg>
116
+ </div>
117
+ )
118
+ }
@@ -0,0 +1,103 @@
1
+ import { useEffect, useId, useRef, useState, type ReactNode } from 'react'
2
+
3
+ export type PopoverPlacement = 'bottom-start' | 'bottom-end' | 'bottom'
4
+
5
+ export interface PopoverProps {
6
+ label: string
7
+ variant?: 'ghost' | 'primary'
8
+ title?: string
9
+ children: ReactNode
10
+ placement?: PopoverPlacement
11
+ }
12
+
13
+ const FOCUSABLE =
14
+ 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
15
+
16
+ export function Popover({
17
+ label,
18
+ variant = 'ghost',
19
+ title,
20
+ children,
21
+ placement = 'bottom-start',
22
+ }: PopoverProps) {
23
+ const titleId = useId()
24
+ const [open, setOpen] = useState(false)
25
+ const rootRef = useRef<HTMLDivElement>(null)
26
+ const triggerRef = useRef<HTMLButtonElement>(null)
27
+ const panelRef = useRef<HTMLDivElement>(null)
28
+
29
+ useEffect(() => {
30
+ if (!open) return
31
+ const panel = panelRef.current
32
+ const first = panel?.querySelector<HTMLElement>(FOCUSABLE)
33
+ ;(first ?? panel)?.focus()
34
+
35
+ function onKey(e: KeyboardEvent) {
36
+ if (e.key === 'Escape') {
37
+ e.preventDefault()
38
+ setOpen(false)
39
+ triggerRef.current?.focus()
40
+ return
41
+ }
42
+ if (e.key === 'Tab' && panel) {
43
+ const items = Array.from(panel.querySelectorAll<HTMLElement>(FOCUSABLE))
44
+ if (!items.length) {
45
+ e.preventDefault()
46
+ return
47
+ }
48
+ const first = items[0]!
49
+ const last = items[items.length - 1]!
50
+ const active = document.activeElement
51
+ if (e.shiftKey && active === first) {
52
+ e.preventDefault()
53
+ last.focus()
54
+ } else if (!e.shiftKey && active === last) {
55
+ e.preventDefault()
56
+ first.focus()
57
+ }
58
+ }
59
+ }
60
+
61
+ function onPointer(e: PointerEvent) {
62
+ if (!rootRef.current?.contains(e.target as Node)) setOpen(false)
63
+ }
64
+
65
+ window.addEventListener('keydown', onKey)
66
+ document.addEventListener('pointerdown', onPointer)
67
+ return () => {
68
+ window.removeEventListener('keydown', onKey)
69
+ document.removeEventListener('pointerdown', onPointer)
70
+ }
71
+ }, [open])
72
+
73
+ return (
74
+ <div ref={rootRef} className="popover-root">
75
+ <button
76
+ ref={triggerRef}
77
+ type="button"
78
+ className={`btn btn-${variant}`}
79
+ aria-haspopup="dialog"
80
+ aria-expanded={open}
81
+ onClick={() => setOpen((o) => !o)}
82
+ >
83
+ {label}
84
+ </button>
85
+ {open && (
86
+ <div
87
+ ref={panelRef}
88
+ role="dialog"
89
+ aria-labelledby={title ? titleId : undefined}
90
+ className={`popover popover-${placement}`}
91
+ tabIndex={-1}
92
+ >
93
+ {title && (
94
+ <div id={titleId} className="popover-h">
95
+ {title}
96
+ </div>
97
+ )}
98
+ <div className="popover-b">{children}</div>
99
+ </div>
100
+ )}
101
+ </div>
102
+ )
103
+ }
@@ -0,0 +1,33 @@
1
+ export interface ProgressBarProps {
2
+ value: number
3
+ label?: string
4
+ valueText?: string
5
+ color?: string
6
+ }
7
+
8
+ export function ProgressBar({ value, label, valueText, color = '#006FFF' }: ProgressBarProps) {
9
+ const clamped = Math.max(0, Math.min(100, value))
10
+ const displayed = valueText ?? `${Math.round(clamped)}%`
11
+ const hasHeader = label !== undefined || valueText !== undefined
12
+ return (
13
+ <div className="pb">
14
+ {hasHeader && (
15
+ <div className="pb-header">
16
+ {label && <span className="pb-label">{label}</span>}
17
+ <span className="pb-value">{displayed}</span>
18
+ </div>
19
+ )}
20
+ <div
21
+ className="pb-track"
22
+ role="progressbar"
23
+ aria-valuenow={Math.round(clamped)}
24
+ aria-valuemin={0}
25
+ aria-valuemax={100}
26
+ aria-label={label}
27
+ aria-valuetext={displayed}
28
+ >
29
+ <div className="pb-fill" style={{ width: `${clamped}%`, background: color }} />
30
+ </div>
31
+ </div>
32
+ )
33
+ }