@reshape-biotech/design-system 1.2.5 → 1.2.7
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 +3 -1
- package/dist/app.css +97 -97
- package/dist/components/activity/Activity.stories.svelte +104 -104
- package/dist/components/activity/Activity.svelte +112 -112
- package/dist/components/avatar/Avatar.stories.svelte +23 -23
- package/dist/components/avatar/Avatar.svelte +54 -54
- package/dist/components/banner/Banner.stories.svelte +105 -105
- package/dist/components/banner/Banner.svelte +42 -42
- package/dist/components/button/Button.stories.svelte +61 -61
- package/dist/components/button/Button.svelte +95 -95
- package/dist/components/card/Card.stories.svelte +112 -112
- package/dist/components/card/Card.svelte +18 -18
- package/dist/components/checkbox/Checkbox.stories.svelte +8 -8
- package/dist/components/checkbox/Checkbox.svelte +17 -17
- package/dist/components/collapsible/Collapsible.stories.svelte +26 -26
- package/dist/components/collapsible/components/collapsible-content.svelte +20 -20
- package/dist/components/collapsible/components/collapsible-trigger.svelte +12 -12
- package/dist/components/combobox/Combobox.stories.svelte +412 -412
- package/dist/components/combobox/components/combobox-add.svelte +8 -8
- package/dist/components/combobox/components/combobox-content.svelte +39 -39
- package/dist/components/combobox/components/combobox-indicator.svelte +1 -1
- package/dist/components/datepicker/DatePicker.stories.svelte +196 -196
- package/dist/components/datepicker/DatePicker.svelte +173 -173
- package/dist/components/divider/Divider.stories.svelte +7 -7
- package/dist/components/divider/Divider.svelte +9 -9
- package/dist/components/drawer/Drawer.stories.svelte +51 -51
- package/dist/components/drawer/Drawer.svelte +33 -33
- package/dist/components/drawer/DrawerLabel.svelte +10 -10
- package/dist/components/dropdown/Dropdown.stories.svelte +210 -210
- package/dist/components/dropdown/Dropdown.svelte +57 -57
- package/dist/components/dropdown/components/DropdownContent.svelte +16 -16
- package/dist/components/dropdown/components/DropdownMenu.svelte +10 -10
- package/dist/components/dropdown/components/DropdownTrigger.svelte +37 -37
- package/dist/components/dropdown/components/OutlinedButton.svelte +9 -9
- package/dist/components/empty-content/EmptyContent.stories.svelte +38 -38
- package/dist/components/empty-content/EmptyContent.svelte +12 -12
- package/dist/components/graphs/bar-chart/BarChart.stories.svelte +91 -91
- package/dist/components/graphs/bar-chart/BarChart.svelte +147 -147
- package/dist/components/graphs/bar-chart/StackedBarChart.stories.svelte +57 -57
- package/dist/components/graphs/bar-chart/StackedBarChart.svelte +198 -199
- package/dist/components/graphs/chart/Chart.stories.svelte +96 -96
- package/dist/components/graphs/chart/Chart.svelte +207 -207
- package/dist/components/graphs/line/LineChart.stories.svelte +138 -138
- package/dist/components/graphs/line/LineChart.svelte +140 -142
- package/dist/components/graphs/matrix/Matrix.stories.svelte +117 -117
- package/dist/components/graphs/matrix/Matrix.svelte +141 -141
- package/dist/components/graphs/multiline/MultiLineChart.stories.svelte +168 -168
- package/dist/components/graphs/multiline/MultiLineChart.svelte +236 -236
- package/dist/components/graphs/scatterplot/Scatterplot.stories.svelte +84 -84
- package/dist/components/graphs/scatterplot/Scatterplot.svelte +302 -302
- package/dist/components/graphs/utils/duration.d.ts +1 -0
- package/dist/components/graphs/utils/duration.js +33 -0
- package/dist/components/graphs/utils/tooltipFormatter.js +1 -1
- package/dist/components/icon-button/IconButton.stories.svelte +64 -64
- package/dist/components/icon-button/IconButton.svelte +88 -88
- package/dist/components/icons/AnalysisIcon.stories.svelte +18 -18
- package/dist/components/icons/AnalysisIcon.svelte +96 -96
- package/dist/components/icons/Icon.stories.svelte +111 -111
- package/dist/components/icons/Icon.svelte +17 -17
- package/dist/components/icons/PrincipalIcon.svelte +59 -59
- package/dist/components/icons/custom/Halo.svelte +31 -31
- package/dist/components/icons/custom/Well.svelte +27 -27
- package/dist/components/icons/index.d.ts +3 -2
- package/dist/components/icons/index.js +3 -1
- package/dist/components/image/Image.svelte +42 -42
- package/dist/components/input/Input.stories.svelte +55 -55
- package/dist/components/input/Input.svelte +121 -121
- package/dist/components/label/Label.stories.svelte +18 -18
- package/dist/components/label/Label.svelte +11 -11
- package/dist/components/list/List.stories.svelte +84 -84
- package/dist/components/list/List.svelte +20 -20
- package/dist/components/logo/Logo.stories.svelte +15 -15
- package/dist/components/logo/Logo.svelte +30 -30
- package/dist/components/manual-cfu-counter/ManualCFUCounter.stories.svelte +102 -102
- package/dist/components/manual-cfu-counter/ManualCFUCounter.svelte +557 -565
- package/dist/components/manual-cfu-counter/test/ManualCFUCounterTestWrapper.svelte +11 -11
- package/dist/components/markdown/Markdown.stories.svelte +10 -10
- package/dist/components/markdown/Markdown.svelte +6 -6
- package/dist/components/modal/Modal.stories.svelte +29 -29
- package/dist/components/modal/Modal.svelte +71 -71
- package/dist/components/multi-cfu-counter/MultiCFUCounter.stories.svelte +215 -0
- package/dist/components/multi-cfu-counter/MultiCFUCounter.stories.svelte.d.ts +3 -0
- package/dist/components/multi-cfu-counter/MultiCFUCounter.svelte +662 -0
- package/dist/components/multi-cfu-counter/MultiCFUCounter.svelte.d.ts +32 -0
- package/dist/components/multi-cfu-counter/index.d.ts +1 -0
- package/dist/components/multi-cfu-counter/index.js +1 -0
- package/dist/components/multi-cfu-counter/test/MultiCFUCounterTestWrapper.svelte +28 -0
- package/dist/components/multi-cfu-counter/test/MultiCFUCounterTestWrapper.svelte.d.ts +20 -0
- package/dist/components/notification-popup/NotificationPopup.stories.svelte +18 -18
- package/dist/components/notification-popup/NotificationPopup.svelte +26 -26
- package/dist/components/notifications/Notifications.stories.svelte +101 -101
- package/dist/components/notifications/Notifications.svelte +9 -9
- package/dist/components/pill/Pill.stories.svelte +8 -8
- package/dist/components/pill/Pill.svelte +27 -27
- package/dist/components/progress-circle/ProgressCircle.stories.svelte +8 -8
- package/dist/components/progress-circle/ProgressCircle.svelte +54 -54
- package/dist/components/required-status-indicator/RequiredStatusIndicator.stories.svelte +18 -18
- package/dist/components/required-status-indicator/RequiredStatusIndicator.svelte +14 -14
- package/dist/components/segmented-control-buttons/ControlButton.svelte +36 -36
- package/dist/components/segmented-control-buttons/SegmentedControlButtons.stories.svelte +35 -35
- package/dist/components/segmented-control-buttons/SegmentedControlButtons.svelte +13 -13
- package/dist/components/select/Select.stories.svelte +77 -77
- package/dist/components/select/Select.svelte +114 -114
- package/dist/components/select-new/Select.stories.svelte +188 -188
- package/dist/components/select-new/components/Group.svelte +17 -17
- package/dist/components/select-new/components/MultiSelectTrigger.svelte +57 -57
- package/dist/components/select-new/components/SelectContent.svelte +22 -22
- package/dist/components/select-new/components/SelectGroupHeading.svelte +10 -10
- package/dist/components/select-new/components/SelectItem.svelte +25 -25
- package/dist/components/select-new/components/SelectTrigger.svelte +38 -38
- package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte +76 -76
- package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte.d.ts +1 -1
- package/dist/components/sjsf-wrappers/SjsfTextInputWrapper.svelte +53 -53
- package/dist/components/sjsf-wrappers/SjsfTextInputWrapper.svelte.d.ts +1 -1
- package/dist/components/sjsf-wrappers/sjsfCustomTheme.js +1 -1
- package/dist/components/skeleton-loader/SkeletonLoader.stories.svelte +32 -32
- package/dist/components/skeleton-loader/SkeletonLoader.svelte +10 -10
- package/dist/components/skeleton-loader/StatcardSkeleton.svelte +9 -9
- package/dist/components/skeleton-loader/components/Skeleton.svelte +7 -7
- package/dist/components/skeleton-loader/components/SkeletonImage.svelte +12 -12
- package/dist/components/slider/Slider.stories.svelte +23 -23
- package/dist/components/slider/Slider.svelte +107 -107
- package/dist/components/spinner/Spinner.stories.svelte +8 -8
- package/dist/components/spinner/Spinner.svelte +18 -18
- package/dist/components/stat-card/StatCard.stories.svelte +26 -26
- package/dist/components/stat-card/StatCard.svelte +128 -128
- package/dist/components/status-badge/StatusBadge.stories.svelte +365 -365
- package/dist/components/status-badge/StatusBadge.svelte +54 -54
- package/dist/components/stepper/Stepper.stories.svelte +219 -219
- package/dist/components/stepper/components/stepper-root.svelte +12 -12
- package/dist/components/stepper/components/stepper-step.svelte +83 -83
- package/dist/components/table/Table.stories.svelte +87 -87
- package/dist/components/table/Table.svelte +32 -32
- package/dist/components/table/components/TBody.svelte +7 -7
- package/dist/components/table/components/THead.svelte +7 -7
- package/dist/components/table/components/Td.svelte +8 -8
- package/dist/components/table/components/Th.svelte +8 -8
- package/dist/components/table/components/Tr.svelte +11 -11
- package/dist/components/tabs/Tabs.stories.svelte +20 -20
- package/dist/components/tabs/Tabs.svelte +8 -8
- package/dist/components/tabs/components/Content.svelte +8 -8
- package/dist/components/tabs/components/Tab.svelte +14 -14
- package/dist/components/tabs/components/Tabs.svelte +7 -7
- package/dist/components/tag/Tag.stories.svelte +57 -57
- package/dist/components/tag/Tag.svelte +95 -95
- package/dist/components/textarea/Textarea.stories.svelte +70 -70
- package/dist/components/textarea/Textarea.svelte +76 -76
- package/dist/components/toast/Toast.stories.svelte +204 -204
- package/dist/components/toast/Toast.svelte +53 -53
- package/dist/components/toggle/Toggle.stories.svelte +9 -9
- package/dist/components/toggle/Toggle.svelte +53 -53
- package/dist/components/toggle-icon-button/ToggleIconButton.stories.svelte +152 -152
- package/dist/components/toggle-icon-button/ToggleIconButton.svelte +99 -99
- package/dist/components/tooltip/Tooltip.stories.svelte +105 -105
- package/dist/components/tooltip/Tooltip.svelte +26 -26
- package/dist/fonts/index.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +5 -1
- package/dist/notifications.d.ts +1 -4
- package/dist/notifications.js +1 -1
- package/dist/styles.d.ts +1 -0
- package/dist/styles.js +4 -0
- package/dist/tailwind-safelist.js +406 -398
- package/dist/tailwind.preset.d.ts +4 -0
- package/dist/tailwind.preset.js +10 -10
- package/dist/tokens/colors.d.ts +246 -0
- package/dist/tokens/colors.js +139 -0
- package/dist/tokens/index.d.ts +3 -0
- package/dist/tokens/index.js +5 -0
- package/dist/tokens/typography.d.ts +48 -0
- package/dist/tokens/typography.js +48 -0
- package/dist/tokens.d.ts +16 -252
- package/dist/tokens.js +33 -164
- package/dist/types/fonts.d.ts +2 -2
- package/package.json +398 -78
|
@@ -1,581 +1,573 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
};
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
// Effect to render when marks prop changes from parent or internally
|
|
473
|
-
$effect(() => {
|
|
474
|
-
renderMarkers();
|
|
475
|
-
});
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Button } from '../button';
|
|
4
|
+
import { textColor, borderColor } from '../../tokens';
|
|
5
|
+
import { Icon } from '../icons';
|
|
6
|
+
import IconButton from '../icon-button/IconButton.svelte';
|
|
7
|
+
import Divider from '../divider/Divider.svelte';
|
|
8
|
+
|
|
9
|
+
// Constants for the component
|
|
10
|
+
const BASE_IMAGE_SIZE = 464; // Base size, will be overriden by container dimensions
|
|
11
|
+
const BASE_MARKER_SIZE = 8; // Base size at zoom level 1
|
|
12
|
+
const BASE_MARKER_FONT_SIZE = 8; // Base font size at zoom level 1
|
|
13
|
+
const MAX_ZOOM = 10;
|
|
14
|
+
const MIN_ZOOM = 1;
|
|
15
|
+
const ZOOM_STEP = 0.001;
|
|
16
|
+
const MARKER_COLOR = textColor['icon-blue'] || '#2563eb';
|
|
17
|
+
const MARKER_BORDER_COLOR = borderColor['interactive'] || '#12192A1A';
|
|
18
|
+
const TEXT_COLOR = textColor['primary-inverse'];
|
|
19
|
+
const DRAG_THRESHOLD = 5;
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
imageUrl: string;
|
|
23
|
+
marks?: Array<{ x: number; y: number }>;
|
|
24
|
+
onclick?: (event: MouseEvent, marks: Array<{ x: number; y: number }>) => void;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
hideMarkers?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
imageUrl,
|
|
31
|
+
onclick,
|
|
32
|
+
disabled = false,
|
|
33
|
+
hideMarkers = false,
|
|
34
|
+
marks = $bindable([]),
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
let svgElement: SVGSVGElement;
|
|
38
|
+
let viewport: SVGGraphicsElement;
|
|
39
|
+
let dotsGroup: SVGElement;
|
|
40
|
+
let container: HTMLDivElement;
|
|
41
|
+
|
|
42
|
+
let containerW = BASE_IMAGE_SIZE;
|
|
43
|
+
let containerH = BASE_IMAGE_SIZE;
|
|
44
|
+
|
|
45
|
+
let transform = $state({ x: 0, y: 0, scale: 1 });
|
|
46
|
+
|
|
47
|
+
// Panning state: null = not panning, 'ready' = mouse button down, 'active' = dragging
|
|
48
|
+
let panningState = $state<null | 'ready' | 'active'>(null);
|
|
49
|
+
let startPoint = $state({ x: 0, y: 0 });
|
|
50
|
+
let isShiftPressed = $state(false);
|
|
51
|
+
let resizeObserver: ResizeObserver;
|
|
52
|
+
|
|
53
|
+
let imageAspectRatio = $state(1); // default 1:1 ratio
|
|
54
|
+
let imageDisplayWidth = $state(0);
|
|
55
|
+
let imageDisplayHeight = $state(0);
|
|
56
|
+
|
|
57
|
+
function handleResize(entries: ResizeObserverEntry[]) {
|
|
58
|
+
const entry = entries[0];
|
|
59
|
+
if (entry) {
|
|
60
|
+
const oldWidth = imageDisplayWidth || containerW;
|
|
61
|
+
const oldHeight = imageDisplayHeight || containerH;
|
|
62
|
+
|
|
63
|
+
containerW = entry.contentRect.width;
|
|
64
|
+
containerH = entry.contentRect.height;
|
|
65
|
+
|
|
66
|
+
// If we already had dimensions and marks, update their positions
|
|
67
|
+
if (oldWidth > 0 && oldHeight > 0 && marks.length > 0) {
|
|
68
|
+
// Adjust marks positions based on new container dimensions
|
|
69
|
+
marks = marks.map((mark) => ({
|
|
70
|
+
x: (mark.x / oldWidth) * containerW,
|
|
71
|
+
y: (mark.y / oldHeight) * containerH,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
updateImageDimensions();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onImageLoad() {
|
|
80
|
+
if (container) {
|
|
81
|
+
containerW = container.clientWidth;
|
|
82
|
+
containerH = container.clientHeight;
|
|
83
|
+
|
|
84
|
+
// get original image aspect ratio
|
|
85
|
+
const img = new Image();
|
|
86
|
+
img.src = imageUrl;
|
|
87
|
+
img.onload = () => {
|
|
88
|
+
imageAspectRatio = img.naturalWidth / img.naturalHeight;
|
|
89
|
+
updateImageDimensions();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (img.complete && img.naturalWidth) {
|
|
93
|
+
imageAspectRatio = img.naturalWidth / img.naturalHeight;
|
|
94
|
+
updateImageDimensions();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function updateImageDimensions() {
|
|
100
|
+
// calculate dimensions that maintain aspect ratio within container
|
|
101
|
+
const containerRatio = containerW / containerH;
|
|
102
|
+
|
|
103
|
+
if (containerRatio > imageAspectRatio) {
|
|
104
|
+
// container is wider than image - fit to height
|
|
105
|
+
imageDisplayHeight = containerH;
|
|
106
|
+
imageDisplayWidth = containerH * imageAspectRatio;
|
|
107
|
+
} else {
|
|
108
|
+
// container is taller than image - fit to width
|
|
109
|
+
imageDisplayWidth = containerW;
|
|
110
|
+
imageDisplayHeight = containerW / imageAspectRatio;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
updateTransform();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Properly constrain the transform to prevent moving outside container
|
|
117
|
+
function clampTransform() {
|
|
118
|
+
// Calculate maximum translation offsets based on scale > 1
|
|
119
|
+
const maxX = 0;
|
|
120
|
+
const maxY = 0;
|
|
121
|
+
// Prevent division by zero or unexpected behavior if container dimensions aren't ready
|
|
122
|
+
const safeContainerW = containerW || 1;
|
|
123
|
+
const safeContainerH = containerH || 1;
|
|
124
|
+
const minX = safeContainerW * (1 - transform.scale);
|
|
125
|
+
const minY = safeContainerH * (1 - transform.scale);
|
|
126
|
+
|
|
127
|
+
if (transform.scale <= 1) {
|
|
128
|
+
// If scale is 1 or less, center the view
|
|
129
|
+
// Check if scale is exactly 1 before modifying to avoid floating point issues if needed
|
|
130
|
+
// Or simply reset if it goes below
|
|
131
|
+
transform.scale = 1;
|
|
132
|
+
transform.x = 0;
|
|
133
|
+
transform.y = 0;
|
|
134
|
+
} else {
|
|
135
|
+
// Apply clamping for scale > 1
|
|
136
|
+
transform.x = Math.max(minX, Math.min(maxX, transform.x));
|
|
137
|
+
transform.y = Math.max(minY, Math.min(maxY, transform.y));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function updateTransform() {
|
|
142
|
+
if (!viewport) return;
|
|
143
|
+
|
|
144
|
+
// update viewport transform
|
|
145
|
+
viewport.setAttribute(
|
|
146
|
+
'transform',
|
|
147
|
+
`translate(${transform.x}, ${transform.y}) scale(${transform.scale})`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// set image dimensions based on calculated size that maintains aspect ratio
|
|
151
|
+
const imageElement = viewport.querySelector('image');
|
|
152
|
+
if (imageElement) {
|
|
153
|
+
imageElement.setAttribute('width', String(imageDisplayWidth));
|
|
154
|
+
imageElement.setAttribute('height', String(imageDisplayHeight));
|
|
155
|
+
|
|
156
|
+
// center the image in the container
|
|
157
|
+
const offsetX = (containerW - imageDisplayWidth) / 2;
|
|
158
|
+
const offsetY = (containerH - imageDisplayHeight) / 2;
|
|
159
|
+
|
|
160
|
+
imageElement.setAttribute('x', String(offsetX));
|
|
161
|
+
imageElement.setAttribute('y', String(offsetY));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// update markers to maintain consistent size
|
|
165
|
+
renderMarkers();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getSvgPoint(event: MouseEvent) {
|
|
169
|
+
if (!svgElement || !container) return { x: 0, y: 0 };
|
|
170
|
+
|
|
171
|
+
// get container bounding rect
|
|
172
|
+
const rect = container.getBoundingClientRect();
|
|
173
|
+
|
|
174
|
+
// calculate position relative to container
|
|
175
|
+
const relativeX = event.clientX - rect.left;
|
|
176
|
+
const relativeY = event.clientY - rect.top;
|
|
177
|
+
|
|
178
|
+
// apply inverse of current transform to get SVG coordinates
|
|
179
|
+
const svgX = (relativeX - transform.x) / transform.scale;
|
|
180
|
+
const svgY = (relativeY - transform.y) / transform.scale;
|
|
181
|
+
|
|
182
|
+
// adjust for image position within container (letterboxing)
|
|
183
|
+
const offsetX = (containerW - imageDisplayWidth) / 2;
|
|
184
|
+
const offsetY = (containerH - imageDisplayHeight) / 2;
|
|
185
|
+
|
|
186
|
+
// return coordinates relative to the image, not the container
|
|
187
|
+
return {
|
|
188
|
+
x: svgX - offsetX,
|
|
189
|
+
y: svgY - offsetY,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Render SVG markers from the marks array
|
|
194
|
+
function renderMarkers() {
|
|
195
|
+
if (!dotsGroup) return;
|
|
196
|
+
|
|
197
|
+
while (dotsGroup.firstChild) {
|
|
198
|
+
dotsGroup.removeChild(dotsGroup.firstChild);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const offsetX = (containerW - imageDisplayWidth) / 2;
|
|
202
|
+
const offsetY = (containerH - imageDisplayHeight) / 2;
|
|
203
|
+
|
|
204
|
+
const svgns = 'http://www.w3.org/2000/svg';
|
|
205
|
+
const adjustedMarkerSize = BASE_MARKER_SIZE / transform.scale;
|
|
206
|
+
const adjustedFontSize = BASE_MARKER_FONT_SIZE / transform.scale;
|
|
207
|
+
|
|
208
|
+
marks.forEach((mark, index) => {
|
|
209
|
+
// Create a marker group
|
|
210
|
+
const group = document.createElementNS(svgns, 'g');
|
|
211
|
+
group.setAttribute('data-testid', `marker-${index + 1}`);
|
|
212
|
+
|
|
213
|
+
const markX = mark.x + offsetX;
|
|
214
|
+
const markY = mark.y + offsetY;
|
|
215
|
+
|
|
216
|
+
const circle = document.createElementNS(svgns, 'circle');
|
|
217
|
+
circle.setAttribute('cx', String(markX));
|
|
218
|
+
circle.setAttribute('cy', String(markY));
|
|
219
|
+
circle.setAttribute('r', String(adjustedMarkerSize));
|
|
220
|
+
circle.setAttribute('fill', MARKER_COLOR);
|
|
221
|
+
circle.setAttribute('class', 'drop-shadow-sm');
|
|
222
|
+
circle.setAttribute('opacity', '0.75');
|
|
223
|
+
circle.setAttribute('stroke', MARKER_BORDER_COLOR);
|
|
224
|
+
circle.setAttribute('stroke-width', String(1 / transform.scale));
|
|
225
|
+
group.appendChild(circle);
|
|
226
|
+
|
|
227
|
+
const text = document.createElementNS(svgns, 'text');
|
|
228
|
+
text.setAttribute('x', String(markX));
|
|
229
|
+
text.setAttribute('y', String(markY));
|
|
230
|
+
text.setAttribute('text-anchor', 'middle');
|
|
231
|
+
text.setAttribute('dominant-baseline', 'central');
|
|
232
|
+
text.setAttribute('fill', TEXT_COLOR);
|
|
233
|
+
text.setAttribute('font-size', String(adjustedFontSize));
|
|
234
|
+
text.setAttribute('font-weight', 'bold');
|
|
235
|
+
text.textContent = String(index + 1);
|
|
236
|
+
group.appendChild(text);
|
|
237
|
+
|
|
238
|
+
dotsGroup.appendChild(group);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleClick(event: MouseEvent) {
|
|
243
|
+
if (disabled || hideMarkers || !dotsGroup || isShiftPressed || panningState !== null) {
|
|
244
|
+
panningState = null;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const pt = getSvgPoint(event);
|
|
248
|
+
marks.push({ x: pt.x, y: pt.y });
|
|
249
|
+
|
|
250
|
+
renderMarkers();
|
|
251
|
+
if (onclick) {
|
|
252
|
+
onclick(event, marks);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
257
|
+
if (event.shiftKey) {
|
|
258
|
+
isShiftPressed = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleKeyUp(event: KeyboardEvent) {
|
|
263
|
+
let shouldResetReadyState = false;
|
|
264
|
+
|
|
265
|
+
if (!event.shiftKey && isShiftPressed) {
|
|
266
|
+
isShiftPressed = false;
|
|
267
|
+
shouldResetReadyState = true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// If panning was in 'ready' state and the relevant key was released, reset state
|
|
271
|
+
if (shouldResetReadyState && panningState === 'ready') {
|
|
272
|
+
panningState = null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function handleMouseDown(event: MouseEvent) {
|
|
277
|
+
// Only start panning on right click, shift+left click
|
|
278
|
+
const isPanningTrigger = event.button === 2 || (event.button === 0 && isShiftPressed);
|
|
279
|
+
if (!isPanningTrigger) return;
|
|
280
|
+
|
|
281
|
+
event.preventDefault(); // Prevent default context menu or text selection
|
|
282
|
+
panningState = 'ready';
|
|
283
|
+
startPoint = { x: event.clientX, y: event.clientY };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Handle panning on mouse move
|
|
287
|
+
function handleMouseMove(event: MouseEvent) {
|
|
288
|
+
if (panningState === null) return;
|
|
289
|
+
|
|
290
|
+
const dx = event.clientX - startPoint.x;
|
|
291
|
+
const dy = event.clientY - startPoint.y;
|
|
292
|
+
|
|
293
|
+
// Detect if we're dragging (moved beyond threshold)
|
|
294
|
+
if (panningState === 'ready' && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
|
295
|
+
panningState = 'active';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Only update position if we're actually panning
|
|
299
|
+
if (panningState === 'active') {
|
|
300
|
+
startPoint = { x: event.clientX, y: event.clientY };
|
|
301
|
+
|
|
302
|
+
transform.x += dx;
|
|
303
|
+
transform.y += dy;
|
|
304
|
+
|
|
305
|
+
// Ensure pan stays within bounds
|
|
306
|
+
clampTransform();
|
|
307
|
+
updateTransform();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function handleMouseUp() {
|
|
312
|
+
panningState = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function handleMouseLeave() {
|
|
316
|
+
panningState = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function handleContextMenu(event: MouseEvent) {
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function handleWheel(event: WheelEvent) {
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
|
|
326
|
+
if (!svgElement || !viewport || !container) return;
|
|
327
|
+
|
|
328
|
+
// Update container dimensions just in case they changed
|
|
329
|
+
containerW = container.clientWidth;
|
|
330
|
+
containerH = container.clientHeight;
|
|
331
|
+
|
|
332
|
+
const zoomIntensity = ZOOM_STEP;
|
|
333
|
+
const delta = event.deltaY;
|
|
334
|
+
const zoomFactor = 1 - delta * zoomIntensity;
|
|
335
|
+
const currentScale = transform.scale; // Store current scale before modification
|
|
336
|
+
|
|
337
|
+
let potentialNewScale = currentScale * zoomFactor;
|
|
338
|
+
|
|
339
|
+
// Clamp the scale
|
|
340
|
+
let finalScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, potentialNewScale));
|
|
341
|
+
|
|
342
|
+
if (finalScale <= MIN_ZOOM) {
|
|
343
|
+
// Reset view if zooming out below minimum or exactly at minimum
|
|
344
|
+
transform.scale = MIN_ZOOM;
|
|
345
|
+
transform.x = 0;
|
|
346
|
+
transform.y = 0;
|
|
347
|
+
} else {
|
|
348
|
+
// Calculate transform based on finalScale (which might be MAX_ZOOM)
|
|
349
|
+
const rect = container.getBoundingClientRect();
|
|
350
|
+
const mouseX = event.clientX - rect.left;
|
|
351
|
+
const mouseY = event.clientY - rect.top;
|
|
352
|
+
|
|
353
|
+
// Calculate distance from origin at current scale
|
|
354
|
+
const distX = mouseX - transform.x;
|
|
355
|
+
const distY = mouseY - transform.y;
|
|
356
|
+
|
|
357
|
+
// Calculate what the distance should be at the final scale
|
|
358
|
+
// Use currentScale from *before* this zoom event for the ratio
|
|
359
|
+
const scaleRatio = finalScale / currentScale;
|
|
360
|
+
const newDistX = distX * scaleRatio;
|
|
361
|
+
const newDistY = distY * scaleRatio;
|
|
362
|
+
|
|
363
|
+
// Update the transform to maintain the mouse position relative to the final scale
|
|
364
|
+
transform.x = mouseX - newDistX;
|
|
365
|
+
transform.y = mouseY - newDistY;
|
|
366
|
+
transform.scale = finalScale;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
clampTransform();
|
|
370
|
+
updateTransform();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Simplified undo function - just pop the last mark
|
|
374
|
+
function undo() {
|
|
375
|
+
if (marks.length === 0) return;
|
|
376
|
+
|
|
377
|
+
marks.pop();
|
|
378
|
+
marks = marks;
|
|
379
|
+
renderMarkers();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function reset() {
|
|
383
|
+
if (marks.length === 0) return;
|
|
384
|
+
|
|
385
|
+
marks = [];
|
|
386
|
+
renderMarkers();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --- Zoom Functions ---
|
|
390
|
+
function applyZoom(zoomFactor: number) {
|
|
391
|
+
if (!container) return;
|
|
392
|
+
|
|
393
|
+
const currentScale = transform.scale;
|
|
394
|
+
const potentialNewScale = currentScale * zoomFactor;
|
|
395
|
+
const finalScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, potentialNewScale));
|
|
396
|
+
|
|
397
|
+
// If scale didn't change (already at min/max), do nothing
|
|
398
|
+
if (finalScale === currentScale) return;
|
|
399
|
+
|
|
400
|
+
// Get center of the container view
|
|
401
|
+
const centerX = containerW / 2;
|
|
402
|
+
const centerY = containerH / 2;
|
|
403
|
+
|
|
404
|
+
// Calculate distance from origin at current scale
|
|
405
|
+
const distX = centerX - transform.x;
|
|
406
|
+
const distY = centerY - transform.y;
|
|
407
|
+
|
|
408
|
+
// Calculate what the distance should be at the final scale
|
|
409
|
+
const scaleRatio = finalScale / currentScale;
|
|
410
|
+
const newDistX = distX * scaleRatio;
|
|
411
|
+
const newDistY = distY * scaleRatio;
|
|
412
|
+
|
|
413
|
+
// Update the transform to maintain the center position relative to the final scale
|
|
414
|
+
transform.x = centerX - newDistX;
|
|
415
|
+
transform.y = centerY - newDistY;
|
|
416
|
+
transform.scale = finalScale;
|
|
417
|
+
|
|
418
|
+
clampTransform();
|
|
419
|
+
updateTransform();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function zoomIn() {
|
|
423
|
+
applyZoom(1.2); // Zoom in by 20%
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function zoomOut() {
|
|
427
|
+
applyZoom(1 / 1.2); // Zoom out by 20%
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
onMount(() => {
|
|
431
|
+
if (container) {
|
|
432
|
+
containerW = container.clientWidth;
|
|
433
|
+
containerH = container.clientHeight;
|
|
434
|
+
|
|
435
|
+
// setup resize observer
|
|
436
|
+
resizeObserver = new ResizeObserver(handleResize);
|
|
437
|
+
resizeObserver.observe(container);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
441
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
442
|
+
|
|
443
|
+
updateTransform();
|
|
444
|
+
renderMarkers();
|
|
445
|
+
|
|
446
|
+
const img = new Image();
|
|
447
|
+
img.onload = onImageLoad;
|
|
448
|
+
img.src = imageUrl;
|
|
449
|
+
|
|
450
|
+
if (img.complete) {
|
|
451
|
+
onImageLoad();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return () => {
|
|
455
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
456
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
457
|
+
// cleanup resize observer
|
|
458
|
+
if (resizeObserver) {
|
|
459
|
+
resizeObserver.disconnect();
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Effect to render when marks prop changes from parent or internally
|
|
465
|
+
$effect(() => {
|
|
466
|
+
renderMarkers();
|
|
467
|
+
});
|
|
476
468
|
</script>
|
|
477
469
|
|
|
478
470
|
{#snippet TopLeftActions()}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
471
|
+
{#if marks.length > 0}
|
|
472
|
+
<div
|
|
473
|
+
class="absolute left-2 top-2 z-20 flex items-center gap-1 rounded-lg border border-interactive-inverse bg-base-inverse p-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
|
474
|
+
>
|
|
475
|
+
<IconButton
|
|
476
|
+
variant="transparent-inverse"
|
|
477
|
+
rounded={false}
|
|
478
|
+
onclick={undo}
|
|
479
|
+
disabled={marks.length === 0 || disabled}
|
|
480
|
+
aria-label="Undo last mark"
|
|
481
|
+
>
|
|
482
|
+
<Icon iconName={'ArrowUUpLeft'} />
|
|
483
|
+
</IconButton>
|
|
484
|
+
<Divider vertical inverse class="!h-5" />
|
|
485
|
+
<Button
|
|
486
|
+
variant="transparent-inverse"
|
|
487
|
+
size="sm"
|
|
488
|
+
onClick={reset}
|
|
489
|
+
disabled={marks.length === 0 || disabled}
|
|
490
|
+
accessibilityLabel="Reset all marks"
|
|
491
|
+
class="!text-primary-inverse"
|
|
492
|
+
>
|
|
493
|
+
Clear all
|
|
494
|
+
</Button>
|
|
495
|
+
</div>
|
|
496
|
+
{/if}
|
|
505
497
|
{/snippet}
|
|
506
498
|
|
|
507
499
|
{#snippet ZoomControls()}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
500
|
+
<div
|
|
501
|
+
class="absolute bottom-2 right-2 z-20 flex flex-col items-center gap-1 rounded-md border border-interactive-inverse bg-base-inverse p-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
|
502
|
+
>
|
|
503
|
+
<IconButton
|
|
504
|
+
variant="transparent-inverse"
|
|
505
|
+
rounded={false}
|
|
506
|
+
onclick={zoomIn}
|
|
507
|
+
aria-label="Zoom In"
|
|
508
|
+
disabled={transform.scale >= MAX_ZOOM || disabled}
|
|
509
|
+
>
|
|
510
|
+
<Icon iconName={'Plus'} />
|
|
511
|
+
</IconButton>
|
|
512
|
+
<IconButton
|
|
513
|
+
variant="transparent-inverse"
|
|
514
|
+
rounded={false}
|
|
515
|
+
onclick={zoomOut}
|
|
516
|
+
aria-label="Zoom Out"
|
|
517
|
+
disabled={transform.scale <= MIN_ZOOM || disabled}
|
|
518
|
+
>
|
|
519
|
+
<Icon iconName={'Minus'} />
|
|
520
|
+
</IconButton>
|
|
521
|
+
</div>
|
|
530
522
|
{/snippet}
|
|
531
523
|
|
|
532
524
|
<div bind:this={container} class="group relative h-full w-full overflow-hidden rounded-lg border">
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
525
|
+
{#if !hideMarkers && !disabled}
|
|
526
|
+
{@render TopLeftActions()}
|
|
527
|
+
{@render ZoomControls()}
|
|
528
|
+
{/if}
|
|
537
529
|
|
|
538
|
-
|
|
530
|
+
<!--
|
|
539
531
|
We need to use SVG for this interactive component.
|
|
540
532
|
The SVG element is treated as a canvas for clicking, panning, and zooming.
|
|
541
533
|
We add accessibility attributes to make it more accessible despite the interactive nature.
|
|
542
534
|
-->
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
535
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
536
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
537
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
538
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
539
|
+
<svg
|
|
540
|
+
bind:this={svgElement}
|
|
541
|
+
onclick={handleClick}
|
|
542
|
+
onmousedown={handleMouseDown}
|
|
543
|
+
onmousemove={handleMouseMove}
|
|
544
|
+
onmouseup={handleMouseUp}
|
|
545
|
+
onmouseleave={handleMouseLeave}
|
|
546
|
+
oncontextmenu={handleContextMenu}
|
|
547
|
+
onwheel={handleWheel}
|
|
548
|
+
class:cursor-grabbing={panningState === 'active'}
|
|
549
|
+
class:cursor-grab={!disabled && (panningState === 'ready' || (isShiftPressed && !panningState))}
|
|
550
|
+
class:cursor-not-allowed={disabled}
|
|
551
|
+
class:cursor-crosshair={!disabled && !panningState && !isShiftPressed}
|
|
552
|
+
class="h-full w-full"
|
|
553
|
+
role="application"
|
|
554
|
+
aria-label={disabled
|
|
555
|
+
? 'CFU Counter (disabled)'
|
|
556
|
+
: 'CFU Counter - Click to add markers, right click or shift+click to pan'}
|
|
557
|
+
tabindex="0"
|
|
558
|
+
>
|
|
559
|
+
<g bind:this={viewport} id="viewport" class="h-full w-full">
|
|
560
|
+
<image href={imageUrl} x="0" y="0" width="100%" />
|
|
561
|
+
<g
|
|
562
|
+
bind:this={dotsGroup}
|
|
563
|
+
id="dots"
|
|
564
|
+
class="pointer-events-none"
|
|
565
|
+
aria-hidden={hideMarkers}
|
|
566
|
+
class:hidden={hideMarkers}
|
|
567
|
+
/>
|
|
568
|
+
</g>
|
|
569
|
+
</svg>
|
|
570
|
+
|
|
571
|
+
<!-- Debug info for marks count - useful for testing -->
|
|
572
|
+
<span class="sr-only" data-testid="marks-count">{marks.length}</span>
|
|
581
573
|
</div>
|