@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.
Files changed (195) hide show
  1. package/README.md +8 -0
  2. package/package.json +57 -0
  3. package/src/lib/components/Accordion.svelte +3 -0
  4. package/src/lib/components/AccordionItem.svelte +40 -0
  5. package/src/lib/components/ActionMenu.svelte +96 -0
  6. package/src/lib/components/ActivityFeed.svelte +60 -0
  7. package/src/lib/components/Alert.svelte +19 -0
  8. package/src/lib/components/AnnotatedTimeSeries.svelte +90 -0
  9. package/src/lib/components/ArcDiagram.svelte +74 -0
  10. package/src/lib/components/AreaChart.svelte +64 -0
  11. package/src/lib/components/Avatar.svelte +30 -0
  12. package/src/lib/components/AvatarGroup.svelte +35 -0
  13. package/src/lib/components/Badge.svelte +25 -0
  14. package/src/lib/components/Banner.svelte +37 -0
  15. package/src/lib/components/BarChart.svelte +52 -0
  16. package/src/lib/components/BeeswarmChart.svelte +88 -0
  17. package/src/lib/components/BoxPlot.svelte +66 -0
  18. package/src/lib/components/Breadcrumb.svelte +31 -0
  19. package/src/lib/components/BubbleChart.svelte +69 -0
  20. package/src/lib/components/BulletChart.svelte +70 -0
  21. package/src/lib/components/BumpChart.svelte +109 -0
  22. package/src/lib/components/Button.svelte +25 -0
  23. package/src/lib/components/CIDRInput.svelte +142 -0
  24. package/src/lib/components/CalendarHeatmap.svelte +86 -0
  25. package/src/lib/components/Callout.svelte +38 -0
  26. package/src/lib/components/CandlestickChart.svelte +66 -0
  27. package/src/lib/components/Card.svelte +11 -0
  28. package/src/lib/components/Carousel.svelte +64 -0
  29. package/src/lib/components/Checkbox.svelte +45 -0
  30. package/src/lib/components/ChordDiagram.svelte +90 -0
  31. package/src/lib/components/CirclePacking.svelte +119 -0
  32. package/src/lib/components/CodeBlock.svelte +49 -0
  33. package/src/lib/components/ColorPicker.svelte +91 -0
  34. package/src/lib/components/ColumnToggle.svelte +96 -0
  35. package/src/lib/components/Combobox.svelte +138 -0
  36. package/src/lib/components/CommandPalette.svelte +152 -0
  37. package/src/lib/components/ConfirmDialog.svelte +84 -0
  38. package/src/lib/components/ContextMenu.svelte +107 -0
  39. package/src/lib/components/ContextualHelp.svelte +73 -0
  40. package/src/lib/components/ContourPlot.svelte +164 -0
  41. package/src/lib/components/CopyButton.svelte +33 -0
  42. package/src/lib/components/CorrelationMatrix.svelte +90 -0
  43. package/src/lib/components/CountUp.svelte +49 -0
  44. package/src/lib/components/CumulativeDistribution.svelte +67 -0
  45. package/src/lib/components/DatePicker.svelte +201 -0
  46. package/src/lib/components/DateRangePicker.svelte +249 -0
  47. package/src/lib/components/Dendrogram.svelte +128 -0
  48. package/src/lib/components/Donut.svelte +49 -0
  49. package/src/lib/components/DotPlot.svelte +121 -0
  50. package/src/lib/components/Drawer.svelte +60 -0
  51. package/src/lib/components/DualAxisChart.svelte +80 -0
  52. package/src/lib/components/DumbbellChart.svelte +56 -0
  53. package/src/lib/components/DurationInput.svelte +118 -0
  54. package/src/lib/components/EmptyState.svelte +21 -0
  55. package/src/lib/components/ErrorBandChart.svelte +69 -0
  56. package/src/lib/components/ExpandableRow.svelte +42 -0
  57. package/src/lib/components/Field.svelte +40 -0
  58. package/src/lib/components/FileUpload.svelte +96 -0
  59. package/src/lib/components/FilterBuilder.svelte +160 -0
  60. package/src/lib/components/FlameGraph.svelte +88 -0
  61. package/src/lib/components/ForceGraph.svelte +104 -0
  62. package/src/lib/components/FunnelChart.svelte +50 -0
  63. package/src/lib/components/GanttChart.svelte +49 -0
  64. package/src/lib/components/Gauge.svelte +53 -0
  65. package/src/lib/components/GroupedList.svelte +87 -0
  66. package/src/lib/components/HealthBar.svelte +19 -0
  67. package/src/lib/components/HeatMap.svelte +64 -0
  68. package/src/lib/components/HexbinChart.svelte +100 -0
  69. package/src/lib/components/Histogram.svelte +93 -0
  70. package/src/lib/components/HorizonChart.svelte +71 -0
  71. package/src/lib/components/HoverCard.svelte +45 -0
  72. package/src/lib/components/IPInput.svelte +103 -0
  73. package/src/lib/components/IcicleChart.svelte +87 -0
  74. package/src/lib/components/IconButton.svelte +19 -0
  75. package/src/lib/components/InlineEdit.svelte +72 -0
  76. package/src/lib/components/Input.svelte +18 -0
  77. package/src/lib/components/InputGroup.svelte +14 -0
  78. package/src/lib/components/JsonViewer.svelte +15 -0
  79. package/src/lib/components/JsonViewerNode.svelte +59 -0
  80. package/src/lib/components/KVTable.svelte +18 -0
  81. package/src/lib/components/KanbanBoard.svelte +141 -0
  82. package/src/lib/components/Kbd.svelte +16 -0
  83. package/src/lib/components/LineChart.svelte +67 -0
  84. package/src/lib/components/LogViewer.svelte +84 -0
  85. package/src/lib/components/LollipopChart.svelte +79 -0
  86. package/src/lib/components/MACInput.svelte +98 -0
  87. package/src/lib/components/MarimekkoChart.svelte +108 -0
  88. package/src/lib/components/MatrixChart.svelte +55 -0
  89. package/src/lib/components/Menubar.svelte +131 -0
  90. package/src/lib/components/MirroredBarChart.svelte +120 -0
  91. package/src/lib/components/Modal.svelte +90 -0
  92. package/src/lib/components/MultiSelect.svelte +165 -0
  93. package/src/lib/components/NightingaleChart.svelte +63 -0
  94. package/src/lib/components/NotificationPanel.svelte +170 -0
  95. package/src/lib/components/NumberInput.svelte +93 -0
  96. package/src/lib/components/OTPInput.svelte +94 -0
  97. package/src/lib/components/Pagination.svelte +58 -0
  98. package/src/lib/components/ParallelCoordinates.svelte +98 -0
  99. package/src/lib/components/ParetoChart.svelte +88 -0
  100. package/src/lib/components/PasswordInput.svelte +62 -0
  101. package/src/lib/components/PieChart.svelte +91 -0
  102. package/src/lib/components/Pill.svelte +12 -0
  103. package/src/lib/components/PolarHeatmap.svelte +85 -0
  104. package/src/lib/components/Popover.svelte +89 -0
  105. package/src/lib/components/ProgressBar.svelte +29 -0
  106. package/src/lib/components/PunchCard.svelte +73 -0
  107. package/src/lib/components/QuadrantChart.svelte +122 -0
  108. package/src/lib/components/RadarChart.svelte +71 -0
  109. package/src/lib/components/RadialBarChart.svelte +76 -0
  110. package/src/lib/components/RadioGroup.svelte +48 -0
  111. package/src/lib/components/RangeSlider.svelte +68 -0
  112. package/src/lib/components/RankedList.svelte +49 -0
  113. package/src/lib/components/ResizablePanel.svelte +77 -0
  114. package/src/lib/components/RidgelinePlot.svelte +103 -0
  115. package/src/lib/components/RowToggle.svelte +19 -0
  116. package/src/lib/components/SankeyDiagram.svelte +100 -0
  117. package/src/lib/components/ScatterPlot.svelte +54 -0
  118. package/src/lib/components/SearchBox.svelte +11 -0
  119. package/src/lib/components/SegmentedControl.svelte +51 -0
  120. package/src/lib/components/Select.svelte +110 -0
  121. package/src/lib/components/SelectionToolbar.svelte +42 -0
  122. package/src/lib/components/Sidebar.svelte +44 -0
  123. package/src/lib/components/Signal.svelte +12 -0
  124. package/src/lib/components/Skeleton.svelte +23 -0
  125. package/src/lib/components/SkipLink.svelte +9 -0
  126. package/src/lib/components/Slider.svelte +48 -0
  127. package/src/lib/components/SlopeChart.svelte +91 -0
  128. package/src/lib/components/SortHeader.svelte +19 -0
  129. package/src/lib/components/SortableList.svelte +87 -0
  130. package/src/lib/components/Sparkline.svelte +32 -0
  131. package/src/lib/components/SparklineMatrix.svelte +84 -0
  132. package/src/lib/components/Spinner.svelte +6 -0
  133. package/src/lib/components/SplitButton.svelte +102 -0
  134. package/src/lib/components/Spoiler.svelte +41 -0
  135. package/src/lib/components/StackedBarChart.svelte +83 -0
  136. package/src/lib/components/StackedProgress.svelte +32 -0
  137. package/src/lib/components/StarRating.svelte +64 -0
  138. package/src/lib/components/Stat.svelte +24 -0
  139. package/src/lib/components/StatusIndicator.svelte +10 -0
  140. package/src/lib/components/StepChart.svelte +74 -0
  141. package/src/lib/components/Stepper.svelte +40 -0
  142. package/src/lib/components/StreamGraph.svelte +93 -0
  143. package/src/lib/components/StripeChart.svelte +52 -0
  144. package/src/lib/components/SunburstChart.svelte +80 -0
  145. package/src/lib/components/SwitchPortGrid.svelte +45 -0
  146. package/src/lib/components/TabPanel.svelte +17 -0
  147. package/src/lib/components/Tabs.svelte +45 -0
  148. package/src/lib/components/Tag.svelte +19 -0
  149. package/src/lib/components/TagInput.svelte +92 -0
  150. package/src/lib/components/Textarea.svelte +31 -0
  151. package/src/lib/components/ThresholdAreaChart.svelte +72 -0
  152. package/src/lib/components/TimePicker.svelte +116 -0
  153. package/src/lib/components/TimeRange.svelte +104 -0
  154. package/src/lib/components/Timeline.svelte +32 -0
  155. package/src/lib/components/Toast.svelte +52 -0
  156. package/src/lib/components/Toggle.svelte +23 -0
  157. package/src/lib/components/ToggleGroup.svelte +32 -0
  158. package/src/lib/components/Tooltip.svelte +16 -0
  159. package/src/lib/components/Topbar.svelte +63 -0
  160. package/src/lib/components/TransferList.svelte +184 -0
  161. package/src/lib/components/TreeItem.svelte +61 -0
  162. package/src/lib/components/TreeMap.svelte +90 -0
  163. package/src/lib/components/TreeView.svelte +130 -0
  164. package/src/lib/components/UptimeTimeline.svelte +86 -0
  165. package/src/lib/components/VennDiagram.svelte +91 -0
  166. package/src/lib/components/ViolinPlot.svelte +114 -0
  167. package/src/lib/components/VirtualList.svelte +38 -0
  168. package/src/lib/components/WaffleChart.svelte +68 -0
  169. package/src/lib/components/WaterfallChart.svelte +96 -0
  170. package/src/lib/components/WordCloud.svelte +103 -0
  171. package/src/lib/icons/AlarmIcon.svelte +5 -0
  172. package/src/lib/icons/BellIcon.svelte +6 -0
  173. package/src/lib/icons/CaretIcon.svelte +8 -0
  174. package/src/lib/icons/ClientsIcon.svelte +5 -0
  175. package/src/lib/icons/CloseIcon.svelte +6 -0
  176. package/src/lib/icons/DashboardIcon.svelte +7 -0
  177. package/src/lib/icons/DevicesIcon.svelte +5 -0
  178. package/src/lib/icons/DownloadIcon.svelte +6 -0
  179. package/src/lib/icons/HelpIcon.svelte +7 -0
  180. package/src/lib/icons/InfraIcon.svelte +7 -0
  181. package/src/lib/icons/IntegrationsIcon.svelte +5 -0
  182. package/src/lib/icons/LogsIcon.svelte +6 -0
  183. package/src/lib/icons/PlusIcon.svelte +6 -0
  184. package/src/lib/icons/PortsIcon.svelte +6 -0
  185. package/src/lib/icons/SearchIcon.svelte +10 -0
  186. package/src/lib/icons/SecurityIcon.svelte +4 -0
  187. package/src/lib/icons/SettingsIcon.svelte +5 -0
  188. package/src/lib/icons/TopologyIcon.svelte +7 -0
  189. package/src/lib/icons/UpdatesIcon.svelte +6 -0
  190. package/src/lib/icons/VpnIcon.svelte +5 -0
  191. package/src/lib/icons/WifiIcon.svelte +5 -0
  192. package/src/lib/icons/WirelessIcon.svelte +4 -0
  193. package/src/lib/icons.ts +22 -0
  194. package/src/lib/index.ts +275 -0
  195. package/src/lib/styles.css +2 -0
@@ -0,0 +1,249 @@
1
+ <script context="module" lang="ts">
2
+ let counter = 0;
3
+ const DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
4
+ const MONTHS = [
5
+ 'January', 'February', 'March', 'April', 'May', 'June',
6
+ 'July', 'August', 'September', 'October', 'November', 'December',
7
+ ];
8
+
9
+ function isSameDay(a: Date, b: Date) {
10
+ return a.getFullYear() === b.getFullYear() &&
11
+ a.getMonth() === b.getMonth() &&
12
+ a.getDate() === b.getDate();
13
+ }
14
+
15
+ function formatDate(d: Date) {
16
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
17
+ const dd = String(d.getDate()).padStart(2, '0');
18
+ return `${mm}/${dd}/${d.getFullYear()}`;
19
+ }
20
+
21
+ function buildGrid(year: number, month: number): Date[] {
22
+ const first = new Date(year, month, 1);
23
+ const last = new Date(year, month + 1, 0);
24
+ const grid: Date[] = [];
25
+ for (let i = first.getDay() - 1; i >= 0; i--) {
26
+ grid.push(new Date(year, month, -i));
27
+ }
28
+ for (let d = 1; d <= last.getDate(); d++) {
29
+ grid.push(new Date(year, month, d));
30
+ }
31
+ while (grid.length < 42) {
32
+ grid.push(new Date(year, month + 1, grid.length - first.getDay() - last.getDate() + 1));
33
+ }
34
+ return grid;
35
+ }
36
+ </script>
37
+
38
+ <script lang="ts">
39
+ import { createEventDispatcher, onMount, onDestroy } from 'svelte';
40
+
41
+ export interface DateRange {
42
+ start: Date | null;
43
+ end: Date | null;
44
+ }
45
+
46
+ export let value: DateRange = { start: null, end: null };
47
+ export let placeholder: string = 'Select date range';
48
+ export let disabled: boolean = false;
49
+
50
+ const dispatch = createEventDispatcher<{ change: DateRange }>();
51
+ const uid = `dash-ui-drp-${++counter}`;
52
+ const dlgId = `${uid}-dlg`;
53
+ const today = new Date();
54
+
55
+ let open = false;
56
+ let picking: Date | null = null;
57
+ let hoverDate: Date | null = null;
58
+ let viewYear = today.getFullYear();
59
+ let viewMonth = today.getMonth();
60
+ let triggerEl: HTMLButtonElement;
61
+ let dlgEl: HTMLDivElement;
62
+
63
+ $: rightMonth = viewMonth === 11 ? 0 : viewMonth + 1;
64
+ $: rightYear = viewMonth === 11 ? viewYear + 1 : viewYear;
65
+ $: leftGrid = buildGrid(viewYear, viewMonth);
66
+ $: rightGrid = buildGrid(rightYear, rightMonth);
67
+
68
+ function prevMonth() {
69
+ if (viewMonth === 0) { viewYear -= 1; viewMonth = 11; }
70
+ else viewMonth -= 1;
71
+ }
72
+
73
+ function nextMonth() {
74
+ if (viewMonth === 11) { viewYear += 1; viewMonth = 0; }
75
+ else viewMonth += 1;
76
+ }
77
+
78
+ function handleDayClick(day: Date) {
79
+ if (!picking) {
80
+ picking = day;
81
+ dispatch('change', { start: day, end: null });
82
+ } else {
83
+ const [s, e] = day < picking ? [day, picking] : [picking, day];
84
+ picking = null;
85
+ hoverDate = null;
86
+ dispatch('change', { start: s, end: e });
87
+ open = false;
88
+ triggerEl?.focus();
89
+ }
90
+ }
91
+
92
+ function isInRange(day: Date): boolean {
93
+ const start = picking ?? value.start;
94
+ const end = picking ? hoverDate : value.end;
95
+ if (!start || !end) return false;
96
+ const lo = start < end ? start : end;
97
+ const hi = start < end ? end : start;
98
+ return day > lo && day < hi;
99
+ }
100
+
101
+ function isStart(day: Date): boolean {
102
+ const start = picking ?? value.start;
103
+ return start ? isSameDay(day, start) : false;
104
+ }
105
+
106
+ function isEnd(day: Date): boolean {
107
+ if (picking) return hoverDate ? isSameDay(day, hoverDate) : false;
108
+ return value.end ? isSameDay(day, value.end) : false;
109
+ }
110
+
111
+ function formatRange(): string | null {
112
+ if (!value.start) return null;
113
+ if (!value.end) return `${formatDate(value.start)} --`;
114
+ return `${formatDate(value.start)} -- ${formatDate(value.end)}`;
115
+ }
116
+
117
+ function handleOutside(e: MouseEvent) {
118
+ if (open && !dlgEl?.contains(e.target as Node) && !triggerEl?.contains(e.target as Node)) {
119
+ open = false;
120
+ picking = null;
121
+ }
122
+ }
123
+
124
+ onMount(() => document.addEventListener('mousedown', handleOutside));
125
+ onDestroy(() => document.removeEventListener('mousedown', handleOutside));
126
+ </script>
127
+
128
+ <div class="drp-root">
129
+ <button
130
+ bind:this={triggerEl}
131
+ type="button"
132
+ class="btn dp-trigger"
133
+ aria-haspopup="dialog"
134
+ aria-expanded={open}
135
+ aria-controls={open ? dlgId : undefined}
136
+ {disabled}
137
+ on:click={() => { if (open) { open = false; picking = null; } else open = true; }}
138
+ >
139
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
140
+ <rect x="1" y="2" width="12" height="11" rx="1.5" />
141
+ <path d="M1 6h12M4 1v2M10 1v2" />
142
+ </svg>
143
+ {formatRange() ?? placeholder}
144
+ </button>
145
+
146
+ {#if open}
147
+ <div
148
+ id={dlgId}
149
+ bind:this={dlgEl}
150
+ role="dialog"
151
+ aria-modal="true"
152
+ aria-label="Select date range"
153
+ class="drp-cal"
154
+ on:keydown={(e) => {
155
+ if (e.key === 'Escape') { e.preventDefault(); open = false; picking = null; triggerEl?.focus(); }
156
+ }}
157
+ >
158
+ <div class="drp-months">
159
+ <!-- Left calendar -->
160
+ <div class="drp-month">
161
+ <div class="dp-header">
162
+ <button type="button" class="dp-nav" on:click={prevMonth} aria-label="Previous month">
163
+ <svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
164
+ <path d="M7 1L2 6l5 5" />
165
+ </svg>
166
+ </button>
167
+ <span class="dp-month-label" aria-live="polite">{MONTHS[viewMonth]} {viewYear}</span>
168
+ <span style="width:28px;display:inline-block;" aria-hidden="true"></span>
169
+ </div>
170
+ <table role="grid" class="dp-grid" aria-label="{MONTHS[viewMonth]} {viewYear}">
171
+ <thead>
172
+ <tr>{#each DAYS as d}<th scope="col" abbr={d}>{d}</th>{/each}</tr>
173
+ </thead>
174
+ <tbody>
175
+ {#each Array(6) as _, row}
176
+ <tr>
177
+ {#each Array(7) as _, col}
178
+ {@const idx = row * 7 + col}
179
+ {@const day = leftGrid[idx]}
180
+ {@const outside = day.getMonth() !== viewMonth}
181
+ {@const isTodayDay = isSameDay(day, today)}
182
+ {@const start = isStart(day)}
183
+ {@const end = isEnd(day)}
184
+ {@const inRange = isInRange(day)}
185
+ <td role="gridcell" aria-selected={start || end}>
186
+ <button
187
+ type="button"
188
+ class="dp-day{outside ? ' dp-day--outside' : ''}{isTodayDay && !start && !end ? ' dp-day--today' : ''}{start ? ' dp-day--start' : ''}{end ? ' dp-day--end' : ''}{inRange ? ' dp-day--in-range' : ''}"
189
+ aria-label={day.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
190
+ on:click={() => handleDayClick(day)}
191
+ on:mouseenter={() => { if (picking) hoverDate = day; }}
192
+ on:mouseleave={() => { if (picking) hoverDate = null; }}
193
+ >{day.getDate()}</button>
194
+ </td>
195
+ {/each}
196
+ </tr>
197
+ {/each}
198
+ </tbody>
199
+ </table>
200
+ </div>
201
+ <!-- Right calendar -->
202
+ <div class="drp-month">
203
+ <div class="dp-header">
204
+ <span style="width:28px;display:inline-block;" aria-hidden="true"></span>
205
+ <span class="dp-month-label" aria-live="polite">{MONTHS[rightMonth]} {rightYear}</span>
206
+ <button type="button" class="dp-nav" on:click={nextMonth} aria-label="Next month">
207
+ <svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
208
+ <path d="M1 1l5 5-5 5" />
209
+ </svg>
210
+ </button>
211
+ </div>
212
+ <table role="grid" class="dp-grid" aria-label="{MONTHS[rightMonth]} {rightYear}">
213
+ <thead>
214
+ <tr>{#each DAYS as d}<th scope="col" abbr={d}>{d}</th>{/each}</tr>
215
+ </thead>
216
+ <tbody>
217
+ {#each Array(6) as _, row}
218
+ <tr>
219
+ {#each Array(7) as _, col}
220
+ {@const idx = row * 7 + col}
221
+ {@const day = rightGrid[idx]}
222
+ {@const outside = day.getMonth() !== rightMonth}
223
+ {@const isTodayDay = isSameDay(day, today)}
224
+ {@const start = isStart(day)}
225
+ {@const end = isEnd(day)}
226
+ {@const inRange = isInRange(day)}
227
+ <td role="gridcell" aria-selected={start || end}>
228
+ <button
229
+ type="button"
230
+ class="dp-day{outside ? ' dp-day--outside' : ''}{isTodayDay && !start && !end ? ' dp-day--today' : ''}{start ? ' dp-day--start' : ''}{end ? ' dp-day--end' : ''}{inRange ? ' dp-day--in-range' : ''}"
231
+ aria-label={day.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
232
+ on:click={() => handleDayClick(day)}
233
+ on:mouseenter={() => { if (picking) hoverDate = day; }}
234
+ on:mouseleave={() => { if (picking) hoverDate = null; }}
235
+ >{day.getDate()}</button>
236
+ </td>
237
+ {/each}
238
+ </tr>
239
+ {/each}
240
+ </tbody>
241
+ </table>
242
+ </div>
243
+ </div>
244
+ {#if picking}
245
+ <p class="drp-hint" aria-live="polite">Pick the end date</p>
246
+ {/if}
247
+ </div>
248
+ {/if}
249
+ </div>
@@ -0,0 +1,128 @@
1
+ <script lang="ts">
2
+ export interface DendrogramNode {
3
+ id: string;
4
+ label: string;
5
+ children?: DendrogramNode[];
6
+ color?: string;
7
+ }
8
+
9
+ interface PlacedNode {
10
+ id: string;
11
+ label: string;
12
+ x: number;
13
+ y: number;
14
+ color: string;
15
+ isLeaf: boolean;
16
+ children: PlacedNode[];
17
+ }
18
+
19
+ const CHILD_COLORS = ['#006FFF', '#00C8C8', '#F5A623', '#7FB6FF', '#A878F5', '#F56342'];
20
+ const PAD_L = 12;
21
+ const PAD_V = 10;
22
+ const LABEL_LEAF_W = 130;
23
+
24
+ function countLeaves(n: DendrogramNode): number {
25
+ if (!n.children?.length) return 1;
26
+ return n.children.reduce((s, c) => s + countLeaves(c), 0);
27
+ }
28
+
29
+ function treeDepth(n: DendrogramNode): number {
30
+ if (!n.children?.length) return 0;
31
+ return 1 + Math.max(...n.children.map(treeDepth));
32
+ }
33
+
34
+ function placeNodes(
35
+ n: DendrogramNode,
36
+ depth: number,
37
+ cw: number,
38
+ rh: number,
39
+ counter: { v: number },
40
+ color: string,
41
+ ): PlacedNode {
42
+ const nodeColor = n.color ?? color;
43
+ const isLeaf = !n.children?.length;
44
+ if (isLeaf) {
45
+ const y = counter.v * rh + rh / 2;
46
+ counter.v++;
47
+ return { id: n.id, label: n.label, x: depth * cw, y, color: nodeColor, isLeaf: true, children: [] };
48
+ }
49
+ const children = n.children!.map((c, i) =>
50
+ placeNodes(c, depth + 1, cw, rh, counter, CHILD_COLORS[i % CHILD_COLORS.length]),
51
+ );
52
+ const y = (children[0].y + children[children.length - 1].y) / 2;
53
+ return { id: n.id, label: n.label, x: depth * cw, y, color: nodeColor, isLeaf: false, children };
54
+ }
55
+
56
+ function iterNodes(n: PlacedNode): PlacedNode[] {
57
+ return [n, ...n.children.flatMap(iterNodes)];
58
+ }
59
+
60
+ function iterLinks(n: PlacedNode): Array<[PlacedNode, PlacedNode]> {
61
+ return n.children.flatMap((c) => [[n, c] as [PlacedNode, PlacedNode], ...iterLinks(c)]);
62
+ }
63
+
64
+ function compute(root: DendrogramNode, colWidth: number, rowHeight: number) {
65
+ const counter = { v: 0 };
66
+ const raw = placeNodes(root, 0, colWidth, rowHeight, counter, '#8B909E');
67
+ const leafCount = counter.v;
68
+ const placed: PlacedNode = {
69
+ ...raw,
70
+ x: raw.x + PAD_L,
71
+ y: raw.y + PAD_V,
72
+ children: shiftChildren(raw.children, PAD_L, PAD_V),
73
+ };
74
+ const maxDepth = treeDepth(root);
75
+ return {
76
+ placed,
77
+ svgW: PAD_L + (maxDepth + 1) * colWidth + LABEL_LEAF_W,
78
+ svgH: leafCount * rowHeight + PAD_V * 2,
79
+ nodes: iterNodes(placed),
80
+ links: iterLinks(placed),
81
+ };
82
+ }
83
+
84
+ function shiftChildren(children: PlacedNode[], dx: number, dy: number): PlacedNode[] {
85
+ return children.map((c) => ({
86
+ ...c,
87
+ x: c.x + dx,
88
+ y: c.y + dy,
89
+ children: shiftChildren(c.children, dx, dy),
90
+ }));
91
+ }
92
+
93
+ export let root: DendrogramNode;
94
+ export let colWidth: number = 130;
95
+ export let rowHeight: number = 28;
96
+ export let ariaLabel: string = 'Dendrogram';
97
+
98
+ $: ({ svgW, svgH, nodes, links } = compute(root, colWidth, rowHeight));
99
+ </script>
100
+
101
+ <div role="img" aria-label={ariaLabel} style="width:100%;overflow-x:auto">
102
+ <svg
103
+ viewBox="0 0 {svgW} {svgH}"
104
+ style="width:100%;height:{svgH}px;display:block"
105
+ aria-hidden="true"
106
+ focusable="false"
107
+ >
108
+ {#each links as [parent, child], i (i)}
109
+ {@const mx = (parent.x + child.x) / 2}
110
+ <path
111
+ d="M{parent.x},{parent.y} C{mx},{parent.y} {mx},{child.y} {child.x},{child.y}"
112
+ fill="none"
113
+ stroke="rgba(255,255,255,0.13)"
114
+ stroke-width="1.5"
115
+ />
116
+ {/each}
117
+ {#each nodes as n (n.id)}
118
+ <circle cx={n.x} cy={n.y} r={4} fill={n.color} />
119
+ {#if n.isLeaf}
120
+ <text x={n.x + 10} y={n.y + 4} fill="#C8CDD9" font-size="10" font-family="inherit">{n.label}</text>
121
+ {:else if n.x === PAD_L}
122
+ <text x={n.x + 10} y={n.y + 4} fill="#8B909E" font-size="9" font-family="inherit">{n.label}</text>
123
+ {:else}
124
+ <text x={n.x} y={n.y - 7} fill="#8B909E" font-size="9" font-family="inherit" text-anchor="middle">{n.label}</text>
125
+ {/if}
126
+ {/each}
127
+ </svg>
128
+ </div>
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ export interface DonutSegment { label: string; value: number; color: string }
3
+ export let size: number = 96;
4
+ export let segments: DonutSegment[] = [];
5
+ export let centerValue: string | number;
6
+ export let centerLabel: string;
7
+ export let trackColor: string = 'rgba(255,255,255,0.06)';
8
+ export let ariaLabel: string | undefined = undefined;
9
+
10
+ $: total = segments.reduce((s, x) => s + x.value, 0) || 1;
11
+ $: r = (size - 20) / 2;
12
+ $: c = 2 * Math.PI * r;
13
+ $: segDesc = segments.map((s) => `${s.label} ${Math.round((s.value / total) * 100)}%`).join(', ');
14
+ $: label = ariaLabel ?? `${centerValue} ${centerLabel}${segDesc ? `: ${segDesc}` : ''}`;
15
+ $: arcs = (() => {
16
+ let cum = 0;
17
+ return segments.map((seg) => {
18
+ const frac = seg.value / total;
19
+ const offset = c * (1 - cum - frac);
20
+ const rotate = cum * 360;
21
+ cum += frac;
22
+ return { ...seg, offset, rotate };
23
+ });
24
+ })();
25
+ </script>
26
+
27
+ <div role="img" aria-label={label} style="position:relative;width:{size}px;height:{size}px;flex-shrink:0;">
28
+ <svg width={size} height={size} aria-hidden="true" focusable="false" style="transform:rotate(-90deg);">
29
+ <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={trackColor} stroke-width="10" />
30
+ {#each arcs as a (a.label)}
31
+ <circle
32
+ cx={size / 2}
33
+ cy={size / 2}
34
+ r={r}
35
+ fill="none"
36
+ stroke={a.color}
37
+ stroke-width="10"
38
+ stroke-dasharray={c}
39
+ stroke-dashoffset={a.offset}
40
+ stroke-linecap="round"
41
+ transform="rotate({a.rotate} {size / 2} {size / 2})"
42
+ />
43
+ {/each}
44
+ </svg>
45
+ <div aria-hidden="true" style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;">
46
+ <div style="font-size:20px;font-weight:600;letter-spacing:-0.01em;font-variant-numeric:tabular-nums;">{centerValue}</div>
47
+ <div style="font-size:10px;color:#6E7079;">{centerLabel}</div>
48
+ </div>
49
+ </div>
@@ -0,0 +1,121 @@
1
+ <script lang="ts">
2
+ export interface DotPlotItem {
3
+ label: string;
4
+ value: number;
5
+ compare?: number;
6
+ color?: string;
7
+ }
8
+
9
+ export let items: DotPlotItem[] = [];
10
+ export let min: number = 0;
11
+ export let max: number | undefined = undefined;
12
+ export let unit: string = '';
13
+ export let valueLegend: string | undefined = undefined;
14
+ export let compareLegend: string | undefined = undefined;
15
+ export let ariaLabel: string = 'Dot plot';
16
+
17
+ const VW = 380;
18
+ const LABEL_W = 132;
19
+ const VALUE_W = 32;
20
+ const PAD_R = 6;
21
+ const TRACK_W = VW - LABEL_W - VALUE_W - PAD_R;
22
+ const DOT_R = 5;
23
+ const ROW_H = 28;
24
+ const PAD_T = 8;
25
+ const AXIS_H = 18;
26
+ const LEG_H = 18;
27
+ const TICKS = 5;
28
+
29
+ $: hasCompare = items.some((it) => it.compare !== undefined);
30
+ $: maxVal = max ?? Math.max(...items.map((it) => Math.max(it.value, it.compare ?? 0)));
31
+ $: range = Math.max(maxVal - min, 1);
32
+ $: svgH =
33
+ PAD_T +
34
+ items.length * ROW_H +
35
+ AXIS_H +
36
+ (hasCompare && (valueLegend || compareLegend) ? LEG_H + 4 : 0);
37
+ $: ticks = Array.from({ length: TICKS }, (_, i) => min + (i / (TICKS - 1)) * range);
38
+ $: legY = PAD_T + items.length * ROW_H + AXIS_H + 4;
39
+
40
+ function tx(v: number): number {
41
+ return LABEL_W + ((v - min) / range) * TRACK_W;
42
+ }
43
+
44
+ function fmtTick(t: number): string {
45
+ return (Number.isInteger(t) ? t : t.toFixed(1)) + unit;
46
+ }
47
+ </script>
48
+
49
+ <div role="img" aria-label={ariaLabel} style="width:100%;">
50
+ <svg
51
+ viewBox="0 0 {VW} {svgH}"
52
+ style="width:100%;height:auto;display:block;"
53
+ aria-hidden="true"
54
+ focusable="false"
55
+ >
56
+ {#each ticks as t, i (i)}
57
+ <g>
58
+ <line
59
+ x1={tx(t)} y1={PAD_T}
60
+ x2={tx(t)} y2={PAD_T + items.length * ROW_H}
61
+ stroke="rgba(255,255,255,0.06)" stroke-width="1"
62
+ />
63
+ <text
64
+ x={tx(t)} y={PAD_T + items.length * ROW_H + 13}
65
+ fill="#6E7079" font-size="9" text-anchor="middle" font-family="inherit"
66
+ >{fmtTick(t)}</text>
67
+ </g>
68
+ {/each}
69
+
70
+ {#each items as item, i (item.label)}
71
+ {@const cy = PAD_T + i * ROW_H + ROW_H / 2}
72
+ {@const cx = tx(item.value)}
73
+ {@const ccx = item.compare !== undefined ? tx(item.compare) : null}
74
+ {@const color = item.color ?? '#006FFF'}
75
+ <g>
76
+ <text
77
+ x={LABEL_W - 8} y={cy + 4}
78
+ fill="#A4A7B5" font-size="10" text-anchor="end" font-family="inherit"
79
+ >{item.label}</text>
80
+
81
+ <line
82
+ x1={LABEL_W} y1={cy} x2={LABEL_W + TRACK_W} y2={cy}
83
+ stroke="rgba(255,255,255,0.06)" stroke-width="1"
84
+ />
85
+
86
+ {#if ccx !== null}
87
+ <line
88
+ x1={Math.min(cx, ccx)} y1={cy}
89
+ x2={Math.max(cx, ccx)} y2={cy}
90
+ stroke={color} stroke-width="1.5" opacity="0.35"
91
+ />
92
+ <circle cx={ccx} cy={cy} r={DOT_R - 1} fill="none" stroke="#A4A7B5" stroke-width="1.5" />
93
+ {/if}
94
+
95
+ <circle cx={cx} cy={cy} r={DOT_R} fill={color} />
96
+
97
+ <text
98
+ x={LABEL_W + TRACK_W + 6} y={cy + 4}
99
+ fill="#A4A7B5" font-size="9" font-family="inherit"
100
+ >{item.value}{unit}</text>
101
+ </g>
102
+ {/each}
103
+
104
+ {#if hasCompare && (valueLegend || compareLegend)}
105
+ {#if valueLegend}
106
+ <circle cx={LABEL_W} cy={legY + 5} r="4" fill="#006FFF" />
107
+ <text x={LABEL_W + 10} y={legY + 9} fill="#A4A7B5" font-size="9" font-family="inherit">{valueLegend}</text>
108
+ {/if}
109
+ {#if compareLegend}
110
+ <circle
111
+ cx={LABEL_W + (valueLegend ? 80 : 0)} cy={legY + 5}
112
+ r="3" fill="none" stroke="#A4A7B5" stroke-width="1.5"
113
+ />
114
+ <text
115
+ x={LABEL_W + (valueLegend ? 90 : 10)} y={legY + 9}
116
+ fill="#A4A7B5" font-size="9" font-family="inherit"
117
+ >{compareLegend}</text>
118
+ {/if}
119
+ {/if}
120
+ </svg>
121
+ </div>
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import IconButton from './IconButton.svelte';
4
+ import CloseIcon from '../icons/CloseIcon.svelte';
5
+ export let open: boolean = false;
6
+ export let title: string;
7
+ const titleId = `drawer-title-${Math.random().toString(36).slice(2, 9)}`;
8
+ const FOCUSABLE =
9
+ 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
10
+ let panelEl: HTMLDivElement;
11
+ let prev: HTMLElement | null = null;
12
+ let wasOpen = false;
13
+ $: if (open && !wasOpen) {
14
+ prev = typeof document !== 'undefined' ? (document.activeElement as HTMLElement | null) : null;
15
+ wasOpen = true;
16
+ tick().then(() => {
17
+ const first = panelEl?.querySelector<HTMLElement>(FOCUSABLE);
18
+ (first ?? panelEl)?.focus();
19
+ });
20
+ } else if (!open && wasOpen) {
21
+ wasOpen = false;
22
+ prev?.focus?.();
23
+ }
24
+ function onKeydown(e: KeyboardEvent) {
25
+ if (!open) return;
26
+ if (e.key === 'Escape') { open = false; return; }
27
+ if (e.key !== 'Tab' || !panelEl) return;
28
+ const items = Array.from(panelEl.querySelectorAll<HTMLElement>(FOCUSABLE));
29
+ if (!items.length) { e.preventDefault(); return; }
30
+ const f = items[0]!, l = items[items.length - 1]!;
31
+ const active = document.activeElement;
32
+ if (e.shiftKey && active === f) { e.preventDefault(); l.focus(); }
33
+ else if (!e.shiftKey && active === l) { e.preventDefault(); f.focus(); }
34
+ }
35
+ </script>
36
+
37
+ <svelte:window on:keydown={onKeydown} />
38
+
39
+ <div
40
+ class="drawer-overlay {open ? 'show' : ''}"
41
+ on:click={() => (open = false)}
42
+ aria-hidden="true"
43
+ role="presentation"
44
+ />
45
+ <div
46
+ bind:this={panelEl}
47
+ class="drawer-panel {open ? 'show' : ''}"
48
+ role="dialog"
49
+ aria-modal="true"
50
+ aria-labelledby={titleId}
51
+ tabindex="-1"
52
+ >
53
+ <div class="drawer-h">
54
+ <h2 id={titleId}>{title}</h2>
55
+ <IconButton title="Close" on:click={() => (open = false)}>
56
+ <CloseIcon />
57
+ </IconButton>
58
+ </div>
59
+ <div class="drawer-b"><slot /></div>
60
+ </div>
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ export interface DualAxisSeries {
3
+ label: string;
4
+ color: string;
5
+ values: number[];
6
+ }
7
+
8
+ export let bars: DualAxisSeries;
9
+ export let line: DualAxisSeries;
10
+ export let labels: string[] = [];
11
+ export let height = 160;
12
+ export let ariaLabel = 'Dual axis chart';
13
+
14
+ const VW = 400;
15
+ const PAD = { t: 16, r: 44, b: 28, l: 44 };
16
+
17
+ function fmtTick(n: number): string {
18
+ if (n >= 1000) return `${Math.round(n / 1000)}k`;
19
+ return String(Math.round(n));
20
+ }
21
+
22
+ $: chartW = VW - PAD.l - PAD.r;
23
+ $: chartH = height - PAD.t - PAD.b;
24
+ $: bot = PAD.t + chartH;
25
+ $: n = bars.values.length;
26
+ $: gw = n > 0 ? chartW / n : chartW;
27
+ $: barW = gw * 0.55;
28
+ $: barMax = Math.max(...bars.values, 1);
29
+ $: lineMin = Math.min(...line.values);
30
+ $: lineMax = Math.max(...line.values, lineMin + 1);
31
+ $: lineRange = lineMax - lineMin || 1;
32
+ $: gridLines = [0, 0.25, 0.5, 0.75, 1].map((f) => PAD.t + (1 - f) * chartH);
33
+ $: leftTicks = [0, 0.5, 1].map((f) => ({ y: PAD.t + (1 - f) * chartH, label: fmtTick(barMax * f) }));
34
+ $: rightTicks = [0, 0.5, 1].map((f) => ({ y: PAD.t + (1 - f) * chartH, label: fmtTick(lineMin + lineRange * f) }));
35
+ $: barRects = bars.values.map((v, i) => ({
36
+ x: (PAD.l + i * gw + gw / 2 - barW / 2).toFixed(1),
37
+ y: (bot - (v / barMax) * chartH).toFixed(1),
38
+ w: barW.toFixed(1),
39
+ h: ((v / barMax) * chartH).toFixed(1),
40
+ }));
41
+ $: linePoints = line.values.map((v, i) =>
42
+ `${(PAD.l + (n > 1 ? (i / (n - 1)) * chartW : chartW / 2)).toFixed(1)},${(PAD.t + chartH - ((v - lineMin) / lineRange) * chartH).toFixed(1)}`
43
+ );
44
+ $: linePath = linePoints.length ? `M ${linePoints.join(' L ')}` : '';
45
+ $: lineArea = linePoints.length
46
+ ? `M ${PAD.l.toFixed(1)},${bot.toFixed(1)} L ${linePoints.join(' L ')} L ${(PAD.l + (n > 1 ? chartW : 0)).toFixed(1)},${bot.toFixed(1)} Z`
47
+ : '';
48
+ </script>
49
+
50
+ <div role="img" aria-label={ariaLabel} style="width:100%;">
51
+ <svg viewBox="0 0 {VW} {height}" style="width:100%;height:auto;display:block;" aria-hidden="true" focusable="false">
52
+ {#each gridLines as y}
53
+ <line x1={PAD.l} y1={y} x2={VW - PAD.r} y2={y} stroke="rgba(255,255,255,0.06)" stroke-width="1" />
54
+ {/each}
55
+ {#each leftTicks as t}
56
+ <text x={PAD.l - 4} y={t.y + 4} text-anchor="end" fill={bars.color} font-size="9" font-family="inherit" fill-opacity="0.8">{t.label}</text>
57
+ {/each}
58
+ {#each rightTicks as t}
59
+ <text x={VW - PAD.r + 4} y={t.y + 4} text-anchor="start" fill={line.color} font-size="9" font-family="inherit" fill-opacity="0.8">{t.label}</text>
60
+ {/each}
61
+ {#each barRects as b}
62
+ <rect x={b.x} y={b.y} width={b.w} height={b.h} fill={bars.color} fill-opacity="0.75" rx="2" />
63
+ {/each}
64
+ {#if lineArea}<path d={lineArea} fill={line.color} fill-opacity="0.12" />{/if}
65
+ {#if linePath}<path d={linePath} fill="none" stroke={line.color} stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />{/if}
66
+ {#each labels as lbl, i}
67
+ <text x={(PAD.l + (i + 0.5) * gw).toFixed(1)} y={height - 4} text-anchor="middle" fill="#6E7079" font-size="9" font-family="inherit">{lbl}</text>
68
+ {/each}
69
+ </svg>
70
+ <div style="display:flex;gap:16px;justify-content:center;font-size:11px;color:#A4A7B5;margin-top:4px;">
71
+ <span style="display:flex;align-items:center;gap:4px;">
72
+ <span style="display:inline-block;width:10px;height:10px;background:{bars.color};border-radius:2px;opacity:0.75;"></span>
73
+ {bars.label}
74
+ </span>
75
+ <span style="display:flex;align-items:center;gap:4px;">
76
+ <span style="display:inline-block;width:16px;height:2px;background:{line.color};border-radius:1px;"></span>
77
+ {line.label}
78
+ </span>
79
+ </div>
80
+ </div>