@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,52 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
import { writable } from 'svelte/store';
|
|
3
|
+
|
|
4
|
+
export type ToastVariant = 'success' | 'info' | 'warn' | 'danger';
|
|
5
|
+
|
|
6
|
+
interface ToastItem {
|
|
7
|
+
id: string;
|
|
8
|
+
message: string;
|
|
9
|
+
variant: ToastVariant;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const store = writable<ToastItem[]>([]);
|
|
13
|
+
let seq = 0;
|
|
14
|
+
|
|
15
|
+
function dismiss(id: string) {
|
|
16
|
+
store.update((prev) => prev.filter((t) => t.id !== id));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const toast = {
|
|
20
|
+
show(message: string, variant: ToastVariant = 'info') {
|
|
21
|
+
const id = String(++seq);
|
|
22
|
+
store.update((prev) => [...prev, { id, message, variant }]);
|
|
23
|
+
setTimeout(() => dismiss(id), 4000);
|
|
24
|
+
},
|
|
25
|
+
success: (msg: string) => toast.show(msg, 'success'),
|
|
26
|
+
warn: (msg: string) => toast.show(msg, 'warn'),
|
|
27
|
+
danger: (msg: string) => toast.show(msg, 'danger'),
|
|
28
|
+
info: (msg: string) => toast.show(msg, 'info'),
|
|
29
|
+
};
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<script lang="ts">
|
|
33
|
+
// store and dismiss are in module scope — accessible in template via $store
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div class="toaster" role="log" aria-label="Notifications" aria-live="polite" aria-atomic="false">
|
|
37
|
+
{#each $store as item (item.id)}
|
|
38
|
+
<div class="toast toast-{item.variant}" role={item.variant === 'danger' ? 'alert' : 'status'}>
|
|
39
|
+
<span class="toast-msg">{item.message}</span>
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
class="toast-dismiss icon-btn"
|
|
43
|
+
on:click={() => dismiss(item.id)}
|
|
44
|
+
aria-label="Dismiss notification"
|
|
45
|
+
>
|
|
46
|
+
<svg viewBox="0 0 14 14" width="14" height="14" aria-hidden="true" focusable="false">
|
|
47
|
+
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
48
|
+
</svg>
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
{/each}
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
export let on: boolean = false;
|
|
4
|
+
export let ariaLabel: string | undefined = undefined;
|
|
5
|
+
export let ariaDescribedBy: string | undefined = undefined;
|
|
6
|
+
const dispatch = createEventDispatcher<{ toggle: boolean }>();
|
|
7
|
+
function flip() {
|
|
8
|
+
on = !on;
|
|
9
|
+
dispatch('toggle', on);
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
role="switch"
|
|
16
|
+
aria-checked={on}
|
|
17
|
+
aria-label={ariaLabel}
|
|
18
|
+
aria-describedby={ariaDescribedBy}
|
|
19
|
+
class="toggle-track {on ? 'on' : 'off'}"
|
|
20
|
+
on:click={flip}
|
|
21
|
+
>
|
|
22
|
+
<span class="knob"></span>
|
|
23
|
+
</button>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
|
|
4
|
+
export let options: { value: string; label: string; disabled?: boolean }[] = [];
|
|
5
|
+
export let value: string[] = [];
|
|
6
|
+
export let ariaLabel: string = '';
|
|
7
|
+
export let size: 'sm' | 'md' = 'md';
|
|
8
|
+
let klass: string = '';
|
|
9
|
+
export { klass as class };
|
|
10
|
+
|
|
11
|
+
const dispatch = createEventDispatcher<{ change: string[] }>();
|
|
12
|
+
|
|
13
|
+
function toggle(v: string) {
|
|
14
|
+
const next = value.includes(v) ? value.filter((x) => x !== v) : [...value, v];
|
|
15
|
+
value = next;
|
|
16
|
+
dispatch('change', next);
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div role="group" aria-label={ariaLabel} class="tg-group tg-group--{size}{klass ? ` ${klass}` : ''}">
|
|
21
|
+
{#each options as opt}
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
aria-pressed={value.includes(opt.value)}
|
|
25
|
+
disabled={opt.disabled}
|
|
26
|
+
class="tg-btn{value.includes(opt.value) ? ' tg-btn--on' : ''}"
|
|
27
|
+
on:click={() => toggle(opt.value)}
|
|
28
|
+
>
|
|
29
|
+
{opt.label}
|
|
30
|
+
</button>
|
|
31
|
+
{/each}
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
let counter = 0;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
export let label: string;
|
|
7
|
+
export let placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
|
|
8
|
+
let className = '';
|
|
9
|
+
export { className as class };
|
|
10
|
+
const id = `dash-ui-tooltip-${++counter}`;
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<span class="tooltip-wrapper tooltip-{placement} {className}">
|
|
14
|
+
<span class="tooltip-trigger" aria-describedby={id}><slot /></span>
|
|
15
|
+
<span {id} role="tooltip" class="tooltip-content">{label}</span>
|
|
16
|
+
</span>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
|
+
import { appLogos, type AppLogoKey as AppLogo } from '@w5-ui/assets';
|
|
4
|
+
import IconButton from './IconButton.svelte';
|
|
5
|
+
import Avatar from './Avatar.svelte';
|
|
6
|
+
import CaretIcon from '../icons/CaretIcon.svelte';
|
|
7
|
+
import SearchIcon from '../icons/SearchIcon.svelte';
|
|
8
|
+
import UpdatesIcon from '../icons/UpdatesIcon.svelte';
|
|
9
|
+
import BellIcon from '../icons/BellIcon.svelte';
|
|
10
|
+
import HelpIcon from '../icons/HelpIcon.svelte';
|
|
11
|
+
|
|
12
|
+
export interface AppDef { id: string; label: string; logo: AppLogo }
|
|
13
|
+
export const DEFAULT_APPS: AppDef[] = [
|
|
14
|
+
{ id: 'system', label: 'System', logo: 'system' },
|
|
15
|
+
{ id: 'instances', label: 'Instances', logo: 'instances' },
|
|
16
|
+
{ id: 'agents', label: 'Agents', logo: 'agents' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export let siteName: string;
|
|
20
|
+
export let activeApp: string;
|
|
21
|
+
export let apps: AppDef[] = DEFAULT_APPS;
|
|
22
|
+
export let initials: string = 'MS';
|
|
23
|
+
export let notificationCount: number = 1;
|
|
24
|
+
const dispatch = createEventDispatcher<{ appchange: string }>();
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<header class="topbar">
|
|
28
|
+
<button type="button" class="site-switch" aria-label="Switch site: {siteName}" aria-haspopup="menu">
|
|
29
|
+
<span class="status-ring"></span>
|
|
30
|
+
<span class="site-name" aria-hidden="true">{siteName}</span>
|
|
31
|
+
<CaretIcon class="caret" aria-hidden="true" />
|
|
32
|
+
</button>
|
|
33
|
+
<nav class="app-tabs" aria-label="Apps">
|
|
34
|
+
{#each apps as a (a.id)}
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="app-tab {a.id === activeApp ? 'active' : ''}"
|
|
38
|
+
aria-current={a.id === activeApp ? 'page' : undefined}
|
|
39
|
+
on:click={() => { activeApp = a.id; dispatch('appchange', a.id); }}
|
|
40
|
+
>
|
|
41
|
+
<img src={appLogos[a.logo]} alt="" width="24" height="24" />
|
|
42
|
+
<span class="label">{a.label}</span>
|
|
43
|
+
</button>
|
|
44
|
+
{/each}
|
|
45
|
+
</nav>
|
|
46
|
+
<div class="topbar-spacer"></div>
|
|
47
|
+
<div class="topbar-right">
|
|
48
|
+
<IconButton aria-label="Search" title="Search"><SearchIcon /></IconButton>
|
|
49
|
+
<IconButton aria-label="Updates" title="Updates"><UpdatesIcon /></IconButton>
|
|
50
|
+
<IconButton
|
|
51
|
+
aria-label={notificationCount > 0 ? `Notifications, ${notificationCount} new` : 'Notifications'}
|
|
52
|
+
title="Notifications"
|
|
53
|
+
style="position:relative;"
|
|
54
|
+
>
|
|
55
|
+
<BellIcon />
|
|
56
|
+
{#if notificationCount > 0}
|
|
57
|
+
<span aria-hidden="true" style="position:absolute;top:6px;right:6px;width:6px;height:6px;background:#F03A3A;border-radius:50%;border:1.5px solid #0A0A0B;"></span>
|
|
58
|
+
{/if}
|
|
59
|
+
</IconButton>
|
|
60
|
+
<IconButton aria-label="Help" title="Help"><HelpIcon /></IconButton>
|
|
61
|
+
<Avatar initials={initials} size="sm" alt="Account, {initials}" />
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
export interface TransferListItem {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { createEventDispatcher } from 'svelte';
|
|
11
|
+
|
|
12
|
+
export let sourceLabel: string = 'Available';
|
|
13
|
+
export let targetLabel: string = 'Selected';
|
|
14
|
+
export let source: TransferListItem[] = [];
|
|
15
|
+
export let target: TransferListItem[] = [];
|
|
16
|
+
let className = '';
|
|
17
|
+
export { className as class };
|
|
18
|
+
|
|
19
|
+
const dispatch = createEventDispatcher<{ change: { source: TransferListItem[]; target: TransferListItem[] } }>();
|
|
20
|
+
|
|
21
|
+
let selSrc = new Set<string>();
|
|
22
|
+
let selTgt = new Set<string>();
|
|
23
|
+
let focSrc = 0;
|
|
24
|
+
let focTgt = 0;
|
|
25
|
+
let srcEls: (HTMLLIElement | null)[] = [];
|
|
26
|
+
let tgtEls: (HTMLLIElement | null)[] = [];
|
|
27
|
+
|
|
28
|
+
function commit() {
|
|
29
|
+
dispatch('change', { source, target });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function moveRight(ids: Set<string>) {
|
|
33
|
+
if (!ids.size) return;
|
|
34
|
+
const moving = source.filter((i) => ids.has(i.id));
|
|
35
|
+
source = source.filter((i) => !ids.has(i.id));
|
|
36
|
+
target = [...target, ...moving];
|
|
37
|
+
selSrc = new Set();
|
|
38
|
+
focSrc = 0;
|
|
39
|
+
commit();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function moveLeft(ids: Set<string>) {
|
|
43
|
+
if (!ids.size) return;
|
|
44
|
+
const moving = target.filter((i) => ids.has(i.id));
|
|
45
|
+
target = target.filter((i) => !ids.has(i.id));
|
|
46
|
+
source = [...source, ...moving];
|
|
47
|
+
selTgt = new Set();
|
|
48
|
+
focTgt = 0;
|
|
49
|
+
commit();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toggleSrc(id: string) {
|
|
53
|
+
const n = new Set(selSrc);
|
|
54
|
+
n.has(id) ? n.delete(id) : n.add(id);
|
|
55
|
+
selSrc = n;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toggleTgt(id: string) {
|
|
59
|
+
const n = new Set(selTgt);
|
|
60
|
+
n.has(id) ? n.delete(id) : n.add(id);
|
|
61
|
+
selTgt = n;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function srcKey(e: KeyboardEvent) {
|
|
65
|
+
if (!source.length) return;
|
|
66
|
+
if (e.key === 'ArrowDown') {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
focSrc = Math.min(focSrc + 1, source.length - 1);
|
|
69
|
+
srcEls[focSrc]?.focus();
|
|
70
|
+
} else if (e.key === 'ArrowUp') {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
focSrc = Math.max(focSrc - 1, 0);
|
|
73
|
+
srcEls[focSrc]?.focus();
|
|
74
|
+
} else if (e.key === ' ') {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
const item = source[focSrc];
|
|
77
|
+
if (item) toggleSrc(item.id);
|
|
78
|
+
} else if (e.key === 'Enter') {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
moveRight(selSrc);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function tgtKey(e: KeyboardEvent) {
|
|
85
|
+
if (!target.length) return;
|
|
86
|
+
if (e.key === 'ArrowDown') {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
focTgt = Math.min(focTgt + 1, target.length - 1);
|
|
89
|
+
tgtEls[focTgt]?.focus();
|
|
90
|
+
} else if (e.key === 'ArrowUp') {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
focTgt = Math.max(focTgt - 1, 0);
|
|
93
|
+
tgtEls[focTgt]?.focus();
|
|
94
|
+
} else if (e.key === ' ') {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
const item = target[focTgt];
|
|
97
|
+
if (item) toggleTgt(item.id);
|
|
98
|
+
} else if (e.key === 'Enter') {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
moveLeft(selTgt);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<div class="tl {className}">
|
|
106
|
+
<div class="tl__panel">
|
|
107
|
+
<div class="tl__header">
|
|
108
|
+
{sourceLabel} <span class="tl__count">{source.length}</span>
|
|
109
|
+
</div>
|
|
110
|
+
<ul
|
|
111
|
+
role="listbox"
|
|
112
|
+
aria-label={sourceLabel}
|
|
113
|
+
aria-multiselectable="true"
|
|
114
|
+
class="tl__list"
|
|
115
|
+
on:keydown={srcKey}
|
|
116
|
+
>
|
|
117
|
+
{#if source.length === 0}
|
|
118
|
+
<li class="tl__empty" role="option" aria-selected="false" aria-disabled="true">Empty</li>
|
|
119
|
+
{:else}
|
|
120
|
+
{#each source as item, i (item.id)}
|
|
121
|
+
<li
|
|
122
|
+
bind:this={srcEls[i]}
|
|
123
|
+
role="option"
|
|
124
|
+
aria-selected={selSrc.has(item.id)}
|
|
125
|
+
tabindex={i === focSrc ? 0 : -1}
|
|
126
|
+
class="tl__item{selSrc.has(item.id) ? ' tl__item--sel' : ''}"
|
|
127
|
+
on:click={() => { focSrc = i; toggleSrc(item.id); }}
|
|
128
|
+
on:keydown={(e) => { if (e.key === ' ') { e.preventDefault(); focSrc = i; toggleSrc(item.id); } }}
|
|
129
|
+
>
|
|
130
|
+
<span class="tl__item-label">{item.label}</span>
|
|
131
|
+
{#if item.description}<span class="tl__item-desc">{item.description}</span>{/if}
|
|
132
|
+
</li>
|
|
133
|
+
{/each}
|
|
134
|
+
{/if}
|
|
135
|
+
</ul>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="tl__controls" role="group" aria-label="Transfer controls">
|
|
139
|
+
<button type="button" class="tl__btn" aria-label="Move all to {targetLabel}"
|
|
140
|
+
disabled={source.length === 0}
|
|
141
|
+
on:click={() => moveRight(new Set(source.map((i) => i.id)))}>»</button>
|
|
142
|
+
<button type="button" class="tl__btn" aria-label="Move selected to {targetLabel}"
|
|
143
|
+
disabled={selSrc.size === 0}
|
|
144
|
+
on:click={() => moveRight(selSrc)}>›</button>
|
|
145
|
+
<button type="button" class="tl__btn" aria-label="Move selected back to {sourceLabel}"
|
|
146
|
+
disabled={selTgt.size === 0}
|
|
147
|
+
on:click={() => moveLeft(selTgt)}>‹</button>
|
|
148
|
+
<button type="button" class="tl__btn" aria-label="Move all back to {sourceLabel}"
|
|
149
|
+
disabled={target.length === 0}
|
|
150
|
+
on:click={() => moveLeft(new Set(target.map((i) => i.id)))}>«</button>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="tl__panel">
|
|
154
|
+
<div class="tl__header">
|
|
155
|
+
{targetLabel} <span class="tl__count">{target.length}</span>
|
|
156
|
+
</div>
|
|
157
|
+
<ul
|
|
158
|
+
role="listbox"
|
|
159
|
+
aria-label={targetLabel}
|
|
160
|
+
aria-multiselectable="true"
|
|
161
|
+
class="tl__list"
|
|
162
|
+
on:keydown={tgtKey}
|
|
163
|
+
>
|
|
164
|
+
{#if target.length === 0}
|
|
165
|
+
<li class="tl__empty" role="option" aria-selected="false" aria-disabled="true">Empty</li>
|
|
166
|
+
{:else}
|
|
167
|
+
{#each target as item, i (item.id)}
|
|
168
|
+
<li
|
|
169
|
+
bind:this={tgtEls[i]}
|
|
170
|
+
role="option"
|
|
171
|
+
aria-selected={selTgt.has(item.id)}
|
|
172
|
+
tabindex={i === focTgt ? 0 : -1}
|
|
173
|
+
class="tl__item{selTgt.has(item.id) ? ' tl__item--sel' : ''}"
|
|
174
|
+
on:click={() => { focTgt = i; toggleTgt(item.id); }}
|
|
175
|
+
on:keydown={(e) => { if (e.key === ' ') { e.preventDefault(); focTgt = i; toggleTgt(item.id); } }}
|
|
176
|
+
>
|
|
177
|
+
<span class="tl__item-label">{item.label}</span>
|
|
178
|
+
{#if item.description}<span class="tl__item-desc">{item.description}</span>{/if}
|
|
179
|
+
</li>
|
|
180
|
+
{/each}
|
|
181
|
+
{/if}
|
|
182
|
+
</ul>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
meta?: string;
|
|
6
|
+
children?: TreeNode[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export let node: TreeNode;
|
|
10
|
+
export let expanded: Set<string>;
|
|
11
|
+
export let selected: string | undefined;
|
|
12
|
+
export let focusedId: string;
|
|
13
|
+
|
|
14
|
+
$: hasChildren = !!node.children?.length;
|
|
15
|
+
$: isExpanded = expanded.has(node.id);
|
|
16
|
+
$: isSelected = selected === node.id;
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<li
|
|
20
|
+
role="treeitem"
|
|
21
|
+
aria-expanded={hasChildren ? isExpanded : undefined}
|
|
22
|
+
aria-selected={isSelected}
|
|
23
|
+
tabindex={focusedId === node.id ? 0 : -1}
|
|
24
|
+
data-tree-id={node.id}
|
|
25
|
+
class="tree__item"
|
|
26
|
+
>
|
|
27
|
+
<div class="tree__row{isSelected ? ' tree__row--selected' : ''}">
|
|
28
|
+
{#if hasChildren}
|
|
29
|
+
<svg
|
|
30
|
+
class="tree__chevron{isExpanded ? ' tree__chevron--open' : ''}"
|
|
31
|
+
viewBox="0 0 12 12"
|
|
32
|
+
fill="none"
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
>
|
|
35
|
+
<path
|
|
36
|
+
d="M4 2l4 4-4 4"
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
stroke-width="1.5"
|
|
39
|
+
stroke-linecap="round"
|
|
40
|
+
stroke-linejoin="round"
|
|
41
|
+
/>
|
|
42
|
+
</svg>
|
|
43
|
+
{:else}
|
|
44
|
+
<span class="tree__indent" aria-hidden="true"></span>
|
|
45
|
+
{/if}
|
|
46
|
+
<span class="tree__label">{node.label}</span>
|
|
47
|
+
{#if node.meta}<span class="tree__meta">{node.meta}</span>{/if}
|
|
48
|
+
</div>
|
|
49
|
+
{#if hasChildren && isExpanded}
|
|
50
|
+
<ul role="group" class="tree__group">
|
|
51
|
+
{#each node.children as child (child.id)}
|
|
52
|
+
<svelte:self
|
|
53
|
+
node={child}
|
|
54
|
+
{expanded}
|
|
55
|
+
{selected}
|
|
56
|
+
{focusedId}
|
|
57
|
+
/>
|
|
58
|
+
{/each}
|
|
59
|
+
</ul>
|
|
60
|
+
{/if}
|
|
61
|
+
</li>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export interface TreeMapNode {
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export let nodes: TreeMapNode[] = [];
|
|
9
|
+
export let height: number = 160;
|
|
10
|
+
export let ariaLabel: string = 'Treemap chart';
|
|
11
|
+
|
|
12
|
+
const VW = 320;
|
|
13
|
+
const PAD = 4;
|
|
14
|
+
const GAP = 2;
|
|
15
|
+
const PALETTE = ['#006FFF', '#00C875', '#F04949', '#A78BFA', '#F5A623', '#50B8E7'];
|
|
16
|
+
|
|
17
|
+
interface LayoutRect {
|
|
18
|
+
x: number; y: number; w: number; h: number;
|
|
19
|
+
label: string; value: number; color: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function layout(
|
|
23
|
+
ns: { label: string; value: number; color: string }[],
|
|
24
|
+
x: number, y: number, w: number, h: number
|
|
25
|
+
): LayoutRect[] {
|
|
26
|
+
if (!ns.length) return [];
|
|
27
|
+
if (ns.length === 1) return [{ x, y, w: Math.max(w, 0), h: Math.max(h, 0), ...ns[0] }];
|
|
28
|
+
const total = ns.reduce((s, n) => s + n.value, 0);
|
|
29
|
+
let acc = 0;
|
|
30
|
+
let split = ns.length - 1;
|
|
31
|
+
for (let i = 0; i < ns.length - 1; i++) {
|
|
32
|
+
acc += ns[i].value;
|
|
33
|
+
if (acc / total >= 0.5) { split = i + 1; break; }
|
|
34
|
+
}
|
|
35
|
+
const ratio = ns.slice(0, split).reduce((s, n) => s + n.value, 0) / total;
|
|
36
|
+
if (w > h) {
|
|
37
|
+
const w1 = Math.max((w - GAP) * ratio, 0);
|
|
38
|
+
return [...layout(ns.slice(0, split), x, y, w1, h), ...layout(ns.slice(split), x + w1 + GAP, y, Math.max(w - w1 - GAP, 0), h)];
|
|
39
|
+
}
|
|
40
|
+
const h1 = Math.max((h - GAP) * ratio, 0);
|
|
41
|
+
return [...layout(ns.slice(0, split), x, y, w, h1), ...layout(ns.slice(split), x, y + h1 + GAP, w, Math.max(h - h1 - GAP, 0))];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$: rects = (() => {
|
|
45
|
+
if (!nodes.length) return [] as LayoutRect[];
|
|
46
|
+
const sorted = [...nodes]
|
|
47
|
+
.sort((a, b) => b.value - a.value)
|
|
48
|
+
.map((n, i) => ({ ...n, color: n.color ?? PALETTE[i % PALETTE.length] }));
|
|
49
|
+
return layout(sorted, PAD, PAD, VW - PAD * 2, height - PAD * 2);
|
|
50
|
+
})();
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<div role="img" aria-label={ariaLabel} style="width:100%;">
|
|
54
|
+
<svg viewBox="0 0 {VW} {height}" style="width:100%;height:auto;display:block;" aria-hidden="true" focusable="false">
|
|
55
|
+
{#each rects as r, i (i)}
|
|
56
|
+
{@const showLabel = r.w >= 36 && r.h >= 22}
|
|
57
|
+
{@const fs = Math.min(10, Math.max(7, Math.floor(r.w / 6)))}
|
|
58
|
+
<g>
|
|
59
|
+
<rect
|
|
60
|
+
x={r.x.toFixed(1)}
|
|
61
|
+
y={r.y.toFixed(1)}
|
|
62
|
+
width={r.w.toFixed(1)}
|
|
63
|
+
height={r.h.toFixed(1)}
|
|
64
|
+
fill={r.color}
|
|
65
|
+
fill-opacity="0.85"
|
|
66
|
+
rx="3"
|
|
67
|
+
/>
|
|
68
|
+
{#if showLabel}
|
|
69
|
+
<text
|
|
70
|
+
x={(r.x + r.w / 2).toFixed(1)}
|
|
71
|
+
y={(r.y + r.h / 2 - 4).toFixed(1)}
|
|
72
|
+
text-anchor="middle"
|
|
73
|
+
fill="#fff"
|
|
74
|
+
font-size={fs}
|
|
75
|
+
font-family="inherit"
|
|
76
|
+
font-weight="600"
|
|
77
|
+
>{r.label}</text>
|
|
78
|
+
<text
|
|
79
|
+
x={(r.x + r.w / 2).toFixed(1)}
|
|
80
|
+
y={(r.y + r.h / 2 + 9).toFixed(1)}
|
|
81
|
+
text-anchor="middle"
|
|
82
|
+
fill="rgba(255,255,255,0.65)"
|
|
83
|
+
font-size={Math.max(fs - 1, 7)}
|
|
84
|
+
font-family="inherit"
|
|
85
|
+
>{r.value}</text>
|
|
86
|
+
{/if}
|
|
87
|
+
</g>
|
|
88
|
+
{/each}
|
|
89
|
+
</svg>
|
|
90
|
+
</div>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script context="module" lang="ts">
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
meta?: string;
|
|
6
|
+
children?: TreeNode[];
|
|
7
|
+
}
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<script lang="ts">
|
|
11
|
+
import { tick } from 'svelte';
|
|
12
|
+
import TreeItem from './TreeItem.svelte';
|
|
13
|
+
|
|
14
|
+
export let nodes: TreeNode[] = [];
|
|
15
|
+
export let selected: string | undefined = undefined;
|
|
16
|
+
export let defaultExpanded: string[] = [];
|
|
17
|
+
export let label: string | undefined = undefined;
|
|
18
|
+
export let className: string = '';
|
|
19
|
+
|
|
20
|
+
let expanded = new Set<string>(defaultExpanded);
|
|
21
|
+
let focusedId: string = nodes[0]?.id ?? '';
|
|
22
|
+
let rootEl: HTMLUListElement;
|
|
23
|
+
|
|
24
|
+
function visibleIds(list: TreeNode[]): string[] {
|
|
25
|
+
const ids: string[] = [];
|
|
26
|
+
function walk(ns: TreeNode[]) {
|
|
27
|
+
for (const n of ns) {
|
|
28
|
+
ids.push(n.id);
|
|
29
|
+
if (n.children?.length && expanded.has(n.id)) walk(n.children);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
walk(list);
|
|
33
|
+
return ids;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findNode(id: string, list: TreeNode[]): TreeNode | undefined {
|
|
37
|
+
for (const n of list) {
|
|
38
|
+
if (n.id === id) return n;
|
|
39
|
+
if (n.children) {
|
|
40
|
+
const found = findNode(id, n.children);
|
|
41
|
+
if (found) return found;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function moveFocus(id: string) {
|
|
47
|
+
focusedId = id;
|
|
48
|
+
await tick();
|
|
49
|
+
rootEl?.querySelector<HTMLElement>(`[data-tree-id="${id}"]`)?.focus();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toggle(id: string) {
|
|
53
|
+
if (expanded.has(id)) expanded.delete(id);
|
|
54
|
+
else expanded.add(id);
|
|
55
|
+
expanded = expanded;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleFocusin(e: FocusEvent) {
|
|
59
|
+
const el = (e.target as HTMLElement).closest('[data-tree-id]');
|
|
60
|
+
if (el) focusedId = el.getAttribute('data-tree-id') ?? focusedId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleClick(e: MouseEvent) {
|
|
64
|
+
const row = (e.target as HTMLElement).closest('.tree__row');
|
|
65
|
+
if (!row) return;
|
|
66
|
+
const item = row.closest('[data-tree-id]');
|
|
67
|
+
if (!item) return;
|
|
68
|
+
const id = item.getAttribute('data-tree-id');
|
|
69
|
+
if (!id) return;
|
|
70
|
+
selected = id;
|
|
71
|
+
focusedId = id;
|
|
72
|
+
const node = findNode(id, nodes);
|
|
73
|
+
if (node?.children?.length) toggle(id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
77
|
+
const el = (e.target as HTMLElement).closest('[data-tree-id]');
|
|
78
|
+
if (!el) return;
|
|
79
|
+
const id = el.getAttribute('data-tree-id');
|
|
80
|
+
if (!id) return;
|
|
81
|
+
|
|
82
|
+
const visible = visibleIds(nodes);
|
|
83
|
+
const idx = visible.indexOf(id);
|
|
84
|
+
const node = findNode(id, nodes);
|
|
85
|
+
const hasChildren = !!node?.children?.length;
|
|
86
|
+
const isNodeExpanded = expanded.has(id);
|
|
87
|
+
|
|
88
|
+
if (e.key === 'ArrowDown') {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
const next = visible[idx + 1];
|
|
91
|
+
if (next) moveFocus(next);
|
|
92
|
+
} else if (e.key === 'ArrowUp') {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
const prev = visible[idx - 1];
|
|
95
|
+
if (prev) moveFocus(prev);
|
|
96
|
+
} else if (e.key === 'ArrowRight') {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
if (hasChildren && !isNodeExpanded) toggle(id);
|
|
99
|
+
else if (hasChildren && isNodeExpanded) moveFocus(node!.children![0]!.id);
|
|
100
|
+
} else if (e.key === 'ArrowLeft') {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
if (hasChildren && isNodeExpanded) toggle(id);
|
|
103
|
+
} else if (e.key === 'Home') {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
if (visible[0]) moveFocus(visible[0]);
|
|
106
|
+
} else if (e.key === 'End') {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
const last = visible[visible.length - 1];
|
|
109
|
+
if (last) moveFocus(last);
|
|
110
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
selected = id;
|
|
113
|
+
focusedId = id;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<ul
|
|
119
|
+
bind:this={rootEl}
|
|
120
|
+
role="tree"
|
|
121
|
+
aria-label={label}
|
|
122
|
+
class={`tree${className ? ' ' + className : ''}`}
|
|
123
|
+
on:keydown={handleKeydown}
|
|
124
|
+
on:focusin={handleFocusin}
|
|
125
|
+
on:click={handleClick}
|
|
126
|
+
>
|
|
127
|
+
{#each nodes as node (node.id)}
|
|
128
|
+
<TreeItem {node} {expanded} {selected} {focusedId} />
|
|
129
|
+
{/each}
|
|
130
|
+
</ul>
|