@panelwave/player 1.0.1 → 1.2.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.
@@ -4,12 +4,11 @@ import { BehaviorSubject, throwError, of, Subject, takeUntil } from 'rxjs';
4
4
  import { HttpClient } from '@angular/common/http';
5
5
  import { map, catchError, distinctUntilChanged } from 'rxjs/operators';
6
6
  import jsonLogic from 'json-logic-js';
7
- import * as i1$1 from '@ngx-translate/core';
8
- import { TranslateService, TranslateModule } from '@ngx-translate/core';
7
+ import { TranslateService, TranslatePipe } from '@ngx-translate/core';
9
8
  import { DomSanitizer } from '@angular/platform-browser';
10
9
  import * as i1 from '@angular/common';
11
10
  import { CommonModule } from '@angular/common';
12
- import * as i1$2 from '@angular/forms';
11
+ import * as i1$1 from '@angular/forms';
13
12
  import { FormsModule } from '@angular/forms';
14
13
 
15
14
  /**
@@ -7053,12 +7052,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
7053
7052
  * free preview.
7054
7053
  * - Panel-scoped rules gate exactly their `targetPanelIds` — preview does NOT
7055
7054
  * override an explicit panel gate.
7055
+ * - Chapter-scoped rules gate only the panels of their chapter(s) (`refId`);
7056
+ * their free preview counts panels WITHIN that chapter. They take
7057
+ * precedence over a work gate for those panels.
7058
+ * - Extras-scoped rules never gate panels — they gate the extras block named
7059
+ * by `refId` (see `isExtraLocked`).
7056
7060
  * - A gated panel unlocks when the gating rule's requirement is satisfied by
7057
7061
  * the reader's entitlement snapshot.
7058
7062
  */
7059
7063
  const DEFAULT_MINIMUM_AGE = 18;
7060
- /** Does the reader's snapshot satisfy this rule's requirement? */
7064
+ /** Does the rule carry an age requirement the reader has not met? */
7065
+ function ageMissing(rule, entitlement) {
7066
+ if (rule.entitlementType !== 'age_gate' && rule.minimumAge === undefined)
7067
+ return false;
7068
+ const min = rule.minimumAge ?? DEFAULT_MINIMUM_AGE;
7069
+ return !(entitlement.ageVerified && (entitlement.age ?? 0) >= min);
7070
+ }
7071
+ /**
7072
+ * Does the reader's snapshot satisfy this rule's requirement? A rule with a
7073
+ * `minimumAge` needs the age AND its entitlement.
7074
+ */
7061
7075
  function satisfiesRule(rule, entitlement) {
7076
+ return !ageMissing(rule, entitlement) && hasEntitlement(rule, entitlement);
7077
+ }
7078
+ /** The entitlement part of a rule (the age part is `ageMissing`). */
7079
+ function hasEntitlement(rule, entitlement) {
7062
7080
  switch (rule.entitlementType) {
7063
7081
  case 'free':
7064
7082
  return true;
@@ -7074,13 +7092,17 @@ function satisfiesRule(rule, entitlement) {
7074
7092
  return entitlement.purchasedProductIds.length > 0;
7075
7093
  return required.some((id) => entitlement.purchasedProductIds.includes(id));
7076
7094
  }
7077
- case 'age_gate': {
7078
- const min = rule.minimumAge ?? DEFAULT_MINIMUM_AGE;
7079
- return entitlement.ageVerified && (entitlement.age ?? 0) >= min;
7080
- }
7095
+ case 'age_gate':
7096
+ return true; // the age is the whole requirement (checked by ageMissing)
7081
7097
  }
7082
7098
  }
7083
- function lockReasonFor(rule) {
7099
+ /**
7100
+ * Why a rule locks the reader out. The age comes first: a reader who fails
7101
+ * the age check is asked for it before being offered anything to buy.
7102
+ */
7103
+ function lockReasonFor(rule, entitlement) {
7104
+ if (ageMissing(rule, entitlement))
7105
+ return 'age_verification_required';
7084
7106
  switch (rule.entitlementType) {
7085
7107
  case 'subscription':
7086
7108
  return 'subscription_required';
@@ -7108,36 +7130,48 @@ function evaluatePanelAccess(rules, entitlement, panel) {
7108
7130
  return {
7109
7131
  panelId: panel.id,
7110
7132
  locked: !ok,
7111
- reason: ok ? null : lockReasonFor(panelRule),
7133
+ reason: ok ? null : lockReasonFor(panelRule, entitlement),
7112
7134
  appliedRuleId: panelRule.id,
7113
7135
  preview: false,
7114
7136
  };
7115
7137
  }
7116
- // 2. Work/global gate with free preview.
7138
+ // 2. Chapter gates: only the panels of the named chapter, with the free
7139
+ // preview counted within that chapter.
7140
+ const chapterRule = panel.chapterId === undefined
7141
+ ? undefined
7142
+ : active.find((r) => r.scope === 'chapter' && (r.targetIds ?? []).includes(panel.chapterId));
7143
+ if (chapterRule) {
7144
+ return gateWithPreview(chapterRule, entitlement, panel.id, panel.chapterIndex ?? Number.MAX_SAFE_INTEGER);
7145
+ }
7146
+ // 3. Work/global gate with free preview.
7117
7147
  const workRule = findWorkGateRule(active);
7118
7148
  if (workRule) {
7119
- const previewCount = workRule.previewPanelCount ?? 0;
7120
- if (panel.index < previewCount) {
7121
- return {
7122
- panelId: panel.id,
7123
- locked: false,
7124
- reason: null,
7125
- appliedRuleId: workRule.id,
7126
- preview: true,
7127
- };
7128
- }
7129
- const ok = satisfiesRule(workRule, entitlement);
7130
- return {
7131
- panelId: panel.id,
7132
- locked: !ok,
7133
- reason: ok ? null : lockReasonFor(workRule),
7134
- appliedRuleId: workRule.id,
7135
- preview: false,
7136
- };
7149
+ return gateWithPreview(workRule, entitlement, panel.id, panel.index);
7137
7150
  }
7138
- // 3. No gate at all — free content.
7151
+ // 4. No gate at all — free content.
7139
7152
  return { panelId: panel.id, locked: false, reason: null, appliedRuleId: null, preview: false };
7140
7153
  }
7154
+ /** A work/chapter gate: panels before `previewPanelCount` (by `index`) are free. */
7155
+ function gateWithPreview(rule, entitlement, panelId, index) {
7156
+ if (index < (rule.previewPanelCount ?? 0)) {
7157
+ return { panelId, locked: false, reason: null, appliedRuleId: rule.id, preview: true };
7158
+ }
7159
+ const ok = satisfiesRule(rule, entitlement);
7160
+ return {
7161
+ panelId,
7162
+ locked: !ok,
7163
+ reason: ok ? null : lockReasonFor(rule, entitlement),
7164
+ appliedRuleId: rule.id,
7165
+ preview: false,
7166
+ };
7167
+ }
7168
+ /** Is the extras block `extraId` locked by an extras-scoped rule? */
7169
+ function isExtraLocked(rules, entitlement, extraId) {
7170
+ return rules.some((r) => r.isActive &&
7171
+ r.scope === 'extras' &&
7172
+ (r.targetIds ?? []).includes(extraId) &&
7173
+ !satisfiesRule(r, entitlement));
7174
+ }
7141
7175
  /** Evaluate a whole work (panels in reading order). */
7142
7176
  function evaluateWorkAccess(rules, entitlement, panels) {
7143
7177
  return panels.map((panel) => evaluatePanelAccess(rules, entitlement, panel));
@@ -7164,8 +7198,10 @@ const KNOWN_ENTITLEMENT_TYPES = [
7164
7198
  * `minimumAge` written by the CMS exporter. Prefer the CMS names and fall
7165
7199
  * back to the originals, so a manifest from either era gates identically.
7166
7200
  *
7167
- * A rule with `minimumAge`/`ageGate` but no recognised entitlement type is an
7168
- * age gate — that is the only sane reading of a rule that names an age.
7201
+ * `ageGate` / `minimumAge` is an age check ON TOP OF the entitlement (schema
7202
+ * semantics). Only a rule that declares no entitlement at all but names an
7203
+ * age becomes a pure age gate; a custom marker plus an age stays a purchase
7204
+ * (or subscription) gate that also requires the age.
7169
7205
  */
7170
7206
  function fromManifestRule(rule, index) {
7171
7207
  const raw = rule;
@@ -7175,9 +7211,6 @@ function fromManifestRule(rule, index) {
7175
7211
  if (declared && KNOWN_ENTITLEMENT_TYPES.includes(declared)) {
7176
7212
  entitlementType = declared;
7177
7213
  }
7178
- else if (minimumAge !== undefined) {
7179
- entitlementType = 'age_gate';
7180
- }
7181
7214
  else if (declared === 'premium') {
7182
7215
  // Original format's subscription marker.
7183
7216
  entitlementType = 'subscription';
@@ -7186,10 +7219,17 @@ function fromManifestRule(rule, index) {
7186
7219
  // 'token', 'purchaseId', or any custom marker: treat as a purchase gate.
7187
7220
  entitlementType = 'purchase';
7188
7221
  }
7222
+ else if (minimumAge !== undefined) {
7223
+ // No entitlement declared, only an age: a pure age gate.
7224
+ entitlementType = 'age_gate';
7225
+ }
7189
7226
  else {
7190
7227
  entitlementType = 'free';
7191
7228
  }
7192
- const scope = raw['scope'] === 'extras' ? 'work' : raw['scope'];
7229
+ // Extras-scoped rules keep their scope: they gate an extras block, never
7230
+ // panels (mapping them onto 'work' used to lock the whole work).
7231
+ const scope = raw['scope'];
7232
+ const refId = raw['refId'];
7193
7233
  return {
7194
7234
  id: raw['id'] ?? `rule-${index}`,
7195
7235
  scope: scope ?? 'work',
@@ -7203,9 +7243,12 @@ function fromManifestRule(rule, index) {
7203
7243
  targetPanelIds: raw['targetPanelIds'] ??
7204
7244
  // A panel-scoped rule from the original format names its single target
7205
7245
  // in refId rather than a list.
7206
- (raw['scope'] === 'panel' && raw['refId']
7207
- ? [raw['refId']]
7208
- : undefined),
7246
+ (scope === 'panel' && refId ? [refId] : undefined),
7247
+ targetIds: (scope === 'chapter' || scope === 'extras') && refId ? [refId] : undefined,
7248
+ name: raw['name'],
7249
+ description: raw['description'],
7250
+ price: raw['price'],
7251
+ entitlementKey: raw['requireEntitlement'],
7209
7252
  };
7210
7253
  }
7211
7254
  /** Normalise every rule in a manifest. Returns [] when the work has no paywall. */
@@ -7215,6 +7258,14 @@ function rulesFromManifest(manifest) {
7215
7258
  return [];
7216
7259
  return rules.map(fromManifestRule);
7217
7260
  }
7261
+ /** Chapter id and within-chapter index per panel id (chapter-rule previews). */
7262
+ function chapterOrderFromManifest(manifest) {
7263
+ const out = new Map();
7264
+ for (const chapter of manifest?.chapters ?? []) {
7265
+ Object.keys(chapter.panels ?? {}).forEach((panelId, chapterIndex) => out.set(panelId, { chapterId: chapter.id, chapterIndex }));
7266
+ }
7267
+ return out;
7268
+ }
7218
7269
  /**
7219
7270
  * Global reading order for preview counting: chapters in array order, and
7220
7271
  * within a chapter the panel declaration order.
@@ -7267,7 +7318,9 @@ class PaywallService {
7267
7318
  constructor() {
7268
7319
  this.rules = [];
7269
7320
  this.readingOrder = new Map();
7321
+ this.chapterOrder = new Map();
7270
7322
  this.manifest = null;
7323
+ this.products = new Map();
7271
7324
  this.snapshotSubject = new BehaviorSubject(ANONYMOUS_READER);
7272
7325
  this.snapshot$ = this.snapshotSubject.asObservable();
7273
7326
  }
@@ -7276,6 +7329,17 @@ class PaywallService {
7276
7329
  this.manifest = manifest;
7277
7330
  this.rules = rulesFromManifest(manifest);
7278
7331
  this.readingOrder = new Map(readingOrderFromManifest(manifest).map((panelId, index) => [panelId, index]));
7332
+ this.chapterOrder = chapterOrderFromManifest(manifest);
7333
+ this.products = PaywallService.productsFromManifest(manifest);
7334
+ }
7335
+ /** `paywall.products` by id (format 1.6); the first entry wins on duplicate ids. */
7336
+ static productsFromManifest(manifest) {
7337
+ const out = new Map();
7338
+ for (const product of manifest?.paywall?.products ?? []) {
7339
+ if (product?.id && !out.has(product.id))
7340
+ out.set(product.id, product);
7341
+ }
7342
+ return out;
7279
7343
  }
7280
7344
  /** Replace what the reader owns (after sign-in, or a completed purchase). */
7281
7345
  setSnapshot(snapshot) {
@@ -7304,7 +7368,15 @@ class PaywallService {
7304
7368
  return { panelId, locked: false, reason: null, appliedRuleId: null, preview: false };
7305
7369
  }
7306
7370
  const index = this.readingOrder.get(panelId) ?? Number.MAX_SAFE_INTEGER;
7307
- return evaluatePanelAccess(this.rules, this.snapshotSubject.value, { id: panelId, index });
7371
+ return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
7372
+ id: panelId,
7373
+ index,
7374
+ ...this.chapterOrder.get(panelId),
7375
+ });
7376
+ }
7377
+ /** Is this extras block locked by an extras-scoped rule the reader does not satisfy? */
7378
+ isExtraLocked(extraId) {
7379
+ return isExtraLocked(this.rules, this.snapshotSubject.value, extraId);
7308
7380
  }
7309
7381
  /** Convenience: may the reader open this panel? */
7310
7382
  canAccess(panelId) {
@@ -7313,9 +7385,11 @@ class PaywallService {
7313
7385
  /**
7314
7386
  * Describe the gate blocking a panel, or null when it is not gated.
7315
7387
  * The shape is the player's existing `PaywallGate`, so the overlay needs
7316
- * no changes to render it.
7388
+ * no changes to render it. `locale` picks the language of product names
7389
+ * and descriptions from `paywall.products` (fallback: the work's default
7390
+ * locale).
7317
7391
  */
7318
- gateFor(panelId) {
7392
+ gateFor(panelId, locale) {
7319
7393
  const decision = this.evaluate(panelId);
7320
7394
  if (!decision.locked)
7321
7395
  return null;
@@ -7323,14 +7397,52 @@ class PaywallService {
7323
7397
  const scope = rule?.scope === 'panel' ? 'panel' : rule?.scope === 'chapter' ? 'chapter' : 'work';
7324
7398
  return {
7325
7399
  scope,
7326
- refId: rule?.scope === 'panel' ? panelId : undefined,
7400
+ refId: rule?.scope === 'panel' ? panelId : scope === 'chapter' ? this.chapterOrder.get(panelId)?.chapterId : undefined,
7327
7401
  requireEntitlement: rule?.entitlementType,
7328
7402
  reason: PaywallService.readerMessage(decision.reason),
7329
7403
  preview: rule?.previewPanelCount
7330
7404
  ? { previewPanels: rule.previewPanelCount }
7331
7405
  : undefined,
7406
+ ruleId: rule?.id,
7407
+ lockReason: decision.reason ?? undefined,
7408
+ options: rule
7409
+ ? PaywallService.purchaseOptions(rule, this.products, locale ?? this.defaultLocale, this.defaultLocale)
7410
+ : [],
7332
7411
  };
7333
7412
  }
7413
+ /**
7414
+ * What the reader can buy to pass a rule: one option per required product
7415
+ * (purchase) or per subscription tier (subscription). Without explicit
7416
+ * ids, the rule's `requireEntitlement` key names the product, else the
7417
+ * rule id. Age gates and free rules offer nothing to buy.
7418
+ *
7419
+ * Labels (format 1.6): an option whose id has an entry in
7420
+ * `paywall.products` takes that entry's localized name, description and
7421
+ * price. Otherwise it falls back to the rule's name (a single option) or
7422
+ * the bare id (several options), and to the rule's description and price.
7423
+ */
7424
+ static purchaseOptions(rule, products = new Map(), locale = 'en-US', fallbackLocale = 'en-US') {
7425
+ const type = rule.entitlementType === 'purchase' ? 'one-time' : rule.entitlementType;
7426
+ if (type !== 'one-time' && type !== 'subscription')
7427
+ return [];
7428
+ const listed = type === 'one-time' ? rule.requiredProductIds : rule.subscriptionTiers;
7429
+ const key = rule.entitlementKey !== rule.entitlementType ? rule.entitlementKey : undefined;
7430
+ const ids = listed?.length ? listed : [key ?? rule.id];
7431
+ return ids.map((productId) => {
7432
+ const product = products.get(productId);
7433
+ const name = product?.name ? resolveLocalizedString(product.name, locale, fallbackLocale) : '';
7434
+ const description = product?.description
7435
+ ? resolveLocalizedString(product.description, locale, fallbackLocale)
7436
+ : '';
7437
+ return {
7438
+ productId,
7439
+ name: name || (ids.length > 1 ? productId : rule.name ?? ''),
7440
+ description: description || rule.description,
7441
+ price: product?.price ?? rule.price,
7442
+ type,
7443
+ };
7444
+ });
7445
+ }
7334
7446
  /** The rule that gates a panel, for callers that need its price or name. */
7335
7447
  ruleFor(panelId) {
7336
7448
  const decision = this.evaluate(panelId);
@@ -7338,11 +7450,17 @@ class PaywallService {
7338
7450
  return null;
7339
7451
  return this.rules.find((r) => r.id === decision.appliedRuleId) ?? null;
7340
7452
  }
7453
+ /** The work's default locale (product-label fallback). */
7454
+ get defaultLocale() {
7455
+ return this.manifest?.meta?.default_locale ?? 'en-US';
7456
+ }
7341
7457
  /** Reset to a manifest-less, entitlement-less state. */
7342
7458
  clear() {
7343
7459
  this.manifest = null;
7344
7460
  this.rules = [];
7461
+ this.products.clear();
7345
7462
  this.readingOrder.clear();
7463
+ this.chapterOrder.clear();
7346
7464
  this.snapshotSubject.next(ANONYMOUS_READER);
7347
7465
  }
7348
7466
  /**
@@ -8648,8 +8766,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
8648
8766
  class TranslationService {
8649
8767
  constructor() {
8650
8768
  this.translate = inject(TranslateService);
8651
- // Set default language - translations will be loaded via HTTP
8652
- this.translate.setDefaultLang('en');
8769
+ // Fallback language - translations will be loaded via HTTP.
8770
+ // setFallbackLang / getCurrentLang / getFallbackLang exist in
8771
+ // ngx-translate 17 and 18 (the default* aliases are gone in 18).
8772
+ this.translate.setFallbackLang('en');
8653
8773
  this.translate.use('en');
8654
8774
  }
8655
8775
  /**
@@ -8670,7 +8790,7 @@ class TranslationService {
8670
8790
  * Get current GUI language
8671
8791
  */
8672
8792
  getCurrentLanguage() {
8673
- return this.translate.currentLang || this.translate.defaultLang || 'en';
8793
+ return this.translate.getCurrentLang() || this.translate.getFallbackLang() || 'en';
8674
8794
  }
8675
8795
  /**
8676
8796
  * Extract language code from locale code
@@ -13735,11 +13855,11 @@ class ToolbarComponent {
13735
13855
  this.close.emit();
13736
13856
  }
13737
13857
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13738
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ToolbarComponent, isStandalone: true, selector: "pw-toolbar", inputs: { locale: "locale", availableLocales: "availableLocales", viewMode: "viewMode", pageViewAvailable: "pageViewAvailable", speechEnabled: "speechEnabled", audioEnabled: "audioEnabled", sfxEnabled: "sfxEnabled", autoplayEnabled: "autoplayEnabled", secondsPerPanel: "secondsPerPanel", autoplayProgress: "autoplayProgress", thumbnailsVisible: "thumbnailsVisible", hasAlternatives: "hasAlternatives", hasBranches: "hasBranches", liked: "liked", bookmarked: "bookmarked", showSocial: "showSocial", visible: "visible" }, outputs: { toggleView: "toggleView", localeChange: "localeChange", toggleSpeech: "toggleSpeech", toggleAudio: "toggleAudio", toggleSfx: "toggleSfx", toggleAutoplay: "toggleAutoplay", secondsPerPanelChange: "secondsPerPanelChange", toggleThumbnails: "toggleThumbnails", openToc: "openToc", openSettings: "openSettings", openCharacters: "openCharacters", cycleAlternative: "cycleAlternative", showBranches: "showBranches", openExtras: "openExtras", like: "like", bookmark: "bookmark", share: "share", openComments: "openComments", close: "close", openLanguage: "openLanguage" }, ngImport: i0, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13858
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ToolbarComponent, isStandalone: true, selector: "pw-toolbar", inputs: { locale: "locale", availableLocales: "availableLocales", viewMode: "viewMode", pageViewAvailable: "pageViewAvailable", speechEnabled: "speechEnabled", audioEnabled: "audioEnabled", sfxEnabled: "sfxEnabled", autoplayEnabled: "autoplayEnabled", secondsPerPanel: "secondsPerPanel", autoplayProgress: "autoplayProgress", thumbnailsVisible: "thumbnailsVisible", hasAlternatives: "hasAlternatives", hasBranches: "hasBranches", liked: "liked", bookmarked: "bookmarked", showSocial: "showSocial", visible: "visible" }, outputs: { toggleView: "toggleView", localeChange: "localeChange", toggleSpeech: "toggleSpeech", toggleAudio: "toggleAudio", toggleSfx: "toggleSfx", toggleAutoplay: "toggleAutoplay", secondsPerPanelChange: "secondsPerPanelChange", toggleThumbnails: "toggleThumbnails", openToc: "openToc", openSettings: "openSettings", openCharacters: "openCharacters", cycleAlternative: "cycleAlternative", showBranches: "showBranches", openExtras: "openExtras", like: "like", bookmark: "bookmark", share: "share", openComments: "openComments", close: "close", openLanguage: "openLanguage" }, ngImport: i0, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13739
13859
  }
13740
13860
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ToolbarComponent, decorators: [{
13741
13861
  type: Component,
13742
- args: [{ selector: 'pw-toolbar', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"] }]
13862
+ args: [{ selector: 'pw-toolbar', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"] }]
13743
13863
  }], propDecorators: { locale: [{
13744
13864
  type: Input
13745
13865
  }], availableLocales: [{
@@ -13870,11 +13990,11 @@ class LanguageModalComponent {
13870
13990
  }
13871
13991
  }
13872
13992
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LanguageModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13873
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LanguageModalComponent, isStandalone: true, selector: "pw-language-modal", inputs: { visible: "visible", currentLocale: "currentLocale", availableLocales: "availableLocales" }, outputs: { closeModal: "closeModal", localeSelected: "localeSelected" }, ngImport: i0, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13993
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LanguageModalComponent, isStandalone: true, selector: "pw-language-modal", inputs: { visible: "visible", currentLocale: "currentLocale", availableLocales: "availableLocales" }, outputs: { closeModal: "closeModal", localeSelected: "localeSelected" }, ngImport: i0, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13874
13994
  }
13875
13995
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LanguageModalComponent, decorators: [{
13876
13996
  type: Component,
13877
- args: [{ selector: 'pw-language-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"] }]
13997
+ args: [{ selector: 'pw-language-modal', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"] }]
13878
13998
  }], propDecorators: { visible: [{
13879
13999
  type: Input
13880
14000
  }], currentLocale: [{
@@ -14540,11 +14660,11 @@ class TocOverlayComponent {
14540
14660
  return this.filteredChapters[this.selectedChapterIndex];
14541
14661
  }
14542
14662
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TocOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
14543
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: TocOverlayComponent, isStandalone: true, selector: "pw-toc-overlay", inputs: { manifest: "manifest", chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14663
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: TocOverlayComponent, isStandalone: true, selector: "pw-toc-overlay", inputs: { manifest: "manifest", chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14544
14664
  }
14545
14665
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TocOverlayComponent, decorators: [{
14546
14666
  type: Component,
14547
- args: [{ selector: 'pw-toc-overlay', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"] }]
14667
+ args: [{ selector: 'pw-toc-overlay', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"] }]
14548
14668
  }], propDecorators: { manifest: [{
14549
14669
  type: Input
14550
14670
  }], chapters: [{
@@ -14568,6 +14688,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
14568
14688
  args: ['window:keydown', ['$event']]
14569
14689
  }] } });
14570
14690
 
14691
+ /**
14692
+ * Translate a built-in UI string. Falls back to the given English text (with
14693
+ * `{{param}}` interpolation) when no TranslateService is provided or the key
14694
+ * is not loaded, so a public component embedded without ngx-translate never
14695
+ * shows a raw key.
14696
+ */
14697
+ function uiText(translate, key, english, params) {
14698
+ const value = translate?.instant(key, params);
14699
+ if (typeof value === 'string' && value !== key) {
14700
+ return value;
14701
+ }
14702
+ return (english ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
14703
+ }
14704
+
14571
14705
  /**
14572
14706
  * Paywall Overlay Component
14573
14707
  * Displays paywall gate UI with purchase options
@@ -14594,6 +14728,10 @@ const PAYWALL_TEXT_EN = {
14594
14728
  'paywall.go_back': 'Go Back',
14595
14729
  'paywall.maybe_later': 'Maybe Later',
14596
14730
  'paywall.secure_payment': 'Secure payment processing',
14731
+ 'paywall.reason_subscription_required': 'This part of the story is included with a subscription.',
14732
+ 'paywall.reason_purchase_required': 'This part of the story is available to buy.',
14733
+ 'paywall.reason_age_verification_required': 'This content is age-restricted. Confirm your age to continue.',
14734
+ 'paywall.reason_entitlement_required': 'This content requires an entitlement to access.',
14597
14735
  };
14598
14736
  /**
14599
14737
  * Paywall Overlay Component
@@ -14672,17 +14810,24 @@ class PaywallOverlayComponent {
14672
14810
  if (this.message) {
14673
14811
  return this.getLocalizedString(this.message);
14674
14812
  }
14813
+ // A known lock reason is translated; otherwise the gate's own sentence.
14814
+ const key = 'paywall.reason_' + this.gate?.lockReason;
14815
+ const translated = this.t(key);
14816
+ if (translated !== key) {
14817
+ return translated;
14818
+ }
14675
14819
  if (this.gate?.reason) {
14676
14820
  return this.gate.reason;
14677
14821
  }
14678
14822
  return this.t('paywall.message_default');
14679
14823
  }
14680
14824
  /**
14681
- * Handle purchase option selection
14825
+ * Handle purchase option selection: `purchase` carries the product id,
14826
+ * `action` is 'subscribe' for a subscription option, else 'purchase'.
14682
14827
  */
14683
- onPurchaseOption(productId) {
14684
- this.purchase.emit(productId);
14685
- this.action.emit('purchase');
14828
+ onPurchaseOption(option) {
14829
+ this.purchase.emit(option.productId);
14830
+ this.action.emit(option.type === 'subscription' ? 'subscribe' : 'purchase');
14686
14831
  }
14687
14832
  /**
14688
14833
  * Handle login action
@@ -14745,18 +14890,14 @@ class PaywallOverlayComponent {
14745
14890
  * Manifest-provided texts (title/message/gate.reason) bypass this.
14746
14891
  */
14747
14892
  t(key, params) {
14748
- const value = this.translate?.instant(key, params);
14749
- if (typeof value === 'string' && value !== key) {
14750
- return value;
14751
- }
14752
- return (PAYWALL_TEXT_EN[key] ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
14893
+ return uiText(this.translate, key, PAYWALL_TEXT_EN[key], params);
14753
14894
  }
14754
14895
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
14755
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option.productId)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name }}</span>\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin && !purchaseOptions.length) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14896
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14756
14897
  }
14757
14898
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, decorators: [{
14758
14899
  type: Component,
14759
- args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option.productId)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name }}</span>\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin && !purchaseOptions.length) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"] }]
14900
+ args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"] }]
14760
14901
  }], propDecorators: { visible: [{
14761
14902
  type: Input
14762
14903
  }], gate: [{
@@ -15079,11 +15220,11 @@ class SettingsModalComponent {
15079
15220
  return variable.max;
15080
15221
  }
15081
15222
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SettingsModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15082
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: SettingsModalComponent, isStandalone: true, selector: "pw-settings-modal", inputs: { preferences: "preferences", availableLocales: "availableLocales", locale: "locale", variables: "variables", variableValues: "variableValues", visible: "visible" }, outputs: { preferencesChange: "preferencesChange", localeChange: "localeChange", variableChange: "variableChange", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15223
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: SettingsModalComponent, isStandalone: true, selector: "pw-settings-modal", inputs: { preferences: "preferences", availableLocales: "availableLocales", locale: "locale", variables: "variables", variableValues: "variableValues", visible: "visible" }, outputs: { preferencesChange: "preferencesChange", localeChange: "localeChange", variableChange: "variableChange", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15083
15224
  }
15084
15225
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SettingsModalComponent, decorators: [{
15085
15226
  type: Component,
15086
- args: [{ selector: 'pw-settings-modal', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"] }]
15227
+ args: [{ selector: 'pw-settings-modal', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"] }]
15087
15228
  }], propDecorators: { preferences: [{
15088
15229
  type: Input
15089
15230
  }], availableLocales: [{
@@ -15404,11 +15545,11 @@ class CharacterRosterComponent {
15404
15545
  }
15405
15546
  }
15406
15547
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CharacterRosterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15407
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CharacterRosterComponent, isStandalone: true, selector: "pw-character-roster", inputs: { characters: "characters", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { characterSelect: "characterSelect", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15548
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CharacterRosterComponent, isStandalone: true, selector: "pw-character-roster", inputs: { characters: "characters", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { characterSelect: "characterSelect", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15408
15549
  }
15409
15550
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CharacterRosterComponent, decorators: [{
15410
15551
  type: Component,
15411
- args: [{ selector: 'pw-character-roster', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"] }]
15552
+ args: [{ selector: 'pw-character-roster', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"] }]
15412
15553
  }], propDecorators: { characters: [{
15413
15554
  type: Input
15414
15555
  }], locale: [{
@@ -15673,11 +15814,11 @@ class ExtrasViewerComponent {
15673
15814
  }
15674
15815
  }
15675
15816
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ExtrasViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15676
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ExtrasViewerComponent, isStandalone: true, selector: "pw-extras-viewer", inputs: { extras: "extras", initialExtraId: "initialExtraId", locale: "locale", baseUrl: "baseUrl", visible: "visible", hasPremiumAccess: "hasPremiumAccess" }, outputs: { purchase: "purchase", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15817
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ExtrasViewerComponent, isStandalone: true, selector: "pw-extras-viewer", inputs: { extras: "extras", initialExtraId: "initialExtraId", locale: "locale", baseUrl: "baseUrl", visible: "visible", hasPremiumAccess: "hasPremiumAccess" }, outputs: { purchase: "purchase", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15677
15818
  }
15678
15819
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ExtrasViewerComponent, decorators: [{
15679
15820
  type: Component,
15680
- args: [{ selector: 'pw-extras-viewer', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"] }]
15821
+ args: [{ selector: 'pw-extras-viewer', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"] }]
15681
15822
  }], propDecorators: { extras: [{
15682
15823
  type: Input
15683
15824
  }], initialExtraId: [{
@@ -15765,12 +15906,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
15765
15906
  * Age Gate Component
15766
15907
  * Displays age verification UI for restricted content
15767
15908
  */
15909
+ /**
15910
+ * Built-in English UI strings (keys under `age_gate.`), used when no
15911
+ * TranslateService is provided or a key is not loaded. Must match the
15912
+ * "age_gate" section of assets/i18n/en.json.
15913
+ */
15914
+ const AGE_GATE_TEXT_EN = {
15915
+ title: 'Age Verification Required',
15916
+ close: 'Close',
15917
+ warning: 'This content is restricted to users {{age}} years of age or older.',
15918
+ prompt: 'Please enter your birth date:',
15919
+ month: 'Month',
15920
+ day: 'Day',
15921
+ year: 'Year',
15922
+ verify: 'Verify Age',
15923
+ privacy: 'Your information is private and will not be stored.',
15924
+ error_incomplete: 'Please enter your complete birth date.',
15925
+ error_month: 'Please enter a valid month (1-12).',
15926
+ error_day: 'Please enter a valid day (1-{{max}}).',
15927
+ error_year: 'Please enter a valid year (1900-{{max}}).',
15928
+ error_too_young: 'You must be at least {{age}} years old to access this content.',
15929
+ };
15768
15930
  /**
15769
15931
  * Age Gate Component
15770
15932
  * Modal overlay for age verification
15771
15933
  */
15772
15934
  class AgeGateComponent {
15773
15935
  constructor() {
15936
+ /** Optional: the gate is a public component and works (in English) without ngx-translate. */
15937
+ this.translate = inject(TranslateService, { optional: true });
15774
15938
  /**
15775
15939
  * Visibility state
15776
15940
  */
@@ -15821,7 +15985,11 @@ class AgeGateComponent {
15821
15985
  if (this.warningMessage) {
15822
15986
  return this.warningMessage;
15823
15987
  }
15824
- return `This content is restricted to users ${this.minimumAge} years of age or older.`;
15988
+ return this.t('warning', { age: this.minimumAge });
15989
+ }
15990
+ /** Translate an `age_gate.*` UI string (English fallback). */
15991
+ t(key, params) {
15992
+ return uiText(this.translate, 'age_gate.' + key, AGE_GATE_TEXT_EN[key], params);
15825
15993
  }
15826
15994
  /**
15827
15995
  * Handle verification submit
@@ -15830,7 +15998,7 @@ class AgeGateComponent {
15830
15998
  this.errorMessage = '';
15831
15999
  // Validate inputs
15832
16000
  if (!this.birthMonth || !this.birthDay || !this.birthYear) {
15833
- this.errorMessage = 'Please enter your complete birth date.';
16001
+ this.errorMessage = this.t('error_incomplete');
15834
16002
  return;
15835
16003
  }
15836
16004
  const month = parseInt(this.birthMonth, 10);
@@ -15838,30 +16006,30 @@ class AgeGateComponent {
15838
16006
  const year = parseInt(this.birthYear, 10);
15839
16007
  // Validate ranges
15840
16008
  if (month < 1 || month > 12) {
15841
- this.errorMessage = 'Please enter a valid month (1-12).';
16009
+ this.errorMessage = this.t('error_month');
15842
16010
  return;
15843
16011
  }
15844
16012
  if (day < 1 || day > 31) {
15845
- this.errorMessage = 'Please enter a valid day (1-31).';
16013
+ this.errorMessage = this.t('error_day', { max: 31 });
15846
16014
  return;
15847
16015
  }
15848
16016
  const currentYear = new Date().getFullYear();
15849
16017
  if (year < 1900 || year > currentYear) {
15850
- this.errorMessage = `Please enter a valid year (1900-${currentYear}).`;
16018
+ this.errorMessage = this.t('error_year', { max: currentYear });
15851
16019
  return;
15852
16020
  }
15853
16021
  // Reject calendar-impossible dates (31 Apr, 30 Feb, 29 Feb in non-leap
15854
16022
  // years): new Date() would silently roll them over into the next month.
15855
16023
  const daysInMonth = new Date(year, month, 0).getDate();
15856
16024
  if (day > daysInMonth) {
15857
- this.errorMessage = `Please enter a valid day (1-${daysInMonth}).`;
16025
+ this.errorMessage = this.t('error_day', { max: daysInMonth });
15858
16026
  return;
15859
16027
  }
15860
16028
  // Calculate age
15861
16029
  const birthDate = new Date(year, month - 1, day);
15862
16030
  const age = this.calculateAge(birthDate);
15863
16031
  if (age < this.minimumAge) {
15864
- this.errorMessage = `You must be at least ${this.minimumAge} years old to access this content.`;
16032
+ this.errorMessage = this.t('error_too_young', { age: this.minimumAge });
15865
16033
  this.verify.emit({
15866
16034
  verified: false,
15867
16035
  age,
@@ -15906,17 +16074,26 @@ class AgeGateComponent {
15906
16074
  return age;
15907
16075
  }
15908
16076
  /**
15909
- * Get month options
16077
+ * Month options, named in the current locale (Intl; cached per locale).
15910
16078
  */
15911
16079
  getMonthOptions() {
15912
- const months = [
15913
- 'January', 'February', 'March', 'April', 'May', 'June',
15914
- 'July', 'August', 'September', 'October', 'November', 'December'
15915
- ];
15916
- return months.map((month, index) => ({
15917
- value: String(index + 1),
15918
- label: month,
15919
- }));
16080
+ if (this.monthCache?.locale !== this.locale) {
16081
+ let format;
16082
+ try {
16083
+ format = new Intl.DateTimeFormat(this.locale, { month: 'long', timeZone: 'UTC' });
16084
+ }
16085
+ catch {
16086
+ format = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' });
16087
+ }
16088
+ this.monthCache = {
16089
+ locale: this.locale,
16090
+ options: Array.from({ length: 12 }, (_, index) => ({
16091
+ value: String(index + 1),
16092
+ label: format.format(Date.UTC(2000, index, 1)),
16093
+ })),
16094
+ };
16095
+ }
16096
+ return this.monthCache.options;
15920
16097
  }
15921
16098
  /**
15922
16099
  * Get day options
@@ -15936,11 +16113,11 @@ class AgeGateComponent {
15936
16113
  return years;
15937
16114
  }
15938
16115
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15939
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Age Verification\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">Age Verification Required</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">Please enter your birth date:</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">Month</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">Day</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">Year</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n Verify Age\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Your information is private and will not be stored.</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16116
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15940
16117
  }
15941
16118
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, decorators: [{
15942
16119
  type: Component,
15943
- args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Age Verification\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">Age Verification Required</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">Please enter your birth date:</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">Month</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">Day</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">Year</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n Verify Age\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>Your information is private and will not be stored.</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"] }]
16120
+ args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"] }]
15944
16121
  }], propDecorators: { visible: [{
15945
16122
  type: Input
15946
16123
  }], minimumAge: [{
@@ -15967,8 +16144,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
15967
16144
  * priority. Opened from the toolbar's "Choices" button; follows the shell's
15968
16145
  * modal contract (visible input + close output).
15969
16146
  */
16147
+ /** English fallback for the `branch_chooser.*` keys (see assets/i18n/en.json). */
16148
+ const BRANCH_TEXT_EN = {
16149
+ title: 'Choose your path',
16150
+ close: 'Close',
16151
+ option: 'Option {{n}}',
16152
+ };
15970
16153
  class BranchChooserComponent {
15971
16154
  constructor() {
16155
+ this.translate = inject(TranslateService, { optional: true });
15972
16156
  /** Visible state */
15973
16157
  this.visible = false;
15974
16158
  /** Current locale (labels are already resolved by the shell). */
@@ -15980,6 +16164,10 @@ class BranchChooserComponent {
15980
16164
  /** Close requested without choosing. */
15981
16165
  this.close = new EventEmitter();
15982
16166
  }
16167
+ /** Translate a `branch_chooser.*` UI string (English fallback). */
16168
+ t(key, params) {
16169
+ return uiText(this.translate, 'branch_chooser.' + key, BRANCH_TEXT_EN[key], params);
16170
+ }
15983
16171
  onEscape() {
15984
16172
  if (this.visible) {
15985
16173
  this.close.emit();
@@ -15994,11 +16182,11 @@ class BranchChooserComponent {
15994
16182
  return choice.index;
15995
16183
  }
15996
16184
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15997
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">Choose your path</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16185
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15998
16186
  }
15999
16187
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, decorators: [{
16000
16188
  type: Component,
16001
- args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">Choose your path</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"] }]
16189
+ args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"] }]
16002
16190
  }], propDecorators: { visible: [{
16003
16191
  type: Input
16004
16192
  }], locale: [{
@@ -16314,11 +16502,11 @@ class ShareModalComponent {
16314
16502
  }
16315
16503
  }
16316
16504
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ShareModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
16317
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ShareModalComponent, isStandalone: true, selector: "pw-share-modal", inputs: { shareUrl: "shareUrl", shareTitle: "shareTitle", shareDescription: "shareDescription", visible: "visible", shareable: "shareable" }, outputs: { close: "close", share: "share" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16505
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ShareModalComponent, isStandalone: true, selector: "pw-share-modal", inputs: { shareUrl: "shareUrl", shareTitle: "shareTitle", shareDescription: "shareDescription", visible: "visible", shareable: "shareable" }, outputs: { close: "close", share: "share" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16318
16506
  }
16319
16507
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ShareModalComponent, decorators: [{
16320
16508
  type: Component,
16321
- args: [{ selector: 'pw-share-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"] }]
16509
+ args: [{ selector: 'pw-share-modal', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"] }]
16322
16510
  }], propDecorators: { shareUrl: [{
16323
16511
  type: Input
16324
16512
  }], shareTitle: [{
@@ -16557,11 +16745,11 @@ class CommentsDrawerComponent {
16557
16745
  }
16558
16746
  }
16559
16747
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CommentsDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
16560
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CommentsDrawerComponent, isStandalone: true, selector: "pw-comments-drawer", inputs: { comments: "comments", visible: "visible", loading: "loading", currentPage: "currentPage", totalPages: "totalPages", hasMore: "hasMore", isAuthenticated: "isAuthenticated" }, outputs: { postComment: "postComment", loadMore: "loadMore", likeComment: "likeComment", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16748
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CommentsDrawerComponent, isStandalone: true, selector: "pw-comments-drawer", inputs: { comments: "comments", visible: "visible", loading: "loading", currentPage: "currentPage", totalPages: "totalPages", hasMore: "hasMore", isAuthenticated: "isAuthenticated" }, outputs: { postComment: "postComment", loadMore: "loadMore", likeComment: "likeComment", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
16561
16749
  }
16562
16750
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CommentsDrawerComponent, decorators: [{
16563
16751
  type: Component,
16564
- args: [{ selector: 'pw-comments-drawer', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"] }]
16752
+ args: [{ selector: 'pw-comments-drawer', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"] }]
16565
16753
  }], propDecorators: { comments: [{
16566
16754
  type: Input
16567
16755
  }], visible: [{
@@ -16607,8 +16795,11 @@ function createTranslateLoader(http) {
16607
16795
  class PlayerShellComponent {
16608
16796
  constructor() {
16609
16797
  /**
16610
- * Emitted when a reader acts on the paywall overlay. The host owns
16611
- * checkout — the player never talks to a payment provider itself.
16798
+ * Emitted when a reader acts on the paywall overlay: `purchase` /
16799
+ * `subscribe` (with the chosen option's `productId` — a product id or a
16800
+ * subscription tier), `login` or `dismiss`. The host owns checkout — the
16801
+ * player never talks to a payment provider itself; call
16802
+ * `refreshEntitlements()` (or pass a new `entitlementSnapshot`) afterwards.
16612
16803
  */
16613
16804
  this.paywallAction = new EventEmitter();
16614
16805
  /**
@@ -16669,7 +16860,8 @@ class PlayerShellComponent {
16669
16860
  */
16670
16861
  this.ready = new EventEmitter();
16671
16862
  /**
16672
- * Panel changed
16863
+ * The current panel changed. Carries the panel's id (`panelId`, its key in
16864
+ * `chapter.panels`) and the id of the panel before it (`previousPanelId`).
16673
16865
  */
16674
16866
  this.panelChange = new EventEmitter();
16675
16867
  /**
@@ -16890,6 +17082,10 @@ class PlayerShellComponent {
16890
17082
  this.translationService = inject(TranslationService);
16891
17083
  this.canvasCamera = inject(CanvasCameraService);
16892
17084
  this.paywallService = inject(PaywallService);
17085
+ /** ngOnInit ran: later input changes are live updates, not initial values. */
17086
+ this.initialized = false;
17087
+ /** The state subscriptions are set up once per instance, not per (re)load. */
17088
+ this.stateSubscribed = false;
16893
17089
  }
16894
17090
  /**
16895
17091
  * Effective reduced-motion state for every transition the shell drives
@@ -16903,6 +17099,7 @@ class PlayerShellComponent {
16903
17099
  * Initialize component
16904
17100
  */
16905
17101
  ngOnInit() {
17102
+ this.initialized = true;
16906
17103
  this.toolbarVisible = this.showToolbar;
16907
17104
  this.watchReducedMotion();
16908
17105
  this.subscribeToVideoSignals();
@@ -16913,6 +17110,101 @@ class PlayerShellComponent {
16913
17110
  window.addEventListener('pagehide', this.onPageHide);
16914
17111
  }
16915
17112
  }
17113
+ /**
17114
+ * Inputs changed after init. The first round (before ngOnInit) is skipped:
17115
+ * ngOnInit reads every input itself, so nothing loads twice.
17116
+ */
17117
+ ngOnChanges(changes) {
17118
+ if (!this.initialized) {
17119
+ return;
17120
+ }
17121
+ if (changes['manifest'] || changes['manifestUrl']) {
17122
+ // A new work: reload re-reads every other input as well.
17123
+ void this.reload();
17124
+ return;
17125
+ }
17126
+ if (changes['locale']) {
17127
+ this.applyLocale();
17128
+ }
17129
+ if (changes['entitlementSnapshot']) {
17130
+ if (!this.entitlementSnapshot && !this.httpEntitlement) {
17131
+ this.paywallService.setSnapshot(null);
17132
+ }
17133
+ this.refreshEntitlements(this.entitlementSnapshot).catch(() => undefined);
17134
+ }
17135
+ if (changes['viewModeOverride']) {
17136
+ this.applyViewModeOverride();
17137
+ }
17138
+ if (changes['showToolbar']) {
17139
+ this.toolbarVisible = this.showToolbar;
17140
+ }
17141
+ if (changes['autoplay'] && this.autoplay !== this.autoplayEnabled) {
17142
+ this.onToggleAutoplay();
17143
+ }
17144
+ else if ((changes['secondsPerPanel'] || changes['reducedMotion']) && this.autoplayEnabled) {
17145
+ // Re-arm with the new timing / video start modes.
17146
+ this.startAutoplay();
17147
+ }
17148
+ this.cdr.markForCheck();
17149
+ }
17150
+ /**
17151
+ * Reload the work from the current `manifest` / `manifestUrl` inputs
17152
+ * (also the error screen's Retry). Story state of the previous load —
17153
+ * position, variables except persistent ones, open overlays, autoplay —
17154
+ * is reset; the analytics session of the previous work is closed.
17155
+ */
17156
+ async reload() {
17157
+ this.stopAutoplay();
17158
+ this.autoplayEnabled = false;
17159
+ this.videoSequencer.reset();
17160
+ this.panelAudio.stopAll();
17161
+ this.endAnalyticsSession();
17162
+ this.analyticsSessionStarted = this.analyticsSessionEnded = this.workCompleteTracked = false;
17163
+ this.lastTrackedPanelId = undefined;
17164
+ this.lastPanelChangeId = undefined;
17165
+ this.closePaywall();
17166
+ this.closeAgeGate();
17167
+ this.tocVisible = this.settingsVisible = this.languageModalVisible = this.charactersVisible = false;
17168
+ this.extrasVisible = this.shareVisible = this.commentsVisible = this.thumbnailsVisible = false;
17169
+ this.actionModalVisible = this.branchChooserVisible = false;
17170
+ this.variantOverrides.clear();
17171
+ this.variableStore.setDefinitions([]);
17172
+ for (const scope of ['global', 'chapter', 'page', 'session']) {
17173
+ this.variableStore.resetScope(scope);
17174
+ }
17175
+ this.currentChapter = this.currentPanel = this.currentPage = this.effectivePanel = undefined;
17176
+ this.viewMode = 'panel';
17177
+ this.visitedPanelIds = [];
17178
+ this.httpEntitlement = undefined;
17179
+ this.hasError = false;
17180
+ this.errorMessage = '';
17181
+ await this.initializePlayer();
17182
+ }
17183
+ /** Content locale + GUI language follow `locale`. */
17184
+ applyLocale() {
17185
+ this.playerState.setLocale(this.locale);
17186
+ this.translationService.setLanguage(this.locale);
17187
+ }
17188
+ /** Apply a changed `viewModeOverride` to the running view. */
17189
+ applyViewModeOverride() {
17190
+ const before = this.viewMode;
17191
+ if (this.viewModeOverride === 'panel' && this.viewMode === 'page') {
17192
+ this.viewMode = 'panel';
17193
+ this.videoSequencer.reset();
17194
+ }
17195
+ else {
17196
+ this.updateCanvasAvailability();
17197
+ }
17198
+ if (this.viewMode === before) {
17199
+ return;
17200
+ }
17201
+ if (this.viewMode === 'canvas') {
17202
+ this.jumpCameraToCurrentPanel();
17203
+ }
17204
+ if (this.autoplayEnabled) {
17205
+ this.startAutoplay();
17206
+ }
17207
+ }
16916
17208
  /**
16917
17209
  * Cleanup on destroy
16918
17210
  */
@@ -17029,8 +17321,7 @@ class PlayerShellComponent {
17029
17321
  this.variableStore.seed(context);
17030
17322
  }
17031
17323
  // Set initial locale and configure translations
17032
- this.playerState.setLocale(this.locale);
17033
- this.translationService.setLanguage(this.locale);
17324
+ this.applyLocale();
17034
17325
  // Check if page view is available
17035
17326
  const loadedManifest = this.manifestService.getManifest();
17036
17327
  if (loadedManifest?.chapters) {
@@ -17065,12 +17356,19 @@ class PlayerShellComponent {
17065
17356
  }
17066
17357
  // Subscribe to state changes
17067
17358
  this.subscribeToStateChanges();
17359
+ // The host's `autoplay` input decides at start; the reader's toolbar
17360
+ // toggle takes over afterwards.
17361
+ if (this.autoplay && !this.autoplayEnabled) {
17362
+ this.onToggleAutoplay();
17363
+ }
17068
17364
  this.loading = false;
17069
17365
  this.ready.emit();
17070
17366
  }
17071
17367
  catch (err) {
17072
17368
  this.handleError(err);
17073
17369
  }
17370
+ // Async completion (e.g. a reload from ngOnChanges): repaint the OnPush view.
17371
+ this.cdr.markForCheck();
17074
17372
  }
17075
17373
  /**
17076
17374
  * Configure the TrackingService from the manifest's `tracking` section:
@@ -17175,9 +17473,7 @@ class PlayerShellComponent {
17175
17473
  * one-time `work_complete` signal.
17176
17474
  */
17177
17475
  trackPanelView(panel, chapter) {
17178
- // Panels don't carry their own id — resolve it from the chapter's map
17179
- // (same identity lookup as getCurrentPanelId).
17180
- const panelId = Object.entries(chapter.panels ?? {}).find(([, p]) => p === panel)?.[0];
17476
+ const panelId = this.panelIdOf(panel, chapter);
17181
17477
  if (!panelId || panelId === this.lastTrackedPanelId) {
17182
17478
  return;
17183
17479
  }
@@ -17195,6 +17491,14 @@ class PlayerShellComponent {
17195
17491
  });
17196
17492
  }
17197
17493
  }
17494
+ /**
17495
+ * A panel's id: its key in `chapter.panels`. Panels don't reliably carry
17496
+ * their own id (`Panel.id` is optional in the format), so resolve it by
17497
+ * identity, as getCurrentPanelId() does.
17498
+ */
17499
+ panelIdOf(panel, chapter) {
17500
+ return Object.entries(chapter.panels ?? {}).find(([, p]) => p === panel)?.[0];
17501
+ }
17198
17502
  /** An end panel (no outgoing edges) of the manifest's last chapter. */
17199
17503
  isEndOfWork(panelId, chapter) {
17200
17504
  const chapters = this.manifestService.getManifest()?.chapters ?? [];
@@ -17225,6 +17529,10 @@ class PlayerShellComponent {
17225
17529
  * Subscribe to state changes
17226
17530
  */
17227
17531
  subscribeToStateChanges() {
17532
+ if (this.stateSubscribed) {
17533
+ return;
17534
+ }
17535
+ this.stateSubscribed = true;
17228
17536
  // Listen to panel changes
17229
17537
  this.playerState.currentPanel$
17230
17538
  .pipe(takeUntil(this.destroy$))
@@ -17234,7 +17542,16 @@ class PlayerShellComponent {
17234
17542
  // Resolve variants before emitting so consumers (and the
17235
17543
  // tracked event) see the effective panel state.
17236
17544
  this.refreshResolvedPanels();
17237
- this.panelChange.emit({ panel, chapter: this.currentChapter });
17545
+ // Panels are keyed by id in chapter.panels; the lookup always hits
17546
+ // for a panel the shell navigated to ('' only guards the type).
17547
+ const panelId = this.panelIdOf(panel, this.currentChapter) ?? '';
17548
+ this.panelChange.emit({
17549
+ panel,
17550
+ chapter: this.currentChapter,
17551
+ panelId,
17552
+ previousPanelId: this.lastPanelChangeId,
17553
+ });
17554
+ this.lastPanelChangeId = panelId || undefined;
17238
17555
  this.trackPanelView(panel, this.currentChapter);
17239
17556
  }
17240
17557
  });
@@ -17294,8 +17611,11 @@ class PlayerShellComponent {
17294
17611
  * @param transition - Optional transition to render for this panel change
17295
17612
  * (already resolved against the outputPresets default). Omitted for
17296
17613
  * non-adjacent jumps (TOC, initial load), which swap instantly.
17614
+ * @param mutations - Variable mutations of the traversed edge. Applied only
17615
+ * once the navigation is allowed (after the paywall / age gate), so a
17616
+ * blocked attempt never changes story state.
17297
17617
  */
17298
- async navigateToPanel(chapterId, panelId, transition, cameraMove) {
17618
+ async navigateToPanel(chapterId, panelId, transition, cameraMove, mutations) {
17299
17619
  try {
17300
17620
  this.viewportTransition = transition ?? null;
17301
17621
  // Check entitlement. A host-supplied adapter still wins — it is the
@@ -17304,10 +17624,11 @@ class PlayerShellComponent {
17304
17624
  if (this.entitlementAdapter) {
17305
17625
  const hasAccess = await this.entitlementAdapter.hasAccess(panelId);
17306
17626
  if (!hasAccess) {
17307
- this.openPaywall(this.paywallService.gateFor(panelId) ?? {
17627
+ this.openPaywall(this.paywallService.gateFor(panelId, this.locale) ?? {
17308
17628
  scope: 'panel',
17309
17629
  refId: panelId,
17310
- reason: 'This content requires an entitlement to access.',
17630
+ reason: PaywallService.readerMessage(null),
17631
+ lockReason: 'entitlement_required',
17311
17632
  });
17312
17633
  return;
17313
17634
  }
@@ -17318,10 +17639,10 @@ class PlayerShellComponent {
17318
17639
  // the last panel they were entitled to see.
17319
17640
  if (this.paywallService.evaluate(panelId).reason === 'age_verification_required') {
17320
17641
  // An age gate is answered in-player (birth date), not by checkout.
17321
- this.openAgeGate(chapterId, panelId, transition, cameraMove);
17642
+ this.openAgeGate(chapterId, panelId, transition, cameraMove, mutations);
17322
17643
  return;
17323
17644
  }
17324
- const gate = this.paywallService.gateFor(panelId);
17645
+ const gate = this.paywallService.gateFor(panelId, this.locale);
17325
17646
  if (gate) {
17326
17647
  this.openPaywall(gate);
17327
17648
  return;
@@ -17335,6 +17656,10 @@ class PlayerShellComponent {
17335
17656
  if (!panelData) {
17336
17657
  throw new Error(`Panel not found: ${panelId}`);
17337
17658
  }
17659
+ // Edge `action` mutations (schema: "applied when traversing this edge").
17660
+ if (mutations?.length) {
17661
+ this.variableStore.applyMutations(mutations, { chapterId });
17662
+ }
17338
17663
  const chapterChanged = this.currentChapter !== chapter;
17339
17664
  const leavingPanelId = this.getCurrentPanelId();
17340
17665
  this.currentChapter = chapter;
@@ -17469,7 +17794,8 @@ class PlayerShellComponent {
17469
17794
  buildExtrasList(manifest) {
17470
17795
  // Media (images / video / audio / thumbnails) resolve through the asset
17471
17796
  // catalog; see utils/extras-utils.ts for the ExtraBlock mapping.
17472
- this.extrasList = extrasFromManifest(manifest?.extras, (assetId) => this.manifestService.getAsset(assetId));
17797
+ // An extras-scoped paywall rule the reader does not satisfy locks its block.
17798
+ this.extrasList = extrasFromManifest(manifest?.extras, (assetId) => this.manifestService.getAsset(assetId)).map((extra) => (this.paywallService.isExtraLocked(extra.id) ? { ...extra, gated: true } : extra));
17473
17799
  }
17474
17800
  /**
17475
17801
  * Canvas view is available when the current chapter has a canvas layout
@@ -17529,12 +17855,7 @@ class PlayerShellComponent {
17529
17855
  return;
17530
17856
  const result = this.flowEngine.getNextPanel(this.currentChapter.graph, currentPanelId, context, this.flowEngine.getDefaultTransition(this.manifestService.getManifest()?.settings), this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
17531
17857
  if (result.nextPanelId) {
17532
- // Edge `action` mutations (schema: "applied when traversing this
17533
- // edge") were returned by the flow engine but never applied here.
17534
- if (result.action?.length) {
17535
- this.variableStore.applyMutations(result.action, { chapterId: this.currentChapter.id });
17536
- }
17537
- await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove);
17858
+ await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove, result.action);
17538
17859
  }
17539
17860
  }
17540
17861
  catch (err) {
@@ -17663,16 +17984,7 @@ class PlayerShellComponent {
17663
17984
  else if (newMode === 'canvas') {
17664
17985
  this.viewMode = 'canvas';
17665
17986
  this.videoSequencer.reset();
17666
- // Land the camera on the current panel when entering canvas view.
17667
- if (this.currentChapter) {
17668
- const panelId = this.getCurrentPanelId();
17669
- if (panelId) {
17670
- const placement = this.currentChapter.canvas?.placements?.[panelId];
17671
- if (placement) {
17672
- this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
17673
- }
17674
- }
17675
- }
17987
+ this.jumpCameraToCurrentPanel();
17676
17988
  }
17677
17989
  else {
17678
17990
  // Switch to panel view
@@ -17685,6 +17997,14 @@ class PlayerShellComponent {
17685
17997
  }
17686
17998
  console.log('View mode:', this.viewMode);
17687
17999
  }
18000
+ /** Land the canvas camera on the current panel (entering canvas view). */
18001
+ jumpCameraToCurrentPanel() {
18002
+ const panelId = this.getCurrentPanelId();
18003
+ const placement = panelId ? this.currentChapter?.canvas?.placements?.[panelId] : undefined;
18004
+ if (placement) {
18005
+ this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
18006
+ }
18007
+ }
17688
18008
  /**
17689
18009
  * Tap on a placed panel in canvas view. Traverses the matching out-edge of
17690
18010
  * the current panel when its condition passes (the graph still gates
@@ -17707,7 +18027,7 @@ class PlayerShellComponent {
17707
18027
  if (edge) {
17708
18028
  this.navigationAttempt.emit({ direction: 'panel', target: panelId });
17709
18029
  void this.navigateToPanel(this.currentChapter.id, panelId, edge.transition, edge.cameraMove ??
17710
- this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
18030
+ this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings), edge.action);
17711
18031
  return;
17712
18032
  }
17713
18033
  if (this.visitedPanelIds.includes(panelId)) {
@@ -17715,6 +18035,14 @@ class PlayerShellComponent {
17715
18035
  void this.navigateToPanel(this.currentChapter.id, panelId);
17716
18036
  }
17717
18037
  }
18038
+ /**
18039
+ * The manifest actually loaded — from `manifest` or fetched via
18040
+ * `manifestUrl`. Everything that lists the work (ToC, thumbnails) binds to
18041
+ * this, never to the `manifest` input, which is unset for URL loading.
18042
+ */
18043
+ get loadedManifest() {
18044
+ return this.manifestService.getManifest() ?? undefined;
18045
+ }
17718
18046
  /** Balloon defaults for the canvas stage (same source as the viewport). */
17719
18047
  get manifestBalloonConfig() {
17720
18048
  return this.manifestService.getManifest()?.settings?.typography?.balloon_config ?? null;
@@ -17728,12 +18056,9 @@ class PlayerShellComponent {
17728
18056
  return this.manifestService.getManifest()?.settings?.preload ?? null;
17729
18057
  }
17730
18058
  onLocaleChange(locale) {
17731
- // Update content locale
18059
+ // Content locale and GUI language stay in sync.
17732
18060
  this.locale = locale;
17733
- this.playerState.setLocale(locale);
17734
- // Update GUI language (sync with content locale)
17735
- this.translationService.setLanguage(locale);
17736
- console.log('Locale changed to:', locale);
18061
+ this.applyLocale();
17737
18062
  }
17738
18063
  onToggleSpeech() {
17739
18064
  this.setSpeechEnabled(!this.speechEnabled);
@@ -18055,7 +18380,15 @@ class PlayerShellComponent {
18055
18380
  */
18056
18381
  handleKeyboard(event) {
18057
18382
  // Don't handle if user is typing in an input
18058
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
18383
+ if (event.target instanceof HTMLInputElement ||
18384
+ event.target instanceof HTMLTextAreaElement ||
18385
+ event.target instanceof HTMLSelectElement) {
18386
+ return;
18387
+ }
18388
+ // The paywall and the age gate block the story: no shortcut may move
18389
+ // past them. Their own Escape handling applies (paywall: "Maybe later"
18390
+ // dismisses; age gate: dismiss keeps the reader where they were).
18391
+ if (this.paywallVisible || this.ageGateVisible) {
18059
18392
  return;
18060
18393
  }
18061
18394
  // While a dialog is open the story behind it must not react: Escape
@@ -18192,6 +18525,9 @@ class PlayerShellComponent {
18192
18525
  * Handle swipe gesture
18193
18526
  */
18194
18527
  onSwipe(direction) {
18528
+ if (this.paywallVisible || this.ageGateVisible) {
18529
+ return;
18530
+ }
18195
18531
  if (direction === 'left') {
18196
18532
  // Swipe left = navigate forward
18197
18533
  if (this.viewMode === 'page') {
@@ -18589,9 +18925,11 @@ class PlayerShellComponent {
18589
18925
  */
18590
18926
  onPaywallAction(action) {
18591
18927
  const gate = this.paywallGate;
18928
+ const productId = action === 'purchase' || action === 'subscribe' ? this.paywallProductId : undefined;
18592
18929
  if (gate) {
18593
- this.paywallAction.emit({ action, gate });
18930
+ this.paywallAction.emit({ action, gate, ...(productId ? { productId } : {}) });
18594
18931
  }
18932
+ this.paywallProductId = undefined;
18595
18933
  this.closePaywall();
18596
18934
  }
18597
18935
  /**
@@ -18608,6 +18946,10 @@ class PlayerShellComponent {
18608
18946
  await this.httpEntitlement.resolveEntitlement({ workId: manifest?.meta?.id ?? '' });
18609
18947
  this.paywallService.setSnapshot(this.httpEntitlement.getSnapshot());
18610
18948
  }
18949
+ // What the host passed replaces the snapshot wholesale; an age check the
18950
+ // reader already passed on this device still counts.
18951
+ this.restoreAgeVerification();
18952
+ this.buildExtrasList(this.manifestService.getManifest() ?? null);
18611
18953
  const gatedPanelId = this.paywallGate?.refId ?? this.getCurrentPanelId();
18612
18954
  if (gatedPanelId && this.paywallService.canAccess(gatedPanelId)) {
18613
18955
  this.closePaywall();
@@ -18616,9 +18958,9 @@ class PlayerShellComponent {
18616
18958
  // ── Age gate ──────────────────────────────────────────────────────────────
18617
18959
  static { this.AGE_VERIFIED_STORAGE_KEY = 'pw-age-verified'; }
18618
18960
  /** Raise the age gate for a panel and remember where the reader was going. */
18619
- openAgeGate(chapterId, panelId, transition, cameraMove) {
18961
+ openAgeGate(chapterId, panelId, transition, cameraMove, mutations) {
18620
18962
  this.ageGateMinimumAge = this.paywallService.ruleFor(panelId)?.minimumAge ?? 18;
18621
- this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove };
18963
+ this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove, mutations };
18622
18964
  this.ageGateVisible = true;
18623
18965
  this.cdr.markForCheck();
18624
18966
  }
@@ -18648,8 +18990,11 @@ class PlayerShellComponent {
18648
18990
  this.ageGateVisible = false;
18649
18991
  this.pendingAgeGatedNavigation = null;
18650
18992
  this.cdr.markForCheck();
18651
- if (pending && this.paywallService.canAccess(pending.panelId)) {
18652
- await this.navigateToPanel(pending.chapterId, pending.panelId, pending.transition, pending.cameraMove);
18993
+ // Retry the move: navigateToPanel re-checks the gate, so a rule that
18994
+ // also needs a purchase / subscription now raises the paywall (with its
18995
+ // Buy / Subscribe options) instead of letting the reader through.
18996
+ if (pending) {
18997
+ await this.navigateToPanel(pending.chapterId, pending.panelId, pending.transition, pending.cameraMove, pending.mutations);
18653
18998
  }
18654
18999
  }
18655
19000
  persistAgeVerification(age) {
@@ -18725,41 +19070,43 @@ class PlayerShellComponent {
18725
19070
  bookmark.panelId === this.getCurrentPanelId();
18726
19071
  }
18727
19072
  // ── Branch choices ────────────────────────────────────────────────────────
18728
- /** More than one edge leaves the current panel — the toolbar shows "Choices". */
19073
+ /**
19074
+ * At least two paths out of the current panel are open right now (edge
19075
+ * conditions evaluated exactly like the chooser does) — the toolbar shows
19076
+ * "Choices". A closed conditional edge does not count.
19077
+ */
18729
19078
  get hasBranchesAhead() {
18730
- const panelId = this.getCurrentPanelId();
18731
- const edges = this.currentChapter?.graph?.edges;
18732
- if (!panelId || !edges) {
18733
- return false;
18734
- }
18735
- let count = 0;
18736
- for (const edge of edges) {
18737
- if (edge.from === panelId && ++count > 1) {
18738
- return true;
18739
- }
18740
- }
18741
- return false;
19079
+ return this.openEdges().length > 1;
18742
19080
  }
18743
- /** Outgoing edges whose conditions pass right now, with reader-facing labels. */
18744
- computeBranchChoices() {
19081
+ /** Outgoing edges of the current panel whose conditions pass right now. */
19082
+ openEdges() {
18745
19083
  const chapter = this.currentChapter;
18746
19084
  const panelId = this.getCurrentPanelId();
18747
- if (!chapter?.graph || !panelId) {
19085
+ if (!chapter || !panelId) {
18748
19086
  return [];
18749
19087
  }
19088
+ const outgoing = chapter.graph?.edges?.filter((edge) => edge.from === panelId) ?? [];
19089
+ if (!outgoing.some((edge) => edge.condition)) {
19090
+ // Nothing to evaluate: skip building a variable context on every check.
19091
+ return outgoing;
19092
+ }
18750
19093
  const context = this.variableStore.createContext(chapter.id);
18751
- return chapter.graph.edges
18752
- .filter((edge) => edge.from === panelId)
18753
- .filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context))
18754
- .map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter, index) }));
19094
+ return outgoing.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context));
18755
19095
  }
18756
- branchLabel(edge, chapter, index) {
19096
+ /** Open outgoing edges with reader-facing labels. */
19097
+ computeBranchChoices() {
19098
+ const chapter = this.currentChapter;
19099
+ return chapter
19100
+ ? this.openEdges().map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter) }))
19101
+ : [];
19102
+ }
19103
+ branchLabel(edge, chapter) {
18757
19104
  const fromEdge = this.localizedText(edge.label);
18758
19105
  if (fromEdge) {
18759
19106
  return fromEdge;
18760
19107
  }
18761
- const fromTarget = this.localizedText(chapter.panels?.[edge.to]?.title);
18762
- return fromTarget || `Option ${index + 1}`;
19108
+ // Neither: the chooser renders a translated "Option N".
19109
+ return this.localizedText(chapter.panels?.[edge.to]?.title);
18763
19110
  }
18764
19111
  localizedText(value) {
18765
19112
  if (!value) {
@@ -18784,18 +19131,15 @@ class PlayerShellComponent {
18784
19131
  to: choice.edge.to,
18785
19132
  index: choice.index,
18786
19133
  });
18787
- if (choice.edge.action?.length) {
18788
- this.variableStore.applyMutations(choice.edge.action, { chapterId: chapter.id });
18789
- }
18790
- await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings));
19134
+ 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);
18791
19135
  }
18792
19136
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
18793
- 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", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, providers: [CanvasCameraService], 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)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\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 [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (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]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($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]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\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 floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\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]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\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 [baseUrl]=\"''\"\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 [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: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.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:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@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: "ngmodule", type: TranslateModule }, { kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "panels", "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", "panelFocus", "hotspotActivate", "deadClick"] }, { 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", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "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", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "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", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "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: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
19137
+ 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", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, 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 <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\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 [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (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]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($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]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\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 floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\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]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\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 [baseUrl]=\"''\"\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 [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: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.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:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@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", "panels", "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", "panelFocus", "hotspotActivate", "deadClick"] }, { 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", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "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", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "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", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "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 }); }
18794
19138
  }
18795
19139
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, decorators: [{
18796
19140
  type: Component,
18797
19141
  args: [{ selector: 'pw-player-shell', providers: [CanvasCameraService], imports: [
18798
- TranslateModule,
19142
+ TranslatePipe,
18799
19143
  ViewportComponent,
18800
19144
  CanvasStageComponent,
18801
19145
  ToolbarComponent,
@@ -18812,7 +19156,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
18812
19156
  ShareModalComponent,
18813
19157
  CommentsDrawerComponent,
18814
19158
  PwIconComponent
18815
- ], 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)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\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 [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (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]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($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]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\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 floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\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]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\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 [baseUrl]=\"''\"\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 [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: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.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:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@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"] }]
19159
+ ], 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 <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\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 [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (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]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($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]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\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 floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\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]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\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 [baseUrl]=\"''\"\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 [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: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.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:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@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"] }]
18816
19160
  }], propDecorators: { manifest: [{
18817
19161
  type: Input
18818
19162
  }], manifestUrl: [{
@@ -18978,5 +19322,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
18978
19322
  * Generated bundle index. Do not edit.
18979
19323
  */
18980
19324
 
18981
- export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
19325
+ export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, chapterOrderFromManifest, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, isExtraLocked, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
18982
19326
  //# sourceMappingURL=panelwave-player.mjs.map