triiiceratops 0.11.2 → 0.12.1

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 (40) hide show
  1. package/dist/{ArrowCounterClockwise-B_hB6fl1.js → ArrowCounterClockwise-CM9mGGcp.js} +1 -1
  2. package/dist/X-Bn7S7vUL.js +963 -0
  3. package/dist/{annotation_tool_point-rZvdAtGa.js → annotation_tool_point-LoRp_nrI.js} +1 -1
  4. package/dist/components/DemoHeader.svelte +69 -0
  5. package/dist/components/MetadataDialog.svelte +20 -4
  6. package/dist/components/OSDViewer.svelte +31 -1
  7. package/dist/components/ThumbnailGallery.svelte +226 -35
  8. package/dist/components/Toolbar.svelte +102 -3
  9. package/dist/components/TriiiceratopsViewer.svelte +37 -12
  10. package/dist/{image_filters_reset-CAUhlDWt.js → image_filters_reset-CmWuQiOc.js} +1 -1
  11. package/dist/paraglide/messages/_index.d.ts +9 -0
  12. package/dist/paraglide/messages/_index.js +10 -1
  13. package/dist/paraglide/messages/settings_toggle_show_viewing_mode.d.ts +4 -0
  14. package/dist/paraglide/messages/settings_toggle_show_viewing_mode.js +33 -0
  15. package/dist/paraglide/messages/show_mode_toggle.d.ts +4 -0
  16. package/dist/paraglide/messages/show_mode_toggle.js +33 -0
  17. package/dist/paraglide/messages/toggle_single_page_mode.d.ts +4 -0
  18. package/dist/paraglide/messages/toggle_single_page_mode.js +33 -0
  19. package/dist/paraglide/messages/toggle_two_page_mode.d.ts +4 -0
  20. package/dist/paraglide/messages/toggle_two_page_mode.js +33 -0
  21. package/dist/paraglide/messages/two_page_mode.d.ts +4 -0
  22. package/dist/paraglide/messages/two_page_mode.js +33 -0
  23. package/dist/paraglide/messages/viewing_mode_individuals.d.ts +4 -0
  24. package/dist/paraglide/messages/viewing_mode_individuals.js +33 -0
  25. package/dist/paraglide/messages/viewing_mode_label.d.ts +4 -0
  26. package/dist/paraglide/messages/viewing_mode_label.js +33 -0
  27. package/dist/paraglide/messages/viewing_mode_paged.d.ts +4 -0
  28. package/dist/paraglide/messages/viewing_mode_paged.js +33 -0
  29. package/dist/paraglide/messages/viewing_mode_shift_pairing.d.ts +4 -0
  30. package/dist/paraglide/messages/viewing_mode_shift_pairing.js +33 -0
  31. package/dist/plugins/annotation-editor.js +3 -3
  32. package/dist/plugins/image-manipulation.js +3 -3
  33. package/dist/state/viewer.svelte.d.ts +18 -0
  34. package/dist/state/viewer.svelte.js +142 -8
  35. package/dist/triiiceratops-bundle.js +3068 -2514
  36. package/dist/triiiceratops-element.iife.js +25 -25
  37. package/dist/triiiceratops.css +1 -1
  38. package/dist/types/config.d.ts +33 -0
  39. package/package.json +1 -1
  40. package/dist/X-Boj9jj2h.js +0 -890
@@ -1,4 +1,4 @@
1
- import { a as t } from "./X-Boj9jj2h.js";
1
+ import { a as t } from "./X-Bn7S7vUL.js";
2
2
  const a = (
3
3
  /** @type {(inputs: {}) => LocalizedString} */
4
4
  () => (
@@ -434,6 +434,49 @@
434
434
  />
435
435
  </label>
436
436
  </li>
437
+ <li>
438
+ <label class="label cursor-pointer py-1">
439
+ <span class="label-text"
440
+ >{m.settings_toggle_show_viewing_mode()}</span
441
+ >
442
+ <input
443
+ type="checkbox"
444
+ class="checkbox checkbox-xs"
445
+ checked={config.toolbar
446
+ ?.showViewingMode ?? true}
447
+ onchange={(e) => {
448
+ if (!config.toolbar)
449
+ config.toolbar = {};
450
+ config.toolbar.showViewingMode =
451
+ e.currentTarget.checked;
452
+ }}
453
+ />
454
+ </label>
455
+ </li>
456
+ <li>
457
+ <label class="label cursor-pointer py-1">
458
+ <span class="label-text"
459
+ >{m.viewing_mode_label()}</span
460
+ >
461
+ <select
462
+ class="select select-bordered select-xs"
463
+ value={config.viewingMode ??
464
+ 'individuals'}
465
+ onchange={(e) => {
466
+ config.viewingMode = (
467
+ e.currentTarget as HTMLSelectElement
468
+ ).value as 'individuals' | 'paged';
469
+ }}
470
+ >
471
+ <option value="individuals"
472
+ >{m.viewing_mode_individuals()}</option
473
+ >
474
+ <option value="paged"
475
+ >{m.viewing_mode_paged()}</option
476
+ >
477
+ </select>
478
+ </label>
479
+ </li>
437
480
  </ul>
438
481
  </details>
439
482
  </li>
@@ -534,6 +577,32 @@
534
577
  </select>
535
578
  </label>
536
579
  </li>
580
+ <li>
581
+ <label class="label cursor-pointer py-1 gap-2">
582
+ <span class="label-text"
583
+ >Thumbnail Height</span
584
+ >
585
+ <input
586
+ type="range"
587
+ min="50"
588
+ max="300"
589
+ value={config.gallery?.fixedHeight ??
590
+ 120}
591
+ class="range range-xs range-primary w-24"
592
+ oninput={(e) => {
593
+ if (!config.gallery)
594
+ config.gallery = {};
595
+ config.gallery.fixedHeight =
596
+ parseInt(e.currentTarget.value);
597
+ }}
598
+ />
599
+ <span
600
+ class="text-xs opacity-50 w-8 text-right"
601
+ >{config.gallery?.fixedHeight ??
602
+ 120}px</span
603
+ >
604
+ </label>
605
+ </li>
537
606
  </ul>
538
607
  </details>
539
608
  </li>
@@ -103,20 +103,20 @@
103
103
  </div>
104
104
  {/if}
105
105
 
106
- <dl class="grid grid-cols-1 md:grid-cols-[200px_1fr]">
106
+ <dl>
107
107
  {#if attribution}
108
108
  <dt class="font-bold text-lg opacity-70 mt-6">
109
109
  {m.attribution()}
110
110
  </dt>
111
111
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
112
- <dd class="text-sm ps-2">{@html attribution}</dd>
112
+ <dd class="text-sm ps-4">{@html attribution}</dd>
113
113
  {/if}
114
114
 
115
115
  {#if license}
116
116
  <dt class="font-bold text-lg opacity-70 mt-6">
117
117
  {m.license()}
118
118
  </dt>
119
- <dd class="text-sm ps-2">
119
+ <dd class="text-sm ps-4">
120
120
  <a
121
121
  href={license}
122
122
  target="_blank"
@@ -131,8 +131,24 @@
131
131
  {item.label}
132
132
  </dt>
133
133
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
134
- <dd class="text-sm ps-2">{@html item.value}</dd>
134
+ <dd class="text-sm ps-4">{@html item.value}</dd>
135
135
  {/each}
136
+
137
+ {#if viewerState.manifestId}
138
+ <dt class="font-bold text-lg opacity-70 mt-6">
139
+ {m.iiif_manifest_label()}
140
+ </dt>
141
+ <dd class="text-sm ps-4">
142
+ <a
143
+ href={viewerState.manifestId}
144
+ target="_blank"
145
+ rel="noreferrer"
146
+ class="link link-primary break-all"
147
+ >
148
+ {viewerState.manifestId}
149
+ </a>
150
+ </dd>
151
+ {/if}
136
152
  </dl>
137
153
  </div>
138
154
 
@@ -17,6 +17,8 @@
17
17
 
18
18
  // Track OSD state changes for reactivity
19
19
  let osdVersion = $state(0);
20
+ // Track last opened tile source to prevent unnecessary resets
21
+ let lastTileSourceStr = '';
20
22
 
21
23
  // Get all annotations for current canvas (manifest + search)
22
24
  let allAnnotations = $derived.by(() => {
@@ -231,7 +233,35 @@
231
233
  $effect(() => {
232
234
  if (!viewer || !tileSources) return;
233
235
 
234
- viewer.open(tileSources);
236
+ // Check if source actually changed to avoid resetting zoom
237
+ const currentStr = JSON.stringify(tileSources);
238
+ if (currentStr === lastTileSourceStr) return;
239
+ lastTileSourceStr = currentStr;
240
+
241
+ if (
242
+ viewerState.viewingMode === 'paged' &&
243
+ tileSources instanceof Array &&
244
+ tileSources.length === 2
245
+ ) {
246
+ const secondPageLocation = 1.025;
247
+ const twoPageSpread = [
248
+ {
249
+ tileSource: tileSources[0],
250
+ x: 0,
251
+ y: 0,
252
+ width: 1.0,
253
+ },
254
+ {
255
+ tileSource: tileSources[1],
256
+ x: secondPageLocation, // small gap between pages
257
+ y: 0,
258
+ width: 1.0,
259
+ },
260
+ ];
261
+ viewer.open(twoPageSpread);
262
+ } else {
263
+ viewer.open(tileSources);
264
+ }
235
265
  });
236
266
  </script>
237
267
 
@@ -61,6 +61,28 @@
61
61
  };
62
62
  let galleryElement: HTMLElement | null = $state(null);
63
63
 
64
+ // Initialize position and size from config if available (only once on mount)
65
+ $effect(() => {
66
+ if (
67
+ viewerState.config.gallery?.width &&
68
+ viewerState.config.gallery?.height
69
+ ) {
70
+ viewerState.gallerySize = {
71
+ width: viewerState.config.gallery.width,
72
+ height: viewerState.config.gallery.height,
73
+ };
74
+ }
75
+ if (
76
+ viewerState.config.gallery?.x !== undefined &&
77
+ viewerState.config.gallery?.y !== undefined
78
+ ) {
79
+ viewerState.galleryPosition = {
80
+ x: viewerState.config.gallery.x,
81
+ y: viewerState.config.gallery.y,
82
+ };
83
+ }
84
+ });
85
+
64
86
  // Generate thumbnail data
65
87
  let thumbnails = $derived.by(() => {
66
88
  if (!canvases || !Array.isArray(canvases))
@@ -322,7 +344,26 @@
322
344
  }
323
345
 
324
346
  function selectCanvas(canvasId: string) {
325
- viewerState.setCanvas(canvasId);
347
+ if (viewerState.viewingMode === 'paged') {
348
+ const canvasIndex = thumbnails.findIndex((t) => t.id === canvasId);
349
+ const singlePages = viewerState.pagedOffset;
350
+ // If within single pages section, select directly
351
+ if (canvasIndex < singlePages) {
352
+ viewerState.setCanvas(canvasId);
353
+ } else {
354
+ // Check if this is a left-hand page (start of a pair)
355
+ const pairPosition = (canvasIndex - singlePages) % 2;
356
+ if (pairPosition === 0) {
357
+ viewerState.setCanvas(canvasId);
358
+ } else {
359
+ // Right-hand page, select the left page of this pair
360
+ const prevCanvas = thumbnails[canvasIndex - 1];
361
+ viewerState.setCanvas(prevCanvas.id);
362
+ }
363
+ }
364
+ } else {
365
+ viewerState.setCanvas(canvasId);
366
+ }
326
367
  }
327
368
 
328
369
  // State for docking
@@ -384,6 +425,8 @@
384
425
  (dockSide === 'none' && viewerState.gallerySize.height < 320),
385
426
  );
386
427
 
428
+ let fixedHeight = $derived(viewerState.galleryFixedHeight);
429
+
387
430
  function startDrag(e: MouseEvent) {
388
431
  if (!draggable) return; // Dragging disabled in config
389
432
  if ((e.target as HTMLElement).closest('.resize-handle')) return; // Don't drag if resizing
@@ -444,6 +487,53 @@
444
487
  dockSide = 'none';
445
488
  }
446
489
  }
490
+
491
+ // Grouped thumbnail mode (for two-page mode)
492
+ const groupedThumbnailIndices = $derived.by(() => {
493
+ const indices: number[] = [];
494
+ if (viewerState.viewingMode === 'paged' && canvases) {
495
+ // Single pages at the start: pagedOffset (default 0, shifted = 1)
496
+ const singlePages = viewerState.pagedOffset;
497
+ // Add indices for single pages
498
+ for (let i = 0; i < singlePages && i < canvases.length; i++) {
499
+ indices.push(i);
500
+ }
501
+ // Add indices for paired pages (step by 2 starting from singlePages)
502
+ for (let i = singlePages; i < canvases.length; i += 2) {
503
+ indices.push(i);
504
+ }
505
+ }
506
+ return indices;
507
+ });
508
+
509
+ const groupedThumbnails = $derived.by(() => {
510
+ const groups: Array<{
511
+ id: string;
512
+ label: string;
513
+ srcs: string[];
514
+ index: number;
515
+ }> = [];
516
+ const thumbs = thumbnails;
517
+ const singlePages = viewerState.pagedOffset;
518
+ for (const i of groupedThumbnailIndices) {
519
+ const first = thumbs[i];
520
+ // Only pair if we're past the single pages section
521
+ const second = i < singlePages ? null : thumbs[i + 1];
522
+ const groupId = first.id;
523
+ const groupLabel = first.label;
524
+ const groupSrcs = [first.src];
525
+ if (second) {
526
+ groupSrcs.push(second.src);
527
+ }
528
+ groups.push({
529
+ id: groupId,
530
+ label: groupLabel,
531
+ srcs: groupSrcs,
532
+ index: i,
533
+ });
534
+ }
535
+ return groups;
536
+ });
447
537
  </script>
448
538
 
449
539
  {#if viewerState.showThumbnailGallery}
@@ -514,44 +604,145 @@
514
604
  : 'grid gap-2'}
515
605
  style={isHorizontal
516
606
  ? ''
517
- : 'grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));'}
607
+ : `grid-template-columns: repeat(auto-fill, minmax(${fixedHeight}px, 1fr));`}
518
608
  >
519
- {#each thumbnails as thumb (thumb.id)}
520
- <button
521
- class="group flex flex-col gap-1 p-1 rounded hover:bg-base-200 transition-colors text-left relative shrink-0 {isHorizontal
522
- ? 'w-[90px]'
523
- : ''} {viewerState.canvasId === thumb.id
524
- ? 'ring-2 ring-primary bg-primary/5'
525
- : ''}"
526
- onclick={() => selectCanvas(thumb.id)}
527
- data-id={thumb.id}
528
- aria-label="Select canvas {thumb.label}"
529
- >
530
- <div
531
- class="aspect-3/4 bg-base-300 rounded overflow-hidden relative w-full flex items-center justify-center"
609
+ {#if viewerState.viewingMode === 'paged'}
610
+ <!-- grouped thumbnail display -->
611
+ {#each groupedThumbnails as thumbGroup}
612
+ <button
613
+ class="group flex flex-col gap-1 p-1 rounded hover:bg-base-200 transition-colors text-left relative shrink-0 {isHorizontal
614
+ ? 'w-auto'
615
+ : thumbGroup.srcs.length > 1
616
+ ? 'col-span-2'
617
+ : ''} {viewerState.canvasId === thumbGroup.id
618
+ ? 'ring-2 ring-primary bg-primary/5'
619
+ : ''}"
620
+ style={isHorizontal
621
+ ? `height: ${fixedHeight + 24}px`
622
+ : ''}
623
+ onclick={() => selectCanvas(thumbGroup.id)}
624
+ data-id={thumbGroup.id}
625
+ aria-label="Select canvas {thumbGroup.label}"
532
626
  >
533
- {#if thumb.src}
534
- <img
535
- src={thumb.src}
536
- alt={thumb.label}
537
- class="object-contain w-full h-full"
538
- loading="lazy"
539
- draggable="false"
540
- />
541
- {:else}
542
- <span class="opacity-20 text-4xl">?</span>
543
- {/if}
544
- </div>
545
- <div
546
- class="text-xs font-medium truncate w-full opacity-70 group-hover:opacity-100"
627
+ <div
628
+ class="{isHorizontal
629
+ ? 'h-full w-auto flex-row'
630
+ : thumbGroup.srcs.length > 1
631
+ ? 'aspect-3/2 w-full'
632
+ : 'aspect-3/4 w-full'} bg-base-300 rounded overflow-hidden relative flex items-center justify-center gap-px"
633
+ style={isHorizontal
634
+ ? `height: ${fixedHeight}px`
635
+ : ''}
636
+ >
637
+ <div
638
+ class="flex items-center justify-center overflow-hidden {isHorizontal
639
+ ? 'h-full w-auto'
640
+ : 'h-full ' +
641
+ (thumbGroup.srcs.length > 1
642
+ ? 'w-1/2'
643
+ : 'w-full')}"
644
+ >
645
+ {#if thumbGroup.srcs[0]}
646
+ <img
647
+ src={thumbGroup.srcs[0]}
648
+ alt={thumbGroup.label}
649
+ class="object-contain {isHorizontal
650
+ ? 'h-full w-auto'
651
+ : 'w-full h-full'} {thumbGroup
652
+ .srcs.length > 1
653
+ ? 'object-right'
654
+ : 'object-center'}"
655
+ loading="lazy"
656
+ draggable="false"
657
+ />
658
+ {:else}
659
+ <span class="opacity-20 text-4xl"
660
+ >?</span
661
+ >
662
+ {/if}
663
+ </div>
664
+ {#if thumbGroup.srcs.length > 1}
665
+ <div
666
+ class="flex items-center justify-center overflow-hidden {isHorizontal
667
+ ? 'h-full w-auto'
668
+ : 'h-full w-1/2'}"
669
+ >
670
+ {#if thumbGroup.srcs[1]}
671
+ <img
672
+ src={thumbGroup.srcs[1]}
673
+ alt={thumbGroup.label}
674
+ class="object-contain {isHorizontal
675
+ ? 'h-full w-auto'
676
+ : 'w-full h-full'} object-left"
677
+ loading="lazy"
678
+ draggable="false"
679
+ />
680
+ {:else}
681
+ <span class="opacity-20 text-4xl"
682
+ >?</span
683
+ >
684
+ {/if}
685
+ </div>
686
+ {/if}
687
+ </div>
688
+ <div
689
+ class="text-xs font-medium truncate w-full opacity-70 group-hover:opacity-100"
690
+ >
691
+ <span class="font-bold mr-1"
692
+ >{thumbGroup.index + 1}.</span
693
+ >
694
+ {thumbGroup.label}
695
+ </div>
696
+ </button>
697
+ {/each}
698
+ {:else}
699
+ {#each thumbnails as thumb}
700
+ <button
701
+ class="group flex flex-col gap-1 p-1 rounded hover:bg-base-200 transition-colors text-left relative shrink-0 {isHorizontal
702
+ ? 'w-auto'
703
+ : ''} {viewerState.canvasId === thumb.id
704
+ ? 'ring-2 ring-primary bg-primary/5'
705
+ : ''}"
706
+ style={isHorizontal
707
+ ? `height: ${fixedHeight + 24}px`
708
+ : ''}
709
+ onclick={() => selectCanvas(thumb.id)}
710
+ data-id={thumb.id}
711
+ aria-label="Select canvas {thumb.label}"
547
712
  >
548
- <span class="font-bold mr-1"
549
- >{thumb.index + 1}.</span
713
+ <div
714
+ class="{isHorizontal
715
+ ? 'h-full w-auto'
716
+ : 'aspect-3/4 w-full'} bg-base-300 rounded overflow-hidden relative flex items-center justify-center"
717
+ style={isHorizontal
718
+ ? `height: ${fixedHeight}px`
719
+ : ''}
720
+ >
721
+ {#if thumb.src}
722
+ <img
723
+ src={thumb.src}
724
+ alt={thumb.label}
725
+ class="object-contain {isHorizontal
726
+ ? 'h-full w-auto'
727
+ : 'w-full h-full'}"
728
+ loading="lazy"
729
+ draggable="false"
730
+ />
731
+ {:else}
732
+ <span class="opacity-20 text-4xl">?</span>
733
+ {/if}
734
+ </div>
735
+ <div
736
+ class="text-xs font-medium truncate w-full opacity-70 group-hover:opacity-100"
550
737
  >
551
- {thumb.label}
552
- </div>
553
- </button>
554
- {/each}
738
+ <span class="font-bold mr-1"
739
+ >{thumb.index + 1}.</span
740
+ >
741
+ {thumb.label}
742
+ </div>
743
+ </button>
744
+ {/each}
745
+ {/if}
555
746
  </div>
556
747
  </div>
557
748
 
@@ -7,6 +7,8 @@
7
7
  import ChatCenteredText from 'phosphor-svelte/lib/ChatCenteredText';
8
8
  import Info from 'phosphor-svelte/lib/Info';
9
9
  import List from 'phosphor-svelte/lib/List';
10
+ import BookOpen from 'phosphor-svelte/lib/BookOpen';
11
+ import Scroll from 'phosphor-svelte/lib/Scroll';
10
12
  import X from 'phosphor-svelte/lib/X';
11
13
  import { VIEWER_STATE_KEY, type ViewerState } from '../state/viewer.svelte';
12
14
  import { m, language } from '../state/i18n.svelte';
@@ -34,6 +36,7 @@
34
36
  const showFullscreen = $derived(toolbarConfig.showFullscreen !== false);
35
37
  const showAnnotations = $derived(toolbarConfig.showAnnotations !== false);
36
38
  const showInfo = $derived(toolbarConfig.showInfo !== false);
39
+ const showViewingMode = $derived(toolbarConfig.showViewingMode !== false);
37
40
 
38
41
  // Derived list of sorted plugin buttons
39
42
  let sortedPluginButtons = $derived.by(() => {
@@ -46,6 +49,18 @@
46
49
  function toggleOpen() {
47
50
  viewerState.toggleToolbar();
48
51
  }
52
+
53
+ let isOverflowVisible = $state(false);
54
+ $effect(() => {
55
+ if (isOpen) {
56
+ const timer = setTimeout(() => {
57
+ isOverflowVisible = true;
58
+ }, 320); // Slightly longer than 300ms to ensure transition is done
59
+ return () => clearTimeout(timer);
60
+ } else {
61
+ isOverflowVisible = false;
62
+ }
63
+ });
49
64
  </script>
50
65
 
51
66
  <div
@@ -60,7 +75,7 @@
60
75
  <!-- Collapsible Toolbar -->
61
76
  <div
62
77
  class={[
63
- 'pointer-events-auto bg-base-100/95 backdrop-blur shadow-xl transition-all duration-300 ease-in-out flex overflow-hidden',
78
+ 'pointer-events-auto bg-base-100/95 backdrop-blur shadow-xl transition-all duration-300 ease-in-out flex',
64
79
  // Layout based on position
65
80
  isTop &&
66
81
  'flex-row-reverse h-12 w-auto max-w-full rounded-b-xl border-x border-b border-base-200 origin-top',
@@ -76,6 +91,9 @@
76
91
  !isOpen && !isTop && 'w-0 opacity-0',
77
92
  !isOpen && !isTop && isLeft && '-translate-x-full',
78
93
  !isOpen && !isTop && !isLeft && 'translate-x-full',
94
+
95
+ // Overflow handling
96
+ isOverflowVisible ? 'overflow-visible' : 'overflow-hidden',
79
97
  ]}
80
98
  >
81
99
  <!-- Close Button (Inside Menu) -->
@@ -104,13 +122,19 @@
104
122
  <div class={isTop ? 'w-2' : 'h-2'}></div>
105
123
  {/if}
106
124
 
125
+ <!-- Scrollable Actions -->
107
126
  <ul
108
127
  class={[
109
128
  'menu menu-md gap-2 flex-nowrap items-center min-h-0',
129
+ isTop && 'px-2 py-1 menu-horizontal w-auto flex-row-reverse',
110
130
  isTop &&
111
- 'px-2 py-1 menu-horizontal w-auto overflow-x-auto overflow-y-hidden flex-row-reverse',
131
+ !isOverflowVisible &&
132
+ 'overflow-x-auto overflow-y-hidden',
133
+ isTop && isOverflowVisible && 'overflow-visible',
112
134
  !isTop &&
135
+ !isOverflowVisible &&
113
136
  'py-2 px-1 flex-1 overflow-y-auto overflow-x-hidden w-12',
137
+ !isTop && isOverflowVisible && 'py-2 px-1 flex-1 w-12',
114
138
  ]}
115
139
  >
116
140
  <!-- --- Standard Actions --- -->
@@ -157,6 +181,81 @@
157
181
  </li>
158
182
  {/if}
159
183
 
184
+ {#if showViewingMode}
185
+ <li
186
+ class="dropdown {isTop
187
+ ? 'dropdown-bottom'
188
+ : isLeft
189
+ ? 'dropdown-right'
190
+ : 'dropdown-left'}"
191
+ >
192
+ <div
193
+ tabindex="0"
194
+ role="button"
195
+ class="flex items-center justify-center"
196
+ use:tooltip={{
197
+ content: m.viewing_mode_label(),
198
+ position: tooltipPos,
199
+ }}
200
+ aria-label={m.viewing_mode_label()}
201
+ >
202
+ {#if viewerState.viewingMode === 'paged'}
203
+ <BookOpen size={24} weight="bold" />
204
+ {:else}
205
+ <Scroll size={24} weight="bold" />
206
+ {/if}
207
+ </div>
208
+ <ul
209
+ tabindex="-1"
210
+ class="dropdown-content z-50 menu p-2 shadow bg-base-100 rounded-box w-48 border border-base-200 font-normal {isTop
211
+ ? 'left-1/2 -translate-x-1/2'
212
+ : ''}"
213
+ >
214
+ <li>
215
+ <button
216
+ class={viewerState.viewingMode === 'individuals'
217
+ ? 'active'
218
+ : ''}
219
+ onclick={() =>
220
+ viewerState.setViewingMode('individuals')}
221
+ >
222
+ <Scroll size={16} />
223
+ {m.viewing_mode_individuals()}
224
+ </button>
225
+ </li>
226
+ <li>
227
+ <button
228
+ class={viewerState.viewingMode === 'paged'
229
+ ? 'active'
230
+ : ''}
231
+ onclick={() =>
232
+ viewerState.setViewingMode('paged')}
233
+ >
234
+ <BookOpen size={16} />
235
+ {m.viewing_mode_paged()}
236
+ </button>
237
+ </li>
238
+ {#if viewerState.viewingMode === 'paged'}
239
+ <div class="divider my-1"></div>
240
+ <li>
241
+ <label class="label cursor-pointer py-1 gap-2">
242
+ <span class="label-text text-sm"
243
+ >{m.viewing_mode_shift_pairing()}</span
244
+ >
245
+ <input
246
+ type="checkbox"
247
+ class="checkbox checkbox-sm"
248
+ checked={viewerState.pagedOffset === 1}
249
+ onchange={() =>
250
+ viewerState.togglePagedOffset()}
251
+ />
252
+ </label>
253
+ </li>
254
+ {/if}
255
+ </ul>
256
+ </li>
257
+ {/if}
258
+
160
259
  {#if showFullscreen}
161
260
  <li>
162
261
  <button
@@ -227,7 +326,7 @@
227
326
  {/if}
228
327
 
229
328
  <!-- Separator if both groups exist -->
230
- {#if (showSearch || showGallery || showFullscreen || showAnnotations || showInfo) && sortedPluginButtons.length > 0}
329
+ {#if (showSearch || showGallery || showFullscreen || showAnnotations || showInfo || showViewingMode) && sortedPluginButtons.length > 0}
231
330
  <div
232
331
  class={[
233
332
  'divider',