@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.
- package/README.md +9 -0
- package/package.json +59 -0
- package/src/components/Accordion.tsx +65 -0
- package/src/components/ActionMenu.tsx +131 -0
- package/src/components/ActivityFeed.tsx +77 -0
- package/src/components/Alert.tsx +33 -0
- package/src/components/AnnotatedTimeSeries.tsx +148 -0
- package/src/components/AppShell.tsx +158 -0
- package/src/components/ArcDiagram.tsx +114 -0
- package/src/components/AreaChart.tsx +101 -0
- package/src/components/Avatar.tsx +37 -0
- package/src/components/AvatarGroup.tsx +36 -0
- package/src/components/Badge.tsx +36 -0
- package/src/components/Banner.tsx +49 -0
- package/src/components/BarChart.tsx +106 -0
- package/src/components/BeeswarmChart.tsx +142 -0
- package/src/components/BoxPlot.tsx +142 -0
- package/src/components/Breadcrumb.tsx +44 -0
- package/src/components/BubbleChart.tsx +115 -0
- package/src/components/BulletChart.tsx +121 -0
- package/src/components/BumpChart.tsx +127 -0
- package/src/components/Button.tsx +61 -0
- package/src/components/CIDRInput.tsx +191 -0
- package/src/components/CalendarHeatmap.tsx +117 -0
- package/src/components/Callout.tsx +82 -0
- package/src/components/CandlestickChart.tsx +113 -0
- package/src/components/Card.tsx +27 -0
- package/src/components/Carousel.tsx +78 -0
- package/src/components/Checkbox.tsx +42 -0
- package/src/components/ChordDiagram.tsx +133 -0
- package/src/components/CirclePacking.tsx +189 -0
- package/src/components/CodeBlock.tsx +27 -0
- package/src/components/ColorPicker.tsx +120 -0
- package/src/components/ColumnToggle.tsx +106 -0
- package/src/components/Combobox.tsx +211 -0
- package/src/components/CommandPalette.tsx +168 -0
- package/src/components/ConfirmDialog.tsx +114 -0
- package/src/components/ContextMenu.tsx +127 -0
- package/src/components/ContextualHelp.tsx +69 -0
- package/src/components/ContourPlot.tsx +255 -0
- package/src/components/CopyButton.tsx +47 -0
- package/src/components/CorrelationMatrix.tsx +107 -0
- package/src/components/CountUp.tsx +70 -0
- package/src/components/CumulativeDistribution.tsx +155 -0
- package/src/components/DatePicker.tsx +315 -0
- package/src/components/DateRangePicker.tsx +327 -0
- package/src/components/Dendrogram.tsx +150 -0
- package/src/components/Donut.tsx +101 -0
- package/src/components/DotPlot.tsx +195 -0
- package/src/components/Drawer.tsx +81 -0
- package/src/components/DualAxisChart.tsx +209 -0
- package/src/components/DumbbellChart.tsx +102 -0
- package/src/components/DurationInput.tsx +144 -0
- package/src/components/EmptyState.tsx +40 -0
- package/src/components/ErrorBandChart.tsx +124 -0
- package/src/components/ExpandableRow.tsx +58 -0
- package/src/components/FileUpload.tsx +129 -0
- package/src/components/FilterBuilder.tsx +217 -0
- package/src/components/FlameGraph.tsx +119 -0
- package/src/components/ForceGraph.tsx +149 -0
- package/src/components/FunnelChart.tsx +79 -0
- package/src/components/GanttChart.tsx +96 -0
- package/src/components/Gauge.tsx +71 -0
- package/src/components/GroupedList.tsx +125 -0
- package/src/components/HealthBar.tsx +25 -0
- package/src/components/HeatMap.tsx +106 -0
- package/src/components/HexbinChart.tsx +152 -0
- package/src/components/Histogram.tsx +121 -0
- package/src/components/HorizonChart.tsx +110 -0
- package/src/components/HoverCard.tsx +52 -0
- package/src/components/IPInput.tsx +147 -0
- package/src/components/IcicleChart.tsx +118 -0
- package/src/components/InlineEdit.tsx +119 -0
- package/src/components/Input.tsx +94 -0
- package/src/components/InputGroup.tsx +21 -0
- package/src/components/JsonViewer.tsx +116 -0
- package/src/components/KVTable.tsx +29 -0
- package/src/components/KanbanBoard.tsx +171 -0
- package/src/components/Kbd.tsx +22 -0
- package/src/components/LineChart.tsx +112 -0
- package/src/components/LogViewer.tsx +102 -0
- package/src/components/LollipopChart.tsx +110 -0
- package/src/components/MACInput.tsx +164 -0
- package/src/components/MarimekkoChart.tsx +143 -0
- package/src/components/MatrixChart.tsx +118 -0
- package/src/components/Menubar.tsx +194 -0
- package/src/components/MirroredBarChart.tsx +148 -0
- package/src/components/Modal.tsx +87 -0
- package/src/components/MultiSelect.tsx +232 -0
- package/src/components/NightingaleChart.tsx +101 -0
- package/src/components/NotificationPanel.tsx +187 -0
- package/src/components/NumberInput.tsx +129 -0
- package/src/components/OTPInput.tsx +135 -0
- package/src/components/Pagination.tsx +73 -0
- package/src/components/ParallelCoordinates.tsx +149 -0
- package/src/components/ParetoChart.tsx +159 -0
- package/src/components/PasswordInput.tsx +112 -0
- package/src/components/PieChart.tsx +109 -0
- package/src/components/Pill.tsx +26 -0
- package/src/components/PolarHeatmap.tsx +118 -0
- package/src/components/Popover.tsx +103 -0
- package/src/components/ProgressBar.tsx +33 -0
- package/src/components/PunchCard.tsx +93 -0
- package/src/components/QuadrantChart.tsx +256 -0
- package/src/components/RadarChart.tsx +119 -0
- package/src/components/RadialBarChart.tsx +103 -0
- package/src/components/RadioGroup.tsx +69 -0
- package/src/components/RangeSlider.tsx +116 -0
- package/src/components/RankedList.tsx +61 -0
- package/src/components/ResizablePanel.tsx +122 -0
- package/src/components/RidgelinePlot.tsx +155 -0
- package/src/components/SankeyDiagram.tsx +156 -0
- package/src/components/ScatterPlot.tsx +96 -0
- package/src/components/SegmentedControl.tsx +78 -0
- package/src/components/Select.tsx +158 -0
- package/src/components/SelectionToolbar.tsx +55 -0
- package/src/components/Signal.tsx +19 -0
- package/src/components/Skeleton.tsx +37 -0
- package/src/components/SkipLink.tsx +19 -0
- package/src/components/Slider.tsx +74 -0
- package/src/components/SlopeChart.tsx +205 -0
- package/src/components/SortableList.tsx +130 -0
- package/src/components/SortableTable.tsx +46 -0
- package/src/components/Sparkline.tsx +50 -0
- package/src/components/SparklineMatrix.tsx +117 -0
- package/src/components/Spinner.tsx +8 -0
- package/src/components/SplitButton.tsx +151 -0
- package/src/components/Spoiler.tsx +47 -0
- package/src/components/StackedBarChart.tsx +145 -0
- package/src/components/StackedProgress.tsx +47 -0
- package/src/components/StarRating.tsx +103 -0
- package/src/components/Stat.tsx +48 -0
- package/src/components/StatusIndicator.tsx +26 -0
- package/src/components/StepChart.tsx +130 -0
- package/src/components/Stepper.tsx +51 -0
- package/src/components/StreamGraph.tsx +97 -0
- package/src/components/StripeChart.tsx +76 -0
- package/src/components/SunburstChart.tsx +142 -0
- package/src/components/SwitchPortGrid.tsx +59 -0
- package/src/components/Tabs.tsx +91 -0
- package/src/components/Tag.tsx +37 -0
- package/src/components/TagInput.tsx +99 -0
- package/src/components/Textarea.tsx +9 -0
- package/src/components/ThresholdAreaChart.tsx +113 -0
- package/src/components/TimePicker.tsx +144 -0
- package/src/components/TimeRange.tsx +152 -0
- package/src/components/Timeline.tsx +31 -0
- package/src/components/Toast.tsx +72 -0
- package/src/components/Toggle.tsx +68 -0
- package/src/components/ToggleGroup.tsx +57 -0
- package/src/components/Tooltip.tsx +25 -0
- package/src/components/TransferList.tsx +227 -0
- package/src/components/TreeMap.tsx +126 -0
- package/src/components/TreeView.tsx +221 -0
- package/src/components/UptimeTimeline.tsx +138 -0
- package/src/components/VennDiagram.tsx +151 -0
- package/src/components/ViolinPlot.tsx +158 -0
- package/src/components/VirtualList.tsx +59 -0
- package/src/components/WaffleChart.tsx +79 -0
- package/src/components/WaterfallChart.tsx +164 -0
- package/src/components/WordCloud.tsx +107 -0
- package/src/icons.tsx +241 -0
- package/src/index.ts +160 -0
- package/src/styles.css +2 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface TreeMapNode {
|
|
4
|
+
label: string
|
|
5
|
+
value: number
|
|
6
|
+
color?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TreeMapProps {
|
|
10
|
+
nodes: TreeMapNode[]
|
|
11
|
+
height?: number
|
|
12
|
+
ariaLabel?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VW = 320
|
|
16
|
+
const PAD = 4
|
|
17
|
+
const GAP = 2
|
|
18
|
+
const PALETTE = ['#006FFF', '#00C875', '#F04949', '#A78BFA', '#F5A623', '#50B8E7']
|
|
19
|
+
|
|
20
|
+
interface LayoutRect {
|
|
21
|
+
x: number
|
|
22
|
+
y: number
|
|
23
|
+
w: number
|
|
24
|
+
h: number
|
|
25
|
+
label: string
|
|
26
|
+
value: number
|
|
27
|
+
color: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function layout(
|
|
31
|
+
nodes: { label: string; value: number; color: string }[],
|
|
32
|
+
x: number,
|
|
33
|
+
y: number,
|
|
34
|
+
w: number,
|
|
35
|
+
h: number,
|
|
36
|
+
): LayoutRect[] {
|
|
37
|
+
if (!nodes.length) return []
|
|
38
|
+
if (nodes.length === 1) return [{ x, y, w: Math.max(w, 0), h: Math.max(h, 0), ...nodes[0] }]
|
|
39
|
+
const total = nodes.reduce((s, n) => s + n.value, 0)
|
|
40
|
+
let acc = 0
|
|
41
|
+
let split = nodes.length - 1
|
|
42
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
43
|
+
acc += nodes[i].value
|
|
44
|
+
if (acc / total >= 0.5) {
|
|
45
|
+
split = i + 1
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const ratio = nodes.slice(0, split).reduce((s, n) => s + n.value, 0) / total
|
|
50
|
+
if (w > h) {
|
|
51
|
+
const w1 = Math.max((w - GAP) * ratio, 0)
|
|
52
|
+
return [
|
|
53
|
+
...layout(nodes.slice(0, split), x, y, w1, h),
|
|
54
|
+
...layout(nodes.slice(split), x + w1 + GAP, y, Math.max(w - w1 - GAP, 0), h),
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
const h1 = Math.max((h - GAP) * ratio, 0)
|
|
58
|
+
return [
|
|
59
|
+
...layout(nodes.slice(0, split), x, y, w, h1),
|
|
60
|
+
...layout(nodes.slice(split), x, y + h1 + GAP, w, Math.max(h - h1 - GAP, 0)),
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function TreeMap({ nodes, height = 160, ariaLabel = 'Treemap chart' }: TreeMapProps) {
|
|
65
|
+
const rects = useMemo(() => {
|
|
66
|
+
if (!nodes.length) return []
|
|
67
|
+
const sorted = [...nodes]
|
|
68
|
+
.sort((a, b) => b.value - a.value)
|
|
69
|
+
.map((n, i) => ({ ...n, color: n.color ?? PALETTE[i % PALETTE.length] }))
|
|
70
|
+
return layout(sorted, PAD, PAD, VW - PAD * 2, height - PAD * 2)
|
|
71
|
+
}, [nodes, height])
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
|
|
75
|
+
<svg
|
|
76
|
+
viewBox={`0 0 ${VW} ${height}`}
|
|
77
|
+
style={{ width: '100%', height: 'auto', display: 'block' }}
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
focusable="false"
|
|
80
|
+
>
|
|
81
|
+
{rects.map((r, i) => {
|
|
82
|
+
const showLabel = r.w >= 36 && r.h >= 22
|
|
83
|
+
const fs = Math.min(10, Math.max(7, Math.floor(r.w / 6)))
|
|
84
|
+
return (
|
|
85
|
+
<g key={i}>
|
|
86
|
+
<rect
|
|
87
|
+
x={r.x.toFixed(1)}
|
|
88
|
+
y={r.y.toFixed(1)}
|
|
89
|
+
width={r.w.toFixed(1)}
|
|
90
|
+
height={r.h.toFixed(1)}
|
|
91
|
+
fill={r.color}
|
|
92
|
+
fillOpacity={0.85}
|
|
93
|
+
rx={3}
|
|
94
|
+
/>
|
|
95
|
+
{showLabel && (
|
|
96
|
+
<>
|
|
97
|
+
<text
|
|
98
|
+
x={(r.x + r.w / 2).toFixed(1)}
|
|
99
|
+
y={(r.y + r.h / 2 - 4).toFixed(1)}
|
|
100
|
+
textAnchor="middle"
|
|
101
|
+
fill="#fff"
|
|
102
|
+
fontSize={fs}
|
|
103
|
+
fontFamily="inherit"
|
|
104
|
+
fontWeight={600}
|
|
105
|
+
>
|
|
106
|
+
{r.label}
|
|
107
|
+
</text>
|
|
108
|
+
<text
|
|
109
|
+
x={(r.x + r.w / 2).toFixed(1)}
|
|
110
|
+
y={(r.y + r.h / 2 + 9).toFixed(1)}
|
|
111
|
+
textAnchor="middle"
|
|
112
|
+
fill="rgba(255,255,255,0.65)"
|
|
113
|
+
fontSize={Math.max(fs - 1, 7)}
|
|
114
|
+
fontFamily="inherit"
|
|
115
|
+
>
|
|
116
|
+
{r.value}
|
|
117
|
+
</text>
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</g>
|
|
121
|
+
)
|
|
122
|
+
})}
|
|
123
|
+
</svg>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { useState, useRef, useCallback } from 'react'
|
|
2
|
+
import type { HTMLAttributes, KeyboardEvent, ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface TreeNode {
|
|
5
|
+
id: string
|
|
6
|
+
label: string
|
|
7
|
+
icon?: ReactNode
|
|
8
|
+
meta?: string
|
|
9
|
+
children?: TreeNode[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TreeViewProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect'> {
|
|
13
|
+
nodes: TreeNode[]
|
|
14
|
+
selected?: string
|
|
15
|
+
onSelect?: (id: string) => void
|
|
16
|
+
defaultExpanded?: string[]
|
|
17
|
+
label?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function visibleIds(nodes: TreeNode[], expanded: Set<string>): string[] {
|
|
21
|
+
const ids: string[] = []
|
|
22
|
+
function walk(list: TreeNode[]) {
|
|
23
|
+
for (const n of list) {
|
|
24
|
+
ids.push(n.id)
|
|
25
|
+
if (n.children?.length && expanded.has(n.id)) walk(n.children)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
walk(nodes)
|
|
29
|
+
return ids
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findNode(id: string, list: TreeNode[]): TreeNode | undefined {
|
|
33
|
+
for (const n of list) {
|
|
34
|
+
if (n.id === id) return n
|
|
35
|
+
if (n.children) {
|
|
36
|
+
const found = findNode(id, n.children)
|
|
37
|
+
if (found) return found
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TreeItem({
|
|
43
|
+
node,
|
|
44
|
+
selected,
|
|
45
|
+
focusedId,
|
|
46
|
+
expanded,
|
|
47
|
+
}: {
|
|
48
|
+
node: TreeNode
|
|
49
|
+
selected: string | undefined
|
|
50
|
+
focusedId: string
|
|
51
|
+
expanded: Set<string>
|
|
52
|
+
}) {
|
|
53
|
+
const hasChildren = !!node.children?.length
|
|
54
|
+
const isExpanded = expanded.has(node.id)
|
|
55
|
+
const isSelected = selected === node.id
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<li
|
|
59
|
+
role="treeitem"
|
|
60
|
+
aria-expanded={hasChildren ? isExpanded : undefined}
|
|
61
|
+
aria-selected={isSelected}
|
|
62
|
+
tabIndex={focusedId === node.id ? 0 : -1}
|
|
63
|
+
data-tree-id={node.id}
|
|
64
|
+
className="tree__item"
|
|
65
|
+
>
|
|
66
|
+
<div className={`tree__row${isSelected ? ' tree__row--selected' : ''}`}>
|
|
67
|
+
{hasChildren ? (
|
|
68
|
+
<svg
|
|
69
|
+
className={`tree__chevron${isExpanded ? ' tree__chevron--open' : ''}`}
|
|
70
|
+
viewBox="0 0 12 12"
|
|
71
|
+
fill="none"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
<path
|
|
75
|
+
d="M4 2l4 4-4 4"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
strokeWidth="1.5"
|
|
78
|
+
strokeLinecap="round"
|
|
79
|
+
strokeLinejoin="round"
|
|
80
|
+
/>
|
|
81
|
+
</svg>
|
|
82
|
+
) : (
|
|
83
|
+
<span className="tree__indent" aria-hidden="true" />
|
|
84
|
+
)}
|
|
85
|
+
{node.icon && (
|
|
86
|
+
<span className="tree__icon" aria-hidden="true">
|
|
87
|
+
{node.icon}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
<span className="tree__label">{node.label}</span>
|
|
91
|
+
{node.meta && <span className="tree__meta">{node.meta}</span>}
|
|
92
|
+
</div>
|
|
93
|
+
{hasChildren && isExpanded && (
|
|
94
|
+
<ul role="group" className="tree__group">
|
|
95
|
+
{node.children!.map((child) => (
|
|
96
|
+
<TreeItem
|
|
97
|
+
key={child.id}
|
|
98
|
+
node={child}
|
|
99
|
+
selected={selected}
|
|
100
|
+
focusedId={focusedId}
|
|
101
|
+
expanded={expanded}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</ul>
|
|
105
|
+
)}
|
|
106
|
+
</li>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function TreeView({
|
|
111
|
+
nodes,
|
|
112
|
+
selected,
|
|
113
|
+
onSelect,
|
|
114
|
+
defaultExpanded = [],
|
|
115
|
+
label,
|
|
116
|
+
className = '',
|
|
117
|
+
...rest
|
|
118
|
+
}: TreeViewProps) {
|
|
119
|
+
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(defaultExpanded))
|
|
120
|
+
const [focusedId, setFocusedId] = useState<string>(() => nodes[0]?.id ?? '')
|
|
121
|
+
const rootRef = useRef<HTMLUListElement>(null)
|
|
122
|
+
|
|
123
|
+
const toggle = useCallback((id: string) => {
|
|
124
|
+
setExpanded((prev) => {
|
|
125
|
+
const next = new Set(prev)
|
|
126
|
+
if (next.has(id)) next.delete(id)
|
|
127
|
+
else next.add(id)
|
|
128
|
+
return next
|
|
129
|
+
})
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
function moveFocus(id: string) {
|
|
133
|
+
setFocusedId(id)
|
|
134
|
+
requestAnimationFrame(() => {
|
|
135
|
+
rootRef.current?.querySelector<HTMLElement>(`[data-tree-id="${id}"]`)?.focus()
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleFocus(e: React.FocusEvent<HTMLUListElement>) {
|
|
140
|
+
const el = (e.target as HTMLElement).closest('[data-tree-id]')
|
|
141
|
+
if (el) setFocusedId(el.getAttribute('data-tree-id') ?? focusedId)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleClick(e: React.MouseEvent<HTMLUListElement>) {
|
|
145
|
+
const row = (e.target as HTMLElement).closest('.tree__row')
|
|
146
|
+
if (!row) return
|
|
147
|
+
const item = row.closest('[data-tree-id]')
|
|
148
|
+
if (!item) return
|
|
149
|
+
const id = item.getAttribute('data-tree-id')
|
|
150
|
+
if (!id) return
|
|
151
|
+
onSelect?.(id)
|
|
152
|
+
setFocusedId(id)
|
|
153
|
+
moveFocus(id)
|
|
154
|
+
const node = findNode(id, nodes)
|
|
155
|
+
if (node?.children?.length) toggle(id)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleKeyDown(e: KeyboardEvent<HTMLUListElement>) {
|
|
159
|
+
const el = (e.target as HTMLElement).closest('[data-tree-id]')
|
|
160
|
+
if (!el) return
|
|
161
|
+
const id = el.getAttribute('data-tree-id')
|
|
162
|
+
if (!id) return
|
|
163
|
+
|
|
164
|
+
const visible = visibleIds(nodes, expanded)
|
|
165
|
+
const idx = visible.indexOf(id)
|
|
166
|
+
const node = findNode(id, nodes)
|
|
167
|
+
const hasChildren = !!node?.children?.length
|
|
168
|
+
const isNodeExpanded = expanded.has(id)
|
|
169
|
+
|
|
170
|
+
if (e.key === 'ArrowDown') {
|
|
171
|
+
e.preventDefault()
|
|
172
|
+
const next = visible[idx + 1]
|
|
173
|
+
if (next) moveFocus(next)
|
|
174
|
+
} else if (e.key === 'ArrowUp') {
|
|
175
|
+
e.preventDefault()
|
|
176
|
+
const prev = visible[idx - 1]
|
|
177
|
+
if (prev) moveFocus(prev)
|
|
178
|
+
} else if (e.key === 'ArrowRight') {
|
|
179
|
+
e.preventDefault()
|
|
180
|
+
if (hasChildren && !isNodeExpanded) toggle(id)
|
|
181
|
+
else if (hasChildren && isNodeExpanded) moveFocus(node!.children![0]!.id)
|
|
182
|
+
} else if (e.key === 'ArrowLeft') {
|
|
183
|
+
e.preventDefault()
|
|
184
|
+
if (hasChildren && isNodeExpanded) toggle(id)
|
|
185
|
+
} else if (e.key === 'Home') {
|
|
186
|
+
e.preventDefault()
|
|
187
|
+
if (visible[0]) moveFocus(visible[0])
|
|
188
|
+
} else if (e.key === 'End') {
|
|
189
|
+
e.preventDefault()
|
|
190
|
+
const last = visible[visible.length - 1]
|
|
191
|
+
if (last) moveFocus(last)
|
|
192
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
193
|
+
e.preventDefault()
|
|
194
|
+
onSelect?.(id)
|
|
195
|
+
setFocusedId(id)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<ul
|
|
201
|
+
ref={rootRef}
|
|
202
|
+
role="tree"
|
|
203
|
+
aria-label={label}
|
|
204
|
+
className={`tree ${className}`.trim()}
|
|
205
|
+
onFocus={handleFocus}
|
|
206
|
+
onClick={handleClick}
|
|
207
|
+
onKeyDown={handleKeyDown}
|
|
208
|
+
{...rest}
|
|
209
|
+
>
|
|
210
|
+
{nodes.map((node) => (
|
|
211
|
+
<TreeItem
|
|
212
|
+
key={node.id}
|
|
213
|
+
node={node}
|
|
214
|
+
selected={selected}
|
|
215
|
+
focusedId={focusedId}
|
|
216
|
+
expanded={expanded}
|
|
217
|
+
/>
|
|
218
|
+
))}
|
|
219
|
+
</ul>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export type UptimeStatus = 'up' | 'degraded' | 'down'
|
|
2
|
+
|
|
3
|
+
export interface UptimeSegment {
|
|
4
|
+
from: number
|
|
5
|
+
to: number
|
|
6
|
+
status: UptimeStatus
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UptimeSeries {
|
|
10
|
+
label: string
|
|
11
|
+
segments: UptimeSegment[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UptimeTimelineProps {
|
|
15
|
+
series: UptimeSeries[]
|
|
16
|
+
xLabels?: string[]
|
|
17
|
+
ariaLabel?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VW = 400
|
|
21
|
+
const LABEL_W = 88
|
|
22
|
+
const PAD_R = 10
|
|
23
|
+
const PAD_T = 6
|
|
24
|
+
const ROW_H = 14
|
|
25
|
+
const ROW_GAP = 8
|
|
26
|
+
const XLBL_H = 16
|
|
27
|
+
const LEG_H = 16
|
|
28
|
+
const PAD_B = XLBL_H + LEG_H + 4
|
|
29
|
+
|
|
30
|
+
const STATUS_COLOR: Record<UptimeStatus, string> = {
|
|
31
|
+
up: '#00C875',
|
|
32
|
+
degraded: '#F5A623',
|
|
33
|
+
down: '#F03E3E',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const LEGEND: { status: UptimeStatus; label: string }[] = [
|
|
37
|
+
{ status: 'up', label: 'Up' },
|
|
38
|
+
{ status: 'degraded', label: 'Degraded' },
|
|
39
|
+
{ status: 'down', label: 'Down' },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
export function UptimeTimeline({
|
|
43
|
+
series,
|
|
44
|
+
xLabels,
|
|
45
|
+
ariaLabel = 'Uptime timeline',
|
|
46
|
+
}: UptimeTimelineProps) {
|
|
47
|
+
const trackW = VW - LABEL_W - PAD_R
|
|
48
|
+
const rowsH = series.length * (ROW_H + ROW_GAP) - ROW_GAP
|
|
49
|
+
const totalH = PAD_T + rowsH + PAD_B
|
|
50
|
+
const xlblY = PAD_T + rowsH + XLBL_H
|
|
51
|
+
const legY = xlblY + LEG_H
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
|
|
55
|
+
<svg
|
|
56
|
+
viewBox={`0 0 ${VW} ${totalH}`}
|
|
57
|
+
style={{ width: '100%', height: 'auto', display: 'block' }}
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
focusable="false"
|
|
60
|
+
>
|
|
61
|
+
{series.map((s, si) => {
|
|
62
|
+
const y = PAD_T + si * (ROW_H + ROW_GAP)
|
|
63
|
+
return (
|
|
64
|
+
<g key={s.label}>
|
|
65
|
+
<text
|
|
66
|
+
x={LABEL_W - 8}
|
|
67
|
+
y={y + ROW_H / 2 + 4}
|
|
68
|
+
textAnchor="end"
|
|
69
|
+
fill="#A4A7B5"
|
|
70
|
+
fontSize={11}
|
|
71
|
+
fontFamily="inherit"
|
|
72
|
+
>
|
|
73
|
+
{s.label}
|
|
74
|
+
</text>
|
|
75
|
+
<rect
|
|
76
|
+
x={LABEL_W}
|
|
77
|
+
y={y}
|
|
78
|
+
width={trackW}
|
|
79
|
+
height={ROW_H}
|
|
80
|
+
fill="rgba(255,255,255,0.04)"
|
|
81
|
+
rx={3}
|
|
82
|
+
/>
|
|
83
|
+
{s.segments.map((seg, i) => (
|
|
84
|
+
<rect
|
|
85
|
+
key={i}
|
|
86
|
+
x={LABEL_W + seg.from * trackW}
|
|
87
|
+
y={y}
|
|
88
|
+
width={(seg.to - seg.from) * trackW}
|
|
89
|
+
height={ROW_H}
|
|
90
|
+
fill={STATUS_COLOR[seg.status]}
|
|
91
|
+
rx={2}
|
|
92
|
+
fillOpacity={0.85}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</g>
|
|
96
|
+
)
|
|
97
|
+
})}
|
|
98
|
+
|
|
99
|
+
{xLabels?.map((lbl, i) => (
|
|
100
|
+
<text
|
|
101
|
+
key={lbl}
|
|
102
|
+
x={LABEL_W + (xLabels.length > 1 ? i / (xLabels.length - 1) : 0) * trackW}
|
|
103
|
+
y={xlblY}
|
|
104
|
+
textAnchor={i === 0 ? 'start' : i === xLabels.length - 1 ? 'end' : 'middle'}
|
|
105
|
+
fill="#6E7079"
|
|
106
|
+
fontSize={10}
|
|
107
|
+
fontFamily="inherit"
|
|
108
|
+
>
|
|
109
|
+
{lbl}
|
|
110
|
+
</text>
|
|
111
|
+
))}
|
|
112
|
+
|
|
113
|
+
{LEGEND.map(({ status, label }, i) => (
|
|
114
|
+
<g key={status}>
|
|
115
|
+
<rect
|
|
116
|
+
x={LABEL_W + i * 80}
|
|
117
|
+
y={legY - 8}
|
|
118
|
+
width={12}
|
|
119
|
+
height={8}
|
|
120
|
+
fill={STATUS_COLOR[status]}
|
|
121
|
+
rx={2}
|
|
122
|
+
fillOpacity={0.85}
|
|
123
|
+
/>
|
|
124
|
+
<text
|
|
125
|
+
x={LABEL_W + i * 80 + 16}
|
|
126
|
+
y={legY}
|
|
127
|
+
fill="#6E7079"
|
|
128
|
+
fontSize={10}
|
|
129
|
+
fontFamily="inherit"
|
|
130
|
+
>
|
|
131
|
+
{label}
|
|
132
|
+
</text>
|
|
133
|
+
</g>
|
|
134
|
+
))}
|
|
135
|
+
</svg>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export interface VennSet {
|
|
2
|
+
id: string
|
|
3
|
+
label: string
|
|
4
|
+
value: number
|
|
5
|
+
color?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface VennIntersection {
|
|
9
|
+
sets: string[]
|
|
10
|
+
value: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface VennDiagramProps {
|
|
14
|
+
sets: VennSet[]
|
|
15
|
+
intersections?: VennIntersection[]
|
|
16
|
+
height?: number
|
|
17
|
+
ariaLabel?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PALETTE = ['#006FFF', '#00C875', '#A78BFA']
|
|
21
|
+
const VW = 340
|
|
22
|
+
|
|
23
|
+
function fmt(n: number): string {
|
|
24
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function VennDiagram({
|
|
28
|
+
sets,
|
|
29
|
+
intersections = [],
|
|
30
|
+
height = 240,
|
|
31
|
+
ariaLabel = 'Venn diagram',
|
|
32
|
+
}: VennDiagramProps) {
|
|
33
|
+
if (sets.length < 2 || sets.length > 3) return null
|
|
34
|
+
|
|
35
|
+
const is3 = sets.length === 3
|
|
36
|
+
const cx = VW / 2
|
|
37
|
+
const cy = height / 2
|
|
38
|
+
|
|
39
|
+
const circles = is3
|
|
40
|
+
? [
|
|
41
|
+
{ x: cx, y: cy - 40, r: 75 },
|
|
42
|
+
{ x: cx - 65, y: cy + 50, r: 75 },
|
|
43
|
+
{ x: cx + 65, y: cy + 50, r: 75 },
|
|
44
|
+
]
|
|
45
|
+
: [
|
|
46
|
+
{ x: cx - 58, y: cy, r: 85 },
|
|
47
|
+
{ x: cx + 58, y: cy, r: 85 },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const exclusiveValues = sets.map((s) =>
|
|
51
|
+
Math.max(
|
|
52
|
+
0,
|
|
53
|
+
s.value -
|
|
54
|
+
intersections.filter((i) => i.sets.includes(s.id)).reduce((sum, i) => sum + i.value, 0),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
function getPt(inter: VennIntersection): { x: number; y: number } | null {
|
|
59
|
+
const idxById: Record<string, number> = {}
|
|
60
|
+
sets.forEach((s, i) => {
|
|
61
|
+
idxById[s.id] = i
|
|
62
|
+
})
|
|
63
|
+
const idxs = inter.sets
|
|
64
|
+
.map((id) => idxById[id])
|
|
65
|
+
.filter((v) => v !== undefined)
|
|
66
|
+
.sort((a, b) => a - b)
|
|
67
|
+
if (!is3) return { x: cx, y: cy }
|
|
68
|
+
if (idxs.length >= 3) return { x: cx, y: cy + 18 }
|
|
69
|
+
if (idxs[0] === 0 && idxs[1] === 1) return { x: cx - 33, y: cy + 4 }
|
|
70
|
+
if (idxs[0] === 0 && idxs[1] === 2) return { x: cx + 33, y: cy + 4 }
|
|
71
|
+
if (idxs[0] === 1 && idxs[1] === 2) return { x: cx, y: cy + 50 }
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div role="img" aria-label={ariaLabel} style={{ width: '100%' }}>
|
|
77
|
+
<svg
|
|
78
|
+
viewBox={`0 0 ${VW} ${height}`}
|
|
79
|
+
style={{ width: '100%', height: 'auto', display: 'block' }}
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
focusable="false"
|
|
82
|
+
>
|
|
83
|
+
{sets.map((s, i) => {
|
|
84
|
+
const c = circles[i]
|
|
85
|
+
const color = s.color ?? PALETTE[i % PALETTE.length]
|
|
86
|
+
return (
|
|
87
|
+
<circle
|
|
88
|
+
key={i}
|
|
89
|
+
cx={c.x}
|
|
90
|
+
cy={c.y}
|
|
91
|
+
r={c.r}
|
|
92
|
+
fill={color}
|
|
93
|
+
fillOpacity={0.15}
|
|
94
|
+
stroke={color}
|
|
95
|
+
strokeOpacity={0.55}
|
|
96
|
+
strokeWidth={1.5}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
})}
|
|
100
|
+
|
|
101
|
+
{sets.map((s, i) => {
|
|
102
|
+
const c = circles[i]
|
|
103
|
+
const color = s.color ?? PALETTE[i % PALETTE.length]
|
|
104
|
+
return (
|
|
105
|
+
<g key={i}>
|
|
106
|
+
<text
|
|
107
|
+
x={c.x}
|
|
108
|
+
y={c.y - 8}
|
|
109
|
+
fill={color}
|
|
110
|
+
fontSize={11}
|
|
111
|
+
textAnchor="middle"
|
|
112
|
+
fontFamily="inherit"
|
|
113
|
+
fontWeight={600}
|
|
114
|
+
>
|
|
115
|
+
{s.label}
|
|
116
|
+
</text>
|
|
117
|
+
<text
|
|
118
|
+
x={c.x}
|
|
119
|
+
y={c.y + 8}
|
|
120
|
+
fill="#A4A7B5"
|
|
121
|
+
fontSize={10}
|
|
122
|
+
textAnchor="middle"
|
|
123
|
+
fontFamily="inherit"
|
|
124
|
+
>
|
|
125
|
+
{fmt(exclusiveValues[i])}
|
|
126
|
+
</text>
|
|
127
|
+
</g>
|
|
128
|
+
)
|
|
129
|
+
})}
|
|
130
|
+
|
|
131
|
+
{intersections.map((inter, i) => {
|
|
132
|
+
const pt = getPt(inter)
|
|
133
|
+
if (!pt) return null
|
|
134
|
+
return (
|
|
135
|
+
<text
|
|
136
|
+
key={i}
|
|
137
|
+
x={pt.x}
|
|
138
|
+
y={pt.y}
|
|
139
|
+
fill="rgba(255,255,255,0.65)"
|
|
140
|
+
fontSize={10}
|
|
141
|
+
textAnchor="middle"
|
|
142
|
+
fontFamily="inherit"
|
|
143
|
+
>
|
|
144
|
+
{fmt(inter.value)}
|
|
145
|
+
</text>
|
|
146
|
+
)
|
|
147
|
+
})}
|
|
148
|
+
</svg>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|