@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,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">"{v}"</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">"{k}"</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">"{k}"</span>
|
|
66
|
+
<span className="jv-colon">: </span>
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
<span className="jv-bracket">{ob}</span>
|
|
70
|
+
{!open && (
|
|
71
|
+
<>
|
|
72
|
+
<span className="jv-count"> {count} </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
|
+
}
|