@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,110 @@
1
+ export interface HorizonSeries {
2
+ label: string
3
+ values: number[]
4
+ color?: string
5
+ }
6
+
7
+ export interface HorizonChartProps {
8
+ series: HorizonSeries[]
9
+ xLabels?: string[]
10
+ bands?: number
11
+ ariaLabel?: string
12
+ }
13
+
14
+ const LABEL_W = 72
15
+ const ROW_H = 24
16
+ const ROW_GAP = 3
17
+ const X_H = 14
18
+ const W = 400
19
+
20
+ export function HorizonChart({
21
+ series,
22
+ xLabels,
23
+ bands = 3,
24
+ ariaLabel = 'Horizon chart',
25
+ }: HorizonChartProps) {
26
+ if (!series.length) return null
27
+ const n = series[0].values.length
28
+ const allVals = series.flatMap((s) => s.values)
29
+ const maxVal = Math.max(...allVals, 1)
30
+ const step = maxVal / bands
31
+ const chartW = W - LABEL_W
32
+ const colW = chartW / Math.max(n, 1)
33
+ const totalH = series.length * (ROW_H + ROW_GAP) - ROW_GAP + (xLabels ? X_H + 4 : 0)
34
+
35
+ return (
36
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
37
+ <svg
38
+ viewBox={`0 0 ${W} ${totalH}`}
39
+ style={{ width: '100%', height: 'auto', display: 'block' }}
40
+ aria-hidden="true"
41
+ focusable="false"
42
+ >
43
+ {series.map((s, si) => {
44
+ const rowY = si * (ROW_H + ROW_GAP)
45
+ const color = s.color ?? '#006FFF'
46
+ return (
47
+ <g key={si}>
48
+ <text
49
+ x={LABEL_W - 6}
50
+ y={rowY + ROW_H / 2 + 4}
51
+ textAnchor="end"
52
+ fill="#C8C9D0"
53
+ fontSize={9}
54
+ fontFamily="inherit"
55
+ >
56
+ {s.label}
57
+ </text>
58
+ <rect
59
+ x={LABEL_W}
60
+ y={rowY}
61
+ width={chartW}
62
+ height={ROW_H}
63
+ fill="rgba(255,255,255,0.04)"
64
+ rx={2}
65
+ />
66
+ {s.values.flatMap((v, ti) =>
67
+ Array.from({ length: bands }, (_, k) => {
68
+ const lo = k * step
69
+ const fill = Math.min(Math.max(v - lo, 0), step) / step
70
+ if (fill <= 0) return null
71
+ const alpha = 0.25 + 0.3 * k
72
+ const rH = fill * ROW_H
73
+ return (
74
+ <rect
75
+ key={`${ti}-${k}`}
76
+ x={LABEL_W + ti * colW}
77
+ y={rowY + ROW_H - rH}
78
+ width={Math.max(colW - 0.5, 0.5)}
79
+ height={rH}
80
+ fill={color}
81
+ opacity={alpha}
82
+ rx={1}
83
+ />
84
+ )
85
+ }),
86
+ )}
87
+ </g>
88
+ )
89
+ })}
90
+ {xLabels &&
91
+ xLabels.map((label, i) => {
92
+ const x = LABEL_W + (i / Math.max(xLabels.length - 1, 1)) * chartW
93
+ return (
94
+ <text
95
+ key={i}
96
+ x={x}
97
+ y={totalH}
98
+ textAnchor={i === 0 ? 'start' : i === xLabels.length - 1 ? 'end' : 'middle'}
99
+ fill="#6E7079"
100
+ fontSize={8}
101
+ fontFamily="inherit"
102
+ >
103
+ {label}
104
+ </text>
105
+ )
106
+ })}
107
+ </svg>
108
+ </div>
109
+ )
110
+ }
@@ -0,0 +1,52 @@
1
+ import { useId, useRef, useState, type ReactNode } from 'react'
2
+
3
+ export type HoverCardPlacement = 'top' | 'bottom' | 'left' | 'right'
4
+
5
+ export interface HoverCardProps {
6
+ content: ReactNode
7
+ children: ReactNode
8
+ placement?: HoverCardPlacement
9
+ delay?: number
10
+ className?: string
11
+ }
12
+
13
+ export function HoverCard({
14
+ content,
15
+ children,
16
+ placement = 'bottom',
17
+ delay = 300,
18
+ className = '',
19
+ }: HoverCardProps) {
20
+ const id = useId()
21
+ const [open, setOpen] = useState(false)
22
+ const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
23
+
24
+ function show() {
25
+ if (timer.current) clearTimeout(timer.current)
26
+ timer.current = setTimeout(() => setOpen(true), delay)
27
+ }
28
+
29
+ function hide() {
30
+ if (timer.current) clearTimeout(timer.current)
31
+ setOpen(false)
32
+ }
33
+
34
+ return (
35
+ <div
36
+ className={`hovercard-wrapper hovercard-${placement} ${className}`.trim()}
37
+ onMouseEnter={show}
38
+ onMouseLeave={hide}
39
+ onFocus={show}
40
+ onBlur={hide}
41
+ >
42
+ <div className="hovercard-trigger" aria-describedby={open ? id : undefined}>
43
+ {children}
44
+ </div>
45
+ {open && (
46
+ <div id={id} role="tooltip" className="hovercard">
47
+ {content}
48
+ </div>
49
+ )}
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,147 @@
1
+ import { useId, useRef, useState } from 'react'
2
+ import type { ReactNode, ClipboardEvent, KeyboardEvent, ChangeEvent } from 'react'
3
+
4
+ export interface IPInputProps {
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 parseIP(ip: string): [string, string, string, string] {
22
+ const parts = ip.split('.')
23
+ return [parts[0] ?? '0', parts[1] ?? '0', parts[2] ?? '0', parts[3] ?? '0']
24
+ }
25
+
26
+ export function IPInput({
27
+ label,
28
+ value: valueProp,
29
+ defaultValue = '0.0.0.0',
30
+ disabled = false,
31
+ onChange,
32
+ id,
33
+ className = '',
34
+ }: IPInputProps) {
35
+ const gid = useId()
36
+ const inputId = id ?? gid
37
+ const [internal, setInternal] = useState<[string, string, string, string]>(() =>
38
+ parseIP(valueProp ?? defaultValue),
39
+ )
40
+ const refs = useRef<(HTMLInputElement | null)[]>([null, null, null, null])
41
+
42
+ const octets = valueProp !== undefined ? parseIP(valueProp) : internal
43
+
44
+ function focusAt(i: number) {
45
+ const el = refs.current[i]
46
+ if (el) {
47
+ el.focus()
48
+ el.select()
49
+ }
50
+ }
51
+
52
+ function commit(i: number, val: string) {
53
+ const next = [...octets] as [string, string, string, string]
54
+ next[i] = clampOctet(val)
55
+ if (valueProp === undefined) setInternal(next)
56
+ onChange?.(next.join('.'))
57
+ }
58
+
59
+ function handleChange(i: number, e: ChangeEvent<HTMLInputElement>) {
60
+ const raw = e.target.value
61
+ if (!/^\d{0,3}$/.test(raw)) return
62
+ commit(i, raw)
63
+ if (raw.length === 3 && i < 3) focusAt(i + 1)
64
+ }
65
+
66
+ function handleKey(i: number, e: KeyboardEvent<HTMLInputElement>) {
67
+ const el = refs.current[i]!
68
+ if (e.key === '.') {
69
+ e.preventDefault()
70
+ if (i < 3) focusAt(i + 1)
71
+ } else if (e.key === 'ArrowRight' && el.selectionStart === el.value.length) {
72
+ if (i < 3) {
73
+ e.preventDefault()
74
+ focusAt(i + 1)
75
+ }
76
+ } else if (e.key === 'ArrowLeft' && el.selectionStart === 0) {
77
+ if (i > 0) {
78
+ e.preventDefault()
79
+ focusAt(i - 1)
80
+ }
81
+ } else if (e.key === 'Backspace' && el.value === '' && i > 0) {
82
+ e.preventDefault()
83
+ focusAt(i - 1)
84
+ }
85
+ }
86
+
87
+ function handlePaste(e: ClipboardEvent<HTMLInputElement>) {
88
+ const text = e.clipboardData.getData('text').trim()
89
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(text)) {
90
+ e.preventDefault()
91
+ const next = text.split('.').map(clampOctet) as [string, string, string, string]
92
+ if (valueProp === undefined) setInternal(next)
93
+ onChange?.(next.join('.'))
94
+ focusAt(3)
95
+ }
96
+ }
97
+
98
+ return (
99
+ <div className={`ip-input-wrapper ${className}`.trim()}>
100
+ {label && (
101
+ <label htmlFor={`${inputId}-0`} className="ip-input__label">
102
+ {label}
103
+ </label>
104
+ )}
105
+ <div
106
+ className={`ip-input${disabled ? ' ip-input--disabled' : ''}`}
107
+ role="group"
108
+ aria-label={label ?? 'IP address'}
109
+ >
110
+ {octets.flatMap((oct, i) => {
111
+ const nodes: ReactNode[] = [
112
+ <input
113
+ key={`o${i}`}
114
+ ref={(el) => {
115
+ refs.current[i] = el
116
+ }}
117
+ id={i === 0 ? `${inputId}-0` : undefined}
118
+ type="text"
119
+ inputMode="numeric"
120
+ pattern="[0-9]*"
121
+ role="spinbutton"
122
+ aria-label={`Octet ${i + 1} of 4`}
123
+ aria-valuenow={oct === '' ? 0 : Number(oct)}
124
+ aria-valuemin={0}
125
+ aria-valuemax={255}
126
+ value={oct}
127
+ disabled={disabled}
128
+ maxLength={3}
129
+ className="ip-input__octet"
130
+ onChange={(e) => handleChange(i, e)}
131
+ onKeyDown={(e) => handleKey(i, e)}
132
+ onPaste={handlePaste}
133
+ onFocus={(e) => e.currentTarget.select()}
134
+ />,
135
+ ]
136
+ if (i < 3)
137
+ nodes.push(
138
+ <span key={`d${i}`} className="ip-input__dot" aria-hidden="true">
139
+ .
140
+ </span>,
141
+ )
142
+ return nodes
143
+ })}
144
+ </div>
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,118 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface IcicleNode {
4
+ label: string
5
+ value?: number
6
+ color?: string
7
+ children?: IcicleNode[]
8
+ }
9
+
10
+ export interface IcicleChartProps {
11
+ root: IcicleNode
12
+ height?: number
13
+ ariaLabel?: string
14
+ }
15
+
16
+ interface FlatCell {
17
+ x: number
18
+ y: number
19
+ w: number
20
+ h: number
21
+ label: string
22
+ color: string
23
+ depth: number
24
+ }
25
+
26
+ const PALETTE = ['#FF7B7B', '#F5A623', '#A78BFA', '#34D399', '#006FFF', '#00C875', '#FB923C']
27
+ const VW = 340
28
+ const GAP = 1.5
29
+
30
+ function nodeValue(node: IcicleNode): number {
31
+ if (node.children?.length) return node.children.reduce((s, c) => s + nodeValue(c), 0)
32
+ return node.value ?? 1
33
+ }
34
+
35
+ function treeDepth(node: IcicleNode): number {
36
+ if (!node.children?.length) return 0
37
+ return 1 + Math.max(...node.children.map(treeDepth))
38
+ }
39
+
40
+ function flatten(
41
+ node: IcicleNode,
42
+ x: number,
43
+ y: number,
44
+ w: number,
45
+ levelH: number,
46
+ depth: number,
47
+ inheritColor: string,
48
+ colorIdx: { n: number },
49
+ cells: FlatCell[],
50
+ ): void {
51
+ let color = node.color ?? ''
52
+ if (!color) {
53
+ if (depth === 0) color = 'rgba(255,255,255,0.10)'
54
+ else if (depth === 1) color = PALETTE[colorIdx.n++ % PALETTE.length]
55
+ else color = inheritColor
56
+ }
57
+ cells.push({ x, y, w, h: levelH, label: node.label, color, depth })
58
+ if (node.children?.length && w > 0) {
59
+ const total = nodeValue(node)
60
+ let cx = x
61
+ for (const child of node.children) {
62
+ const cw = total > 0 ? (nodeValue(child) / total) * w : 0
63
+ flatten(child, cx, y + levelH, cw, levelH, depth + 1, color, colorIdx, cells)
64
+ cx += cw
65
+ }
66
+ }
67
+ }
68
+
69
+ export function IcicleChart({ root, height = 160, ariaLabel = 'Icicle chart' }: IcicleChartProps) {
70
+ const cells = useMemo(() => {
71
+ const depth = treeDepth(root)
72
+ const levelH = height / (depth + 1)
73
+ const result: FlatCell[] = []
74
+ flatten(root, 0, 0, VW, levelH, 0, PALETTE[0], { n: 0 }, result)
75
+ return result
76
+ }, [root, height])
77
+
78
+ if (!cells.length) return null
79
+
80
+ return (
81
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
82
+ <svg
83
+ viewBox={`0 0 ${VW} ${height}`}
84
+ style={{ width: '100%', height: 'auto', display: 'block' }}
85
+ aria-hidden="true"
86
+ focusable="false"
87
+ >
88
+ {cells.map((cell, i) =>
89
+ cell.w > GAP * 2 ? (
90
+ <g key={i}>
91
+ <rect
92
+ x={cell.x + GAP / 2}
93
+ y={cell.y + GAP / 2}
94
+ width={Math.max(0, cell.w - GAP)}
95
+ height={Math.max(0, cell.h - GAP)}
96
+ fill={cell.color}
97
+ fillOpacity={cell.depth === 0 ? 1 : cell.depth === 1 ? 0.7 : 0.85}
98
+ rx={2}
99
+ />
100
+ {cell.w > 32 && (
101
+ <text
102
+ x={cell.x + cell.w / 2}
103
+ y={cell.y + cell.h / 2 + 4}
104
+ textAnchor="middle"
105
+ fontSize={Math.min(10, (cell.w - GAP) / 6, cell.h - 4)}
106
+ fill="#fff"
107
+ fontFamily="inherit"
108
+ >
109
+ {cell.label}
110
+ </text>
111
+ )}
112
+ </g>
113
+ ) : null,
114
+ )}
115
+ </svg>
116
+ </div>
117
+ )
118
+ }
@@ -0,0 +1,119 @@
1
+ import { useState, useRef, useId } from 'react'
2
+
3
+ const PencilIcon = () => (
4
+ <svg viewBox="0 0 14 14" width="12" height="12" fill="none" aria-hidden="true" focusable={false}>
5
+ <path
6
+ d="M9.5 2l2.5 2.5-8 8H1.5v-2.5l8-8z"
7
+ stroke="currentColor"
8
+ strokeWidth="1.5"
9
+ strokeLinecap="round"
10
+ strokeLinejoin="round"
11
+ />
12
+ </svg>
13
+ )
14
+
15
+ const CheckIcon = () => (
16
+ <svg viewBox="0 0 14 14" width="12" height="12" fill="none" aria-hidden="true" focusable={false}>
17
+ <path
18
+ d="M2.5 7.5l3.5 3.5L12 3"
19
+ stroke="currentColor"
20
+ strokeWidth="1.5"
21
+ strokeLinecap="round"
22
+ strokeLinejoin="round"
23
+ />
24
+ </svg>
25
+ )
26
+
27
+ const CrossIcon = () => (
28
+ <svg viewBox="0 0 14 14" width="12" height="12" fill="none" aria-hidden="true" focusable={false}>
29
+ <path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
30
+ </svg>
31
+ )
32
+
33
+ export interface InlineEditProps {
34
+ value: string
35
+ onConfirm: (value: string) => void
36
+ label: string
37
+ placeholder?: string
38
+ }
39
+
40
+ export function InlineEdit({ value, onConfirm, label, placeholder }: InlineEditProps) {
41
+ const [editing, setEditing] = useState(false)
42
+ const [draft, setDraft] = useState(value)
43
+ const inputRef = useRef<HTMLInputElement>(null)
44
+ const editBtnRef = useRef<HTMLButtonElement>(null)
45
+ const inputId = useId()
46
+
47
+ function startEdit() {
48
+ setDraft(value)
49
+ setEditing(true)
50
+ requestAnimationFrame(() => inputRef.current?.focus())
51
+ }
52
+
53
+ function confirm() {
54
+ const trimmed = draft.trim()
55
+ if (trimmed) onConfirm(trimmed)
56
+ setEditing(false)
57
+ requestAnimationFrame(() => editBtnRef.current?.focus())
58
+ }
59
+
60
+ function cancel() {
61
+ setDraft(value)
62
+ setEditing(false)
63
+ requestAnimationFrame(() => editBtnRef.current?.focus())
64
+ }
65
+
66
+ function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
67
+ if (e.key === 'Enter') {
68
+ e.preventDefault()
69
+ confirm()
70
+ }
71
+ if (e.key === 'Escape') {
72
+ e.preventDefault()
73
+ cancel()
74
+ }
75
+ }
76
+
77
+ if (editing) {
78
+ return (
79
+ <span className="ie ie--editing">
80
+ <input
81
+ ref={inputRef}
82
+ id={inputId}
83
+ className="ie__input"
84
+ aria-label={`Edit ${label}`}
85
+ value={draft}
86
+ placeholder={placeholder}
87
+ onChange={(e) => setDraft(e.target.value)}
88
+ onKeyDown={handleKeyDown}
89
+ />
90
+ <button type="button" className="ie__btn ie__btn--save" aria-label="Save" onClick={confirm}>
91
+ <CheckIcon />
92
+ </button>
93
+ <button
94
+ type="button"
95
+ className="ie__btn ie__btn--cancel"
96
+ aria-label="Cancel"
97
+ onClick={cancel}
98
+ >
99
+ <CrossIcon />
100
+ </button>
101
+ </span>
102
+ )
103
+ }
104
+
105
+ return (
106
+ <span className="ie">
107
+ <span className="ie__text">{value}</span>
108
+ <button
109
+ ref={editBtnRef}
110
+ type="button"
111
+ className="ie__edit"
112
+ aria-label={`Edit ${label}`}
113
+ onClick={startEdit}
114
+ >
115
+ <PencilIcon />
116
+ </button>
117
+ </span>
118
+ )
119
+ }
@@ -0,0 +1,94 @@
1
+ import { useId } from 'react'
2
+ import type { InputHTMLAttributes, ReactNode } from 'react'
3
+ import { SearchIcon } from '../icons.js'
4
+
5
+ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
6
+
7
+ export function Input({ className = '', ...rest }: InputProps) {
8
+ return <input className={`input ${className}`.trim()} {...rest} />
9
+ }
10
+
11
+ export interface FieldProps {
12
+ label: string
13
+ children?: ReactNode
14
+ defaultValue?: string
15
+ value?: string
16
+ id?: string
17
+ error?: string
18
+ required?: boolean
19
+ hint?: string
20
+ }
21
+
22
+ export function Field({
23
+ label,
24
+ children,
25
+ defaultValue,
26
+ value,
27
+ id,
28
+ error,
29
+ required,
30
+ hint,
31
+ }: FieldProps) {
32
+ const generatedId = useId()
33
+ const inputId = id ?? generatedId
34
+ const errorId = error ? `${inputId}-error` : undefined
35
+ const hintId = hint ? `${inputId}-hint` : undefined
36
+ const describedBy = [hintId, errorId].filter(Boolean).join(' ') || undefined
37
+ return (
38
+ <div className="field">
39
+ <label htmlFor={inputId}>
40
+ {label}
41
+ {required && (
42
+ <span className="req" aria-hidden="true">
43
+ {' '}
44
+ *
45
+ </span>
46
+ )}
47
+ </label>
48
+ {hint && (
49
+ <div className="field-hint" id={hintId}>
50
+ {hint}
51
+ </div>
52
+ )}
53
+ {children ?? (
54
+ <Input
55
+ id={inputId}
56
+ defaultValue={defaultValue}
57
+ value={value}
58
+ required={required}
59
+ aria-invalid={error ? true : undefined}
60
+ aria-describedby={describedBy}
61
+ />
62
+ )}
63
+ {error && (
64
+ <div className="field-error" id={errorId} role="alert">
65
+ {error}
66
+ </div>
67
+ )}
68
+ </div>
69
+ )
70
+ }
71
+
72
+ export interface SearchBoxProps extends InputHTMLAttributes<HTMLInputElement> {
73
+ placeholder?: string
74
+ }
75
+
76
+ export function SearchBox({ placeholder, type, 'aria-label': ariaLabel, ...rest }: SearchBoxProps) {
77
+ return (
78
+ <div className="search" role="search">
79
+ <SearchIcon
80
+ style={{ color: '#6E7079' }}
81
+ width={14}
82
+ height={14}
83
+ aria-hidden="true"
84
+ focusable="false"
85
+ />
86
+ <input
87
+ type={type ?? 'search'}
88
+ placeholder={placeholder}
89
+ aria-label={ariaLabel ?? placeholder}
90
+ {...rest}
91
+ />
92
+ </div>
93
+ )
94
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export interface InputGroupProps {
4
+ prefix?: ReactNode
5
+ suffix?: ReactNode
6
+ children: ReactNode
7
+ }
8
+
9
+ export function InputGroup({ prefix, suffix, children }: InputGroupProps) {
10
+ return (
11
+ <div className="input-group">
12
+ {prefix !== undefined && (
13
+ <span className="input-group-addon input-group-addon--prefix">{prefix}</span>
14
+ )}
15
+ {children}
16
+ {suffix !== undefined && (
17
+ <span className="input-group-addon input-group-addon--suffix">{suffix}</span>
18
+ )}
19
+ </div>
20
+ )
21
+ }