@joewinke/jatui 0.1.11 → 0.1.19

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 (90) hide show
  1. package/README.md +123 -0
  2. package/package.json +2 -1
  3. package/src/lib/actions/railNav.ts +473 -0
  4. package/src/lib/components/AnnotationLayer.svelte +108 -0
  5. package/src/lib/components/AnnotationPanel.svelte +319 -0
  6. package/src/lib/components/AudioWaveform.svelte +9 -5
  7. package/src/lib/components/AvailabilityModal.svelte +7 -3
  8. package/src/lib/components/AvatarUpload.svelte +27 -4
  9. package/src/lib/components/BookingForm.svelte +11 -9
  10. package/src/lib/components/BurndownChart.svelte +778 -0
  11. package/src/lib/components/Button.svelte +10 -1
  12. package/src/lib/components/CalendarPicker.svelte +3 -3
  13. package/src/lib/components/Card.svelte +2 -2
  14. package/src/lib/components/ChipInput.svelte +21 -15
  15. package/src/lib/components/ColorSelector.svelte +17 -13
  16. package/src/lib/components/CommentThread.svelte +773 -0
  17. package/src/lib/components/ConfirmDialog.svelte +348 -0
  18. package/src/lib/components/ConfirmModal.svelte +78 -11
  19. package/src/lib/components/ContextMenu.svelte +59 -19
  20. package/src/lib/components/CountdownTimer.svelte +1 -1
  21. package/src/lib/components/DateRangePicker.svelte +6 -4
  22. package/src/lib/components/Drawer.svelte +36 -3
  23. package/src/lib/components/EntityPreviewCard.svelte +104 -0
  24. package/src/lib/components/FileDropzone.svelte +493 -0
  25. package/src/lib/components/FilePicker.svelte +83 -14
  26. package/src/lib/components/FileThumbnail.svelte +80 -0
  27. package/src/lib/components/FilterDropdown.svelte +11 -11
  28. package/src/lib/components/HunkDiffView.svelte +348 -0
  29. package/src/lib/components/ImageLightbox.svelte +274 -0
  30. package/src/lib/components/ImageUpload.svelte +58 -9
  31. package/src/lib/components/InlineEdit.svelte +15 -9
  32. package/src/lib/components/InputDialog.svelte +327 -0
  33. package/src/lib/components/LazyImage.svelte +1 -0
  34. package/src/lib/components/LinkShortener.svelte +1 -1
  35. package/src/lib/components/LoadingSpinner.svelte +6 -2
  36. package/src/lib/components/MarkupEditor.svelte +485 -0
  37. package/src/lib/components/MarkupOverlay.svelte +55 -0
  38. package/src/lib/components/MediaWorkbench.svelte +871 -0
  39. package/src/lib/components/MilestoneCard.svelte +1 -1
  40. package/src/lib/components/MilestoneTimeline.svelte +1 -1
  41. package/src/lib/components/Modal.svelte +39 -4
  42. package/src/lib/components/PDFViewer.svelte +105 -0
  43. package/src/lib/components/PdfThumbnail.svelte +3 -1
  44. package/src/lib/components/PhoneInput.svelte +1 -1
  45. package/src/lib/components/ResizablePanel.svelte +4 -4
  46. package/src/lib/components/SearchDropdown.svelte +26 -13
  47. package/src/lib/components/SelectInput.svelte +26 -4
  48. package/src/lib/components/SidebarUserFooter.svelte +1 -1
  49. package/src/lib/components/SignaturePad.svelte +8 -4
  50. package/src/lib/components/SmartImageEditor.svelte +720 -0
  51. package/src/lib/components/SortDropdown.svelte +9 -3
  52. package/src/lib/components/Sparkline.svelte +9 -0
  53. package/src/lib/components/StatusBadge.svelte +20 -18
  54. package/src/lib/components/TextArea.svelte +24 -5
  55. package/src/lib/components/TextInput.svelte +29 -6
  56. package/src/lib/components/ThemeSelector.svelte +15 -4
  57. package/src/lib/components/TimeSlotPicker.svelte +7 -7
  58. package/src/lib/components/UserAvatar.svelte +14 -1
  59. package/src/lib/components/VariablePicker.svelte +170 -0
  60. package/src/lib/components/VoicePlayer.svelte +4 -3
  61. package/src/lib/components/markup.ts +287 -0
  62. package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
  63. package/src/lib/components/messaging/ChannelList.svelte +1 -1
  64. package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
  65. package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
  66. package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
  67. package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
  68. package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
  69. package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
  70. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
  71. package/src/lib/components/messaging/MessageInput.svelte +1 -1
  72. package/src/lib/components/messaging/MessageItem.svelte +6 -3
  73. package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
  74. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
  75. package/src/lib/components/messaging/StartDMModal.svelte +1 -1
  76. package/src/lib/components/pipeline/Pipeline.svelte +4 -4
  77. package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
  78. package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
  79. package/src/lib/index.ts +91 -0
  80. package/src/lib/stores/confirmDialog.svelte.ts +48 -0
  81. package/src/lib/stores/inputDialog.svelte.ts +51 -0
  82. package/src/lib/styles/rail.css +63 -0
  83. package/src/lib/types/annotation.ts +38 -0
  84. package/src/lib/types/comments.ts +97 -0
  85. package/src/lib/types/entityPreview.ts +45 -0
  86. package/src/lib/types/filePicker.ts +2 -0
  87. package/src/lib/types/smartImageEditor.ts +39 -0
  88. package/src/lib/types/templateVars.ts +36 -0
  89. package/src/lib/utils/dateFormatters.ts +12 -10
  90. package/src/lib/utils/taskUtils.ts +21 -7
@@ -0,0 +1,485 @@
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from "svelte"
3
+ import type { MarkupShape, MarkupTool, Point } from './markup'
4
+ import {
5
+ MARKUP_COLORS,
6
+ DEFAULT_STROKE_WIDTH,
7
+ renderAllShapes,
8
+ renderShape,
9
+ nextShapeId,
10
+ } from './markup'
11
+
12
+ interface Props {
13
+ imageUrl: string
14
+ initialShapes?: MarkupShape[]
15
+ onsave: (shapes: MarkupShape[]) => void
16
+ oncancel: () => void
17
+ }
18
+
19
+ let { imageUrl, initialShapes = [], onsave, oncancel }: Props = $props()
20
+
21
+ let tool = $state<MarkupTool>("arrow")
22
+ let color = $state(MARKUP_COLORS[0])
23
+ // svelte-ignore state_referenced_locally
24
+ let shapes = $state<MarkupShape[]>([...initialShapes])
25
+
26
+ // Canvas refs
27
+ let baseCanvas: HTMLCanvasElement | undefined = $state()
28
+ let overlayCanvas: HTMLCanvasElement | undefined = $state()
29
+
30
+ // Image dimensions (native)
31
+ let imgW = $state(0)
32
+ let imgH = $state(0)
33
+ let loaded = $state(false)
34
+ let loadError = $state(false)
35
+
36
+ // Drawing state
37
+ type DrawState = "idle" | "drawing" | "typing"
38
+ let drawState = $state<DrawState>("idle")
39
+ let startPoint: Point = { x: 0, y: 0 }
40
+ let freehandPoints: Point[] = []
41
+
42
+ // Text input
43
+ let textInput: HTMLInputElement | undefined = $state()
44
+ let textInputPos = $state({ x: 0, y: 0 })
45
+ let textInputCss = $state({ left: "0px", top: "0px" })
46
+ let textValue = $state("")
47
+
48
+ let baseImage: HTMLImageElement | null = null
49
+
50
+ onMount(() => {
51
+ const img = new Image()
52
+ img.crossOrigin = "anonymous"
53
+ img.onload = () => {
54
+ imgW = img.naturalWidth
55
+ imgH = img.naturalHeight
56
+ baseImage = img
57
+ loaded = true
58
+ requestAnimationFrame(() => redrawBase(img))
59
+ }
60
+ img.onerror = () => {
61
+ loadError = true
62
+ }
63
+ img.src = imageUrl
64
+ })
65
+
66
+ function redrawBase(img?: HTMLImageElement) {
67
+ if (!baseCanvas) return
68
+ const ctx = baseCanvas.getContext("2d")
69
+ if (!ctx) return
70
+ if (!img) img = baseImage ?? undefined
71
+ if (!img) return
72
+ baseCanvas.width = imgW
73
+ baseCanvas.height = imgH
74
+ ctx.drawImage(img, 0, 0, imgW, imgH)
75
+ renderAllShapes(ctx, shapes)
76
+ }
77
+
78
+ function clearOverlay() {
79
+ if (!overlayCanvas) return
80
+ const ctx = overlayCanvas.getContext("2d")
81
+ if (!ctx) return
82
+ overlayCanvas.width = imgW
83
+ overlayCanvas.height = imgH
84
+ ctx.clearRect(0, 0, imgW, imgH)
85
+ }
86
+
87
+ function canvasCoords(e: MouseEvent): Point {
88
+ if (!overlayCanvas) return { x: 0, y: 0 }
89
+ const rect = overlayCanvas.getBoundingClientRect()
90
+ const scaleX = imgW / rect.width
91
+ const scaleY = imgH / rect.height
92
+ return {
93
+ x: (e.clientX - rect.left) * scaleX,
94
+ y: (e.clientY - rect.top) * scaleY,
95
+ }
96
+ }
97
+
98
+ function canvasToCss(p: Point): { left: string; top: string } {
99
+ if (!overlayCanvas) return { left: "0px", top: "0px" }
100
+ const rect = overlayCanvas.getBoundingClientRect()
101
+ return {
102
+ left: `${rect.left + p.x / (imgW / rect.width)}px`,
103
+ top: `${rect.top + p.y / (imgH / rect.height)}px`,
104
+ }
105
+ }
106
+
107
+ function buildShape(end: Point): MarkupShape | null {
108
+ const base = { color, strokeWidth: DEFAULT_STROKE_WIDTH }
109
+ switch (tool) {
110
+ case "arrow":
111
+ return {
112
+ ...base,
113
+ id: nextShapeId(),
114
+ type: "arrow",
115
+ start: startPoint,
116
+ end,
117
+ }
118
+ case "rectangle":
119
+ return {
120
+ ...base,
121
+ id: nextShapeId(),
122
+ type: "rectangle",
123
+ start: startPoint,
124
+ end,
125
+ }
126
+ case "ellipse":
127
+ return {
128
+ ...base,
129
+ id: nextShapeId(),
130
+ type: "ellipse",
131
+ start: startPoint,
132
+ end,
133
+ }
134
+ case "cloud":
135
+ return {
136
+ ...base,
137
+ id: nextShapeId(),
138
+ type: "cloud",
139
+ start: startPoint,
140
+ end,
141
+ }
142
+ case "freehand":
143
+ return {
144
+ ...base,
145
+ id: nextShapeId(),
146
+ type: "freehand",
147
+ points: [...freehandPoints, end],
148
+ }
149
+ default:
150
+ return null
151
+ }
152
+ }
153
+
154
+ function onPointerDown(e: MouseEvent) {
155
+ if (drawState === "typing") {
156
+ commitText()
157
+ return
158
+ }
159
+ const p = canvasCoords(e)
160
+ if (tool === "text") {
161
+ drawState = "typing"
162
+ textInputPos = p
163
+ textInputCss = canvasToCss(p)
164
+ textValue = ""
165
+ requestAnimationFrame(() => textInput?.focus())
166
+ return
167
+ }
168
+ drawState = "drawing"
169
+ startPoint = p
170
+ freehandPoints = [p]
171
+ }
172
+
173
+ function onPointerMove(e: MouseEvent) {
174
+ if (drawState !== "drawing") return
175
+ const p = canvasCoords(e)
176
+ if (tool === "freehand") freehandPoints.push(p)
177
+ clearOverlay()
178
+ const preview = buildShape(p)
179
+ if (preview && overlayCanvas) {
180
+ const ctx = overlayCanvas.getContext("2d")
181
+ if (ctx) renderShape(ctx, preview)
182
+ }
183
+ }
184
+
185
+ function onPointerUp(e: MouseEvent) {
186
+ if (drawState !== "drawing") return
187
+ const p = canvasCoords(e)
188
+ const shape = buildShape(p)
189
+ if (shape) {
190
+ shapes = [...shapes, shape]
191
+ }
192
+ drawState = "idle"
193
+ freehandPoints = []
194
+ clearOverlay()
195
+ redrawBase()
196
+ }
197
+
198
+ function commitText() {
199
+ if (textValue.trim()) {
200
+ const shape: MarkupShape = {
201
+ id: nextShapeId(),
202
+ type: "text",
203
+ color,
204
+ strokeWidth: DEFAULT_STROKE_WIDTH,
205
+ position: textInputPos,
206
+ content: textValue.trim(),
207
+ fontSize: 20,
208
+ }
209
+ shapes = [...shapes, shape]
210
+ redrawBase()
211
+ }
212
+ textValue = ""
213
+ drawState = "idle"
214
+ }
215
+
216
+ function handleTextKeydown(e: KeyboardEvent) {
217
+ e.stopPropagation()
218
+ if (e.key === "Enter") {
219
+ e.preventDefault()
220
+ commitText()
221
+ } else if (e.key === "Escape") {
222
+ e.preventDefault()
223
+ textValue = ""
224
+ drawState = "idle"
225
+ }
226
+ }
227
+
228
+ function undo() {
229
+ if (shapes.length === 0) return
230
+ shapes = shapes.slice(0, -1)
231
+ redrawBase()
232
+ }
233
+
234
+ function clearAll() {
235
+ shapes = []
236
+ redrawBase()
237
+ }
238
+
239
+ function handleSave() {
240
+ onsave(shapes)
241
+ }
242
+
243
+ function handleKeydown(e: KeyboardEvent) {
244
+ e.stopPropagation()
245
+ if (e.key === "Escape" && drawState !== "typing") {
246
+ oncancel()
247
+ }
248
+ if ((e.ctrlKey || e.metaKey) && e.key === "z") {
249
+ e.preventDefault()
250
+ undo()
251
+ }
252
+ }
253
+
254
+ const toolDefs: { tool: MarkupTool; label: string; icon: string }[] = [
255
+ { tool: "arrow", label: "Arrow", icon: "M5 19L19 5M19 5H9M19 5V15" },
256
+ { tool: "rectangle", label: "Box", icon: "M3 3h18v18H3z" },
257
+ {
258
+ tool: "ellipse",
259
+ label: "Circle",
260
+ icon: "", // special SVG
261
+ },
262
+ {
263
+ tool: "cloud",
264
+ label: "Cloud",
265
+ icon: "", // special SVG
266
+ },
267
+ {
268
+ tool: "freehand",
269
+ label: "Draw",
270
+ icon: "M3 17c1-2 3-6 5-6s3 4 5 4 3-6 5-6 2 4 3 4",
271
+ },
272
+ {
273
+ tool: "text",
274
+ label: "Text",
275
+ icon: "M6 4v16M18 4v16M6 12h12M8 4h-4M20 4h-4M8 20h-4M20 20h-4",
276
+ },
277
+ ]
278
+ </script>
279
+
280
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
281
+ <div
282
+ class="fixed inset-0 z-50 bg-black/90 flex flex-col items-center"
283
+ onkeydown={handleKeydown}
284
+ onkeyup={(e) => e.stopPropagation()}
285
+ onkeypress={(e) => e.stopPropagation()}
286
+ >
287
+ <!-- Toolbar -->
288
+ <div
289
+ class="flex items-center gap-1.5 px-3 py-2 bg-base-300 border-b border-base-content/10 w-full flex-shrink-0 flex-wrap"
290
+ >
291
+ <!-- Tool buttons -->
292
+ <div class="flex items-center gap-1">
293
+ {#each toolDefs as t}
294
+ <button
295
+ class="btn btn-xs gap-1 {tool === t.tool
296
+ ? 'btn-primary'
297
+ : 'btn-ghost text-base-content/60 hover:text-base-content'}"
298
+ onclick={() => {
299
+ tool = t.tool
300
+ }}
301
+ title={t.label}
302
+ >
303
+ {#if t.tool === "ellipse"}
304
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
305
+ <ellipse
306
+ cx="12"
307
+ cy="12"
308
+ rx="10"
309
+ ry="7"
310
+ stroke="currentColor"
311
+ stroke-width="2"
312
+ fill="none"
313
+ />
314
+ </svg>
315
+ {:else if t.tool === "cloud"}
316
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
317
+ <path
318
+ d="M6 19a4 4 0 01-.88-7.903A5 5 0 1115.9 6h.1a4 4 0 110 8h-4"
319
+ stroke="currentColor"
320
+ stroke-width="2"
321
+ stroke-linecap="round"
322
+ stroke-linejoin="round"
323
+ fill="none"
324
+ />
325
+ </svg>
326
+ {:else}
327
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
328
+ <path
329
+ d={t.icon}
330
+ stroke="currentColor"
331
+ stroke-width="2"
332
+ stroke-linecap="round"
333
+ stroke-linejoin="round"
334
+ fill="none"
335
+ />
336
+ </svg>
337
+ {/if}
338
+ <span class="hidden sm:inline text-[11px]">{t.label}</span>
339
+ </button>
340
+ {/each}
341
+ </div>
342
+
343
+ <div class="divider divider-horizontal mx-0.5 h-5"></div>
344
+
345
+ <!-- Color swatches -->
346
+ <div class="flex items-center gap-1">
347
+ {#each MARKUP_COLORS as c}
348
+ <button
349
+ class="w-5 h-5 rounded-full border-2 transition-transform {color === c
350
+ ? 'border-white scale-110'
351
+ : 'border-transparent hover:scale-105'}"
352
+ style="background: {c}; {c === '#111827'
353
+ ? 'border-color: #6b7280;'
354
+ : ''}"
355
+ onclick={() => {
356
+ color = c
357
+ }}
358
+ title={c}
359
+ ></button>
360
+ {/each}
361
+ </div>
362
+
363
+ <div class="divider divider-horizontal mx-0.5 h-5"></div>
364
+
365
+ <!-- Actions -->
366
+ <div class="flex items-center gap-1">
367
+ <button
368
+ class="btn btn-xs btn-ghost text-base-content/60"
369
+ onclick={undo}
370
+ disabled={shapes.length === 0}
371
+ title="Undo (Ctrl+Z)"
372
+ >
373
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
374
+ <path
375
+ d="M3 10h10a5 5 0 015 5v0a5 5 0 01-5 5H8M3 10l4-4M3 10l4 4"
376
+ stroke="currentColor"
377
+ stroke-width="2"
378
+ stroke-linecap="round"
379
+ stroke-linejoin="round"
380
+ />
381
+ </svg>
382
+ <span class="hidden sm:inline">Undo</span>
383
+ </button>
384
+ <button
385
+ class="btn btn-xs btn-ghost text-base-content/60"
386
+ onclick={clearAll}
387
+ disabled={shapes.length === 0}
388
+ title="Clear all"
389
+ >
390
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
391
+ <path
392
+ d="M3 6h18M8 6V4h8v2M10 11v6M14 11v6M5 6l1 14h12l1-14"
393
+ stroke="currentColor"
394
+ stroke-width="2"
395
+ stroke-linecap="round"
396
+ stroke-linejoin="round"
397
+ />
398
+ </svg>
399
+ <span class="hidden sm:inline">Clear</span>
400
+ </button>
401
+ </div>
402
+
403
+ <div class="flex-1"></div>
404
+
405
+ <!-- Save/Cancel -->
406
+ <div class="flex items-center gap-1.5">
407
+ <button class="btn btn-xs btn-ghost" onclick={oncancel}>Cancel</button>
408
+ <button class="btn btn-xs btn-primary" onclick={handleSave}>
409
+ Save Markup
410
+ </button>
411
+ </div>
412
+ </div>
413
+
414
+ <!-- Canvas area -->
415
+ <div
416
+ class="flex-1 flex items-center justify-center relative overflow-hidden p-5"
417
+ >
418
+ {#if loadError}
419
+ <div class="text-center text-error">
420
+ <p>Failed to load image</p>
421
+ </div>
422
+ {:else if loaded}
423
+ <canvas
424
+ bind:this={baseCanvas}
425
+ width={imgW}
426
+ height={imgH}
427
+ class="markup-base-canvas rounded"
428
+ ></canvas>
429
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
430
+ <canvas
431
+ bind:this={overlayCanvas}
432
+ width={imgW}
433
+ height={imgH}
434
+ class="markup-overlay-canvas"
435
+ class:cursor-crosshair={tool !== "text"}
436
+ class:cursor-text={tool === "text"}
437
+ onmousedown={onPointerDown}
438
+ onmousemove={onPointerMove}
439
+ onmouseup={onPointerUp}
440
+ ></canvas>
441
+ {:else}
442
+ <span class="loading loading-spinner loading-lg text-primary"></span>
443
+ {/if}
444
+ </div>
445
+
446
+ <!-- Text input overlay -->
447
+ {#if drawState === "typing"}
448
+ <input
449
+ bind:this={textInput}
450
+ type="text"
451
+ class="fixed bg-transparent border border-dashed border-white/50 outline-none text-base font-bold font-sans px-1 py-0.5 z-[60] min-w-[80px]"
452
+ style="left: {textInputCss.left}; top: {textInputCss.top}; color: {color};"
453
+ bind:value={textValue}
454
+ onkeydown={handleTextKeydown}
455
+ onblur={commitText}
456
+ placeholder="Type here..."
457
+ />
458
+ {/if}
459
+
460
+ <!-- Shapes count indicator -->
461
+ {#if shapes.length > 0}
462
+ <div
463
+ class="absolute bottom-4 left-4 badge badge-neutral badge-sm opacity-60"
464
+ >
465
+ {shapes.length} shape{shapes.length !== 1 ? "s" : ""}
466
+ </div>
467
+ {/if}
468
+ </div>
469
+
470
+ <style>
471
+ .markup-base-canvas,
472
+ .markup-overlay-canvas {
473
+ max-width: calc(100vw - 80px);
474
+ max-height: calc(100vh - 120px);
475
+ object-fit: contain;
476
+ }
477
+
478
+ .markup-base-canvas {
479
+ display: block;
480
+ }
481
+
482
+ .markup-overlay-canvas {
483
+ position: absolute;
484
+ }
485
+ </style>
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte"
3
+ import type { MarkupShape } from './markup'
4
+ import { renderAllShapes } from './markup'
5
+
6
+ interface Props {
7
+ shapes: MarkupShape[]
8
+ imageUrl: string
9
+ }
10
+
11
+ let { shapes, imageUrl }: Props = $props()
12
+
13
+ let canvas: HTMLCanvasElement | undefined = $state()
14
+ let imgW = $state(0)
15
+ let imgH = $state(0)
16
+ let loaded = $state(false)
17
+
18
+ function render() {
19
+ if (!canvas || !loaded || shapes.length === 0) return
20
+ const ctx = canvas.getContext("2d")
21
+ if (!ctx) return
22
+ canvas.width = imgW
23
+ canvas.height = imgH
24
+ ctx.clearRect(0, 0, imgW, imgH)
25
+ renderAllShapes(ctx, shapes)
26
+ }
27
+
28
+ onMount(() => {
29
+ const img = new Image()
30
+ img.crossOrigin = "anonymous"
31
+ img.onload = () => {
32
+ imgW = img.naturalWidth
33
+ imgH = img.naturalHeight
34
+ loaded = true
35
+ requestAnimationFrame(render)
36
+ }
37
+ img.src = imageUrl
38
+ })
39
+
40
+ // Re-render when shapes change
41
+ $effect(() => {
42
+ if (shapes && loaded) {
43
+ render()
44
+ }
45
+ })
46
+ </script>
47
+
48
+ {#if loaded && shapes.length > 0}
49
+ <canvas
50
+ bind:this={canvas}
51
+ width={imgW}
52
+ height={imgH}
53
+ class="absolute inset-0 w-full h-full pointer-events-none z-[5]"
54
+ ></canvas>
55
+ {/if}