triiiceratops 0.12.6 → 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.
@@ -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
- // Derived state for "All Visible" status
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
- // Calculate connection points
128
- // List item: Left center
129
- const startX = listRect.left;
130
- const startY = listRect.top + listRect.height / 2;
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
- // Visual: Center of the annotation visual
133
- const endX = visualRect.left + visualRect.width / 2;
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
- <!-- Unified Annotation Toolbar -->
200
- {#if viewerState.showAnnotations}
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}
@@ -0,0 +1,3 @@
1
+ declare const AnnotationPanel: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type AnnotationPanel = ReturnType<typeof AnnotationPanel>;
3
+ export default AnnotationPanel;
@@ -74,8 +74,6 @@
74
74
  // Filter based on visibility
75
75
  if (anno.isSearchHit) {
76
76
  // Search hits are always visible
77
- } else if (!viewerState.showAnnotations) {
78
- continue;
79
77
  } else if (!viewerState.visibleAnnotationIds.has(anno.id)) {
80
78
  continue;
81
79
  }