@joewinke/jatui 0.2.6 → 0.2.8
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/CLAUDE.md +1 -1
- package/dist/components/AnnotationLayer.svelte +6 -0
- package/dist/components/AnnotationLayer.svelte.d.ts +3 -0
- package/dist/components/AnnotationPanel.svelte +25 -2
- package/dist/components/AnnotationPanel.svelte.d.ts +8 -0
- package/dist/components/ImageLightbox.svelte +9 -2
- package/dist/components/ImageLightbox.svelte.d.ts +4 -0
- package/dist/components/MarkupOverlay.svelte +23 -3
- package/dist/components/MarkupOverlay.svelte.d.ts +3 -0
- package/dist/components/SearchDropdown.svelte +13 -0
- package/dist/components/SearchDropdown.svelte.d.ts +2 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -108,7 +108,7 @@ Then import:
|
|
|
108
108
|
| `MarkupOverlay` | Read-only canvas layer that renders saved `MarkupShape[]` over an image | `shapes: MarkupShape[]`, `imageUrl` |
|
|
109
109
|
| `ImageLightbox` | Inline image viewer with fullscreen modal, zoom controls, optional markup + annotation overlays | `imageUrl`, `filename?`, `markupShapes?`, `annotations?`, `activeAnnotationId?`, `annotationMode?`, `showResolved?` (forwarded to the internal `AnnotationLayer`; default false), `onAnnotationClick?`, `onImageClick?`, `onClose?` |
|
|
110
110
|
| `AnnotationLayer` | Absolute-positioned pin overlay — renders threaded annotation pins at `x_percent`/`y_percent` coordinates | `annotations: Annotation[]`, `activeAnnotationId?`, `annotationMode?`, `showResolved?`, `onAnnotationClick?`, `onImageClick?` |
|
|
111
|
-
| `AnnotationPanel` | Sidebar thread list for annotations — reply, resolve, edit, delete. Uses callback props for async ops. | `annotations`, `profiles`, `activeAnnotationId?`, `currentUserId`, `callbacks: AnnotationCallbacks`, `readOnly?` (
|
|
111
|
+
| `AnnotationPanel` | Sidebar thread list for annotations — reply, resolve, edit, delete. Uses callback props for async ops. | `annotations`, `profiles`, `activeAnnotationId?`, `currentUserId`, `callbacks: AnnotationCallbacks`, `readOnly?`, `title?` (panel heading, default "Annotations"; pass "Comments" to match host vocabulary), `emptyText?` (empty-state line, default "No annotations yet"), `badges?` (per-annotation chip label by id, e.g. "Markup"), `onAnnotationHover?` (fires root id on hover / null on leave), `onAnnotationSelect?`, `onAnnotationsChanged?` |
|
|
112
112
|
|
|
113
113
|
**Types & utilities:**
|
|
114
114
|
```typescript
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
showResolved?: boolean
|
|
9
9
|
onAnnotationClick?: (annotation: Annotation) => void
|
|
10
10
|
onImageClick?: (x: number, y: number) => void
|
|
11
|
+
/** Fires with a pin's id on hover, null on leave — lets the host light up
|
|
12
|
+
* whatever that annotation links to (e.g. its markup). */
|
|
13
|
+
onAnnotationHover?: (id: string | null) => void
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
let {
|
|
@@ -17,6 +20,7 @@
|
|
|
17
20
|
showResolved = false,
|
|
18
21
|
onAnnotationClick,
|
|
19
22
|
onImageClick,
|
|
23
|
+
onAnnotationHover,
|
|
20
24
|
}: Props = $props()
|
|
21
25
|
|
|
22
26
|
// Group into threads (only show root pins)
|
|
@@ -60,6 +64,8 @@
|
|
|
60
64
|
<button
|
|
61
65
|
class="relative group flex flex-col items-center"
|
|
62
66
|
onclick={(e) => { e.stopPropagation(); onAnnotationClick?.(thread.root) }}
|
|
67
|
+
onmouseenter={() => onAnnotationHover?.(thread.root.id)}
|
|
68
|
+
onmouseleave={() => onAnnotationHover?.(null)}
|
|
63
69
|
title={thread.root.content.slice(0, 80)}
|
|
64
70
|
>
|
|
65
71
|
<!-- Pin icon -->
|
|
@@ -6,6 +6,9 @@ interface Props {
|
|
|
6
6
|
showResolved?: boolean;
|
|
7
7
|
onAnnotationClick?: (annotation: Annotation) => void;
|
|
8
8
|
onImageClick?: (x: number, y: number) => void;
|
|
9
|
+
/** Fires with a pin's id on hover, null on leave — lets the host light up
|
|
10
|
+
* whatever that annotation links to (e.g. its markup). */
|
|
11
|
+
onAnnotationHover?: (id: string | null) => void;
|
|
9
12
|
}
|
|
10
13
|
declare const AnnotationLayer: import("svelte").Component<Props, {}, "">;
|
|
11
14
|
type AnnotationLayer = ReturnType<typeof AnnotationLayer>;
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
callbacks: AnnotationCallbacks
|
|
10
10
|
/** Hides reply/resolve/edit/delete affordances — thread list stays readable */
|
|
11
11
|
readOnly?: boolean
|
|
12
|
+
/** Panel heading. Defaults to "Annotations"; pass e.g. "Comments" to match host vocabulary. */
|
|
13
|
+
title?: string
|
|
14
|
+
/** Empty-state line. Defaults to "No annotations yet". */
|
|
15
|
+
emptyText?: string
|
|
16
|
+
/** Optional per-annotation chip label, keyed by annotation id (e.g. "Markup"). */
|
|
17
|
+
badges?: Record<string, string>
|
|
18
|
+
/** Fires with a thread's root id on hover, null on leave. */
|
|
19
|
+
onAnnotationHover?: (id: string | null) => void
|
|
12
20
|
onAnnotationSelect?: (annotation: Annotation | null) => void
|
|
13
21
|
onAnnotationsChanged?: () => void
|
|
14
22
|
}
|
|
@@ -20,6 +28,10 @@
|
|
|
20
28
|
currentUserId,
|
|
21
29
|
callbacks,
|
|
22
30
|
readOnly = false,
|
|
31
|
+
title = "Annotations",
|
|
32
|
+
emptyText = "No annotations yet",
|
|
33
|
+
badges,
|
|
34
|
+
onAnnotationHover,
|
|
23
35
|
onAnnotationSelect,
|
|
24
36
|
onAnnotationsChanged,
|
|
25
37
|
}: Props = $props()
|
|
@@ -135,7 +147,7 @@
|
|
|
135
147
|
<!-- Header -->
|
|
136
148
|
<div class="flex items-center justify-between px-3 py-2 border-b border-base-300 flex-shrink-0">
|
|
137
149
|
<div class="flex items-center gap-2">
|
|
138
|
-
<h4 class="text-sm font-semibold">
|
|
150
|
+
<h4 class="text-sm font-semibold">{title}</h4>
|
|
139
151
|
{#if totalRoots > 0}
|
|
140
152
|
<span class="badge badge-xs badge-ghost">{totalRoots - resolvedCount} open</span>
|
|
141
153
|
{/if}
|
|
@@ -159,14 +171,17 @@
|
|
|
159
171
|
<svg class="mx-auto h-10 w-10 text-base-content/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
160
172
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
|
161
173
|
</svg>
|
|
162
|
-
<p class="text-sm text-base-content/40 mt-2">
|
|
174
|
+
<p class="text-sm text-base-content/40 mt-2">{emptyText}</p>
|
|
163
175
|
<p class="text-xs text-base-content/30 mt-1">Click the pin icon then click on the image to add one</p>
|
|
164
176
|
</div>
|
|
165
177
|
{:else}
|
|
166
178
|
{#each threads as thread (thread.root.id)}
|
|
167
179
|
{@const isActive = activeAnnotationId === thread.root.id}
|
|
180
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
168
181
|
<div
|
|
169
182
|
class="border-b border-base-200 transition-colors {isActive ? 'bg-primary/5' : 'hover:bg-base-200/50'}"
|
|
183
|
+
onmouseenter={() => onAnnotationHover?.(thread.root.id)}
|
|
184
|
+
onmouseleave={() => onAnnotationHover?.(null)}
|
|
170
185
|
>
|
|
171
186
|
<!-- Root annotation -->
|
|
172
187
|
<button
|
|
@@ -182,6 +197,14 @@
|
|
|
182
197
|
<div class="flex items-center gap-1.5">
|
|
183
198
|
<span class="text-xs font-medium text-base-content">{getAuthorName(thread.root.user_id)}</span>
|
|
184
199
|
<span class="text-[10px] text-base-content/40">{formatTime(thread.root.created_at)}</span>
|
|
200
|
+
{#if badges?.[thread.root.id]}
|
|
201
|
+
<span class="badge badge-xs badge-outline gap-0.5">
|
|
202
|
+
<svg class="w-2.5 h-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
203
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
204
|
+
</svg>
|
|
205
|
+
{badges[thread.root.id]}
|
|
206
|
+
</span>
|
|
207
|
+
{/if}
|
|
185
208
|
{#if thread.root.resolved}
|
|
186
209
|
<span class="badge badge-xs badge-success gap-0.5">
|
|
187
210
|
<svg class="w-2.5 h-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -7,6 +7,14 @@ interface Props {
|
|
|
7
7
|
callbacks: AnnotationCallbacks;
|
|
8
8
|
/** Hides reply/resolve/edit/delete affordances — thread list stays readable */
|
|
9
9
|
readOnly?: boolean;
|
|
10
|
+
/** Panel heading. Defaults to "Annotations"; pass e.g. "Comments" to match host vocabulary. */
|
|
11
|
+
title?: string;
|
|
12
|
+
/** Empty-state line. Defaults to "No annotations yet". */
|
|
13
|
+
emptyText?: string;
|
|
14
|
+
/** Optional per-annotation chip label, keyed by annotation id (e.g. "Markup"). */
|
|
15
|
+
badges?: Record<string, string>;
|
|
16
|
+
/** Fires with a thread's root id on hover, null on leave. */
|
|
17
|
+
onAnnotationHover?: (id: string | null) => void;
|
|
10
18
|
onAnnotationSelect?: (annotation: Annotation | null) => void;
|
|
11
19
|
onAnnotationsChanged?: () => void;
|
|
12
20
|
}
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
onAnnotationClick?: (annotation: Annotation) => void
|
|
24
24
|
/** Called when the image is clicked in annotation mode */
|
|
25
25
|
onImageClick?: (x: number, y: number) => void
|
|
26
|
+
/** Called with a pin's id on hover, null on leave */
|
|
27
|
+
onAnnotationHover?: (id: string | null) => void
|
|
28
|
+
/** Markup shape ids to accentuate (e.g. a hovered comment's linked markup) */
|
|
29
|
+
highlightedShapeIds?: number[]
|
|
26
30
|
/** Called when the lightbox requests close */
|
|
27
31
|
onClose?: () => void
|
|
28
32
|
}
|
|
@@ -37,6 +41,8 @@
|
|
|
37
41
|
showResolved = false,
|
|
38
42
|
onAnnotationClick,
|
|
39
43
|
onImageClick,
|
|
44
|
+
onAnnotationHover,
|
|
45
|
+
highlightedShapeIds,
|
|
40
46
|
onClose,
|
|
41
47
|
}: Props = $props()
|
|
42
48
|
|
|
@@ -162,7 +168,7 @@
|
|
|
162
168
|
|
|
163
169
|
<!-- Markup shapes overlay -->
|
|
164
170
|
{#if markupShapes.length > 0}
|
|
165
|
-
<MarkupOverlay shapes={markupShapes} {imageUrl} />
|
|
171
|
+
<MarkupOverlay shapes={markupShapes} {imageUrl} {highlightedShapeIds} />
|
|
166
172
|
{/if}
|
|
167
173
|
|
|
168
174
|
<!-- Annotation overlay -->
|
|
@@ -174,6 +180,7 @@
|
|
|
174
180
|
{showResolved}
|
|
175
181
|
{onAnnotationClick}
|
|
176
182
|
{onImageClick}
|
|
183
|
+
{onAnnotationHover}
|
|
177
184
|
/>
|
|
178
185
|
{/if}
|
|
179
186
|
</div>
|
|
@@ -262,7 +269,7 @@
|
|
|
262
269
|
style="transform: scale({zoom}); transform-origin: center center;"
|
|
263
270
|
/>
|
|
264
271
|
{#if markupShapes.length > 0}
|
|
265
|
-
<MarkupOverlay shapes={markupShapes} {imageUrl} />
|
|
272
|
+
<MarkupOverlay shapes={markupShapes} {imageUrl} {highlightedShapeIds} />
|
|
266
273
|
{/if}
|
|
267
274
|
{#if annotations.length > 0}
|
|
268
275
|
<AnnotationLayer
|
|
@@ -19,6 +19,10 @@ interface Props {
|
|
|
19
19
|
onAnnotationClick?: (annotation: Annotation) => void;
|
|
20
20
|
/** Called when the image is clicked in annotation mode */
|
|
21
21
|
onImageClick?: (x: number, y: number) => void;
|
|
22
|
+
/** Called with a pin's id on hover, null on leave */
|
|
23
|
+
onAnnotationHover?: (id: string | null) => void;
|
|
24
|
+
/** Markup shape ids to accentuate (e.g. a hovered comment's linked markup) */
|
|
25
|
+
highlightedShapeIds?: number[];
|
|
22
26
|
/** Called when the lightbox requests close */
|
|
23
27
|
onClose?: () => void;
|
|
24
28
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from "svelte"
|
|
3
3
|
import type { MarkupShape } from './markup'
|
|
4
|
-
import { renderAllShapes } from './markup'
|
|
4
|
+
import { renderAllShapes, renderShape } from './markup'
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
shapes: MarkupShape[]
|
|
8
8
|
imageUrl: string
|
|
9
|
+
/** Shape ids to accentuate (e.g. the markup a hovered comment links to).
|
|
10
|
+
* Drawn again on top with a colored glow so the linkage is visible. */
|
|
11
|
+
highlightedShapeIds?: number[]
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
let { shapes, imageUrl }: Props = $props()
|
|
14
|
+
let { shapes, imageUrl, highlightedShapeIds }: Props = $props()
|
|
12
15
|
|
|
13
16
|
let canvas: HTMLCanvasElement | undefined = $state()
|
|
14
17
|
let imgW = $state(0)
|
|
@@ -23,6 +26,22 @@
|
|
|
23
26
|
canvas.height = imgH
|
|
24
27
|
ctx.clearRect(0, 0, imgW, imgH)
|
|
25
28
|
renderAllShapes(ctx, shapes)
|
|
29
|
+
|
|
30
|
+
// Accentuate linked shapes: a soft glow in the shape's own colour plus a
|
|
31
|
+
// second pass for a bolder line, so a hovered comment's markup stands out.
|
|
32
|
+
const hi = highlightedShapeIds
|
|
33
|
+
if (hi && hi.length) {
|
|
34
|
+
const set = new Set(hi)
|
|
35
|
+
for (const shape of shapes) {
|
|
36
|
+
if (!set.has(shape.id)) continue
|
|
37
|
+
ctx.save()
|
|
38
|
+
ctx.shadowColor = shape.color
|
|
39
|
+
ctx.shadowBlur = 24
|
|
40
|
+
renderShape(ctx, shape)
|
|
41
|
+
renderShape(ctx, shape)
|
|
42
|
+
ctx.restore()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
onMount(() => {
|
|
@@ -37,8 +56,9 @@
|
|
|
37
56
|
img.src = imageUrl
|
|
38
57
|
})
|
|
39
58
|
|
|
40
|
-
// Re-render when shapes change
|
|
59
|
+
// Re-render when shapes OR the highlighted set change
|
|
41
60
|
$effect(() => {
|
|
61
|
+
void highlightedShapeIds
|
|
42
62
|
if (shapes && loaded) {
|
|
43
63
|
render()
|
|
44
64
|
}
|
|
@@ -2,6 +2,9 @@ import type { MarkupShape } from './markup';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
shapes: MarkupShape[];
|
|
4
4
|
imageUrl: string;
|
|
5
|
+
/** Shape ids to accentuate (e.g. the markup a hovered comment links to).
|
|
6
|
+
* Drawn again on top with a colored glow so the linkage is visible. */
|
|
7
|
+
highlightedShapeIds?: number[];
|
|
5
8
|
}
|
|
6
9
|
declare const MarkupOverlay: import("svelte").Component<Props, {}, "">;
|
|
7
10
|
type MarkupOverlay = ReturnType<typeof MarkupOverlay>;
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
disabled = false,
|
|
26
26
|
displayValue,
|
|
27
27
|
colorFn,
|
|
28
|
+
header,
|
|
28
29
|
footer,
|
|
29
30
|
variant = 'default',
|
|
30
31
|
size = 'md',
|
|
@@ -41,6 +42,8 @@
|
|
|
41
42
|
disabled?: boolean;
|
|
42
43
|
displayValue?: string;
|
|
43
44
|
colorFn?: (value: string) => string | undefined;
|
|
45
|
+
/** Rendered above the search input — use for panel titles, descriptions, or controls */
|
|
46
|
+
header?: Snippet;
|
|
44
47
|
footer?: Snippet;
|
|
45
48
|
/** 'chip' renders a compact TopBar-style pill instead of the full-width trigger */
|
|
46
49
|
variant?: 'default' | 'chip';
|
|
@@ -197,6 +200,11 @@
|
|
|
197
200
|
</script>
|
|
198
201
|
|
|
199
202
|
{#snippet panelBody()}
|
|
203
|
+
{#if header}
|
|
204
|
+
<div class="sd-header">
|
|
205
|
+
{@render header()}
|
|
206
|
+
</div>
|
|
207
|
+
{/if}
|
|
200
208
|
<!-- Search input -->
|
|
201
209
|
<div class="sd-search">
|
|
202
210
|
<div class="sd-search-inner">
|
|
@@ -525,6 +533,11 @@
|
|
|
525
533
|
box-shadow: 0 -4px 24px oklch(0 0 0 / 0.4);
|
|
526
534
|
}
|
|
527
535
|
|
|
536
|
+
/* Header section */
|
|
537
|
+
.sd-header {
|
|
538
|
+
border-bottom: 1px solid oklch(0.25 0.02 250);
|
|
539
|
+
}
|
|
540
|
+
|
|
528
541
|
/* Search section */
|
|
529
542
|
.sd-search {
|
|
530
543
|
padding: 0.375rem 0.5rem;
|
|
@@ -16,6 +16,8 @@ type $$ComponentProps = {
|
|
|
16
16
|
disabled?: boolean;
|
|
17
17
|
displayValue?: string;
|
|
18
18
|
colorFn?: (value: string) => string | undefined;
|
|
19
|
+
/** Rendered above the search input — use for panel titles, descriptions, or controls */
|
|
20
|
+
header?: Snippet;
|
|
19
21
|
footer?: Snippet;
|
|
20
22
|
/** 'chip' renders a compact TopBar-style pill instead of the full-width trigger */
|
|
21
23
|
variant?: 'default' | 'chip';
|