@pecb-ui/components 1.1.15 → 1.1.16

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, input, output, HostBinding, Component, ChangeDetectionStrategy, model, computed, signal, HostListener, ContentChild, effect, forwardRef, ViewChild, ViewContainerRef, viewChild, ViewEncapsulation, Directive, PLATFORM_ID, DestroyRef, ElementRef, booleanAttribute, numberAttribute, linkedSignal, untracked, ApplicationRef, EnvironmentInjector, createComponent, contentChildren, NgZone, afterNextRender, Injector, Inject, ViewChildren, Renderer2 } from '@angular/core';
2
+ import { Injectable, inject, input, output, HostBinding, Component, ChangeDetectionStrategy, model, computed, signal, HostListener, ContentChild, effect, forwardRef, ViewChild, ViewContainerRef, viewChild, ViewEncapsulation, Directive, PLATFORM_ID, DestroyRef, ElementRef, booleanAttribute, numberAttribute, linkedSignal, untracked, ApplicationRef, EnvironmentInjector, createComponent, contentChildren, NgZone, afterNextRender, Injector, TemplateRef, contentChild, Inject, ViewChildren, Renderer2 } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule, isPlatformBrowser, NgTemplateOutlet, NgOptimizedImage } from '@angular/common';
5
5
  import * as i1$2 from '@angular/platform-browser';
@@ -15006,6 +15006,1146 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
15006
15006
  args: [{ selector: 'pecb-course-player', standalone: true, imports: [VideoPlayerComponent, CourseContentPanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'pecb-course-player-host' }, template: "<div [class]=\"hostClasses()\">\n <!-- \u2500\u2500 Video stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-course-player__stage\">\n <pecb-video-player\n class=\"pecb-course-player__video\"\n [src]=\"videoSrc()\"\n [poster]=\"videoPoster()\"\n [tracks]=\"videoTracks()\"\n [title]=\"videoTitle()\"\n [label]=\"videoStageLabel()\"\n [watermark]=\"watermark()\"\n [aspectRatio]=\"aspectRatio()\"\n [preload]=\"preload()\"\n [crossOrigin]=\"crossOrigin()\"\n [seekStep]=\"seekStep()\"\n [startTime]=\"startTime()\"\n [labels]=\"videoLabels()\"\n [(playing)]=\"playing\"\n [(currentTime)]=\"currentTime\"\n (timeUpdate)=\"timeUpdate.emit($event)\"\n (playbackEnded)=\"onPlaybackEnded()\"\n (playerError)=\"playerError.emit($event)\"\n ></pecb-video-player>\n\n @if (showLessonTitle() && lessonTitle()) {\n <h2 class=\"pecb-course-player__title\">{{ lessonTitle() }}</h2>\n }\n </div>\n\n <!-- \u2500\u2500 Content sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-course-player__panel\">\n <div class=\"pecb-course-player__panel-inner\">\n <pecb-course-content-panel\n [modules]=\"modules()\"\n [transcript]=\"transcript()\"\n [(activeLessonId)]=\"activeLessonId\"\n [(tab)]=\"panelTab\"\n [playing]=\"playing()\"\n [currentTime]=\"currentTime()\"\n [layout]=\"layout()\"\n [density]=\"density()\"\n [accent]=\"accent()\"\n [showTabs]=\"showTabs()\"\n [showSearch]=\"showSearch()\"\n [showProgress]=\"showProgress()\"\n [soloSingleLessonModules]=\"soloSingleLessonModules()\"\n [labels]=\"panelLabels()\"\n (lessonSelect)=\"onLessonSelect($event)\"\n (transcriptSeek)=\"onTranscriptSeek($event)\"\n (certificateClick)=\"certificateClick.emit()\"\n ></pecb-course-content-panel>\n </div>\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0;container-type:inline-size;container-name:pecb-course-player}.pecb-course-player{display:grid;grid-template-columns:minmax(0,1fr) var(--pecb-course-player-panel-width, 408px);gap:var(--pecb-course-player-gap, 20px);align-items:stretch;height:100%;min-height:0}.pecb-course-player--panel-start{grid-template-columns:var(--pecb-course-player-panel-width, 408px) minmax(0,1fr)}.pecb-course-player--panel-start .pecb-course-player__stage{order:2}.pecb-course-player--panel-start .pecb-course-player__panel{order:1}.pecb-course-player__stage{display:flex;flex-direction:column;gap:14px;min-width:0;min-height:0}.pecb-course-player__video{flex:0 0 auto}.pecb-course-player__title{flex:0 0 auto;margin:0;padding:0 2px;font-family:Plus Jakarta Sans,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:16px;font-weight:700;line-height:1.35;color:#212427;text-wrap:pretty}.pecb-course-player__panel{position:relative;min-width:0;min-height:240px}.pecb-course-player__panel-inner{position:absolute;inset:0;display:flex;overflow:hidden;border:1px solid #ececec;border-radius:var(--pecb-course-player-radius, 12px);background:#fff}.pecb-course-player pecb-course-content-panel{flex:1 1 auto;min-width:0}@container pecb-course-player (max-width: 900px){.pecb-course-player,.pecb-course-player--panel-start{grid-template-columns:minmax(0,1fr);height:auto}.pecb-course-player__stage,.pecb-course-player--panel-start .pecb-course-player__stage{order:1}.pecb-course-player__panel,.pecb-course-player--panel-start .pecb-course-player__panel{order:2;position:static;min-height:0;height:var(--pecb-course-player-panel-stacked-height, 460px)}.pecb-course-player__panel-inner{position:static;height:100%}}@container pecb-course-player (max-width: 560px){.pecb-course-player{gap:16px}.pecb-course-player__panel{height:var(--pecb-course-player-panel-stacked-height, 400px)}.pecb-course-player__title{font-size:14px}}\n"] }]
15007
15007
  }], ctorParameters: () => [], propDecorators: { modules: [{ type: i0.Input, args: [{ isSignal: true, alias: "modules", required: false }] }], transcript: [{ type: i0.Input, args: [{ isSignal: true, alias: "transcript", required: false }] }], activeLessonId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeLessonId", required: false }] }, { type: i0.Output, args: ["activeLessonIdChange"] }], playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }, { type: i0.Output, args: ["playingChange"] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }, { type: i0.Output, args: ["currentTimeChange"] }], panelTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelTab", required: false }] }, { type: i0.Output, args: ["panelTabChange"] }], poster: [{ type: i0.Input, args: [{ isSignal: true, alias: "poster", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], tracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "tracks", required: false }] }], stageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "stageLabel", required: false }] }], watermark: [{ type: i0.Input, args: [{ isSignal: true, alias: "watermark", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], preload: [{ type: i0.Input, args: [{ isSignal: true, alias: "preload", required: false }] }], crossOrigin: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossOrigin", required: false }] }], seekStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "seekStep", required: false }] }], videoLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoLabels", required: false }] }], autoPlayOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoPlayOnSelect", required: false }] }], showLessonTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLessonTitle", required: false }] }], panelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelPosition", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], accent: [{ type: i0.Input, args: [{ isSignal: true, alias: "accent", required: false }] }], showTabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTabs", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], showProgress: [{ type: i0.Input, args: [{ isSignal: true, alias: "showProgress", required: false }] }], soloSingleLessonModules: [{ type: i0.Input, args: [{ isSignal: true, alias: "soloSingleLessonModules", required: false }] }], panelLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelLabels", required: false }] }], lessonSelect: [{ type: i0.Output, args: ["lessonSelect"] }], lessonEnded: [{ type: i0.Output, args: ["lessonEnded"] }], timeUpdate: [{ type: i0.Output, args: ["timeUpdate"] }], certificateClick: [{ type: i0.Output, args: ["certificateClick"] }], playerError: [{ type: i0.Output, args: ["playerError"] }] } });
15008
15008
 
15009
+ const DEFAULT_SLIDE_VIEWER_KEY_MAP = {
15010
+ next: ['ArrowRight', 'PageDown'],
15011
+ previous: ['ArrowLeft', 'PageUp'],
15012
+ first: ['Home'],
15013
+ last: ['End'],
15014
+ fullscreen: ['f', 'F'],
15015
+ search: ['/'],
15016
+ zoomIn: ['+', '='],
15017
+ zoomOut: ['-', '_'],
15018
+ zoomReset: ['0'],
15019
+ };
15020
+ const DEFAULT_SLIDE_VIEWER_LABELS = {
15021
+ viewer: 'Slide viewer',
15022
+ slide: 'Slide {n}',
15023
+ of: '/',
15024
+ slideNumber: 'Slide number',
15025
+ first: 'First slide',
15026
+ previous: 'Previous slide',
15027
+ next: 'Next slide',
15028
+ last: 'Last slide',
15029
+ enterFullscreen: 'Full screen',
15030
+ exitFullscreen: 'Exit full screen',
15031
+ zoomIn: 'Zoom in',
15032
+ zoomOut: 'Zoom out',
15033
+ zoomReset: 'Reset zoom',
15034
+ note: 'Note',
15035
+ days: 'Days',
15036
+ thumbnails: 'Slides',
15037
+ searchPlaceholder: 'Search slides using any keyword',
15038
+ searchResults: '{n} of {total} slides',
15039
+ noResults: 'No slides match your search.',
15040
+ loading: 'Loading slides…',
15041
+ selectSlide: 'Select slide {n}',
15042
+ dragHint: 'Drag to reorder',
15043
+ hidden: 'Hidden',
15044
+ emptyTitle: 'No slides yet',
15045
+ emptyHint: 'There is nothing to show for this day.',
15046
+ errorTitle: 'The slides could not be loaded',
15047
+ errorHint: 'Check the file and try again.',
15048
+ retry: 'Try again',
15049
+ slideAnnouncement: 'Slide {n} of {total}',
15050
+ loadingProgress: 'Downloading slides… {percent}%',
15051
+ notes: 'My Notes',
15052
+ feedback: 'Feedback',
15053
+ courseFiles: 'Download Course Files',
15054
+ downloadSlides: 'Download Slides',
15055
+ downloadUnavailable: 'Slides are not available for download yet.',
15056
+ };
15057
+ /** Default set and order of the top-bar tools. */
15058
+ const DEFAULT_SLIDE_VIEWER_TOOLS = [
15059
+ 'notes',
15060
+ 'feedback',
15061
+ 'courseFiles',
15062
+ 'downloadSlides',
15063
+ ];
15064
+ /** Zoom steps offered by the stage (1 = fit). */
15065
+ const DEFAULT_SLIDE_VIEWER_ZOOM_STEPS = [1, 1.5, 2, 3];
15066
+ /** Clamp a 1-based page into `[1, total]` (`1` when there are no slides). */
15067
+ function clampSlidePage(page, total) {
15068
+ if (!Number.isFinite(page) || total <= 0)
15069
+ return 1;
15070
+ return Math.min(total, Math.max(1, Math.round(page)));
15071
+ }
15072
+ // ─── Template directives ──────────────────────────────────────────────────────
15073
+ /**
15074
+ * Custom content rendered over each sidebar tile (badges, checkmarks, a drag handle…).
15075
+ * ```html
15076
+ * <ng-template pecbSlideViewerThumbnail let-slide let-n="index">
15077
+ * @if (slide.meta?.hasNotes) { <span class="badge">Notes</span> }
15078
+ * </ng-template>
15079
+ * ```
15080
+ */
15081
+ class SlideViewerThumbnailDirective {
15082
+ constructor() {
15083
+ this.templateRef = inject((TemplateRef));
15084
+ }
15085
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: SlideViewerThumbnailDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
15086
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: SlideViewerThumbnailDirective, isStandalone: true, selector: "ng-template[pecbSlideViewerThumbnail]", ngImport: i0 }); }
15087
+ }
15088
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: SlideViewerThumbnailDirective, decorators: [{
15089
+ type: Directive,
15090
+ args: [{ selector: 'ng-template[pecbSlideViewerThumbnail]', standalone: true }]
15091
+ }] });
15092
+ /**
15093
+ * Custom content of each day pill (status dot, counter…). The pill itself stays.
15094
+ * ```html
15095
+ * <ng-template pecbSlideViewerDay let-day let-active="active">{{ day.label }} · {{ day.meta?.count }}</ng-template>
15096
+ * ```
15097
+ */
15098
+ class SlideViewerDayDirective {
15099
+ constructor() {
15100
+ this.templateRef = inject((TemplateRef));
15101
+ }
15102
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: SlideViewerDayDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
15103
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: SlideViewerDayDirective, isStandalone: true, selector: "ng-template[pecbSlideViewerDay]", ngImport: i0 }); }
15104
+ }
15105
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: SlideViewerDayDirective, decorators: [{
15106
+ type: Directive,
15107
+ args: [{ selector: 'ng-template[pecbSlideViewerDay]', standalone: true }]
15108
+ }] });
15109
+ // ─── Component ────────────────────────────────────────────────────────────────
15110
+ /**
15111
+ * PECB course slide viewer.
15112
+ *
15113
+ * Reproduces the "Open course" slide view of the PECB Profile redesign: a top
15114
+ * bar with day pills and tool buttons, a 16:10 stage, a navigation bar
15115
+ * (first / previous / next / last, slide-number input, projected version
15116
+ * select, zoom, fullscreen), a collapsible per-slide note and a sticky sidebar
15117
+ * with keyword search and thumbnails.
15118
+ *
15119
+ * The element is purely presentational: it receives days, slide images and a
15120
+ * note, keeps the current page / search / zoom / fullscreen / selection state
15121
+ * and emits every intent (`dayChange`, `slideChange`, `tool`, `slideMove`, …)
15122
+ * so the host app owns data, dialogs and persistence. No PDF library is
15123
+ * involved — feed it images; leave `image` out and answer `fullImageRequest`
15124
+ * to render large images lazily.
15125
+ *
15126
+ * Empty / broken decks: with no slides and nothing loading the stage shows `emptyTitle`
15127
+ * / `emptyHint`; with `error` set it shows `errorTitle` / `errorHint` and a retry button
15128
+ * (`retry` output). Both messages can be replaced through the `pecbSlideViewerEmpty` slot.
15129
+ *
15130
+ * Slots: `pecbSlideViewerToolsStart` / `pecbSlideViewerTools` (tool bar),
15131
+ * `pecbSlideViewerVersion` (navigation bar, right), `pecbSlideViewerStage`
15132
+ * (overlay on the stage), `pecbSlideViewerNote` (inside the note panel),
15133
+ * `pecbSlideViewerBelow` (under the stage column), `pecbSlideViewerSidebarHeader`
15134
+ * / `pecbSlideViewerSidebarFooter` (sidebar). Templates: `pecbSlideViewerThumbnail`,
15135
+ * `pecbSlideViewerDay`.
15136
+ *
15137
+ * @example
15138
+ * ```html
15139
+ * <pecb-slide-viewer
15140
+ * [days]="days" [(activeDayId)]="dayId" (dayChange)="loadDay($event)"
15141
+ * [slides]="slides" [(page)]="page" [note]="noteHtml"
15142
+ * (fullImageRequest)="renderFull($event)"
15143
+ * (tool)="onTool($event)" (slideChange)="track($event)">
15144
+ * <pecb-dropdown pecbSlideViewerVersion [options]="versions" [(ngModel)]="version" />
15145
+ * </pecb-slide-viewer>
15146
+ * ```
15147
+ *
15148
+ * Theming — override the CSS custom properties on the host:
15149
+ * ```css
15150
+ * pecb-slide-viewer { --pecb-slide-sidebar-width: 300px; --pecb-slide-active: #a11e29; }
15151
+ * ```
15152
+ */
15153
+ class SlideViewerComponent {
15154
+ constructor() {
15155
+ this.platformId = inject(PLATFORM_ID);
15156
+ this.zone = inject(NgZone);
15157
+ this.destroyRef = inject(DestroyRef);
15158
+ this.isBrowser = isPlatformBrowser(this.platformId);
15159
+ /** Unique id used to wire ARIA relationships inside the template. */
15160
+ this.uid = generateUniqueId('pecb-slides');
15161
+ // ── Element refs / templates ────────────────────────────────────────────────
15162
+ this.rootRef = viewChild('root', ...(ngDevMode ? [{ debugName: "rootRef" }] : []));
15163
+ this.stageRef = viewChild('stage', ...(ngDevMode ? [{ debugName: "stageRef" }] : []));
15164
+ this.thumbsRef = viewChild('thumbs', ...(ngDevMode ? [{ debugName: "thumbsRef" }] : []));
15165
+ this.searchRef = viewChild('search', ...(ngDevMode ? [{ debugName: "searchRef" }] : []));
15166
+ this.thumbnailTemplate = contentChild(SlideViewerThumbnailDirective, ...(ngDevMode ? [{ debugName: "thumbnailTemplate" }] : []));
15167
+ this.dayTemplate = contentChild(SlideViewerDayDirective, ...(ngDevMode ? [{ debugName: "dayTemplate" }] : []));
15168
+ // ── Data inputs ─────────────────────────────────────────────────────────────
15169
+ /** Day pills. The switcher is hidden when empty. */
15170
+ this.days = input([], ...(ngDevMode ? [{ debugName: "days" }] : []));
15171
+ /** Slides of the active day, in order. */
15172
+ this.slides = input([], ...(ngDevMode ? [{ debugName: "slides" }] : []));
15173
+ /**
15174
+ * Note for the current slide — an HTML string (sanitized by Angular) or a `SafeHtml`
15175
+ * the host already trusts. The panel is hidden when empty unless `showNote` is set
15176
+ * (e.g. when the note body is projected through `pecbSlideViewerNote`).
15177
+ */
15178
+ this.note = input(null, ...(ngDevMode ? [{ debugName: "note" }] : []));
15179
+ /** Force the note panel on/off; `undefined` shows it whenever `note` has content. */
15180
+ this.showNote = input(undefined, ...(ngDevMode ? [{ debugName: "showNote" }] : []));
15181
+ /** Slides are being prepared: the stage shows a veil and the list its skeletons. */
15182
+ this.loading = input(false, ...(ngDevMode ? [{ debugName: "loading", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15183
+ /** Download / preparation progress, 0–100, shown on the stage while loading. `null` hides it. */
15184
+ this.loadingProgress = input(null, ...(ngDevMode ? [{ debugName: "loadingProgress" }] : []));
15185
+ /**
15186
+ * The slides could not be produced (missing / broken file): the stage shows the error
15187
+ * message and a retry button (`retry` output) instead of the slides.
15188
+ */
15189
+ this.error = input(false, ...(ngDevMode ? [{ debugName: "error", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15190
+ // ── Appearance / behaviour inputs ───────────────────────────────────────────
15191
+ /** Which built-in tool buttons render, in this order. */
15192
+ this.tools = input(DEFAULT_SLIDE_VIEWER_TOOLS, ...(ngDevMode ? [{ debugName: "tools" }] : []));
15193
+ /** Enables the "Download Slides" tool. When false it renders disabled with a tooltip. @default true */
15194
+ this.canDownloadSlides = input(true, ...(ngDevMode ? [{ debugName: "canDownloadSlides", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15195
+ /** CSS `aspect-ratio` of the stage. @default '16 / 10' */
15196
+ this.stageAspectRatio = input('16 / 10', ...(ngDevMode ? [{ debugName: "stageAspectRatio" }] : []));
15197
+ /** Where the thumbnail sidebar sits. @default 'end' */
15198
+ this.sidebarPosition = input('end', ...(ngDevMode ? [{ debugName: "sidebarPosition" }] : []));
15199
+ /** Show the thumbnail sidebar (shorthand for `sidebarPosition="none"`). @default true */
15200
+ this.showThumbnails = input(true, ...(ngDevMode ? [{ debugName: "showThumbnails", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15201
+ /** Show the keyword search above the thumbnails. @default true */
15202
+ this.showSearch = input(true, ...(ngDevMode ? [{ debugName: "showSearch", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15203
+ /** Show the slide-number input. @default true */
15204
+ this.showSlideInput = input(true, ...(ngDevMode ? [{ debugName: "showSlideInput", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15205
+ /** Show the fullscreen button. @default true */
15206
+ this.showFullscreen = input(true, ...(ngDevMode ? [{ debugName: "showFullscreen", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15207
+ /** Show the zoom buttons and allow zooming the stage. @default false */
15208
+ this.zoomable = input(false, ...(ngDevMode ? [{ debugName: "zoomable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15209
+ /** Zoom levels cycled by the zoom buttons / keys (1 = fit). */
15210
+ this.zoomSteps = input(DEFAULT_SLIDE_VIEWER_ZOOM_STEPS, ...(ngDevMode ? [{ debugName: "zoomSteps" }] : []));
15211
+ /** Navigation skips slides flagged `hidden`. @default false */
15212
+ this.skipHidden = input(false, ...(ngDevMode ? [{ debugName: "skipHidden", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15213
+ /** Tiles get a checkbox and `selectedIds` tracks the selection. @default false */
15214
+ this.selectable = input(false, ...(ngDevMode ? [{ debugName: "selectable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15215
+ /** Tiles can be dragged to a new position; the host receives `slideMove`. @default false */
15216
+ this.reorderable = input(false, ...(ngDevMode ? [{ debugName: "reorderable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15217
+ /**
15218
+ * Above this many slides the sidebar is virtualised: only the tiles in view (plus
15219
+ * `overscan` on each side) exist in the DOM, a spacer keeps the scroll height. Built
15220
+ * for decks of several hundred slides. @default 40
15221
+ */
15222
+ this.virtualizeAbove = input(40, ...(ngDevMode ? [{ debugName: "virtualizeAbove", transform: numberAttribute }] : [{ transform: numberAttribute }]));
15223
+ /** Tiles rendered beyond the visible window on each side. @default 6 */
15224
+ this.overscan = input(6, ...(ngDevMode ? [{ debugName: "overscan", transform: numberAttribute }] : [{ transform: numberAttribute }]));
15225
+ /**
15226
+ * How many neighbours on each side of the current slide are requested
15227
+ * (`fullImageRequest`) and decoded ahead so arrows feel instant. @default 1
15228
+ */
15229
+ this.prefetch = input(1, ...(ngDevMode ? [{ debugName: "prefetch", transform: numberAttribute }] : [{ transform: numberAttribute }]));
15230
+ /** Horizontal swipe on the stage moves slides (touch / pen). @default true */
15231
+ this.swipe = input(true, ...(ngDevMode ? [{ debugName: "swipe", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15232
+ /** Handle keyboard shortcuts on the region. @default true */
15233
+ this.keyboard = input(true, ...(ngDevMode ? [{ debugName: "keyboard", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
15234
+ /** Partial override of the keyboard shortcuts (see `DEFAULT_SLIDE_VIEWER_KEY_MAP`). */
15235
+ this.keyMap = input({}, ...(ngDevMode ? [{ debugName: "keyMap" }] : []));
15236
+ /** Milliseconds the keyword search waits after typing. @default 250 */
15237
+ this.searchDebounce = input(250, ...(ngDevMode ? [{ debugName: "searchDebounce", transform: numberAttribute }] : [{ transform: numberAttribute }]));
15238
+ /** Partial override of the user-visible strings (i18n). */
15239
+ this.labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : []));
15240
+ // ── Two-way state (models) ──────────────────────────────────────────────────
15241
+ /** Selected day. `null` selects nothing. */
15242
+ this.activeDayId = model(null, ...(ngDevMode ? [{ debugName: "activeDayId" }] : []));
15243
+ /** Current slide, 1-based. Clamped to the slide count. */
15244
+ this.page = model(1, ...(ngDevMode ? [{ debugName: "page" }] : []));
15245
+ /** Keyword filtering the thumbnails. */
15246
+ this.searchTerm = model('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
15247
+ /** Whether the note panel is expanded. */
15248
+ this.noteOpen = model(true, ...(ngDevMode ? [{ debugName: "noteOpen" }] : []));
15249
+ /** Stage zoom factor (1 = fit). Only applied while `zoomable`. */
15250
+ this.zoom = model(1, ...(ngDevMode ? [{ debugName: "zoom" }] : []));
15251
+ /** Ids of the selected slides (selection mode). */
15252
+ this.selectedIds = model([], ...(ngDevMode ? [{ debugName: "selectedIds" }] : []));
15253
+ // ── Outputs ─────────────────────────────────────────────────────────────────
15254
+ /** A day pill was chosen. */
15255
+ this.dayChange = output();
15256
+ /** The current slide changed (any source), with the slide and what moved it. */
15257
+ this.slideChange = output();
15258
+ /** A tool button was pressed. */
15259
+ this.tool = output();
15260
+ /** The stage entered / left fullscreen. */
15261
+ this.fullscreenChange = output();
15262
+ /** The keyword or its matches changed. */
15263
+ this.searchChange = output();
15264
+ /** The stage needs the full image of a page that only has a thumbnail (1-based). Emitted once per page. */
15265
+ this.fullImageRequest = output();
15266
+ /** A tile was dropped at a new position (reorder mode). The host reorders `slides`. */
15267
+ this.slideMove = output();
15268
+ /** "Try again" pressed in the error state. */
15269
+ this.retry = output();
15270
+ // ── Internal state ──────────────────────────────────────────────────────────
15271
+ this.isFullscreen = signal(false, ...(ngDevMode ? [{ debugName: "isFullscreen" }] : []));
15272
+ /** Stage image for the current page has finished loading. */
15273
+ this.stageImageLoaded = signal(false, ...(ngDevMode ? [{ debugName: "stageImageLoaded" }] : []));
15274
+ /**
15275
+ * Source currently painted on the stage. It lags behind `stageSrc` while the next
15276
+ * image is decoded off screen, so a page change or a thumbnail→full-image upgrade
15277
+ * never blanks the stage (no blink, no spinner over a picture that is already there).
15278
+ */
15279
+ this.displayedSrc = signal(null, ...(ngDevMode ? [{ debugName: "displayedSrc" }] : []));
15280
+ /** A newer image than the displayed one is being decoded. */
15281
+ this.stageBusy = signal(false, ...(ngDevMode ? [{ debugName: "stageBusy" }] : []));
15282
+ /** Text in the number input while the user edits it (null = mirror `page`). */
15283
+ this.pageDraft = signal(null, ...(ngDevMode ? [{ debugName: "pageDraft" }] : []));
15284
+ /** Virtualised sidebar: index range (into `matches`) of the tiles in the DOM. */
15285
+ this.windowRange = signal({ start: 0, end: 0 }, ...(ngDevMode ? [{ debugName: "windowRange" }] : []));
15286
+ /** Measured height of one tile incl. its gap, in px (a sensible guess until measured). */
15287
+ this.tileStride = signal(212, ...(ngDevMode ? [{ debugName: "tileStride" }] : []));
15288
+ /** Announced to assistive tech on slide change. */
15289
+ this.announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : []));
15290
+ /** Tile currently dragged (1-based) and the drop target it hovers. */
15291
+ this.dragFrom = signal(null, ...(ngDevMode ? [{ debugName: "dragFrom" }] : []));
15292
+ this.dragOver = signal(null, ...(ngDevMode ? [{ debugName: "dragOver" }] : []));
15293
+ this.searchTimer = null;
15294
+ this.lastEmittedPage = 0;
15295
+ this.lastStageSrc = undefined;
15296
+ this.stageLoadToken = 0;
15297
+ this.requestedFullImages = new Set();
15298
+ this.lastSlidesRef = null;
15299
+ this.pendingSource = 'api';
15300
+ /** Decoded neighbour images kept warm (bounded so 700-slide decks don't pile up). */
15301
+ this.decodeCache = new Map();
15302
+ this.scrollListener = null;
15303
+ this.resizeObserver = null;
15304
+ this.scrollRaf = null;
15305
+ this.swipeStart = null;
15306
+ // ── Computed ────────────────────────────────────────────────────────────────
15307
+ this.text = computed(() => ({
15308
+ ...DEFAULT_SLIDE_VIEWER_LABELS,
15309
+ ...this.labels(),
15310
+ }), ...(ngDevMode ? [{ debugName: "text" }] : []));
15311
+ this.keys = computed(() => ({
15312
+ ...DEFAULT_SLIDE_VIEWER_KEY_MAP,
15313
+ ...this.keyMap(),
15314
+ }), ...(ngDevMode ? [{ debugName: "keys" }] : []));
15315
+ this.total = computed(() => this.slides().length, ...(ngDevMode ? [{ debugName: "total" }] : []));
15316
+ /** `page` clamped into the current slide range. */
15317
+ this.currentPage = computed(() => clampSlidePage(this.page(), this.total()), ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
15318
+ this.currentSlide = computed(() => this.slides()[this.currentPage() - 1] ?? null, ...(ngDevMode ? [{ debugName: "currentSlide" }] : []));
15319
+ /** Stage image: full image, else the thumbnail while the full one is being produced. */
15320
+ this.stageSrc = computed(() => {
15321
+ const s = this.currentSlide();
15322
+ return s?.image ?? s?.thumbnail ?? null;
15323
+ }, ...(ngDevMode ? [{ debugName: "stageSrc" }] : []));
15324
+ /** The stage is showing a thumbnail in place of a missing full image. */
15325
+ this.stageIsFallback = computed(() => {
15326
+ const s = this.currentSlide();
15327
+ return !!s && !s.image && !!s.thumbnail;
15328
+ }, ...(ngDevMode ? [{ debugName: "stageIsFallback" }] : []));
15329
+ this.progressText = computed(() => {
15330
+ const p = this.loadingProgress();
15331
+ if (p == null || !Number.isFinite(p))
15332
+ return '';
15333
+ return this.format(this.text().loadingProgress, { percent: Math.max(0, Math.min(100, Math.round(p))) });
15334
+ }, ...(ngDevMode ? [{ debugName: "progressText" }] : []));
15335
+ /** Spinner veil: only while nothing is painted yet (first image) or the deck loads. */
15336
+ this.stageVeil = computed(() => !this.error() &&
15337
+ (this.loading() || (!!this.stageSrc() && !this.displayedSrc() && !this.stageImageLoaded())), ...(ngDevMode ? [{ debugName: "stageVeil" }] : []));
15338
+ /** Skeleton shimmer on the stage: something is coming and nothing is painted yet. */
15339
+ this.stageSkeleton = computed(() => this.stageVeil() && !this.displayedSrc(), ...(ngDevMode ? [{ debugName: "stageSkeleton" }] : []));
15340
+ /** Nothing to show and nothing on the way: the stage explains it. */
15341
+ this.isEmpty = computed(() => !this.error() && !this.loading() && this.total() === 0, ...(ngDevMode ? [{ debugName: "isEmpty" }] : []));
15342
+ this.isFirst = computed(() => this.currentPage() <= 1, ...(ngDevMode ? [{ debugName: "isFirst" }] : []));
15343
+ this.isLast = computed(() => this.currentPage() >= this.total(), ...(ngDevMode ? [{ debugName: "isLast" }] : []));
15344
+ this.navDisabled = computed(() => this.loading() || this.error() || this.total() === 0, ...(ngDevMode ? [{ debugName: "navDisabled" }] : []));
15345
+ this.pageInputValue = computed(() => this.pageDraft() ?? String(this.currentPage()), ...(ngDevMode ? [{ debugName: "pageInputValue" }] : []));
15346
+ /** The word before the number input ("Slide"), derived from the `slide` label. */
15347
+ this.slideWord = computed(() => this.text().slide.replace('{n}', '').trim(), ...(ngDevMode ? [{ debugName: "slideWord" }] : []));
15348
+ this.hasNote = computed(() => {
15349
+ const forced = this.showNote();
15350
+ if (forced !== undefined)
15351
+ return forced;
15352
+ const n = this.note();
15353
+ if (n == null)
15354
+ return false;
15355
+ return typeof n === 'string' ? n.trim().length > 0 : true;
15356
+ }, ...(ngDevMode ? [{ debugName: "hasNote" }] : []));
15357
+ this.showDays = computed(() => this.days().length > 0, ...(ngDevMode ? [{ debugName: "showDays" }] : []));
15358
+ this.sidebar = computed(() => this.showThumbnails() ? this.sidebarPosition() : 'none', ...(ngDevMode ? [{ debugName: "sidebar" }] : []));
15359
+ this.showDownloadTool = computed(() => this.tools().includes('downloadSlides'), ...(ngDevMode ? [{ debugName: "showDownloadTool" }] : []));
15360
+ this.zoomLevel = computed(() => (this.zoomable() ? Math.max(1, this.zoom()) : 1), ...(ngDevMode ? [{ debugName: "zoomLevel" }] : []));
15361
+ this.isZoomed = computed(() => this.zoomLevel() > 1.001, ...(ngDevMode ? [{ debugName: "isZoomed" }] : []));
15362
+ this.zoomPercent = computed(() => Math.round(this.zoomLevel() * 100), ...(ngDevMode ? [{ debugName: "zoomPercent" }] : []));
15363
+ /** Virtualisation only kicks in for long decks. */
15364
+ this.windowed = computed(() => this.total() > this.virtualizeAbove(), ...(ngDevMode ? [{ debugName: "windowed" }] : []));
15365
+ /** Tiles to render: everything, or the current window of `matches` when virtualised. */
15366
+ this.visibleTiles = computed(() => {
15367
+ const all = this.matches();
15368
+ if (!this.windowed())
15369
+ return all;
15370
+ const { start, end } = this.windowRange();
15371
+ return all.slice(start, end);
15372
+ }, ...(ngDevMode ? [{ debugName: "visibleTiles" }] : []));
15373
+ /** Spacer heights (px) above / below the rendered window. */
15374
+ this.spacerTop = computed(() => this.windowed() ? this.windowRange().start * this.tileStride() : 0, ...(ngDevMode ? [{ debugName: "spacerTop" }] : []));
15375
+ this.spacerBottom = computed(() => this.windowed() ? Math.max(0, this.matches().length - this.windowRange().end) * this.tileStride() : 0, ...(ngDevMode ? [{ debugName: "spacerBottom" }] : []));
15376
+ this.selectedSet = computed(() => new Set(this.selectedIds()), ...(ngDevMode ? [{ debugName: "selectedSet" }] : []));
15377
+ /** 1-based numbers of the slides matching the keyword (all when empty). */
15378
+ this.matches = computed(() => {
15379
+ const term = this.searchTerm().trim().toLowerCase();
15380
+ const slides = this.slides();
15381
+ const out = [];
15382
+ for (let i = 0; i < slides.length; i++) {
15383
+ if (!term || this.slideMatches(slides[i], i + 1, term))
15384
+ out.push(i + 1);
15385
+ }
15386
+ return out;
15387
+ }, ...(ngDevMode ? [{ debugName: "matches" }] : []));
15388
+ this.matchSet = computed(() => new Set(this.matches()), ...(ngDevMode ? [{ debugName: "matchSet" }] : []));
15389
+ this.isSearching = computed(() => this.searchTerm().trim().length > 0, ...(ngDevMode ? [{ debugName: "isSearching" }] : []));
15390
+ this.searchSummary = computed(() => this.format(this.text().searchResults, {
15391
+ n: this.matches().length,
15392
+ total: this.total(),
15393
+ }), ...(ngDevMode ? [{ debugName: "searchSummary" }] : []));
15394
+ /** Skeleton rows drawn in the sidebar while loading. */
15395
+ this.skeletonRows = [0, 1, 2, 3];
15396
+ this.hostClasses = computed(() => [
15397
+ 'pecb-slide-viewer',
15398
+ this.loading() ? 'pecb-slide-viewer--loading' : '',
15399
+ this.error() ? 'pecb-slide-viewer--error' : '',
15400
+ this.isEmpty() ? 'pecb-slide-viewer--empty' : '',
15401
+ this.isFullscreen() ? 'pecb-slide-viewer--fullscreen' : '',
15402
+ this.sidebar() === 'none' ? 'pecb-slide-viewer--no-sidebar' : '',
15403
+ this.sidebar() === 'start' ? 'pecb-slide-viewer--sidebar-start' : '',
15404
+ this.isSearching() ? 'pecb-slide-viewer--searching' : '',
15405
+ this.isZoomed() ? 'pecb-slide-viewer--zoomed' : '',
15406
+ this.selectable() ? 'pecb-slide-viewer--selectable' : '',
15407
+ this.reorderable() ? 'pecb-slide-viewer--reorderable' : '',
15408
+ ]
15409
+ .filter(Boolean)
15410
+ .join(' '), ...(ngDevMode ? [{ debugName: "hostClasses" }] : []));
15411
+ // Keep `page` inside the range whenever slides change (a shorter day, a reload).
15412
+ effect(() => {
15413
+ const clamped = this.currentPage();
15414
+ untracked(() => {
15415
+ if (this.page() !== clamped)
15416
+ this.page.set(clamped);
15417
+ });
15418
+ });
15419
+ // Double-buffered stage: decode the next source off screen, then swap. The flag is
15420
+ // reset per source, not per page/slides update (a deck that fills in progressively
15421
+ // replaces `slides` on every page).
15422
+ effect(() => {
15423
+ const src = this.stageSrc();
15424
+ untracked(() => {
15425
+ if (src === this.lastStageSrc)
15426
+ return;
15427
+ this.lastStageSrc = src;
15428
+ this.showStageSource(src);
15429
+ });
15430
+ });
15431
+ // Lazy full images: ask the host once per page for the current slide and its
15432
+ // neighbours (`prefetch`), current first. Decoded neighbours are kept warm.
15433
+ effect(() => {
15434
+ const page = this.currentPage();
15435
+ const slides = this.slides();
15436
+ const reach = Math.max(0, this.prefetch());
15437
+ untracked(() => {
15438
+ if (this.lastSlidesRef !== slides) {
15439
+ // A new array: keep only pages that still lack an image, so a later day can
15440
+ // ask again for the same numbers.
15441
+ this.lastSlidesRef = slides;
15442
+ this.requestedFullImages = new Set([...this.requestedFullImages].filter((p) => slides[p - 1] && !slides[p - 1].image));
15443
+ }
15444
+ if (slides.length === 0)
15445
+ return;
15446
+ const order = [page];
15447
+ for (let d = 1; d <= reach; d++) {
15448
+ if (page + d <= slides.length)
15449
+ order.push(page + d);
15450
+ if (page - d >= 1)
15451
+ order.push(page - d);
15452
+ }
15453
+ for (const n of order) {
15454
+ const slide = slides[n - 1];
15455
+ if (!slide)
15456
+ continue;
15457
+ if (!slide.image) {
15458
+ if (!this.requestedFullImages.has(n)) {
15459
+ this.requestedFullImages.add(n);
15460
+ this.fullImageRequest.emit(n);
15461
+ }
15462
+ }
15463
+ else if (n !== page) {
15464
+ this.warmDecode(slide.image);
15465
+ }
15466
+ }
15467
+ });
15468
+ });
15469
+ // Announce the slide for assistive tech.
15470
+ effect(() => {
15471
+ const page = this.currentPage();
15472
+ const total = this.total();
15473
+ untracked(() => {
15474
+ this.announcement.set(total > 0 ? this.format(this.text().slideAnnouncement, { n: page, total }) : '');
15475
+ });
15476
+ });
15477
+ // Announce page changes once, whatever moved the page (models included).
15478
+ effect(() => {
15479
+ const page = this.currentPage();
15480
+ const total = this.total();
15481
+ untracked(() => {
15482
+ this.pageDraft.set(null);
15483
+ if (total > 0 && page !== this.lastEmittedPage) {
15484
+ this.lastEmittedPage = page;
15485
+ this.slideChange.emit({ page, total, slide: this.currentSlide(), source: this.pendingSource });
15486
+ this.pendingSource = 'api';
15487
+ }
15488
+ this.scrollActiveThumbnailIntoView();
15489
+ });
15490
+ });
15491
+ // Report search matches.
15492
+ effect(() => {
15493
+ const term = this.searchTerm();
15494
+ const matches = this.matches();
15495
+ untracked(() => this.searchChange.emit({ term, matches }));
15496
+ });
15497
+ // Zoom is only meaningful while zoomable.
15498
+ effect(() => {
15499
+ if (!this.zoomable())
15500
+ untracked(() => this.zoom.set(1));
15501
+ });
15502
+ // Recompute the rendered window whenever the list, the mode or the sidebar changes.
15503
+ effect(() => {
15504
+ this.matches();
15505
+ this.windowed();
15506
+ this.sidebar();
15507
+ this.tileStride();
15508
+ untracked(() => this.scheduleWindowUpdate());
15509
+ });
15510
+ if (this.isBrowser) {
15511
+ afterNextRender(() => {
15512
+ this.attachSidebarListeners();
15513
+ this.scheduleWindowUpdate();
15514
+ this.scrollActiveThumbnailIntoView(false);
15515
+ });
15516
+ this.destroyRef.onDestroy(() => {
15517
+ this.clearSearchTimer();
15518
+ this.detachSidebarListeners();
15519
+ this.decodeCache.clear();
15520
+ });
15521
+ }
15522
+ }
15523
+ // ── Public API ──────────────────────────────────────────────────────────────
15524
+ /** Go to a 1-based slide (clamped). Hidden slides are stepped over while `skipHidden`. */
15525
+ goTo(page, source = 'api') {
15526
+ if (this.navDisabled())
15527
+ return;
15528
+ let target = clampSlidePage(page, this.total());
15529
+ if (this.skipHidden() && this.isHidden(target)) {
15530
+ const forward = target >= this.currentPage();
15531
+ const resolved = forward
15532
+ ? (this.nextVisible(target) ?? this.previousVisible(target))
15533
+ : (this.previousVisible(target) ?? this.nextVisible(target));
15534
+ if (resolved === null)
15535
+ return;
15536
+ target = resolved;
15537
+ }
15538
+ if (target === this.currentPage()) {
15539
+ this.pageDraft.set(null);
15540
+ return;
15541
+ }
15542
+ this.pendingSource = source;
15543
+ this.page.set(target);
15544
+ }
15545
+ first(source = 'nav') {
15546
+ this.goTo(1, source);
15547
+ }
15548
+ previous(source = 'nav') {
15549
+ const target = this.skipHidden() ? this.previousVisible(this.currentPage()) : this.currentPage() - 1;
15550
+ if (target !== null)
15551
+ this.goTo(target, source);
15552
+ }
15553
+ next(source = 'nav') {
15554
+ const target = this.skipHidden() ? this.nextVisible(this.currentPage()) : this.currentPage() + 1;
15555
+ if (target !== null)
15556
+ this.goTo(target, source);
15557
+ }
15558
+ last(source = 'nav') {
15559
+ this.goTo(this.total(), source);
15560
+ }
15561
+ /** Select a day: emits `dayChange`, resets the page and clears the search. */
15562
+ selectDay(day) {
15563
+ if (day.id === this.activeDayId())
15564
+ return;
15565
+ this.activeDayId.set(day.id);
15566
+ this.clearSearch();
15567
+ this.pendingSource = 'day';
15568
+ this.page.set(1);
15569
+ this.dayChange.emit(day);
15570
+ }
15571
+ /** Press a built-in tool. The disabled download tool is ignored. */
15572
+ pressTool(tool) {
15573
+ if (tool === 'downloadSlides' && !this.canDownloadSlides())
15574
+ return;
15575
+ this.tool.emit({ tool, page: this.currentPage() });
15576
+ }
15577
+ toggleNote() {
15578
+ this.noteOpen.update((open) => !open);
15579
+ }
15580
+ /** "Try again" in the error state. */
15581
+ requestRetry() {
15582
+ this.retry.emit();
15583
+ }
15584
+ /** Enter / exit fullscreen on the stage. */
15585
+ async toggleFullscreen() {
15586
+ if (!this.isBrowser)
15587
+ return;
15588
+ const stage = this.stageRef()?.nativeElement;
15589
+ if (!stage)
15590
+ return;
15591
+ try {
15592
+ if (document.fullscreenElement) {
15593
+ await document.exitFullscreen();
15594
+ }
15595
+ else if (stage.requestFullscreen) {
15596
+ await stage.requestFullscreen();
15597
+ }
15598
+ }
15599
+ catch {
15600
+ // Fullscreen can be denied (iframe sandbox, user-gesture rules) — ignore.
15601
+ }
15602
+ }
15603
+ /** Next zoom step (wraps back to fit after the last one). */
15604
+ zoomIn() {
15605
+ if (!this.zoomable())
15606
+ return;
15607
+ const steps = this.zoomSteps();
15608
+ const current = this.zoomLevel();
15609
+ const next = steps.find((s) => s > current + 0.001);
15610
+ this.zoom.set(next ?? steps[0] ?? 1);
15611
+ }
15612
+ zoomOut() {
15613
+ if (!this.zoomable())
15614
+ return;
15615
+ const steps = [...this.zoomSteps()].reverse();
15616
+ const current = this.zoomLevel();
15617
+ const prev = steps.find((s) => s < current - 0.001);
15618
+ this.zoom.set(prev ?? 1);
15619
+ }
15620
+ zoomReset() {
15621
+ this.zoom.set(1);
15622
+ }
15623
+ /** Clear the keyword filter. */
15624
+ clearSearch() {
15625
+ this.clearSearchTimer();
15626
+ if (this.searchTerm() !== '')
15627
+ this.searchTerm.set('');
15628
+ const input = this.searchRef()?.nativeElement;
15629
+ if (input)
15630
+ input.value = '';
15631
+ }
15632
+ /** Move keyboard focus to the search field. */
15633
+ focusSearch() {
15634
+ this.searchRef()?.nativeElement.focus();
15635
+ }
15636
+ /** Toggle a slide in the selection (selection mode). */
15637
+ toggleSelected(slide, n) {
15638
+ const id = this.slideId(slide, n);
15639
+ this.selectedIds.update((ids) => (ids.includes(id) ? ids.filter((x) => x !== id) : [...ids, id]));
15640
+ }
15641
+ /** Select every (matching) slide, or clear the selection. */
15642
+ selectAll(selected = true) {
15643
+ if (!selected) {
15644
+ this.selectedIds.set([]);
15645
+ return;
15646
+ }
15647
+ const slides = this.slides();
15648
+ this.selectedIds.set(this.matches().map((n) => this.slideId(slides[n - 1], n)));
15649
+ }
15650
+ // ── Template handlers ───────────────────────────────────────────────────────
15651
+ onStageImageLoad() {
15652
+ this.stageImageLoaded.set(true);
15653
+ }
15654
+ onStageImageError() {
15655
+ // Treat a broken image like a loaded one so the veil doesn't stay forever.
15656
+ this.stageImageLoaded.set(true);
15657
+ }
15658
+ onPageInput(event) {
15659
+ this.pageDraft.set(event.target.value);
15660
+ }
15661
+ onPageCommit(event) {
15662
+ const input = event.target;
15663
+ const value = Number(input.value);
15664
+ if (Number.isFinite(value) && value > 0) {
15665
+ this.goTo(value, 'input');
15666
+ }
15667
+ this.resetPageInput(input);
15668
+ }
15669
+ onPageInputKeydown(event) {
15670
+ if (event.key === 'Enter') {
15671
+ event.preventDefault();
15672
+ this.onPageCommit(event);
15673
+ event.target.blur();
15674
+ }
15675
+ else if (event.key === 'Escape') {
15676
+ this.resetPageInput(event.target);
15677
+ event.target.blur();
15678
+ }
15679
+ }
15680
+ onSearchInput(event) {
15681
+ const value = event.target.value;
15682
+ this.clearSearchTimer();
15683
+ const delay = this.searchDebounce();
15684
+ if (delay <= 0) {
15685
+ this.searchTerm.set(value);
15686
+ return;
15687
+ }
15688
+ this.zone.runOutsideAngular(() => {
15689
+ this.searchTimer = setTimeout(() => {
15690
+ this.searchTimer = null;
15691
+ this.zone.run(() => this.searchTerm.set(value));
15692
+ }, delay);
15693
+ });
15694
+ }
15695
+ onSearchKeydown(event) {
15696
+ if (event.key === 'Escape') {
15697
+ event.preventDefault();
15698
+ this.clearSearch();
15699
+ }
15700
+ }
15701
+ onThumbnailClick(page) {
15702
+ this.goTo(page, 'thumbnail');
15703
+ }
15704
+ onTileCheckbox(event, slide, n) {
15705
+ event.stopPropagation();
15706
+ this.toggleSelected(slide, n);
15707
+ }
15708
+ /** Touch / pen swipe on the stage (not while zoomed: then the finger pans). */
15709
+ onStagePointerDown(event) {
15710
+ if (!this.swipe() || this.isZoomed() || event.pointerType === 'mouse') {
15711
+ this.swipeStart = null;
15712
+ return;
15713
+ }
15714
+ this.swipeStart = { x: event.clientX, y: event.clientY, id: event.pointerId };
15715
+ }
15716
+ onStagePointerUp(event) {
15717
+ const start = this.swipeStart;
15718
+ this.swipeStart = null;
15719
+ if (!start || start.id !== event.pointerId)
15720
+ return;
15721
+ const dx = event.clientX - start.x;
15722
+ const dy = event.clientY - start.y;
15723
+ // Mostly horizontal and long enough to be deliberate.
15724
+ if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy) * 1.5)
15725
+ return;
15726
+ if (dx < 0)
15727
+ this.next('wheel');
15728
+ else
15729
+ this.previous('wheel');
15730
+ }
15731
+ onStagePointerCancel() {
15732
+ this.swipeStart = null;
15733
+ }
15734
+ /** Mouse wheel over the stage: zooms with Ctrl/⌘ while zoomable, navigates in fullscreen. */
15735
+ onStageWheel(event) {
15736
+ if ((event.ctrlKey || event.metaKey) && this.zoomable()) {
15737
+ event.preventDefault();
15738
+ if (event.deltaY < 0)
15739
+ this.zoomIn();
15740
+ else
15741
+ this.zoomOut();
15742
+ return;
15743
+ }
15744
+ if (!this.isFullscreen() || this.isZoomed())
15745
+ return;
15746
+ if (Math.abs(event.deltaY) < 4)
15747
+ return;
15748
+ event.preventDefault();
15749
+ if (event.deltaY > 0)
15750
+ this.next('wheel');
15751
+ else
15752
+ this.previous('wheel');
15753
+ }
15754
+ /** Keyboard shortcuts on the viewer region (see `keyMap`). */
15755
+ onRootKeydown(event) {
15756
+ if (!this.keyboard())
15757
+ return;
15758
+ const target = event.target;
15759
+ const root = this.rootRef()?.nativeElement;
15760
+ // Let inputs, selects and the projected controls handle their own keys.
15761
+ if (target &&
15762
+ target !== root &&
15763
+ target.matches('input, select, textarea, [contenteditable="true"]')) {
15764
+ return;
15765
+ }
15766
+ if (event.altKey || event.ctrlKey || event.metaKey)
15767
+ return;
15768
+ const action = this.actionForKey(event.key);
15769
+ if (!action)
15770
+ return;
15771
+ switch (action) {
15772
+ case 'next':
15773
+ this.next('keyboard');
15774
+ break;
15775
+ case 'previous':
15776
+ this.previous('keyboard');
15777
+ break;
15778
+ case 'first':
15779
+ this.first('keyboard');
15780
+ break;
15781
+ case 'last':
15782
+ this.last('keyboard');
15783
+ break;
15784
+ case 'fullscreen':
15785
+ if (!this.showFullscreen())
15786
+ return;
15787
+ void this.toggleFullscreen();
15788
+ break;
15789
+ case 'search':
15790
+ if (this.sidebar() === 'none' || !this.showSearch())
15791
+ return;
15792
+ this.focusSearch();
15793
+ break;
15794
+ case 'zoomIn':
15795
+ if (!this.zoomable())
15796
+ return;
15797
+ this.zoomIn();
15798
+ break;
15799
+ case 'zoomOut':
15800
+ if (!this.zoomable())
15801
+ return;
15802
+ this.zoomOut();
15803
+ break;
15804
+ case 'zoomReset':
15805
+ if (!this.zoomable())
15806
+ return;
15807
+ this.zoomReset();
15808
+ break;
15809
+ }
15810
+ event.preventDefault();
15811
+ }
15812
+ onDocumentFullscreenChange() {
15813
+ if (!this.isBrowser)
15814
+ return;
15815
+ const stage = this.stageRef()?.nativeElement;
15816
+ const active = !!stage && document.fullscreenElement === stage;
15817
+ if (active !== this.isFullscreen()) {
15818
+ this.isFullscreen.set(active);
15819
+ this.fullscreenChange.emit(active);
15820
+ }
15821
+ }
15822
+ // ── Drag & drop reorder ─────────────────────────────────────────────────────
15823
+ onTileDragStart(event, n) {
15824
+ if (!this.reorderable())
15825
+ return;
15826
+ this.dragFrom.set(n);
15827
+ if (event.dataTransfer) {
15828
+ event.dataTransfer.effectAllowed = 'move';
15829
+ event.dataTransfer.setData('text/plain', String(n));
15830
+ }
15831
+ }
15832
+ onTileDragOver(event, n) {
15833
+ if (!this.reorderable() || this.dragFrom() === null)
15834
+ return;
15835
+ event.preventDefault();
15836
+ if (event.dataTransfer)
15837
+ event.dataTransfer.dropEffect = 'move';
15838
+ if (this.dragOver() !== n)
15839
+ this.dragOver.set(n);
15840
+ }
15841
+ onTileDrop(event, n) {
15842
+ if (!this.reorderable())
15843
+ return;
15844
+ event.preventDefault();
15845
+ const from = this.dragFrom();
15846
+ this.dragFrom.set(null);
15847
+ this.dragOver.set(null);
15848
+ if (from === null || from === n)
15849
+ return;
15850
+ const slide = this.slides()[from - 1];
15851
+ if (slide)
15852
+ this.slideMove.emit({ from, to: n, slide });
15853
+ }
15854
+ onTileDragEnd() {
15855
+ this.dragFrom.set(null);
15856
+ this.dragOver.set(null);
15857
+ }
15858
+ // ── Helpers ─────────────────────────────────────────────────────────────────
15859
+ /** Accessible title of a slide (`title`, else `{slide} {n}`). */
15860
+ slideTitle(slide, n) {
15861
+ return slide?.title?.trim() || this.format(this.text().slide, { n });
15862
+ }
15863
+ slideId(slide, n) {
15864
+ return slide.id ?? n;
15865
+ }
15866
+ thumbnailSrc(slide) {
15867
+ return slide.thumbnail ?? slide.image ?? null;
15868
+ }
15869
+ /** A tile whose thumbnail is still being produced (big decks publish pages first). */
15870
+ tilePending(slide) {
15871
+ return !slide.thumbnail && !slide.image;
15872
+ }
15873
+ isSelected(slide, n) {
15874
+ return this.selectedSet().has(this.slideId(slide, n));
15875
+ }
15876
+ selectLabel(n) {
15877
+ return this.format(this.text().selectSlide, { n });
15878
+ }
15879
+ thumbnailContext(slide, n) {
15880
+ return {
15881
+ $implicit: slide,
15882
+ index: n,
15883
+ active: n === this.currentPage(),
15884
+ selected: this.isSelected(slide, n),
15885
+ };
15886
+ }
15887
+ dayContext(day) {
15888
+ return { $implicit: day, active: day.id === this.activeDayId() };
15889
+ }
15890
+ isHidden(n) {
15891
+ return !!this.slides()[n - 1]?.hidden;
15892
+ }
15893
+ nextVisible(from) {
15894
+ for (let p = from + 1; p <= this.total(); p++) {
15895
+ if (!this.isHidden(p))
15896
+ return p;
15897
+ }
15898
+ return null;
15899
+ }
15900
+ previousVisible(from) {
15901
+ for (let p = from - 1; p >= 1; p--) {
15902
+ if (!this.isHidden(p))
15903
+ return p;
15904
+ }
15905
+ return null;
15906
+ }
15907
+ actionForKey(key) {
15908
+ const map = this.keys();
15909
+ for (const action of Object.keys(map)) {
15910
+ if (map[action]?.includes(key))
15911
+ return action;
15912
+ }
15913
+ return null;
15914
+ }
15915
+ slideMatches(slide, n, term) {
15916
+ const haystack = [
15917
+ slide.title ?? '',
15918
+ slide.text ?? '',
15919
+ this.format(this.text().slide, { n }),
15920
+ String(n),
15921
+ ]
15922
+ .join(' ')
15923
+ .toLowerCase();
15924
+ return haystack.includes(term);
15925
+ }
15926
+ format(template, values) {
15927
+ return template.replace(/\{(\w+)\}/g, (_, key) => key in values ? String(values[key]) : `{${key}}`);
15928
+ }
15929
+ /**
15930
+ * Show the current page in the number field again. Written to the DOM directly:
15931
+ * a number input sanitises junk to `''`, and the `[value]` binding would not
15932
+ * re-apply an unchanged `'20'` on top of it.
15933
+ */
15934
+ resetPageInput(input) {
15935
+ this.pageDraft.set(null);
15936
+ if (input)
15937
+ input.value = String(this.currentPage());
15938
+ }
15939
+ scrollActiveThumbnailIntoView(smooth = true) {
15940
+ if (!this.isBrowser)
15941
+ return;
15942
+ const list = this.thumbsRef()?.nativeElement;
15943
+ if (!list)
15944
+ return;
15945
+ const reduce = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
15946
+ const behavior = smooth && !reduce ? 'smooth' : 'auto';
15947
+ if (this.windowed()) {
15948
+ // The active tile may not be in the DOM: position from its index. The window is
15949
+ // moved first so the spacers are right when the scroll lands (no anchoring jump).
15950
+ const index = this.matches().indexOf(this.currentPage());
15951
+ if (index < 0)
15952
+ return;
15953
+ this.measureTile(list);
15954
+ const stride = this.tileStride();
15955
+ const top = index * stride;
15956
+ const bottom = top + stride;
15957
+ let target = null;
15958
+ if (top < list.scrollTop)
15959
+ target = top;
15960
+ else if (bottom > list.scrollTop + list.clientHeight)
15961
+ target = bottom - list.clientHeight;
15962
+ if (target === null)
15963
+ return;
15964
+ // Smooth only for short hops; a jump across hundreds of tiles snaps.
15965
+ const far = Math.abs(target - list.scrollTop) > 3 * Math.max(list.clientHeight, stride);
15966
+ const over = Math.max(0, this.overscan());
15967
+ const visible = Math.ceil((list.clientHeight || 600) / stride) + 1;
15968
+ const first = Math.floor(target / stride);
15969
+ this.windowRange.set({
15970
+ start: Math.max(0, first - over),
15971
+ end: Math.min(this.matches().length, first + visible + over),
15972
+ });
15973
+ const goal = target;
15974
+ requestAnimationFrame(() => {
15975
+ // 'instant' beats the list's CSS `scroll-behavior: smooth` for long jumps.
15976
+ list.scrollTo({ top: goal, behavior: far ? 'instant' : behavior });
15977
+ this.scheduleWindowUpdate();
15978
+ });
15979
+ return;
15980
+ }
15981
+ // The active class lands on the next render.
15982
+ requestAnimationFrame(() => {
15983
+ const active = list.querySelector('.pecb-slide-viewer__thumb--active');
15984
+ active?.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior });
15985
+ });
15986
+ }
15987
+ // ── Virtualised sidebar ─────────────────────────────────────────────────────
15988
+ attachSidebarListeners() {
15989
+ const list = this.thumbsRef()?.nativeElement;
15990
+ if (!list || this.scrollListener)
15991
+ return;
15992
+ this.zone.runOutsideAngular(() => {
15993
+ this.scrollListener = () => this.scheduleWindowUpdate();
15994
+ list.addEventListener('scroll', this.scrollListener, { passive: true });
15995
+ if (typeof ResizeObserver !== 'undefined') {
15996
+ this.resizeObserver = new ResizeObserver(() => this.scheduleWindowUpdate());
15997
+ this.resizeObserver.observe(list);
15998
+ }
15999
+ });
16000
+ }
16001
+ detachSidebarListeners() {
16002
+ const list = this.thumbsRef()?.nativeElement;
16003
+ if (list && this.scrollListener)
16004
+ list.removeEventListener('scroll', this.scrollListener);
16005
+ this.scrollListener = null;
16006
+ this.resizeObserver?.disconnect();
16007
+ this.resizeObserver = null;
16008
+ if (this.scrollRaf !== null)
16009
+ cancelAnimationFrame(this.scrollRaf);
16010
+ this.scrollRaf = null;
16011
+ }
16012
+ /** Coalesce scroll / resize / data changes into one window computation per frame. */
16013
+ scheduleWindowUpdate() {
16014
+ if (!this.isBrowser)
16015
+ return;
16016
+ if (this.scrollRaf !== null)
16017
+ return;
16018
+ this.scrollRaf = requestAnimationFrame(() => {
16019
+ this.scrollRaf = null;
16020
+ this.updateWindow();
16021
+ });
16022
+ }
16023
+ updateWindow() {
16024
+ const list = this.thumbsRef()?.nativeElement;
16025
+ if (!list)
16026
+ return;
16027
+ if (!this.scrollListener)
16028
+ this.attachSidebarListeners();
16029
+ if (!this.windowed()) {
16030
+ this.measureTile(list);
16031
+ return;
16032
+ }
16033
+ this.measureTile(list);
16034
+ const stride = this.tileStride();
16035
+ const count = this.matches().length;
16036
+ const viewport = list.clientHeight || 600;
16037
+ const first = Math.floor(list.scrollTop / stride);
16038
+ const visible = Math.ceil(viewport / stride) + 1;
16039
+ const over = Math.max(0, this.overscan());
16040
+ const start = Math.max(0, first - over);
16041
+ const end = Math.min(count, first + visible + over);
16042
+ const current = this.windowRange();
16043
+ if (current.start !== start || current.end !== end) {
16044
+ this.zone.run(() => this.windowRange.set({ start, end }));
16045
+ }
16046
+ }
16047
+ /** Tile height incl. the list gap, measured from the first rendered tile. */
16048
+ measureTile(list) {
16049
+ const tile = list.querySelector('.pecb-slide-viewer__thumb:not(.pecb-slide-viewer__thumb--skeleton)');
16050
+ if (!tile)
16051
+ return;
16052
+ const gap = parseFloat(getComputedStyle(list).rowGap || '0') || 0;
16053
+ const stride = Math.round(tile.getBoundingClientRect().height + gap);
16054
+ const previous = this.tileStride();
16055
+ if (stride > 0 && Math.abs(stride - previous) > 1) {
16056
+ // Keep the same tile at the top when the estimate is corrected.
16057
+ const index = list.scrollTop / previous;
16058
+ this.zone.run(() => this.tileStride.set(stride));
16059
+ if (index > 0)
16060
+ list.scrollTo({ top: index * stride, behavior: 'instant' });
16061
+ }
16062
+ }
16063
+ // ── Neighbour decode cache ──────────────────────────────────────────────────
16064
+ /** Decode an image off screen so a later swap paints instantly. Bounded to 8 entries. */
16065
+ warmDecode(src) {
16066
+ if (!this.isBrowser || typeof Image === 'undefined' || this.decodeCache.has(src))
16067
+ return;
16068
+ const img = new Image();
16069
+ img.decoding = 'async';
16070
+ img.src = src;
16071
+ this.decodeCache.set(src, img);
16072
+ if (typeof img.decode === 'function')
16073
+ img.decode().catch(() => undefined);
16074
+ while (this.decodeCache.size > 8) {
16075
+ const oldest = this.decodeCache.keys().next().value;
16076
+ if (oldest === undefined)
16077
+ break;
16078
+ this.decodeCache.delete(oldest);
16079
+ }
16080
+ }
16081
+ /**
16082
+ * Paint `src` on the stage once it is decoded. While decoding, the previous image
16083
+ * stays on screen (`stageBusy` marks the transition); the first image, with nothing
16084
+ * to keep, shows the veil instead.
16085
+ */
16086
+ showStageSource(src) {
16087
+ const token = ++this.stageLoadToken;
16088
+ if (!src) {
16089
+ this.displayedSrc.set(null);
16090
+ this.stageImageLoaded.set(false);
16091
+ this.stageBusy.set(false);
16092
+ return;
16093
+ }
16094
+ if (!this.isBrowser || typeof Image === 'undefined') {
16095
+ this.displayedSrc.set(src);
16096
+ this.stageImageLoaded.set(true);
16097
+ return;
16098
+ }
16099
+ if (this.displayedSrc()) {
16100
+ this.stageBusy.set(true);
16101
+ }
16102
+ else {
16103
+ this.stageImageLoaded.set(false);
16104
+ }
16105
+ // A neighbour decoded ahead paints straight away.
16106
+ const probe = this.decodeCache.get(src) ?? new Image();
16107
+ probe.decoding = 'async';
16108
+ const commit = () => {
16109
+ if (token !== this.stageLoadToken)
16110
+ return; // superseded by a newer source
16111
+ this.zone.run(() => {
16112
+ this.displayedSrc.set(src);
16113
+ this.stageImageLoaded.set(true);
16114
+ this.stageBusy.set(false);
16115
+ });
16116
+ };
16117
+ const ready = () => {
16118
+ // decode() paints without a flash of unpainted image; fall back if unsupported.
16119
+ const decoded = typeof probe.decode === 'function' ? probe.decode() : Promise.resolve();
16120
+ decoded.then(commit, commit);
16121
+ };
16122
+ if (probe.complete && probe.naturalWidth > 0) {
16123
+ ready();
16124
+ return;
16125
+ }
16126
+ probe.onload = ready;
16127
+ probe.onerror = commit; // a broken image still replaces the stage (alt text shows)
16128
+ if (!probe.src)
16129
+ probe.src = src;
16130
+ }
16131
+ clearSearchTimer() {
16132
+ if (this.searchTimer !== null) {
16133
+ clearTimeout(this.searchTimer);
16134
+ this.searchTimer = null;
16135
+ }
16136
+ }
16137
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: SlideViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
16138
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: SlideViewerComponent, isStandalone: true, selector: "pecb-slide-viewer", inputs: { days: { classPropertyName: "days", publicName: "days", isSignal: true, isRequired: false, transformFunction: null }, slides: { classPropertyName: "slides", publicName: "slides", isSignal: true, isRequired: false, transformFunction: null }, note: { classPropertyName: "note", publicName: "note", isSignal: true, isRequired: false, transformFunction: null }, showNote: { classPropertyName: "showNote", publicName: "showNote", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingProgress: { classPropertyName: "loadingProgress", publicName: "loadingProgress", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, tools: { classPropertyName: "tools", publicName: "tools", isSignal: true, isRequired: false, transformFunction: null }, canDownloadSlides: { classPropertyName: "canDownloadSlides", publicName: "canDownloadSlides", isSignal: true, isRequired: false, transformFunction: null }, stageAspectRatio: { classPropertyName: "stageAspectRatio", publicName: "stageAspectRatio", isSignal: true, isRequired: false, transformFunction: null }, sidebarPosition: { classPropertyName: "sidebarPosition", publicName: "sidebarPosition", isSignal: true, isRequired: false, transformFunction: null }, showThumbnails: { classPropertyName: "showThumbnails", publicName: "showThumbnails", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, showSlideInput: { classPropertyName: "showSlideInput", publicName: "showSlideInput", isSignal: true, isRequired: false, transformFunction: null }, showFullscreen: { classPropertyName: "showFullscreen", publicName: "showFullscreen", isSignal: true, isRequired: false, transformFunction: null }, zoomable: { classPropertyName: "zoomable", publicName: "zoomable", isSignal: true, isRequired: false, transformFunction: null }, zoomSteps: { classPropertyName: "zoomSteps", publicName: "zoomSteps", isSignal: true, isRequired: false, transformFunction: null }, skipHidden: { classPropertyName: "skipHidden", publicName: "skipHidden", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, virtualizeAbove: { classPropertyName: "virtualizeAbove", publicName: "virtualizeAbove", isSignal: true, isRequired: false, transformFunction: null }, overscan: { classPropertyName: "overscan", publicName: "overscan", isSignal: true, isRequired: false, transformFunction: null }, prefetch: { classPropertyName: "prefetch", publicName: "prefetch", isSignal: true, isRequired: false, transformFunction: null }, swipe: { classPropertyName: "swipe", publicName: "swipe", isSignal: true, isRequired: false, transformFunction: null }, keyboard: { classPropertyName: "keyboard", publicName: "keyboard", isSignal: true, isRequired: false, transformFunction: null }, keyMap: { classPropertyName: "keyMap", publicName: "keyMap", isSignal: true, isRequired: false, transformFunction: null }, searchDebounce: { classPropertyName: "searchDebounce", publicName: "searchDebounce", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, activeDayId: { classPropertyName: "activeDayId", publicName: "activeDayId", isSignal: true, isRequired: false, transformFunction: null }, page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: false, transformFunction: null }, searchTerm: { classPropertyName: "searchTerm", publicName: "searchTerm", isSignal: true, isRequired: false, transformFunction: null }, noteOpen: { classPropertyName: "noteOpen", publicName: "noteOpen", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeDayId: "activeDayIdChange", page: "pageChange", searchTerm: "searchTermChange", noteOpen: "noteOpenChange", zoom: "zoomChange", selectedIds: "selectedIdsChange", dayChange: "dayChange", slideChange: "slideChange", tool: "tool", fullscreenChange: "fullscreenChange", searchChange: "searchChange", fullImageRequest: "fullImageRequest", slideMove: "slideMove", retry: "retry" }, host: { listeners: { "document:fullscreenchange": "onDocumentFullscreenChange()" }, properties: { "style.--pecb-slide-aspect-ratio": "stageAspectRatio()" }, classAttribute: "pecb-slide-viewer-host" }, queries: [{ propertyName: "thumbnailTemplate", first: true, predicate: SlideViewerThumbnailDirective, descendants: true, isSignal: true }, { propertyName: "dayTemplate", first: true, predicate: SlideViewerDayDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "rootRef", first: true, predicate: ["root"], descendants: true, isSignal: true }, { propertyName: "stageRef", first: true, predicate: ["stage"], descendants: true, isSignal: true }, { propertyName: "thumbsRef", first: true, predicate: ["thumbs"], descendants: true, isSignal: true }, { propertyName: "searchRef", first: true, predicate: ["search"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #root\n [class]=\"hostClasses()\"\n role=\"region\"\n [attr.aria-label]=\"text().viewer\"\n aria-roledescription=\"slide viewer\"\n tabindex=\"0\"\n (keydown)=\"onRootKeydown($event)\"\n>\n <!-- \u2500\u2500 Top bar: day pills + tools (mock `.sl-top`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showDays() || tools().length > 0) {\n <div class=\"pecb-slide-viewer__top\">\n @if (showDays()) {\n <div class=\"pecb-slide-viewer__days\" role=\"tablist\" [attr.aria-label]=\"text().days\">\n @for (day of days(); track day.id) {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__day\"\n role=\"tab\"\n [class.pecb-slide-viewer__day--active]=\"day.id === activeDayId()\"\n [attr.aria-selected]=\"day.id === activeDayId()\"\n [attr.tabindex]=\"day.id === activeDayId() ? 0 : -1\"\n (click)=\"selectDay(day)\"\n >\n @if (dayTemplate(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl.templateRef; context: dayContext(day)\" />\n } @else {\n {{ day.label }}\n }\n </button>\n }\n </div>\n }\n\n <div class=\"pecb-slide-viewer__tools\" role=\"toolbar\">\n <!-- Product-specific tools placed before the built-in ones (e.g. a notes menu) -->\n <ng-content select=\"[pecbSlideViewerToolsStart]\"></ng-content>\n @for (t of tools(); track t) {\n @switch (t) {\n @case ('notes') {\n <button type=\"button\" class=\"pecb-slide-viewer__tool\" (click)=\"pressTool('notes')\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M15 6 10.5 1.5h-6A1.5 1.5 0 0 0 3 3v12a1.5 1.5 0 0 0 1.5 1.5h9A1.5 1.5 0 0 0 15 15V6ZM10.5 1.5V6H15M12 9.75H6M12 12.75H6M7.5 6.75H6\" />\n </svg>\n <span>{{ text().notes }}</span>\n </button>\n }\n @case ('feedback') {\n <button type=\"button\" class=\"pecb-slide-viewer__tool\" (click)=\"pressTool('feedback')\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M15.75 8.625a6.6 6.6 0 0 1-.675 2.85 6.4 6.4 0 0 1-5.7 3.525 6.6 6.6 0 0 1-2.85-.675L2.25 15.75l1.425-4.275A6.6 6.6 0 0 1 3 8.625a6.4 6.4 0 0 1 3.525-5.7 6.6 6.6 0 0 1 2.85-.675h.375a6.4 6.4 0 0 1 6 6v.375Z\" />\n </svg>\n <span>{{ text().feedback }}</span>\n </button>\n }\n @case ('courseFiles') {\n <button type=\"button\" class=\"pecb-slide-viewer__tool\" (click)=\"pressTool('courseFiles')\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M15 9.375V5.1c0-1.26 0-1.89-.245-2.371a2.25 2.25 0 0 0-.984-.984C13.29 1.5 12.66 1.5 11.4 1.5H6.6c-1.26 0-1.89 0-2.371.245a2.25 2.25 0 0 0-.984.984C3 3.21 3 3.84 3 5.1v7.8c0 1.26 0 1.89.245 2.371a2.25 2.25 0 0 0 .984.984c.481.245 1.111.245 2.371.245h2.775M10.5 8.25H6M7.5 11.25H6M12 5.25H6M15.75 14.25 13.5 16.5l-2.25-2.25M13.5 16.5V12\" />\n </svg>\n <span>{{ text().courseFiles }}</span>\n </button>\n }\n @case ('downloadSlides') {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__tool\"\n [disabled]=\"!canDownloadSlides()\"\n [attr.title]=\"canDownloadSlides() ? null : text().downloadUnavailable\"\n [attr.aria-disabled]=\"canDownloadSlides() ? null : 'true'\"\n (click)=\"pressTool('downloadSlides')\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"m7.2 12.15-1.35 3.6M12.446 15.7l-1.591-3.5M7.2 7.7l1.76 1.75 1.84-1.832M8.96 9.45v-4.5M3.6 12.15a1.8 1.8 0 0 1-1.8-1.8v-6.3a1.8 1.8 0 0 1 1.8-1.8h10.8a1.8 1.8 0 0 1 1.8 1.8v6.3a1.8 1.8 0 0 1-1.8 1.8H3.6Z\" />\n </svg>\n <span>{{ text().downloadSlides }}</span>\n </button>\n }\n }\n }\n <!-- Product-specific tools (e.g. an LTI \"Finish\" button) -->\n <ng-content select=\"[pecbSlideViewerTools]\"></ng-content>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Main: stage column + sidebar (mock `.sl-main`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-slide-viewer__main\">\n <div class=\"pecb-slide-viewer__stagecol\">\n <!-- Stage -->\n <div\n #stage\n class=\"pecb-slide-viewer__stage\"\n [class.pecb-slide-viewer__stage--fallback]=\"stageIsFallback()\"\n [class.pecb-slide-viewer__stage--skeleton]=\"stageSkeleton()\"\n [attr.aria-busy]=\"stageVeil() ? 'true' : null\"\n (wheel)=\"onStageWheel($event)\"\n (pointerdown)=\"onStagePointerDown($event)\"\n (pointerup)=\"onStagePointerUp($event)\"\n (pointercancel)=\"onStagePointerCancel()\"\n >\n <div class=\"pecb-slide-viewer__viewport\">\n @if (error() || isEmpty()) {\n <!-- Nothing to show: explain why (host can replace the message through the slot). -->\n <div class=\"pecb-slide-viewer__message\" [class.pecb-slide-viewer__message--error]=\"error()\" role=\"status\">\n <ng-content select=\"[pecbSlideViewerEmpty]\"></ng-content>\n <div class=\"pecb-slide-viewer__message-default\">\n @if (error()) {\n <svg width=\"40\" height=\"40\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /><path d=\"m9.5 12.5 5 5M14.5 12.5l-5 5\" />\n </svg>\n <strong>{{ text().errorTitle }}</strong>\n <span>{{ text().errorHint }}</span>\n <button type=\"button\" class=\"pecb-slide-viewer__retry\" (click)=\"requestRetry()\">{{ text().retry }}</button>\n } @else {\n <svg width=\"40\" height=\"40\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\" /><path d=\"M12 16v4M8 20h8M7 12l3-3 2 2 3-3 2 2\" />\n </svg>\n <strong>{{ text().emptyTitle }}</strong>\n <span>{{ text().emptyHint }}</span>\n }\n </div>\n </div>\n } @else if (displayedSrc(); as src) {\n <img\n class=\"pecb-slide-viewer__image\"\n [src]=\"src\"\n [alt]=\"slideTitle(currentSlide(), currentPage())\"\n [style.width.%]=\"zoomPercent()\"\n [style.height.%]=\"zoomPercent()\"\n decoding=\"sync\"\n draggable=\"false\"\n (load)=\"onStageImageLoad()\"\n (error)=\"onStageImageError()\"\n />\n }\n </div>\n\n @if (stageBusy()) {\n <!-- Next image decoding: a thin activity line, the current slide stays put. -->\n <div class=\"pecb-slide-viewer__progress\" aria-hidden=\"true\"></div>\n }\n\n <!-- Host overlay: annotations, laser pointer, badges\u2026 -->\n <div class=\"pecb-slide-viewer__overlay\">\n <ng-content select=\"[pecbSlideViewerStage]\"></ng-content>\n </div>\n\n @if (total() > 0) {\n <span class=\"pecb-slide-viewer__stage-n\" aria-hidden=\"true\">{{ currentPage() }}</span>\n }\n <span class=\"pecb-slide-viewer__sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</span>\n\n @if (stageVeil()) {\n <!-- No spinner: the shimmering stage is the loading state; the percentage, when known, sits on it. -->\n <div class=\"pecb-slide-viewer__veil\" role=\"status\">\n @if (loading() && progressText()) {\n <span class=\"pecb-slide-viewer__veil-text\">{{ progressText() }}</span>\n } @else {\n <span class=\"pecb-slide-viewer__sr-only\">{{ text().loading }}</span>\n }\n </div>\n }\n\n @if (isFullscreen() && showFullscreen()) {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__fs-exit\"\n [attr.aria-label]=\"text().exitFullscreen\"\n (click)=\"toggleFullscreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3m13-5h-3a2 2 0 0 0-2 2v3\" />\n </svg>\n </button>\n }\n </div>\n\n <!-- Navigation bar (mock `.sl-bar`) -->\n <div class=\"pecb-slide-viewer__bar\">\n <div class=\"pecb-slide-viewer__nav\">\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().first\" [disabled]=\"navDisabled() || isFirst()\" (click)=\"first()\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M5.96.835.836 5.96l5.125 5.125M11.502.835 6.377 5.96l5.125 5.125\" />\n </svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().previous\" [disabled]=\"navDisabled() || isFirst()\" (click)=\"previous()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M12.502 6.669H.835M6.669.835.835 6.669l5.834 5.833\" />\n </svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().next\" [disabled]=\"navDisabled() || isLast()\" (click)=\"next()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M.836 6.668h11.667M6.669 12.502l5.834-5.834L6.669.835\" />\n </svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().last\" [disabled]=\"navDisabled() || isLast()\" (click)=\"last()\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"m6.377 11.085 5.125-5.125L6.377.835M.836 11.085 5.961 5.96.836.835\" />\n </svg>\n </button>\n </div>\n\n @if (showSlideInput()) {\n <div class=\"pecb-slide-viewer__count\">\n <label [attr.for]=\"uid + '-page'\">{{ slideWord() }}</label>\n <input\n class=\"pecb-slide-viewer__page-input\"\n type=\"number\"\n inputmode=\"numeric\"\n [id]=\"uid + '-page'\"\n min=\"1\"\n [attr.max]=\"total() || 1\"\n [value]=\"pageInputValue()\"\n [disabled]=\"navDisabled()\"\n [attr.aria-label]=\"text().slideNumber\"\n (input)=\"onPageInput($event)\"\n (change)=\"onPageCommit($event)\"\n (keydown)=\"onPageInputKeydown($event)\"\n />\n <span aria-hidden=\"true\">{{ text().of }}{{ total() }}</span>\n </div>\n }\n\n @if (zoomable()) {\n <div class=\"pecb-slide-viewer__zoom\" role=\"group\">\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().zoomOut\" [disabled]=\"navDisabled() || !isZoomed()\" (click)=\"zoomOut()\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\" /></svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__zoom-value\" [attr.aria-label]=\"text().zoomReset\" [disabled]=\"navDisabled() || !isZoomed()\" (click)=\"zoomReset()\">\n {{ zoomPercent() }}%\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().zoomIn\" [disabled]=\"navDisabled()\" (click)=\"zoomIn()\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M12 5v14M5 12h14\" /></svg>\n </button>\n </div>\n }\n\n <div class=\"pecb-slide-viewer__right\">\n <!-- Version select or any other control the product wants here -->\n <ng-content select=\"[pecbSlideViewerVersion]\"></ng-content>\n\n @if (showFullscreen()) {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__nb\"\n [attr.aria-label]=\"isFullscreen() ? text().exitFullscreen : text().enterFullscreen\"\n [attr.aria-pressed]=\"isFullscreen()\"\n [disabled]=\"navDisabled()\"\n (click)=\"toggleFullscreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3\" />\n </svg>\n </button>\n }\n </div>\n </div>\n\n <!-- Note (mock `.sl-note`) -->\n @if (hasNote()) {\n <div class=\"pecb-slide-viewer__note\" [class.pecb-slide-viewer__note--open]=\"noteOpen()\">\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__note-h\"\n [attr.aria-expanded]=\"noteOpen()\"\n [attr.aria-controls]=\"uid + '-note'\"\n (click)=\"toggleNote()\"\n >\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M14 11H8M10 15H8M16 7H8M20 6.8v10.4c0 1.68 0 2.52-.327 3.162a3 3 0 0 1-1.311 1.311C17.72 22 16.88 22 15.2 22H8.8c-1.68 0-2.52 0-3.162-.327a3 3 0 0 1-1.311-1.311C4 19.72 4 18.88 4 17.2V6.8c0-1.68 0-2.52.327-3.162a3 3 0 0 1 1.311-1.311C6.28 2 7.12 2 8.8 2h6.4c1.68 0 2.52 0 3.162.327a3 3 0 0 1 1.311 1.311C20 4.28 20 5.12 20 6.8Z\" />\n </svg>\n <span>{{ text().note }}</span>\n <i class=\"pecb-slide-viewer__chev\" aria-hidden=\"true\">\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </i>\n </button>\n <div class=\"pecb-slide-viewer__note-b\" [id]=\"uid + '-note'\" [attr.hidden]=\"noteOpen() ? null : ''\">\n @if (note(); as n) {\n <div [innerHTML]=\"n\"></div>\n }\n <!-- Host-rendered note body (an editor, actions\u2026) -->\n <ng-content select=\"[pecbSlideViewerNote]\"></ng-content>\n </div>\n </div>\n }\n\n <!-- Anything the product wants under the stage column (e.g. an LTI table) -->\n <ng-content select=\"[pecbSlideViewerBelow]\"></ng-content>\n </div>\n\n <!-- \u2500\u2500 Sidebar: search + thumbnails (mock `.sl-side`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (sidebar() !== 'none') {\n <aside class=\"pecb-slide-viewer__side\" [attr.aria-label]=\"text().thumbnails\">\n <!-- Host header: a title, \"Add slide\"\u2026 -->\n <ng-content select=\"[pecbSlideViewerSidebarHeader]\"></ng-content>\n\n @if (showSearch()) {\n <div class=\"pecb-slide-viewer__search\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.3-4.3\" />\n </svg>\n <input\n #search\n type=\"search\"\n [attr.placeholder]=\"text().searchPlaceholder\"\n [attr.aria-label]=\"text().searchPlaceholder\"\n [attr.aria-controls]=\"uid + '-thumbs'\"\n [value]=\"searchTerm()\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n />\n </div>\n @if (isSearching()) {\n <p class=\"pecb-slide-viewer__results\" aria-live=\"polite\">{{ searchSummary() }}</p>\n }\n }\n\n <div #thumbs class=\"pecb-slide-viewer__thumbs\" [id]=\"uid + '-thumbs'\">\n @if (error() || isEmpty()) {\n <p class=\"pecb-slide-viewer__empty\">{{ error() ? text().errorTitle : text().emptyTitle }}</p>\n } @else if (loading() && total() === 0) {\n @for (row of skeletonRows; track row) {\n <div class=\"pecb-slide-viewer__thumb pecb-slide-viewer__thumb--skeleton\" aria-hidden=\"true\">\n <div class=\"pecb-slide-viewer__thumb-img\"></div>\n <div class=\"pecb-slide-viewer__thumb-n\"></div>\n </div>\n }\n } @else {\n <!-- Spacers are always present: removing one mid-render would let the browser clamp the scroll position. -->\n <div class=\"pecb-slide-viewer__spacer\" [style.height.px]=\"spacerTop()\" aria-hidden=\"true\"></div>\n @for (n of visibleTiles(); track n) {\n @let slide = slides()[n - 1];\n @if (slide) {\n <div\n class=\"pecb-slide-viewer__thumb\"\n [class.pecb-slide-viewer__thumb--pending]=\"tilePending(slide)\"\n role=\"button\"\n tabindex=\"0\"\n [attr.data-n]=\"n\"\n [class.pecb-slide-viewer__thumb--active]=\"n === currentPage()\"\n [class.pecb-slide-viewer__thumb--hidden]=\"slide.hidden\"\n [class.pecb-slide-viewer__thumb--selected]=\"selectable() && isSelected(slide, n)\"\n [class.pecb-slide-viewer__thumb--dragging]=\"dragFrom() === n\"\n [class.pecb-slide-viewer__thumb--drop-target]=\"dragOver() === n && dragFrom() !== n\"\n [attr.aria-current]=\"n === currentPage() ? 'true' : null\"\n [attr.aria-label]=\"slideTitle(slide, n)\"\n [attr.aria-description]=\"reorderable() ? text().dragHint : null\"\n [attr.draggable]=\"reorderable() ? 'true' : null\"\n (click)=\"onThumbnailClick(n)\"\n (keydown.enter)=\"onThumbnailClick(n)\"\n (keydown.space)=\"onThumbnailClick(n); $event.preventDefault()\"\n (dragstart)=\"onTileDragStart($event, n)\"\n (dragover)=\"onTileDragOver($event, n)\"\n (drop)=\"onTileDrop($event, n)\"\n (dragend)=\"onTileDragEnd()\"\n >\n <div class=\"pecb-slide-viewer__thumb-img\">\n @if (thumbnailSrc(slide); as src) {\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" decoding=\"async\" draggable=\"false\" />\n } @else if (slide.title) {\n <span>{{ slide.title }}</span>\n }\n\n @if (selectable()) {\n <label class=\"pecb-slide-viewer__check\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"isSelected(slide, n)\"\n [attr.aria-label]=\"selectLabel(n)\"\n (change)=\"onTileCheckbox($event, slide, n)\"\n />\n <span class=\"pecb-slide-viewer__check-box\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 6 9 17l-5-5\" /></svg>\n </span>\n </label>\n }\n\n @if (slide.hidden) {\n <span class=\"pecb-slide-viewer__hidden-badge\">{{ text().hidden }}</span>\n }\n\n @if (reorderable()) {\n <span class=\"pecb-slide-viewer__grip\" aria-hidden=\"true\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"9\" cy=\"6\" r=\"1.6\" /><circle cx=\"15\" cy=\"6\" r=\"1.6\" /><circle cx=\"9\" cy=\"12\" r=\"1.6\" /><circle cx=\"15\" cy=\"12\" r=\"1.6\" /><circle cx=\"9\" cy=\"18\" r=\"1.6\" /><circle cx=\"15\" cy=\"18\" r=\"1.6\" /></svg>\n </span>\n }\n\n @if (thumbnailTemplate(); as tpl) {\n <div class=\"pecb-slide-viewer__thumb-extra\">\n <ng-container *ngTemplateOutlet=\"tpl.templateRef; context: thumbnailContext(slide, n)\" />\n </div>\n }\n </div>\n <div class=\"pecb-slide-viewer__thumb-n\">{{ n }}</div>\n </div>\n }\n }\n <div class=\"pecb-slide-viewer__spacer\" [style.height.px]=\"spacerBottom()\" aria-hidden=\"true\"></div>\n @if (isSearching() && matches().length === 0) {\n <p class=\"pecb-slide-viewer__empty\">{{ text().noResults }}</p>\n }\n }\n </div>\n\n <!-- Host footer: comments, bulk actions\u2026 -->\n <ng-content select=\"[pecbSlideViewerSidebarFooter]\"></ng-content>\n </aside>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;max-width:100%;container-type:inline-size;container-name:pecb-slide-viewer;--pecb-slide-radius: 12px;--pecb-slide-gap: 20px;--pecb-slide-sidebar-width: 340px;--pecb-slide-sidebar-top: 86px;--pecb-slide-thumbs-max-height: 620px;--pecb-slide-note-max-height: 210px;--pecb-slide-active: #212427;--pecb-slide-border: #ececec;--pecb-slide-stage-bg: #f4f4f4;--pecb-slide-fullscreen-bg: #0b0b0d;--pecb-slide-thumb-radius: 8px;--pecb-slide-number-bg: var(--pecb-slide-active);--pecb-slide-font: Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif}.pecb-slide-viewer{display:flex;flex-direction:column;gap:16px;font-family:var(--pecb-slide-font);color:#212427;outline:none}.pecb-slide-viewer:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #212427;border-radius:var(--pecb-slide-radius)}.pecb-slide-viewer__top{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;background:#fff;border:1px solid var(--pecb-slide-border);border-radius:var(--pecb-slide-radius);padding:12px 14px}.pecb-slide-viewer__days{display:flex;gap:8px;flex-wrap:wrap}.pecb-slide-viewer__day{border:none;background:none;color:inherit;height:38px;padding:0 18px;border:1px solid var(--pecb-slide-border);background:#fff;border-radius:8px;font:inherit;font-size:14px;font-weight:600;color:#616161;cursor:pointer;transition:background .15s ease-in-out,color .15s ease-in-out,border-color .15s ease-in-out}.pecb-slide-viewer__day:focus{outline:none}.pecb-slide-viewer__day:hover{background:#f4f4f4}.pecb-slide-viewer__day:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__day--active{background:var(--pecb-slide-active);border-color:var(--pecb-slide-active);color:#fff}.pecb-slide-viewer__day--active:hover{background:var(--pecb-slide-active)}.pecb-slide-viewer__tools{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.pecb-slide-viewer__tool{border:none;background:none;color:inherit;display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 12px;border-radius:8px;font:inherit;font-size:13.5px;font-weight:600;color:#212427;cursor:pointer;white-space:nowrap;transition:background .15s ease-in-out}.pecb-slide-viewer__tool:focus{outline:none}.pecb-slide-viewer__tool svg{flex:none;color:#616161}.pecb-slide-viewer__tool:hover:not(:disabled){background:#f4f4f4}.pecb-slide-viewer__tool:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__tool:disabled{color:#c2c2c2;cursor:not-allowed}.pecb-slide-viewer__tool:disabled svg{color:#c2c2c2}.pecb-slide-viewer__main{display:grid;grid-template-columns:minmax(0,1fr) var(--pecb-slide-sidebar-width);gap:var(--pecb-slide-gap);align-items:start}.pecb-slide-viewer--no-sidebar .pecb-slide-viewer__main{grid-template-columns:minmax(0,1fr)}.pecb-slide-viewer--sidebar-start .pecb-slide-viewer__main{grid-template-columns:var(--pecb-slide-sidebar-width) minmax(0,1fr)}.pecb-slide-viewer--sidebar-start .pecb-slide-viewer__stagecol{order:2}.pecb-slide-viewer--sidebar-start .pecb-slide-viewer__side{order:1}.pecb-slide-viewer__stagecol{min-width:0;display:flex;flex-direction:column;gap:14px}.pecb-slide-viewer__stage{position:relative;width:100%;aspect-ratio:var(--pecb-slide-aspect-ratio, 16/10);background:var(--pecb-slide-stage-bg);border-radius:var(--pecb-slide-radius);overflow:hidden;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:pan-y pinch-zoom}.pecb-slide-viewer__stage:fullscreen{border-radius:0;background:var(--pecb-slide-fullscreen-bg);aspect-ratio:auto;width:100vw;height:100vh}.pecb-slide-viewer__stage--skeleton{background:#ececec}.pecb-slide-viewer__stage--skeleton:before{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.7) 45%,rgba(255,255,255,.7) 55%,transparent);animation:pecb-slide-shimmer 1.6s ease-in-out infinite}.pecb-slide-viewer__viewport{position:absolute;inset:0;overflow:hidden}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport{overflow:auto;cursor:grab}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar{width:8px;height:8px}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar-track{background:transparent}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar-thumb{background:#c2c2c2;border-radius:12px}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar-thumb:hover{background:#a9a9a9}.pecb-slide-viewer__image{display:block;width:100%;height:100%;object-fit:contain;background:var(--pecb-slide-stage-bg)}.pecb-slide-viewer__stage:fullscreen .pecb-slide-viewer__image{background:var(--pecb-slide-fullscreen-bg)}.pecb-slide-viewer__stage--fallback .pecb-slide-viewer__image{image-rendering:auto;filter:saturate(.98)}.pecb-slide-viewer__overlay{position:absolute;inset:0;pointer-events:none}.pecb-slide-viewer__overlay>*{pointer-events:auto}.pecb-slide-viewer__overlay:empty{display:none}.pecb-slide-viewer__message{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;text-align:center;color:#616161}.pecb-slide-viewer__message:has(>:not(.pecb-slide-viewer__message-default)) .pecb-slide-viewer__message-default{display:none}.pecb-slide-viewer__message--error{color:#313131}.pecb-slide-viewer__message--error svg{color:#a11e29}.pecb-slide-viewer__message-default{display:flex;flex-direction:column;align-items:center;gap:6px;max-width:360px}.pecb-slide-viewer__message-default svg{color:#919191;margin-bottom:6px}.pecb-slide-viewer__message-default strong{font-size:16px;font-weight:600;color:#212427}.pecb-slide-viewer__message-default span{font-size:14px;line-height:1.5}.pecb-slide-viewer__retry{border:none;background:none;color:inherit;margin-top:10px;height:38px;padding:0 18px;border:1px solid #c2c2c2;border-radius:8px;background:#fff;font:inherit;font-size:14px;font-weight:600;color:#212427;cursor:pointer;transition:background .15s ease-in-out}.pecb-slide-viewer__retry:focus{outline:none}.pecb-slide-viewer__retry:hover{background:#f4f4f4}.pecb-slide-viewer__retry:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__stage-n{position:absolute;left:16px;bottom:16px;font-size:13px;font-weight:600;color:#ffffffd9;text-shadow:0 1px 2px rgba(0,0,0,.35);pointer-events:none}.pecb-slide-viewer__progress{position:absolute;left:0;right:0;top:0;height:3px;overflow:hidden;pointer-events:none}.pecb-slide-viewer__progress:after{content:\"\";position:absolute;inset:0;width:40%;background:var(--pecb-slide-active);opacity:.7;border-radius:999px;animation:pecb-slide-progress 1s ease-in-out infinite}.pecb-slide-viewer__veil{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none}.pecb-slide-viewer__veil-text{padding:6px 12px;border-radius:999px;background:#ffffffd9;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;color:#616161}.pecb-slide-viewer__fs-exit{padding:0;border:none;background:none;font:inherit;color:inherit;position:absolute;top:16px;right:16px;width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;background:#1414168c;color:#fff;cursor:pointer}.pecb-slide-viewer__fs-exit:focus{outline:none}.pecb-slide-viewer__fs-exit:hover{background:#141416bf}.pecb-slide-viewer__fs-exit:focus-visible{outline:2px solid #ffffff;outline-offset:2px}.pecb-slide-viewer__bar{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.pecb-slide-viewer__nav{display:flex;gap:8px}.pecb-slide-viewer__nb{padding:0;border:none;background:none;font:inherit;color:inherit;width:44px;height:44px;display:inline-flex;align-items:center;justify-content:center;border:1px solid #c2c2c2;background:#fff;border-radius:8px;color:#212427;cursor:pointer;transition:transform .1s,background .12s}.pecb-slide-viewer__nb:focus{outline:none}.pecb-slide-viewer__nb svg{display:block}.pecb-slide-viewer__nb:hover:not(:disabled){background:#f4f4f4}.pecb-slide-viewer__nb:active:not(:disabled){transform:scale(.92);background:#ececec}.pecb-slide-viewer__nb:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__nb:disabled{color:#c2c2c2;cursor:not-allowed}.pecb-slide-viewer__count{display:flex;align-items:center;gap:10px;font-size:14px;color:#616161}.pecb-slide-viewer__count label{cursor:default}.pecb-slide-viewer__page-input{width:70px;height:44px;border:1px solid var(--pecb-slide-border);border-radius:8px;font:inherit;font-size:14px;font-variant-numeric:tabular-nums;text-align:center;color:#212427;background:#fff;-moz-appearance:textfield;appearance:textfield}.pecb-slide-viewer__page-input::-webkit-outer-spin-button,.pecb-slide-viewer__page-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.pecb-slide-viewer__page-input:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:1px}.pecb-slide-viewer__page-input:disabled{color:#c2c2c2;background:#fafafa}.pecb-slide-viewer__zoom{display:flex;align-items:center;gap:6px}.pecb-slide-viewer__zoom-value{border:none;background:none;color:inherit;min-width:56px;height:44px;padding:0 8px;border:1px solid var(--pecb-slide-border);border-radius:8px;background:#fff;font:inherit;font-size:13px;font-variant-numeric:tabular-nums;color:#616161;cursor:pointer}.pecb-slide-viewer__zoom-value:focus{outline:none}.pecb-slide-viewer__zoom-value:disabled{cursor:default;color:#919191}.pecb-slide-viewer__zoom-value:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__right{display:flex;align-items:center;gap:10px;margin-left:auto;min-width:0}.pecb-slide-viewer__right>:not(.pecb-slide-viewer__nb){min-width:220px}.pecb-slide-viewer__note{background:#fff;border:1px solid var(--pecb-slide-border);border-radius:var(--pecb-slide-radius);overflow:hidden}.pecb-slide-viewer__note-h{border:none;background:none;color:inherit;width:100%;display:flex;align-items:center;gap:10px;padding:18px 20px;font:inherit;font-size:15px;font-weight:600;color:#212427;cursor:pointer;text-align:left}.pecb-slide-viewer__note-h:focus{outline:none}.pecb-slide-viewer__note-h svg{flex:none;color:#616161}.pecb-slide-viewer__note-h:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:-2px;border-radius:var(--pecb-slide-radius)}.pecb-slide-viewer__chev{margin-left:auto;display:inline-flex;transition:transform .18s}.pecb-slide-viewer__note--open .pecb-slide-viewer__chev{transform:rotate(180deg)}.pecb-slide-viewer__note-b{padding:0 20px 20px;font-size:13.5px;line-height:1.6;color:#616161;max-height:var(--pecb-slide-note-max-height);overflow-y:auto}.pecb-slide-viewer__note-b::-webkit-scrollbar{width:8px;height:8px}.pecb-slide-viewer__note-b::-webkit-scrollbar-track{background:#f4f4f4}.pecb-slide-viewer__note-b::-webkit-scrollbar-thumb{background:#c2c2c2;border-radius:12px}.pecb-slide-viewer__note-b::-webkit-scrollbar-thumb:hover{background:#a9a9a9}.pecb-slide-viewer__note-b[hidden]{display:none}.pecb-slide-viewer__note-b ::ng-deep p{margin:0 0 12px}.pecb-slide-viewer__note-b ::ng-deep p:last-child{margin:0}.pecb-slide-viewer__note-b ::ng-deep ul,.pecb-slide-viewer__note-b ::ng-deep ol{padding-left:20px;margin:0 0 12px}.pecb-slide-viewer__note-b>[pecbSlideViewerNote]{display:block}.pecb-slide-viewer__side{position:sticky;top:var(--pecb-slide-sidebar-top);min-width:0;background:#fff;border:1px solid var(--pecb-slide-border);border-radius:var(--pecb-slide-radius);padding:14px;display:flex;flex-direction:column;gap:14px}.pecb-slide-viewer__search{display:flex;align-items:center;gap:10px;height:46px;border:1px solid var(--pecb-slide-border);border-radius:8px;padding:0 12px;background:#fff}.pecb-slide-viewer__search svg{flex:none;color:#c2c2c2}.pecb-slide-viewer__search input{flex:1 1 auto;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:14px;color:#212427}.pecb-slide-viewer__search input::placeholder{color:#919191}.pecb-slide-viewer__search input::-webkit-search-cancel-button{-webkit-appearance:none}.pecb-slide-viewer__search:focus-within{border-color:#c2c2c2;box-shadow:0 0 0 3px #21242714}.pecb-slide-viewer__results{margin:-6px 0 0;font-size:12px;font-weight:600;color:#919191}.pecb-slide-viewer__thumbs{display:flex;flex-direction:column;gap:12px;max-height:var(--pecb-slide-thumbs-max-height);overflow-y:auto;overflow-anchor:none;padding-right:6px;scroll-behavior:smooth}.pecb-slide-viewer__thumbs::-webkit-scrollbar{width:8px;height:8px}.pecb-slide-viewer__thumbs::-webkit-scrollbar-track{background:#f4f4f4}.pecb-slide-viewer__thumbs::-webkit-scrollbar-thumb{background:#c2c2c2;border-radius:12px}.pecb-slide-viewer__thumbs::-webkit-scrollbar-thumb:hover{background:#a9a9a9}.pecb-slide-viewer__thumb{display:block;width:100%;flex:none;border-radius:var(--pecb-slide-thumb-radius);overflow:hidden;border:2px solid transparent;background:#fff;cursor:pointer;text-align:left;-webkit-user-select:none;user-select:none;transition:border-color .15s ease-in-out,opacity .15s ease-in-out}.pecb-slide-viewer__thumb:hover{border-color:#c2c2c2}.pecb-slide-viewer__thumb:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__thumb--active{border-color:var(--pecb-slide-active)}.pecb-slide-viewer__thumb--active .pecb-slide-viewer__thumb-n{background:var(--pecb-slide-number-bg);color:#fff}.pecb-slide-viewer__thumb--selected{border-color:var(--pecb-slide-active)}.pecb-slide-viewer__thumb--hidden{opacity:.55}.pecb-slide-viewer__thumb--dragging{opacity:.4}.pecb-slide-viewer__thumb--drop-target{border-color:var(--pecb-slide-active);border-style:dashed}.pecb-slide-viewer--reorderable .pecb-slide-viewer__thumb{cursor:grab}.pecb-slide-viewer__thumb-img{position:relative;aspect-ratio:16/9;display:flex;align-items:center;background:var(--pecb-slide-stage-bg);overflow:hidden}.pecb-slide-viewer__thumb-img img{display:block;width:100%;height:100%;object-fit:cover}.pecb-slide-viewer__thumb-img span{padding:0 14px;font-size:14px;font-weight:600;color:#616161}.pecb-slide-viewer__thumb-n{text-align:center;font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums;color:#616161;padding:6px 0}.pecb-slide-viewer__check{position:absolute;top:8px;left:8px;margin:0;cursor:pointer}.pecb-slide-viewer__check input{position:absolute;opacity:0;width:1px;height:1px}.pecb-slide-viewer__check input:focus-visible+.pecb-slide-viewer__check-box{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__check input:checked+.pecb-slide-viewer__check-box{background:var(--pecb-slide-active);border-color:var(--pecb-slide-active);color:#fff}.pecb-slide-viewer__check-box{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:5px;border:1.5px solid #c2c2c2;background:#ffffffeb;color:transparent;transition:background .15s ease-in-out,border-color .15s ease-in-out}.pecb-slide-viewer__hidden-badge{position:absolute;top:8px;right:8px;padding:2px 7px;border-radius:999px;background:#212427bf;color:#fff;font-size:11px;font-weight:600;letter-spacing:.02em}.pecb-slide-viewer__grip{position:absolute;right:6px;bottom:6px;display:inline-flex;padding:3px;border-radius:4px;background:#ffffffd9;color:#616161}.pecb-slide-viewer__thumb-extra{position:absolute;inset:0;pointer-events:none}.pecb-slide-viewer__thumb-extra>*{pointer-events:auto}.pecb-slide-viewer__spacer{flex:none;width:100%}.pecb-slide-viewer__spacer[style*=\"height: 0px\"]{display:none}.pecb-slide-viewer__thumb--pending .pecb-slide-viewer__thumb-img{background:#ececec}.pecb-slide-viewer__thumb--pending .pecb-slide-viewer__thumb-img:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.7) 50%,transparent);animation:pecb-slide-shimmer 1.6s ease-in-out infinite}.pecb-slide-viewer__thumb--skeleton{pointer-events:none}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-img,.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n{position:relative;overflow:hidden;background:#ececec}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n{height:28px;margin-top:4px;border-radius:6px}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-img:after,.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.7) 50%,transparent);animation:pecb-slide-shimmer 1.6s ease-in-out infinite}.pecb-slide-viewer__empty{margin:0;padding:12px;font-size:13.5px;color:#919191}.pecb-slide-viewer__sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}@container pecb-slide-viewer (max-width: 1080px){.pecb-slide-viewer__main{grid-template-columns:minmax(0,1fr)}.pecb-slide-viewer__side{position:static}}@container pecb-slide-viewer (max-width: 640px){.pecb-slide-viewer__top{padding:10px 12px}.pecb-slide-viewer__tool span{display:none}.pecb-slide-viewer__tool{padding:0 10px}.pecb-slide-viewer__nb{width:40px;height:40px}.pecb-slide-viewer__page-input{height:40px;width:60px}.pecb-slide-viewer__right{margin-left:0;flex:1 1 100%}.pecb-slide-viewer__thumbs{max-height:360px}}@keyframes pecb-slide-progress{0%{transform:translate(-100%)}to{transform:translate(250%)}}@keyframes pecb-slide-shimmer{0%{transform:translate(-100%)}to{transform:translate(100%)}}@media (prefers-reduced-motion: reduce){.pecb-slide-viewer__day,.pecb-slide-viewer__tool,.pecb-slide-viewer__nb,.pecb-slide-viewer__chev,.pecb-slide-viewer__thumb{transition:none}.pecb-slide-viewer__thumbs{scroll-behavior:auto}.pecb-slide-viewer__progress:after{animation:none;width:100%}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-img:after,.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n:after,.pecb-slide-viewer__thumb--pending .pecb-slide-viewer__thumb-img:after,.pecb-slide-viewer__stage--skeleton:before{animation:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16139
+ }
16140
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: SlideViewerComponent, decorators: [{
16141
+ type: Component,
16142
+ args: [{ selector: 'pecb-slide-viewer', standalone: true, imports: [NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
16143
+ class: 'pecb-slide-viewer-host',
16144
+ '[style.--pecb-slide-aspect-ratio]': 'stageAspectRatio()',
16145
+ '(document:fullscreenchange)': 'onDocumentFullscreenChange()',
16146
+ }, template: "<div\n #root\n [class]=\"hostClasses()\"\n role=\"region\"\n [attr.aria-label]=\"text().viewer\"\n aria-roledescription=\"slide viewer\"\n tabindex=\"0\"\n (keydown)=\"onRootKeydown($event)\"\n>\n <!-- \u2500\u2500 Top bar: day pills + tools (mock `.sl-top`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showDays() || tools().length > 0) {\n <div class=\"pecb-slide-viewer__top\">\n @if (showDays()) {\n <div class=\"pecb-slide-viewer__days\" role=\"tablist\" [attr.aria-label]=\"text().days\">\n @for (day of days(); track day.id) {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__day\"\n role=\"tab\"\n [class.pecb-slide-viewer__day--active]=\"day.id === activeDayId()\"\n [attr.aria-selected]=\"day.id === activeDayId()\"\n [attr.tabindex]=\"day.id === activeDayId() ? 0 : -1\"\n (click)=\"selectDay(day)\"\n >\n @if (dayTemplate(); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl.templateRef; context: dayContext(day)\" />\n } @else {\n {{ day.label }}\n }\n </button>\n }\n </div>\n }\n\n <div class=\"pecb-slide-viewer__tools\" role=\"toolbar\">\n <!-- Product-specific tools placed before the built-in ones (e.g. a notes menu) -->\n <ng-content select=\"[pecbSlideViewerToolsStart]\"></ng-content>\n @for (t of tools(); track t) {\n @switch (t) {\n @case ('notes') {\n <button type=\"button\" class=\"pecb-slide-viewer__tool\" (click)=\"pressTool('notes')\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M15 6 10.5 1.5h-6A1.5 1.5 0 0 0 3 3v12a1.5 1.5 0 0 0 1.5 1.5h9A1.5 1.5 0 0 0 15 15V6ZM10.5 1.5V6H15M12 9.75H6M12 12.75H6M7.5 6.75H6\" />\n </svg>\n <span>{{ text().notes }}</span>\n </button>\n }\n @case ('feedback') {\n <button type=\"button\" class=\"pecb-slide-viewer__tool\" (click)=\"pressTool('feedback')\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M15.75 8.625a6.6 6.6 0 0 1-.675 2.85 6.4 6.4 0 0 1-5.7 3.525 6.6 6.6 0 0 1-2.85-.675L2.25 15.75l1.425-4.275A6.6 6.6 0 0 1 3 8.625a6.4 6.4 0 0 1 3.525-5.7 6.6 6.6 0 0 1 2.85-.675h.375a6.4 6.4 0 0 1 6 6v.375Z\" />\n </svg>\n <span>{{ text().feedback }}</span>\n </button>\n }\n @case ('courseFiles') {\n <button type=\"button\" class=\"pecb-slide-viewer__tool\" (click)=\"pressTool('courseFiles')\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M15 9.375V5.1c0-1.26 0-1.89-.245-2.371a2.25 2.25 0 0 0-.984-.984C13.29 1.5 12.66 1.5 11.4 1.5H6.6c-1.26 0-1.89 0-2.371.245a2.25 2.25 0 0 0-.984.984C3 3.21 3 3.84 3 5.1v7.8c0 1.26 0 1.89.245 2.371a2.25 2.25 0 0 0 .984.984c.481.245 1.111.245 2.371.245h2.775M10.5 8.25H6M7.5 11.25H6M12 5.25H6M15.75 14.25 13.5 16.5l-2.25-2.25M13.5 16.5V12\" />\n </svg>\n <span>{{ text().courseFiles }}</span>\n </button>\n }\n @case ('downloadSlides') {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__tool\"\n [disabled]=\"!canDownloadSlides()\"\n [attr.title]=\"canDownloadSlides() ? null : text().downloadUnavailable\"\n [attr.aria-disabled]=\"canDownloadSlides() ? null : 'true'\"\n (click)=\"pressTool('downloadSlides')\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"m7.2 12.15-1.35 3.6M12.446 15.7l-1.591-3.5M7.2 7.7l1.76 1.75 1.84-1.832M8.96 9.45v-4.5M3.6 12.15a1.8 1.8 0 0 1-1.8-1.8v-6.3a1.8 1.8 0 0 1 1.8-1.8h10.8a1.8 1.8 0 0 1 1.8 1.8v6.3a1.8 1.8 0 0 1-1.8 1.8H3.6Z\" />\n </svg>\n <span>{{ text().downloadSlides }}</span>\n </button>\n }\n }\n }\n <!-- Product-specific tools (e.g. an LTI \"Finish\" button) -->\n <ng-content select=\"[pecbSlideViewerTools]\"></ng-content>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Main: stage column + sidebar (mock `.sl-main`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"pecb-slide-viewer__main\">\n <div class=\"pecb-slide-viewer__stagecol\">\n <!-- Stage -->\n <div\n #stage\n class=\"pecb-slide-viewer__stage\"\n [class.pecb-slide-viewer__stage--fallback]=\"stageIsFallback()\"\n [class.pecb-slide-viewer__stage--skeleton]=\"stageSkeleton()\"\n [attr.aria-busy]=\"stageVeil() ? 'true' : null\"\n (wheel)=\"onStageWheel($event)\"\n (pointerdown)=\"onStagePointerDown($event)\"\n (pointerup)=\"onStagePointerUp($event)\"\n (pointercancel)=\"onStagePointerCancel()\"\n >\n <div class=\"pecb-slide-viewer__viewport\">\n @if (error() || isEmpty()) {\n <!-- Nothing to show: explain why (host can replace the message through the slot). -->\n <div class=\"pecb-slide-viewer__message\" [class.pecb-slide-viewer__message--error]=\"error()\" role=\"status\">\n <ng-content select=\"[pecbSlideViewerEmpty]\"></ng-content>\n <div class=\"pecb-slide-viewer__message-default\">\n @if (error()) {\n <svg width=\"40\" height=\"40\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" /><path d=\"M14 2v6h6\" /><path d=\"m9.5 12.5 5 5M14.5 12.5l-5 5\" />\n </svg>\n <strong>{{ text().errorTitle }}</strong>\n <span>{{ text().errorHint }}</span>\n <button type=\"button\" class=\"pecb-slide-viewer__retry\" (click)=\"requestRetry()\">{{ text().retry }}</button>\n } @else {\n <svg width=\"40\" height=\"40\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"12\" rx=\"2\" /><path d=\"M12 16v4M8 20h8M7 12l3-3 2 2 3-3 2 2\" />\n </svg>\n <strong>{{ text().emptyTitle }}</strong>\n <span>{{ text().emptyHint }}</span>\n }\n </div>\n </div>\n } @else if (displayedSrc(); as src) {\n <img\n class=\"pecb-slide-viewer__image\"\n [src]=\"src\"\n [alt]=\"slideTitle(currentSlide(), currentPage())\"\n [style.width.%]=\"zoomPercent()\"\n [style.height.%]=\"zoomPercent()\"\n decoding=\"sync\"\n draggable=\"false\"\n (load)=\"onStageImageLoad()\"\n (error)=\"onStageImageError()\"\n />\n }\n </div>\n\n @if (stageBusy()) {\n <!-- Next image decoding: a thin activity line, the current slide stays put. -->\n <div class=\"pecb-slide-viewer__progress\" aria-hidden=\"true\"></div>\n }\n\n <!-- Host overlay: annotations, laser pointer, badges\u2026 -->\n <div class=\"pecb-slide-viewer__overlay\">\n <ng-content select=\"[pecbSlideViewerStage]\"></ng-content>\n </div>\n\n @if (total() > 0) {\n <span class=\"pecb-slide-viewer__stage-n\" aria-hidden=\"true\">{{ currentPage() }}</span>\n }\n <span class=\"pecb-slide-viewer__sr-only\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</span>\n\n @if (stageVeil()) {\n <!-- No spinner: the shimmering stage is the loading state; the percentage, when known, sits on it. -->\n <div class=\"pecb-slide-viewer__veil\" role=\"status\">\n @if (loading() && progressText()) {\n <span class=\"pecb-slide-viewer__veil-text\">{{ progressText() }}</span>\n } @else {\n <span class=\"pecb-slide-viewer__sr-only\">{{ text().loading }}</span>\n }\n </div>\n }\n\n @if (isFullscreen() && showFullscreen()) {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__fs-exit\"\n [attr.aria-label]=\"text().exitFullscreen\"\n (click)=\"toggleFullscreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3M3 16h3a2 2 0 0 1 2 2v3m13-5h-3a2 2 0 0 0-2 2v3\" />\n </svg>\n </button>\n }\n </div>\n\n <!-- Navigation bar (mock `.sl-bar`) -->\n <div class=\"pecb-slide-viewer__bar\">\n <div class=\"pecb-slide-viewer__nav\">\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().first\" [disabled]=\"navDisabled() || isFirst()\" (click)=\"first()\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M5.96.835.836 5.96l5.125 5.125M11.502.835 6.377 5.96l5.125 5.125\" />\n </svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().previous\" [disabled]=\"navDisabled() || isFirst()\" (click)=\"previous()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M12.502 6.669H.835M6.669.835.835 6.669l5.834 5.833\" />\n </svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().next\" [disabled]=\"navDisabled() || isLast()\" (click)=\"next()\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M.836 6.668h11.667M6.669 12.502l5.834-5.834L6.669.835\" />\n </svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().last\" [disabled]=\"navDisabled() || isLast()\" (click)=\"last()\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.67\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"m6.377 11.085 5.125-5.125L6.377.835M.836 11.085 5.961 5.96.836.835\" />\n </svg>\n </button>\n </div>\n\n @if (showSlideInput()) {\n <div class=\"pecb-slide-viewer__count\">\n <label [attr.for]=\"uid + '-page'\">{{ slideWord() }}</label>\n <input\n class=\"pecb-slide-viewer__page-input\"\n type=\"number\"\n inputmode=\"numeric\"\n [id]=\"uid + '-page'\"\n min=\"1\"\n [attr.max]=\"total() || 1\"\n [value]=\"pageInputValue()\"\n [disabled]=\"navDisabled()\"\n [attr.aria-label]=\"text().slideNumber\"\n (input)=\"onPageInput($event)\"\n (change)=\"onPageCommit($event)\"\n (keydown)=\"onPageInputKeydown($event)\"\n />\n <span aria-hidden=\"true\">{{ text().of }}{{ total() }}</span>\n </div>\n }\n\n @if (zoomable()) {\n <div class=\"pecb-slide-viewer__zoom\" role=\"group\">\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().zoomOut\" [disabled]=\"navDisabled() || !isZoomed()\" (click)=\"zoomOut()\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\" /></svg>\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__zoom-value\" [attr.aria-label]=\"text().zoomReset\" [disabled]=\"navDisabled() || !isZoomed()\" (click)=\"zoomReset()\">\n {{ zoomPercent() }}%\n </button>\n <button type=\"button\" class=\"pecb-slide-viewer__nb\" [attr.aria-label]=\"text().zoomIn\" [disabled]=\"navDisabled()\" (click)=\"zoomIn()\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M12 5v14M5 12h14\" /></svg>\n </button>\n </div>\n }\n\n <div class=\"pecb-slide-viewer__right\">\n <!-- Version select or any other control the product wants here -->\n <ng-content select=\"[pecbSlideViewerVersion]\"></ng-content>\n\n @if (showFullscreen()) {\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__nb\"\n [attr.aria-label]=\"isFullscreen() ? text().exitFullscreen : text().enterFullscreen\"\n [attr.aria-pressed]=\"isFullscreen()\"\n [disabled]=\"navDisabled()\"\n (click)=\"toggleFullscreen()\"\n >\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3\" />\n </svg>\n </button>\n }\n </div>\n </div>\n\n <!-- Note (mock `.sl-note`) -->\n @if (hasNote()) {\n <div class=\"pecb-slide-viewer__note\" [class.pecb-slide-viewer__note--open]=\"noteOpen()\">\n <button\n type=\"button\"\n class=\"pecb-slide-viewer__note-h\"\n [attr.aria-expanded]=\"noteOpen()\"\n [attr.aria-controls]=\"uid + '-note'\"\n (click)=\"toggleNote()\"\n >\n <svg width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M14 11H8M10 15H8M16 7H8M20 6.8v10.4c0 1.68 0 2.52-.327 3.162a3 3 0 0 1-1.311 1.311C17.72 22 16.88 22 15.2 22H8.8c-1.68 0-2.52 0-3.162-.327a3 3 0 0 1-1.311-1.311C4 19.72 4 18.88 4 17.2V6.8c0-1.68 0-2.52.327-3.162a3 3 0 0 1 1.311-1.311C6.28 2 7.12 2 8.8 2h6.4c1.68 0 2.52 0 3.162.327a3 3 0 0 1 1.311 1.311C20 4.28 20 5.12 20 6.8Z\" />\n </svg>\n <span>{{ text().note }}</span>\n <i class=\"pecb-slide-viewer__chev\" aria-hidden=\"true\">\n <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </i>\n </button>\n <div class=\"pecb-slide-viewer__note-b\" [id]=\"uid + '-note'\" [attr.hidden]=\"noteOpen() ? null : ''\">\n @if (note(); as n) {\n <div [innerHTML]=\"n\"></div>\n }\n <!-- Host-rendered note body (an editor, actions\u2026) -->\n <ng-content select=\"[pecbSlideViewerNote]\"></ng-content>\n </div>\n </div>\n }\n\n <!-- Anything the product wants under the stage column (e.g. an LTI table) -->\n <ng-content select=\"[pecbSlideViewerBelow]\"></ng-content>\n </div>\n\n <!-- \u2500\u2500 Sidebar: search + thumbnails (mock `.sl-side`) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (sidebar() !== 'none') {\n <aside class=\"pecb-slide-viewer__side\" [attr.aria-label]=\"text().thumbnails\">\n <!-- Host header: a title, \"Add slide\"\u2026 -->\n <ng-content select=\"[pecbSlideViewerSidebarHeader]\"></ng-content>\n\n @if (showSearch()) {\n <div class=\"pecb-slide-viewer__search\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"11\" cy=\"11\" r=\"8\" />\n <path d=\"m21 21-4.3-4.3\" />\n </svg>\n <input\n #search\n type=\"search\"\n [attr.placeholder]=\"text().searchPlaceholder\"\n [attr.aria-label]=\"text().searchPlaceholder\"\n [attr.aria-controls]=\"uid + '-thumbs'\"\n [value]=\"searchTerm()\"\n (input)=\"onSearchInput($event)\"\n (keydown)=\"onSearchKeydown($event)\"\n />\n </div>\n @if (isSearching()) {\n <p class=\"pecb-slide-viewer__results\" aria-live=\"polite\">{{ searchSummary() }}</p>\n }\n }\n\n <div #thumbs class=\"pecb-slide-viewer__thumbs\" [id]=\"uid + '-thumbs'\">\n @if (error() || isEmpty()) {\n <p class=\"pecb-slide-viewer__empty\">{{ error() ? text().errorTitle : text().emptyTitle }}</p>\n } @else if (loading() && total() === 0) {\n @for (row of skeletonRows; track row) {\n <div class=\"pecb-slide-viewer__thumb pecb-slide-viewer__thumb--skeleton\" aria-hidden=\"true\">\n <div class=\"pecb-slide-viewer__thumb-img\"></div>\n <div class=\"pecb-slide-viewer__thumb-n\"></div>\n </div>\n }\n } @else {\n <!-- Spacers are always present: removing one mid-render would let the browser clamp the scroll position. -->\n <div class=\"pecb-slide-viewer__spacer\" [style.height.px]=\"spacerTop()\" aria-hidden=\"true\"></div>\n @for (n of visibleTiles(); track n) {\n @let slide = slides()[n - 1];\n @if (slide) {\n <div\n class=\"pecb-slide-viewer__thumb\"\n [class.pecb-slide-viewer__thumb--pending]=\"tilePending(slide)\"\n role=\"button\"\n tabindex=\"0\"\n [attr.data-n]=\"n\"\n [class.pecb-slide-viewer__thumb--active]=\"n === currentPage()\"\n [class.pecb-slide-viewer__thumb--hidden]=\"slide.hidden\"\n [class.pecb-slide-viewer__thumb--selected]=\"selectable() && isSelected(slide, n)\"\n [class.pecb-slide-viewer__thumb--dragging]=\"dragFrom() === n\"\n [class.pecb-slide-viewer__thumb--drop-target]=\"dragOver() === n && dragFrom() !== n\"\n [attr.aria-current]=\"n === currentPage() ? 'true' : null\"\n [attr.aria-label]=\"slideTitle(slide, n)\"\n [attr.aria-description]=\"reorderable() ? text().dragHint : null\"\n [attr.draggable]=\"reorderable() ? 'true' : null\"\n (click)=\"onThumbnailClick(n)\"\n (keydown.enter)=\"onThumbnailClick(n)\"\n (keydown.space)=\"onThumbnailClick(n); $event.preventDefault()\"\n (dragstart)=\"onTileDragStart($event, n)\"\n (dragover)=\"onTileDragOver($event, n)\"\n (drop)=\"onTileDrop($event, n)\"\n (dragend)=\"onTileDragEnd()\"\n >\n <div class=\"pecb-slide-viewer__thumb-img\">\n @if (thumbnailSrc(slide); as src) {\n <img [src]=\"src\" alt=\"\" loading=\"lazy\" decoding=\"async\" draggable=\"false\" />\n } @else if (slide.title) {\n <span>{{ slide.title }}</span>\n }\n\n @if (selectable()) {\n <label class=\"pecb-slide-viewer__check\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"isSelected(slide, n)\"\n [attr.aria-label]=\"selectLabel(n)\"\n (change)=\"onTileCheckbox($event, slide, n)\"\n />\n <span class=\"pecb-slide-viewer__check-box\" aria-hidden=\"true\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 6 9 17l-5-5\" /></svg>\n </span>\n </label>\n }\n\n @if (slide.hidden) {\n <span class=\"pecb-slide-viewer__hidden-badge\">{{ text().hidden }}</span>\n }\n\n @if (reorderable()) {\n <span class=\"pecb-slide-viewer__grip\" aria-hidden=\"true\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"9\" cy=\"6\" r=\"1.6\" /><circle cx=\"15\" cy=\"6\" r=\"1.6\" /><circle cx=\"9\" cy=\"12\" r=\"1.6\" /><circle cx=\"15\" cy=\"12\" r=\"1.6\" /><circle cx=\"9\" cy=\"18\" r=\"1.6\" /><circle cx=\"15\" cy=\"18\" r=\"1.6\" /></svg>\n </span>\n }\n\n @if (thumbnailTemplate(); as tpl) {\n <div class=\"pecb-slide-viewer__thumb-extra\">\n <ng-container *ngTemplateOutlet=\"tpl.templateRef; context: thumbnailContext(slide, n)\" />\n </div>\n }\n </div>\n <div class=\"pecb-slide-viewer__thumb-n\">{{ n }}</div>\n </div>\n }\n }\n <div class=\"pecb-slide-viewer__spacer\" [style.height.px]=\"spacerBottom()\" aria-hidden=\"true\"></div>\n @if (isSearching() && matches().length === 0) {\n <p class=\"pecb-slide-viewer__empty\">{{ text().noResults }}</p>\n }\n }\n </div>\n\n <!-- Host footer: comments, bulk actions\u2026 -->\n <ng-content select=\"[pecbSlideViewerSidebarFooter]\"></ng-content>\n </aside>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;max-width:100%;container-type:inline-size;container-name:pecb-slide-viewer;--pecb-slide-radius: 12px;--pecb-slide-gap: 20px;--pecb-slide-sidebar-width: 340px;--pecb-slide-sidebar-top: 86px;--pecb-slide-thumbs-max-height: 620px;--pecb-slide-note-max-height: 210px;--pecb-slide-active: #212427;--pecb-slide-border: #ececec;--pecb-slide-stage-bg: #f4f4f4;--pecb-slide-fullscreen-bg: #0b0b0d;--pecb-slide-thumb-radius: 8px;--pecb-slide-number-bg: var(--pecb-slide-active);--pecb-slide-font: Plus Jakarta Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif}.pecb-slide-viewer{display:flex;flex-direction:column;gap:16px;font-family:var(--pecb-slide-font);color:#212427;outline:none}.pecb-slide-viewer:focus-visible{box-shadow:0 0 0 2px #fff,0 0 0 4px #212427;border-radius:var(--pecb-slide-radius)}.pecb-slide-viewer__top{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;background:#fff;border:1px solid var(--pecb-slide-border);border-radius:var(--pecb-slide-radius);padding:12px 14px}.pecb-slide-viewer__days{display:flex;gap:8px;flex-wrap:wrap}.pecb-slide-viewer__day{border:none;background:none;color:inherit;height:38px;padding:0 18px;border:1px solid var(--pecb-slide-border);background:#fff;border-radius:8px;font:inherit;font-size:14px;font-weight:600;color:#616161;cursor:pointer;transition:background .15s ease-in-out,color .15s ease-in-out,border-color .15s ease-in-out}.pecb-slide-viewer__day:focus{outline:none}.pecb-slide-viewer__day:hover{background:#f4f4f4}.pecb-slide-viewer__day:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__day--active{background:var(--pecb-slide-active);border-color:var(--pecb-slide-active);color:#fff}.pecb-slide-viewer__day--active:hover{background:var(--pecb-slide-active)}.pecb-slide-viewer__tools{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.pecb-slide-viewer__tool{border:none;background:none;color:inherit;display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 12px;border-radius:8px;font:inherit;font-size:13.5px;font-weight:600;color:#212427;cursor:pointer;white-space:nowrap;transition:background .15s ease-in-out}.pecb-slide-viewer__tool:focus{outline:none}.pecb-slide-viewer__tool svg{flex:none;color:#616161}.pecb-slide-viewer__tool:hover:not(:disabled){background:#f4f4f4}.pecb-slide-viewer__tool:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__tool:disabled{color:#c2c2c2;cursor:not-allowed}.pecb-slide-viewer__tool:disabled svg{color:#c2c2c2}.pecb-slide-viewer__main{display:grid;grid-template-columns:minmax(0,1fr) var(--pecb-slide-sidebar-width);gap:var(--pecb-slide-gap);align-items:start}.pecb-slide-viewer--no-sidebar .pecb-slide-viewer__main{grid-template-columns:minmax(0,1fr)}.pecb-slide-viewer--sidebar-start .pecb-slide-viewer__main{grid-template-columns:var(--pecb-slide-sidebar-width) minmax(0,1fr)}.pecb-slide-viewer--sidebar-start .pecb-slide-viewer__stagecol{order:2}.pecb-slide-viewer--sidebar-start .pecb-slide-viewer__side{order:1}.pecb-slide-viewer__stagecol{min-width:0;display:flex;flex-direction:column;gap:14px}.pecb-slide-viewer__stage{position:relative;width:100%;aspect-ratio:var(--pecb-slide-aspect-ratio, 16/10);background:var(--pecb-slide-stage-bg);border-radius:var(--pecb-slide-radius);overflow:hidden;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:pan-y pinch-zoom}.pecb-slide-viewer__stage:fullscreen{border-radius:0;background:var(--pecb-slide-fullscreen-bg);aspect-ratio:auto;width:100vw;height:100vh}.pecb-slide-viewer__stage--skeleton{background:#ececec}.pecb-slide-viewer__stage--skeleton:before{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.7) 45%,rgba(255,255,255,.7) 55%,transparent);animation:pecb-slide-shimmer 1.6s ease-in-out infinite}.pecb-slide-viewer__viewport{position:absolute;inset:0;overflow:hidden}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport{overflow:auto;cursor:grab}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar{width:8px;height:8px}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar-track{background:transparent}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar-thumb{background:#c2c2c2;border-radius:12px}.pecb-slide-viewer--zoomed .pecb-slide-viewer__viewport::-webkit-scrollbar-thumb:hover{background:#a9a9a9}.pecb-slide-viewer__image{display:block;width:100%;height:100%;object-fit:contain;background:var(--pecb-slide-stage-bg)}.pecb-slide-viewer__stage:fullscreen .pecb-slide-viewer__image{background:var(--pecb-slide-fullscreen-bg)}.pecb-slide-viewer__stage--fallback .pecb-slide-viewer__image{image-rendering:auto;filter:saturate(.98)}.pecb-slide-viewer__overlay{position:absolute;inset:0;pointer-events:none}.pecb-slide-viewer__overlay>*{pointer-events:auto}.pecb-slide-viewer__overlay:empty{display:none}.pecb-slide-viewer__message{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;text-align:center;color:#616161}.pecb-slide-viewer__message:has(>:not(.pecb-slide-viewer__message-default)) .pecb-slide-viewer__message-default{display:none}.pecb-slide-viewer__message--error{color:#313131}.pecb-slide-viewer__message--error svg{color:#a11e29}.pecb-slide-viewer__message-default{display:flex;flex-direction:column;align-items:center;gap:6px;max-width:360px}.pecb-slide-viewer__message-default svg{color:#919191;margin-bottom:6px}.pecb-slide-viewer__message-default strong{font-size:16px;font-weight:600;color:#212427}.pecb-slide-viewer__message-default span{font-size:14px;line-height:1.5}.pecb-slide-viewer__retry{border:none;background:none;color:inherit;margin-top:10px;height:38px;padding:0 18px;border:1px solid #c2c2c2;border-radius:8px;background:#fff;font:inherit;font-size:14px;font-weight:600;color:#212427;cursor:pointer;transition:background .15s ease-in-out}.pecb-slide-viewer__retry:focus{outline:none}.pecb-slide-viewer__retry:hover{background:#f4f4f4}.pecb-slide-viewer__retry:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__stage-n{position:absolute;left:16px;bottom:16px;font-size:13px;font-weight:600;color:#ffffffd9;text-shadow:0 1px 2px rgba(0,0,0,.35);pointer-events:none}.pecb-slide-viewer__progress{position:absolute;left:0;right:0;top:0;height:3px;overflow:hidden;pointer-events:none}.pecb-slide-viewer__progress:after{content:\"\";position:absolute;inset:0;width:40%;background:var(--pecb-slide-active);opacity:.7;border-radius:999px;animation:pecb-slide-progress 1s ease-in-out infinite}.pecb-slide-viewer__veil{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none}.pecb-slide-viewer__veil-text{padding:6px 12px;border-radius:999px;background:#ffffffd9;font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;color:#616161}.pecb-slide-viewer__fs-exit{padding:0;border:none;background:none;font:inherit;color:inherit;position:absolute;top:16px;right:16px;width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;background:#1414168c;color:#fff;cursor:pointer}.pecb-slide-viewer__fs-exit:focus{outline:none}.pecb-slide-viewer__fs-exit:hover{background:#141416bf}.pecb-slide-viewer__fs-exit:focus-visible{outline:2px solid #ffffff;outline-offset:2px}.pecb-slide-viewer__bar{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.pecb-slide-viewer__nav{display:flex;gap:8px}.pecb-slide-viewer__nb{padding:0;border:none;background:none;font:inherit;color:inherit;width:44px;height:44px;display:inline-flex;align-items:center;justify-content:center;border:1px solid #c2c2c2;background:#fff;border-radius:8px;color:#212427;cursor:pointer;transition:transform .1s,background .12s}.pecb-slide-viewer__nb:focus{outline:none}.pecb-slide-viewer__nb svg{display:block}.pecb-slide-viewer__nb:hover:not(:disabled){background:#f4f4f4}.pecb-slide-viewer__nb:active:not(:disabled){transform:scale(.92);background:#ececec}.pecb-slide-viewer__nb:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__nb:disabled{color:#c2c2c2;cursor:not-allowed}.pecb-slide-viewer__count{display:flex;align-items:center;gap:10px;font-size:14px;color:#616161}.pecb-slide-viewer__count label{cursor:default}.pecb-slide-viewer__page-input{width:70px;height:44px;border:1px solid var(--pecb-slide-border);border-radius:8px;font:inherit;font-size:14px;font-variant-numeric:tabular-nums;text-align:center;color:#212427;background:#fff;-moz-appearance:textfield;appearance:textfield}.pecb-slide-viewer__page-input::-webkit-outer-spin-button,.pecb-slide-viewer__page-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.pecb-slide-viewer__page-input:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:1px}.pecb-slide-viewer__page-input:disabled{color:#c2c2c2;background:#fafafa}.pecb-slide-viewer__zoom{display:flex;align-items:center;gap:6px}.pecb-slide-viewer__zoom-value{border:none;background:none;color:inherit;min-width:56px;height:44px;padding:0 8px;border:1px solid var(--pecb-slide-border);border-radius:8px;background:#fff;font:inherit;font-size:13px;font-variant-numeric:tabular-nums;color:#616161;cursor:pointer}.pecb-slide-viewer__zoom-value:focus{outline:none}.pecb-slide-viewer__zoom-value:disabled{cursor:default;color:#919191}.pecb-slide-viewer__zoom-value:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__right{display:flex;align-items:center;gap:10px;margin-left:auto;min-width:0}.pecb-slide-viewer__right>:not(.pecb-slide-viewer__nb){min-width:220px}.pecb-slide-viewer__note{background:#fff;border:1px solid var(--pecb-slide-border);border-radius:var(--pecb-slide-radius);overflow:hidden}.pecb-slide-viewer__note-h{border:none;background:none;color:inherit;width:100%;display:flex;align-items:center;gap:10px;padding:18px 20px;font:inherit;font-size:15px;font-weight:600;color:#212427;cursor:pointer;text-align:left}.pecb-slide-viewer__note-h:focus{outline:none}.pecb-slide-viewer__note-h svg{flex:none;color:#616161}.pecb-slide-viewer__note-h:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:-2px;border-radius:var(--pecb-slide-radius)}.pecb-slide-viewer__chev{margin-left:auto;display:inline-flex;transition:transform .18s}.pecb-slide-viewer__note--open .pecb-slide-viewer__chev{transform:rotate(180deg)}.pecb-slide-viewer__note-b{padding:0 20px 20px;font-size:13.5px;line-height:1.6;color:#616161;max-height:var(--pecb-slide-note-max-height);overflow-y:auto}.pecb-slide-viewer__note-b::-webkit-scrollbar{width:8px;height:8px}.pecb-slide-viewer__note-b::-webkit-scrollbar-track{background:#f4f4f4}.pecb-slide-viewer__note-b::-webkit-scrollbar-thumb{background:#c2c2c2;border-radius:12px}.pecb-slide-viewer__note-b::-webkit-scrollbar-thumb:hover{background:#a9a9a9}.pecb-slide-viewer__note-b[hidden]{display:none}.pecb-slide-viewer__note-b ::ng-deep p{margin:0 0 12px}.pecb-slide-viewer__note-b ::ng-deep p:last-child{margin:0}.pecb-slide-viewer__note-b ::ng-deep ul,.pecb-slide-viewer__note-b ::ng-deep ol{padding-left:20px;margin:0 0 12px}.pecb-slide-viewer__note-b>[pecbSlideViewerNote]{display:block}.pecb-slide-viewer__side{position:sticky;top:var(--pecb-slide-sidebar-top);min-width:0;background:#fff;border:1px solid var(--pecb-slide-border);border-radius:var(--pecb-slide-radius);padding:14px;display:flex;flex-direction:column;gap:14px}.pecb-slide-viewer__search{display:flex;align-items:center;gap:10px;height:46px;border:1px solid var(--pecb-slide-border);border-radius:8px;padding:0 12px;background:#fff}.pecb-slide-viewer__search svg{flex:none;color:#c2c2c2}.pecb-slide-viewer__search input{flex:1 1 auto;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:14px;color:#212427}.pecb-slide-viewer__search input::placeholder{color:#919191}.pecb-slide-viewer__search input::-webkit-search-cancel-button{-webkit-appearance:none}.pecb-slide-viewer__search:focus-within{border-color:#c2c2c2;box-shadow:0 0 0 3px #21242714}.pecb-slide-viewer__results{margin:-6px 0 0;font-size:12px;font-weight:600;color:#919191}.pecb-slide-viewer__thumbs{display:flex;flex-direction:column;gap:12px;max-height:var(--pecb-slide-thumbs-max-height);overflow-y:auto;overflow-anchor:none;padding-right:6px;scroll-behavior:smooth}.pecb-slide-viewer__thumbs::-webkit-scrollbar{width:8px;height:8px}.pecb-slide-viewer__thumbs::-webkit-scrollbar-track{background:#f4f4f4}.pecb-slide-viewer__thumbs::-webkit-scrollbar-thumb{background:#c2c2c2;border-radius:12px}.pecb-slide-viewer__thumbs::-webkit-scrollbar-thumb:hover{background:#a9a9a9}.pecb-slide-viewer__thumb{display:block;width:100%;flex:none;border-radius:var(--pecb-slide-thumb-radius);overflow:hidden;border:2px solid transparent;background:#fff;cursor:pointer;text-align:left;-webkit-user-select:none;user-select:none;transition:border-color .15s ease-in-out,opacity .15s ease-in-out}.pecb-slide-viewer__thumb:hover{border-color:#c2c2c2}.pecb-slide-viewer__thumb:focus-visible{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__thumb--active{border-color:var(--pecb-slide-active)}.pecb-slide-viewer__thumb--active .pecb-slide-viewer__thumb-n{background:var(--pecb-slide-number-bg);color:#fff}.pecb-slide-viewer__thumb--selected{border-color:var(--pecb-slide-active)}.pecb-slide-viewer__thumb--hidden{opacity:.55}.pecb-slide-viewer__thumb--dragging{opacity:.4}.pecb-slide-viewer__thumb--drop-target{border-color:var(--pecb-slide-active);border-style:dashed}.pecb-slide-viewer--reorderable .pecb-slide-viewer__thumb{cursor:grab}.pecb-slide-viewer__thumb-img{position:relative;aspect-ratio:16/9;display:flex;align-items:center;background:var(--pecb-slide-stage-bg);overflow:hidden}.pecb-slide-viewer__thumb-img img{display:block;width:100%;height:100%;object-fit:cover}.pecb-slide-viewer__thumb-img span{padding:0 14px;font-size:14px;font-weight:600;color:#616161}.pecb-slide-viewer__thumb-n{text-align:center;font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums;color:#616161;padding:6px 0}.pecb-slide-viewer__check{position:absolute;top:8px;left:8px;margin:0;cursor:pointer}.pecb-slide-viewer__check input{position:absolute;opacity:0;width:1px;height:1px}.pecb-slide-viewer__check input:focus-visible+.pecb-slide-viewer__check-box{outline:2px solid var(--pecb-slide-active);outline-offset:2px}.pecb-slide-viewer__check input:checked+.pecb-slide-viewer__check-box{background:var(--pecb-slide-active);border-color:var(--pecb-slide-active);color:#fff}.pecb-slide-viewer__check-box{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:5px;border:1.5px solid #c2c2c2;background:#ffffffeb;color:transparent;transition:background .15s ease-in-out,border-color .15s ease-in-out}.pecb-slide-viewer__hidden-badge{position:absolute;top:8px;right:8px;padding:2px 7px;border-radius:999px;background:#212427bf;color:#fff;font-size:11px;font-weight:600;letter-spacing:.02em}.pecb-slide-viewer__grip{position:absolute;right:6px;bottom:6px;display:inline-flex;padding:3px;border-radius:4px;background:#ffffffd9;color:#616161}.pecb-slide-viewer__thumb-extra{position:absolute;inset:0;pointer-events:none}.pecb-slide-viewer__thumb-extra>*{pointer-events:auto}.pecb-slide-viewer__spacer{flex:none;width:100%}.pecb-slide-viewer__spacer[style*=\"height: 0px\"]{display:none}.pecb-slide-viewer__thumb--pending .pecb-slide-viewer__thumb-img{background:#ececec}.pecb-slide-viewer__thumb--pending .pecb-slide-viewer__thumb-img:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.7) 50%,transparent);animation:pecb-slide-shimmer 1.6s ease-in-out infinite}.pecb-slide-viewer__thumb--skeleton{pointer-events:none}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-img,.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n{position:relative;overflow:hidden;background:#ececec}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n{height:28px;margin-top:4px;border-radius:6px}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-img:after,.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n:after{content:\"\";position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.7) 50%,transparent);animation:pecb-slide-shimmer 1.6s ease-in-out infinite}.pecb-slide-viewer__empty{margin:0;padding:12px;font-size:13.5px;color:#919191}.pecb-slide-viewer__sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}@container pecb-slide-viewer (max-width: 1080px){.pecb-slide-viewer__main{grid-template-columns:minmax(0,1fr)}.pecb-slide-viewer__side{position:static}}@container pecb-slide-viewer (max-width: 640px){.pecb-slide-viewer__top{padding:10px 12px}.pecb-slide-viewer__tool span{display:none}.pecb-slide-viewer__tool{padding:0 10px}.pecb-slide-viewer__nb{width:40px;height:40px}.pecb-slide-viewer__page-input{height:40px;width:60px}.pecb-slide-viewer__right{margin-left:0;flex:1 1 100%}.pecb-slide-viewer__thumbs{max-height:360px}}@keyframes pecb-slide-progress{0%{transform:translate(-100%)}to{transform:translate(250%)}}@keyframes pecb-slide-shimmer{0%{transform:translate(-100%)}to{transform:translate(100%)}}@media (prefers-reduced-motion: reduce){.pecb-slide-viewer__day,.pecb-slide-viewer__tool,.pecb-slide-viewer__nb,.pecb-slide-viewer__chev,.pecb-slide-viewer__thumb{transition:none}.pecb-slide-viewer__thumbs{scroll-behavior:auto}.pecb-slide-viewer__progress:after{animation:none;width:100%}.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-img:after,.pecb-slide-viewer__thumb--skeleton .pecb-slide-viewer__thumb-n:after,.pecb-slide-viewer__thumb--pending .pecb-slide-viewer__thumb-img:after,.pecb-slide-viewer__stage--skeleton:before{animation:none}}\n"] }]
16147
+ }], ctorParameters: () => [], propDecorators: { rootRef: [{ type: i0.ViewChild, args: ['root', { isSignal: true }] }], stageRef: [{ type: i0.ViewChild, args: ['stage', { isSignal: true }] }], thumbsRef: [{ type: i0.ViewChild, args: ['thumbs', { isSignal: true }] }], searchRef: [{ type: i0.ViewChild, args: ['search', { isSignal: true }] }], thumbnailTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SlideViewerThumbnailDirective), { isSignal: true }] }], dayTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SlideViewerDayDirective), { isSignal: true }] }], days: [{ type: i0.Input, args: [{ isSignal: true, alias: "days", required: false }] }], slides: [{ type: i0.Input, args: [{ isSignal: true, alias: "slides", required: false }] }], note: [{ type: i0.Input, args: [{ isSignal: true, alias: "note", required: false }] }], showNote: [{ type: i0.Input, args: [{ isSignal: true, alias: "showNote", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingProgress: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingProgress", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], tools: [{ type: i0.Input, args: [{ isSignal: true, alias: "tools", required: false }] }], canDownloadSlides: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDownloadSlides", required: false }] }], stageAspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "stageAspectRatio", required: false }] }], sidebarPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "sidebarPosition", required: false }] }], showThumbnails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showThumbnails", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], showSlideInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSlideInput", required: false }] }], showFullscreen: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFullscreen", required: false }] }], zoomable: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomable", required: false }] }], zoomSteps: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomSteps", required: false }] }], skipHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipHidden", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], virtualizeAbove: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualizeAbove", required: false }] }], overscan: [{ type: i0.Input, args: [{ isSignal: true, alias: "overscan", required: false }] }], prefetch: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefetch", required: false }] }], swipe: [{ type: i0.Input, args: [{ isSignal: true, alias: "swipe", required: false }] }], keyboard: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboard", required: false }] }], keyMap: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyMap", required: false }] }], searchDebounce: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchDebounce", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], activeDayId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDayId", required: false }] }, { type: i0.Output, args: ["activeDayIdChange"] }], page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: false }] }, { type: i0.Output, args: ["pageChange"] }], searchTerm: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchTerm", required: false }] }, { type: i0.Output, args: ["searchTermChange"] }], noteOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "noteOpen", required: false }] }, { type: i0.Output, args: ["noteOpenChange"] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }, { type: i0.Output, args: ["zoomChange"] }], selectedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIds", required: false }] }, { type: i0.Output, args: ["selectedIdsChange"] }], dayChange: [{ type: i0.Output, args: ["dayChange"] }], slideChange: [{ type: i0.Output, args: ["slideChange"] }], tool: [{ type: i0.Output, args: ["tool"] }], fullscreenChange: [{ type: i0.Output, args: ["fullscreenChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], fullImageRequest: [{ type: i0.Output, args: ["fullImageRequest"] }], slideMove: [{ type: i0.Output, args: ["slideMove"] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
16148
+
15009
16149
  /**
15010
16150
  * An injectable, Toastr-style notification service.
15011
16151
  *
@@ -21905,5 +23045,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
21905
23045
  * Generated bundle index. Do not edit.
21906
23046
  */
21907
23047
 
21908
- export { APP_HEADER_TRANSLATIONS, APP_SHELL_LOCALES, APP_SIDEBAR_MENU_ICONS, APP_SIDEBAR_MENU_TRANSLATIONS, APP_SIDEBAR_TRANSLATIONS, AccordionItemComponent, AccordionSmallComponent, ActionItemComponent, AddButtonComponent, AdminHeaderComponent, AdminHeaderFieldTemplateDirective, AffixComponent, AlertComponent, AnchorComponent, AppHeaderComponent, AppSidebarComponent, ApplicationStatusBarComponent, AuditorStatusComponent, AuthorDateTimeComponent, BackToTopComponent, BadgeComponent, BlurDirective, BottomSheetComponent, BreadcrumbsComponent, ButtonComponent, ButtonGroupComponent, ButtonGroupItemComponent, CancelUpdateButtonsComponent, CardBodyComponent, CardComponent, CardFooterComponent, CardHeaderComponent, CertificateUploadBarComponent, CheckDeleteIconComponent, CheckboxComponent, CheckboxDisplayComponent, CodeInputComponent, CodeSnippetComponent, ColorPaletteComponent, ConfirmationComponent, ContentTypeTagComponent, CourseContentPanelComponent, CoursePlayerComponent, CourseTileComponent, CredentialCardComponent, DEFAULT_APP_HEADER_LABELS, DEFAULT_APP_SIDEBAR_LABELS, DEFAULT_APP_SIDEBAR_LANGUAGES, DEFAULT_APP_SIDEBAR_MENU, DEFAULT_APP_SIDEBAR_ROLES, DEFAULT_COURSE_CONTENT_PANEL_LABELS, DEFAULT_LANGUAGES, DEFAULT_VIDEO_PLAYER_LABELS, DEFAULT_VIDEO_PLAYER_RATES, DashboardGridComponent, DatepickerComponent, DividerComponent, DropdownComponent, EMPTY_STATE_MAX_BUTTONS, EditButtonComponent, EditCoverPhotoComponent, EmptyStateComponent, ExpandableRowTableComponent, FileUploadComponent, FilterColumnsComponent, FloatButtonComponent, FloatButtonItemComponent, FormSectionCardComponent, FullscreenModalComponent, FullscreenModalContentDirective, FullscreenModalFooterDirective, FullscreenModalHeaderDirective, GeneralComponent, GridComponent, GridItemComponent, HeaderActionsComponent, HeaderComponent, HeaderDividerComponent, HeaderLanguageComponent, HeaderSearchComponent, HeaderUserComponent, HierarchicalTableComponent, HorizontalStepsComponent, HubCardComponent, HubCardGridComponent, IconComponent, IconRegistry, IconTagComponent, InformationBoxComponent, InputComponent, LanguageDropdownComponent, LinkButtonComponent, ListItemComponent, LoadingService, MediaComponent, MessageBubbleComponent, MessageItemComponent, MetricsCardComponent, MiniIconButtonComponent, NoResultsComponent, NoteSidebarItemComponent, NotesPanelComponent, NotificationService, NotificationStatusLinkComponent, OutlinedFieldComponent, PECB_COLOR_PALETTE, PECB_CUSTOM_ICONS, PECB_FONT_STYLES, PECB_ICONS, PECB_TYPE_SCALE, PaginationComponent, PaymentPlanCardComponent, PaymentScheduleCardComponent, PaymentSummaryComponent, PeriodPaymentModalComponent, PriceMethodComponent, ProfileComponent, ProfileElementsCardComponent, ProfileGroupComponent, ProgressArcComponent, ProgressBarComponent, ProgressCircleComponent, ProjectLayoutComponent, QuantitySelectorComponent, QuestionTypeTagComponent, RadioComponent, RadioDisplayComponent, RatingNumberComponent, ReasonForReturnComponent, RequestSentByComponent, RequestStatusBarComponent, ResultPageComponent, RightModalComponent, RightModalContentDirective, RightModalFooterDirective, RightModalHeaderDirective, ShadowDirective, SidebarComponent, SkeletonComponent, SlidePointsComponent, SpacerComponent, SpinnerComponent, StandardsPdfCardComponent, StatisticsCardComponent, StatusComponent, StepperComponent, TabComponent, TableComponent, TagComponent, TagSelectComponent, TestimonialComponent, TextFormFieldComponent, ThemeService, ToggleComponent, ToolbarBarComponent, TooltipComponent, TooltipDirective, TourComponent, TranscriptLineComponent, TypographyComponent, UserWithEmailComponent, VerifyChecklistComponent, VideoPlayerComponent, VideoUploadBarComponent, VirtualTableComponent, addClass, announceToScreenReader, appHeaderLabels, appSidebarLabels, appSidebarMenu, appSidebarRoles, capitalize, closestElement, copyToClipboard, courseModuleStats, courseProgress, createAuthError, createAuthorizationError, createConfigError, createError, createNetworkError, createValidationError, disableBodyScroll, escapeHtml, findCourseLesson, findCourseModule, formatCount, formatCourseDuration, formatCourseTime, formatErrorForLog, formatErrorMessage, formatVideoTime, generateLinkedIds, generateUniqueId, getAriaCurrent, getButtonAriaAttributes, getComputedStyleValue, getDialogAriaAttributes, getFocusableElements, getInitials, getInputAriaAttributes, getOptionAriaAttributes, getProgressAriaAttributes, getScrollParent, getTabAriaAttributes, getVisuallyHiddenStyles, handleError, hasClass, isBlank, isBrowser, isElementVisible, isNotBlank, isPecbError, isRecoverableError, matchesSelector, parseLessonPartTitle, pluralize, prefersHighContrast, prefersReducedMotion, registerErrorHandler, removeClass, resolveAppShellLocale, resolveLessonStatus, scrollIntoView, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toggleClass, trapFocus, truncate, tryAsync, trySync, wrapError };
23048
+ export { APP_HEADER_TRANSLATIONS, APP_SHELL_LOCALES, APP_SIDEBAR_MENU_ICONS, APP_SIDEBAR_MENU_TRANSLATIONS, APP_SIDEBAR_TRANSLATIONS, AccordionItemComponent, AccordionSmallComponent, ActionItemComponent, AddButtonComponent, AdminHeaderComponent, AdminHeaderFieldTemplateDirective, AffixComponent, AlertComponent, AnchorComponent, AppHeaderComponent, AppSidebarComponent, ApplicationStatusBarComponent, AuditorStatusComponent, AuthorDateTimeComponent, BackToTopComponent, BadgeComponent, BlurDirective, BottomSheetComponent, BreadcrumbsComponent, ButtonComponent, ButtonGroupComponent, ButtonGroupItemComponent, CancelUpdateButtonsComponent, CardBodyComponent, CardComponent, CardFooterComponent, CardHeaderComponent, CertificateUploadBarComponent, CheckDeleteIconComponent, CheckboxComponent, CheckboxDisplayComponent, CodeInputComponent, CodeSnippetComponent, ColorPaletteComponent, ConfirmationComponent, ContentTypeTagComponent, CourseContentPanelComponent, CoursePlayerComponent, CourseTileComponent, CredentialCardComponent, DEFAULT_APP_HEADER_LABELS, DEFAULT_APP_SIDEBAR_LABELS, DEFAULT_APP_SIDEBAR_LANGUAGES, DEFAULT_APP_SIDEBAR_MENU, DEFAULT_APP_SIDEBAR_ROLES, DEFAULT_COURSE_CONTENT_PANEL_LABELS, DEFAULT_LANGUAGES, DEFAULT_SLIDE_VIEWER_KEY_MAP, DEFAULT_SLIDE_VIEWER_LABELS, DEFAULT_SLIDE_VIEWER_TOOLS, DEFAULT_SLIDE_VIEWER_ZOOM_STEPS, DEFAULT_VIDEO_PLAYER_LABELS, DEFAULT_VIDEO_PLAYER_RATES, DashboardGridComponent, DatepickerComponent, DividerComponent, DropdownComponent, EMPTY_STATE_MAX_BUTTONS, EditButtonComponent, EditCoverPhotoComponent, EmptyStateComponent, ExpandableRowTableComponent, FileUploadComponent, FilterColumnsComponent, FloatButtonComponent, FloatButtonItemComponent, FormSectionCardComponent, FullscreenModalComponent, FullscreenModalContentDirective, FullscreenModalFooterDirective, FullscreenModalHeaderDirective, GeneralComponent, GridComponent, GridItemComponent, HeaderActionsComponent, HeaderComponent, HeaderDividerComponent, HeaderLanguageComponent, HeaderSearchComponent, HeaderUserComponent, HierarchicalTableComponent, HorizontalStepsComponent, HubCardComponent, HubCardGridComponent, IconComponent, IconRegistry, IconTagComponent, InformationBoxComponent, InputComponent, LanguageDropdownComponent, LinkButtonComponent, ListItemComponent, LoadingService, MediaComponent, MessageBubbleComponent, MessageItemComponent, MetricsCardComponent, MiniIconButtonComponent, NoResultsComponent, NoteSidebarItemComponent, NotesPanelComponent, NotificationService, NotificationStatusLinkComponent, OutlinedFieldComponent, PECB_COLOR_PALETTE, PECB_CUSTOM_ICONS, PECB_FONT_STYLES, PECB_ICONS, PECB_TYPE_SCALE, PaginationComponent, PaymentPlanCardComponent, PaymentScheduleCardComponent, PaymentSummaryComponent, PeriodPaymentModalComponent, PriceMethodComponent, ProfileComponent, ProfileElementsCardComponent, ProfileGroupComponent, ProgressArcComponent, ProgressBarComponent, ProgressCircleComponent, ProjectLayoutComponent, QuantitySelectorComponent, QuestionTypeTagComponent, RadioComponent, RadioDisplayComponent, RatingNumberComponent, ReasonForReturnComponent, RequestSentByComponent, RequestStatusBarComponent, ResultPageComponent, RightModalComponent, RightModalContentDirective, RightModalFooterDirective, RightModalHeaderDirective, ShadowDirective, SidebarComponent, SkeletonComponent, SlidePointsComponent, SlideViewerComponent, SlideViewerDayDirective, SlideViewerThumbnailDirective, SpacerComponent, SpinnerComponent, StandardsPdfCardComponent, StatisticsCardComponent, StatusComponent, StepperComponent, TabComponent, TableComponent, TagComponent, TagSelectComponent, TestimonialComponent, TextFormFieldComponent, ThemeService, ToggleComponent, ToolbarBarComponent, TooltipComponent, TooltipDirective, TourComponent, TranscriptLineComponent, TypographyComponent, UserWithEmailComponent, VerifyChecklistComponent, VideoPlayerComponent, VideoUploadBarComponent, VirtualTableComponent, addClass, announceToScreenReader, appHeaderLabels, appSidebarLabels, appSidebarMenu, appSidebarRoles, capitalize, clampSlidePage, closestElement, copyToClipboard, courseModuleStats, courseProgress, createAuthError, createAuthorizationError, createConfigError, createError, createNetworkError, createValidationError, disableBodyScroll, escapeHtml, findCourseLesson, findCourseModule, formatCount, formatCourseDuration, formatCourseTime, formatErrorForLog, formatErrorMessage, formatVideoTime, generateLinkedIds, generateUniqueId, getAriaCurrent, getButtonAriaAttributes, getComputedStyleValue, getDialogAriaAttributes, getFocusableElements, getInitials, getInputAriaAttributes, getOptionAriaAttributes, getProgressAriaAttributes, getScrollParent, getTabAriaAttributes, getVisuallyHiddenStyles, handleError, hasClass, isBlank, isBrowser, isElementVisible, isNotBlank, isPecbError, isRecoverableError, matchesSelector, parseLessonPartTitle, pluralize, prefersHighContrast, prefersReducedMotion, registerErrorHandler, removeClass, resolveAppShellLocale, resolveLessonStatus, scrollIntoView, slugify, stripHtml, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toggleClass, trapFocus, truncate, tryAsync, trySync, wrapError };
21909
23049
  //# sourceMappingURL=pecb-ui-components.mjs.map