@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.
- package/data-display/index.d.ts +1 -1
- package/fesm2022/pecb-ui-components-data-display.mjs +2 -2
- package/fesm2022/pecb-ui-components-data-display.mjs.map +1 -1
- package/fesm2022/pecb-ui-components.mjs +1142 -2
- package/fesm2022/pecb-ui-components.mjs.map +1 -1
- package/index.d.ts +507 -2
- package/package.json +1 -1
|
@@ -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
|