triiiceratops 0.12.7 → 0.12.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/README.md +44 -9
- package/dist/{ArrowCounterClockwise-BkrSndVO.js → ArrowCounterClockwise-DvXnnh0Z.js} +1 -1
- package/dist/{Check-CB_Zdp64.js → Check-BUdiej1n.js} +1 -1
- package/dist/{X-B3XldraD.js → X-DaQiCkfE.js} +225 -218
- package/dist/components/AnnotationOverlay.svelte +33 -248
- package/dist/components/AnnotationPanel.svelte +269 -0
- package/dist/components/AnnotationPanel.svelte.d.ts +3 -0
- package/dist/components/OSDViewer.svelte +0 -2
- package/dist/components/SettingsMenu.svelte +87 -1
- package/dist/components/TriiiceratopsViewer.svelte +19 -0
- package/dist/{image_filters_reset-jtpS22ff.js → image_filters_reset-P5hB896L.js} +1 -1
- package/dist/plugins/annotation-editor.js +3 -3
- package/dist/plugins/image-manipulation.js +3 -3
- package/dist/state/viewer.svelte.d.ts +1 -0
- package/dist/state/viewer.svelte.js +1 -0
- package/dist/triiiceratops-bundle.js +3393 -3329
- package/dist/triiiceratops-element.iife.js +24 -24
- package/dist/triiiceratops.css +1 -1
- package/dist/types/config.d.ts +15 -0
- package/package.json +1 -1
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
|
-
import CaretDown from 'phosphor-svelte/lib/CaretDown';
|
|
4
|
-
import Eye from 'phosphor-svelte/lib/Eye';
|
|
5
|
-
import EyeSlash from 'phosphor-svelte/lib/EyeSlash';
|
|
6
3
|
import { manifestsState } from '../state/manifests.svelte';
|
|
7
4
|
import { VIEWER_STATE_KEY, type ViewerState } from '../state/viewer.svelte';
|
|
8
|
-
import { m } from '../state/i18n.svelte';
|
|
9
|
-
import { extractBody } from '../utils/annotationAdapter';
|
|
10
5
|
|
|
11
6
|
const viewerState = getContext<ViewerState>(VIEWER_STATE_KEY);
|
|
12
7
|
|
|
@@ -54,52 +49,13 @@
|
|
|
54
49
|
}
|
|
55
50
|
});
|
|
56
51
|
|
|
57
|
-
//
|
|
58
|
-
let isAllVisible = $derived.by(() => {
|
|
59
|
-
if (annotations.length === 0) return false;
|
|
60
|
-
return annotations.every((a: any) => {
|
|
61
|
-
const id = getAnnotationId(a);
|
|
62
|
-
return !id || viewerState.visibleAnnotationIds.has(id);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
function toggleAnnotation(id: string) {
|
|
67
|
-
if (viewerState.visibleAnnotationIds.has(id)) {
|
|
68
|
-
viewerState.visibleAnnotationIds.delete(id);
|
|
69
|
-
} else {
|
|
70
|
-
viewerState.visibleAnnotationIds.add(id);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function toggleAllAnnotations() {
|
|
75
|
-
if (isAllVisible) {
|
|
76
|
-
// Hide all
|
|
77
|
-
viewerState.visibleAnnotationIds.clear();
|
|
78
|
-
} else {
|
|
79
|
-
// Show all
|
|
80
|
-
viewerState.visibleAnnotationIds.clear();
|
|
81
|
-
annotations.forEach((a: any) => {
|
|
82
|
-
const id = getAnnotationId(a);
|
|
83
|
-
if (id) viewerState.visibleAnnotationIds.add(id);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// State for hovered annotation to draw connecting line
|
|
89
|
-
let hoveredAnnotationId: string | null = $state(null);
|
|
52
|
+
// Connecting line logic
|
|
90
53
|
let toolbarContainer: HTMLElement | undefined = $state();
|
|
91
|
-
|
|
92
|
-
// Calculate coordinates for connecting line
|
|
93
|
-
let _connectingLine = $derived.by(() => {
|
|
94
|
-
if (!hoveredAnnotationId) return null;
|
|
95
|
-
return null;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
54
|
let lineCoords: { x1: number; y1: number; x2: number; y2: number } | null =
|
|
99
55
|
$state(null);
|
|
100
56
|
|
|
101
57
|
$effect(() => {
|
|
102
|
-
if (!hoveredAnnotationId) {
|
|
58
|
+
if (!viewerState.hoveredAnnotationId) {
|
|
103
59
|
lineCoords = null;
|
|
104
60
|
return;
|
|
105
61
|
}
|
|
@@ -113,25 +69,45 @@
|
|
|
113
69
|
}
|
|
114
70
|
}
|
|
115
71
|
|
|
72
|
+
// Note: The list item ID is now in AnnotationPanel, which must be rendered for this to work
|
|
116
73
|
const listItem = root.getElementById(
|
|
117
|
-
`annotation-list-item-${hoveredAnnotationId}`,
|
|
74
|
+
`annotation-list-item-${viewerState.hoveredAnnotationId}`,
|
|
118
75
|
);
|
|
119
76
|
const visual = root.getElementById(
|
|
120
|
-
`annotation-visual-${hoveredAnnotationId}`,
|
|
77
|
+
`annotation-visual-${viewerState.hoveredAnnotationId}`,
|
|
121
78
|
);
|
|
122
79
|
|
|
123
80
|
if (listItem && visual) {
|
|
124
81
|
const listRect = listItem.getBoundingClientRect();
|
|
125
82
|
const visualRect = visual.getBoundingClientRect();
|
|
126
83
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
const
|
|
84
|
+
// Determine start point based on panel position (left or right)
|
|
85
|
+
// We can heuristic this: if list is on right half of screen, connect to its left edge.
|
|
86
|
+
// If list is on left half, connect to its right edge.
|
|
87
|
+
const isRightPanel = listRect.left > window.innerWidth / 2;
|
|
88
|
+
|
|
89
|
+
let startX, startY, endX, endY;
|
|
90
|
+
|
|
91
|
+
if (isRightPanel) {
|
|
92
|
+
// Panel is on right, connect from left edge of list item
|
|
93
|
+
startX = listRect.left;
|
|
94
|
+
startY = listRect.top + listRect.height / 2;
|
|
95
|
+
|
|
96
|
+
// Connect to right edge of visual
|
|
97
|
+
endX = visualRect.right;
|
|
98
|
+
endY = visualRect.top + visualRect.height / 2;
|
|
99
|
+
} else {
|
|
100
|
+
// Panel is on left, connect from right edge of list item
|
|
101
|
+
startX = listRect.right;
|
|
102
|
+
startY = listRect.top + listRect.height / 2;
|
|
103
|
+
|
|
104
|
+
// Connect to left edge of visual
|
|
105
|
+
endX = visualRect.left;
|
|
106
|
+
endY = visualRect.top + visualRect.height / 2;
|
|
107
|
+
}
|
|
131
108
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const endY = visualRect.top + visualRect.height / 2;
|
|
109
|
+
endX = visualRect.left + visualRect.width / 2;
|
|
110
|
+
endY = visualRect.top + visualRect.height / 2;
|
|
135
111
|
|
|
136
112
|
lineCoords = { x1: startX, y1: startY, x2: endX, y2: endY };
|
|
137
113
|
} else {
|
|
@@ -148,23 +124,6 @@
|
|
|
148
124
|
|
|
149
125
|
return () => clearInterval(interval);
|
|
150
126
|
});
|
|
151
|
-
|
|
152
|
-
let renderedAnnotations = $derived.by(() => {
|
|
153
|
-
if (!annotations.length) return [];
|
|
154
|
-
|
|
155
|
-
return annotations.map((anno: any) => {
|
|
156
|
-
const bodies = extractBody(anno);
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
id: getAnnotationId(anno),
|
|
160
|
-
bodies,
|
|
161
|
-
label:
|
|
162
|
-
(typeof anno.getLabel === 'function'
|
|
163
|
-
? anno.getLabel()
|
|
164
|
-
: anno.label) || '',
|
|
165
|
-
};
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
127
|
</script>
|
|
169
128
|
|
|
170
129
|
{#if lineCoords}
|
|
@@ -196,179 +155,5 @@
|
|
|
196
155
|
</svg>
|
|
197
156
|
{/if}
|
|
198
157
|
|
|
199
|
-
<!--
|
|
200
|
-
{
|
|
201
|
-
<div
|
|
202
|
-
bind:this={toolbarContainer}
|
|
203
|
-
class="absolute top-4 right-4 z-40 pointer-events-auto transition-all duration-300"
|
|
204
|
-
>
|
|
205
|
-
<!-- z-index increased for Leaflet (z-400 is map) -->
|
|
206
|
-
<details class="group relative">
|
|
207
|
-
<summary
|
|
208
|
-
class="flex items-center gap-2 bg-base-200/90 backdrop-blur shadow-lg rounded-full px-4 py-2 cursor-pointer list-none hover:bg-base-200 transition-all select-none border border-base-300 pointer-events-auto"
|
|
209
|
-
>
|
|
210
|
-
<!-- Toggle Button -->
|
|
211
|
-
<button
|
|
212
|
-
class="btn btn-xs btn-circle btn-ghost"
|
|
213
|
-
onclick={(e) => {
|
|
214
|
-
e.preventDefault();
|
|
215
|
-
toggleAllAnnotations();
|
|
216
|
-
}}
|
|
217
|
-
title={isAllVisible
|
|
218
|
-
? m.hide_all_annotations()
|
|
219
|
-
: m.show_all_annotations()}
|
|
220
|
-
>
|
|
221
|
-
{#if isAllVisible}
|
|
222
|
-
<Eye size={16} weight="bold" />
|
|
223
|
-
{:else}
|
|
224
|
-
<EyeSlash size={16} weight="bold" />
|
|
225
|
-
{/if}
|
|
226
|
-
</button>
|
|
227
|
-
|
|
228
|
-
<!-- Badge Text -->
|
|
229
|
-
<span class="text-sm font-medium">
|
|
230
|
-
{m.annotations_count({ count: annotations.length })}
|
|
231
|
-
<span class="opacity-50 text-xs font-normal ml-1">
|
|
232
|
-
{m.visible_count({
|
|
233
|
-
count: viewerState.visibleAnnotationIds.size,
|
|
234
|
-
})}
|
|
235
|
-
</span>
|
|
236
|
-
</span>
|
|
237
|
-
|
|
238
|
-
<CaretDown
|
|
239
|
-
size={16}
|
|
240
|
-
weight="bold"
|
|
241
|
-
class="group-open:rotate-180 transition-transform opacity-80"
|
|
242
|
-
/>
|
|
243
|
-
</summary>
|
|
244
|
-
|
|
245
|
-
<!-- Expanded List -->
|
|
246
|
-
<div
|
|
247
|
-
class="absolute right-0 mt-2 w-96 bg-base-200/95 backdrop-blur shadow-xl rounded-box p-0 max-h-[60vh] overflow-y-auto border border-base-300 flex flex-col divide-y divide-base-300"
|
|
248
|
-
>
|
|
249
|
-
{#each renderedAnnotations as anno, i (anno.id)}
|
|
250
|
-
{@const isVisible = viewerState.visibleAnnotationIds.has(
|
|
251
|
-
anno.id,
|
|
252
|
-
)}
|
|
253
|
-
<!-- List Item Row -->
|
|
254
|
-
<div
|
|
255
|
-
id="annotation-list-item-{anno.id}"
|
|
256
|
-
class="w-full text-left p-3 hover:bg-base-300 transition-colors cursor-pointer flex gap-3 group/item items-start focus:outline-none focus:bg-base-300 relative"
|
|
257
|
-
role="button"
|
|
258
|
-
tabindex="0"
|
|
259
|
-
onmouseenter={() => (hoveredAnnotationId = anno.id)}
|
|
260
|
-
onmouseleave={() => (hoveredAnnotationId = null)}
|
|
261
|
-
onclick={(e) => {
|
|
262
|
-
e.preventDefault();
|
|
263
|
-
toggleAnnotation(anno.id);
|
|
264
|
-
}}
|
|
265
|
-
onkeypress={(e) => {
|
|
266
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
267
|
-
e.preventDefault();
|
|
268
|
-
toggleAnnotation(anno.id);
|
|
269
|
-
}
|
|
270
|
-
}}
|
|
271
|
-
>
|
|
272
|
-
<!-- Visual Toggle Indicator (eye icon button) -->
|
|
273
|
-
<!-- If user wants only the eye to toggle, we should stopPropagation on it,
|
|
274
|
-
and maybe remove toggle from the main click?
|
|
275
|
-
Wait, user updated task: "Changing the list item interaction so that clicking the item no longer key-toggles visibility. ... Disregard this."
|
|
276
|
-
So click anywhere ON the row still toggles.
|
|
277
|
-
-->
|
|
278
|
-
<button
|
|
279
|
-
class="btn btn-xs btn-circle btn-ghost mt-0.5 shrink-0"
|
|
280
|
-
onclick={(e) => {
|
|
281
|
-
e.stopPropagation();
|
|
282
|
-
toggleAnnotation(anno.id);
|
|
283
|
-
}}
|
|
284
|
-
>
|
|
285
|
-
{#if isVisible}
|
|
286
|
-
<Eye size={16} weight="bold" />
|
|
287
|
-
{:else}
|
|
288
|
-
<EyeSlash size={16} weight="bold" />
|
|
289
|
-
{/if}
|
|
290
|
-
</button>
|
|
291
|
-
|
|
292
|
-
<div class="flex-1 min-w-0 pointer-events-none">
|
|
293
|
-
<div class="flex items-start justify-between">
|
|
294
|
-
<span class="font-bold text-sm text-primary"
|
|
295
|
-
>#{i + 1}</span
|
|
296
|
-
>
|
|
297
|
-
<!-- Only show label separately if it's different from the content being displayed -->
|
|
298
|
-
{#if anno.label && !anno.bodies.some((b) => b.value === anno.label)}
|
|
299
|
-
<span
|
|
300
|
-
class="text-xs opacity-50 truncate max-w-[150px]"
|
|
301
|
-
>{anno.label}</span
|
|
302
|
-
>
|
|
303
|
-
{/if}
|
|
304
|
-
</div>
|
|
305
|
-
<div
|
|
306
|
-
class="text-sm prose prose-sm max-w-none prose-p:my-0 prose-a:text-blue-500 wrap-break-word text-left {isVisible
|
|
307
|
-
? ''
|
|
308
|
-
: 'opacity-50'} space-y-2"
|
|
309
|
-
>
|
|
310
|
-
{#each anno.bodies as body, i (i)}
|
|
311
|
-
<div
|
|
312
|
-
class="flex flex-wrap gap-2 pointer-events-auto"
|
|
313
|
-
>
|
|
314
|
-
{#if body.purpose === 'tagging'}
|
|
315
|
-
<span
|
|
316
|
-
class="badge badge-primary badge-outline badge-sm"
|
|
317
|
-
>
|
|
318
|
-
{body.value}
|
|
319
|
-
</span>
|
|
320
|
-
{:else if body.purpose === 'linking'}
|
|
321
|
-
<a
|
|
322
|
-
href={body.value}
|
|
323
|
-
target="_blank"
|
|
324
|
-
rel="noopener noreferrer"
|
|
325
|
-
class="flex items-center gap-1 text-primary hover:underline hover:text-primary-focus p-1 rounded hover:bg-base-200 -ml-1 transition-colors"
|
|
326
|
-
onclick={(e) =>
|
|
327
|
-
e.stopPropagation()}
|
|
328
|
-
>
|
|
329
|
-
<!-- Link Icon -->
|
|
330
|
-
<svg
|
|
331
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
332
|
-
width="12"
|
|
333
|
-
height="12"
|
|
334
|
-
fill="currentColor"
|
|
335
|
-
viewBox="0 0 256 256"
|
|
336
|
-
><path
|
|
337
|
-
d="M136.37,187.53a12,12,0,0,1,0,17l-5.94,5.94a60,60,0,0,1-84.88-84.88l24.12-24.12A60,60,0,0,1,152.06,99,12,12,0,1,1,135,116a36,36,0,0,0-50.93,1.57L60,141.66a36,36,0,0,0,50.93,50.93l5.94-5.94A12,12,0,0,1,136.37,187.53Zm81.51-149.41a60,60,0,0,0-84.88,0l-5.94,5.94a12,12,0,0,0,17,17l5.94-5.94a36,36,0,0,1,50.93,50.93l-24.11,24.12A36,36,0,0,1,121,140a12,12,0,1,0-17.08,17,60,60,0,0,0,82.39,2.46l24.12-24.12A60,60,0,0,0,217.88,38.12Z"
|
|
338
|
-
></path></svg
|
|
339
|
-
>
|
|
340
|
-
<span
|
|
341
|
-
class="truncate max-w-[200px]"
|
|
342
|
-
>{body.value}</span
|
|
343
|
-
>
|
|
344
|
-
</a>
|
|
345
|
-
{:else}
|
|
346
|
-
<!-- Commenting / Default -->
|
|
347
|
-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
348
|
-
{#if body.isHtml}
|
|
349
|
-
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
350
|
-
{@html body.value}
|
|
351
|
-
{:else}
|
|
352
|
-
{body.value || '(No content)'}
|
|
353
|
-
{/if}
|
|
354
|
-
{/if}
|
|
355
|
-
</div>
|
|
356
|
-
{/each}
|
|
357
|
-
|
|
358
|
-
{#if anno.bodies.length === 0}
|
|
359
|
-
<span class="opacity-50 italic text-xs"
|
|
360
|
-
>(No content)</span
|
|
361
|
-
>
|
|
362
|
-
{/if}
|
|
363
|
-
</div>
|
|
364
|
-
</div>
|
|
365
|
-
</div>
|
|
366
|
-
{:else}
|
|
367
|
-
<div class="p-4 text-center opacity-50 text-sm">
|
|
368
|
-
No annotations available.
|
|
369
|
-
</div>
|
|
370
|
-
{/each}
|
|
371
|
-
</div>
|
|
372
|
-
</details>
|
|
373
|
-
</div>
|
|
374
|
-
{/if}
|
|
158
|
+
<!-- Hidden element to capture root node context if needed, though document.getElementById usually works globally -->
|
|
159
|
+
<div bind:this={toolbarContainer} class="hidden"></div>
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext } from 'svelte';
|
|
3
|
+
import X from 'phosphor-svelte/lib/X';
|
|
4
|
+
import Eye from 'phosphor-svelte/lib/Eye';
|
|
5
|
+
import EyeSlash from 'phosphor-svelte/lib/EyeSlash';
|
|
6
|
+
import ListDashes from 'phosphor-svelte/lib/ListDashes';
|
|
7
|
+
import { VIEWER_STATE_KEY, type ViewerState } from '../state/viewer.svelte';
|
|
8
|
+
import { manifestsState } from '../state/manifests.svelte';
|
|
9
|
+
import { m } from '../state/i18n.svelte';
|
|
10
|
+
import { extractBody } from '../utils/annotationAdapter';
|
|
11
|
+
|
|
12
|
+
const viewerState = getContext<ViewerState>(VIEWER_STATE_KEY);
|
|
13
|
+
|
|
14
|
+
let width = $derived(viewerState.config.annotations?.width ?? '320px');
|
|
15
|
+
let position = $derived(
|
|
16
|
+
viewerState.config.annotations?.position ?? 'right',
|
|
17
|
+
);
|
|
18
|
+
let showCloseButton = $derived(
|
|
19
|
+
viewerState.config.annotations?.showCloseButton ?? true,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
let annotations = $derived.by(() => {
|
|
23
|
+
if (!viewerState.manifestId || !viewerState.canvasId) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const manifestAnnotations = manifestsState.getAnnotations(
|
|
27
|
+
viewerState.manifestId,
|
|
28
|
+
viewerState.canvasId,
|
|
29
|
+
);
|
|
30
|
+
// Add search hits for current canvas
|
|
31
|
+
const searchAnnotations = viewerState.currentCanvasSearchAnnotations;
|
|
32
|
+
|
|
33
|
+
return [...manifestAnnotations, ...searchAnnotations];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Helper to get ID from annotation object
|
|
37
|
+
function getAnnotationId(anno: any): string {
|
|
38
|
+
return (
|
|
39
|
+
anno.id ||
|
|
40
|
+
anno['@id'] ||
|
|
41
|
+
(typeof anno.getId === 'function' ? anno.getId() : '') ||
|
|
42
|
+
''
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let renderedAnnotations = $derived.by(() => {
|
|
47
|
+
if (!annotations.length) return [];
|
|
48
|
+
|
|
49
|
+
return annotations.map((anno: any) => {
|
|
50
|
+
const bodies = extractBody(anno);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: getAnnotationId(anno),
|
|
54
|
+
bodies,
|
|
55
|
+
label:
|
|
56
|
+
(typeof anno.getLabel === 'function'
|
|
57
|
+
? anno.getLabel()
|
|
58
|
+
: anno.label) || '',
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Derived state for "All Visible" status
|
|
64
|
+
let isAllVisible = $derived.by(() => {
|
|
65
|
+
if (annotations.length === 0) return false;
|
|
66
|
+
return annotations.every((a: any) => {
|
|
67
|
+
const id = getAnnotationId(a);
|
|
68
|
+
return !id || viewerState.visibleAnnotationIds.has(id);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function toggleAnnotation(id: string) {
|
|
73
|
+
if (viewerState.visibleAnnotationIds.has(id)) {
|
|
74
|
+
viewerState.visibleAnnotationIds.delete(id);
|
|
75
|
+
} else {
|
|
76
|
+
viewerState.visibleAnnotationIds.add(id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function toggleAllAnnotations() {
|
|
81
|
+
if (isAllVisible) {
|
|
82
|
+
// Hide all
|
|
83
|
+
viewerState.visibleAnnotationIds.clear();
|
|
84
|
+
} else {
|
|
85
|
+
// Show all
|
|
86
|
+
viewerState.visibleAnnotationIds.clear();
|
|
87
|
+
annotations.forEach((a: any) => {
|
|
88
|
+
const id = getAnnotationId(a);
|
|
89
|
+
if (id) viewerState.visibleAnnotationIds.add(id);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<!-- Drawer / Panel -->
|
|
96
|
+
{#if viewerState.showAnnotations}
|
|
97
|
+
<div
|
|
98
|
+
class="h-full bg-base-200 shadow-2xl z-100 flex flex-col transition-[width] duration-200 {viewerState
|
|
99
|
+
.config.transparentBackground
|
|
100
|
+
? ''
|
|
101
|
+
: position === 'left'
|
|
102
|
+
? 'border-r border-base-300'
|
|
103
|
+
: 'border-l border-base-300'}"
|
|
104
|
+
style="width: {width}"
|
|
105
|
+
role="dialog"
|
|
106
|
+
aria-label={m.settings_submenu_annotations()}
|
|
107
|
+
>
|
|
108
|
+
<!-- Header -->
|
|
109
|
+
<div
|
|
110
|
+
class="flex items-center justify-between p-4 border-b border-base-300"
|
|
111
|
+
>
|
|
112
|
+
<div class="flex items-center gap-2">
|
|
113
|
+
<ListDashes size={20} weight="bold" />
|
|
114
|
+
<h2 class="font-bold text-lg">
|
|
115
|
+
{m.settings_submenu_annotations()}
|
|
116
|
+
</h2>
|
|
117
|
+
</div>
|
|
118
|
+
{#if showCloseButton}
|
|
119
|
+
<button
|
|
120
|
+
class="btn btn-sm btn-circle btn-ghost"
|
|
121
|
+
onclick={() => viewerState.toggleAnnotations()}
|
|
122
|
+
aria-label={m.close()}
|
|
123
|
+
>
|
|
124
|
+
<X size={20} weight="bold" />
|
|
125
|
+
</button>
|
|
126
|
+
{/if}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Toolbar / Stats -->
|
|
130
|
+
<div
|
|
131
|
+
class="p-4 border-b border-base-300 bg-base-100/50 flex items-center justify-between"
|
|
132
|
+
>
|
|
133
|
+
<div class="text-sm font-medium opacity-80">
|
|
134
|
+
{m.annotations_count({ count: annotations.length })}
|
|
135
|
+
</div>
|
|
136
|
+
<button
|
|
137
|
+
class="btn btn-sm btn-ghost gap-2"
|
|
138
|
+
onclick={toggleAllAnnotations}
|
|
139
|
+
disabled={annotations.length === 0}
|
|
140
|
+
>
|
|
141
|
+
{#if isAllVisible}
|
|
142
|
+
<Eye size={16} weight="bold" />
|
|
143
|
+
{m.hide_all_annotations()}
|
|
144
|
+
{:else}
|
|
145
|
+
<EyeSlash size={16} weight="bold" />
|
|
146
|
+
{m.show_all_annotations()}
|
|
147
|
+
{/if}
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- List -->
|
|
152
|
+
<div
|
|
153
|
+
class="flex-1 overflow-y-auto p-0 flex flex-col divide-y divide-base-300"
|
|
154
|
+
>
|
|
155
|
+
{#each renderedAnnotations as anno, i (anno.id)}
|
|
156
|
+
{@const isVisible = viewerState.visibleAnnotationIds.has(
|
|
157
|
+
anno.id,
|
|
158
|
+
)}
|
|
159
|
+
<!-- List Item Row -->
|
|
160
|
+
<div
|
|
161
|
+
class="w-full text-left p-4 hover:bg-base-100 transition-colors cursor-pointer flex gap-3 group/item items-start focus:outline-none focus:bg-base-100 relative {isVisible
|
|
162
|
+
? ''
|
|
163
|
+
: 'opacity-60 bg-base-200/50'}"
|
|
164
|
+
role="button"
|
|
165
|
+
tabindex="0"
|
|
166
|
+
id="annotation-list-item-{anno.id}"
|
|
167
|
+
onmouseenter={() =>
|
|
168
|
+
(viewerState.hoveredAnnotationId = anno.id)}
|
|
169
|
+
onmouseleave={() =>
|
|
170
|
+
(viewerState.hoveredAnnotationId = null)}
|
|
171
|
+
onclick={(e) => {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
toggleAnnotation(anno.id);
|
|
174
|
+
}}
|
|
175
|
+
onkeypress={(e) => {
|
|
176
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
toggleAnnotation(anno.id);
|
|
179
|
+
}
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
<!-- Visual Toggle Indicator (eye icon button) -->
|
|
183
|
+
<button
|
|
184
|
+
class="btn btn-xs btn-circle btn-ghost mt-0.5 shrink-0"
|
|
185
|
+
onclick={(e) => {
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
toggleAnnotation(anno.id);
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{#if isVisible}
|
|
191
|
+
<Eye size={16} weight="bold" />
|
|
192
|
+
{:else}
|
|
193
|
+
<EyeSlash size={16} weight="bold" />
|
|
194
|
+
{/if}
|
|
195
|
+
</button>
|
|
196
|
+
|
|
197
|
+
<div class="flex-1 min-w-0 pointer-events-none">
|
|
198
|
+
<div class="flex items-start justify-between mb-1">
|
|
199
|
+
<span class="font-bold text-sm text-primary"
|
|
200
|
+
>#{i + 1}</span
|
|
201
|
+
>
|
|
202
|
+
<!-- Only show label separately if it's different from the content being displayed -->
|
|
203
|
+
{#if anno.label && !anno.bodies.some((b) => b.value === anno.label)}
|
|
204
|
+
<span
|
|
205
|
+
class="text-xs opacity-50 truncate max-w-[150px]"
|
|
206
|
+
>{anno.label}</span
|
|
207
|
+
>
|
|
208
|
+
{/if}
|
|
209
|
+
</div>
|
|
210
|
+
<div
|
|
211
|
+
class="text-sm prose prose-sm max-w-none prose-p:my-0 prose-a:text-blue-500 wrap-break-word text-left space-y-2"
|
|
212
|
+
>
|
|
213
|
+
{#each anno.bodies as body, i (i)}
|
|
214
|
+
<div
|
|
215
|
+
class="flex flex-wrap gap-2 pointer-events-auto"
|
|
216
|
+
>
|
|
217
|
+
{#if body.purpose === 'tagging'}
|
|
218
|
+
<span
|
|
219
|
+
class="badge badge-primary badge-outline badge-sm"
|
|
220
|
+
>
|
|
221
|
+
{body.value}
|
|
222
|
+
</span>
|
|
223
|
+
{:else if body.purpose === 'linking'}
|
|
224
|
+
<a
|
|
225
|
+
href={body.value}
|
|
226
|
+
target="_blank"
|
|
227
|
+
rel="noopener noreferrer"
|
|
228
|
+
class="flex items-center gap-1 text-primary hover:underline hover:text-primary-focus p-1 rounded hover:bg-base-200 -ml-1 transition-colors"
|
|
229
|
+
onclick={(e) => e.stopPropagation()}
|
|
230
|
+
>
|
|
231
|
+
<!-- Link Icon -->
|
|
232
|
+
<svg
|
|
233
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
234
|
+
width="12"
|
|
235
|
+
height="12"
|
|
236
|
+
fill="currentColor"
|
|
237
|
+
viewBox="0 0 256 256"
|
|
238
|
+
><path
|
|
239
|
+
d="M136.37,187.53a12,12,0,0,1,0,17l-5.94,5.94a60,60,0,0,1-84.88-84.88l24.12-24.12A60,60,0,0,1,152.06,99,12,12,0,1,1,135,116a36,36,0,0,0-50.93,1.57L60,141.66a36,36,0,0,0,50.93,50.93l5.94-5.94A12,12,0,0,1,136.37,187.53Zm81.51-149.41a60,60,0,0,0-84.88,0l-5.94,5.94a12,12,0,0,0,17,17l5.94-5.94a36,36,0,0,1,50.93,50.93l-24.11,24.12A36,36,0,0,1,121,140a12,12,0,1,0-17.08,17,60,60,0,0,0,82.39,2.46l24.12-24.12A60,60,0,0,0,217.88,38.12Z"
|
|
240
|
+
></path></svg
|
|
241
|
+
>
|
|
242
|
+
<span class="truncate max-w-[200px]"
|
|
243
|
+
>{body.value}</span
|
|
244
|
+
>
|
|
245
|
+
</a>
|
|
246
|
+
{:else if body.isHtml}
|
|
247
|
+
{@html body.value}
|
|
248
|
+
{:else}
|
|
249
|
+
{body.value || '(No content)'}
|
|
250
|
+
{/if}
|
|
251
|
+
</div>
|
|
252
|
+
{/each}
|
|
253
|
+
|
|
254
|
+
{#if anno.bodies.length === 0}
|
|
255
|
+
<span class="opacity-50 italic text-xs"
|
|
256
|
+
>{m.no_content()}</span
|
|
257
|
+
>
|
|
258
|
+
{/if}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
{:else}
|
|
263
|
+
<div class="p-8 text-center opacity-50 text-sm">
|
|
264
|
+
{m.no_annotations_available()}
|
|
265
|
+
</div>
|
|
266
|
+
{/each}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
{/if}
|