triiiceratops 0.11.1 → 0.12.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.
Files changed (58) hide show
  1. package/README.md +17 -9
  2. package/dist/{ArrowCounterClockwise-CN8KGaI0.js → ArrowCounterClockwise-CM9mGGcp.js} +1 -1
  3. package/dist/X-Bn7S7vUL.js +963 -0
  4. package/dist/{annotation_tool_point-BpZXtX5D.js → annotation_tool_point-LoRp_nrI.js} +1 -1
  5. package/dist/annotorious-openseadragon.es-tb5X-LtF.js +33045 -0
  6. package/dist/components/AnnotationOverlay.svelte +10 -17
  7. package/dist/components/DemoHeader.svelte +73 -5
  8. package/dist/components/MetadataDialog.svelte +4 -1
  9. package/dist/components/OSDViewer.svelte +39 -3
  10. package/dist/components/SearchPanel.svelte +8 -5
  11. package/dist/components/ThemeToggle.svelte +1 -1
  12. package/dist/components/ThumbnailGallery.svelte +229 -38
  13. package/dist/components/Toolbar.svelte +105 -6
  14. package/dist/components/TriiiceratopsViewer.svelte +37 -12
  15. package/dist/components/TriiiceratopsViewerElement.svelte +3 -1
  16. package/dist/custom-element.js +1 -0
  17. package/dist/{image_filters_reset-CyWg622b.js → image_filters_reset-CmWuQiOc.js} +1 -1
  18. package/dist/paraglide/messages/_index.d.ts +9 -0
  19. package/dist/paraglide/messages/_index.js +10 -1
  20. package/dist/paraglide/messages/settings_toggle_show_viewing_mode.d.ts +4 -0
  21. package/dist/paraglide/messages/settings_toggle_show_viewing_mode.js +33 -0
  22. package/dist/paraglide/messages/show_mode_toggle.d.ts +4 -0
  23. package/dist/paraglide/messages/show_mode_toggle.js +33 -0
  24. package/dist/paraglide/messages/toggle_single_page_mode.d.ts +4 -0
  25. package/dist/paraglide/messages/toggle_single_page_mode.js +33 -0
  26. package/dist/paraglide/messages/toggle_two_page_mode.d.ts +4 -0
  27. package/dist/paraglide/messages/toggle_two_page_mode.js +33 -0
  28. package/dist/paraglide/messages/two_page_mode.d.ts +4 -0
  29. package/dist/paraglide/messages/two_page_mode.js +33 -0
  30. package/dist/paraglide/messages/viewing_mode_individuals.d.ts +4 -0
  31. package/dist/paraglide/messages/viewing_mode_individuals.js +33 -0
  32. package/dist/paraglide/messages/viewing_mode_label.d.ts +4 -0
  33. package/dist/paraglide/messages/viewing_mode_label.js +33 -0
  34. package/dist/paraglide/messages/viewing_mode_paged.d.ts +4 -0
  35. package/dist/paraglide/messages/viewing_mode_paged.js +33 -0
  36. package/dist/paraglide/messages/viewing_mode_shift_pairing.d.ts +4 -0
  37. package/dist/paraglide/messages/viewing_mode_shift_pairing.js +33 -0
  38. package/dist/plugins/annotation-editor/AnnotationEditorController.svelte +5 -3
  39. package/dist/plugins/annotation-editor/AnnotationEditorPanel.svelte +3 -3
  40. package/dist/plugins/annotation-editor/AnnotationManager.svelte.d.ts +3 -0
  41. package/dist/plugins/annotation-editor/AnnotationManager.svelte.js +19 -14
  42. package/dist/plugins/annotation-editor/loader.svelte.js +2 -2
  43. package/dist/plugins/annotation-editor.js +1228 -32159
  44. package/dist/plugins/image-manipulation/ImageManipulationController.svelte +1 -1
  45. package/dist/plugins/image-manipulation.js +3 -3
  46. package/dist/state/manifests.svelte.d.ts +2 -1
  47. package/dist/state/manifests.svelte.js +5 -9
  48. package/dist/state/manifests.test.js +52 -50
  49. package/dist/state/viewer.svelte.d.ts +20 -1
  50. package/dist/state/viewer.svelte.js +150 -16
  51. package/dist/triiiceratops-bundle.js +3107 -2584
  52. package/dist/triiiceratops-element.iife.js +26 -26
  53. package/dist/triiiceratops.css +1 -1
  54. package/dist/types/config.d.ts +33 -0
  55. package/dist/utils/annotationAdapter.js +2 -2
  56. package/dist/utils/annotationAdapter.test.js +0 -1
  57. package/package.json +12 -2
  58. package/dist/X-i_EmjXwW.js +0 -906
@@ -9,7 +9,7 @@
9
9
  import ImageManipulationPanel from './ImageManipulationPanel.svelte';
10
10
 
11
11
  // Props from the plugin system
12
- let { isOpen = false, close } = $props();
12
+ let { isOpen: _isOpen = false, close } = $props();
13
13
 
14
14
  const viewerState = getContext<ViewerState>(VIEWER_STATE_KEY);
15
15
  let filters = $state<ImageFilters>({ ...DEFAULT_FILTERS });
@@ -1,9 +1,9 @@
1
1
  import "svelte/internal/disclose-version";
2
2
  import * as e from "svelte/internal/client";
3
3
  import { getContext as s0 } from "svelte";
4
- import { l as l0, s as i0, X as n0, c as o0, V as c0, g as v0 } from "../X-i_EmjXwW.js";
5
- import { A as d0 } from "../ArrowCounterClockwise-CN8KGaI0.js";
6
- import { i as _0, a as g0, b as f0, g as u0, c as h0, e as m0, d as p0, f as b0 } from "../image_filters_reset-CyWg622b.js";
4
+ import { l as l0, s as i0, X as n0, c as o0, V as c0, g as v0 } from "../X-Bn7S7vUL.js";
5
+ import { A as d0 } from "../ArrowCounterClockwise-CM9mGGcp.js";
6
+ import { i as _0, a as g0, b as f0, g as u0, c as h0, e as m0, d as p0, f as b0 } from "../image_filters_reset-CmWuQiOc.js";
7
7
  const G = {
8
8
  brightness: 100,
9
9
  contrast: 100,
@@ -1,3 +1,4 @@
1
+ import { SvelteMap } from 'svelte/reactivity';
1
2
  export interface ManifestEntry {
2
3
  json?: any;
3
4
  manifesto?: any;
@@ -6,7 +7,7 @@ export interface ManifestEntry {
6
7
  }
7
8
  export declare class ManifestsState {
8
9
  manifests: Record<string, ManifestEntry>;
9
- userAnnotations: Map<string, any[]>;
10
+ userAnnotations: SvelteMap<string, any[]>;
10
11
  constructor();
11
12
  private userAnnotationKey;
12
13
  setUserAnnotations(manifestId: string, canvasId: string, annotations: any[]): void;
@@ -1,8 +1,9 @@
1
+ import { SvelteMap } from 'svelte/reactivity';
1
2
  import * as manifesto from 'manifesto.js';
2
3
  export class ManifestsState {
3
4
  manifests = $state({});
4
5
  // User-created annotations (from plugins like annotation editor)
5
- userAnnotations = $state(new Map());
6
+ userAnnotations = new SvelteMap();
6
7
  constructor() { }
7
8
  // === User Annotations API ===
8
9
  userAnnotationKey(manifestId, canvasId) {
@@ -10,17 +11,12 @@ export class ManifestsState {
10
11
  }
11
12
  setUserAnnotations(manifestId, canvasId, annotations) {
12
13
  const key = this.userAnnotationKey(manifestId, canvasId);
13
- // Create a new Map to trigger reactivity
14
- const newMap = new Map(this.userAnnotations);
15
- newMap.set(key, annotations);
16
- this.userAnnotations = newMap;
14
+ this.userAnnotations.set(key, annotations);
17
15
  }
18
16
  clearUserAnnotations(manifestId, canvasId) {
19
17
  const key = this.userAnnotationKey(manifestId, canvasId);
20
18
  if (this.userAnnotations.has(key)) {
21
- const newMap = new Map(this.userAnnotations);
22
- newMap.delete(key);
23
- this.userAnnotations = newMap;
19
+ this.userAnnotations.delete(key);
24
20
  }
25
21
  }
26
22
  getUserAnnotations(manifestId, canvasId) {
@@ -109,7 +105,7 @@ export class ManifestsState {
109
105
  // Manifesto wraps the JSON. We can access the underlying JSON via canvas.__jsonld
110
106
  // Or better, use canvas.getContent() if it works, but for external lists manual fetch is robust.
111
107
  const canvasJson = canvas.__jsonld;
112
- let annotations = [];
108
+ const annotations = [];
113
109
  // Helper to parse list using Manifesto
114
110
  const parseList = (listJson) => {
115
111
  // manifesto.create is not available in 4.3.0 or not exported nicely?
@@ -1,33 +1,33 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { ManifestsState } from "./manifests.svelte";
3
- import * as manifesto from "manifesto.js";
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ManifestsState } from './manifests.svelte';
3
+ import * as manifesto from 'manifesto.js';
4
4
  // Mock manifesto.js since it's an external dependency
5
- vi.mock("manifesto.js", async (importOriginal) => {
5
+ vi.mock('manifesto.js', async (importOriginal) => {
6
6
  const actual = await importOriginal();
7
7
  return {
8
8
  ...actual,
9
- parseManifest: vi.fn((json) => {
9
+ parseManifest: vi.fn((_json) => {
10
10
  // Minimal mock of a manifesto object
11
11
  return {
12
12
  getSequences: () => [
13
13
  {
14
- getCanvases: () => [{ id: "canvas1" }],
14
+ getCanvases: () => [{ id: 'canvas1' }],
15
15
  getCanvasById: (id) => {
16
- if (id === "canvas1") {
16
+ if (id === 'canvas1') {
17
17
  return {
18
- id: "canvas1",
18
+ id: 'canvas1',
19
19
  __jsonld: {
20
20
  otherContent: [
21
21
  {
22
- "@id": "http://example.org/list1",
23
- "@type": "sc:AnnotationList",
22
+ '@id': 'http://example.org/list1',
23
+ '@type': 'sc:AnnotationList',
24
24
  },
25
25
  ],
26
26
  annotations: [
27
27
  // v3 style
28
28
  {
29
- id: "http://example.org/list2",
30
- type: "AnnotationPage",
29
+ id: 'http://example.org/list2',
30
+ type: 'AnnotationPage',
31
31
  },
32
32
  ],
33
33
  },
@@ -41,80 +41,82 @@ vi.mock("manifesto.js", async (importOriginal) => {
41
41
  }),
42
42
  };
43
43
  });
44
- describe("ManifestsState", () => {
44
+ describe('ManifestsState', () => {
45
45
  let state;
46
46
  const mockFetch = vi.fn();
47
47
  beforeEach(() => {
48
- vi.stubGlobal("fetch", mockFetch);
48
+ vi.stubGlobal('fetch', mockFetch);
49
49
  state = new ManifestsState();
50
50
  mockFetch.mockReset();
51
51
  });
52
52
  afterEach(() => {
53
53
  vi.restoreAllMocks();
54
54
  });
55
- describe("fetchManifest", () => {
56
- it("should fetch and store a manifest", async () => {
55
+ describe('fetchManifest', () => {
56
+ it('should fetch and store a manifest', async () => {
57
57
  const mockManifest = {
58
- "@id": "http://example.org/manifest",
59
- label: "Test Manifest",
58
+ '@id': 'http://example.org/manifest',
59
+ label: 'Test Manifest',
60
60
  };
61
61
  mockFetch.mockResolvedValueOnce({
62
62
  ok: true,
63
63
  json: async () => mockManifest,
64
64
  });
65
- await state.fetchManifest("http://example.org/manifest");
66
- expect(mockFetch).toHaveBeenCalledWith("http://example.org/manifest");
67
- expect(state.manifests["http://example.org/manifest"]).toBeDefined();
68
- expect(state.manifests["http://example.org/manifest"].json).toEqual(mockManifest);
69
- expect(state.manifests["http://example.org/manifest"].isFetching).toBe(false);
65
+ await state.fetchManifest('http://example.org/manifest');
66
+ expect(mockFetch).toHaveBeenCalledWith('http://example.org/manifest');
67
+ expect(state.manifests['http://example.org/manifest']).toBeDefined();
68
+ expect(state.manifests['http://example.org/manifest'].json).toEqual(mockManifest);
69
+ expect(state.manifests['http://example.org/manifest'].isFetching).toBe(false);
70
70
  expect(manifesto.parseManifest).toHaveBeenCalledWith(mockManifest);
71
71
  });
72
- it("should handle fetch errors", async () => {
73
- mockFetch.mockRejectedValueOnce(new Error("Network Error"));
74
- await state.fetchManifest("http://example.org/error");
75
- expect(state.manifests["http://example.org/error"].error).toBe("Network Error");
76
- expect(state.manifests["http://example.org/error"].isFetching).toBe(false);
72
+ it('should handle fetch errors', async () => {
73
+ mockFetch.mockRejectedValueOnce(new Error('Network Error'));
74
+ await state.fetchManifest('http://example.org/error');
75
+ expect(state.manifests['http://example.org/error'].error).toBe('Network Error');
76
+ expect(state.manifests['http://example.org/error'].isFetching).toBe(false);
77
77
  });
78
- it("should not fetch if already fetched", async () => {
78
+ it('should not fetch if already fetched', async () => {
79
79
  // Prime the state
80
- state.manifests["http://example.org/cached"] = {
80
+ state.manifests['http://example.org/cached'] = {
81
81
  isFetching: false,
82
82
  json: {},
83
83
  };
84
- await state.fetchManifest("http://example.org/cached");
84
+ await state.fetchManifest('http://example.org/cached');
85
85
  expect(mockFetch).not.toHaveBeenCalled();
86
86
  });
87
87
  });
88
- describe("getCanvases", () => {
89
- it("should return canvases from parsed manifest", async () => {
88
+ describe('getCanvases', () => {
89
+ it('should return canvases from parsed manifest', async () => {
90
90
  // Mock internal state directly to avoid fetch overhead
91
- state.manifests["http://example.org/manifest"] = {
91
+ state.manifests['http://example.org/manifest'] = {
92
92
  manifesto: {
93
93
  getSequences: () => [
94
94
  {
95
- getCanvases: () => ["mockCanvas1", "mockCanvas2"],
95
+ getCanvases: () => ['mockCanvas1', 'mockCanvas2'],
96
96
  },
97
97
  ],
98
98
  },
99
99
  };
100
- const canvases = state.getCanvases("http://example.org/manifest");
101
- expect(canvases).toEqual(["mockCanvas1", "mockCanvas2"]);
100
+ const canvases = state.getCanvases('http://example.org/manifest');
101
+ expect(canvases).toEqual(['mockCanvas1', 'mockCanvas2']);
102
102
  });
103
- it("should return empty array if manifest not found", () => {
104
- const canvases = state.getCanvases("http://example.org/missing");
103
+ it('should return empty array if manifest not found', () => {
104
+ const canvases = state.getCanvases('http://example.org/missing');
105
105
  expect(canvases).toEqual([]);
106
106
  });
107
107
  });
108
- describe("manualGetAnnotations", () => {
109
- it("should extract annotations and trigger fetch for external lists", async () => {
108
+ describe('manualGetAnnotations', () => {
109
+ it('should extract annotations and trigger fetch for external lists', async () => {
110
110
  // Setup mock state with a manifest that has a canvas
111
- state.manifests["http://example.org/manifest"] = {
111
+ state.manifests['http://example.org/manifest'] = {
112
112
  manifesto: {
113
113
  getSequences: () => [
114
114
  {
115
115
  getCanvasById: () => ({
116
116
  __jsonld: {
117
- otherContent: [{ "@id": "http://example.org/list1" }],
117
+ otherContent: [
118
+ { '@id': 'http://example.org/list1' },
119
+ ],
118
120
  },
119
121
  }),
120
122
  },
@@ -124,24 +126,24 @@ describe("ManifestsState", () => {
124
126
  // Mock the fetch for the annotation list
125
127
  mockFetch.mockResolvedValue({
126
128
  ok: true,
127
- json: async () => ({ resources: [{ "@id": "anno1" }] }),
129
+ json: async () => ({ resources: [{ '@id': 'anno1' }] }),
128
130
  });
129
131
  // First call triggers fetch
130
132
  // manualGetAnnotations calls fetchAnnotationList which is async, but manualGetAnnotations itself is synchronous and returns partial data
131
- state.manualGetAnnotations("http://example.org/manifest", "canvas1");
133
+ state.manualGetAnnotations('http://example.org/manifest', 'canvas1');
132
134
  // We need to wait for the async fetchAnnotationList to complete.
133
135
  // Since it's not returned, we can wait a tick or use `vi.waitFor` if available,
134
136
  // but simpler here is just to await a small delay since we are mocking.
135
137
  await new Promise((resolve) => setTimeout(resolve, 0));
136
- expect(mockFetch).toHaveBeenCalledWith("http://example.org/list1");
138
+ expect(mockFetch).toHaveBeenCalledWith('http://example.org/list1');
137
139
  // Simulate update after fetch (in real app this is reactive, here we manually update state)
138
- state.manifests["http://example.org/list1"] = {
139
- json: { resources: [{ "@id": "anno1" }] },
140
+ state.manifests['http://example.org/list1'] = {
141
+ json: { resources: [{ '@id': 'anno1' }] },
140
142
  };
141
143
  // Second call should return the annotations
142
- const annos = state.manualGetAnnotations("http://example.org/manifest", "canvas1");
144
+ const annos = state.manualGetAnnotations('http://example.org/manifest', 'canvas1');
143
145
  expect(annos).toHaveLength(1);
144
- expect(annos[0]["@id"]).toBe("anno1");
146
+ expect(annos[0]['@id']).toBe('anno1');
145
147
  });
146
148
  });
147
149
  });
@@ -1,3 +1,4 @@
1
+ import { SvelteSet } from 'svelte/reactivity';
1
2
  import type { ViewerConfig } from '../types/config';
2
3
  import type { PluginMenuButton, PluginPanel, PluginDef } from '../types/plugin';
3
4
  /**
@@ -15,6 +16,15 @@ export interface ViewerStateSnapshot {
15
16
  searchQuery: string;
16
17
  isFullScreen: boolean;
17
18
  dockSide: string;
19
+ viewingMode: 'individuals' | 'paged';
20
+ galleryPosition: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ gallerySize: {
25
+ width: number;
26
+ height: number;
27
+ };
18
28
  }
19
29
  export declare class ViewerState {
20
30
  manifestId: string | null;
@@ -27,11 +37,15 @@ export declare class ViewerState {
27
37
  isFullScreen: boolean;
28
38
  showMetadataDialog: boolean;
29
39
  dockSide: string;
30
- visibleAnnotationIds: Set<string>;
40
+ visibleAnnotationIds: SvelteSet<string>;
31
41
  config: ViewerConfig;
32
42
  get showToggle(): boolean;
33
43
  get showCanvasNav(): boolean;
34
44
  get showZoomControls(): boolean;
45
+ get galleryFixedHeight(): number;
46
+ get viewingMode(): "individuals" | "paged";
47
+ set viewingMode(value: 'individuals' | 'paged');
48
+ pagedOffset: number;
35
49
  galleryPosition: {
36
50
  x: number;
37
51
  y: number;
@@ -98,6 +112,8 @@ export declare class ViewerState {
98
112
  setViewerElement(element: HTMLElement): void;
99
113
  toggleFullScreen(): void;
100
114
  toggleMetadataDialog(): void;
115
+ setViewingMode(mode: 'individuals' | 'paged'): void;
116
+ togglePagedOffset(): void;
101
117
  searchQuery: string;
102
118
  pendingSearchQuery: string | null;
103
119
  searchResults: any[];
@@ -105,6 +121,9 @@ export declare class ViewerState {
105
121
  showSearchPanel: boolean;
106
122
  toggleSearchPanel(): void;
107
123
  searchAnnotations: any[];
124
+ /**
125
+ * This function now accounts for two-page mode when returning current canvas search annotations offset accordingly.
126
+ */
108
127
  get currentCanvasSearchAnnotations(): any[];
109
128
  search(query: string): Promise<void>;
110
129
  private _performSearch;
@@ -1,3 +1,4 @@
1
+ import { SvelteSet, SvelteMap } from 'svelte/reactivity';
1
2
  import { manifestsState } from './manifests.svelte.js';
2
3
  export class ViewerState {
3
4
  manifestId = $state(null);
@@ -10,7 +11,7 @@ export class ViewerState {
10
11
  isFullScreen = $state(false);
11
12
  showMetadataDialog = $state(false);
12
13
  dockSide = $state('bottom');
13
- visibleAnnotationIds = $state(new Set());
14
+ visibleAnnotationIds = new SvelteSet();
14
15
  // UI Configuration
15
16
  config = $state({});
16
17
  // Derived configuration specific getters
@@ -23,6 +24,17 @@ export class ViewerState {
23
24
  get showZoomControls() {
24
25
  return this.config.showZoomControls ?? true;
25
26
  }
27
+ get galleryFixedHeight() {
28
+ return this.config.gallery?.fixedHeight ?? 120;
29
+ }
30
+ get viewingMode() {
31
+ return this.config.viewingMode ?? 'individuals';
32
+ }
33
+ set viewingMode(value) {
34
+ this.config.viewingMode = value;
35
+ }
36
+ // Pairing offset for paged mode: 0 = default (pairs start at 1+2), 1 = shifted (page 1 alone, pairs start at 2+3)
37
+ pagedOffset = $state(0);
26
38
  // Gallery State (Lifted for persistence during re-docking)
27
39
  galleryPosition = $state({ x: 20, y: 100 });
28
40
  gallerySize = $state({ width: 300, height: 400 });
@@ -74,6 +86,9 @@ export class ViewerState {
74
86
  searchQuery: this.searchQuery,
75
87
  isFullScreen: this.isFullScreen,
76
88
  dockSide: this.dockSide,
89
+ viewingMode: this.viewingMode,
90
+ galleryPosition: this.galleryPosition,
91
+ gallerySize: this.gallerySize,
77
92
  };
78
93
  }
79
94
  /**
@@ -141,23 +156,61 @@ export class ViewerState {
141
156
  });
142
157
  }
143
158
  get hasNext() {
144
- return this.currentCanvasIndex < this.canvases.length - 1;
159
+ if (this.viewingMode === 'paged') {
160
+ // Account for paged offset: with offset 1, page 1 is single, pairs start at 2+3
161
+ const singlePages = this.pagedOffset;
162
+ if (this.currentCanvasIndex < singlePages) {
163
+ return this.currentCanvasIndex < this.canvases.length - 1;
164
+ }
165
+ return this.currentCanvasIndex < this.canvases.length - 2;
166
+ }
167
+ else {
168
+ return this.currentCanvasIndex < this.canvases.length - 1;
169
+ }
145
170
  }
146
171
  get hasPrevious() {
147
172
  return this.currentCanvasIndex > 0;
148
173
  }
149
174
  nextCanvas() {
150
175
  if (this.hasNext) {
151
- const nextIndex = this.currentCanvasIndex + 1;
152
- const canvas = this.canvases[nextIndex];
153
- this.setCanvas(canvas.id);
176
+ if (this.viewingMode === 'paged') {
177
+ // Single pages at the start: pagedOffset (default 0, shifted = 1)
178
+ const singlePages = this.pagedOffset;
179
+ const nextIndex = this.currentCanvasIndex < singlePages
180
+ ? this.currentCanvasIndex + 1
181
+ : this.currentCanvasIndex + 2;
182
+ const canvas = this.canvases[nextIndex];
183
+ this.setCanvas(canvas.id);
184
+ }
185
+ else {
186
+ const nextIndex = this.currentCanvasIndex + 1;
187
+ const canvas = this.canvases[nextIndex];
188
+ this.setCanvas(canvas.id);
189
+ }
154
190
  }
155
191
  }
156
192
  previousCanvas() {
157
193
  if (this.hasPrevious) {
158
- const prevIndex = this.currentCanvasIndex - 1;
159
- const canvas = this.canvases[prevIndex];
160
- this.setCanvas(canvas.id);
194
+ if (this.viewingMode === 'paged') {
195
+ // Single pages at the start: pagedOffset (default 0, shifted = 1)
196
+ const singlePages = this.pagedOffset;
197
+ let prevIndex;
198
+ if (this.currentCanvasIndex <= singlePages) {
199
+ // Going back within single pages or to a single page
200
+ prevIndex = this.currentCanvasIndex - 1;
201
+ }
202
+ else {
203
+ // Going back in paired pages, but don't go past the last single page
204
+ prevIndex = Math.max(this.currentCanvasIndex - 2, singlePages);
205
+ }
206
+ const canvas = this.canvases[prevIndex];
207
+ this.setCanvas(canvas.id);
208
+ }
209
+ else {
210
+ const prevIndex = this.currentCanvasIndex - 1;
211
+ const canvas = this.canvases[prevIndex];
212
+ this.setCanvas(canvas.id);
213
+ }
161
214
  }
162
215
  }
163
216
  zoomIn() {
@@ -189,6 +242,10 @@ export class ViewerState {
189
242
  if (newConfig.toolbarOpen !== undefined) {
190
243
  this.toolbarOpen = newConfig.toolbarOpen;
191
244
  }
245
+ if (newConfig.viewingMode) {
246
+ // direct assignment works because of the setter
247
+ this.viewingMode = newConfig.viewingMode;
248
+ }
192
249
  if (newConfig.gallery) {
193
250
  if (newConfig.gallery.open !== undefined) {
194
251
  this.showThumbnailGallery = newConfig.gallery.open;
@@ -196,6 +253,18 @@ export class ViewerState {
196
253
  if (newConfig.gallery.dockPosition !== undefined) {
197
254
  this.dockSide = newConfig.gallery.dockPosition;
198
255
  }
256
+ if (newConfig.gallery.width !== undefined) {
257
+ this.gallerySize.width = newConfig.gallery.width;
258
+ }
259
+ if (newConfig.gallery.height !== undefined) {
260
+ this.gallerySize.height = newConfig.gallery.height;
261
+ }
262
+ if (newConfig.gallery.x !== undefined) {
263
+ this.galleryPosition.x = newConfig.gallery.x;
264
+ }
265
+ if (newConfig.gallery.y !== undefined) {
266
+ this.galleryPosition.y = newConfig.gallery.y;
267
+ }
199
268
  }
200
269
  if (newConfig.search) {
201
270
  if (newConfig.search.open !== undefined) {
@@ -262,6 +331,39 @@ export class ViewerState {
262
331
  toggleMetadataDialog() {
263
332
  this.showMetadataDialog = !this.showMetadataDialog;
264
333
  }
334
+ setViewingMode(mode) {
335
+ this.viewingMode = mode;
336
+ if (mode === 'paged') {
337
+ const singlePages = this.pagedOffset;
338
+ // If we're past the single pages, check if we're on a right-hand page
339
+ if (this.currentCanvasIndex >= singlePages) {
340
+ // Calculate position relative to where pairs start
341
+ const pairPosition = (this.currentCanvasIndex - singlePages) % 2;
342
+ if (pairPosition === 1) {
343
+ // We're on a right-hand page, move back one
344
+ const newIndex = this.currentCanvasIndex - 1;
345
+ const canvas = this.canvases[newIndex];
346
+ this.setCanvas(canvas.id);
347
+ }
348
+ }
349
+ }
350
+ this.dispatchStateChange();
351
+ }
352
+ togglePagedOffset() {
353
+ this.pagedOffset = this.pagedOffset === 0 ? 1 : 0;
354
+ // Adjust current canvas position if needed
355
+ const singlePages = this.pagedOffset;
356
+ if (this.currentCanvasIndex >= singlePages) {
357
+ const pairPosition = (this.currentCanvasIndex - singlePages) % 2;
358
+ if (pairPosition === 1) {
359
+ // We're now on a right-hand page after the shift, move back
360
+ const newIndex = this.currentCanvasIndex - 1;
361
+ const canvas = this.canvases[newIndex];
362
+ this.setCanvas(canvas.id);
363
+ }
364
+ }
365
+ this.dispatchStateChange();
366
+ }
265
367
  searchQuery = $state('');
266
368
  pendingSearchQuery = $state(null);
267
369
  searchResults = $state([]);
@@ -276,10 +378,43 @@ export class ViewerState {
276
378
  this.dispatchStateChange();
277
379
  }
278
380
  searchAnnotations = $state([]);
381
+ /**
382
+ * This function now accounts for two-page mode when returning current canvas search annotations offset accordingly.
383
+ */
279
384
  get currentCanvasSearchAnnotations() {
280
385
  if (!this.canvasId)
281
386
  return [];
282
- return this.searchAnnotations.filter((a) => a.canvasId === this.canvasId);
387
+ if (this.viewingMode === 'paged') {
388
+ let annotations = this.searchAnnotations.filter((a) => a.canvasId === this.canvasId);
389
+ const currentIndex = this.currentCanvasIndex;
390
+ const singlePages = this.pagedOffset;
391
+ // Only include next canvas annotations if we're in a two-page spread
392
+ if (currentIndex >= singlePages) {
393
+ const nextIndex = currentIndex + 1;
394
+ if (nextIndex < this.canvases.length) {
395
+ const nextCanvas = this.canvases[nextIndex];
396
+ const nextCanvasId = nextCanvas.id || nextCanvas['@id'];
397
+ const xOffset = 1.025; // account for small gap between pages
398
+ const annoOffset = this.canvases[currentIndex].getWidth() * xOffset;
399
+ const nextAnnotations = this.searchAnnotations.filter((a) => a.canvasId === nextCanvasId);
400
+ // update x coordinates for display on the right side in two-page mode
401
+ const nextAnnotationsUpdated = nextAnnotations.map((a) => {
402
+ const parts = a.on.split('#xywh=');
403
+ const coords = parts[1].split(',').map(Number);
404
+ const shiftedX = coords[0] + annoOffset;
405
+ return {
406
+ ...a,
407
+ on: `${parts[0]}#xywh=${shiftedX},${coords[1]},${coords[2]},${coords[3]}`,
408
+ };
409
+ });
410
+ annotations = annotations.concat(nextAnnotationsUpdated);
411
+ }
412
+ }
413
+ return annotations;
414
+ }
415
+ else {
416
+ return this.searchAnnotations.filter((a) => a.canvasId === this.canvasId);
417
+ }
283
418
  }
284
419
  async search(query) {
285
420
  this.dispatchStateChange();
@@ -328,7 +463,7 @@ export class ViewerState {
328
463
  const data = await response.json();
329
464
  const resources = data.resources || [];
330
465
  // Group results by canvas index
331
- const resultsByCanvas = new Map();
466
+ const resultsByCanvas = new SvelteMap();
332
467
  // Helper to parse xywh
333
468
  const parseSelector = (onVal) => {
334
469
  const val = typeof onVal === 'string'
@@ -362,7 +497,7 @@ export class ViewerState {
362
497
  // We will take the first valid canvas we find for the annotations.
363
498
  let canvasIndex = -1;
364
499
  let bounds = null;
365
- let allBounds = [];
500
+ const allBounds = [];
366
501
  for (const annoId of annotations) {
367
502
  const annotation = resources.find((r) => r['@id'] === annoId || r.id === annoId);
368
503
  if (annotation && annotation.on) {
@@ -406,7 +541,7 @@ export class ViewerState {
406
541
  label = canvas.label[0]?.value;
407
542
  }
408
543
  }
409
- catch (e) {
544
+ catch (_e) {
410
545
  /* ignore */
411
546
  }
412
547
  resultsByCanvas.set(canvasIndex, {
@@ -454,7 +589,7 @@ export class ViewerState {
454
589
  label = canvas.label[0]?.value;
455
590
  }
456
591
  }
457
- catch (e) {
592
+ catch (_e) {
458
593
  /* ignore */
459
594
  }
460
595
  if (!resultsByCanvas.has(canvasIndex)) {
@@ -527,7 +662,7 @@ export class ViewerState {
527
662
  /** OpenSeadragon viewer instance (set by OSDViewer) */
528
663
  osdViewer = $state.raw(null);
529
664
  /** Event handlers for inter-plugin communication */
530
- pluginEventHandlers = new Map();
665
+ pluginEventHandlers = new SvelteMap();
531
666
  // ==================== PLUGIN METHODS ====================
532
667
  /**
533
668
  * Register a plugin with this viewer instance.
@@ -557,8 +692,7 @@ export class ViewerState {
557
692
  isVisible: () => isOpen,
558
693
  props: {
559
694
  ...def.props,
560
- // Pass isOpen state and closer to component
561
- isOpen: isOpen,
695
+ // Pass closer to component
562
696
  close: () => {
563
697
  isOpen = false;
564
698
  },