@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,116 @@
1
+ import { useState } from 'react'
2
+
3
+ type JsVal = string | number | boolean | null | JsVal[] | { [k: string]: JsVal }
4
+
5
+ function Primitive({ v }: { v: string | number | boolean | null }) {
6
+ if (v === null) return <span className="jv-null">null</span>
7
+ if (typeof v === 'string') return <span className="jv-string">&quot;{v}&quot;</span>
8
+ if (typeof v === 'number') return <span className="jv-number">{v}</span>
9
+ return <span className="jv-boolean">{String(v)}</span>
10
+ }
11
+
12
+ function JvNode({
13
+ v,
14
+ k,
15
+ depth,
16
+ maxDepth,
17
+ startOpen,
18
+ }: {
19
+ v: JsVal
20
+ k?: string
21
+ depth: number
22
+ maxDepth: number
23
+ startOpen: boolean
24
+ }) {
25
+ const isArr = Array.isArray(v)
26
+ const isObj = v !== null && typeof v === 'object'
27
+ const [open, setOpen] = useState(startOpen && depth < maxDepth)
28
+
29
+ const indent = depth * 14
30
+
31
+ if (!isArr && !isObj) {
32
+ return (
33
+ <div role="treeitem" aria-selected={false} className="jv-row" style={{ paddingLeft: indent }}>
34
+ {k != null && (
35
+ <>
36
+ <span className="jv-key">&quot;{k}&quot;</span>
37
+ <span className="jv-colon">: </span>
38
+ </>
39
+ )}
40
+ <Primitive v={v as string | number | boolean | null} />
41
+ </div>
42
+ )
43
+ }
44
+
45
+ const entries: [string | number, JsVal][] = isArr
46
+ ? (v as JsVal[]).map((item, i) => [i, item])
47
+ : Object.entries(v as { [key: string]: JsVal })
48
+ const count = entries.length
49
+ const [ob, cb] = isArr ? ['[', ']'] : ['{', '}']
50
+
51
+ return (
52
+ <div role="treeitem" aria-selected={false} aria-expanded={open} className="jv-node">
53
+ <button
54
+ type="button"
55
+ className="jv-toggle-row"
56
+ onClick={() => setOpen((s) => !s)}
57
+ aria-label={`${open ? 'Collapse' : 'Expand'} ${k ?? (isArr ? 'array' : 'object')}`}
58
+ style={{ paddingLeft: indent }}
59
+ >
60
+ <span className="jv-caret" aria-hidden="true">
61
+ {open ? '▾' : '▸'}
62
+ </span>
63
+ {k != null && (
64
+ <>
65
+ <span className="jv-key">&quot;{k}&quot;</span>
66
+ <span className="jv-colon">: </span>
67
+ </>
68
+ )}
69
+ <span className="jv-bracket">{ob}</span>
70
+ {!open && (
71
+ <>
72
+ <span className="jv-count">&thinsp;{count}&thinsp;</span>
73
+ <span className="jv-bracket">{cb}</span>
74
+ </>
75
+ )}
76
+ </button>
77
+ {open && (
78
+ <div role="group" className="jv-children">
79
+ {entries.map(([ek, ev]) => (
80
+ <JvNode
81
+ key={String(ek)}
82
+ v={ev}
83
+ k={isArr ? undefined : String(ek)}
84
+ depth={depth + 1}
85
+ maxDepth={maxDepth}
86
+ startOpen={startOpen}
87
+ />
88
+ ))}
89
+ <div className="jv-row jv-close" style={{ paddingLeft: indent }}>
90
+ <span className="jv-bracket">{cb}</span>
91
+ </div>
92
+ </div>
93
+ )}
94
+ </div>
95
+ )
96
+ }
97
+
98
+ export interface JsonViewerProps {
99
+ data: unknown
100
+ label?: string
101
+ defaultExpanded?: boolean
102
+ maxDepth?: number
103
+ }
104
+
105
+ export function JsonViewer({
106
+ data,
107
+ label = 'JSON viewer',
108
+ defaultExpanded = true,
109
+ maxDepth = 3,
110
+ }: JsonViewerProps) {
111
+ return (
112
+ <div className="json-viewer" role="tree" aria-label={label}>
113
+ <JvNode v={data as JsVal} depth={0} maxDepth={maxDepth} startOpen={defaultExpanded} />
114
+ </div>
115
+ )
116
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export interface KVRow {
4
+ label: string
5
+ value: ReactNode
6
+ }
7
+
8
+ export interface KVTableProps {
9
+ rows: KVRow[]
10
+ caption?: string
11
+ }
12
+
13
+ export function KVTable({ rows, caption }: KVTableProps) {
14
+ return (
15
+ <table className="kv-table">
16
+ {caption && <caption className="sr-only">{caption}</caption>}
17
+ <tbody>
18
+ {rows.map(({ label, value }) => (
19
+ <tr key={label} className="kv-table__row">
20
+ <th scope="row" className="kv-table__label">
21
+ {label}
22
+ </th>
23
+ <td className="kv-table__value">{value}</td>
24
+ </tr>
25
+ ))}
26
+ </tbody>
27
+ </table>
28
+ )
29
+ }
@@ -0,0 +1,171 @@
1
+ import { useId, useRef, useState } from 'react'
2
+
3
+ export interface KanbanCard {
4
+ id: string
5
+ title: string
6
+ subtitle?: string
7
+ meta?: string
8
+ }
9
+
10
+ export interface KanbanColumn {
11
+ id: string
12
+ title: string
13
+ color?: string
14
+ cards: KanbanCard[]
15
+ }
16
+
17
+ export interface KanbanBoardProps {
18
+ columns: KanbanColumn[]
19
+ onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string) => void
20
+ ariaLabel?: string
21
+ }
22
+
23
+ export function KanbanBoard({ columns, onCardMove, ariaLabel = 'Kanban board' }: KanbanBoardProps) {
24
+ const announceId = useId()
25
+ const [announce, setAnnounce] = useState('')
26
+ const [grabbed, setGrabbed] = useState<{ cardId: string; colId: string } | null>(null)
27
+ const [dragSrc, setDragSrc] = useState<{ cardId: string; colId: string } | null>(null)
28
+ const [dragOverCol, setDragOverCol] = useState<string | null>(null)
29
+ const cardRefs = useRef<Record<string, HTMLButtonElement | null>>({})
30
+
31
+ function say(msg: string) {
32
+ setAnnounce(msg)
33
+ }
34
+
35
+ function colOf(cardId: string): KanbanColumn | undefined {
36
+ return columns.find((c) => c.cards.some((k) => k.id === cardId))
37
+ }
38
+
39
+ function handleKeyDown(
40
+ e: React.KeyboardEvent<HTMLButtonElement>,
41
+ card: KanbanCard,
42
+ col: KanbanColumn,
43
+ ) {
44
+ if (e.key === ' ' || e.key === 'Enter') {
45
+ e.preventDefault()
46
+ if (grabbed?.cardId === card.id) {
47
+ setGrabbed(null)
48
+ say(`Dropped ${card.title} in ${col.title}.`)
49
+ } else {
50
+ setGrabbed({ cardId: card.id, colId: col.id })
51
+ say(
52
+ `Picked up ${card.title} from ${col.title}. Use left/right arrows to move between columns, Space to drop, Escape to cancel.`,
53
+ )
54
+ }
55
+ } else if (e.key === 'Escape' && grabbed?.cardId === card.id) {
56
+ e.preventDefault()
57
+ setGrabbed(null)
58
+ say('Move cancelled.')
59
+ } else if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && grabbed?.cardId === card.id) {
60
+ e.preventDefault()
61
+ const idx = columns.findIndex((c) => c.id === grabbed.colId)
62
+ const nextIdx = e.key === 'ArrowLeft' ? idx - 1 : idx + 1
63
+ if (nextIdx < 0 || nextIdx >= columns.length) return
64
+ const nextCol = columns[nextIdx]
65
+ onCardMove?.(card.id, grabbed.colId, nextCol.id)
66
+ setGrabbed({ cardId: card.id, colId: nextCol.id })
67
+ say(`Moved ${card.title} to ${nextCol.title}.`)
68
+ setTimeout(() => {
69
+ cardRefs.current[card.id]?.focus()
70
+ }, 0)
71
+ }
72
+ }
73
+
74
+ function handleDragStart(e: React.DragEvent, cardId: string, colId: string) {
75
+ e.dataTransfer.effectAllowed = 'move'
76
+ setDragSrc({ cardId, colId })
77
+ }
78
+
79
+ function handleDrop(e: React.DragEvent, toColId: string) {
80
+ e.preventDefault()
81
+ if (!dragSrc || dragSrc.colId === toColId) return
82
+ const card = colOf(dragSrc.cardId)?.cards.find((c) => c.id === dragSrc.cardId)
83
+ const toCol = columns.find((c) => c.id === toColId)
84
+ onCardMove?.(dragSrc.cardId, dragSrc.colId, toColId)
85
+ if (card && toCol) say(`Moved ${card.title} to ${toCol.title}.`)
86
+ setDragSrc(null)
87
+ setDragOverCol(null)
88
+ }
89
+
90
+ return (
91
+ <div className="kanban" role="region" aria-label={ariaLabel}>
92
+ <span
93
+ id={announceId}
94
+ role="status"
95
+ aria-live="assertive"
96
+ aria-atomic="true"
97
+ className="sr-only"
98
+ >
99
+ {announce}
100
+ </span>
101
+
102
+ <div className="kanban-board" role="list">
103
+ {columns.map((col) => (
104
+ <div
105
+ key={col.id}
106
+ className={`kanban-col${dragOverCol === col.id ? ' kanban-col--over' : ''}`}
107
+ role="listitem"
108
+ onDragOver={(e) => {
109
+ e.preventDefault()
110
+ e.dataTransfer.dropEffect = 'move'
111
+ setDragOverCol(col.id)
112
+ }}
113
+ onDragLeave={() => setDragOverCol(null)}
114
+ onDrop={(e) => handleDrop(e, col.id)}
115
+ >
116
+ <div className="kanban-col-header">
117
+ {col.color && (
118
+ <span
119
+ className="kanban-col-dot"
120
+ style={{ background: col.color }}
121
+ aria-hidden="true"
122
+ />
123
+ )}
124
+ <span className="kanban-col-title">{col.title}</span>
125
+ <span
126
+ className="kanban-col-count"
127
+ aria-label={`${col.cards.length} card${col.cards.length !== 1 ? 's' : ''}`}
128
+ >
129
+ {col.cards.length}
130
+ </span>
131
+ </div>
132
+ <ul className="kanban-cards" aria-label={`${col.title} cards`}>
133
+ {col.cards.map((card) => {
134
+ const isGrabbed = grabbed?.cardId === card.id
135
+ return (
136
+ <li key={card.id} className={`kanban-card-item${isGrabbed ? ' is-grabbed' : ''}`}>
137
+ <button
138
+ ref={(el) => {
139
+ cardRefs.current[card.id] = el
140
+ }}
141
+ type="button"
142
+ className="kanban-card"
143
+ draggable
144
+ aria-pressed={isGrabbed}
145
+ aria-label={`${card.title}${card.subtitle ? `, ${card.subtitle}` : ''}${card.meta ? `, ${card.meta}` : ''}. ${isGrabbed ? 'Moving. Use arrow keys to change column, Space to drop, Escape to cancel.' : 'Press Space to move.'}`}
146
+ onKeyDown={(e) => handleKeyDown(e, card, col)}
147
+ onDragStart={(e) => handleDragStart(e, card.id, col.id)}
148
+ onDragEnd={() => {
149
+ setDragSrc(null)
150
+ setDragOverCol(null)
151
+ }}
152
+ >
153
+ <span className="kanban-card-title">{card.title}</span>
154
+ {card.subtitle && <span className="kanban-card-sub">{card.subtitle}</span>}
155
+ {card.meta && <span className="kanban-card-meta">{card.meta}</span>}
156
+ </button>
157
+ </li>
158
+ )
159
+ })}
160
+ {col.cards.length === 0 && (
161
+ <li className="kanban-empty" role="note" aria-label={`${col.title} is empty`}>
162
+ Empty
163
+ </li>
164
+ )}
165
+ </ul>
166
+ </div>
167
+ ))}
168
+ </div>
169
+ </div>
170
+ )
171
+ }
@@ -0,0 +1,22 @@
1
+ export interface KbdProps {
2
+ keys: string | string[]
3
+ className?: string
4
+ }
5
+
6
+ export function Kbd({ keys, className = '' }: KbdProps) {
7
+ const parts = typeof keys === 'string' ? keys.split('+') : keys
8
+ return (
9
+ <kbd className={`kbd ${className}`.trim()}>
10
+ {parts.map((k, i) => (
11
+ <span key={i} className="kbd__segment">
12
+ {i > 0 && (
13
+ <span className="kbd__plus" aria-hidden="true">
14
+ +
15
+ </span>
16
+ )}
17
+ <kbd className="kbd__key">{k}</kbd>
18
+ </span>
19
+ ))}
20
+ </kbd>
21
+ )
22
+ }
@@ -0,0 +1,112 @@
1
+ import { useMemo } from 'react'
2
+
3
+ export interface LineChartSeries {
4
+ label: string
5
+ color: string
6
+ values: number[]
7
+ }
8
+
9
+ export interface LineChartProps {
10
+ series: LineChartSeries[]
11
+ labels?: string[]
12
+ height?: number
13
+ ariaLabel?: string
14
+ }
15
+
16
+ const VW = 400
17
+ const PAD = { t: 12, r: 8, b: 28, l: 8 }
18
+
19
+ export function LineChart({
20
+ series,
21
+ labels = [],
22
+ height = 160,
23
+ ariaLabel = 'Line chart',
24
+ }: LineChartProps) {
25
+ const { paths, areas, gridLines } = useMemo(() => {
26
+ const allValues = series.flatMap((s) => s.values)
27
+ if (!allValues.length) return { paths: [], areas: [], gridLines: [] }
28
+ const minV = Math.min(...allValues)
29
+ const maxV = Math.max(...allValues)
30
+ const range = maxV - minV || 1
31
+ const chartW = VW - PAD.l - PAD.r
32
+ const chartH = height - PAD.t - PAD.b
33
+ const n = series[0]?.values.length ?? 0
34
+
35
+ const toX = (i: number) => PAD.l + (n > 1 ? (i / (n - 1)) * chartW : chartW / 2)
36
+ const toY = (v: number) => PAD.t + chartH - ((v - minV) / range) * chartH
37
+
38
+ const gridLines = [0, 0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH)
39
+
40
+ const paths = series.map((s) => {
41
+ const pts = s.values.map((v, i) => `${toX(i).toFixed(1)},${toY(v).toFixed(1)}`)
42
+ return { color: s.color, label: s.label, d: `M ${pts.join(' L ')}` }
43
+ })
44
+
45
+ const areas = series.map((s) => {
46
+ const pts = s.values.map((v, i) => `${toX(i).toFixed(1)},${toY(v).toFixed(1)}`)
47
+ const bot = (PAD.t + chartH).toFixed(1)
48
+ return {
49
+ color: s.color,
50
+ label: s.label,
51
+ d: `M ${toX(0).toFixed(1)},${bot} L ${pts.join(' L ')} L ${toX(n - 1).toFixed(1)},${bot} Z`,
52
+ }
53
+ })
54
+
55
+ return { paths, areas, gridLines }
56
+ }, [series, height])
57
+
58
+ const chartW = VW - PAD.l - PAD.r
59
+
60
+ return (
61
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
62
+ <svg
63
+ viewBox={`0 0 ${VW} ${height}`}
64
+ style={{ width: '100%', height: 'auto', display: 'block' }}
65
+ aria-hidden="true"
66
+ focusable="false"
67
+ >
68
+ {gridLines.map((y, i) => (
69
+ <line
70
+ key={i}
71
+ x1={PAD.l}
72
+ y1={y}
73
+ x2={VW - PAD.r}
74
+ y2={y}
75
+ stroke="rgba(255,255,255,0.06)"
76
+ strokeWidth={1}
77
+ />
78
+ ))}
79
+ {areas.map((s) => (
80
+ <path key={s.label} d={s.d} fill={s.color} fillOpacity={0.1} />
81
+ ))}
82
+ {paths.map((s) => (
83
+ <path
84
+ key={s.label}
85
+ d={s.d}
86
+ fill="none"
87
+ stroke={s.color}
88
+ strokeWidth={2}
89
+ strokeLinejoin="round"
90
+ strokeLinecap="round"
91
+ />
92
+ ))}
93
+ {labels.map((lbl, i) => {
94
+ const x = PAD.l + (labels.length > 1 ? (i / (labels.length - 1)) * chartW : chartW / 2)
95
+ return (
96
+ <text
97
+ key={i}
98
+ x={x}
99
+ y={height - 4}
100
+ textAnchor="middle"
101
+ fill="#6E7079"
102
+ fontSize={9}
103
+ fontFamily="inherit"
104
+ >
105
+ {lbl}
106
+ </text>
107
+ )
108
+ })}
109
+ </svg>
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,102 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+
3
+ export interface LogEntry {
4
+ id?: string
5
+ timestamp: string
6
+ level: 'info' | 'warn' | 'error' | 'debug'
7
+ category?: string
8
+ event: string
9
+ subject?: string
10
+ detail?: string
11
+ }
12
+
13
+ export interface LogViewerProps {
14
+ entries: LogEntry[]
15
+ height?: number
16
+ defaultFollow?: boolean
17
+ ariaLabel?: string
18
+ className?: string
19
+ }
20
+
21
+ const SEV_DOT: Record<string, string> = {
22
+ error: '#F03A3A',
23
+ warn: '#F5A623',
24
+ info: '#006FFF',
25
+ debug: '#4A4B53',
26
+ }
27
+
28
+ export function LogViewer({
29
+ entries,
30
+ height = 360,
31
+ defaultFollow = true,
32
+ ariaLabel = 'Log entries',
33
+ className = '',
34
+ }: LogViewerProps) {
35
+ const [follow, setFollow] = useState(defaultFollow)
36
+ const bodyRef = useRef<HTMLDivElement>(null)
37
+
38
+ useEffect(() => {
39
+ if (follow && bodyRef.current) {
40
+ bodyRef.current.scrollTop = bodyRef.current.scrollHeight
41
+ }
42
+ }, [entries.length, follow])
43
+
44
+ return (
45
+ <div className={`lv ${className}`.trim()}>
46
+ <div className="lv__toolbar">
47
+ <span className="lv__count">{entries.length} entries</span>
48
+ <button
49
+ type="button"
50
+ className={`lv__follow${follow ? ' lv__follow--on' : ''}`}
51
+ onClick={() => setFollow((f) => !f)}
52
+ aria-pressed={follow}
53
+ >
54
+ Follow
55
+ </button>
56
+ </div>
57
+ <div
58
+ ref={bodyRef}
59
+ role="log"
60
+ aria-live="polite"
61
+ aria-label={ariaLabel}
62
+ className="lv__body"
63
+ style={{ height }}
64
+ >
65
+ <table className="lv__table">
66
+ <caption className="sr-only">{ariaLabel}</caption>
67
+ <thead>
68
+ <tr>
69
+ <th scope="col">Severity</th>
70
+ <th scope="col">Source</th>
71
+ <th scope="col">Time</th>
72
+ <th scope="col">Event</th>
73
+ <th scope="col">Subject</th>
74
+ <th scope="col">Detail</th>
75
+ </tr>
76
+ </thead>
77
+ <tbody>
78
+ {entries.map((e, i) => (
79
+ <tr key={e.id ?? i}>
80
+ <td>
81
+ <span className={`lv__sev lv__sev--${e.level}`}>
82
+ <span
83
+ className="lv__dot"
84
+ style={{ background: SEV_DOT[e.level] ?? SEV_DOT.info }}
85
+ aria-hidden="true"
86
+ />
87
+ {e.level === 'error' ? 'crit' : e.level}
88
+ </span>
89
+ </td>
90
+ <td className="lv__cat">{e.category ?? ''}</td>
91
+ <td className="lv__time">{e.timestamp}</td>
92
+ <td className="lv__event">{e.event}</td>
93
+ <td className="lv__subject">{e.subject ?? ''}</td>
94
+ <td className="lv__detail">{e.detail ?? ''}</td>
95
+ </tr>
96
+ ))}
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,110 @@
1
+ export interface LollipopItem {
2
+ label: string
3
+ value: number
4
+ color?: string
5
+ }
6
+
7
+ export interface LollipopChartProps {
8
+ items: LollipopItem[]
9
+ unit?: string
10
+ color?: string
11
+ ariaLabel?: string
12
+ }
13
+
14
+ const VW = 340
15
+ const LABEL_W = 110
16
+ const VAL_W = 36
17
+ const PAD_R = 4
18
+ const TRACK_W = VW - LABEL_W - VAL_W - PAD_R
19
+ const DOT_R = 5
20
+ const ROW_H = 26
21
+ const PAD_T = 6
22
+ const AXIS_H = 18
23
+ const TICKS = 4
24
+
25
+ export function LollipopChart({
26
+ items,
27
+ unit = '',
28
+ color = '#006FFF',
29
+ ariaLabel = 'Lollipop chart',
30
+ }: LollipopChartProps) {
31
+ const maxVal = Math.max(...items.map((it) => it.value), 1)
32
+ const svgH = PAD_T + items.length * ROW_H + AXIS_H
33
+
34
+ const tx = (v: number) => LABEL_W + (v / maxVal) * TRACK_W
35
+
36
+ const ticks = Array.from({ length: TICKS + 1 }, (_, i) => Math.round((i / TICKS) * maxVal))
37
+
38
+ return (
39
+ <div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
40
+ <svg
41
+ viewBox={`0 0 ${VW} ${svgH}`}
42
+ style={{ width: '100%', height: 'auto', display: 'block' }}
43
+ aria-hidden="true"
44
+ focusable="false"
45
+ >
46
+ {ticks.map((t, i) => (
47
+ <g key={i}>
48
+ <line
49
+ x1={tx(t)}
50
+ y1={PAD_T}
51
+ x2={tx(t)}
52
+ y2={PAD_T + items.length * ROW_H}
53
+ stroke="rgba(255,255,255,0.06)"
54
+ strokeWidth={1}
55
+ />
56
+ <text
57
+ x={tx(t)}
58
+ y={PAD_T + items.length * ROW_H + 13}
59
+ fill="#6E7079"
60
+ fontSize={9}
61
+ textAnchor="middle"
62
+ fontFamily="inherit"
63
+ >
64
+ {t}
65
+ {unit}
66
+ </text>
67
+ </g>
68
+ ))}
69
+
70
+ {items.map((item, i) => {
71
+ const cy = PAD_T + i * ROW_H + ROW_H / 2
72
+ const cx = tx(item.value)
73
+ const c = item.color ?? color
74
+
75
+ return (
76
+ <g key={i}>
77
+ <text
78
+ x={LABEL_W - 6}
79
+ y={cy + 4}
80
+ fill="#A4A7B5"
81
+ fontSize={10}
82
+ textAnchor="end"
83
+ fontFamily="inherit"
84
+ >
85
+ {item.label}
86
+ </text>
87
+
88
+ <line
89
+ x1={LABEL_W}
90
+ y1={cy}
91
+ x2={cx}
92
+ y2={cy}
93
+ stroke={c}
94
+ strokeWidth={1.5}
95
+ opacity={0.5}
96
+ />
97
+
98
+ <circle cx={cx} cy={cy} r={DOT_R} fill={c} />
99
+
100
+ <text x={cx + DOT_R + 4} y={cy + 4} fill="#A4A7B5" fontSize={9} fontFamily="inherit">
101
+ {item.value}
102
+ {unit}
103
+ </text>
104
+ </g>
105
+ )
106
+ })}
107
+ </svg>
108
+ </div>
109
+ )
110
+ }