@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,191 @@
1
+ import { useId, useRef, useState } from 'react'
2
+ import type { ReactNode, ClipboardEvent, KeyboardEvent, ChangeEvent } from 'react'
3
+
4
+ export interface CIDRInputProps {
5
+ label?: string
6
+ value?: string
7
+ defaultValue?: string
8
+ disabled?: boolean
9
+ onChange?: (value: string) => void
10
+ id?: string
11
+ className?: string
12
+ }
13
+
14
+ function clampOctet(s: string): string {
15
+ if (s === '') return s
16
+ const n = Number(s)
17
+ if (isNaN(n)) return s
18
+ return String(Math.min(255, Math.max(0, n)))
19
+ }
20
+
21
+ function clampPrefix(s: string): string {
22
+ if (s === '') return s
23
+ const n = Number(s)
24
+ if (isNaN(n)) return s
25
+ return String(Math.min(32, Math.max(0, n)))
26
+ }
27
+
28
+ function parseCIDR(cidr: string): [string, string, string, string, string] {
29
+ const [ip = '0.0.0.0', prefix = '24'] = cidr.split('/')
30
+ const parts = ip.split('.')
31
+ return [parts[0] ?? '0', parts[1] ?? '0', parts[2] ?? '0', parts[3] ?? '0', prefix]
32
+ }
33
+
34
+ export function CIDRInput({
35
+ label,
36
+ value: valueProp,
37
+ defaultValue = '0.0.0.0/0',
38
+ disabled = false,
39
+ onChange,
40
+ id,
41
+ className = '',
42
+ }: CIDRInputProps) {
43
+ const gid = useId()
44
+ const inputId = id ?? gid
45
+ const [internal, setInternal] = useState<[string, string, string, string, string]>(() =>
46
+ parseCIDR(valueProp ?? defaultValue),
47
+ )
48
+ const refs = useRef<(HTMLInputElement | null)[]>([null, null, null, null, null])
49
+
50
+ const fields = valueProp !== undefined ? parseCIDR(valueProp) : internal
51
+
52
+ function focusAt(i: number) {
53
+ const el = refs.current[i]
54
+ if (el) {
55
+ el.focus()
56
+ el.select()
57
+ }
58
+ }
59
+
60
+ function commitFields(next: [string, string, string, string, string]) {
61
+ if (valueProp === undefined) setInternal(next)
62
+ onChange?.(`${next.slice(0, 4).join('.')}/${next[4]}`)
63
+ }
64
+
65
+ function handleChange(i: number, e: ChangeEvent<HTMLInputElement>) {
66
+ const raw = e.target.value
67
+ const next = [...fields] as [string, string, string, string, string]
68
+ if (i < 4) {
69
+ if (!/^\d{0,3}$/.test(raw)) return
70
+ next[i] = clampOctet(raw)
71
+ commitFields(next)
72
+ if (raw.length === 3 && i < 4) focusAt(i + 1)
73
+ } else {
74
+ if (!/^\d{0,2}$/.test(raw)) return
75
+ next[4] = clampPrefix(raw)
76
+ commitFields(next)
77
+ }
78
+ }
79
+
80
+ function handleKey(i: number, e: KeyboardEvent<HTMLInputElement>) {
81
+ const el = refs.current[i]!
82
+ if (e.key === '.' && i < 3) {
83
+ e.preventDefault()
84
+ focusAt(i + 1)
85
+ } else if (e.key === '/' && i <= 3) {
86
+ e.preventDefault()
87
+ focusAt(4)
88
+ } else if (e.key === 'ArrowRight' && el.selectionStart === el.value.length) {
89
+ if (i < 4) {
90
+ e.preventDefault()
91
+ focusAt(i + 1)
92
+ }
93
+ } else if (e.key === 'ArrowLeft' && el.selectionStart === 0) {
94
+ if (i > 0) {
95
+ e.preventDefault()
96
+ focusAt(i - 1)
97
+ }
98
+ } else if (e.key === 'Backspace' && el.value === '' && i > 0) {
99
+ e.preventDefault()
100
+ focusAt(i - 1)
101
+ }
102
+ }
103
+
104
+ function handlePaste(e: ClipboardEvent<HTMLInputElement>) {
105
+ const text = e.clipboardData.getData('text').trim()
106
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/.test(text)) {
107
+ e.preventDefault()
108
+ const [ip = '0.0.0.0', prefix = '0'] = text.split('/')
109
+ const next = [...ip.split('.').map(clampOctet), clampPrefix(prefix)] as [
110
+ string,
111
+ string,
112
+ string,
113
+ string,
114
+ string,
115
+ ]
116
+ if (valueProp === undefined) setInternal(next)
117
+ onChange?.(`${next.slice(0, 4).join('.')}/${next[4]}`)
118
+ focusAt(4)
119
+ }
120
+ }
121
+
122
+ return (
123
+ <div className={`cidr-input-wrapper ${className}`.trim()}>
124
+ {label && (
125
+ <label htmlFor={`${inputId}-0`} className="cidr-input__label">
126
+ {label}
127
+ </label>
128
+ )}
129
+ <div
130
+ className={`cidr-input${disabled ? ' cidr-input--disabled' : ''}`}
131
+ role="group"
132
+ aria-label={label ?? 'CIDR'}
133
+ >
134
+ {([0, 1, 2, 3] as const).flatMap((i) => {
135
+ const nodes: ReactNode[] = [
136
+ <input
137
+ key={`o${i}`}
138
+ ref={(el) => {
139
+ refs.current[i] = el
140
+ }}
141
+ id={i === 0 ? `${inputId}-0` : undefined}
142
+ type="text"
143
+ inputMode="numeric"
144
+ pattern="[0-9]*"
145
+ role="spinbutton"
146
+ aria-label={`Octet ${i + 1} of 4`}
147
+ aria-valuenow={fields[i] === '' ? 0 : Number(fields[i])}
148
+ aria-valuemin={0}
149
+ aria-valuemax={255}
150
+ value={fields[i]}
151
+ disabled={disabled}
152
+ maxLength={3}
153
+ className="cidr-input__octet"
154
+ onChange={(e) => handleChange(i, e)}
155
+ onKeyDown={(e) => handleKey(i, e)}
156
+ onPaste={handlePaste}
157
+ onFocus={(e) => e.currentTarget.select()}
158
+ />,
159
+ ]
160
+ nodes.push(
161
+ <span key={`d${i}`} className="cidr-input__dot" aria-hidden="true">
162
+ {i < 3 ? '.' : '/'}
163
+ </span>,
164
+ )
165
+ return nodes
166
+ })}
167
+ <input
168
+ ref={(el) => {
169
+ refs.current[4] = el
170
+ }}
171
+ type="text"
172
+ inputMode="numeric"
173
+ pattern="[0-9]*"
174
+ role="spinbutton"
175
+ aria-label="Prefix length"
176
+ aria-valuenow={fields[4] === '' ? 0 : Number(fields[4])}
177
+ aria-valuemin={0}
178
+ aria-valuemax={32}
179
+ value={fields[4]}
180
+ disabled={disabled}
181
+ maxLength={2}
182
+ className="cidr-input__prefix"
183
+ onChange={(e) => handleChange(4, e)}
184
+ onKeyDown={(e) => handleKey(4, e)}
185
+ onPaste={handlePaste}
186
+ onFocus={(e) => e.currentTarget.select()}
187
+ />
188
+ </div>
189
+ </div>
190
+ )
191
+ }
@@ -0,0 +1,117 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface CalendarDay {
4
+ date: string
5
+ value: number
6
+ }
7
+
8
+ export interface CalendarHeatmapProps {
9
+ data: CalendarDay[]
10
+ maxValue?: number
11
+ ariaLabel?: string
12
+ }
13
+
14
+ const CELL = 10
15
+ const GAP = 2
16
+ const STEP = CELL + GAP
17
+ const WEEKS = 52
18
+ const DAYS = 7
19
+ const LEFT = 22
20
+ const TOP = 16
21
+ const COLORS = ['rgba(255,255,255,0.06)', '#0d2a5e', '#1a4da6', '#2979ff', '#5ba4ff']
22
+ const DAY_LABELS = ['Mon', '', 'Wed', '', 'Fri', '', '']
23
+
24
+ function lvl(v: number, mx: number): number {
25
+ if (!v) return 0
26
+ const r = v / mx
27
+ return r < 0.25 ? 1 : r < 0.5 ? 2 : r < 0.75 ? 3 : 4
28
+ }
29
+
30
+ function toDateStr(d: Date): string {
31
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
32
+ }
33
+
34
+ const W = LEFT + WEEKS * STEP
35
+ const H = TOP + DAYS * STEP + 2
36
+
37
+ export function CalendarHeatmap({
38
+ data,
39
+ maxValue,
40
+ ariaLabel = 'Calendar heatmap',
41
+ }: CalendarHeatmapProps) {
42
+ const { cells, monthLabels } = useMemo(() => {
43
+ const byDate = new Map(data.map((d) => [d.date, d.value]))
44
+ const mx = maxValue ?? Math.max(1, ...data.map((d) => d.value))
45
+
46
+ const today = new Date()
47
+ today.setHours(0, 0, 0, 0)
48
+ const start = new Date(today)
49
+ start.setDate(start.getDate() - WEEKS * 7 + 1)
50
+ start.setDate(start.getDate() - start.getDay())
51
+
52
+ const cells: { x: number; y: number; fill: string }[] = []
53
+ const seenMonths = new Set<string>()
54
+ const monthLabels: { x: number; label: string }[] = []
55
+
56
+ for (let w = 0; w < WEEKS; w++) {
57
+ for (let d = 0; d < DAYS; d++) {
58
+ const date = new Date(start)
59
+ date.setDate(start.getDate() + w * 7 + d)
60
+ const monthKey = `${date.getFullYear()}-${date.getMonth()}`
61
+ if (!seenMonths.has(monthKey) && d === 0) {
62
+ seenMonths.add(monthKey)
63
+ monthLabels.push({
64
+ x: LEFT + w * STEP,
65
+ label: date.toLocaleString('en', { month: 'short' }),
66
+ })
67
+ }
68
+ const val = byDate.get(toDateStr(date)) ?? 0
69
+ cells.push({ x: LEFT + w * STEP, y: TOP + d * STEP, fill: COLORS[lvl(val, mx)] })
70
+ }
71
+ }
72
+
73
+ return { cells, monthLabels }
74
+ }, [data, maxValue])
75
+
76
+ return (
77
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%', overflowX: 'auto' }}>
78
+ <svg
79
+ viewBox={`0 0 ${W} ${H}`}
80
+ style={{ width: '100%', height: 'auto', display: 'block' }}
81
+ aria-hidden="true"
82
+ focusable="false"
83
+ >
84
+ {DAY_LABELS.map((label, d) =>
85
+ label ? (
86
+ <text
87
+ key={d}
88
+ x={LEFT - 3}
89
+ y={TOP + d * STEP + CELL * 0.8}
90
+ textAnchor="end"
91
+ fill="#6E7079"
92
+ fontSize={7}
93
+ fontFamily="inherit"
94
+ >
95
+ {label}
96
+ </text>
97
+ ) : null,
98
+ )}
99
+ {monthLabels.map((m) => (
100
+ <text
101
+ key={`${m.label}-${m.x}`}
102
+ x={m.x}
103
+ y={TOP - 3}
104
+ fill="#6E7079"
105
+ fontSize={7}
106
+ fontFamily="inherit"
107
+ >
108
+ {m.label}
109
+ </text>
110
+ ))}
111
+ {cells.map((c, i) => (
112
+ <rect key={i} x={c.x} y={c.y} width={CELL} height={CELL} rx={2} fill={c.fill} />
113
+ ))}
114
+ </svg>
115
+ </div>
116
+ )
117
+ }
@@ -0,0 +1,82 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export type CalloutVariant = 'info' | 'success' | 'warn' | 'danger' | 'tip'
4
+
5
+ export interface CalloutProps {
6
+ variant?: CalloutVariant
7
+ title?: string
8
+ children: ReactNode
9
+ }
10
+
11
+ function InfoIcon() {
12
+ return (
13
+ <svg className="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
14
+ <path
15
+ fillRule="evenodd"
16
+ d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1ZM7.25 5.5a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0ZM7 7.5h2v4H7v-4Z"
17
+ />
18
+ </svg>
19
+ )
20
+ }
21
+
22
+ function SuccessIcon() {
23
+ return (
24
+ <svg className="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
25
+ <path
26
+ fillRule="evenodd"
27
+ d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1Zm3.78 5.47-4.25 4.5a.75.75 0 0 1-1.06.03L4.72 9.22a.75.75 0 1 1 1.06-1.06l1.22 1.22 3.72-3.94a.75.75 0 1 1 1.06 1.03Z"
28
+ />
29
+ </svg>
30
+ )
31
+ }
32
+
33
+ function WarnIcon() {
34
+ return (
35
+ <svg className="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
36
+ <path
37
+ fillRule="evenodd"
38
+ d="M8.9 1.6a1 1 0 0 0-1.8 0L.6 13a1 1 0 0 0 .9 1.5h13a1 1 0 0 0 .9-1.5L8.9 1.6ZM7.25 6.5a.75.75 0 1 1 1.5 0v3a.75.75 0 0 1-1.5 0v-3Zm.75 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
39
+ />
40
+ </svg>
41
+ )
42
+ }
43
+
44
+ function DangerIcon() {
45
+ return (
46
+ <svg className="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
47
+ <path
48
+ fillRule="evenodd"
49
+ d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1ZM5.97 5.97a.75.75 0 0 1 1.06 0L8 6.94l.97-.97a.75.75 0 1 1 1.06 1.06L9.06 8l.97.97a.75.75 0 0 1-1.06 1.06L8 9.06l-.97.97a.75.75 0 0 1-1.06-1.06L6.94 8l-.97-.97a.75.75 0 0 1 0-1.06Z"
50
+ />
51
+ </svg>
52
+ )
53
+ }
54
+
55
+ function TipIcon() {
56
+ return (
57
+ <svg className="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
58
+ <path d="M8 1a5 5 0 0 1 3 9.01V11H5v-.99A5 5 0 0 1 8 1Zm-1 12h2v1.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V13Z" />
59
+ </svg>
60
+ )
61
+ }
62
+
63
+ const ICON: Record<CalloutVariant, () => JSX.Element> = {
64
+ info: InfoIcon,
65
+ success: SuccessIcon,
66
+ warn: WarnIcon,
67
+ danger: DangerIcon,
68
+ tip: TipIcon,
69
+ }
70
+
71
+ export function Callout({ variant = 'info', title, children }: CalloutProps) {
72
+ const Icon = ICON[variant]
73
+ return (
74
+ <div className={`callout callout--${variant}`} role="note">
75
+ <Icon />
76
+ <div className="callout-body">
77
+ {title && <div className="callout-title">{title}</div>}
78
+ <div>{children}</div>
79
+ </div>
80
+ </div>
81
+ )
82
+ }
@@ -0,0 +1,113 @@
1
+ export interface CandlestickBar {
2
+ label: string
3
+ open: number
4
+ close: number
5
+ high: number
6
+ low: number
7
+ }
8
+
9
+ export interface CandlestickChartProps {
10
+ bars: CandlestickBar[]
11
+ yRange?: [number, number]
12
+ height?: number
13
+ unit?: string
14
+ upColor?: string
15
+ downColor?: string
16
+ ariaLabel?: string
17
+ }
18
+
19
+ const VW = 340
20
+ const PAD_L = 28
21
+ const PAD_R = 8
22
+ const PAD_T = 8
23
+ const PAD_B = 20
24
+ const PLOT_W = VW - PAD_L - PAD_R
25
+
26
+ export function CandlestickChart({
27
+ bars,
28
+ yRange,
29
+ height = 180,
30
+ unit = '',
31
+ upColor = '#00C875',
32
+ downColor = '#FF7B7B',
33
+ ariaLabel = 'Candlestick chart',
34
+ }: CandlestickChartProps) {
35
+ if (!bars.length) return null
36
+
37
+ const n = bars.length
38
+ const PLOT_H = height - PAD_T - PAD_B
39
+
40
+ const allVals = bars.flatMap((b) => [b.low, b.high])
41
+ const minV = yRange ? yRange[0] : Math.min(...allVals)
42
+ const maxV = yRange ? yRange[1] : Math.max(...allVals)
43
+ const range = maxV - minV || 1
44
+
45
+ const tx = (i: number) => PAD_L + ((i + 0.5) / n) * PLOT_W
46
+ const ty = (v: number) => PAD_T + (1 - (v - minV) / range) * PLOT_H
47
+
48
+ const candleW = Math.max(4, (PLOT_W / n) * 0.5)
49
+ const yTicks = [minV, (minV + maxV) / 2, maxV]
50
+
51
+ return (
52
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
53
+ <svg
54
+ viewBox={`0 0 ${VW} ${height}`}
55
+ style={{ width: '100%', height: 'auto', display: 'block' }}
56
+ aria-hidden="true"
57
+ focusable="false"
58
+ >
59
+ {yTicks.map((v, i) => (
60
+ <g key={i}>
61
+ <line
62
+ x1={PAD_L}
63
+ y1={ty(v)}
64
+ x2={PAD_L + PLOT_W}
65
+ y2={ty(v)}
66
+ stroke="rgba(255,255,255,0.06)"
67
+ strokeWidth={1}
68
+ />
69
+ <text
70
+ x={PAD_L - 4}
71
+ y={ty(v) + 4}
72
+ fill="#6E7079"
73
+ fontSize={8}
74
+ textAnchor="end"
75
+ fontFamily="inherit"
76
+ >
77
+ {Math.round(v)}
78
+ {unit}
79
+ </text>
80
+ </g>
81
+ ))}
82
+
83
+ {bars.map((b, i) => {
84
+ const x = tx(i)
85
+ const color = b.close >= b.open ? upColor : downColor
86
+ const bodyTop = ty(Math.max(b.open, b.close))
87
+ const bodyBot = ty(Math.min(b.open, b.close))
88
+ const bodyH = Math.max(1, bodyBot - bodyTop)
89
+ return (
90
+ <g key={i}>
91
+ <line x1={x} y1={ty(b.high)} x2={x} y2={ty(b.low)} stroke={color} strokeWidth={1} />
92
+ <rect x={x - candleW / 2} y={bodyTop} width={candleW} height={bodyH} fill={color} />
93
+ </g>
94
+ )
95
+ })}
96
+
97
+ {bars.map((b, i) => (
98
+ <text
99
+ key={i}
100
+ x={tx(i)}
101
+ y={height - 4}
102
+ fill="#6E7079"
103
+ fontSize={9}
104
+ textAnchor="middle"
105
+ fontFamily="inherit"
106
+ >
107
+ {b.label}
108
+ </text>
109
+ ))}
110
+ </svg>
111
+ </div>
112
+ )
113
+ }
@@ -0,0 +1,27 @@
1
+ import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
4
+ span?: number
5
+ children?: ReactNode
6
+ }
7
+
8
+ export function Card({ span, style, className = '', children, ...rest }: CardProps) {
9
+ const merged: CSSProperties = { ...style, ...(span ? { gridColumn: `span ${span}` } : null) }
10
+ return (
11
+ <div className={`card ${className}`.trim()} style={merged} {...rest}>
12
+ {children}
13
+ </div>
14
+ )
15
+ }
16
+
17
+ export interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
18
+ children?: ReactNode
19
+ }
20
+
21
+ export function CardTitle({ children, className = '', ...rest }: CardTitleProps) {
22
+ return (
23
+ <h3 className={className} {...rest}>
24
+ {children}
25
+ </h3>
26
+ )
27
+ }
@@ -0,0 +1,78 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ export interface CarouselSlide {
4
+ id: string
5
+ title: string
6
+ description?: string
7
+ color?: string
8
+ }
9
+
10
+ export interface CarouselProps {
11
+ slides: CarouselSlide[]
12
+ label: string
13
+ className?: string
14
+ }
15
+
16
+ export function Carousel({ slides, label, className }: CarouselProps) {
17
+ const [index, setIndex] = useState(0)
18
+
19
+ const prev = useCallback(
20
+ () => setIndex((i) => (i - 1 + slides.length) % slides.length),
21
+ [slides.length],
22
+ )
23
+ const next = useCallback(() => setIndex((i) => (i + 1) % slides.length), [slides.length])
24
+
25
+ return (
26
+ <section
27
+ className={`carousel${className ? ' ' + className : ''}`}
28
+ aria-roledescription="carousel"
29
+ aria-label={label}
30
+ >
31
+ <div className="carousel-track" aria-live="polite" aria-atomic="false">
32
+ {slides.map((slide, i) => (
33
+ <div
34
+ key={slide.id}
35
+ role="group"
36
+ aria-roledescription="slide"
37
+ aria-label={`${i + 1} of ${slides.length}`}
38
+ className={`carousel-slide${i === index ? ' is-active' : ''}`}
39
+ aria-hidden={i !== index ? true : undefined}
40
+ >
41
+ {slide.color && (
42
+ <div
43
+ className="carousel-slide-accent"
44
+ style={{ background: slide.color }}
45
+ aria-hidden="true"
46
+ />
47
+ )}
48
+ <div className="carousel-slide-body">
49
+ <div className="carousel-slide-title">{slide.title}</div>
50
+ {slide.description && <div className="carousel-slide-desc">{slide.description}</div>}
51
+ </div>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ <div className="carousel-controls">
56
+ <button type="button" className="carousel-btn" aria-label="Previous slide" onClick={prev}>
57
+ &#8249;
58
+ </button>
59
+ <div role="tablist" aria-label="Choose slide to display" className="carousel-indicators">
60
+ {slides.map((slide, i) => (
61
+ <button
62
+ key={slide.id}
63
+ type="button"
64
+ role="tab"
65
+ aria-selected={i === index}
66
+ aria-label={`Slide ${i + 1}: ${slide.title}`}
67
+ className={`carousel-dot${i === index ? ' is-active' : ''}`}
68
+ onClick={() => setIndex(i)}
69
+ />
70
+ ))}
71
+ </div>
72
+ <button type="button" className="carousel-btn" aria-label="Next slide" onClick={next}>
73
+ &#8250;
74
+ </button>
75
+ </div>
76
+ </section>
77
+ )
78
+ }
@@ -0,0 +1,42 @@
1
+ import { useId, useRef, useEffect } from 'react'
2
+ import type { InputHTMLAttributes } from 'react'
3
+
4
+ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
+ label?: string
6
+ indeterminate?: boolean
7
+ }
8
+
9
+ export function Checkbox({
10
+ label,
11
+ indeterminate = false,
12
+ id,
13
+ className = '',
14
+ ...rest
15
+ }: CheckboxProps) {
16
+ const generatedId = useId()
17
+ const inputId = id ?? generatedId
18
+ const ref = useRef<HTMLInputElement>(null)
19
+
20
+ useEffect(() => {
21
+ if (ref.current) ref.current.indeterminate = indeterminate
22
+ }, [indeterminate])
23
+
24
+ const input = (
25
+ <input
26
+ ref={ref}
27
+ type="checkbox"
28
+ id={inputId}
29
+ className={`checkbox ${className}`.trim()}
30
+ {...rest}
31
+ />
32
+ )
33
+
34
+ if (!label) return input
35
+
36
+ return (
37
+ <label className="checkbox-label" htmlFor={inputId}>
38
+ {input}
39
+ <span>{label}</span>
40
+ </label>
41
+ )
42
+ }