@w5-ui/svelte 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 +8 -0
- package/package.json +57 -0
- package/src/lib/components/Accordion.svelte +3 -0
- package/src/lib/components/AccordionItem.svelte +40 -0
- package/src/lib/components/ActionMenu.svelte +96 -0
- package/src/lib/components/ActivityFeed.svelte +60 -0
- package/src/lib/components/Alert.svelte +19 -0
- package/src/lib/components/AnnotatedTimeSeries.svelte +90 -0
- package/src/lib/components/ArcDiagram.svelte +74 -0
- package/src/lib/components/AreaChart.svelte +64 -0
- package/src/lib/components/Avatar.svelte +30 -0
- package/src/lib/components/AvatarGroup.svelte +35 -0
- package/src/lib/components/Badge.svelte +25 -0
- package/src/lib/components/Banner.svelte +37 -0
- package/src/lib/components/BarChart.svelte +52 -0
- package/src/lib/components/BeeswarmChart.svelte +88 -0
- package/src/lib/components/BoxPlot.svelte +66 -0
- package/src/lib/components/Breadcrumb.svelte +31 -0
- package/src/lib/components/BubbleChart.svelte +69 -0
- package/src/lib/components/BulletChart.svelte +70 -0
- package/src/lib/components/BumpChart.svelte +109 -0
- package/src/lib/components/Button.svelte +25 -0
- package/src/lib/components/CIDRInput.svelte +142 -0
- package/src/lib/components/CalendarHeatmap.svelte +86 -0
- package/src/lib/components/Callout.svelte +38 -0
- package/src/lib/components/CandlestickChart.svelte +66 -0
- package/src/lib/components/Card.svelte +11 -0
- package/src/lib/components/Carousel.svelte +64 -0
- package/src/lib/components/Checkbox.svelte +45 -0
- package/src/lib/components/ChordDiagram.svelte +90 -0
- package/src/lib/components/CirclePacking.svelte +119 -0
- package/src/lib/components/CodeBlock.svelte +49 -0
- package/src/lib/components/ColorPicker.svelte +91 -0
- package/src/lib/components/ColumnToggle.svelte +96 -0
- package/src/lib/components/Combobox.svelte +138 -0
- package/src/lib/components/CommandPalette.svelte +152 -0
- package/src/lib/components/ConfirmDialog.svelte +84 -0
- package/src/lib/components/ContextMenu.svelte +107 -0
- package/src/lib/components/ContextualHelp.svelte +73 -0
- package/src/lib/components/ContourPlot.svelte +164 -0
- package/src/lib/components/CopyButton.svelte +33 -0
- package/src/lib/components/CorrelationMatrix.svelte +90 -0
- package/src/lib/components/CountUp.svelte +49 -0
- package/src/lib/components/CumulativeDistribution.svelte +67 -0
- package/src/lib/components/DatePicker.svelte +201 -0
- package/src/lib/components/DateRangePicker.svelte +249 -0
- package/src/lib/components/Dendrogram.svelte +128 -0
- package/src/lib/components/Donut.svelte +49 -0
- package/src/lib/components/DotPlot.svelte +121 -0
- package/src/lib/components/Drawer.svelte +60 -0
- package/src/lib/components/DualAxisChart.svelte +80 -0
- package/src/lib/components/DumbbellChart.svelte +56 -0
- package/src/lib/components/DurationInput.svelte +118 -0
- package/src/lib/components/EmptyState.svelte +21 -0
- package/src/lib/components/ErrorBandChart.svelte +69 -0
- package/src/lib/components/ExpandableRow.svelte +42 -0
- package/src/lib/components/Field.svelte +40 -0
- package/src/lib/components/FileUpload.svelte +96 -0
- package/src/lib/components/FilterBuilder.svelte +160 -0
- package/src/lib/components/FlameGraph.svelte +88 -0
- package/src/lib/components/ForceGraph.svelte +104 -0
- package/src/lib/components/FunnelChart.svelte +50 -0
- package/src/lib/components/GanttChart.svelte +49 -0
- package/src/lib/components/Gauge.svelte +53 -0
- package/src/lib/components/GroupedList.svelte +87 -0
- package/src/lib/components/HealthBar.svelte +19 -0
- package/src/lib/components/HeatMap.svelte +64 -0
- package/src/lib/components/HexbinChart.svelte +100 -0
- package/src/lib/components/Histogram.svelte +93 -0
- package/src/lib/components/HorizonChart.svelte +71 -0
- package/src/lib/components/HoverCard.svelte +45 -0
- package/src/lib/components/IPInput.svelte +103 -0
- package/src/lib/components/IcicleChart.svelte +87 -0
- package/src/lib/components/IconButton.svelte +19 -0
- package/src/lib/components/InlineEdit.svelte +72 -0
- package/src/lib/components/Input.svelte +18 -0
- package/src/lib/components/InputGroup.svelte +14 -0
- package/src/lib/components/JsonViewer.svelte +15 -0
- package/src/lib/components/JsonViewerNode.svelte +59 -0
- package/src/lib/components/KVTable.svelte +18 -0
- package/src/lib/components/KanbanBoard.svelte +141 -0
- package/src/lib/components/Kbd.svelte +16 -0
- package/src/lib/components/LineChart.svelte +67 -0
- package/src/lib/components/LogViewer.svelte +84 -0
- package/src/lib/components/LollipopChart.svelte +79 -0
- package/src/lib/components/MACInput.svelte +98 -0
- package/src/lib/components/MarimekkoChart.svelte +108 -0
- package/src/lib/components/MatrixChart.svelte +55 -0
- package/src/lib/components/Menubar.svelte +131 -0
- package/src/lib/components/MirroredBarChart.svelte +120 -0
- package/src/lib/components/Modal.svelte +90 -0
- package/src/lib/components/MultiSelect.svelte +165 -0
- package/src/lib/components/NightingaleChart.svelte +63 -0
- package/src/lib/components/NotificationPanel.svelte +170 -0
- package/src/lib/components/NumberInput.svelte +93 -0
- package/src/lib/components/OTPInput.svelte +94 -0
- package/src/lib/components/Pagination.svelte +58 -0
- package/src/lib/components/ParallelCoordinates.svelte +98 -0
- package/src/lib/components/ParetoChart.svelte +88 -0
- package/src/lib/components/PasswordInput.svelte +62 -0
- package/src/lib/components/PieChart.svelte +91 -0
- package/src/lib/components/Pill.svelte +12 -0
- package/src/lib/components/PolarHeatmap.svelte +85 -0
- package/src/lib/components/Popover.svelte +89 -0
- package/src/lib/components/ProgressBar.svelte +29 -0
- package/src/lib/components/PunchCard.svelte +73 -0
- package/src/lib/components/QuadrantChart.svelte +122 -0
- package/src/lib/components/RadarChart.svelte +71 -0
- package/src/lib/components/RadialBarChart.svelte +76 -0
- package/src/lib/components/RadioGroup.svelte +48 -0
- package/src/lib/components/RangeSlider.svelte +68 -0
- package/src/lib/components/RankedList.svelte +49 -0
- package/src/lib/components/ResizablePanel.svelte +77 -0
- package/src/lib/components/RidgelinePlot.svelte +103 -0
- package/src/lib/components/RowToggle.svelte +19 -0
- package/src/lib/components/SankeyDiagram.svelte +100 -0
- package/src/lib/components/ScatterPlot.svelte +54 -0
- package/src/lib/components/SearchBox.svelte +11 -0
- package/src/lib/components/SegmentedControl.svelte +51 -0
- package/src/lib/components/Select.svelte +110 -0
- package/src/lib/components/SelectionToolbar.svelte +42 -0
- package/src/lib/components/Sidebar.svelte +44 -0
- package/src/lib/components/Signal.svelte +12 -0
- package/src/lib/components/Skeleton.svelte +23 -0
- package/src/lib/components/SkipLink.svelte +9 -0
- package/src/lib/components/Slider.svelte +48 -0
- package/src/lib/components/SlopeChart.svelte +91 -0
- package/src/lib/components/SortHeader.svelte +19 -0
- package/src/lib/components/SortableList.svelte +87 -0
- package/src/lib/components/Sparkline.svelte +32 -0
- package/src/lib/components/SparklineMatrix.svelte +84 -0
- package/src/lib/components/Spinner.svelte +6 -0
- package/src/lib/components/SplitButton.svelte +102 -0
- package/src/lib/components/Spoiler.svelte +41 -0
- package/src/lib/components/StackedBarChart.svelte +83 -0
- package/src/lib/components/StackedProgress.svelte +32 -0
- package/src/lib/components/StarRating.svelte +64 -0
- package/src/lib/components/Stat.svelte +24 -0
- package/src/lib/components/StatusIndicator.svelte +10 -0
- package/src/lib/components/StepChart.svelte +74 -0
- package/src/lib/components/Stepper.svelte +40 -0
- package/src/lib/components/StreamGraph.svelte +93 -0
- package/src/lib/components/StripeChart.svelte +52 -0
- package/src/lib/components/SunburstChart.svelte +80 -0
- package/src/lib/components/SwitchPortGrid.svelte +45 -0
- package/src/lib/components/TabPanel.svelte +17 -0
- package/src/lib/components/Tabs.svelte +45 -0
- package/src/lib/components/Tag.svelte +19 -0
- package/src/lib/components/TagInput.svelte +92 -0
- package/src/lib/components/Textarea.svelte +31 -0
- package/src/lib/components/ThresholdAreaChart.svelte +72 -0
- package/src/lib/components/TimePicker.svelte +116 -0
- package/src/lib/components/TimeRange.svelte +104 -0
- package/src/lib/components/Timeline.svelte +32 -0
- package/src/lib/components/Toast.svelte +52 -0
- package/src/lib/components/Toggle.svelte +23 -0
- package/src/lib/components/ToggleGroup.svelte +32 -0
- package/src/lib/components/Tooltip.svelte +16 -0
- package/src/lib/components/Topbar.svelte +63 -0
- package/src/lib/components/TransferList.svelte +184 -0
- package/src/lib/components/TreeItem.svelte +61 -0
- package/src/lib/components/TreeMap.svelte +90 -0
- package/src/lib/components/TreeView.svelte +130 -0
- package/src/lib/components/UptimeTimeline.svelte +86 -0
- package/src/lib/components/VennDiagram.svelte +91 -0
- package/src/lib/components/ViolinPlot.svelte +114 -0
- package/src/lib/components/VirtualList.svelte +38 -0
- package/src/lib/components/WaffleChart.svelte +68 -0
- package/src/lib/components/WaterfallChart.svelte +96 -0
- package/src/lib/components/WordCloud.svelte +103 -0
- package/src/lib/icons/AlarmIcon.svelte +5 -0
- package/src/lib/icons/BellIcon.svelte +6 -0
- package/src/lib/icons/CaretIcon.svelte +8 -0
- package/src/lib/icons/ClientsIcon.svelte +5 -0
- package/src/lib/icons/CloseIcon.svelte +6 -0
- package/src/lib/icons/DashboardIcon.svelte +7 -0
- package/src/lib/icons/DevicesIcon.svelte +5 -0
- package/src/lib/icons/DownloadIcon.svelte +6 -0
- package/src/lib/icons/HelpIcon.svelte +7 -0
- package/src/lib/icons/InfraIcon.svelte +7 -0
- package/src/lib/icons/IntegrationsIcon.svelte +5 -0
- package/src/lib/icons/LogsIcon.svelte +6 -0
- package/src/lib/icons/PlusIcon.svelte +6 -0
- package/src/lib/icons/PortsIcon.svelte +6 -0
- package/src/lib/icons/SearchIcon.svelte +10 -0
- package/src/lib/icons/SecurityIcon.svelte +4 -0
- package/src/lib/icons/SettingsIcon.svelte +5 -0
- package/src/lib/icons/TopologyIcon.svelte +7 -0
- package/src/lib/icons/UpdatesIcon.svelte +6 -0
- package/src/lib/icons/VpnIcon.svelte +5 -0
- package/src/lib/icons/WifiIcon.svelte +5 -0
- package/src/lib/icons/WirelessIcon.svelte +4 -0
- package/src/lib/icons.ts +22 -0
- package/src/lib/index.ts +275 -0
- package/src/lib/styles.css +2 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface SunburstNode {
|
|
3
|
+
label: string;
|
|
4
|
+
value?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
children?: SunburstNode[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export let root: SunburstNode;
|
|
10
|
+
export let ariaLabel: string = 'Sunburst chart';
|
|
11
|
+
|
|
12
|
+
const PALETTE = ['#006FFF', '#00C875', '#A78BFA', '#F5A623', '#50B8E7', '#F04949'];
|
|
13
|
+
const CX = 160, CY = 160, R_HOLE = 48, R_MID = 97, R_OUTER = 142, GAP = 0.01;
|
|
14
|
+
|
|
15
|
+
function arcPath(r1: number, r2: number, a1: number, a2: number): string {
|
|
16
|
+
const lg = a2 - a1 > Math.PI ? 1 : 0;
|
|
17
|
+
const p = (r: number, a: number) => `${(CX + r * Math.cos(a)).toFixed(1)},${(CY + r * Math.sin(a)).toFixed(1)}`;
|
|
18
|
+
return `M${p(r2, a1)} A${r2},${r2} 0 ${lg},1 ${p(r2, a2)} L${p(r1, a2)} A${r1},${r1} 0 ${lg},0 ${p(r1, a1)} Z`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type PathData = { d: string; fill: string };
|
|
22
|
+
type LabelData = { x: number; y: number; anchor: string; text: string };
|
|
23
|
+
|
|
24
|
+
$: ({ paths, labels, total } = (() => {
|
|
25
|
+
const items = root.children ?? [];
|
|
26
|
+
const total = items.reduce((s, c) => s + (c.value ?? c.children?.reduce((cs, x) => cs + (x.value ?? 0), 0) ?? 0), 0);
|
|
27
|
+
if (!total) return { paths: [] as PathData[], labels: [] as LabelData[], total: 0 };
|
|
28
|
+
|
|
29
|
+
const paths: PathData[] = [];
|
|
30
|
+
const labels: LabelData[] = [];
|
|
31
|
+
let a = -Math.PI / 2;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < items.length; i++) {
|
|
34
|
+
const c = items[i];
|
|
35
|
+
const fill = c.color ?? PALETTE[i % PALETTE.length];
|
|
36
|
+
const cv = c.value ?? c.children?.reduce((s, x) => s + (x.value ?? 0), 0) ?? 0;
|
|
37
|
+
const span = (cv / total) * 2 * Math.PI;
|
|
38
|
+
const a2 = a + span;
|
|
39
|
+
const midA = (a + a2) / 2;
|
|
40
|
+
|
|
41
|
+
paths.push({ d: arcPath(R_MID + 2, R_OUTER, a + GAP, a2 - GAP), fill });
|
|
42
|
+
|
|
43
|
+
if (span > 0.22) {
|
|
44
|
+
const r = (R_MID + 2 + R_OUTER) / 2;
|
|
45
|
+
const anchor = Math.cos(midA) > 0.15 ? 'start' : Math.cos(midA) < -0.15 ? 'end' : 'middle';
|
|
46
|
+
labels.push({ x: CX + r * Math.cos(midA), y: CY + r * Math.sin(midA) + 4, anchor, text: c.label });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (c.children?.length) {
|
|
50
|
+
let ca = a;
|
|
51
|
+
for (const child of c.children) {
|
|
52
|
+
const cs = ((child.value ?? 0) / cv) * span;
|
|
53
|
+
if (cs > GAP * 2) paths.push({ d: arcPath(R_HOLE, R_MID - 2, ca + GAP, ca + cs - GAP), fill: fill + '99' });
|
|
54
|
+
ca += cs;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
paths.push({ d: arcPath(R_HOLE, R_MID - 2, a + GAP, a2 - GAP), fill: fill + '99' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
a = a2;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { paths, labels, total };
|
|
64
|
+
})());
|
|
65
|
+
|
|
66
|
+
const fmt = (n: number) => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<div role="img" aria-label={ariaLabel} style="width:100%;">
|
|
70
|
+
<svg viewBox="0 0 320 320" style="width:100%;height:auto;display:block;" aria-hidden="true" focusable="false">
|
|
71
|
+
{#each paths as p, i (i)}
|
|
72
|
+
<path d={p.d} fill={p.fill} />
|
|
73
|
+
{/each}
|
|
74
|
+
{#each labels as l, i (i)}
|
|
75
|
+
<text x={l.x.toFixed(1)} y={l.y.toFixed(1)} text-anchor={l.anchor} fill="#fff" font-size="10" font-weight="600" font-family="inherit">{l.text}</text>
|
|
76
|
+
{/each}
|
|
77
|
+
<text x={CX} y={CY - 8} text-anchor="middle" fill="#fff" font-size="22" font-weight="700" font-family="inherit">{fmt(total)}</text>
|
|
78
|
+
<text x={CX} y={CY + 12} text-anchor="middle" fill="#6E7079" font-size="10" font-family="inherit">{root.label}</text>
|
|
79
|
+
</svg>
|
|
80
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
export type SwitchPortStatus = 'up' | 'poe' | 'down';
|
|
3
|
+
export interface SwitchPort {
|
|
4
|
+
status: SwitchPortStatus;
|
|
5
|
+
speed?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
const STATUS_LABEL: Record<SwitchPortStatus, string> = {
|
|
9
|
+
up: 'Connected',
|
|
10
|
+
poe: 'PoE Active',
|
|
11
|
+
down: 'Down',
|
|
12
|
+
};
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
import { createEventDispatcher } from 'svelte';
|
|
17
|
+
export let ports: SwitchPort[] = [];
|
|
18
|
+
export let columns: number = 12;
|
|
19
|
+
export let ariaLabel: string = 'Switch port panel';
|
|
20
|
+
const dispatch = createEventDispatcher<{ portclick: number }>();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<ul class="spg" role="list" aria-label={ariaLabel} style="grid-template-columns: repeat({columns}, 1fr);">
|
|
24
|
+
{#each ports as port, i}
|
|
25
|
+
{@const n = i + 1}
|
|
26
|
+
{@const desc = `Port ${n}: ${STATUS_LABEL[port.status]}${port.speed ? ` · ${port.speed}` : ''}${port.label ? ` · ${port.label}` : ''}`}
|
|
27
|
+
<li>
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="spg__btn"
|
|
31
|
+
data-status={port.status}
|
|
32
|
+
aria-label={desc}
|
|
33
|
+
on:click={() => dispatch('portclick', i)}
|
|
34
|
+
>
|
|
35
|
+
<span class="spg__num">{n}</span>
|
|
36
|
+
{#if port.speed}
|
|
37
|
+
<span class="spg__speed--{port.status}">{port.speed}</span>
|
|
38
|
+
{/if}
|
|
39
|
+
{#if port.status === 'poe'}
|
|
40
|
+
<span class="spg__poe" aria-hidden="true">PoE</span>
|
|
41
|
+
{/if}
|
|
42
|
+
</button>
|
|
43
|
+
</li>
|
|
44
|
+
{/each}
|
|
45
|
+
</ul>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export let id: string;
|
|
3
|
+
export let active: string;
|
|
4
|
+
$: isActive = id === active;
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div
|
|
8
|
+
role="tabpanel"
|
|
9
|
+
id="tabpanel-{id}"
|
|
10
|
+
aria-labelledby="tab-{id}"
|
|
11
|
+
hidden={!isActive}
|
|
12
|
+
tabindex="0"
|
|
13
|
+
>
|
|
14
|
+
{#if isActive}
|
|
15
|
+
<slot />
|
|
16
|
+
{/if}
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
export interface TabItem { id: string; label: string; badge?: string | number }
|
|
4
|
+
export let items: TabItem[] = [];
|
|
5
|
+
export let active: string;
|
|
6
|
+
export let ariaLabel: string | undefined = undefined;
|
|
7
|
+
const dispatch = createEventDispatcher<{ change: string }>();
|
|
8
|
+
|
|
9
|
+
let buttons: (HTMLButtonElement | null)[] = [];
|
|
10
|
+
|
|
11
|
+
function select(idx: number) {
|
|
12
|
+
const next = items[(idx + items.length) % items.length];
|
|
13
|
+
if (!next) return;
|
|
14
|
+
active = next.id;
|
|
15
|
+
dispatch('change', next.id);
|
|
16
|
+
buttons[(idx + items.length) % items.length]?.focus();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function onKey(e: KeyboardEvent, i: number) {
|
|
20
|
+
if (e.key === 'ArrowRight') { e.preventDefault(); select(i + 1); }
|
|
21
|
+
else if (e.key === 'ArrowLeft') { e.preventDefault(); select(i - 1); }
|
|
22
|
+
else if (e.key === 'Home') { e.preventDefault(); select(0); }
|
|
23
|
+
else if (e.key === 'End') { e.preventDefault(); select(items.length - 1); }
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div class="tabs" role="tablist" aria-label={ariaLabel}>
|
|
28
|
+
{#each items as t, i (t.id)}
|
|
29
|
+
<button
|
|
30
|
+
bind:this={buttons[i]}
|
|
31
|
+
type="button"
|
|
32
|
+
class="tab {t.id === active ? 'active' : ''}"
|
|
33
|
+
role="tab"
|
|
34
|
+
id="tab-{t.id}"
|
|
35
|
+
aria-selected={t.id === active}
|
|
36
|
+
aria-controls="tabpanel-{t.id}"
|
|
37
|
+
tabindex={t.id === active ? 0 : -1}
|
|
38
|
+
on:click={() => { active = t.id; dispatch('change', t.id); }}
|
|
39
|
+
on:keydown={(e) => onKey(e, i)}
|
|
40
|
+
>
|
|
41
|
+
{t.label}
|
|
42
|
+
{#if t.badge !== undefined}<span class="badge">{t.badge}</span>{/if}
|
|
43
|
+
</button>
|
|
44
|
+
{/each}
|
|
45
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export let label: string;
|
|
3
|
+
let className = '';
|
|
4
|
+
export { className as class };
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<span class="tag {className}">
|
|
8
|
+
<span class="tag__label">{label}</span>
|
|
9
|
+
<button
|
|
10
|
+
type="button"
|
|
11
|
+
class="tag__remove"
|
|
12
|
+
aria-label="Remove {label} filter"
|
|
13
|
+
on:click
|
|
14
|
+
>
|
|
15
|
+
<svg viewBox="0 0 10 10" width="10" height="10" fill="none" aria-hidden="true" focusable="false">
|
|
16
|
+
<path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
17
|
+
</svg>
|
|
18
|
+
</button>
|
|
19
|
+
</span>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
let counter = 0;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { createEventDispatcher } from 'svelte';
|
|
7
|
+
|
|
8
|
+
export let label: string | undefined = undefined;
|
|
9
|
+
export let value: string[] = [];
|
|
10
|
+
export let placeholder: string = 'Type and press Enter…';
|
|
11
|
+
export let disabled: boolean = false;
|
|
12
|
+
let klass = '';
|
|
13
|
+
export { klass as class };
|
|
14
|
+
|
|
15
|
+
const dispatch = createEventDispatcher<{ change: string[] }>();
|
|
16
|
+
const uid = `dash-ui-tag-input-${++counter}`;
|
|
17
|
+
|
|
18
|
+
let draft = '';
|
|
19
|
+
let inputEl: HTMLInputElement;
|
|
20
|
+
|
|
21
|
+
function addTag(raw: string) {
|
|
22
|
+
const trimmed = raw.trim();
|
|
23
|
+
if (!trimmed || value.includes(trimmed)) return;
|
|
24
|
+
value = [...value, trimmed];
|
|
25
|
+
draft = '';
|
|
26
|
+
dispatch('change', value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function removeTag(index: number) {
|
|
30
|
+
value = value.filter((_, i) => i !== index);
|
|
31
|
+
dispatch('change', value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
35
|
+
if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') {
|
|
36
|
+
if (draft.trim()) {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
addTag(draft);
|
|
39
|
+
}
|
|
40
|
+
} else if (e.key === 'Backspace' && draft === '' && value.length > 0) {
|
|
41
|
+
removeTag(value.length - 1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleBlur() {
|
|
46
|
+
if (draft.trim()) addTag(draft);
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class="tag-input-wrapper {klass}">
|
|
51
|
+
{#if label}
|
|
52
|
+
<label for={uid} class="tag-input__label">{label}</label>
|
|
53
|
+
{/if}
|
|
54
|
+
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
55
|
+
<div
|
|
56
|
+
role="group"
|
|
57
|
+
aria-label={label ?? 'Tags'}
|
|
58
|
+
class="tag-input{disabled ? ' tag-input--disabled' : ''}"
|
|
59
|
+
on:click={() => inputEl?.focus()}
|
|
60
|
+
>
|
|
61
|
+
{#each value as tag, i}
|
|
62
|
+
<span class="tag">
|
|
63
|
+
<span class="tag__label">{tag}</span>
|
|
64
|
+
{#if !disabled}
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
class="tag__remove"
|
|
68
|
+
aria-label="Remove {tag}"
|
|
69
|
+
on:click|stopPropagation={() => removeTag(i)}
|
|
70
|
+
>
|
|
71
|
+
<svg viewBox="0 0 10 10" width="10" height="10" fill="none" aria-hidden="true" focusable="false">
|
|
72
|
+
<path d="M2 2l6 6M8 2l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
75
|
+
{/if}
|
|
76
|
+
</span>
|
|
77
|
+
{/each}
|
|
78
|
+
<input
|
|
79
|
+
bind:this={inputEl}
|
|
80
|
+
id={uid}
|
|
81
|
+
type="text"
|
|
82
|
+
class="tag-input__field"
|
|
83
|
+
bind:value={draft}
|
|
84
|
+
{disabled}
|
|
85
|
+
placeholder={value.length === 0 ? placeholder : ''}
|
|
86
|
+
aria-label={label ?? 'Add tag'}
|
|
87
|
+
autocomplete="off"
|
|
88
|
+
on:keydown={handleKeyDown}
|
|
89
|
+
on:blur={handleBlur}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
let counter = 0;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
export let value: string = '';
|
|
7
|
+
export let rows: number = 4;
|
|
8
|
+
export let placeholder: string | undefined = undefined;
|
|
9
|
+
export let disabled: boolean = false;
|
|
10
|
+
export let readonly: boolean = false;
|
|
11
|
+
export let id: string | undefined = undefined;
|
|
12
|
+
let className = '';
|
|
13
|
+
export { className as class };
|
|
14
|
+
|
|
15
|
+
const uid = `dash-ui-ta-${++counter}`;
|
|
16
|
+
$: inputId = id ?? uid;
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<textarea
|
|
20
|
+
id={inputId}
|
|
21
|
+
class="input textarea {className}"
|
|
22
|
+
{rows}
|
|
23
|
+
{placeholder}
|
|
24
|
+
{disabled}
|
|
25
|
+
readonly={readonly}
|
|
26
|
+
bind:value
|
|
27
|
+
on:input
|
|
28
|
+
on:change
|
|
29
|
+
on:focus
|
|
30
|
+
on:blur
|
|
31
|
+
></textarea>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script context="module">
|
|
2
|
+
let _n = 0;
|
|
3
|
+
</script>
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
const uid = `tac${_n++}`;
|
|
6
|
+
|
|
7
|
+
export let values: number[];
|
|
8
|
+
export let labels: string[] = [];
|
|
9
|
+
export let threshold: number;
|
|
10
|
+
export let thresholdLabel: string | undefined = undefined;
|
|
11
|
+
export let belowColor: string = '#00C875';
|
|
12
|
+
export let aboveColor: string = '#FF7B7B';
|
|
13
|
+
export let height: number = 160;
|
|
14
|
+
export let ariaLabel: string = 'Threshold area chart';
|
|
15
|
+
|
|
16
|
+
const VW = 400;
|
|
17
|
+
const PAD = { t: 16, r: 8, b: 28, l: 8 };
|
|
18
|
+
|
|
19
|
+
$: n = values.length;
|
|
20
|
+
$: chartW = VW - PAD.l - PAD.r;
|
|
21
|
+
$: chartH = height - PAD.t - PAD.b;
|
|
22
|
+
$: maxV = Math.max(...values, threshold, 1);
|
|
23
|
+
$: toX = (i: number) => PAD.l + (n > 1 ? (i / (n - 1)) * chartW : chartW / 2);
|
|
24
|
+
$: toY = (v: number) => PAD.t + chartH - (v / maxV) * chartH;
|
|
25
|
+
$: threshY = toY(threshold);
|
|
26
|
+
$: baseY = PAD.t + chartH;
|
|
27
|
+
$: pts = values.map((v, i) => `${toX(i).toFixed(1)},${toY(v).toFixed(1)}`);
|
|
28
|
+
$: d = `M ${pts.join(' L ')} L ${toX(n - 1).toFixed(1)},${baseY} L ${toX(0).toFixed(1)},${baseY} Z`;
|
|
29
|
+
$: tLabel = thresholdLabel ?? String(threshold);
|
|
30
|
+
$: gridLines = [0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div role="img" aria-label={ariaLabel} style="width:100%">
|
|
34
|
+
<svg
|
|
35
|
+
viewBox="0 0 {VW} {height}"
|
|
36
|
+
style="width:100%;height:auto;display:block"
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
focusable="false"
|
|
39
|
+
>
|
|
40
|
+
<defs>
|
|
41
|
+
<clipPath id="{uid}b">
|
|
42
|
+
<rect x="0" y={threshY} width={VW} height={height - threshY} />
|
|
43
|
+
</clipPath>
|
|
44
|
+
<clipPath id="{uid}a">
|
|
45
|
+
<rect x="0" y="0" width={VW} height={threshY} />
|
|
46
|
+
</clipPath>
|
|
47
|
+
</defs>
|
|
48
|
+
{#each gridLines as y, i (i)}
|
|
49
|
+
<line x1={PAD.l} y1={y} x2={VW - PAD.r} y2={y} stroke="rgba(255,255,255,0.06)" stroke-width="1" />
|
|
50
|
+
{/each}
|
|
51
|
+
<path {d} fill={belowColor} fill-opacity="0.7" clip-path="url(#{uid}b)" />
|
|
52
|
+
<path {d} fill={aboveColor} fill-opacity="0.7" clip-path="url(#{uid}a)" />
|
|
53
|
+
<line
|
|
54
|
+
x1={PAD.l} y1={threshY}
|
|
55
|
+
x2={VW - PAD.r} y2={threshY}
|
|
56
|
+
stroke={aboveColor}
|
|
57
|
+
stroke-width="1"
|
|
58
|
+
stroke-dasharray="4 3"
|
|
59
|
+
opacity="0.8"
|
|
60
|
+
/>
|
|
61
|
+
<text
|
|
62
|
+
x={VW - PAD.r - 2} y={threshY - 4}
|
|
63
|
+
fill={aboveColor} font-size="9" text-anchor="end" font-family="inherit" opacity="0.9"
|
|
64
|
+
>{tLabel}</text>
|
|
65
|
+
{#each labels as lbl, i (i)}
|
|
66
|
+
<text
|
|
67
|
+
x={toX(i)} y={height - 4}
|
|
68
|
+
text-anchor="middle" fill="#6E7079" font-size="9" font-family="inherit"
|
|
69
|
+
>{lbl}</text>
|
|
70
|
+
{/each}
|
|
71
|
+
</svg>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
let counter = 0;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import { createEventDispatcher } from 'svelte';
|
|
7
|
+
|
|
8
|
+
export let label: string | undefined = undefined;
|
|
9
|
+
export let value: string = '00:00';
|
|
10
|
+
export let disabled: boolean = false;
|
|
11
|
+
let className = '';
|
|
12
|
+
export { className as class };
|
|
13
|
+
|
|
14
|
+
const dispatch = createEventDispatcher<{ change: string }>();
|
|
15
|
+
const uid = `dash-ui-tp-${++counter}`;
|
|
16
|
+
|
|
17
|
+
function pad(n: number): string {
|
|
18
|
+
return String(n).padStart(2, '0');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseTime(t: string): [number, number] {
|
|
22
|
+
const [h = '0', m = '0'] = t.split(':');
|
|
23
|
+
return [
|
|
24
|
+
Math.min(23, Math.max(0, parseInt(h, 10) || 0)),
|
|
25
|
+
Math.min(59, Math.max(0, parseInt(m, 10) || 0)),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
$: [hours, minutes] = parseTime(value);
|
|
30
|
+
|
|
31
|
+
let inputs: [HTMLInputElement | null, HTMLInputElement | null] = [null, null];
|
|
32
|
+
|
|
33
|
+
function commit(h: number, m: number) {
|
|
34
|
+
value = `${pad(h)}:${pad(m)}`;
|
|
35
|
+
dispatch('change', value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleKey(field: 0 | 1, e: KeyboardEvent) {
|
|
39
|
+
const val = field === 0 ? hours : minutes;
|
|
40
|
+
const max = field === 0 ? 23 : 59;
|
|
41
|
+
if (e.key === 'ArrowUp') {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
const next = val >= max ? 0 : val + 1;
|
|
44
|
+
field === 0 ? commit(next, minutes) : commit(hours, next);
|
|
45
|
+
} else if (e.key === 'ArrowDown') {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
const next = val <= 0 ? max : val - 1;
|
|
48
|
+
field === 0 ? commit(next, minutes) : commit(hours, next);
|
|
49
|
+
} else if ((e.key === ':' || e.key === 'ArrowRight') && field === 0) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
inputs[1]?.focus();
|
|
52
|
+
} else if (e.key === 'ArrowLeft' && field === 1) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
inputs[0]?.focus();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleInput(field: 0 | 1, e: Event) {
|
|
59
|
+
const raw = (e.target as HTMLInputElement).value;
|
|
60
|
+
if (!/^\d{0,2}$/.test(raw)) return;
|
|
61
|
+
const n = parseInt(raw, 10);
|
|
62
|
+
if (isNaN(n)) return;
|
|
63
|
+
const max = field === 0 ? 23 : 59;
|
|
64
|
+
const clamped = Math.min(max, n);
|
|
65
|
+
field === 0 ? commit(clamped, minutes) : commit(hours, clamped);
|
|
66
|
+
if (raw.length === 2 && field === 0) setTimeout(() => inputs[1]?.focus(), 0);
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div class="time-picker-wrapper {className}">
|
|
71
|
+
{#if label}
|
|
72
|
+
<label for="{uid}-h" class="time-picker__label">{label}</label>
|
|
73
|
+
{/if}
|
|
74
|
+
<div class="time-picker{disabled ? ' time-picker--disabled' : ''}" role="group" aria-label={label ?? 'Time'}>
|
|
75
|
+
<input
|
|
76
|
+
bind:this={inputs[0]}
|
|
77
|
+
id="{uid}-h"
|
|
78
|
+
type="text"
|
|
79
|
+
inputmode="numeric"
|
|
80
|
+
pattern="[0-9]*"
|
|
81
|
+
role="spinbutton"
|
|
82
|
+
aria-label="Hours"
|
|
83
|
+
aria-valuenow={hours}
|
|
84
|
+
aria-valuemin={0}
|
|
85
|
+
aria-valuemax={23}
|
|
86
|
+
aria-valuetext={pad(hours)}
|
|
87
|
+
value={pad(hours)}
|
|
88
|
+
{disabled}
|
|
89
|
+
maxlength={2}
|
|
90
|
+
class="time-picker__field"
|
|
91
|
+
on:input={(e) => handleInput(0, e)}
|
|
92
|
+
on:keydown={(e) => handleKey(0, e)}
|
|
93
|
+
on:focus={(e) => e.currentTarget.select()}
|
|
94
|
+
/>
|
|
95
|
+
<span class="time-picker__sep" aria-hidden="true">:</span>
|
|
96
|
+
<input
|
|
97
|
+
bind:this={inputs[1]}
|
|
98
|
+
type="text"
|
|
99
|
+
inputmode="numeric"
|
|
100
|
+
pattern="[0-9]*"
|
|
101
|
+
role="spinbutton"
|
|
102
|
+
aria-label="Minutes"
|
|
103
|
+
aria-valuenow={minutes}
|
|
104
|
+
aria-valuemin={0}
|
|
105
|
+
aria-valuemax={59}
|
|
106
|
+
aria-valuetext={pad(minutes)}
|
|
107
|
+
value={pad(minutes)}
|
|
108
|
+
{disabled}
|
|
109
|
+
maxlength={2}
|
|
110
|
+
class="time-picker__field"
|
|
111
|
+
on:input={(e) => handleInput(1, e)}
|
|
112
|
+
on:keydown={(e) => handleKey(1, e)}
|
|
113
|
+
on:focus={(e) => e.currentTarget.select()}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
let counter = 0;
|
|
3
|
+
export type TimeRangeId = '1h' | '6h' | '24h' | '7d' | '30d';
|
|
4
|
+
const PRESETS: { id: TimeRangeId; label: string; short: string }[] = [
|
|
5
|
+
{ id: '1h', label: 'Last 1 hour', short: 'Last 1 h' },
|
|
6
|
+
{ id: '6h', label: 'Last 6 hours', short: 'Last 6 h' },
|
|
7
|
+
{ id: '24h', label: 'Last 24 hours', short: 'Last 24 h' },
|
|
8
|
+
{ id: '7d', label: 'Last 7 days', short: 'Last 7 d' },
|
|
9
|
+
{ id: '30d', label: 'Last 30 days', short: 'Last 30 d' },
|
|
10
|
+
];
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
|
|
15
|
+
|
|
16
|
+
export let value: TimeRangeId = '1h';
|
|
17
|
+
|
|
18
|
+
const dispatch = createEventDispatcher<{ change: TimeRangeId }>();
|
|
19
|
+
const uid = `dash-ui-tr-${++counter}`;
|
|
20
|
+
const listId = `${uid}-list`;
|
|
21
|
+
|
|
22
|
+
let open = false;
|
|
23
|
+
let activeIdx = 0;
|
|
24
|
+
let triggerEl: HTMLButtonElement;
|
|
25
|
+
let wrapperEl: HTMLDivElement;
|
|
26
|
+
|
|
27
|
+
$: selected = PRESETS.find((p) => p.id === value) ?? PRESETS[0];
|
|
28
|
+
|
|
29
|
+
function toggle() {
|
|
30
|
+
if (!open) activeIdx = PRESETS.findIndex((p) => p.id === value);
|
|
31
|
+
open = !open;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function select(id: TimeRangeId) {
|
|
35
|
+
dispatch('change', id);
|
|
36
|
+
open = false;
|
|
37
|
+
triggerEl?.focus();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
41
|
+
if (!open) {
|
|
42
|
+
if (['ArrowDown', 'Enter', ' '].includes(e.key)) {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
activeIdx = PRESETS.findIndex((p) => p.id === value);
|
|
45
|
+
open = true;
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (e.key === 'Escape') { e.preventDefault(); open = false; triggerEl?.focus(); }
|
|
50
|
+
else if (e.key === 'ArrowDown') { e.preventDefault(); activeIdx = Math.min(activeIdx + 1, PRESETS.length - 1); }
|
|
51
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); activeIdx = Math.max(activeIdx - 1, 0); }
|
|
52
|
+
else if (e.key === 'Home') { e.preventDefault(); activeIdx = 0; }
|
|
53
|
+
else if (e.key === 'End') { e.preventDefault(); activeIdx = PRESETS.length - 1; }
|
|
54
|
+
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); select(PRESETS[activeIdx].id); }
|
|
55
|
+
else if (e.key === 'Tab') { open = false; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleOutside(e: MouseEvent) {
|
|
59
|
+
if (open && !wrapperEl?.contains(e.target as Node)) open = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onMount(() => document.addEventListener('mousedown', handleOutside));
|
|
63
|
+
onDestroy(() => document.removeEventListener('mousedown', handleOutside));
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<div class="time-range-root" bind:this={wrapperEl}>
|
|
67
|
+
<button
|
|
68
|
+
bind:this={triggerEl}
|
|
69
|
+
type="button"
|
|
70
|
+
class="btn time-range-trigger"
|
|
71
|
+
aria-haspopup="listbox"
|
|
72
|
+
aria-expanded={open}
|
|
73
|
+
aria-controls={open ? listId : undefined}
|
|
74
|
+
on:click={toggle}
|
|
75
|
+
on:keydown={onKeyDown}
|
|
76
|
+
>
|
|
77
|
+
{selected.short}
|
|
78
|
+
<svg class="time-range-chevron" width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
79
|
+
<path d="M1 1l4 4 4-4" />
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
{#if open}
|
|
83
|
+
<ul id={listId} role="listbox" aria-label="Time range" class="time-range-list">
|
|
84
|
+
{#each PRESETS as p, idx (p.id)}
|
|
85
|
+
<li
|
|
86
|
+
role="option"
|
|
87
|
+
aria-selected={p.id === value}
|
|
88
|
+
tabindex="-1"
|
|
89
|
+
data-active={idx === activeIdx ? 'true' : undefined}
|
|
90
|
+
class="time-range-option"
|
|
91
|
+
on:mouseenter={() => { activeIdx = idx; }}
|
|
92
|
+
on:mousedown|preventDefault={() => select(p.id)}
|
|
93
|
+
>
|
|
94
|
+
{p.label}
|
|
95
|
+
{#if p.id === value}
|
|
96
|
+
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
97
|
+
<path d="M2 6l3 3 5-5" />
|
|
98
|
+
</svg>
|
|
99
|
+
{/if}
|
|
100
|
+
</li>
|
|
101
|
+
{/each}
|
|
102
|
+
</ul>
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
export interface TimelineEvent {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
time: string;
|
|
7
|
+
variant?: 'info' | 'warn' | 'danger' | 'success' | 'neutral';
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
export let events: TimelineEvent[];
|
|
13
|
+
let className = '';
|
|
14
|
+
export { className as class };
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<ol class="timeline {className}" aria-label="Event timeline">
|
|
18
|
+
{#each events as e (e.id)}
|
|
19
|
+
<li class="timeline__item timeline__item--{e.variant ?? 'neutral'}">
|
|
20
|
+
<span class="timeline__dot" aria-hidden="true"></span>
|
|
21
|
+
<div class="timeline__content">
|
|
22
|
+
<div class="timeline__header">
|
|
23
|
+
<span class="timeline__title">{e.title}</span>
|
|
24
|
+
<time class="timeline__time">{e.time}</time>
|
|
25
|
+
</div>
|
|
26
|
+
{#if e.description}
|
|
27
|
+
<p class="timeline__desc">{e.description}</p>
|
|
28
|
+
{/if}
|
|
29
|
+
</div>
|
|
30
|
+
</li>
|
|
31
|
+
{/each}
|
|
32
|
+
</ol>
|