@panelwave/player 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5456,6 +5456,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
5456
5456
  * Manages audio playback with Web Audio API
5457
5457
  */
5458
5458
  const ROLES = ['ambient', 'music', 'voiceover', 'sfx'];
5459
+ /**
5460
+ * Whether an audio URL must be loaded with `crossOrigin = 'anonymous'`: an
5461
+ * http(s) file on another origin than the page. Same-origin files, data:/
5462
+ * blob: URLs and `file:` pages (the offline self-hosting package) stay in
5463
+ * no-CORS mode — CORS requests from a file: page fail.
5464
+ */
5465
+ function needsCorsMode(url, pageHref = typeof location !== 'undefined' ? location.href : '') {
5466
+ try {
5467
+ const target = new URL(url, pageHref || undefined);
5468
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') {
5469
+ return false;
5470
+ }
5471
+ const page = pageHref ? new URL(pageHref) : null;
5472
+ if (page && page.protocol !== 'http:' && page.protocol !== 'https:') {
5473
+ return false;
5474
+ }
5475
+ return !page || target.origin !== page.origin;
5476
+ }
5477
+ catch {
5478
+ return false;
5479
+ }
5480
+ }
5459
5481
  /**
5460
5482
  * Audio Engine Service
5461
5483
  * Provides Web Audio API-based audio playback with multiple buses.
@@ -5859,8 +5881,15 @@ class AudioEngineService {
5859
5881
  if (!bus) {
5860
5882
  throw new Error(`Invalid role: ${track.role}`);
5861
5883
  }
5862
- // Create audio element
5863
- const audio = new Audio(track.url);
5884
+ // Create audio element. A file from another origin (object storage, a
5885
+ // CDN) must be fetched in CORS mode: routed through Web Audio
5886
+ // (createMediaElementSource below), a no-CORS cross-origin element plays
5887
+ // but outputs silence. crossOrigin has to be set before the src.
5888
+ const audio = new Audio();
5889
+ if (needsCorsMode(track.url)) {
5890
+ audio.crossOrigin = 'anonymous';
5891
+ }
5892
+ audio.src = track.url;
5864
5893
  audio.loop = track.loop || false;
5865
5894
  const rate = typeof track.playbackRate === 'number' && isFinite(track.playbackRate) ? Math.max(0.25, Math.min(4, track.playbackRate)) : 1;
5866
5895
  if (rate !== 1) {
@@ -20119,6 +20148,7 @@ class PlayerShellComponent {
20119
20148
  }
20120
20149
  // Set initial locale and configure translations
20121
20150
  this.applyLocale();
20151
+ this.lastTrackedLocale = this.locale;
20122
20152
  // Check if page view is available
20123
20153
  const loadedManifest = this.manifestService.getManifest();
20124
20154
  if (loadedManifest?.chapters) {
@@ -20340,6 +20370,16 @@ class PlayerShellComponent {
20340
20370
  panelId,
20341
20371
  previousPanelId: this.lastPanelChangeId,
20342
20372
  });
20373
+ // One `transition` per move between two panels (the flow analytics
20374
+ // aggregate edges from it); the first panel of a session has none.
20375
+ if (panelId && this.lastPanelChangeId && this.lastPanelChangeId !== panelId) {
20376
+ this.trackingService.track('transition', {
20377
+ fromPanelId: this.lastPanelChangeId,
20378
+ toPanelId: panelId,
20379
+ panelId,
20380
+ chapterId: this.currentChapter.id,
20381
+ });
20382
+ }
20343
20383
  this.lastPanelChangeId = panelId || undefined;
20344
20384
  this.trackPanelView(panel, this.currentChapter);
20345
20385
  this.updatePreloadTargets();
@@ -20353,7 +20393,17 @@ class PlayerShellComponent {
20353
20393
  this.playerState.locale$
20354
20394
  .pipe(skip(1), distinctUntilChanged$1(), takeUntil(this.destroy$))
20355
20395
  .subscribe((locale) => {
20396
+ const fromLocale = this.lastTrackedLocale;
20397
+ this.lastTrackedLocale = locale;
20356
20398
  this.localeChange.emit(locale);
20399
+ if (fromLocale && fromLocale !== locale) {
20400
+ this.trackingService.track('lang_change', {
20401
+ from: fromLocale,
20402
+ to: locale,
20403
+ panelId: this.getCurrentPanelId(),
20404
+ chapterId: this.currentChapter?.id,
20405
+ });
20406
+ }
20357
20407
  });
20358
20408
  }
20359
20409
  /**
@@ -20590,6 +20640,13 @@ class PlayerShellComponent {
20590
20640
  // A choice: its path appears in page view, the other options' panels stay placeholders.
20591
20641
  this.madeChoices.set(evt.panelId, effect.to);
20592
20642
  this.refreshBranchVisibility();
20643
+ if (this.currentChapter) {
20644
+ this.trackDecision(this.currentChapter.id, evt.panelId, effect.to, {
20645
+ choiceId: evt.hotspot.id,
20646
+ choiceLabel: this.localizedText(evt.hotspot.label),
20647
+ source: 'hotspot',
20648
+ });
20649
+ }
20593
20650
  }
20594
20651
  this.applyHotspotEffect(effect);
20595
20652
  this.refreshVariableContext();
@@ -20794,6 +20851,24 @@ class PlayerShellComponent {
20794
20851
  this.refreshVariableContext();
20795
20852
  this.variableChange.emit({ key, value });
20796
20853
  }
20854
+ /**
20855
+ * Host-side variable write after load (the CMS preview's live variable
20856
+ * editor, an embed bridge): the value lands in its definition's declared
20857
+ * scope, a read-only variable is seeded the privileged way
20858
+ * `initialVariables` uses, and conditional content reacts at once.
20859
+ * Emits `variableChange`; the story position is kept.
20860
+ */
20861
+ seedVariable(key, value) {
20862
+ const def = this.variableDefinitions.find((d) => d.id === key);
20863
+ if (def?.readOnly) {
20864
+ this.variableStore.seed({ [key]: value });
20865
+ }
20866
+ else {
20867
+ this.variableStore.set(key, value, def?.scope ?? 'session', def ? this.scopeIdFor(def) : undefined);
20868
+ }
20869
+ this.refreshVariableContext();
20870
+ this.variableChange.emit({ key, value });
20871
+ }
20797
20872
  /**
20798
20873
  * Get variable
20799
20874
  */
@@ -21249,7 +21324,12 @@ class PlayerShellComponent {
21249
21324
  entry.liked = this.liked;
21250
21325
  store[this.workKey()] = entry;
21251
21326
  this.writeSocialStore(store);
21252
- this.trackingService.track('like', { workId: this.workKey(), liked: this.liked });
21327
+ this.trackingService.track('like', {
21328
+ workId: this.workKey(),
21329
+ liked: this.liked,
21330
+ panelId: this.getCurrentPanelId(),
21331
+ chapterId: this.currentChapter?.id,
21332
+ });
21253
21333
  this.likeChange.emit({ workId: this.workKey(), liked: this.liked });
21254
21334
  this.cdr.markForCheck();
21255
21335
  }
@@ -21285,6 +21365,16 @@ class PlayerShellComponent {
21285
21365
  this.shareLink = this.buildShareLink();
21286
21366
  this.shareVisible = true;
21287
21367
  }
21368
+ /** The reader copied or posted the link from the share dialog. */
21369
+ onShared(channel) {
21370
+ this.trackingService.track('share', {
21371
+ workId: this.workKey(),
21372
+ channel,
21373
+ panelId: this.getCurrentPanelId(),
21374
+ chapterId: this.currentChapter?.id,
21375
+ url: this.shareLink,
21376
+ });
21377
+ }
21288
21378
  /**
21289
21379
  * What to share: the host's `shareUrl`, else the current address with the
21290
21380
  * reading position — the page in page view, the panel in panel view, the
@@ -22382,6 +22472,38 @@ class PlayerShellComponent {
22382
22472
  openPaywall(gate) {
22383
22473
  this.paywallGate = gate;
22384
22474
  this.paywallVisible = true;
22475
+ this.trackingService.track('paywall_view', this.paywallEventPayload(gate));
22476
+ }
22477
+ /**
22478
+ * Analytics payload shared by `paywall_view` and `paywall_action`: the
22479
+ * gated panel (the gate's panel, else where the reader stands), the rule
22480
+ * that raised the gate and why.
22481
+ */
22482
+ paywallEventPayload(gate) {
22483
+ const panelId = gate.scope === 'panel' && gate.refId ? gate.refId : this.getCurrentPanelId();
22484
+ return {
22485
+ panelId,
22486
+ chapterId: gate.scope === 'chapter' && gate.refId ? gate.refId : this.currentChapter?.id,
22487
+ ruleId: gate.ruleId,
22488
+ scope: gate.scope,
22489
+ refId: gate.refId,
22490
+ lockReason: gate.lockReason,
22491
+ requireEntitlement: gate.requireEntitlement,
22492
+ };
22493
+ }
22494
+ /**
22495
+ * Track a reader's choice between branches — the branch chooser's edge or
22496
+ * a `goTo` hotspot — as a `decision` (schema tracking vocabulary; the flow
22497
+ * analytics aggregate `choiceId` per `panelId`).
22498
+ */
22499
+ trackDecision(chapterId, fromPanelId, toPanelId, choice) {
22500
+ this.trackingService.track('decision', {
22501
+ panelId: fromPanelId,
22502
+ chapterId,
22503
+ fromPanelId,
22504
+ toPanelId,
22505
+ ...choice,
22506
+ });
22385
22507
  }
22386
22508
  /** Dismiss the overlay without changing entitlement. */
22387
22509
  closePaywall() {
@@ -22397,6 +22519,7 @@ class PlayerShellComponent {
22397
22519
  const gate = this.paywallGate;
22398
22520
  const productId = action === 'purchase' || action === 'subscribe' ? this.paywallProductId : undefined;
22399
22521
  if (gate) {
22522
+ this.trackingService.track('paywallAction', { ...this.paywallEventPayload(gate), action, productId });
22400
22523
  this.paywallAction.emit({ action, gate, ...(productId ? { productId } : {}) });
22401
22524
  }
22402
22525
  this.paywallProductId = undefined;
@@ -22621,10 +22744,10 @@ class PlayerShellComponent {
22621
22744
  return;
22622
22745
  }
22623
22746
  const settings = this.manifestService.getManifest()?.settings;
22624
- this.trackingService.track('branch_choice', {
22625
- chapterId: chapter.id,
22626
- from: choice.edge.from,
22627
- to: choice.edge.to,
22747
+ this.trackDecision(chapter.id, choice.edge.from, choice.edge.to, {
22748
+ choiceId: `${choice.edge.from}->${choice.edge.to}`,
22749
+ choiceLabel: choice.label,
22750
+ source: 'branch',
22628
22751
  index: choice.index,
22629
22752
  });
22630
22753
  // Like a hotspot choice: page view reveals the chosen path.
@@ -22633,7 +22756,7 @@ class PlayerShellComponent {
22633
22756
  await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings), choice.edge.action);
22634
22757
  }
22635
22758
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
22636
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", initialPageId: "initialPageId", initialViewMode: "initialViewMode", shareUrl: "shareUrl", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", pageFormat: "pageFormat", showCover: "showCover", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", locationChange: "locationChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "document:fullscreenchange": "onFullscreenChange()", "window:keydown": "handleKeyboard($event)", "window:resize": "onWindowResize()" } }, providers: [CanvasCameraService], usesOnChanges: true, ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Cover (before the first panel / page) -->\n @if (coverVisible) {\n <button\n class=\"pw-cover\"\n type=\"button\"\n [attr.aria-label]=\"'navigation.start_reading' | translate\"\n (click)=\"hideCover()\">\n <img class=\"pw-cover-image\" [src]=\"coverUrl\" [alt]=\"workTitle\" fetchpriority=\"high\" />\n </button>\n }\n\n <!-- Viewport (panel / page view) -->\n @if (!coverVisible && viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [pageBackground]=\"pageBackground\"\n [panels]=\"resolvedPanels\"\n [hiddenPanelIds]=\"hiddenPanelIds\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [preloadTargets]=\"preloadTargets\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"onViewportPrevious()\"\n (navigateNext)=\"onViewportNext()\"\n (panelDoubleClick)=\"onPanelDoubleClick($event)\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\"\n (lockedPanelActivate)=\"onLockedPanelActivate($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (!coverVisible && viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"autoplaySecondsDisplay\"\n [autoplayTiming]=\"autoplayTiming\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n [fullscreen]=\"isFullscreen\"\n [fullscreenAvailable]=\"fullscreenAvailable\"\n (toggleFullscreen)=\"toggleFullscreen()\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (autoplayTimingChange)=\"onAutoplayTimingChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [currentPanelIds]=\"currentPagePanelIds\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave icon: opens the toolbar -->\n @if (!toolbarVisible) {\n <button\n class=\"floating-icon\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.show' | translate\"\n [attr.title]=\"'toolbar.show' | translate\"\n (click)=\"toggleToolbar()\">\n <img class=\"floating-icon-image\" [src]=\"panelwaveIcon\" alt=\"\" />\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [pageFormat]=\"activePageFormat\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareUrl]=\"shareLink\"\n [shareTitle]=\"workTitle\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [":host{font-family:var(--pw-font-ui, \"Barlow\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif)}:host(:fullscreen){display:block;width:100vw;height:100vh;background:#000}:host ::ng-deep :is(button,input,select,textarea){font-family:inherit}.player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.pw-cover{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;margin:0;border:0;background:#000;cursor:pointer;box-sizing:border-box}.pw-cover-image{max-width:100%;max-height:100%;object-fit:contain;box-shadow:0 8px 40px #0009}.pw-cover:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-4px}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:48px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:22%;box-shadow:0 4px 12px #0006;cursor:pointer;opacity:.85;transition:transform .2s ease,opacity .2s ease,box-shadow .2s ease}.floating-icon:hover{opacity:1;transform:scale(1.08);box-shadow:0 6px 16px #00000080}.floating-icon:active{transform:scale(1.02)}.floating-icon:focus-visible{outline:2px solid #fff;outline-offset:3px}.floating-icon-image{width:100%;height:100%;border-radius:inherit;display:block}@media (prefers-reduced-motion: reduce){.floating-icon{transition:none}}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "pageBackground", "panels", "hiddenPanelIds", "preloadTargets", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelDoubleClick", "panelFocus", "hotspotActivate", "deadClick", "lockedPanelActivate"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayTiming", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible", "fullscreen", "fullscreenAvailable"], outputs: ["toggleFullscreen", "toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "autoplayTimingChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "currentPanelIds", "coverThumbnail", "coverCurrent", "locale", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "pageFormat", "coverThumbnail", "coverCurrent", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22759
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", initialPageId: "initialPageId", initialViewMode: "initialViewMode", shareUrl: "shareUrl", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", pageFormat: "pageFormat", showCover: "showCover", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", locationChange: "locationChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "document:fullscreenchange": "onFullscreenChange()", "window:keydown": "handleKeyboard($event)", "window:resize": "onWindowResize()" } }, providers: [CanvasCameraService], usesOnChanges: true, ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Cover (before the first panel / page) -->\n @if (coverVisible) {\n <button\n class=\"pw-cover\"\n type=\"button\"\n [attr.aria-label]=\"'navigation.start_reading' | translate\"\n (click)=\"hideCover()\">\n <img class=\"pw-cover-image\" [src]=\"coverUrl\" [alt]=\"workTitle\" fetchpriority=\"high\" />\n </button>\n }\n\n <!-- Viewport (panel / page view) -->\n @if (!coverVisible && viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [pageBackground]=\"pageBackground\"\n [panels]=\"resolvedPanels\"\n [hiddenPanelIds]=\"hiddenPanelIds\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [preloadTargets]=\"preloadTargets\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"onViewportPrevious()\"\n (navigateNext)=\"onViewportNext()\"\n (panelDoubleClick)=\"onPanelDoubleClick($event)\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\"\n (lockedPanelActivate)=\"onLockedPanelActivate($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (!coverVisible && viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"autoplaySecondsDisplay\"\n [autoplayTiming]=\"autoplayTiming\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n [fullscreen]=\"isFullscreen\"\n [fullscreenAvailable]=\"fullscreenAvailable\"\n (toggleFullscreen)=\"toggleFullscreen()\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (autoplayTimingChange)=\"onAutoplayTimingChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [currentPanelIds]=\"currentPagePanelIds\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave icon: opens the toolbar -->\n @if (!toolbarVisible) {\n <button\n class=\"floating-icon\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.show' | translate\"\n [attr.title]=\"'toolbar.show' | translate\"\n (click)=\"toggleToolbar()\">\n <img class=\"floating-icon-image\" [src]=\"panelwaveIcon\" alt=\"\" />\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [pageFormat]=\"activePageFormat\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareUrl]=\"shareLink\"\n [shareTitle]=\"workTitle\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (share)=\"onShared($event)\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [":host{font-family:var(--pw-font-ui, \"Barlow\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif)}:host(:fullscreen){display:block;width:100vw;height:100vh;background:#000}:host ::ng-deep :is(button,input,select,textarea){font-family:inherit}.player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.pw-cover{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;margin:0;border:0;background:#000;cursor:pointer;box-sizing:border-box}.pw-cover-image{max-width:100%;max-height:100%;object-fit:contain;box-shadow:0 8px 40px #0009}.pw-cover:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-4px}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:48px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:22%;box-shadow:0 4px 12px #0006;cursor:pointer;opacity:.85;transition:transform .2s ease,opacity .2s ease,box-shadow .2s ease}.floating-icon:hover{opacity:1;transform:scale(1.08);box-shadow:0 6px 16px #00000080}.floating-icon:active{transform:scale(1.02)}.floating-icon:focus-visible{outline:2px solid #fff;outline-offset:3px}.floating-icon-image{width:100%;height:100%;border-radius:inherit;display:block}@media (prefers-reduced-motion: reduce){.floating-icon{transition:none}}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "pageBackground", "panels", "hiddenPanelIds", "preloadTargets", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelDoubleClick", "panelFocus", "hotspotActivate", "deadClick", "lockedPanelActivate"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayTiming", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible", "fullscreen", "fullscreenAvailable"], outputs: ["toggleFullscreen", "toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "autoplayTimingChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "currentPanelIds", "coverThumbnail", "coverCurrent", "locale", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "pageFormat", "coverThumbnail", "coverCurrent", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22637
22760
  }
22638
22761
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, decorators: [{
22639
22762
  type: Component,
@@ -22655,7 +22778,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
22655
22778
  ShareModalComponent,
22656
22779
  CommentsDrawerComponent,
22657
22780
  PwIconComponent
22658
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Cover (before the first panel / page) -->\n @if (coverVisible) {\n <button\n class=\"pw-cover\"\n type=\"button\"\n [attr.aria-label]=\"'navigation.start_reading' | translate\"\n (click)=\"hideCover()\">\n <img class=\"pw-cover-image\" [src]=\"coverUrl\" [alt]=\"workTitle\" fetchpriority=\"high\" />\n </button>\n }\n\n <!-- Viewport (panel / page view) -->\n @if (!coverVisible && viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [pageBackground]=\"pageBackground\"\n [panels]=\"resolvedPanels\"\n [hiddenPanelIds]=\"hiddenPanelIds\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [preloadTargets]=\"preloadTargets\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"onViewportPrevious()\"\n (navigateNext)=\"onViewportNext()\"\n (panelDoubleClick)=\"onPanelDoubleClick($event)\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\"\n (lockedPanelActivate)=\"onLockedPanelActivate($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (!coverVisible && viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"autoplaySecondsDisplay\"\n [autoplayTiming]=\"autoplayTiming\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n [fullscreen]=\"isFullscreen\"\n [fullscreenAvailable]=\"fullscreenAvailable\"\n (toggleFullscreen)=\"toggleFullscreen()\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (autoplayTimingChange)=\"onAutoplayTimingChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [currentPanelIds]=\"currentPagePanelIds\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave icon: opens the toolbar -->\n @if (!toolbarVisible) {\n <button\n class=\"floating-icon\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.show' | translate\"\n [attr.title]=\"'toolbar.show' | translate\"\n (click)=\"toggleToolbar()\">\n <img class=\"floating-icon-image\" [src]=\"panelwaveIcon\" alt=\"\" />\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [pageFormat]=\"activePageFormat\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareUrl]=\"shareLink\"\n [shareTitle]=\"workTitle\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [":host{font-family:var(--pw-font-ui, \"Barlow\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif)}:host(:fullscreen){display:block;width:100vw;height:100vh;background:#000}:host ::ng-deep :is(button,input,select,textarea){font-family:inherit}.player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.pw-cover{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;margin:0;border:0;background:#000;cursor:pointer;box-sizing:border-box}.pw-cover-image{max-width:100%;max-height:100%;object-fit:contain;box-shadow:0 8px 40px #0009}.pw-cover:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-4px}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:48px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:22%;box-shadow:0 4px 12px #0006;cursor:pointer;opacity:.85;transition:transform .2s ease,opacity .2s ease,box-shadow .2s ease}.floating-icon:hover{opacity:1;transform:scale(1.08);box-shadow:0 6px 16px #00000080}.floating-icon:active{transform:scale(1.02)}.floating-icon:focus-visible{outline:2px solid #fff;outline-offset:3px}.floating-icon-image{width:100%;height:100%;border-radius:inherit;display:block}@media (prefers-reduced-motion: reduce){.floating-icon{transition:none}}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
22781
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Cover (before the first panel / page) -->\n @if (coverVisible) {\n <button\n class=\"pw-cover\"\n type=\"button\"\n [attr.aria-label]=\"'navigation.start_reading' | translate\"\n (click)=\"hideCover()\">\n <img class=\"pw-cover-image\" [src]=\"coverUrl\" [alt]=\"workTitle\" fetchpriority=\"high\" />\n </button>\n }\n\n <!-- Viewport (panel / page view) -->\n @if (!coverVisible && viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [pageBackground]=\"pageBackground\"\n [panels]=\"resolvedPanels\"\n [hiddenPanelIds]=\"hiddenPanelIds\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [preloadTargets]=\"preloadTargets\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"onViewportPrevious()\"\n (navigateNext)=\"onViewportNext()\"\n (panelDoubleClick)=\"onPanelDoubleClick($event)\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\"\n (lockedPanelActivate)=\"onLockedPanelActivate($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (!coverVisible && viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"autoplaySecondsDisplay\"\n [autoplayTiming]=\"autoplayTiming\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n [fullscreen]=\"isFullscreen\"\n [fullscreenAvailable]=\"fullscreenAvailable\"\n (toggleFullscreen)=\"toggleFullscreen()\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (autoplayTimingChange)=\"onAutoplayTimingChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [currentPanelIds]=\"currentPagePanelIds\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave icon: opens the toolbar -->\n @if (!toolbarVisible) {\n <button\n class=\"floating-icon\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.show' | translate\"\n [attr.title]=\"'toolbar.show' | translate\"\n (click)=\"toggleToolbar()\">\n <img class=\"floating-icon-image\" [src]=\"panelwaveIcon\" alt=\"\" />\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"coverVisible ? undefined : getCurrentPanelId()\"\n [pageFormat]=\"activePageFormat\"\n [coverThumbnail]=\"coverThumbUrl\"\n [coverCurrent]=\"coverVisible\"\n [locale]=\"locale\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareUrl]=\"shareLink\"\n [shareTitle]=\"workTitle\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (share)=\"onShared($event)\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [":host{font-family:var(--pw-font-ui, \"Barlow\", system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif)}:host(:fullscreen){display:block;width:100vw;height:100vh;background:#000}:host ::ng-deep :is(button,input,select,textarea){font-family:inherit}.player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.pw-cover{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;margin:0;border:0;background:#000;cursor:pointer;box-sizing:border-box}.pw-cover-image{max-width:100%;max-height:100%;object-fit:contain;box-shadow:0 8px 40px #0009}.pw-cover:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-4px}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:48px;height:48px;padding:0;display:flex;align-items:center;justify-content:center;background:transparent;border:none;border-radius:22%;box-shadow:0 4px 12px #0006;cursor:pointer;opacity:.85;transition:transform .2s ease,opacity .2s ease,box-shadow .2s ease}.floating-icon:hover{opacity:1;transform:scale(1.08);box-shadow:0 6px 16px #00000080}.floating-icon:active{transform:scale(1.02)}.floating-icon:focus-visible{outline:2px solid #fff;outline-offset:3px}.floating-icon-image{width:100%;height:100%;border-radius:inherit;display:block}@media (prefers-reduced-motion: reduce){.floating-icon{transition:none}}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
22659
22782
  }], propDecorators: { manifest: [{
22660
22783
  type: Input
22661
22784
  }], manifestUrl: [{