@reshape-biotech/design-system 1.2.6 → 2.0.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 +5 -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/collapsible/index.d.ts +1 -1
- 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/combobox/index.d.ts +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.js +1 -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 -557
- 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 +201 -201
- package/dist/components/multi-cfu-counter/MultiCFUCounter.svelte +606 -606
- package/dist/components/multi-cfu-counter/test/MultiCFUCounterTestWrapper.svelte +17 -17
- 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 +200 -94
- package/dist/components/select/Select.stories.svelte.d.ts +1 -1
- package/dist/components/select/components/Group.svelte +24 -0
- package/dist/components/select/components/MultiSelectTrigger.svelte +66 -0
- package/dist/components/select/components/SelectContent.svelte +33 -0
- package/dist/components/select/components/SelectGroupHeading.svelte +19 -0
- package/dist/components/select/components/SelectItem.svelte +39 -0
- package/dist/components/select/components/SelectTrigger.svelte +48 -0
- package/dist/components/select/index.d.ts +10 -7
- package/dist/components/select/index.js +12 -1
- package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte +102 -87
- 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 +85 -111
- package/dist/components/tooltip/Tooltip.svelte +57 -46
- package/dist/components/tooltip/Tooltip.svelte.d.ts +1 -1
- package/dist/components/tooltip/TooltipTest.svelte +31 -0
- package/dist/components/tooltip/TooltipTest.svelte.d.ts +11 -0
- package/dist/fonts/index.js +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/notifications.d.ts +1 -4
- package/dist/notifications.js +1 -1
- package/dist/tailwind-safelist.js +406 -406
- package/dist/tailwind.preset.js +10 -10
- package/dist/tokens/colors.js +18 -18
- package/dist/tokens/typography.js +6 -6
- package/dist/tokens.js +19 -19
- package/dist/types/fonts.d.ts +2 -2
- package/package.json +199 -204
- package/dist/components/select/Select.svelte +0 -139
- package/dist/components/select/Select.svelte.d.ts +0 -60
- package/dist/components/select-new/Select.stories.svelte +0 -219
- package/dist/components/select-new/Select.stories.svelte.d.ts +0 -19
- package/dist/components/select-new/components/Group.svelte +0 -24
- package/dist/components/select-new/components/MultiSelectTrigger.svelte +0 -66
- package/dist/components/select-new/components/SelectContent.svelte +0 -33
- package/dist/components/select-new/components/SelectGroupHeading.svelte +0 -19
- package/dist/components/select-new/components/SelectItem.svelte +0 -39
- package/dist/components/select-new/components/SelectTrigger.svelte +0 -48
- package/dist/components/select-new/index.d.ts +0 -10
- package/dist/components/select-new/index.js +0 -12
- /package/dist/components/{select-new → select}/components/Group.svelte.d.ts +0 -0
- /package/dist/components/{select-new → select}/components/MultiSelectTrigger.svelte.d.ts +0 -0
- /package/dist/components/{select-new → select}/components/SelectContent.svelte.d.ts +0 -0
- /package/dist/components/{select-new → select}/components/SelectGroupHeading.svelte.d.ts +0 -0
- /package/dist/components/{select-new → select}/components/SelectItem.svelte.d.ts +0 -0
- /package/dist/components/{select-new → select}/components/SelectTrigger.svelte.d.ts +0 -0
- /package/dist/components/{select-new → select}/types.d.ts +0 -0
- /package/dist/components/{select-new → select}/types.js +0 -0
|
@@ -1,662 +1,662 @@
|
|
|
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
|
-
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { Button } from '../button';
|
|
4
|
+
import { textColor } from '../../tokens';
|
|
5
|
+
import { Icon, type IconColor } from '../icons';
|
|
6
|
+
import IconButton from '../icon-button/IconButton.svelte';
|
|
7
|
+
import Divider from '../divider/Divider.svelte';
|
|
8
|
+
|
|
9
|
+
const BASE_IMAGE_SIZE = 464;
|
|
10
|
+
const BASE_MARKER_SIZE = 8;
|
|
11
|
+
const BASE_MARKER_FONT_SIZE = 8;
|
|
12
|
+
const MAX_ZOOM = 10;
|
|
13
|
+
const MIN_ZOOM = 1;
|
|
14
|
+
const ZOOM_STEP = 0.001;
|
|
15
|
+
const MARKER_COLOR = textColor['icon-blue'];
|
|
16
|
+
const TEXT_COLOR = textColor['primary-inverse'];
|
|
17
|
+
const DRAG_THRESHOLD = 5;
|
|
18
|
+
|
|
19
|
+
function getMarkerColorHex(semanticColor: string): string {
|
|
20
|
+
switch (semanticColor) {
|
|
21
|
+
case 'icon-blue':
|
|
22
|
+
return textColor['icon-blue'];
|
|
23
|
+
case 'icon-orange':
|
|
24
|
+
return textColor['icon-orange'];
|
|
25
|
+
case 'icon-pink':
|
|
26
|
+
return textColor['icon-pink'];
|
|
27
|
+
case 'icon-lime':
|
|
28
|
+
return textColor['icon-lime'];
|
|
29
|
+
default:
|
|
30
|
+
return textColor['icon-blue'];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ConfiguratedMark {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
configIndex: number;
|
|
38
|
+
color: string;
|
|
39
|
+
id?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
imageUrl: string;
|
|
44
|
+
marks?: Array<{ x: number; y: number }>;
|
|
45
|
+
onclick?: (event: MouseEvent, marks: Array<{ x: number; y: number }>) => void;
|
|
46
|
+
onMarksChange?: (marks: Array<{ x: number; y: number }>) => void;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
hideMarkers?: boolean;
|
|
49
|
+
allMarks?: ConfiguratedMark[];
|
|
50
|
+
activeMarkerColor?: string;
|
|
51
|
+
activeMarkerName?: string;
|
|
52
|
+
editableConfigIndex?: number | null;
|
|
53
|
+
showMultiConfig?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
imageUrl,
|
|
58
|
+
onclick,
|
|
59
|
+
onMarksChange,
|
|
60
|
+
disabled = false,
|
|
61
|
+
hideMarkers = false,
|
|
62
|
+
marks = $bindable([]),
|
|
63
|
+
allMarks = [],
|
|
64
|
+
activeMarkerColor = MARKER_COLOR,
|
|
65
|
+
activeMarkerName = '',
|
|
66
|
+
editableConfigIndex = null,
|
|
67
|
+
showMultiConfig = false,
|
|
68
|
+
}: Props = $props();
|
|
69
|
+
|
|
70
|
+
let previousConfigIndex = $state<number | null>(null);
|
|
71
|
+
|
|
72
|
+
let resolvedActiveMarkerColor = $derived(() => {
|
|
73
|
+
if (!activeMarkerColor) return MARKER_COLOR;
|
|
74
|
+
|
|
75
|
+
// If it starts with #, it's already a hex color
|
|
76
|
+
if (activeMarkerColor.startsWith('#')) {
|
|
77
|
+
return activeMarkerColor;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return getMarkerColorHex(activeMarkerColor);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let svgElement: SVGSVGElement;
|
|
84
|
+
let viewport: SVGGraphicsElement;
|
|
85
|
+
let dotsGroup: SVGElement;
|
|
86
|
+
let container: HTMLDivElement;
|
|
87
|
+
|
|
88
|
+
let containerW = BASE_IMAGE_SIZE;
|
|
89
|
+
let containerH = BASE_IMAGE_SIZE;
|
|
90
|
+
|
|
91
|
+
let transform = $state({ x: 0, y: 0, scale: 1 });
|
|
92
|
+
|
|
93
|
+
let panningState = $state<null | 'ready' | 'active'>(null);
|
|
94
|
+
let startPoint = $state({ x: 0, y: 0 });
|
|
95
|
+
let isShiftPressed = $state(false);
|
|
96
|
+
let resizeObserver: ResizeObserver;
|
|
97
|
+
|
|
98
|
+
let imageAspectRatio = $state(1);
|
|
99
|
+
let imageDisplayWidth = $state(0);
|
|
100
|
+
let imageDisplayHeight = $state(0);
|
|
101
|
+
|
|
102
|
+
function handleResize(entries: ResizeObserverEntry[]) {
|
|
103
|
+
const entry = entries[0];
|
|
104
|
+
if (entry) {
|
|
105
|
+
const oldWidth = imageDisplayWidth || containerW;
|
|
106
|
+
const oldHeight = imageDisplayHeight || containerH;
|
|
107
|
+
|
|
108
|
+
containerW = entry.contentRect.width;
|
|
109
|
+
containerH = entry.contentRect.height;
|
|
110
|
+
|
|
111
|
+
if (oldWidth > 0 && oldHeight > 0 && marks.length > 0) {
|
|
112
|
+
marks = marks.map((mark) => ({
|
|
113
|
+
x: (mark.x / oldWidth) * containerW,
|
|
114
|
+
y: (mark.y / oldHeight) * containerH,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
updateImageDimensions();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function onImageLoad() {
|
|
123
|
+
if (container) {
|
|
124
|
+
containerW = container.clientWidth;
|
|
125
|
+
containerH = container.clientHeight;
|
|
126
|
+
|
|
127
|
+
const img = new Image();
|
|
128
|
+
img.src = imageUrl;
|
|
129
|
+
img.onload = () => {
|
|
130
|
+
imageAspectRatio = img.naturalWidth / img.naturalHeight;
|
|
131
|
+
updateImageDimensions();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (img.complete && img.naturalWidth) {
|
|
135
|
+
imageAspectRatio = img.naturalWidth / img.naturalHeight;
|
|
136
|
+
updateImageDimensions();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function updateImageDimensions() {
|
|
142
|
+
const containerRatio = containerW / containerH;
|
|
143
|
+
|
|
144
|
+
if (containerRatio > imageAspectRatio) {
|
|
145
|
+
imageDisplayHeight = containerH;
|
|
146
|
+
imageDisplayWidth = containerH * imageAspectRatio;
|
|
147
|
+
} else {
|
|
148
|
+
imageDisplayWidth = containerW;
|
|
149
|
+
imageDisplayHeight = containerW / imageAspectRatio;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
updateTransform();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clampTransform() {
|
|
156
|
+
const maxX = 0;
|
|
157
|
+
const maxY = 0;
|
|
158
|
+
// Prevent division by zero or unexpected behavior if container dimensions aren't ready
|
|
159
|
+
const safeContainerW = containerW || 1;
|
|
160
|
+
const safeContainerH = containerH || 1;
|
|
161
|
+
const minX = safeContainerW * (1 - transform.scale);
|
|
162
|
+
const minY = safeContainerH * (1 - transform.scale);
|
|
163
|
+
|
|
164
|
+
if (transform.scale <= 1) {
|
|
165
|
+
transform.scale = 1;
|
|
166
|
+
transform.x = 0;
|
|
167
|
+
transform.y = 0;
|
|
168
|
+
} else {
|
|
169
|
+
transform.x = Math.max(minX, Math.min(maxX, transform.x));
|
|
170
|
+
transform.y = Math.max(minY, Math.min(maxY, transform.y));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function updateTransform() {
|
|
175
|
+
if (!viewport) return;
|
|
176
|
+
|
|
177
|
+
viewport.setAttribute(
|
|
178
|
+
'transform',
|
|
179
|
+
`translate(${transform.x}, ${transform.y}) scale(${transform.scale})`
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const imageElement = viewport.querySelector('image');
|
|
183
|
+
if (imageElement) {
|
|
184
|
+
imageElement.setAttribute('width', String(imageDisplayWidth));
|
|
185
|
+
imageElement.setAttribute('height', String(imageDisplayHeight));
|
|
186
|
+
|
|
187
|
+
// center the image in the container
|
|
188
|
+
const offsetX = (containerW - imageDisplayWidth) / 2;
|
|
189
|
+
const offsetY = (containerH - imageDisplayHeight) / 2;
|
|
190
|
+
|
|
191
|
+
imageElement.setAttribute('x', String(offsetX));
|
|
192
|
+
imageElement.setAttribute('y', String(offsetY));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
renderMarkers();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getSvgPoint(event: MouseEvent) {
|
|
199
|
+
if (!svgElement || !container) return { x: 0, y: 0 };
|
|
200
|
+
|
|
201
|
+
const rect = container.getBoundingClientRect();
|
|
202
|
+
|
|
203
|
+
const relativeX = event.clientX - rect.left;
|
|
204
|
+
const relativeY = event.clientY - rect.top;
|
|
205
|
+
|
|
206
|
+
const svgX = (relativeX - transform.x) / transform.scale;
|
|
207
|
+
const svgY = (relativeY - transform.y) / transform.scale;
|
|
208
|
+
|
|
209
|
+
const offsetX = (containerW - imageDisplayWidth) / 2;
|
|
210
|
+
const offsetY = (containerH - imageDisplayHeight) / 2;
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
x: svgX - offsetX,
|
|
214
|
+
y: svgY - offsetY,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderMarkers() {
|
|
219
|
+
if (!dotsGroup) return;
|
|
220
|
+
|
|
221
|
+
while (dotsGroup.firstChild) {
|
|
222
|
+
dotsGroup.removeChild(dotsGroup.firstChild);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const offsetX = (containerW - imageDisplayWidth) / 2;
|
|
226
|
+
const offsetY = (containerH - imageDisplayHeight) / 2;
|
|
227
|
+
|
|
228
|
+
const svgns = 'http://www.w3.org/2000/svg';
|
|
229
|
+
const adjustedMarkerSize = BASE_MARKER_SIZE / transform.scale;
|
|
230
|
+
const adjustedFontSize = BASE_MARKER_FONT_SIZE / transform.scale;
|
|
231
|
+
|
|
232
|
+
if (showMultiConfig && allMarks && allMarks.length > 0) {
|
|
233
|
+
// Group marks by configuration to number them separately
|
|
234
|
+
const marksByConfig = new Map<number, ConfiguratedMark[]>();
|
|
235
|
+
allMarks.forEach((mark) => {
|
|
236
|
+
if (!marksByConfig.has(mark.configIndex)) {
|
|
237
|
+
marksByConfig.set(mark.configIndex, []);
|
|
238
|
+
}
|
|
239
|
+
marksByConfig.get(mark.configIndex)!.push(mark);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Render multi-configuration marks with per-configuration numbering
|
|
243
|
+
marksByConfig.forEach((configMarks, configIndex) => {
|
|
244
|
+
configMarks.forEach((mark, configMarkIndex) => {
|
|
245
|
+
const group = document.createElementNS(svgns, 'g');
|
|
246
|
+
group.setAttribute(
|
|
247
|
+
'data-testid',
|
|
248
|
+
`config-marker-${mark.configIndex}-${configMarkIndex + 1}`
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const markX = mark.x + offsetX;
|
|
252
|
+
const markY = mark.y + offsetY;
|
|
253
|
+
|
|
254
|
+
// Resolve mark color - supports both hex colors and semantic color names
|
|
255
|
+
const resolvedMarkColor = mark.color.startsWith('#')
|
|
256
|
+
? mark.color
|
|
257
|
+
: getMarkerColorHex(mark.color);
|
|
258
|
+
|
|
259
|
+
const circle = document.createElementNS(svgns, 'circle');
|
|
260
|
+
circle.setAttribute('cx', String(markX));
|
|
261
|
+
circle.setAttribute('cy', String(markY));
|
|
262
|
+
circle.setAttribute('r', String(adjustedMarkerSize));
|
|
263
|
+
circle.setAttribute('fill', resolvedMarkColor);
|
|
264
|
+
circle.setAttribute('class', 'drop-shadow-sm');
|
|
265
|
+
group.appendChild(circle);
|
|
266
|
+
|
|
267
|
+
const text = document.createElementNS(svgns, 'text');
|
|
268
|
+
text.setAttribute('x', String(markX));
|
|
269
|
+
text.setAttribute('y', String(markY));
|
|
270
|
+
text.setAttribute('text-anchor', 'middle');
|
|
271
|
+
text.setAttribute('dominant-baseline', 'central');
|
|
272
|
+
text.setAttribute('fill', TEXT_COLOR);
|
|
273
|
+
text.setAttribute('font-size', String(adjustedFontSize));
|
|
274
|
+
text.setAttribute('font-weight', 'bold');
|
|
275
|
+
text.textContent = String(configMarkIndex + 1); // Number per configuration
|
|
276
|
+
group.appendChild(text);
|
|
277
|
+
|
|
278
|
+
dotsGroup.appendChild(group);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
// Render single-configuration marks (existing behavior)
|
|
283
|
+
marks.forEach((mark, index) => {
|
|
284
|
+
const group = document.createElementNS(svgns, 'g');
|
|
285
|
+
group.setAttribute('data-testid', `marker-${index + 1}`);
|
|
286
|
+
|
|
287
|
+
const markX = mark.x + offsetX;
|
|
288
|
+
const markY = mark.y + offsetY;
|
|
289
|
+
|
|
290
|
+
const circle = document.createElementNS(svgns, 'circle');
|
|
291
|
+
circle.setAttribute('cx', String(markX));
|
|
292
|
+
circle.setAttribute('cy', String(markY));
|
|
293
|
+
circle.setAttribute('r', String(adjustedMarkerSize));
|
|
294
|
+
circle.setAttribute('fill', resolvedActiveMarkerColor());
|
|
295
|
+
circle.setAttribute('class', 'drop-shadow-sm');
|
|
296
|
+
group.appendChild(circle);
|
|
297
|
+
|
|
298
|
+
const text = document.createElementNS(svgns, 'text');
|
|
299
|
+
text.setAttribute('x', String(markX));
|
|
300
|
+
text.setAttribute('y', String(markY));
|
|
301
|
+
text.setAttribute('text-anchor', 'middle');
|
|
302
|
+
text.setAttribute('dominant-baseline', 'central');
|
|
303
|
+
text.setAttribute('fill', TEXT_COLOR);
|
|
304
|
+
text.setAttribute('font-size', String(adjustedFontSize));
|
|
305
|
+
text.setAttribute('font-weight', 'bold');
|
|
306
|
+
text.textContent = String(index + 1);
|
|
307
|
+
group.appendChild(text);
|
|
308
|
+
|
|
309
|
+
dotsGroup.appendChild(group);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function handleClick(event: MouseEvent) {
|
|
315
|
+
if (disabled || hideMarkers || !dotsGroup || isShiftPressed || panningState !== null) {
|
|
316
|
+
panningState = null;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// In multi-config mode, only allow editing if there's an active editable configuration
|
|
321
|
+
if (showMultiConfig && editableConfigIndex === null) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const pt = getSvgPoint(event);
|
|
326
|
+
marks.push({ x: pt.x, y: pt.y });
|
|
327
|
+
|
|
328
|
+
renderMarkers();
|
|
329
|
+
|
|
330
|
+
if (onclick) {
|
|
331
|
+
onclick(event, marks);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
onMarksChange?.(marks);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
338
|
+
if (event.shiftKey) {
|
|
339
|
+
isShiftPressed = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function handleKeyUp(event: KeyboardEvent) {
|
|
344
|
+
let shouldResetReadyState = false;
|
|
345
|
+
|
|
346
|
+
if (!event.shiftKey && isShiftPressed) {
|
|
347
|
+
isShiftPressed = false;
|
|
348
|
+
shouldResetReadyState = true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (shouldResetReadyState && panningState === 'ready') {
|
|
352
|
+
panningState = null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function handleMouseDown(event: MouseEvent) {
|
|
357
|
+
const isPanningTrigger = event.button === 2 || (event.button === 0 && isShiftPressed);
|
|
358
|
+
if (!isPanningTrigger) return;
|
|
359
|
+
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
panningState = 'ready';
|
|
362
|
+
startPoint = { x: event.clientX, y: event.clientY };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function handleMouseMove(event: MouseEvent) {
|
|
366
|
+
if (panningState === null) return;
|
|
367
|
+
|
|
368
|
+
const dx = event.clientX - startPoint.x;
|
|
369
|
+
const dy = event.clientY - startPoint.y;
|
|
370
|
+
|
|
371
|
+
if (panningState === 'ready' && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
|
372
|
+
panningState = 'active';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (panningState === 'active') {
|
|
376
|
+
startPoint = { x: event.clientX, y: event.clientY };
|
|
377
|
+
|
|
378
|
+
transform.x += dx;
|
|
379
|
+
transform.y += dy;
|
|
380
|
+
|
|
381
|
+
clampTransform();
|
|
382
|
+
updateTransform();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
386
|
+
function handleMouseUp() {
|
|
387
|
+
panningState = null;
|
|
388
|
+
}
|
|
389
389
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
function handleMouseLeave() {
|
|
391
|
+
panningState = null;
|
|
392
|
+
}
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
function handleContextMenu(event: MouseEvent) {
|
|
395
|
+
event.preventDefault();
|
|
396
|
+
}
|
|
397
397
|
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
function handleWheel(event: WheelEvent) {
|
|
399
|
+
event.preventDefault();
|
|
400
400
|
|
|
401
|
-
|
|
401
|
+
if (!svgElement || !viewport || !container) return;
|
|
402
402
|
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
containerW = container.clientWidth;
|
|
404
|
+
containerH = container.clientHeight;
|
|
405
405
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
406
|
+
const zoomIntensity = ZOOM_STEP;
|
|
407
|
+
const delta = event.deltaY;
|
|
408
|
+
const zoomFactor = 1 - delta * zoomIntensity;
|
|
409
|
+
const currentScale = transform.scale;
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
let potentialNewScale = currentScale * zoomFactor;
|
|
412
412
|
|
|
413
|
-
|
|
413
|
+
let finalScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, potentialNewScale));
|
|
414
414
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
415
|
+
if (finalScale <= MIN_ZOOM) {
|
|
416
|
+
transform.scale = MIN_ZOOM;
|
|
417
|
+
transform.x = 0;
|
|
418
|
+
transform.y = 0;
|
|
419
|
+
} else {
|
|
420
|
+
const rect = container.getBoundingClientRect();
|
|
421
|
+
const mouseX = event.clientX - rect.left;
|
|
422
|
+
const mouseY = event.clientY - rect.top;
|
|
423
423
|
|
|
424
|
-
|
|
425
|
-
|
|
424
|
+
const distX = mouseX - transform.x;
|
|
425
|
+
const distY = mouseY - transform.y;
|
|
426
426
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
427
|
+
const scaleRatio = finalScale / currentScale;
|
|
428
|
+
const newDistX = distX * scaleRatio;
|
|
429
|
+
const newDistY = distY * scaleRatio;
|
|
430
430
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
transform.x = mouseX - newDistX;
|
|
432
|
+
transform.y = mouseY - newDistY;
|
|
433
|
+
transform.scale = finalScale;
|
|
434
|
+
}
|
|
435
435
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
436
|
+
clampTransform();
|
|
437
|
+
updateTransform();
|
|
438
|
+
}
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
|
|
440
|
+
function undo() {
|
|
441
|
+
if (marks.length === 0) return;
|
|
442
442
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
443
|
+
marks.pop();
|
|
444
|
+
marks = marks;
|
|
445
|
+
renderMarkers();
|
|
446
446
|
|
|
447
|
-
|
|
448
|
-
|
|
447
|
+
onMarksChange?.(marks);
|
|
448
|
+
}
|
|
449
449
|
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
function reset() {
|
|
451
|
+
if (marks.length === 0) return;
|
|
452
452
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
453
|
+
marks = [];
|
|
454
|
+
renderMarkers();
|
|
455
|
+
onMarksChange?.(marks);
|
|
456
|
+
}
|
|
457
457
|
|
|
458
|
-
|
|
459
|
-
|
|
458
|
+
function applyZoom(zoomFactor: number) {
|
|
459
|
+
if (!container) return;
|
|
460
460
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
461
|
+
const currentScale = transform.scale;
|
|
462
|
+
const potentialNewScale = currentScale * zoomFactor;
|
|
463
|
+
const finalScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, potentialNewScale));
|
|
464
464
|
|
|
465
|
-
|
|
465
|
+
if (finalScale === currentScale) return;
|
|
466
466
|
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
const centerX = containerW / 2;
|
|
468
|
+
const centerY = containerH / 2;
|
|
469
469
|
|
|
470
|
-
|
|
471
|
-
|
|
470
|
+
const distX = centerX - transform.x;
|
|
471
|
+
const distY = centerY - transform.y;
|
|
472
472
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
const scaleRatio = finalScale / currentScale;
|
|
474
|
+
const newDistX = distX * scaleRatio;
|
|
475
|
+
const newDistY = distY * scaleRatio;
|
|
476
476
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
477
|
+
transform.x = centerX - newDistX;
|
|
478
|
+
transform.y = centerY - newDistY;
|
|
479
|
+
transform.scale = finalScale;
|
|
480
480
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
481
|
+
clampTransform();
|
|
482
|
+
updateTransform();
|
|
483
|
+
}
|
|
484
484
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
485
|
+
function zoomIn() {
|
|
486
|
+
applyZoom(1.2);
|
|
487
|
+
}
|
|
488
488
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
489
|
+
function zoomOut() {
|
|
490
|
+
applyZoom(1 / 1.2);
|
|
491
|
+
}
|
|
492
492
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
493
|
+
$effect(() => {
|
|
494
|
+
if (editableConfigIndex !== previousConfigIndex) {
|
|
495
|
+
previousConfigIndex = editableConfigIndex;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
498
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
499
|
+
onMount(() => {
|
|
500
|
+
if (container) {
|
|
501
|
+
containerW = container.clientWidth;
|
|
502
|
+
containerH = container.clientHeight;
|
|
503
503
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
504
|
+
resizeObserver = new ResizeObserver(handleResize);
|
|
505
|
+
resizeObserver.observe(container);
|
|
506
|
+
}
|
|
507
507
|
|
|
508
|
-
|
|
509
|
-
|
|
508
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
509
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
510
510
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
511
|
+
updateTransform();
|
|
512
|
+
renderMarkers();
|
|
513
|
+
|
|
514
|
+
const img = new Image();
|
|
515
|
+
img.onload = onImageLoad;
|
|
516
|
+
img.src = imageUrl;
|
|
517
|
+
|
|
518
|
+
if (img.complete) {
|
|
519
|
+
onImageLoad();
|
|
520
|
+
}
|
|
521
521
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
522
|
+
return () => {
|
|
523
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
524
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
525
|
+
if (resizeObserver) {
|
|
526
|
+
resizeObserver.disconnect();
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
530
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
531
|
+
$effect(() => {
|
|
532
|
+
renderMarkers();
|
|
533
|
+
});
|
|
534
534
|
</script>
|
|
535
535
|
|
|
536
536
|
{#snippet UndoResetControls()}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
537
|
+
<div class="flex items-center gap-1">
|
|
538
|
+
<IconButton
|
|
539
|
+
variant="transparent-inverse"
|
|
540
|
+
rounded={false}
|
|
541
|
+
onclick={undo}
|
|
542
|
+
disabled={marks.length === 0 || disabled}
|
|
543
|
+
aria-label="Undo last mark"
|
|
544
|
+
>
|
|
545
|
+
<Icon iconName={'ArrowUUpLeft'} />
|
|
546
|
+
</IconButton>
|
|
547
|
+
<Divider vertical inverse class="!h-5" />
|
|
548
|
+
<Button
|
|
549
|
+
variant="transparent-inverse"
|
|
550
|
+
size="sm"
|
|
551
|
+
onClick={reset}
|
|
552
|
+
disabled={marks.length === 0 || disabled}
|
|
553
|
+
accessibilityLabel="Reset all marks"
|
|
554
|
+
class="!text-primary-inverse"
|
|
555
|
+
>
|
|
556
|
+
Clear all
|
|
557
|
+
</Button>
|
|
558
|
+
</div>
|
|
559
559
|
{/snippet}
|
|
560
560
|
|
|
561
561
|
{#snippet TopLeftActions()}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
562
|
+
{#if activeMarkerName && !hideMarkers && !disabled}
|
|
563
|
+
<div
|
|
564
|
+
class="absolute left-2 top-2 z-20 flex w-full max-w-[calc(100%-1rem)] items-center justify-between rounded-lg border border-interactive-inverse bg-base-inverse px-3 py-2"
|
|
565
|
+
>
|
|
566
|
+
<div class="flex items-center gap-2">
|
|
567
|
+
<div
|
|
568
|
+
class="flex !h-6 !w-6 shrink-0 items-center justify-center rounded-md"
|
|
569
|
+
style="background-color: {resolvedActiveMarkerColor()}"
|
|
570
|
+
>
|
|
571
|
+
<Icon iconName={'CursorClick'} size={16} color="primary-inverse" />
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<span class="text-sm text-primary-inverse">
|
|
575
|
+
Click to count each object in <strong>{activeMarkerName}</strong>
|
|
576
|
+
</span>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
{@render UndoResetControls()}
|
|
580
|
+
</div>
|
|
581
|
+
{/if}
|
|
582
582
|
{/snippet}
|
|
583
583
|
|
|
584
584
|
{#snippet ZoomControls()}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
585
|
+
<div
|
|
586
|
+
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"
|
|
587
|
+
>
|
|
588
|
+
<IconButton
|
|
589
|
+
variant="transparent-inverse"
|
|
590
|
+
rounded={false}
|
|
591
|
+
onclick={zoomIn}
|
|
592
|
+
aria-label="Zoom In"
|
|
593
|
+
disabled={transform.scale >= MAX_ZOOM || disabled}
|
|
594
|
+
>
|
|
595
|
+
<Icon iconName={'Plus'} />
|
|
596
|
+
</IconButton>
|
|
597
|
+
<IconButton
|
|
598
|
+
variant="transparent-inverse"
|
|
599
|
+
rounded={false}
|
|
600
|
+
onclick={zoomOut}
|
|
601
|
+
aria-label="Zoom Out"
|
|
602
|
+
disabled={transform.scale <= MIN_ZOOM || disabled}
|
|
603
|
+
>
|
|
604
|
+
<Icon iconName={'Minus'} />
|
|
605
|
+
</IconButton>
|
|
606
|
+
</div>
|
|
607
607
|
{/snippet}
|
|
608
608
|
|
|
609
609
|
<div
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
610
|
+
bind:this={container}
|
|
611
|
+
class="group relative h-full w-full overflow-hidden rounded-lg border"
|
|
612
|
+
data-testid="manual-cfu-counter"
|
|
613
613
|
>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
614
|
+
{#if !hideMarkers && !disabled}
|
|
615
|
+
{@render TopLeftActions()}
|
|
616
|
+
{@render ZoomControls()}
|
|
617
|
+
{/if}
|
|
618
618
|
|
|
619
|
-
|
|
619
|
+
<!--
|
|
620
620
|
We need to use SVG for this interactive component.
|
|
621
621
|
The SVG element is treated as a canvas for clicking, panning, and zooming.
|
|
622
622
|
We add accessibility attributes to make it more accessible despite the interactive nature.
|
|
623
623
|
-->
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
624
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
625
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
626
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
627
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
628
|
+
<svg
|
|
629
|
+
bind:this={svgElement}
|
|
630
|
+
onclick={handleClick}
|
|
631
|
+
onmousedown={handleMouseDown}
|
|
632
|
+
onmousemove={handleMouseMove}
|
|
633
|
+
onmouseup={handleMouseUp}
|
|
634
|
+
onmouseleave={handleMouseLeave}
|
|
635
|
+
oncontextmenu={handleContextMenu}
|
|
636
|
+
onwheel={handleWheel}
|
|
637
|
+
class:cursor-grabbing={panningState === 'active'}
|
|
638
|
+
class:cursor-grab={!disabled && (panningState === 'ready' || (isShiftPressed && !panningState))}
|
|
639
|
+
class:cursor-not-allowed={disabled}
|
|
640
|
+
class:cursor-crosshair={!disabled && !panningState && !isShiftPressed}
|
|
641
|
+
class="h-full w-full"
|
|
642
|
+
role="application"
|
|
643
|
+
aria-label={disabled
|
|
644
|
+
? 'CFU Counter (disabled)'
|
|
645
|
+
: 'CFU Counter - Click to add markers, right click or shift+click to pan'}
|
|
646
|
+
tabindex="0"
|
|
647
|
+
>
|
|
648
|
+
<g bind:this={viewport} id="viewport" class="h-full w-full">
|
|
649
|
+
<image href={imageUrl} x="0" y="0" width="100%" />
|
|
650
|
+
<g
|
|
651
|
+
bind:this={dotsGroup}
|
|
652
|
+
id="dots"
|
|
653
|
+
class="pointer-events-none"
|
|
654
|
+
aria-hidden={hideMarkers}
|
|
655
|
+
class:hidden={hideMarkers}
|
|
656
|
+
/>
|
|
657
|
+
</g>
|
|
658
|
+
</svg>
|
|
659
|
+
|
|
660
|
+
<!-- Debug info for marks count - useful for testing -->
|
|
661
|
+
<span class="sr-only" data-testid="marks-count">{marks.length}</span>
|
|
662
662
|
</div>
|