@panelwave/player 1.0.0 → 1.1.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.
@@ -7053,12 +7053,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
7053
7053
  * free preview.
7054
7054
  * - Panel-scoped rules gate exactly their `targetPanelIds` — preview does NOT
7055
7055
  * override an explicit panel gate.
7056
+ * - Chapter-scoped rules gate only the panels of their chapter(s) (`refId`);
7057
+ * their free preview counts panels WITHIN that chapter. They take
7058
+ * precedence over a work gate for those panels.
7059
+ * - Extras-scoped rules never gate panels — they gate the extras block named
7060
+ * by `refId` (see `isExtraLocked`).
7056
7061
  * - A gated panel unlocks when the gating rule's requirement is satisfied by
7057
7062
  * the reader's entitlement snapshot.
7058
7063
  */
7059
7064
  const DEFAULT_MINIMUM_AGE = 18;
7060
- /** Does the reader's snapshot satisfy this rule's requirement? */
7065
+ /** Does the rule carry an age requirement the reader has not met? */
7066
+ function ageMissing(rule, entitlement) {
7067
+ if (rule.entitlementType !== 'age_gate' && rule.minimumAge === undefined)
7068
+ return false;
7069
+ const min = rule.minimumAge ?? DEFAULT_MINIMUM_AGE;
7070
+ return !(entitlement.ageVerified && (entitlement.age ?? 0) >= min);
7071
+ }
7072
+ /**
7073
+ * Does the reader's snapshot satisfy this rule's requirement? A rule with a
7074
+ * `minimumAge` needs the age AND its entitlement.
7075
+ */
7061
7076
  function satisfiesRule(rule, entitlement) {
7077
+ return !ageMissing(rule, entitlement) && hasEntitlement(rule, entitlement);
7078
+ }
7079
+ /** The entitlement part of a rule (the age part is `ageMissing`). */
7080
+ function hasEntitlement(rule, entitlement) {
7062
7081
  switch (rule.entitlementType) {
7063
7082
  case 'free':
7064
7083
  return true;
@@ -7074,13 +7093,17 @@ function satisfiesRule(rule, entitlement) {
7074
7093
  return entitlement.purchasedProductIds.length > 0;
7075
7094
  return required.some((id) => entitlement.purchasedProductIds.includes(id));
7076
7095
  }
7077
- case 'age_gate': {
7078
- const min = rule.minimumAge ?? DEFAULT_MINIMUM_AGE;
7079
- return entitlement.ageVerified && (entitlement.age ?? 0) >= min;
7080
- }
7096
+ case 'age_gate':
7097
+ return true; // the age is the whole requirement (checked by ageMissing)
7081
7098
  }
7082
7099
  }
7083
- function lockReasonFor(rule) {
7100
+ /**
7101
+ * Why a rule locks the reader out. The age comes first: a reader who fails
7102
+ * the age check is asked for it before being offered anything to buy.
7103
+ */
7104
+ function lockReasonFor(rule, entitlement) {
7105
+ if (ageMissing(rule, entitlement))
7106
+ return 'age_verification_required';
7084
7107
  switch (rule.entitlementType) {
7085
7108
  case 'subscription':
7086
7109
  return 'subscription_required';
@@ -7108,36 +7131,48 @@ function evaluatePanelAccess(rules, entitlement, panel) {
7108
7131
  return {
7109
7132
  panelId: panel.id,
7110
7133
  locked: !ok,
7111
- reason: ok ? null : lockReasonFor(panelRule),
7134
+ reason: ok ? null : lockReasonFor(panelRule, entitlement),
7112
7135
  appliedRuleId: panelRule.id,
7113
7136
  preview: false,
7114
7137
  };
7115
7138
  }
7116
- // 2. Work/global gate with free preview.
7139
+ // 2. Chapter gates: only the panels of the named chapter, with the free
7140
+ // preview counted within that chapter.
7141
+ const chapterRule = panel.chapterId === undefined
7142
+ ? undefined
7143
+ : active.find((r) => r.scope === 'chapter' && (r.targetIds ?? []).includes(panel.chapterId));
7144
+ if (chapterRule) {
7145
+ return gateWithPreview(chapterRule, entitlement, panel.id, panel.chapterIndex ?? Number.MAX_SAFE_INTEGER);
7146
+ }
7147
+ // 3. Work/global gate with free preview.
7117
7148
  const workRule = findWorkGateRule(active);
7118
7149
  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
- };
7150
+ return gateWithPreview(workRule, entitlement, panel.id, panel.index);
7137
7151
  }
7138
- // 3. No gate at all — free content.
7152
+ // 4. No gate at all — free content.
7139
7153
  return { panelId: panel.id, locked: false, reason: null, appliedRuleId: null, preview: false };
7140
7154
  }
7155
+ /** A work/chapter gate: panels before `previewPanelCount` (by `index`) are free. */
7156
+ function gateWithPreview(rule, entitlement, panelId, index) {
7157
+ if (index < (rule.previewPanelCount ?? 0)) {
7158
+ return { panelId, locked: false, reason: null, appliedRuleId: rule.id, preview: true };
7159
+ }
7160
+ const ok = satisfiesRule(rule, entitlement);
7161
+ return {
7162
+ panelId,
7163
+ locked: !ok,
7164
+ reason: ok ? null : lockReasonFor(rule, entitlement),
7165
+ appliedRuleId: rule.id,
7166
+ preview: false,
7167
+ };
7168
+ }
7169
+ /** Is the extras block `extraId` locked by an extras-scoped rule? */
7170
+ function isExtraLocked(rules, entitlement, extraId) {
7171
+ return rules.some((r) => r.isActive &&
7172
+ r.scope === 'extras' &&
7173
+ (r.targetIds ?? []).includes(extraId) &&
7174
+ !satisfiesRule(r, entitlement));
7175
+ }
7141
7176
  /** Evaluate a whole work (panels in reading order). */
7142
7177
  function evaluateWorkAccess(rules, entitlement, panels) {
7143
7178
  return panels.map((panel) => evaluatePanelAccess(rules, entitlement, panel));
@@ -7164,8 +7199,10 @@ const KNOWN_ENTITLEMENT_TYPES = [
7164
7199
  * `minimumAge` written by the CMS exporter. Prefer the CMS names and fall
7165
7200
  * back to the originals, so a manifest from either era gates identically.
7166
7201
  *
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.
7202
+ * `ageGate` / `minimumAge` is an age check ON TOP OF the entitlement (schema
7203
+ * semantics). Only a rule that declares no entitlement at all but names an
7204
+ * age becomes a pure age gate; a custom marker plus an age stays a purchase
7205
+ * (or subscription) gate that also requires the age.
7169
7206
  */
7170
7207
  function fromManifestRule(rule, index) {
7171
7208
  const raw = rule;
@@ -7175,9 +7212,6 @@ function fromManifestRule(rule, index) {
7175
7212
  if (declared && KNOWN_ENTITLEMENT_TYPES.includes(declared)) {
7176
7213
  entitlementType = declared;
7177
7214
  }
7178
- else if (minimumAge !== undefined) {
7179
- entitlementType = 'age_gate';
7180
- }
7181
7215
  else if (declared === 'premium') {
7182
7216
  // Original format's subscription marker.
7183
7217
  entitlementType = 'subscription';
@@ -7186,10 +7220,17 @@ function fromManifestRule(rule, index) {
7186
7220
  // 'token', 'purchaseId', or any custom marker: treat as a purchase gate.
7187
7221
  entitlementType = 'purchase';
7188
7222
  }
7223
+ else if (minimumAge !== undefined) {
7224
+ // No entitlement declared, only an age: a pure age gate.
7225
+ entitlementType = 'age_gate';
7226
+ }
7189
7227
  else {
7190
7228
  entitlementType = 'free';
7191
7229
  }
7192
- const scope = raw['scope'] === 'extras' ? 'work' : raw['scope'];
7230
+ // Extras-scoped rules keep their scope: they gate an extras block, never
7231
+ // panels (mapping them onto 'work' used to lock the whole work).
7232
+ const scope = raw['scope'];
7233
+ const refId = raw['refId'];
7193
7234
  return {
7194
7235
  id: raw['id'] ?? `rule-${index}`,
7195
7236
  scope: scope ?? 'work',
@@ -7203,9 +7244,12 @@ function fromManifestRule(rule, index) {
7203
7244
  targetPanelIds: raw['targetPanelIds'] ??
7204
7245
  // A panel-scoped rule from the original format names its single target
7205
7246
  // in refId rather than a list.
7206
- (raw['scope'] === 'panel' && raw['refId']
7207
- ? [raw['refId']]
7208
- : undefined),
7247
+ (scope === 'panel' && refId ? [refId] : undefined),
7248
+ targetIds: (scope === 'chapter' || scope === 'extras') && refId ? [refId] : undefined,
7249
+ name: raw['name'],
7250
+ description: raw['description'],
7251
+ price: raw['price'],
7252
+ entitlementKey: raw['requireEntitlement'],
7209
7253
  };
7210
7254
  }
7211
7255
  /** Normalise every rule in a manifest. Returns [] when the work has no paywall. */
@@ -7215,6 +7259,14 @@ function rulesFromManifest(manifest) {
7215
7259
  return [];
7216
7260
  return rules.map(fromManifestRule);
7217
7261
  }
7262
+ /** Chapter id and within-chapter index per panel id (chapter-rule previews). */
7263
+ function chapterOrderFromManifest(manifest) {
7264
+ const out = new Map();
7265
+ for (const chapter of manifest?.chapters ?? []) {
7266
+ Object.keys(chapter.panels ?? {}).forEach((panelId, chapterIndex) => out.set(panelId, { chapterId: chapter.id, chapterIndex }));
7267
+ }
7268
+ return out;
7269
+ }
7218
7270
  /**
7219
7271
  * Global reading order for preview counting: chapters in array order, and
7220
7272
  * within a chapter the panel declaration order.
@@ -7267,7 +7319,9 @@ class PaywallService {
7267
7319
  constructor() {
7268
7320
  this.rules = [];
7269
7321
  this.readingOrder = new Map();
7322
+ this.chapterOrder = new Map();
7270
7323
  this.manifest = null;
7324
+ this.products = new Map();
7271
7325
  this.snapshotSubject = new BehaviorSubject(ANONYMOUS_READER);
7272
7326
  this.snapshot$ = this.snapshotSubject.asObservable();
7273
7327
  }
@@ -7276,6 +7330,17 @@ class PaywallService {
7276
7330
  this.manifest = manifest;
7277
7331
  this.rules = rulesFromManifest(manifest);
7278
7332
  this.readingOrder = new Map(readingOrderFromManifest(manifest).map((panelId, index) => [panelId, index]));
7333
+ this.chapterOrder = chapterOrderFromManifest(manifest);
7334
+ this.products = PaywallService.productsFromManifest(manifest);
7335
+ }
7336
+ /** `paywall.products` by id (format 1.6); the first entry wins on duplicate ids. */
7337
+ static productsFromManifest(manifest) {
7338
+ const out = new Map();
7339
+ for (const product of manifest?.paywall?.products ?? []) {
7340
+ if (product?.id && !out.has(product.id))
7341
+ out.set(product.id, product);
7342
+ }
7343
+ return out;
7279
7344
  }
7280
7345
  /** Replace what the reader owns (after sign-in, or a completed purchase). */
7281
7346
  setSnapshot(snapshot) {
@@ -7304,7 +7369,15 @@ class PaywallService {
7304
7369
  return { panelId, locked: false, reason: null, appliedRuleId: null, preview: false };
7305
7370
  }
7306
7371
  const index = this.readingOrder.get(panelId) ?? Number.MAX_SAFE_INTEGER;
7307
- return evaluatePanelAccess(this.rules, this.snapshotSubject.value, { id: panelId, index });
7372
+ return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
7373
+ id: panelId,
7374
+ index,
7375
+ ...this.chapterOrder.get(panelId),
7376
+ });
7377
+ }
7378
+ /** Is this extras block locked by an extras-scoped rule the reader does not satisfy? */
7379
+ isExtraLocked(extraId) {
7380
+ return isExtraLocked(this.rules, this.snapshotSubject.value, extraId);
7308
7381
  }
7309
7382
  /** Convenience: may the reader open this panel? */
7310
7383
  canAccess(panelId) {
@@ -7313,9 +7386,11 @@ class PaywallService {
7313
7386
  /**
7314
7387
  * Describe the gate blocking a panel, or null when it is not gated.
7315
7388
  * The shape is the player's existing `PaywallGate`, so the overlay needs
7316
- * no changes to render it.
7389
+ * no changes to render it. `locale` picks the language of product names
7390
+ * and descriptions from `paywall.products` (fallback: the work's default
7391
+ * locale).
7317
7392
  */
7318
- gateFor(panelId) {
7393
+ gateFor(panelId, locale) {
7319
7394
  const decision = this.evaluate(panelId);
7320
7395
  if (!decision.locked)
7321
7396
  return null;
@@ -7323,14 +7398,52 @@ class PaywallService {
7323
7398
  const scope = rule?.scope === 'panel' ? 'panel' : rule?.scope === 'chapter' ? 'chapter' : 'work';
7324
7399
  return {
7325
7400
  scope,
7326
- refId: rule?.scope === 'panel' ? panelId : undefined,
7401
+ refId: rule?.scope === 'panel' ? panelId : scope === 'chapter' ? this.chapterOrder.get(panelId)?.chapterId : undefined,
7327
7402
  requireEntitlement: rule?.entitlementType,
7328
7403
  reason: PaywallService.readerMessage(decision.reason),
7329
7404
  preview: rule?.previewPanelCount
7330
7405
  ? { previewPanels: rule.previewPanelCount }
7331
7406
  : undefined,
7407
+ ruleId: rule?.id,
7408
+ lockReason: decision.reason ?? undefined,
7409
+ options: rule
7410
+ ? PaywallService.purchaseOptions(rule, this.products, locale ?? this.defaultLocale, this.defaultLocale)
7411
+ : [],
7332
7412
  };
7333
7413
  }
7414
+ /**
7415
+ * What the reader can buy to pass a rule: one option per required product
7416
+ * (purchase) or per subscription tier (subscription). Without explicit
7417
+ * ids, the rule's `requireEntitlement` key names the product, else the
7418
+ * rule id. Age gates and free rules offer nothing to buy.
7419
+ *
7420
+ * Labels (format 1.6): an option whose id has an entry in
7421
+ * `paywall.products` takes that entry's localized name, description and
7422
+ * price. Otherwise it falls back to the rule's name (a single option) or
7423
+ * the bare id (several options), and to the rule's description and price.
7424
+ */
7425
+ static purchaseOptions(rule, products = new Map(), locale = 'en-US', fallbackLocale = 'en-US') {
7426
+ const type = rule.entitlementType === 'purchase' ? 'one-time' : rule.entitlementType;
7427
+ if (type !== 'one-time' && type !== 'subscription')
7428
+ return [];
7429
+ const listed = type === 'one-time' ? rule.requiredProductIds : rule.subscriptionTiers;
7430
+ const key = rule.entitlementKey !== rule.entitlementType ? rule.entitlementKey : undefined;
7431
+ const ids = listed?.length ? listed : [key ?? rule.id];
7432
+ return ids.map((productId) => {
7433
+ const product = products.get(productId);
7434
+ const name = product?.name ? resolveLocalizedString(product.name, locale, fallbackLocale) : '';
7435
+ const description = product?.description
7436
+ ? resolveLocalizedString(product.description, locale, fallbackLocale)
7437
+ : '';
7438
+ return {
7439
+ productId,
7440
+ name: name || (ids.length > 1 ? productId : rule.name ?? ''),
7441
+ description: description || rule.description,
7442
+ price: product?.price ?? rule.price,
7443
+ type,
7444
+ };
7445
+ });
7446
+ }
7334
7447
  /** The rule that gates a panel, for callers that need its price or name. */
7335
7448
  ruleFor(panelId) {
7336
7449
  const decision = this.evaluate(panelId);
@@ -7338,11 +7451,17 @@ class PaywallService {
7338
7451
  return null;
7339
7452
  return this.rules.find((r) => r.id === decision.appliedRuleId) ?? null;
7340
7453
  }
7454
+ /** The work's default locale (product-label fallback). */
7455
+ get defaultLocale() {
7456
+ return this.manifest?.meta?.default_locale ?? 'en-US';
7457
+ }
7341
7458
  /** Reset to a manifest-less, entitlement-less state. */
7342
7459
  clear() {
7343
7460
  this.manifest = null;
7344
7461
  this.rules = [];
7462
+ this.products.clear();
7345
7463
  this.readingOrder.clear();
7464
+ this.chapterOrder.clear();
7346
7465
  this.snapshotSubject.next(ANONYMOUS_READER);
7347
7466
  }
7348
7467
  /**
@@ -9714,11 +9833,11 @@ class VideoLayerComponent {
9714
9833
  this.frameStepHandle = null;
9715
9834
  }
9716
9835
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: VideoLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
9717
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: VideoLayerComponent, isStandalone: true, selector: "pw-video-layer", inputs: { src: "src", reverseSrc: "reverseSrc", baseUrl: "baseUrl", poster: "poster", controls: "controls", playMode: "playMode", startMode: "startMode", muted: "muted", startAtMs: "startAtMs", loopFromMs: "loopFromMs", streaming: "streaming", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", ariaLabel: "ariaLabel", videoId: "videoId", panelId: "panelId", assetId: "assetId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { videoPlay: "videoPlay", videoPause: "videoPause", videoEnd: "videoEnd", videoLoop: "videoLoop", passComplete: "passComplete", videoError: "videoError" }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\r\n class=\"video-layer-container\"\r\n [class.interactive]=\"isKeyboardInteractive()\"\r\n [attr.tabindex]=\"isKeyboardInteractive() ? 0 : null\"\r\n [attr.role]=\"isKeyboardInteractive() ? 'button' : null\"\r\n [attr.aria-label]=\"ariaLabel || null\"\r\n (click)=\"onClick()\"\r\n (keydown)=\"onKeydown($event)\"\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\">\r\n <video\r\n #videoElement\r\n class=\"video-layer\"\r\n [src]=\"getVideoUrl()\"\r\n [poster]=\"getPosterUrl()\"\r\n [muted]=\"displayMuted\"\r\n [controls]=\"controls\"\r\n playsinline\r\n preload=\"metadata\"\r\n (loadedmetadata)=\"onLoadedMetadata()\"\r\n (error)=\"onError($event)\">\r\n Your browser does not support the video tag.\r\n </video>\r\n\r\n @if (showUnmuteButton) {\r\n <button\r\n type=\"button\"\r\n class=\"video-unmute-affordance\"\r\n aria-label=\"Unmute video\"\r\n (click)=\"onUnmuteClick($event)\">\r\n <span class=\"video-unmute-icon\" aria-hidden=\"true\">\uD83D\uDD0A</span>\r\n <span class=\"video-unmute-label\">Unmute</span>\r\n </button>\r\n }\r\n</div>\r\n", styles: [".video-layer-container{position:relative;width:100%;height:100%;background:#000}.video-layer{width:100%;height:100%;display:block;object-fit:contain}.video-layer::-webkit-media-controls-panel{background:linear-gradient(to bottom,transparent,rgba(0,0,0,.7))}.video-layer-container.interactive{cursor:pointer}.video-layer-container.interactive:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-affordance{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:1;color:#fff;background:#000000a6;border:1px solid rgba(255,255,255,.25);border-radius:4px;cursor:pointer;z-index:2}.video-unmute-affordance:hover{background:#000c}.video-unmute-affordance:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-icon{font-size:15px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9836
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: VideoLayerComponent, isStandalone: true, selector: "pw-video-layer", inputs: { src: "src", reverseSrc: "reverseSrc", baseUrl: "baseUrl", poster: "poster", controls: "controls", playMode: "playMode", startMode: "startMode", muted: "muted", startAtMs: "startAtMs", loopFromMs: "loopFromMs", streaming: "streaming", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", ariaLabel: "ariaLabel", videoId: "videoId", panelId: "panelId", assetId: "assetId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { videoPlay: "videoPlay", videoPause: "videoPause", videoEnd: "videoEnd", videoLoop: "videoLoop", passComplete: "passComplete", videoError: "videoError" }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"video-layer-container\"\n [class.interactive]=\"isKeyboardInteractive()\"\n [attr.tabindex]=\"isKeyboardInteractive() ? 0 : null\"\n [attr.role]=\"isKeyboardInteractive() ? 'button' : null\"\n [attr.aria-label]=\"ariaLabel || null\"\n (click)=\"onClick()\"\n (keydown)=\"onKeydown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\">\n <video\n #videoElement\n class=\"video-layer\"\n [src]=\"getVideoUrl()\"\n [poster]=\"getPosterUrl()\"\n [muted]=\"displayMuted\"\n [controls]=\"controls\"\n playsinline\n preload=\"metadata\"\n (loadedmetadata)=\"onLoadedMetadata()\"\n (error)=\"onError($event)\">\n Your browser does not support the video tag.\n </video>\n\n @if (showUnmuteButton) {\n <button\n type=\"button\"\n class=\"video-unmute-affordance\"\n aria-label=\"Unmute video\"\n (click)=\"onUnmuteClick($event)\">\n <span class=\"video-unmute-icon\" aria-hidden=\"true\">\uD83D\uDD0A</span>\n <span class=\"video-unmute-label\">Unmute</span>\n </button>\n }\n</div>\n", styles: [".video-layer-container{position:relative;width:100%;height:100%;background:#000}.video-layer{width:100%;height:100%;display:block;object-fit:contain}.video-layer::-webkit-media-controls-panel{background:linear-gradient(to bottom,transparent,rgba(0,0,0,.7))}.video-layer-container.interactive{cursor:pointer}.video-layer-container.interactive:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-affordance{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:1;color:#fff;background:#000000a6;border:1px solid rgba(255,255,255,.25);border-radius:4px;cursor:pointer;z-index:2}.video-unmute-affordance:hover{background:#000c}.video-unmute-affordance:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-icon{font-size:15px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
9718
9837
  }
9719
9838
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: VideoLayerComponent, decorators: [{
9720
9839
  type: Component,
9721
- args: [{ selector: 'pw-video-layer', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\r\n class=\"video-layer-container\"\r\n [class.interactive]=\"isKeyboardInteractive()\"\r\n [attr.tabindex]=\"isKeyboardInteractive() ? 0 : null\"\r\n [attr.role]=\"isKeyboardInteractive() ? 'button' : null\"\r\n [attr.aria-label]=\"ariaLabel || null\"\r\n (click)=\"onClick()\"\r\n (keydown)=\"onKeydown($event)\"\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\">\r\n <video\r\n #videoElement\r\n class=\"video-layer\"\r\n [src]=\"getVideoUrl()\"\r\n [poster]=\"getPosterUrl()\"\r\n [muted]=\"displayMuted\"\r\n [controls]=\"controls\"\r\n playsinline\r\n preload=\"metadata\"\r\n (loadedmetadata)=\"onLoadedMetadata()\"\r\n (error)=\"onError($event)\">\r\n Your browser does not support the video tag.\r\n </video>\r\n\r\n @if (showUnmuteButton) {\r\n <button\r\n type=\"button\"\r\n class=\"video-unmute-affordance\"\r\n aria-label=\"Unmute video\"\r\n (click)=\"onUnmuteClick($event)\">\r\n <span class=\"video-unmute-icon\" aria-hidden=\"true\">\uD83D\uDD0A</span>\r\n <span class=\"video-unmute-label\">Unmute</span>\r\n </button>\r\n }\r\n</div>\r\n", styles: [".video-layer-container{position:relative;width:100%;height:100%;background:#000}.video-layer{width:100%;height:100%;display:block;object-fit:contain}.video-layer::-webkit-media-controls-panel{background:linear-gradient(to bottom,transparent,rgba(0,0,0,.7))}.video-layer-container.interactive{cursor:pointer}.video-layer-container.interactive:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-affordance{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:1;color:#fff;background:#000000a6;border:1px solid rgba(255,255,255,.25);border-radius:4px;cursor:pointer;z-index:2}.video-unmute-affordance:hover{background:#000c}.video-unmute-affordance:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-icon{font-size:15px}\n"] }]
9840
+ args: [{ selector: 'pw-video-layer', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"video-layer-container\"\n [class.interactive]=\"isKeyboardInteractive()\"\n [attr.tabindex]=\"isKeyboardInteractive() ? 0 : null\"\n [attr.role]=\"isKeyboardInteractive() ? 'button' : null\"\n [attr.aria-label]=\"ariaLabel || null\"\n (click)=\"onClick()\"\n (keydown)=\"onKeydown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\">\n <video\n #videoElement\n class=\"video-layer\"\n [src]=\"getVideoUrl()\"\n [poster]=\"getPosterUrl()\"\n [muted]=\"displayMuted\"\n [controls]=\"controls\"\n playsinline\n preload=\"metadata\"\n (loadedmetadata)=\"onLoadedMetadata()\"\n (error)=\"onError($event)\">\n Your browser does not support the video tag.\n </video>\n\n @if (showUnmuteButton) {\n <button\n type=\"button\"\n class=\"video-unmute-affordance\"\n aria-label=\"Unmute video\"\n (click)=\"onUnmuteClick($event)\">\n <span class=\"video-unmute-icon\" aria-hidden=\"true\">\uD83D\uDD0A</span>\n <span class=\"video-unmute-label\">Unmute</span>\n </button>\n }\n</div>\n", styles: [".video-layer-container{position:relative;width:100%;height:100%;background:#000}.video-layer{width:100%;height:100%;display:block;object-fit:contain}.video-layer::-webkit-media-controls-panel{background:linear-gradient(to bottom,transparent,rgba(0,0,0,.7))}.video-layer-container.interactive{cursor:pointer}.video-layer-container.interactive:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-affordance{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:1;color:#fff;background:#000000a6;border:1px solid rgba(255,255,255,.25);border-radius:4px;cursor:pointer;z-index:2}.video-unmute-affordance:hover{background:#000c}.video-unmute-affordance:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-icon{font-size:15px}\n"] }]
9722
9841
  }], propDecorators: { src: [{
9723
9842
  type: Input
9724
9843
  }], reverseSrc: [{
@@ -10195,11 +10314,11 @@ class LayerRendererComponent {
10195
10314
  return kind === 'hotspot' || kind === 'button';
10196
10315
  }
10197
10316
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LayerRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
10198
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LayerRendererComponent, isStandalone: true, selector: "pw-layer-renderer", inputs: { layer: "layer", locale: "locale", baseUrl: "baseUrl", targetWidth: "targetWidth", altText: "altText", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", panelId: "panelId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { layerClick: "layerClick" }, ngImport: i0, template: "<div \r\n class=\"layer-wrapper\"\r\n [ngStyle]=\"getLayerStyles()\"\r\n [style.transform]=\"getTransformStyle()\"\r\n [class.interactive]=\"isInteractive()\"\r\n [attr.data-layer-id]=\"layer.id\"\r\n [attr.data-layer-kind]=\"layer.kind\"\r\n [tabindex]=\"isInteractive() ? 0 : -1\"\r\n (click)=\"isInteractive() ? onLayerClick($event) : null\"\r\n (keydown.enter)=\"isInteractive() ? onLayerClick($any($event)) : null\">\r\n\r\n <!-- Image Layer -->\r\n @if (layer.kind === 'image') {\r\n <img \r\n [src]=\"getImageSrc()\"\r\n [alt]=\"getAltText()\"\r\n class=\"layer-image\"\r\n [attr.loading]=\"viewActive ? 'eager' : 'lazy'\"\r\n [attr.fetchpriority]=\"viewActive ? 'high' : null\" />\r\n }\r\n\r\n <!-- Video Layer -->\r\n @if (layer.kind === 'video') {\r\n <pw-video-layer\r\n class=\"layer-video\"\r\n [src]=\"getVideoSrc()\"\r\n [reverseSrc]=\"getVideoReverseSrc()\"\r\n [baseUrl]=\"baseUrl\"\r\n [poster]=\"getVideoPoster()\"\r\n [controls]=\"getVideoControls()\"\r\n [playMode]=\"getVideoPlayMode()\"\r\n [startMode]=\"getVideoStartMode()\"\r\n [muted]=\"getVideoMuted()\"\r\n [startAtMs]=\"getVideoStartAtMs()\"\r\n [loopFromMs]=\"getVideoLoopFromMs()\"\r\n [streaming]=\"getVideoStreaming()\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"viewActive\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [videoId]=\"layer.id\"\r\n [panelId]=\"panelId\"\r\n [assetId]=\"getVideoAssetId()\"\r\n [placementId]=\"placementId || panelId\"\r\n [readingOrderIndex]=\"readingOrderIndex\"\r\n [placementZ]=\"placementZ\"\r\n [placementY]=\"placementY\"\r\n [placementX]=\"placementX\"\r\n [ariaLabel]=\"getAltText()\">\r\n </pw-video-layer>\r\n }\r\n\r\n <!-- Text Layer -->\r\n @if (layer.kind === 'text') {\r\n <div \r\n class=\"layer-text\"\r\n [ngStyle]=\"getTextStyles()\"\r\n [innerHTML]=\"getTextContent()\">\r\n </div>\r\n }\r\n\r\n <!-- Hotspot Layer -->\r\n @if (layer.kind === 'hotspot') {\r\n <div \r\n class=\"layer-hotspot\"\r\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'Interactive hotspot'\">\r\n <div class=\"hotspot-indicator\"></div>\r\n </div>\r\n }\r\n\r\n <!-- Button Layer -->\r\n @if (layer.kind === 'button') {\r\n <button \r\n class=\"layer-button\"\r\n type=\"button\">\r\n {{ getTextContent() }}\r\n </button>\r\n }\r\n\r\n <!-- SVG Layer -->\r\n @if (layer.kind === 'svg') {\r\n <div \r\n class=\"layer-svg\"\r\n [innerHTML]=\"getSvgContent()\">\r\n </div>\r\n }\r\n\r\n <!-- Shape Layer -->\r\n @if (layer.kind === 'shape') {\r\n <div class=\"layer-shape\" [attr.data-shape]=\"getShapeType()\">\r\n <!-- Shape rendering would be implemented here -->\r\n </div>\r\n }\r\n\r\n <!-- Group Layer (container for nested layers) -->\r\n @if (layer.kind === 'group') {\r\n <div class=\"layer-group\">\r\n <!-- Nested layers would be rendered here -->\r\n </div>\r\n }\r\n</div>\r\n", styles: [".layer-wrapper{position:absolute;pointer-events:auto}.layer-wrapper.interactive{cursor:pointer}.layer-image,.layer-video{width:100%;height:100%;object-fit:contain;display:block}.layer-text{width:100%;height:100%;overflow:hidden;word-wrap:break-word}.layer-hotspot{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#0078d71a;border:2px solid rgba(0,120,215,.5);border-radius:4px;transition:all .2s ease}.layer-hotspot:hover{background:#0078d733;border-color:#0078d7cc}.hotspot-indicator{width:12px;height:12px;background:#0078d7;border-radius:50%;animation:pulse 2s infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.5}}.layer-button{width:100%;height:100%;padding:12px 24px;background:#0078d7;color:#fff;border:none;border-radius:4px;font-size:16px;font-weight:500;cursor:pointer;transition:background .2s ease}.layer-button:hover{background:#005a9e}.layer-button:active{background:#004578}.layer-svg{width:100%;height:100%}.layer-svg svg{width:100%;height:100%}.layer-shape{width:100%;height:100%;background:#0000001a;border:1px solid rgba(0,0,0,.2)}.layer-group{width:100%;height:100%;position:relative}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: VideoLayerComponent, selector: "pw-video-layer", inputs: ["src", "reverseSrc", "baseUrl", "poster", "controls", "playMode", "startMode", "muted", "startAtMs", "loopFromMs", "streaming", "viewMode", "viewActive", "reducedMotion", "ariaLabel", "videoId", "panelId", "assetId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["videoPlay", "videoPause", "videoEnd", "videoLoop", "passComplete", "videoError"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
10317
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LayerRendererComponent, isStandalone: true, selector: "pw-layer-renderer", inputs: { layer: "layer", locale: "locale", baseUrl: "baseUrl", targetWidth: "targetWidth", altText: "altText", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", panelId: "panelId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { layerClick: "layerClick" }, ngImport: i0, template: "<div \n class=\"layer-wrapper\"\n [ngStyle]=\"getLayerStyles()\"\n [style.transform]=\"getTransformStyle()\"\n [class.interactive]=\"isInteractive()\"\n [attr.data-layer-id]=\"layer.id\"\n [attr.data-layer-kind]=\"layer.kind\"\n [tabindex]=\"isInteractive() ? 0 : -1\"\n (click)=\"isInteractive() ? onLayerClick($event) : null\"\n (keydown.enter)=\"isInteractive() ? onLayerClick($any($event)) : null\">\n\n <!-- Image Layer -->\n @if (layer.kind === 'image') {\n <img \n [src]=\"getImageSrc()\"\n [alt]=\"getAltText()\"\n class=\"layer-image\"\n [attr.loading]=\"viewActive ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"viewActive ? 'high' : null\" />\n }\n\n <!-- Video Layer -->\n @if (layer.kind === 'video') {\n <pw-video-layer\n class=\"layer-video\"\n [src]=\"getVideoSrc()\"\n [reverseSrc]=\"getVideoReverseSrc()\"\n [baseUrl]=\"baseUrl\"\n [poster]=\"getVideoPoster()\"\n [controls]=\"getVideoControls()\"\n [playMode]=\"getVideoPlayMode()\"\n [startMode]=\"getVideoStartMode()\"\n [muted]=\"getVideoMuted()\"\n [startAtMs]=\"getVideoStartAtMs()\"\n [loopFromMs]=\"getVideoLoopFromMs()\"\n [streaming]=\"getVideoStreaming()\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"viewActive\"\n [reducedMotion]=\"reducedMotion\"\n [videoId]=\"layer.id\"\n [panelId]=\"panelId\"\n [assetId]=\"getVideoAssetId()\"\n [placementId]=\"placementId || panelId\"\n [readingOrderIndex]=\"readingOrderIndex\"\n [placementZ]=\"placementZ\"\n [placementY]=\"placementY\"\n [placementX]=\"placementX\"\n [ariaLabel]=\"getAltText()\">\n </pw-video-layer>\n }\n\n <!-- Text Layer -->\n @if (layer.kind === 'text') {\n <div \n class=\"layer-text\"\n [ngStyle]=\"getTextStyles()\"\n [innerHTML]=\"getTextContent()\">\n </div>\n }\n\n <!-- Hotspot Layer -->\n @if (layer.kind === 'hotspot') {\n <div \n class=\"layer-hotspot\"\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"'Interactive hotspot'\">\n <div class=\"hotspot-indicator\"></div>\n </div>\n }\n\n <!-- Button Layer -->\n @if (layer.kind === 'button') {\n <button \n class=\"layer-button\"\n type=\"button\">\n {{ getTextContent() }}\n </button>\n }\n\n <!-- SVG Layer -->\n @if (layer.kind === 'svg') {\n <div \n class=\"layer-svg\"\n [innerHTML]=\"getSvgContent()\">\n </div>\n }\n\n <!-- Shape Layer -->\n @if (layer.kind === 'shape') {\n <div class=\"layer-shape\" [attr.data-shape]=\"getShapeType()\">\n <!-- Shape rendering would be implemented here -->\n </div>\n }\n\n <!-- Group Layer (container for nested layers) -->\n @if (layer.kind === 'group') {\n <div class=\"layer-group\">\n <!-- Nested layers would be rendered here -->\n </div>\n }\n</div>\n", styles: [".layer-wrapper{position:absolute;pointer-events:auto}.layer-wrapper.interactive{cursor:pointer}.layer-image,.layer-video{width:100%;height:100%;object-fit:contain;display:block}.layer-text{width:100%;height:100%;overflow:hidden;word-wrap:break-word}.layer-hotspot{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#0078d71a;border:2px solid rgba(0,120,215,.5);border-radius:4px;transition:all .2s ease}.layer-hotspot:hover{background:#0078d733;border-color:#0078d7cc}.hotspot-indicator{width:12px;height:12px;background:#0078d7;border-radius:50%;animation:pulse 2s infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.5}}.layer-button{width:100%;height:100%;padding:12px 24px;background:#0078d7;color:#fff;border:none;border-radius:4px;font-size:16px;font-weight:500;cursor:pointer;transition:background .2s ease}.layer-button:hover{background:#005a9e}.layer-button:active{background:#004578}.layer-svg{width:100%;height:100%}.layer-svg svg{width:100%;height:100%}.layer-shape{width:100%;height:100%;background:#0000001a;border:1px solid rgba(0,0,0,.2)}.layer-group{width:100%;height:100%;position:relative}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: VideoLayerComponent, selector: "pw-video-layer", inputs: ["src", "reverseSrc", "baseUrl", "poster", "controls", "playMode", "startMode", "muted", "startAtMs", "loopFromMs", "streaming", "viewMode", "viewActive", "reducedMotion", "ariaLabel", "videoId", "panelId", "assetId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["videoPlay", "videoPause", "videoEnd", "videoLoop", "passComplete", "videoError"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
10199
10318
  }
10200
10319
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LayerRendererComponent, decorators: [{
10201
10320
  type: Component,
10202
- args: [{ selector: 'pw-layer-renderer', imports: [CommonModule, VideoLayerComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \r\n class=\"layer-wrapper\"\r\n [ngStyle]=\"getLayerStyles()\"\r\n [style.transform]=\"getTransformStyle()\"\r\n [class.interactive]=\"isInteractive()\"\r\n [attr.data-layer-id]=\"layer.id\"\r\n [attr.data-layer-kind]=\"layer.kind\"\r\n [tabindex]=\"isInteractive() ? 0 : -1\"\r\n (click)=\"isInteractive() ? onLayerClick($event) : null\"\r\n (keydown.enter)=\"isInteractive() ? onLayerClick($any($event)) : null\">\r\n\r\n <!-- Image Layer -->\r\n @if (layer.kind === 'image') {\r\n <img \r\n [src]=\"getImageSrc()\"\r\n [alt]=\"getAltText()\"\r\n class=\"layer-image\"\r\n [attr.loading]=\"viewActive ? 'eager' : 'lazy'\"\r\n [attr.fetchpriority]=\"viewActive ? 'high' : null\" />\r\n }\r\n\r\n <!-- Video Layer -->\r\n @if (layer.kind === 'video') {\r\n <pw-video-layer\r\n class=\"layer-video\"\r\n [src]=\"getVideoSrc()\"\r\n [reverseSrc]=\"getVideoReverseSrc()\"\r\n [baseUrl]=\"baseUrl\"\r\n [poster]=\"getVideoPoster()\"\r\n [controls]=\"getVideoControls()\"\r\n [playMode]=\"getVideoPlayMode()\"\r\n [startMode]=\"getVideoStartMode()\"\r\n [muted]=\"getVideoMuted()\"\r\n [startAtMs]=\"getVideoStartAtMs()\"\r\n [loopFromMs]=\"getVideoLoopFromMs()\"\r\n [streaming]=\"getVideoStreaming()\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"viewActive\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [videoId]=\"layer.id\"\r\n [panelId]=\"panelId\"\r\n [assetId]=\"getVideoAssetId()\"\r\n [placementId]=\"placementId || panelId\"\r\n [readingOrderIndex]=\"readingOrderIndex\"\r\n [placementZ]=\"placementZ\"\r\n [placementY]=\"placementY\"\r\n [placementX]=\"placementX\"\r\n [ariaLabel]=\"getAltText()\">\r\n </pw-video-layer>\r\n }\r\n\r\n <!-- Text Layer -->\r\n @if (layer.kind === 'text') {\r\n <div \r\n class=\"layer-text\"\r\n [ngStyle]=\"getTextStyles()\"\r\n [innerHTML]=\"getTextContent()\">\r\n </div>\r\n }\r\n\r\n <!-- Hotspot Layer -->\r\n @if (layer.kind === 'hotspot') {\r\n <div \r\n class=\"layer-hotspot\"\r\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.aria-label]=\"'Interactive hotspot'\">\r\n <div class=\"hotspot-indicator\"></div>\r\n </div>\r\n }\r\n\r\n <!-- Button Layer -->\r\n @if (layer.kind === 'button') {\r\n <button \r\n class=\"layer-button\"\r\n type=\"button\">\r\n {{ getTextContent() }}\r\n </button>\r\n }\r\n\r\n <!-- SVG Layer -->\r\n @if (layer.kind === 'svg') {\r\n <div \r\n class=\"layer-svg\"\r\n [innerHTML]=\"getSvgContent()\">\r\n </div>\r\n }\r\n\r\n <!-- Shape Layer -->\r\n @if (layer.kind === 'shape') {\r\n <div class=\"layer-shape\" [attr.data-shape]=\"getShapeType()\">\r\n <!-- Shape rendering would be implemented here -->\r\n </div>\r\n }\r\n\r\n <!-- Group Layer (container for nested layers) -->\r\n @if (layer.kind === 'group') {\r\n <div class=\"layer-group\">\r\n <!-- Nested layers would be rendered here -->\r\n </div>\r\n }\r\n</div>\r\n", styles: [".layer-wrapper{position:absolute;pointer-events:auto}.layer-wrapper.interactive{cursor:pointer}.layer-image,.layer-video{width:100%;height:100%;object-fit:contain;display:block}.layer-text{width:100%;height:100%;overflow:hidden;word-wrap:break-word}.layer-hotspot{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#0078d71a;border:2px solid rgba(0,120,215,.5);border-radius:4px;transition:all .2s ease}.layer-hotspot:hover{background:#0078d733;border-color:#0078d7cc}.hotspot-indicator{width:12px;height:12px;background:#0078d7;border-radius:50%;animation:pulse 2s infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.5}}.layer-button{width:100%;height:100%;padding:12px 24px;background:#0078d7;color:#fff;border:none;border-radius:4px;font-size:16px;font-weight:500;cursor:pointer;transition:background .2s ease}.layer-button:hover{background:#005a9e}.layer-button:active{background:#004578}.layer-svg{width:100%;height:100%}.layer-svg svg{width:100%;height:100%}.layer-shape{width:100%;height:100%;background:#0000001a;border:1px solid rgba(0,0,0,.2)}.layer-group{width:100%;height:100%;position:relative}\n"] }]
10321
+ args: [{ selector: 'pw-layer-renderer', imports: [CommonModule, VideoLayerComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \n class=\"layer-wrapper\"\n [ngStyle]=\"getLayerStyles()\"\n [style.transform]=\"getTransformStyle()\"\n [class.interactive]=\"isInteractive()\"\n [attr.data-layer-id]=\"layer.id\"\n [attr.data-layer-kind]=\"layer.kind\"\n [tabindex]=\"isInteractive() ? 0 : -1\"\n (click)=\"isInteractive() ? onLayerClick($event) : null\"\n (keydown.enter)=\"isInteractive() ? onLayerClick($any($event)) : null\">\n\n <!-- Image Layer -->\n @if (layer.kind === 'image') {\n <img \n [src]=\"getImageSrc()\"\n [alt]=\"getAltText()\"\n class=\"layer-image\"\n [attr.loading]=\"viewActive ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"viewActive ? 'high' : null\" />\n }\n\n <!-- Video Layer -->\n @if (layer.kind === 'video') {\n <pw-video-layer\n class=\"layer-video\"\n [src]=\"getVideoSrc()\"\n [reverseSrc]=\"getVideoReverseSrc()\"\n [baseUrl]=\"baseUrl\"\n [poster]=\"getVideoPoster()\"\n [controls]=\"getVideoControls()\"\n [playMode]=\"getVideoPlayMode()\"\n [startMode]=\"getVideoStartMode()\"\n [muted]=\"getVideoMuted()\"\n [startAtMs]=\"getVideoStartAtMs()\"\n [loopFromMs]=\"getVideoLoopFromMs()\"\n [streaming]=\"getVideoStreaming()\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"viewActive\"\n [reducedMotion]=\"reducedMotion\"\n [videoId]=\"layer.id\"\n [panelId]=\"panelId\"\n [assetId]=\"getVideoAssetId()\"\n [placementId]=\"placementId || panelId\"\n [readingOrderIndex]=\"readingOrderIndex\"\n [placementZ]=\"placementZ\"\n [placementY]=\"placementY\"\n [placementX]=\"placementX\"\n [ariaLabel]=\"getAltText()\">\n </pw-video-layer>\n }\n\n <!-- Text Layer -->\n @if (layer.kind === 'text') {\n <div \n class=\"layer-text\"\n [ngStyle]=\"getTextStyles()\"\n [innerHTML]=\"getTextContent()\">\n </div>\n }\n\n <!-- Hotspot Layer -->\n @if (layer.kind === 'hotspot') {\n <div \n class=\"layer-hotspot\"\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"'Interactive hotspot'\">\n <div class=\"hotspot-indicator\"></div>\n </div>\n }\n\n <!-- Button Layer -->\n @if (layer.kind === 'button') {\n <button \n class=\"layer-button\"\n type=\"button\">\n {{ getTextContent() }}\n </button>\n }\n\n <!-- SVG Layer -->\n @if (layer.kind === 'svg') {\n <div \n class=\"layer-svg\"\n [innerHTML]=\"getSvgContent()\">\n </div>\n }\n\n <!-- Shape Layer -->\n @if (layer.kind === 'shape') {\n <div class=\"layer-shape\" [attr.data-shape]=\"getShapeType()\">\n <!-- Shape rendering would be implemented here -->\n </div>\n }\n\n <!-- Group Layer (container for nested layers) -->\n @if (layer.kind === 'group') {\n <div class=\"layer-group\">\n <!-- Nested layers would be rendered here -->\n </div>\n }\n</div>\n", styles: [".layer-wrapper{position:absolute;pointer-events:auto}.layer-wrapper.interactive{cursor:pointer}.layer-image,.layer-video{width:100%;height:100%;object-fit:contain;display:block}.layer-text{width:100%;height:100%;overflow:hidden;word-wrap:break-word}.layer-hotspot{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#0078d71a;border:2px solid rgba(0,120,215,.5);border-radius:4px;transition:all .2s ease}.layer-hotspot:hover{background:#0078d733;border-color:#0078d7cc}.hotspot-indicator{width:12px;height:12px;background:#0078d7;border-radius:50%;animation:pulse 2s infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.5}}.layer-button{width:100%;height:100%;padding:12px 24px;background:#0078d7;color:#fff;border:none;border-radius:4px;font-size:16px;font-weight:500;cursor:pointer;transition:background .2s ease}.layer-button:hover{background:#005a9e}.layer-button:active{background:#004578}.layer-svg{width:100%;height:100%}.layer-svg svg{width:100%;height:100%}.layer-shape{width:100%;height:100%;background:#0000001a;border:1px solid rgba(0,0,0,.2)}.layer-group{width:100%;height:100%;position:relative}\n"] }]
10203
10322
  }], propDecorators: { layer: [{
10204
10323
  type: Input
10205
10324
  }], locale: [{
@@ -10681,11 +10800,11 @@ class HotspotsOverlayComponent {
10681
10800
  return Math.min(1, Math.max(0, v));
10682
10801
  }
10683
10802
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: HotspotsOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
10684
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: HotspotsOverlayComponent, isStandalone: true, selector: "pw-hotspots-overlay", inputs: { hotspots: "hotspots", locale: "locale", containerWidth: "containerWidth", containerHeight: "containerHeight", context: "context", interactive: "interactive", showIndicators: "showIndicators" }, outputs: { hotspotActivate: "hotspotActivate" }, usesOnChanges: true, ngImport: i0, template: "<svg\r\n class=\"hotspots-svg\"\r\n [class.non-interactive]=\"!interactive\"\r\n [attr.viewBox]=\"'0 0 ' + containerWidth + ' ' + containerHeight\"\r\n preserveAspectRatio=\"none\"\r\n aria-hidden=\"false\"\r\n>\r\n @for (h of visibleHotspots; track h.id) {\r\n <g\r\n class=\"hotspot-shape\"\r\n [class.indicator]=\"showIndicators\"\r\n [attr.role]=\"interactive ? 'button' : null\"\r\n [attr.tabindex]=\"interactive ? 0 : null\"\r\n [attr.aria-label]=\"resolveLabel(h)\"\r\n (click)=\"onShapeClick(h, $event)\"\r\n (keydown)=\"onKeyDown(h, $event)\"\r\n >\r\n @switch (h.shape.type) {\r\n @case ('rect') {\r\n <rect\r\n [attr.x]=\"rectAttrs($any(h.shape)).x\"\r\n [attr.y]=\"rectAttrs($any(h.shape)).y\"\r\n [attr.width]=\"rectAttrs($any(h.shape)).width\"\r\n [attr.height]=\"rectAttrs($any(h.shape)).height\"\r\n rx=\"6\"\r\n />\r\n }\r\n @case ('circle') {\r\n <circle\r\n [attr.cx]=\"circleAttrs($any(h.shape)).cx\"\r\n [attr.cy]=\"circleAttrs($any(h.shape)).cy\"\r\n [attr.r]=\"circleAttrs($any(h.shape)).r\"\r\n />\r\n }\r\n @case ('polygon') {\r\n <polygon [attr.points]=\"polygonPoints($any(h.shape))\" />\r\n }\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:15}.hotspots-svg{width:100%;height:100%;display:block}.hotspot-shape rect,.hotspot-shape circle,.hotspot-shape polygon{fill:#fff0;stroke:transparent;stroke-width:2;pointer-events:auto;cursor:pointer}.hotspots-svg.non-interactive .hotspot-shape rect,.hotspots-svg.non-interactive .hotspot-shape circle,.hotspots-svg.non-interactive .hotspot-shape polygon{pointer-events:none;cursor:default}.hotspot-shape:hover rect,.hotspot-shape:focus-visible rect,.hotspot-shape:hover circle,.hotspot-shape:focus-visible circle,.hotspot-shape:hover polygon,.hotspot-shape:focus-visible polygon{stroke:#ffffffd9;fill:#ffffff14}.hotspot-shape:focus{outline:none}.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:hotspot-pulse 2.4s ease-in-out infinite}@keyframes hotspot-pulse{0%,to{stroke:#fff0}50%{stroke:#ffffff73}}@media (prefers-reduced-motion: reduce){.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
10803
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: HotspotsOverlayComponent, isStandalone: true, selector: "pw-hotspots-overlay", inputs: { hotspots: "hotspots", locale: "locale", containerWidth: "containerWidth", containerHeight: "containerHeight", context: "context", interactive: "interactive", showIndicators: "showIndicators" }, outputs: { hotspotActivate: "hotspotActivate" }, usesOnChanges: true, ngImport: i0, template: "<svg\n class=\"hotspots-svg\"\n [class.non-interactive]=\"!interactive\"\n [attr.viewBox]=\"'0 0 ' + containerWidth + ' ' + containerHeight\"\n preserveAspectRatio=\"none\"\n aria-hidden=\"false\"\n>\n @for (h of visibleHotspots; track h.id) {\n <g\n class=\"hotspot-shape\"\n [class.indicator]=\"showIndicators\"\n [attr.role]=\"interactive ? 'button' : null\"\n [attr.tabindex]=\"interactive ? 0 : null\"\n [attr.aria-label]=\"resolveLabel(h)\"\n (click)=\"onShapeClick(h, $event)\"\n (keydown)=\"onKeyDown(h, $event)\"\n >\n @switch (h.shape.type) {\n @case ('rect') {\n <rect\n [attr.x]=\"rectAttrs($any(h.shape)).x\"\n [attr.y]=\"rectAttrs($any(h.shape)).y\"\n [attr.width]=\"rectAttrs($any(h.shape)).width\"\n [attr.height]=\"rectAttrs($any(h.shape)).height\"\n rx=\"6\"\n />\n }\n @case ('circle') {\n <circle\n [attr.cx]=\"circleAttrs($any(h.shape)).cx\"\n [attr.cy]=\"circleAttrs($any(h.shape)).cy\"\n [attr.r]=\"circleAttrs($any(h.shape)).r\"\n />\n }\n @case ('polygon') {\n <polygon [attr.points]=\"polygonPoints($any(h.shape))\" />\n }\n }\n </g>\n }\n</svg>\n", styles: [":host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:15}.hotspots-svg{width:100%;height:100%;display:block}.hotspot-shape rect,.hotspot-shape circle,.hotspot-shape polygon{fill:#fff0;stroke:transparent;stroke-width:2;pointer-events:auto;cursor:pointer}.hotspots-svg.non-interactive .hotspot-shape rect,.hotspots-svg.non-interactive .hotspot-shape circle,.hotspots-svg.non-interactive .hotspot-shape polygon{pointer-events:none;cursor:default}.hotspot-shape:hover rect,.hotspot-shape:focus-visible rect,.hotspot-shape:hover circle,.hotspot-shape:focus-visible circle,.hotspot-shape:hover polygon,.hotspot-shape:focus-visible polygon{stroke:#ffffffd9;fill:#ffffff14}.hotspot-shape:focus{outline:none}.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:hotspot-pulse 2.4s ease-in-out infinite}@keyframes hotspot-pulse{0%,to{stroke:#fff0}50%{stroke:#ffffff73}}@media (prefers-reduced-motion: reduce){.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
10685
10804
  }
10686
10805
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: HotspotsOverlayComponent, decorators: [{
10687
10806
  type: Component,
10688
- args: [{ selector: 'pw-hotspots-overlay', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\r\n class=\"hotspots-svg\"\r\n [class.non-interactive]=\"!interactive\"\r\n [attr.viewBox]=\"'0 0 ' + containerWidth + ' ' + containerHeight\"\r\n preserveAspectRatio=\"none\"\r\n aria-hidden=\"false\"\r\n>\r\n @for (h of visibleHotspots; track h.id) {\r\n <g\r\n class=\"hotspot-shape\"\r\n [class.indicator]=\"showIndicators\"\r\n [attr.role]=\"interactive ? 'button' : null\"\r\n [attr.tabindex]=\"interactive ? 0 : null\"\r\n [attr.aria-label]=\"resolveLabel(h)\"\r\n (click)=\"onShapeClick(h, $event)\"\r\n (keydown)=\"onKeyDown(h, $event)\"\r\n >\r\n @switch (h.shape.type) {\r\n @case ('rect') {\r\n <rect\r\n [attr.x]=\"rectAttrs($any(h.shape)).x\"\r\n [attr.y]=\"rectAttrs($any(h.shape)).y\"\r\n [attr.width]=\"rectAttrs($any(h.shape)).width\"\r\n [attr.height]=\"rectAttrs($any(h.shape)).height\"\r\n rx=\"6\"\r\n />\r\n }\r\n @case ('circle') {\r\n <circle\r\n [attr.cx]=\"circleAttrs($any(h.shape)).cx\"\r\n [attr.cy]=\"circleAttrs($any(h.shape)).cy\"\r\n [attr.r]=\"circleAttrs($any(h.shape)).r\"\r\n />\r\n }\r\n @case ('polygon') {\r\n <polygon [attr.points]=\"polygonPoints($any(h.shape))\" />\r\n }\r\n }\r\n </g>\r\n }\r\n</svg>\r\n", styles: [":host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:15}.hotspots-svg{width:100%;height:100%;display:block}.hotspot-shape rect,.hotspot-shape circle,.hotspot-shape polygon{fill:#fff0;stroke:transparent;stroke-width:2;pointer-events:auto;cursor:pointer}.hotspots-svg.non-interactive .hotspot-shape rect,.hotspots-svg.non-interactive .hotspot-shape circle,.hotspots-svg.non-interactive .hotspot-shape polygon{pointer-events:none;cursor:default}.hotspot-shape:hover rect,.hotspot-shape:focus-visible rect,.hotspot-shape:hover circle,.hotspot-shape:focus-visible circle,.hotspot-shape:hover polygon,.hotspot-shape:focus-visible polygon{stroke:#ffffffd9;fill:#ffffff14}.hotspot-shape:focus{outline:none}.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:hotspot-pulse 2.4s ease-in-out infinite}@keyframes hotspot-pulse{0%,to{stroke:#fff0}50%{stroke:#ffffff73}}@media (prefers-reduced-motion: reduce){.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:none}}\n"] }]
10807
+ args: [{ selector: 'pw-hotspots-overlay', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\n class=\"hotspots-svg\"\n [class.non-interactive]=\"!interactive\"\n [attr.viewBox]=\"'0 0 ' + containerWidth + ' ' + containerHeight\"\n preserveAspectRatio=\"none\"\n aria-hidden=\"false\"\n>\n @for (h of visibleHotspots; track h.id) {\n <g\n class=\"hotspot-shape\"\n [class.indicator]=\"showIndicators\"\n [attr.role]=\"interactive ? 'button' : null\"\n [attr.tabindex]=\"interactive ? 0 : null\"\n [attr.aria-label]=\"resolveLabel(h)\"\n (click)=\"onShapeClick(h, $event)\"\n (keydown)=\"onKeyDown(h, $event)\"\n >\n @switch (h.shape.type) {\n @case ('rect') {\n <rect\n [attr.x]=\"rectAttrs($any(h.shape)).x\"\n [attr.y]=\"rectAttrs($any(h.shape)).y\"\n [attr.width]=\"rectAttrs($any(h.shape)).width\"\n [attr.height]=\"rectAttrs($any(h.shape)).height\"\n rx=\"6\"\n />\n }\n @case ('circle') {\n <circle\n [attr.cx]=\"circleAttrs($any(h.shape)).cx\"\n [attr.cy]=\"circleAttrs($any(h.shape)).cy\"\n [attr.r]=\"circleAttrs($any(h.shape)).r\"\n />\n }\n @case ('polygon') {\n <polygon [attr.points]=\"polygonPoints($any(h.shape))\" />\n }\n }\n </g>\n }\n</svg>\n", styles: [":host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:15}.hotspots-svg{width:100%;height:100%;display:block}.hotspot-shape rect,.hotspot-shape circle,.hotspot-shape polygon{fill:#fff0;stroke:transparent;stroke-width:2;pointer-events:auto;cursor:pointer}.hotspots-svg.non-interactive .hotspot-shape rect,.hotspots-svg.non-interactive .hotspot-shape circle,.hotspots-svg.non-interactive .hotspot-shape polygon{pointer-events:none;cursor:default}.hotspot-shape:hover rect,.hotspot-shape:focus-visible rect,.hotspot-shape:hover circle,.hotspot-shape:focus-visible circle,.hotspot-shape:hover polygon,.hotspot-shape:focus-visible polygon{stroke:#ffffffd9;fill:#ffffff14}.hotspot-shape:focus{outline:none}.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:hotspot-pulse 2.4s ease-in-out infinite}@keyframes hotspot-pulse{0%,to{stroke:#fff0}50%{stroke:#ffffff73}}@media (prefers-reduced-motion: reduce){.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:none}}\n"] }]
10689
10808
  }], propDecorators: { hotspots: [{
10690
10809
  type: Input
10691
10810
  }], locale: [{
@@ -12228,11 +12347,11 @@ class ViewportComponent {
12228
12347
  console.groupEnd();
12229
12348
  }
12230
12349
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ViewportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
12231
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ViewportComponent, isStandalone: true, selector: "pw-viewport", inputs: { panel: "panel", page: "page", panels: "panels", viewMode: "viewMode", currentPanelId: "currentPanelId", locale: "locale", variableContext: "variableContext", panX: "panX", panY: "panY", zoom: "zoom", reducedMotion: "reducedMotion", transition: "transition", interactive: "interactive", showOverflowArrows: "showOverflowArrows", enableViewportCulling: "enableViewportCulling", enableLazyLoading: "enableLazyLoading", workBalloonConfig: "workBalloonConfig", characters: "characters", speechEnabled: "speechEnabled", graph: "graph", preload: "preload" }, outputs: { viewportClick: "viewportClick", performanceMetrics: "performanceMetrics", transformChange: "transformChange", layerClick: "layerClick", swipe: "swipe", navigatePrevious: "navigatePrevious", navigateNext: "navigateNext", panelFocus: "panelFocus", hotspotActivate: "hotspotActivate", deadClick: "deadClick" }, host: { listeners: { "window:resize": "onWindowResize()", "mousedown": "onMouseDown($event)", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)", "wheel": "onWheel($event)", "touchstart": "onTouchStart($event)", "touchmove": "onTouchMove($event)", "touchend": "onTouchEnd($event)", "touchcancel": "onTouchEnd($event)", "keydown.tab": "onKeydownTab($event)", "keydown.arrowRight": "onKeydownNext($event)", "keydown.arrowDown": "onKeydownNext($event)", "keydown.arrowLeft": "onKeydownPrevious($event)", "keydown.arrowUp": "onKeydownPrevious($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div \r\n class=\"viewport-container\"\r\n [class]=\"getViewModeClass()\"\r\n [class.reduced-motion]=\"reducedMotion\"\r\n [class.interactive]=\"interactive\"\r\n role=\"region\"\r\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\r\n tabindex=\"0\"\r\n (click)=\"onViewportClick($event)\"\r\n (keydown.enter)=\"onKeyboardActivate($event)\"\r\n (keydown.space)=\"onKeyboardActivate($event)\"\r\n (mousemove)=\"onViewportMouseMove($event)\"\r\n (mouseleave)=\"onViewportMouseLeave()\">\r\n \r\n <!-- Panel View -->\r\n @if (viewMode === 'panel') {\r\n <div\r\n class=\"viewport-content\"\r\n [style.transform]=\"getTransformStyle()\"\r\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\r\n [style.--pw-t-duration]=\"transitionDuration\"\r\n [style.--pw-t-easing]=\"transitionEasing\">\r\n\r\n <!-- Outgoing panel during a manifest edge transition -->\r\n @if (leavingPanel) {\r\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\r\n <div class=\"panel-container\">\r\n @for (layer of leavingPanel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\r\n [panelId]=\"leavingPanelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"leavingPanel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div [class]=\"'t-frame ' + enterAnimationClass\">\r\n @if (panel) {\r\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\r\n <!-- Render layers using LayerRenderer -->\r\n @for (layer of panel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"true\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\r\n [panelId]=\"currentPanelId || ''\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay (below speech bubbles) -->\r\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panel.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for panel view -->\r\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"no-panel\">\r\n <p>No panel to display</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Page View -->\r\n @if (viewMode === 'page' && page) {\r\n <div class=\"viewport-page\">\r\n <div class=\"page-stage\">\r\n <!-- Outgoing page during a Page.transitions in/out change -->\r\n @if (leavingPage) {\r\n <div\r\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\r\n [style.--pw-t-duration]=\"pageLeaveDuration\"\r\n [style.--pw-t-easing]=\"pageLeaveEasing\"\r\n aria-hidden=\"true\">\r\n <div class=\"page-canvas\">\r\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\r\n <div\r\n class=\"panel-container\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\">\r\n\r\n @if (getLeavingPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\">\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div\r\n [class]=\"'pt-frame ' + pageEnterClass\"\r\n [style.--pw-t-duration]=\"pageEnterDuration\"\r\n [style.--pw-t-easing]=\"pageEnterEasing\">\r\n <div\r\n class=\"page-canvas\"\r\n [attr.tabindex]=\"-1\"\r\n (click)=\"onPanelClickWithFocus($event)\"\r\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\r\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\r\n \r\n @for (placement of getSortedPanels(); track placement.panelId) {\r\n @if (isPanelVisible(placement)) {\r\n <div \r\n class=\"panel-container\"\r\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\"\r\n [attr.data-panel-id]=\"placement.panelId\"\r\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\r\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\r\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\r\n [attr.loading]=\"getImageLoadingStrategy()\"\r\n (click)=\"onPanelClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\r\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\r\n \r\n @if (getPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\"\r\n tabindex=\"0\"\r\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\r\n <!-- Render panel layers -->\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [altText]=\"getAltText(layer)\"\r\n [viewMode]=\"viewMode\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\"\r\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\r\n [placementZ]=\"placement.z || 0\"\r\n [placementY]=\"placement.y\"\r\n [placementX]=\"placement.x\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay for page view panels -->\r\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panelData.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for page view panels -->\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n \r\n <!-- Focus indicator for rotated panels -->\r\n @if (isPanelFocused(placement.panelId)) {\r\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (viewMode === 'page' && !page) {\r\n <div class=\"no-panel\">\r\n <p>No page to display</p>\r\n </div>\r\n }\r\n\r\n <!-- Overflow navigation arrows -->\r\n @if (showOverflowArrows && panel) {\r\n @if (hasOverflowLeft) {\r\n <button \r\n class=\"overflow-arrow overflow-left\"\r\n type=\"button\"\r\n aria-label=\"Navigate left\"\r\n (click)=\"navigateLeft()\">\r\n <pw-icon name=\"lucideChevronLeft\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowRight) {\r\n <button \r\n class=\"overflow-arrow overflow-right\"\r\n type=\"button\"\r\n aria-label=\"Navigate right\"\r\n (click)=\"navigateRight()\">\r\n <pw-icon name=\"lucideChevronRight\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowTop) {\r\n <button \r\n class=\"overflow-arrow overflow-top\"\r\n type=\"button\"\r\n aria-label=\"Navigate up\"\r\n (click)=\"navigateUp()\">\r\n <pw-icon name=\"lucideChevronUp\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowBottom) {\r\n <button \r\n class=\"overflow-arrow overflow-bottom\"\r\n type=\"button\"\r\n aria-label=\"Navigate down\"\r\n (click)=\"navigateDown()\">\r\n <pw-icon name=\"lucideChevronDown\" />\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Navigation arrows (hover zones) -->\r\n @if (viewMode === 'panel') {\r\n @if (showLeftArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-left\"\r\n type=\"button\"\r\n aria-label=\"Previous panel\"\r\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\r\n </button>\r\n }\r\n\r\n @if (showRightArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-right\"\r\n type=\"button\"\r\n aria-label=\"Next panel\"\r\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\r\n </button>\r\n }\r\n }\r\n</div>\r\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "baseUrl", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12350
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ViewportComponent, isStandalone: true, selector: "pw-viewport", inputs: { panel: "panel", page: "page", panels: "panels", viewMode: "viewMode", currentPanelId: "currentPanelId", locale: "locale", variableContext: "variableContext", panX: "panX", panY: "panY", zoom: "zoom", reducedMotion: "reducedMotion", transition: "transition", interactive: "interactive", showOverflowArrows: "showOverflowArrows", enableViewportCulling: "enableViewportCulling", enableLazyLoading: "enableLazyLoading", workBalloonConfig: "workBalloonConfig", characters: "characters", speechEnabled: "speechEnabled", graph: "graph", preload: "preload" }, outputs: { viewportClick: "viewportClick", performanceMetrics: "performanceMetrics", transformChange: "transformChange", layerClick: "layerClick", swipe: "swipe", navigatePrevious: "navigatePrevious", navigateNext: "navigateNext", panelFocus: "panelFocus", hotspotActivate: "hotspotActivate", deadClick: "deadClick" }, host: { listeners: { "window:resize": "onWindowResize()", "mousedown": "onMouseDown($event)", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)", "wheel": "onWheel($event)", "touchstart": "onTouchStart($event)", "touchmove": "onTouchMove($event)", "touchend": "onTouchEnd($event)", "touchcancel": "onTouchEnd($event)", "keydown.tab": "onKeydownTab($event)", "keydown.arrowRight": "onKeydownNext($event)", "keydown.arrowDown": "onKeydownNext($event)", "keydown.arrowLeft": "onKeydownPrevious($event)", "keydown.arrowUp": "onKeydownPrevious($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <div class=\"panel-container\">\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div class=\"viewport-page\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\">\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel') {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Previous panel\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Next panel\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "baseUrl", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
12232
12351
  }
12233
12352
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ViewportComponent, decorators: [{
12234
12353
  type: Component,
12235
- args: [{ selector: 'pw-viewport', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \r\n class=\"viewport-container\"\r\n [class]=\"getViewModeClass()\"\r\n [class.reduced-motion]=\"reducedMotion\"\r\n [class.interactive]=\"interactive\"\r\n role=\"region\"\r\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\r\n tabindex=\"0\"\r\n (click)=\"onViewportClick($event)\"\r\n (keydown.enter)=\"onKeyboardActivate($event)\"\r\n (keydown.space)=\"onKeyboardActivate($event)\"\r\n (mousemove)=\"onViewportMouseMove($event)\"\r\n (mouseleave)=\"onViewportMouseLeave()\">\r\n \r\n <!-- Panel View -->\r\n @if (viewMode === 'panel') {\r\n <div\r\n class=\"viewport-content\"\r\n [style.transform]=\"getTransformStyle()\"\r\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\r\n [style.--pw-t-duration]=\"transitionDuration\"\r\n [style.--pw-t-easing]=\"transitionEasing\">\r\n\r\n <!-- Outgoing panel during a manifest edge transition -->\r\n @if (leavingPanel) {\r\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\r\n <div class=\"panel-container\">\r\n @for (layer of leavingPanel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\r\n [panelId]=\"leavingPanelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"leavingPanel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div [class]=\"'t-frame ' + enterAnimationClass\">\r\n @if (panel) {\r\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\r\n <!-- Render layers using LayerRenderer -->\r\n @for (layer of panel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"true\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\r\n [panelId]=\"currentPanelId || ''\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay (below speech bubbles) -->\r\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panel.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for panel view -->\r\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"no-panel\">\r\n <p>No panel to display</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Page View -->\r\n @if (viewMode === 'page' && page) {\r\n <div class=\"viewport-page\">\r\n <div class=\"page-stage\">\r\n <!-- Outgoing page during a Page.transitions in/out change -->\r\n @if (leavingPage) {\r\n <div\r\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\r\n [style.--pw-t-duration]=\"pageLeaveDuration\"\r\n [style.--pw-t-easing]=\"pageLeaveEasing\"\r\n aria-hidden=\"true\">\r\n <div class=\"page-canvas\">\r\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\r\n <div\r\n class=\"panel-container\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\">\r\n\r\n @if (getLeavingPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\">\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div\r\n [class]=\"'pt-frame ' + pageEnterClass\"\r\n [style.--pw-t-duration]=\"pageEnterDuration\"\r\n [style.--pw-t-easing]=\"pageEnterEasing\">\r\n <div\r\n class=\"page-canvas\"\r\n [attr.tabindex]=\"-1\"\r\n (click)=\"onPanelClickWithFocus($event)\"\r\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\r\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\r\n \r\n @for (placement of getSortedPanels(); track placement.panelId) {\r\n @if (isPanelVisible(placement)) {\r\n <div \r\n class=\"panel-container\"\r\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\"\r\n [attr.data-panel-id]=\"placement.panelId\"\r\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\r\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\r\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\r\n [attr.loading]=\"getImageLoadingStrategy()\"\r\n (click)=\"onPanelClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\r\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\r\n \r\n @if (getPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\"\r\n tabindex=\"0\"\r\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\r\n <!-- Render panel layers -->\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [altText]=\"getAltText(layer)\"\r\n [viewMode]=\"viewMode\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\"\r\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\r\n [placementZ]=\"placement.z || 0\"\r\n [placementY]=\"placement.y\"\r\n [placementX]=\"placement.x\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay for page view panels -->\r\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panelData.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for page view panels -->\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n \r\n <!-- Focus indicator for rotated panels -->\r\n @if (isPanelFocused(placement.panelId)) {\r\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (viewMode === 'page' && !page) {\r\n <div class=\"no-panel\">\r\n <p>No page to display</p>\r\n </div>\r\n }\r\n\r\n <!-- Overflow navigation arrows -->\r\n @if (showOverflowArrows && panel) {\r\n @if (hasOverflowLeft) {\r\n <button \r\n class=\"overflow-arrow overflow-left\"\r\n type=\"button\"\r\n aria-label=\"Navigate left\"\r\n (click)=\"navigateLeft()\">\r\n <pw-icon name=\"lucideChevronLeft\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowRight) {\r\n <button \r\n class=\"overflow-arrow overflow-right\"\r\n type=\"button\"\r\n aria-label=\"Navigate right\"\r\n (click)=\"navigateRight()\">\r\n <pw-icon name=\"lucideChevronRight\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowTop) {\r\n <button \r\n class=\"overflow-arrow overflow-top\"\r\n type=\"button\"\r\n aria-label=\"Navigate up\"\r\n (click)=\"navigateUp()\">\r\n <pw-icon name=\"lucideChevronUp\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowBottom) {\r\n <button \r\n class=\"overflow-arrow overflow-bottom\"\r\n type=\"button\"\r\n aria-label=\"Navigate down\"\r\n (click)=\"navigateDown()\">\r\n <pw-icon name=\"lucideChevronDown\" />\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Navigation arrows (hover zones) -->\r\n @if (viewMode === 'panel') {\r\n @if (showLeftArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-left\"\r\n type=\"button\"\r\n aria-label=\"Previous panel\"\r\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\r\n </button>\r\n }\r\n\r\n @if (showRightArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-right\"\r\n type=\"button\"\r\n aria-label=\"Next panel\"\r\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\r\n </button>\r\n }\r\n }\r\n</div>\r\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"] }]
12354
+ args: [{ selector: 'pw-viewport', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <div class=\"panel-container\">\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div class=\"viewport-page\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\">\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel') {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Previous panel\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Next panel\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"] }]
12236
12355
  }], propDecorators: { panel: [{
12237
12356
  type: Input
12238
12357
  }], page: [{
@@ -13416,11 +13535,11 @@ class CanvasStageComponent {
13416
13535
  return Math.hypot(points[1].x - points[0].x, points[1].y - points[0].y);
13417
13536
  }
13418
13537
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CanvasStageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13419
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CanvasStageComponent, isStandalone: true, selector: "pw-canvas-stage", inputs: { canvas: "canvas", panels: "panels", graph: "graph", currentPanelId: "currentPanelId", visitedPanelIds: "visitedPanelIds", locale: "locale", variableContext: "variableContext", speechEnabled: "speechEnabled", workBalloonConfig: "workBalloonConfig", characters: "characters", reducedMotion: "reducedMotion", interactive: "interactive", preload: "preload" }, outputs: { panelTap: "panelTap", navigateNext: "navigateNext", navigatePrevious: "navigatePrevious", viewportClick: "viewportClick", hotspotActivate: "hotspotActivate", cameraChange: "cameraChange" }, usesOnChanges: true, ngImport: i0, template: "<div\r\n class=\"canvas-viewport\"\r\n [class.reduced-motion]=\"reducedMotion\"\r\n [style]=\"backgroundStyle()\"\r\n role=\"region\"\r\n aria-label=\"Canvas viewport\"\r\n tabindex=\"0\"\r\n (pointerdown)=\"onPointerDown($event)\"\r\n (pointermove)=\"onPointerMove($event)\"\r\n (pointerup)=\"onPointerUp($event)\"\r\n (pointercancel)=\"onPointerUp($event)\"\r\n (wheel)=\"onWheel($event)\">\r\n\r\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\r\n\r\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\r\n @if (worldBackgroundStyle(); as bgStyle) {\r\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <!-- Non-panel set dressing -->\r\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\r\n <img\r\n class=\"canvas-decoration\"\r\n [src]=\"assetUrl(decoration.assetId)\"\r\n [style]=\"decorationStyle(decoration)\"\r\n alt=\"\"\r\n aria-hidden=\"true\"\r\n loading=\"lazy\" />\r\n }\r\n\r\n <!-- Placed panels (virtualized) -->\r\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\r\n <div\r\n class=\"canvas-panel\"\r\n [class.current]=\"isCurrent(entry.panelId)\"\r\n [class.unrevealed]=\"!entry.revealed\"\r\n [style]=\"placementStyle(entry)\"\r\n [attr.data-panel-id]=\"entry.panelId\"\r\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\r\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\r\n\r\n @if (entry.revealed) {\r\n @for (layer of entry.panel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n viewMode=\"panel\"\r\n [viewActive]=\"isCurrent(entry.panelId)\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\r\n [panelId]=\"entry.panelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Hotspots are interactive on the CURRENT panel (the one the\r\n camera frames); other panels show them render-only, and a\r\n tap there keeps its panel-navigation meaning. -->\r\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"entry.panel.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"entry.placement.w\"\r\n [containerHeight]=\"entry.placement.h\"\r\n [context]=\"variableContext\"\r\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\r\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"entry.panel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"entry.placement.w\"\r\n [containerHeight]=\"entry.placement.h\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n } @else {\r\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\r\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "baseUrl", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13538
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CanvasStageComponent, isStandalone: true, selector: "pw-canvas-stage", inputs: { canvas: "canvas", panels: "panels", graph: "graph", currentPanelId: "currentPanelId", visitedPanelIds: "visitedPanelIds", locale: "locale", variableContext: "variableContext", speechEnabled: "speechEnabled", workBalloonConfig: "workBalloonConfig", characters: "characters", reducedMotion: "reducedMotion", interactive: "interactive", preload: "preload" }, outputs: { panelTap: "panelTap", navigateNext: "navigateNext", navigatePrevious: "navigatePrevious", viewportClick: "viewportClick", hotspotActivate: "hotspotActivate", cameraChange: "cameraChange" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"canvas-viewport\"\n [class.reduced-motion]=\"reducedMotion\"\n [style]=\"backgroundStyle()\"\n role=\"region\"\n aria-label=\"Canvas viewport\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (wheel)=\"onWheel($event)\">\n\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\n\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\n @if (worldBackgroundStyle(); as bgStyle) {\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\n }\n\n <!-- Non-panel set dressing -->\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\n <img\n class=\"canvas-decoration\"\n [src]=\"assetUrl(decoration.assetId)\"\n [style]=\"decorationStyle(decoration)\"\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"lazy\" />\n }\n\n <!-- Placed panels (virtualized) -->\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\n <div\n class=\"canvas-panel\"\n [class.current]=\"isCurrent(entry.panelId)\"\n [class.unrevealed]=\"!entry.revealed\"\n [style]=\"placementStyle(entry)\"\n [attr.data-panel-id]=\"entry.panelId\"\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\n\n @if (entry.revealed) {\n @for (layer of entry.panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n viewMode=\"panel\"\n [viewActive]=\"isCurrent(entry.panelId)\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\n [panelId]=\"entry.panelId\">\n </pw-layer-renderer>\n }\n\n <!-- Hotspots are interactive on the CURRENT panel (the one the\n camera frames); other panels show them render-only, and a\n tap there keeps its panel-navigation meaning. -->\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"entry.panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [context]=\"variableContext\"\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\n </pw-hotspots-overlay>\n }\n\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"entry.panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n } @else {\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "baseUrl", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13420
13539
  }
13421
13540
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CanvasStageComponent, decorators: [{
13422
13541
  type: Component,
13423
- args: [{ selector: 'pw-canvas-stage', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\r\n class=\"canvas-viewport\"\r\n [class.reduced-motion]=\"reducedMotion\"\r\n [style]=\"backgroundStyle()\"\r\n role=\"region\"\r\n aria-label=\"Canvas viewport\"\r\n tabindex=\"0\"\r\n (pointerdown)=\"onPointerDown($event)\"\r\n (pointermove)=\"onPointerMove($event)\"\r\n (pointerup)=\"onPointerUp($event)\"\r\n (pointercancel)=\"onPointerUp($event)\"\r\n (wheel)=\"onWheel($event)\">\r\n\r\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\r\n\r\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\r\n @if (worldBackgroundStyle(); as bgStyle) {\r\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <!-- Non-panel set dressing -->\r\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\r\n <img\r\n class=\"canvas-decoration\"\r\n [src]=\"assetUrl(decoration.assetId)\"\r\n [style]=\"decorationStyle(decoration)\"\r\n alt=\"\"\r\n aria-hidden=\"true\"\r\n loading=\"lazy\" />\r\n }\r\n\r\n <!-- Placed panels (virtualized) -->\r\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\r\n <div\r\n class=\"canvas-panel\"\r\n [class.current]=\"isCurrent(entry.panelId)\"\r\n [class.unrevealed]=\"!entry.revealed\"\r\n [style]=\"placementStyle(entry)\"\r\n [attr.data-panel-id]=\"entry.panelId\"\r\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\r\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\r\n\r\n @if (entry.revealed) {\r\n @for (layer of entry.panel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n viewMode=\"panel\"\r\n [viewActive]=\"isCurrent(entry.panelId)\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\r\n [panelId]=\"entry.panelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Hotspots are interactive on the CURRENT panel (the one the\r\n camera frames); other panels show them render-only, and a\r\n tap there keeps its panel-navigation meaning. -->\r\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"entry.panel.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"entry.placement.w\"\r\n [containerHeight]=\"entry.placement.h\"\r\n [context]=\"variableContext\"\r\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\r\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"entry.panel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"entry.placement.w\"\r\n [containerHeight]=\"entry.placement.h\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n } @else {\r\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\r\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"] }]
13542
+ args: [{ selector: 'pw-canvas-stage', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"canvas-viewport\"\n [class.reduced-motion]=\"reducedMotion\"\n [style]=\"backgroundStyle()\"\n role=\"region\"\n aria-label=\"Canvas viewport\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (wheel)=\"onWheel($event)\">\n\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\n\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\n @if (worldBackgroundStyle(); as bgStyle) {\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\n }\n\n <!-- Non-panel set dressing -->\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\n <img\n class=\"canvas-decoration\"\n [src]=\"assetUrl(decoration.assetId)\"\n [style]=\"decorationStyle(decoration)\"\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"lazy\" />\n }\n\n <!-- Placed panels (virtualized) -->\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\n <div\n class=\"canvas-panel\"\n [class.current]=\"isCurrent(entry.panelId)\"\n [class.unrevealed]=\"!entry.revealed\"\n [style]=\"placementStyle(entry)\"\n [attr.data-panel-id]=\"entry.panelId\"\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\n\n @if (entry.revealed) {\n @for (layer of entry.panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n viewMode=\"panel\"\n [viewActive]=\"isCurrent(entry.panelId)\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\n [panelId]=\"entry.panelId\">\n </pw-layer-renderer>\n }\n\n <!-- Hotspots are interactive on the CURRENT panel (the one the\n camera frames); other panels show them render-only, and a\n tap there keeps its panel-navigation meaning. -->\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"entry.panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [context]=\"variableContext\"\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\n </pw-hotspots-overlay>\n }\n\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"entry.panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n } @else {\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"] }]
13424
13543
  }], propDecorators: { canvas: [{
13425
13544
  type: Input
13426
13545
  }], panels: [{
@@ -13735,11 +13854,11 @@ class ToolbarComponent {
13735
13854
  this.close.emit();
13736
13855
  }
13737
13856
  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\">\r\n <div class=\"toolbar\">\r\n <!-- Left section: View and navigation controls -->\r\n <div class=\"toolbar-section toolbar-left\">\r\n <!-- View toggle (page/panel) -->\r\n <button\r\n class=\"toolbar-btn\"\r\n [class.disabled]=\"!pageViewAvailable\"\r\n [disabled]=\"!pageViewAvailable\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\r\n [attr.title]=\"'toolbar.view_toggle' | translate\"\r\n (click)=\"onToggleView()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\r\n </button>\r\n\r\n <!-- Language selector -->\r\n @if (availableLocales.length > 1) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\r\n (click)=\"openLanguage.emit()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\r\n <span class=\"btn-label\">{{ locale }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Center section: Content controls -->\r\n <div class=\"toolbar-section toolbar-center\">\r\n <!-- Speech bubbles toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"speechEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\r\n [attr.aria-pressed]=\"speechEnabled\"\r\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\r\n (click)=\"onToggleSpeech()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\r\n </button>\r\n\r\n <!-- Audio toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"audioEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\r\n [attr.aria-pressed]=\"audioEnabled\"\r\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\r\n (click)=\"onToggleAudio()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\r\n </button>\r\n\r\n <!-- SFX toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"sfxEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\r\n [attr.aria-pressed]=\"sfxEnabled\"\r\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\r\n (click)=\"onToggleSfx()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\r\n </button>\r\n\r\n <!-- Autoplay control -->\r\n <div class=\"toolbar-dropdown\">\r\n <button\r\n class=\"toolbar-btn toggle-btn autoplay-btn\"\r\n [class.active]=\"autoplayEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\r\n [attr.aria-pressed]=\"autoplayEnabled\"\r\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\r\n (click)=\"onToggleAutoplay()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-progress-bar\">\r\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\r\n </div>\r\n }\r\n </button>\r\n\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-controls\">\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(-1)\">\r\n -\r\n </button>\r\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(1)\">\r\n +\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Thumbnails toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"thumbnailsVisible\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\r\n [attr.aria-pressed]=\"thumbnailsVisible\"\r\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\r\n (click)=\"onToggleThumbnails()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Right section: Navigation and info controls -->\r\n <div class=\"toolbar-section toolbar-right\">\r\n <!-- Table of Contents -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\r\n [attr.title]=\"'toolbar.toc_open' | translate\"\r\n (click)=\"onOpenToc()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\r\n </button>\r\n\r\n <!-- Settings -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\r\n [attr.title]=\"'toolbar.settings_open' | translate\"\r\n (click)=\"onOpenSettings()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\r\n </button>\r\n\r\n <!-- Characters -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\r\n [attr.title]=\"'toolbar.characters_open' | translate\"\r\n (click)=\"onOpenCharacters()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\r\n </button>\r\n\r\n <!-- Alternatives (if available) -->\r\n @if (hasAlternatives) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\r\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\r\n (click)=\"onCycleAlternative()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Branch indicator (if choices ahead) -->\r\n @if (hasBranches) {\r\n <button\r\n class=\"toolbar-btn branch-indicator\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\r\n [attr.title]=\"'toolbar.choices_view' | translate\"\r\n (click)=\"onShowBranches()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Extras -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.extras' | translate\"\r\n [attr.title]=\"'toolbar.extras_open' | translate\"\r\n (click)=\"onOpenExtras()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Social cluster (rightmost) -->\r\n @if (showSocial) {\r\n <div class=\"toolbar-section toolbar-social\">\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"liked\"\r\n [attr.aria-pressed]=\"liked\"\r\n [attr.aria-label]=\"'toolbar.like' | translate\"\r\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\r\n (click)=\"onLike()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"bookmarked\"\r\n [attr.aria-pressed]=\"bookmarked\"\r\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\r\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\r\n (click)=\"onBookmark()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.share' | translate\"\r\n [attr.title]=\"'toolbar.share_panel' | translate\"\r\n (click)=\"onShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.comments' | translate\"\r\n [attr.title]=\"'toolbar.comments_view' | translate\"\r\n (click)=\"onOpenComments()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Close button -->\r\n <button\r\n class=\"toolbar-btn close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.close' | translate\"\r\n [attr.title]=\"'toolbar.close' | translate\"\r\n (click)=\"onClose()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\r\n </button>\r\n </div>\r\n</div>\r\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 }); }
13857
+ 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 }); }
13739
13858
  }
13740
13859
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ToolbarComponent, decorators: [{
13741
13860
  type: Component,
13742
- args: [{ selector: 'pw-toolbar', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\r\n <div class=\"toolbar\">\r\n <!-- Left section: View and navigation controls -->\r\n <div class=\"toolbar-section toolbar-left\">\r\n <!-- View toggle (page/panel) -->\r\n <button\r\n class=\"toolbar-btn\"\r\n [class.disabled]=\"!pageViewAvailable\"\r\n [disabled]=\"!pageViewAvailable\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\r\n [attr.title]=\"'toolbar.view_toggle' | translate\"\r\n (click)=\"onToggleView()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\r\n </button>\r\n\r\n <!-- Language selector -->\r\n @if (availableLocales.length > 1) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\r\n (click)=\"openLanguage.emit()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\r\n <span class=\"btn-label\">{{ locale }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Center section: Content controls -->\r\n <div class=\"toolbar-section toolbar-center\">\r\n <!-- Speech bubbles toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"speechEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\r\n [attr.aria-pressed]=\"speechEnabled\"\r\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\r\n (click)=\"onToggleSpeech()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\r\n </button>\r\n\r\n <!-- Audio toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"audioEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\r\n [attr.aria-pressed]=\"audioEnabled\"\r\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\r\n (click)=\"onToggleAudio()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\r\n </button>\r\n\r\n <!-- SFX toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"sfxEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\r\n [attr.aria-pressed]=\"sfxEnabled\"\r\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\r\n (click)=\"onToggleSfx()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\r\n </button>\r\n\r\n <!-- Autoplay control -->\r\n <div class=\"toolbar-dropdown\">\r\n <button\r\n class=\"toolbar-btn toggle-btn autoplay-btn\"\r\n [class.active]=\"autoplayEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\r\n [attr.aria-pressed]=\"autoplayEnabled\"\r\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\r\n (click)=\"onToggleAutoplay()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-progress-bar\">\r\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\r\n </div>\r\n }\r\n </button>\r\n\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-controls\">\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(-1)\">\r\n -\r\n </button>\r\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(1)\">\r\n +\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Thumbnails toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"thumbnailsVisible\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\r\n [attr.aria-pressed]=\"thumbnailsVisible\"\r\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\r\n (click)=\"onToggleThumbnails()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Right section: Navigation and info controls -->\r\n <div class=\"toolbar-section toolbar-right\">\r\n <!-- Table of Contents -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\r\n [attr.title]=\"'toolbar.toc_open' | translate\"\r\n (click)=\"onOpenToc()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\r\n </button>\r\n\r\n <!-- Settings -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\r\n [attr.title]=\"'toolbar.settings_open' | translate\"\r\n (click)=\"onOpenSettings()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\r\n </button>\r\n\r\n <!-- Characters -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\r\n [attr.title]=\"'toolbar.characters_open' | translate\"\r\n (click)=\"onOpenCharacters()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\r\n </button>\r\n\r\n <!-- Alternatives (if available) -->\r\n @if (hasAlternatives) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\r\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\r\n (click)=\"onCycleAlternative()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Branch indicator (if choices ahead) -->\r\n @if (hasBranches) {\r\n <button\r\n class=\"toolbar-btn branch-indicator\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\r\n [attr.title]=\"'toolbar.choices_view' | translate\"\r\n (click)=\"onShowBranches()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Extras -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.extras' | translate\"\r\n [attr.title]=\"'toolbar.extras_open' | translate\"\r\n (click)=\"onOpenExtras()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Social cluster (rightmost) -->\r\n @if (showSocial) {\r\n <div class=\"toolbar-section toolbar-social\">\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"liked\"\r\n [attr.aria-pressed]=\"liked\"\r\n [attr.aria-label]=\"'toolbar.like' | translate\"\r\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\r\n (click)=\"onLike()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"bookmarked\"\r\n [attr.aria-pressed]=\"bookmarked\"\r\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\r\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\r\n (click)=\"onBookmark()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.share' | translate\"\r\n [attr.title]=\"'toolbar.share_panel' | translate\"\r\n (click)=\"onShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.comments' | translate\"\r\n [attr.title]=\"'toolbar.comments_view' | translate\"\r\n (click)=\"onOpenComments()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Close button -->\r\n <button\r\n class=\"toolbar-btn close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.close' | translate\"\r\n [attr.title]=\"'toolbar.close' | translate\"\r\n (click)=\"onClose()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\r\n </button>\r\n </div>\r\n</div>\r\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"] }]
13861
+ 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"] }]
13743
13862
  }], propDecorators: { locale: [{
13744
13863
  type: Input
13745
13864
  }], availableLocales: [{
@@ -13870,11 +13989,11 @@ class LanguageModalComponent {
13870
13989
  }
13871
13990
  }
13872
13991
  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) {\r\n <div \r\n class=\"language-overlay\" \r\n (click)=\"onBackdropClick($event)\"\r\n (keydown)=\"onBackdropKeydown($event)\"\r\n tabindex=\"-1\">\r\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\r\n <!-- Header -->\r\n <div class=\"language-header\">\r\n <h2 id=\"language-title\" class=\"language-title\">\r\n {{ 'toolbar.select_language' | translate }}\r\n </h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'settings.close' | translate\"\r\n (click)=\"closeModal.emit()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Language list -->\r\n <div class=\"language-content\">\r\n <div class=\"language-list\">\r\n @for (locale of availableLocales; track locale) {\r\n <button\r\n class=\"language-item\"\r\n [class.active]=\"locale === currentLocale\"\r\n type=\"button\"\r\n (click)=\"onSelectLocale(locale)\">\r\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\r\n <span class=\"language-name\">{{ locale }}</span>\r\n @if (locale === currentLocale) {\r\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\r\n }\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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 }); }
13992
+ 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 }); }
13874
13993
  }
13875
13994
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LanguageModalComponent, decorators: [{
13876
13995
  type: Component,
13877
- args: [{ selector: 'pw-language-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div \r\n class=\"language-overlay\" \r\n (click)=\"onBackdropClick($event)\"\r\n (keydown)=\"onBackdropKeydown($event)\"\r\n tabindex=\"-1\">\r\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\r\n <!-- Header -->\r\n <div class=\"language-header\">\r\n <h2 id=\"language-title\" class=\"language-title\">\r\n {{ 'toolbar.select_language' | translate }}\r\n </h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'settings.close' | translate\"\r\n (click)=\"closeModal.emit()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Language list -->\r\n <div class=\"language-content\">\r\n <div class=\"language-list\">\r\n @for (locale of availableLocales; track locale) {\r\n <button\r\n class=\"language-item\"\r\n [class.active]=\"locale === currentLocale\"\r\n type=\"button\"\r\n (click)=\"onSelectLocale(locale)\">\r\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\r\n <span class=\"language-name\">{{ locale }}</span>\r\n @if (locale === currentLocale) {\r\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\r\n }\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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"] }]
13996
+ 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"] }]
13878
13997
  }], propDecorators: { visible: [{
13879
13998
  type: Input
13880
13999
  }], currentLocale: [{
@@ -14216,11 +14335,11 @@ class ThumbnailStripComponent {
14216
14335
  }
14217
14336
  }
14218
14337
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ThumbnailStripComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
14219
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ThumbnailStripComponent, isStandalone: true, selector: "pw-thumbnail-strip", inputs: { chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", baseUrl: "baseUrl", visible: "visible", lockedPanels: "lockedPanels" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (visible) {\r\n <div class=\"thumbnail-strip\">\r\n <!-- Close button -->\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n aria-label=\"Close thumbnail strip\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n\r\n <!-- Left arrow navigation -->\r\n <button\r\n class=\"nav-arrow nav-arrow-left\"\r\n type=\"button\"\r\n aria-label=\"Scroll left\"\r\n [disabled]=\"!canScrollLeft()\"\r\n [class.disabled]=\"!canScrollLeft()\"\r\n (click)=\"scrollLeft()\">\r\n <pw-icon name=\"lucideChevronLeft\" />\r\n </button>\r\n\r\n <!-- Right arrow navigation -->\r\n <button\r\n class=\"nav-arrow nav-arrow-right\"\r\n type=\"button\"\r\n aria-label=\"Scroll right\"\r\n [disabled]=\"!canScrollRight()\"\r\n [class.disabled]=\"!canScrollRight()\"\r\n (click)=\"scrollRight()\">\r\n <pw-icon name=\"lucideChevronRight\" />\r\n </button>\r\n\r\n <!-- Scroll container -->\r\n <div\r\n class=\"strip-scroll-container\"\r\n #scrollContainer\r\n (scroll)=\"onScroll($event)\"\r\n (mousedown)=\"onMouseDown($event)\"\r\n (mousemove)=\"onMouseMove($event)\"\r\n (mouseup)=\"onMouseUp()\"\r\n (mouseleave)=\"onMouseLeave()\">\r\n \r\n <!-- Content wrapper for virtual scrolling -->\r\n <div\r\n class=\"strip-content\"\r\n [style.width.px]=\"getTotalWidth()\">\r\n \r\n @for (item of getVisibleItems(); track item.panelId) {\r\n <div\r\n class=\"thumbnail-wrapper\"\r\n [style.left.px]=\"item.index * (itemWidth + itemGap)\"\r\n [class.chapter-start]=\"item.chapterStart\">\r\n \r\n <!-- Chapter separator -->\r\n @if (item.chapterStart && item.chapterTitle) {\r\n <div class=\"chapter-separator\">\r\n <span class=\"separator-title\">{{ item.chapterTitle }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Thumbnail -->\r\n <button\r\n class=\"thumbnail-item\"\r\n [class.current]=\"item.isCurrentPanel\"\r\n [class.current-chapter]=\"item.isCurrentChapter\"\r\n [class.locked]=\"item.isLocked\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Panel ' + item.panelId\"\r\n [attr.aria-current]=\"item.isCurrentPanel ? 'true' : null\"\r\n (click)=\"onNavigate(item)\">\r\n \r\n @if (getThumbnailUrl(item)) {\r\n <img\r\n [src]=\"getThumbnailUrl(item)\"\r\n [alt]=\"'Panel ' + item.panelId\"\r\n class=\"thumbnail-image\"\r\n loading=\"lazy\" />\r\n } @else {\r\n <div class=\"thumbnail-placeholder\">\r\n <span class=\"placeholder-text\">{{ item.panelId }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Lock overlay -->\r\n @if (item.isLocked) {\r\n <div class=\"lock-overlay\">\r\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n </div>\r\n }\r\n\r\n <!-- Current indicator -->\r\n @if (item.isCurrentPanel) {\r\n <div class=\"current-indicator\"></div>\r\n }\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Navigation hints -->\r\n <div class=\"strip-hints\">\r\n <kbd>\u2190</kbd> <kbd>\u2192</kbd> Navigate\r\n <span class=\"separator\">\u2022</span>\r\n <kbd>Esc</kbd> Close\r\n </div>\r\n </div>\r\n}\r\n", styles: [".thumbnail-strip{position:fixed;bottom:0;left:0;right:0;z-index:120;background:linear-gradient(to top,#000000f2,#000c);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:12px 16px 16px;box-shadow:0 -4px 16px #0006;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.close-btn{position:absolute;top:8px;right:16px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#ffffffb3;background:#ffffff1a;border:none;border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:1}.close-btn:hover{background:#fff3;color:#fff}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#ffffffe6;background:#000000b3;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:2;-webkit-user-select:none;user-select:none}.nav-arrow:hover:not(.disabled){background:#000000d9;border-color:#fff6;transform:translateY(-50%) scale(1.1)}.nav-arrow:active:not(.disabled){transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:12px}.nav-arrow-right{right:12px}.nav-arrow.disabled{opacity:.3;cursor:not-allowed}.nav-arrow:disabled{opacity:.3;cursor:not-allowed}.strip-scroll-container{position:relative;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent;padding:8px 0;cursor:grab;-webkit-user-select:none;user-select:none}.strip-scroll-container:active{cursor:grabbing}.strip-scroll-container::-webkit-scrollbar{height:6px}.strip-scroll-container::-webkit-scrollbar-track{background:#ffffff0d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb:hover{background:#fff6}.strip-content{position:relative;height:140px;min-width:100%}.thumbnail-wrapper{position:absolute;top:0;width:120px;display:flex;flex-direction:column;gap:8px}.thumbnail-wrapper.chapter-start{padding-top:32px}.chapter-separator{position:absolute;top:0;left:0;right:0;display:flex;align-items:center;gap:8px}.chapter-separator:before,.chapter-separator:after{content:\"\";flex:1;height:1px;background:#fff3}.separator-title{font-size:11px;font-weight:600;color:#fff9;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;padding:0 8px}.thumbnail-item{position:relative;width:120px;height:100px;background:#ffffff0d;border:2px solid rgba(255,255,255,.2);border-radius:8px;cursor:pointer;overflow:hidden;transition:all .2s ease;padding:0}.thumbnail-item:hover:not(.locked){border-color:#fff6;transform:translateY(-4px);box-shadow:0 4px 12px #0006}.thumbnail-item:active:not(.locked){transform:translateY(-2px)}.thumbnail-item.current-chapter{border-color:#0078d766}.thumbnail-item.current{border-color:#0078d7cc;border-width:3px;box-shadow:0 0 0 1px #0078d74d,0 4px 12px #0078d766}.thumbnail-item.current:hover{border-color:#0078d7}.thumbnail-item.locked{opacity:.6;cursor:not-allowed}.thumbnail-item.locked:hover{transform:none;border-color:#fff3;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;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-text{font-size:12px;font-weight:600;color:#ffffff80}.lock-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:24px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.current-indicator{position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#0078d7cc,#00b4ffcc);animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.strip-hints{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:12px;font-size:11px;color:#ffffff80}.strip-hints kbd{padding:3px 6px;font-size:10px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:3px}.separator{opacity:.5}@media (max-width: 768px){.thumbnail-strip{padding:10px 12px 12px}.thumbnail-wrapper{width:100px}.thumbnail-item{width:100px;height:80px}.close-btn{width:24px;height:24px;font-size:16px}.nav-arrow{width:32px;height:32px;font-size:24px}.nav-arrow-left{left:8px}.nav-arrow-right{right:8px}.strip-content{height:120px}.separator-title{font-size:10px}.lock-icon{font-size:20px}.strip-hints{margin-top:8px;font-size:10px}}@media (max-width: 480px){.thumbnail-wrapper{width:80px}.thumbnail-item{width:80px;height:64px}.strip-content{height:100px}}.close-btn:focus-visible,.thumbnail-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.thumbnail-strip,.thumbnail-item,.current-indicator{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14338
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ThumbnailStripComponent, isStandalone: true, selector: "pw-thumbnail-strip", inputs: { chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", baseUrl: "baseUrl", visible: "visible", lockedPanels: "lockedPanels" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"thumbnail-strip\">\n <!-- Close button -->\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close thumbnail strip\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n\n <!-- Left arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Scroll left\"\n [disabled]=\"!canScrollLeft()\"\n [class.disabled]=\"!canScrollLeft()\"\n (click)=\"scrollLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n\n <!-- Right arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Scroll right\"\n [disabled]=\"!canScrollRight()\"\n [class.disabled]=\"!canScrollRight()\"\n (click)=\"scrollRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n\n <!-- Scroll container -->\n <div\n class=\"strip-scroll-container\"\n #scrollContainer\n (scroll)=\"onScroll($event)\"\n (mousedown)=\"onMouseDown($event)\"\n (mousemove)=\"onMouseMove($event)\"\n (mouseup)=\"onMouseUp()\"\n (mouseleave)=\"onMouseLeave()\">\n \n <!-- Content wrapper for virtual scrolling -->\n <div\n class=\"strip-content\"\n [style.width.px]=\"getTotalWidth()\">\n \n @for (item of getVisibleItems(); track item.panelId) {\n <div\n class=\"thumbnail-wrapper\"\n [style.left.px]=\"item.index * (itemWidth + itemGap)\"\n [class.chapter-start]=\"item.chapterStart\">\n \n <!-- Chapter separator -->\n @if (item.chapterStart && item.chapterTitle) {\n <div class=\"chapter-separator\">\n <span class=\"separator-title\">{{ item.chapterTitle }}</span>\n </div>\n }\n\n <!-- Thumbnail -->\n <button\n class=\"thumbnail-item\"\n [class.current]=\"item.isCurrentPanel\"\n [class.current-chapter]=\"item.isCurrentChapter\"\n [class.locked]=\"item.isLocked\"\n type=\"button\"\n [attr.aria-label]=\"'Panel ' + item.panelId\"\n [attr.aria-current]=\"item.isCurrentPanel ? 'true' : null\"\n (click)=\"onNavigate(item)\">\n \n @if (getThumbnailUrl(item)) {\n <img\n [src]=\"getThumbnailUrl(item)\"\n [alt]=\"'Panel ' + item.panelId\"\n class=\"thumbnail-image\"\n loading=\"lazy\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <span class=\"placeholder-text\">{{ item.panelId }}</span>\n </div>\n }\n\n <!-- Lock overlay -->\n @if (item.isLocked) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n </div>\n }\n\n <!-- Current indicator -->\n @if (item.isCurrentPanel) {\n <div class=\"current-indicator\"></div>\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!-- Navigation hints -->\n <div class=\"strip-hints\">\n <kbd>\u2190</kbd> <kbd>\u2192</kbd> Navigate\n <span class=\"separator\">\u2022</span>\n <kbd>Esc</kbd> Close\n </div>\n </div>\n}\n", styles: [".thumbnail-strip{position:fixed;bottom:0;left:0;right:0;z-index:120;background:linear-gradient(to top,#000000f2,#000c);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:12px 16px 16px;box-shadow:0 -4px 16px #0006;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.close-btn{position:absolute;top:8px;right:16px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#ffffffb3;background:#ffffff1a;border:none;border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:1}.close-btn:hover{background:#fff3;color:#fff}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#ffffffe6;background:#000000b3;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:2;-webkit-user-select:none;user-select:none}.nav-arrow:hover:not(.disabled){background:#000000d9;border-color:#fff6;transform:translateY(-50%) scale(1.1)}.nav-arrow:active:not(.disabled){transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:12px}.nav-arrow-right{right:12px}.nav-arrow.disabled{opacity:.3;cursor:not-allowed}.nav-arrow:disabled{opacity:.3;cursor:not-allowed}.strip-scroll-container{position:relative;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent;padding:8px 0;cursor:grab;-webkit-user-select:none;user-select:none}.strip-scroll-container:active{cursor:grabbing}.strip-scroll-container::-webkit-scrollbar{height:6px}.strip-scroll-container::-webkit-scrollbar-track{background:#ffffff0d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb:hover{background:#fff6}.strip-content{position:relative;height:140px;min-width:100%}.thumbnail-wrapper{position:absolute;top:0;width:120px;display:flex;flex-direction:column;gap:8px}.thumbnail-wrapper.chapter-start{padding-top:32px}.chapter-separator{position:absolute;top:0;left:0;right:0;display:flex;align-items:center;gap:8px}.chapter-separator:before,.chapter-separator:after{content:\"\";flex:1;height:1px;background:#fff3}.separator-title{font-size:11px;font-weight:600;color:#fff9;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;padding:0 8px}.thumbnail-item{position:relative;width:120px;height:100px;background:#ffffff0d;border:2px solid rgba(255,255,255,.2);border-radius:8px;cursor:pointer;overflow:hidden;transition:all .2s ease;padding:0}.thumbnail-item:hover:not(.locked){border-color:#fff6;transform:translateY(-4px);box-shadow:0 4px 12px #0006}.thumbnail-item:active:not(.locked){transform:translateY(-2px)}.thumbnail-item.current-chapter{border-color:#0078d766}.thumbnail-item.current{border-color:#0078d7cc;border-width:3px;box-shadow:0 0 0 1px #0078d74d,0 4px 12px #0078d766}.thumbnail-item.current:hover{border-color:#0078d7}.thumbnail-item.locked{opacity:.6;cursor:not-allowed}.thumbnail-item.locked:hover{transform:none;border-color:#fff3;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;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-text{font-size:12px;font-weight:600;color:#ffffff80}.lock-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:24px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.current-indicator{position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#0078d7cc,#00b4ffcc);animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.strip-hints{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:12px;font-size:11px;color:#ffffff80}.strip-hints kbd{padding:3px 6px;font-size:10px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:3px}.separator{opacity:.5}@media (max-width: 768px){.thumbnail-strip{padding:10px 12px 12px}.thumbnail-wrapper{width:100px}.thumbnail-item{width:100px;height:80px}.close-btn{width:24px;height:24px;font-size:16px}.nav-arrow{width:32px;height:32px;font-size:24px}.nav-arrow-left{left:8px}.nav-arrow-right{right:8px}.strip-content{height:120px}.separator-title{font-size:10px}.lock-icon{font-size:20px}.strip-hints{margin-top:8px;font-size:10px}}@media (max-width: 480px){.thumbnail-wrapper{width:80px}.thumbnail-item{width:80px;height:64px}.strip-content{height:100px}}.close-btn:focus-visible,.thumbnail-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.thumbnail-strip,.thumbnail-item,.current-indicator{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14220
14339
  }
14221
14340
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ThumbnailStripComponent, decorators: [{
14222
14341
  type: Component,
14223
- args: [{ selector: 'pw-thumbnail-strip', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"thumbnail-strip\">\r\n <!-- Close button -->\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n aria-label=\"Close thumbnail strip\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n\r\n <!-- Left arrow navigation -->\r\n <button\r\n class=\"nav-arrow nav-arrow-left\"\r\n type=\"button\"\r\n aria-label=\"Scroll left\"\r\n [disabled]=\"!canScrollLeft()\"\r\n [class.disabled]=\"!canScrollLeft()\"\r\n (click)=\"scrollLeft()\">\r\n <pw-icon name=\"lucideChevronLeft\" />\r\n </button>\r\n\r\n <!-- Right arrow navigation -->\r\n <button\r\n class=\"nav-arrow nav-arrow-right\"\r\n type=\"button\"\r\n aria-label=\"Scroll right\"\r\n [disabled]=\"!canScrollRight()\"\r\n [class.disabled]=\"!canScrollRight()\"\r\n (click)=\"scrollRight()\">\r\n <pw-icon name=\"lucideChevronRight\" />\r\n </button>\r\n\r\n <!-- Scroll container -->\r\n <div\r\n class=\"strip-scroll-container\"\r\n #scrollContainer\r\n (scroll)=\"onScroll($event)\"\r\n (mousedown)=\"onMouseDown($event)\"\r\n (mousemove)=\"onMouseMove($event)\"\r\n (mouseup)=\"onMouseUp()\"\r\n (mouseleave)=\"onMouseLeave()\">\r\n \r\n <!-- Content wrapper for virtual scrolling -->\r\n <div\r\n class=\"strip-content\"\r\n [style.width.px]=\"getTotalWidth()\">\r\n \r\n @for (item of getVisibleItems(); track item.panelId) {\r\n <div\r\n class=\"thumbnail-wrapper\"\r\n [style.left.px]=\"item.index * (itemWidth + itemGap)\"\r\n [class.chapter-start]=\"item.chapterStart\">\r\n \r\n <!-- Chapter separator -->\r\n @if (item.chapterStart && item.chapterTitle) {\r\n <div class=\"chapter-separator\">\r\n <span class=\"separator-title\">{{ item.chapterTitle }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Thumbnail -->\r\n <button\r\n class=\"thumbnail-item\"\r\n [class.current]=\"item.isCurrentPanel\"\r\n [class.current-chapter]=\"item.isCurrentChapter\"\r\n [class.locked]=\"item.isLocked\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Panel ' + item.panelId\"\r\n [attr.aria-current]=\"item.isCurrentPanel ? 'true' : null\"\r\n (click)=\"onNavigate(item)\">\r\n \r\n @if (getThumbnailUrl(item)) {\r\n <img\r\n [src]=\"getThumbnailUrl(item)\"\r\n [alt]=\"'Panel ' + item.panelId\"\r\n class=\"thumbnail-image\"\r\n loading=\"lazy\" />\r\n } @else {\r\n <div class=\"thumbnail-placeholder\">\r\n <span class=\"placeholder-text\">{{ item.panelId }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Lock overlay -->\r\n @if (item.isLocked) {\r\n <div class=\"lock-overlay\">\r\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n </div>\r\n }\r\n\r\n <!-- Current indicator -->\r\n @if (item.isCurrentPanel) {\r\n <div class=\"current-indicator\"></div>\r\n }\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Navigation hints -->\r\n <div class=\"strip-hints\">\r\n <kbd>\u2190</kbd> <kbd>\u2192</kbd> Navigate\r\n <span class=\"separator\">\u2022</span>\r\n <kbd>Esc</kbd> Close\r\n </div>\r\n </div>\r\n}\r\n", styles: [".thumbnail-strip{position:fixed;bottom:0;left:0;right:0;z-index:120;background:linear-gradient(to top,#000000f2,#000c);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:12px 16px 16px;box-shadow:0 -4px 16px #0006;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.close-btn{position:absolute;top:8px;right:16px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#ffffffb3;background:#ffffff1a;border:none;border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:1}.close-btn:hover{background:#fff3;color:#fff}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#ffffffe6;background:#000000b3;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:2;-webkit-user-select:none;user-select:none}.nav-arrow:hover:not(.disabled){background:#000000d9;border-color:#fff6;transform:translateY(-50%) scale(1.1)}.nav-arrow:active:not(.disabled){transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:12px}.nav-arrow-right{right:12px}.nav-arrow.disabled{opacity:.3;cursor:not-allowed}.nav-arrow:disabled{opacity:.3;cursor:not-allowed}.strip-scroll-container{position:relative;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent;padding:8px 0;cursor:grab;-webkit-user-select:none;user-select:none}.strip-scroll-container:active{cursor:grabbing}.strip-scroll-container::-webkit-scrollbar{height:6px}.strip-scroll-container::-webkit-scrollbar-track{background:#ffffff0d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb:hover{background:#fff6}.strip-content{position:relative;height:140px;min-width:100%}.thumbnail-wrapper{position:absolute;top:0;width:120px;display:flex;flex-direction:column;gap:8px}.thumbnail-wrapper.chapter-start{padding-top:32px}.chapter-separator{position:absolute;top:0;left:0;right:0;display:flex;align-items:center;gap:8px}.chapter-separator:before,.chapter-separator:after{content:\"\";flex:1;height:1px;background:#fff3}.separator-title{font-size:11px;font-weight:600;color:#fff9;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;padding:0 8px}.thumbnail-item{position:relative;width:120px;height:100px;background:#ffffff0d;border:2px solid rgba(255,255,255,.2);border-radius:8px;cursor:pointer;overflow:hidden;transition:all .2s ease;padding:0}.thumbnail-item:hover:not(.locked){border-color:#fff6;transform:translateY(-4px);box-shadow:0 4px 12px #0006}.thumbnail-item:active:not(.locked){transform:translateY(-2px)}.thumbnail-item.current-chapter{border-color:#0078d766}.thumbnail-item.current{border-color:#0078d7cc;border-width:3px;box-shadow:0 0 0 1px #0078d74d,0 4px 12px #0078d766}.thumbnail-item.current:hover{border-color:#0078d7}.thumbnail-item.locked{opacity:.6;cursor:not-allowed}.thumbnail-item.locked:hover{transform:none;border-color:#fff3;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;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-text{font-size:12px;font-weight:600;color:#ffffff80}.lock-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:24px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.current-indicator{position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#0078d7cc,#00b4ffcc);animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.strip-hints{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:12px;font-size:11px;color:#ffffff80}.strip-hints kbd{padding:3px 6px;font-size:10px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:3px}.separator{opacity:.5}@media (max-width: 768px){.thumbnail-strip{padding:10px 12px 12px}.thumbnail-wrapper{width:100px}.thumbnail-item{width:100px;height:80px}.close-btn{width:24px;height:24px;font-size:16px}.nav-arrow{width:32px;height:32px;font-size:24px}.nav-arrow-left{left:8px}.nav-arrow-right{right:8px}.strip-content{height:120px}.separator-title{font-size:10px}.lock-icon{font-size:20px}.strip-hints{margin-top:8px;font-size:10px}}@media (max-width: 480px){.thumbnail-wrapper{width:80px}.thumbnail-item{width:80px;height:64px}.strip-content{height:100px}}.close-btn:focus-visible,.thumbnail-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.thumbnail-strip,.thumbnail-item,.current-indicator{animation:none;transition:none}}\n"] }]
14342
+ args: [{ selector: 'pw-thumbnail-strip', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"thumbnail-strip\">\n <!-- Close button -->\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close thumbnail strip\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n\n <!-- Left arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Scroll left\"\n [disabled]=\"!canScrollLeft()\"\n [class.disabled]=\"!canScrollLeft()\"\n (click)=\"scrollLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n\n <!-- Right arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Scroll right\"\n [disabled]=\"!canScrollRight()\"\n [class.disabled]=\"!canScrollRight()\"\n (click)=\"scrollRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n\n <!-- Scroll container -->\n <div\n class=\"strip-scroll-container\"\n #scrollContainer\n (scroll)=\"onScroll($event)\"\n (mousedown)=\"onMouseDown($event)\"\n (mousemove)=\"onMouseMove($event)\"\n (mouseup)=\"onMouseUp()\"\n (mouseleave)=\"onMouseLeave()\">\n \n <!-- Content wrapper for virtual scrolling -->\n <div\n class=\"strip-content\"\n [style.width.px]=\"getTotalWidth()\">\n \n @for (item of getVisibleItems(); track item.panelId) {\n <div\n class=\"thumbnail-wrapper\"\n [style.left.px]=\"item.index * (itemWidth + itemGap)\"\n [class.chapter-start]=\"item.chapterStart\">\n \n <!-- Chapter separator -->\n @if (item.chapterStart && item.chapterTitle) {\n <div class=\"chapter-separator\">\n <span class=\"separator-title\">{{ item.chapterTitle }}</span>\n </div>\n }\n\n <!-- Thumbnail -->\n <button\n class=\"thumbnail-item\"\n [class.current]=\"item.isCurrentPanel\"\n [class.current-chapter]=\"item.isCurrentChapter\"\n [class.locked]=\"item.isLocked\"\n type=\"button\"\n [attr.aria-label]=\"'Panel ' + item.panelId\"\n [attr.aria-current]=\"item.isCurrentPanel ? 'true' : null\"\n (click)=\"onNavigate(item)\">\n \n @if (getThumbnailUrl(item)) {\n <img\n [src]=\"getThumbnailUrl(item)\"\n [alt]=\"'Panel ' + item.panelId\"\n class=\"thumbnail-image\"\n loading=\"lazy\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <span class=\"placeholder-text\">{{ item.panelId }}</span>\n </div>\n }\n\n <!-- Lock overlay -->\n @if (item.isLocked) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n </div>\n }\n\n <!-- Current indicator -->\n @if (item.isCurrentPanel) {\n <div class=\"current-indicator\"></div>\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!-- Navigation hints -->\n <div class=\"strip-hints\">\n <kbd>\u2190</kbd> <kbd>\u2192</kbd> Navigate\n <span class=\"separator\">\u2022</span>\n <kbd>Esc</kbd> Close\n </div>\n </div>\n}\n", styles: [".thumbnail-strip{position:fixed;bottom:0;left:0;right:0;z-index:120;background:linear-gradient(to top,#000000f2,#000c);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:12px 16px 16px;box-shadow:0 -4px 16px #0006;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.close-btn{position:absolute;top:8px;right:16px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#ffffffb3;background:#ffffff1a;border:none;border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:1}.close-btn:hover{background:#fff3;color:#fff}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#ffffffe6;background:#000000b3;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:2;-webkit-user-select:none;user-select:none}.nav-arrow:hover:not(.disabled){background:#000000d9;border-color:#fff6;transform:translateY(-50%) scale(1.1)}.nav-arrow:active:not(.disabled){transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:12px}.nav-arrow-right{right:12px}.nav-arrow.disabled{opacity:.3;cursor:not-allowed}.nav-arrow:disabled{opacity:.3;cursor:not-allowed}.strip-scroll-container{position:relative;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent;padding:8px 0;cursor:grab;-webkit-user-select:none;user-select:none}.strip-scroll-container:active{cursor:grabbing}.strip-scroll-container::-webkit-scrollbar{height:6px}.strip-scroll-container::-webkit-scrollbar-track{background:#ffffff0d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb:hover{background:#fff6}.strip-content{position:relative;height:140px;min-width:100%}.thumbnail-wrapper{position:absolute;top:0;width:120px;display:flex;flex-direction:column;gap:8px}.thumbnail-wrapper.chapter-start{padding-top:32px}.chapter-separator{position:absolute;top:0;left:0;right:0;display:flex;align-items:center;gap:8px}.chapter-separator:before,.chapter-separator:after{content:\"\";flex:1;height:1px;background:#fff3}.separator-title{font-size:11px;font-weight:600;color:#fff9;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;padding:0 8px}.thumbnail-item{position:relative;width:120px;height:100px;background:#ffffff0d;border:2px solid rgba(255,255,255,.2);border-radius:8px;cursor:pointer;overflow:hidden;transition:all .2s ease;padding:0}.thumbnail-item:hover:not(.locked){border-color:#fff6;transform:translateY(-4px);box-shadow:0 4px 12px #0006}.thumbnail-item:active:not(.locked){transform:translateY(-2px)}.thumbnail-item.current-chapter{border-color:#0078d766}.thumbnail-item.current{border-color:#0078d7cc;border-width:3px;box-shadow:0 0 0 1px #0078d74d,0 4px 12px #0078d766}.thumbnail-item.current:hover{border-color:#0078d7}.thumbnail-item.locked{opacity:.6;cursor:not-allowed}.thumbnail-item.locked:hover{transform:none;border-color:#fff3;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;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-text{font-size:12px;font-weight:600;color:#ffffff80}.lock-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:24px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.current-indicator{position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#0078d7cc,#00b4ffcc);animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.strip-hints{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:12px;font-size:11px;color:#ffffff80}.strip-hints kbd{padding:3px 6px;font-size:10px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:3px}.separator{opacity:.5}@media (max-width: 768px){.thumbnail-strip{padding:10px 12px 12px}.thumbnail-wrapper{width:100px}.thumbnail-item{width:100px;height:80px}.close-btn{width:24px;height:24px;font-size:16px}.nav-arrow{width:32px;height:32px;font-size:24px}.nav-arrow-left{left:8px}.nav-arrow-right{right:8px}.strip-content{height:120px}.separator-title{font-size:10px}.lock-icon{font-size:20px}.strip-hints{margin-top:8px;font-size:10px}}@media (max-width: 480px){.thumbnail-wrapper{width:80px}.thumbnail-item{width:80px;height:64px}.strip-content{height:100px}}.close-btn:focus-visible,.thumbnail-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.thumbnail-strip,.thumbnail-item,.current-indicator{animation:none;transition:none}}\n"] }]
14224
14343
  }], propDecorators: { chapters: [{
14225
14344
  type: Input
14226
14345
  }], currentChapterId: [{
@@ -14540,11 +14659,11 @@ class TocOverlayComponent {
14540
14659
  return this.filteredChapters[this.selectedChapterIndex];
14541
14660
  }
14542
14661
  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) {\r\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\r\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\r\n <!-- Header -->\r\n <div class=\"toc-header\">\r\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toc.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"toc-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'toc.search_chapters' | translate\"\r\n [attr.aria-label]=\"'toc.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"toc-content\">\r\n <!-- Chapter list -->\r\n <div class=\"chapter-list\" role=\"list\">\r\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\r\n <div\r\n class=\"chapter-item\"\r\n [class.selected]=\"i === selectedChapterIndex\"\r\n [class.current]=\"isCurrentChapter(chapter)\"\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"selectChapter(i)\"\r\n (keydown.enter)=\"selectChapter(i)\">\r\n \r\n <!-- Chapter header -->\r\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\r\n <span class=\"chapter-number\">{{ i + 1 }}</span>\r\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\r\n @if (isCurrentChapter(chapter)) {\r\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Panel list (if chapter selected) -->\r\n @if (i === selectedChapterIndex) {\r\n <div class=\"panel-list\">\r\n <!-- If chapter has pages, group by pages -->\r\n @if (hasPages(chapter)) {\r\n @for (page of getPages(chapter); track page.id) {\r\n <div class=\"page-group\">\r\n <div class=\"page-header\">\r\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\r\n </div>\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n } @else {\r\n <!-- No pages, show all panels directly -->\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanels(chapter); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (filteredChapters.length === 0) {\r\n <div class=\"empty-state\">\r\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"toc-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\r\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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 }); }
14662
+ 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 }); }
14544
14663
  }
14545
14664
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TocOverlayComponent, decorators: [{
14546
14665
  type: Component,
14547
- args: [{ selector: 'pw-toc-overlay', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\r\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\r\n <!-- Header -->\r\n <div class=\"toc-header\">\r\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toc.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"toc-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'toc.search_chapters' | translate\"\r\n [attr.aria-label]=\"'toc.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"toc-content\">\r\n <!-- Chapter list -->\r\n <div class=\"chapter-list\" role=\"list\">\r\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\r\n <div\r\n class=\"chapter-item\"\r\n [class.selected]=\"i === selectedChapterIndex\"\r\n [class.current]=\"isCurrentChapter(chapter)\"\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"selectChapter(i)\"\r\n (keydown.enter)=\"selectChapter(i)\">\r\n \r\n <!-- Chapter header -->\r\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\r\n <span class=\"chapter-number\">{{ i + 1 }}</span>\r\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\r\n @if (isCurrentChapter(chapter)) {\r\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Panel list (if chapter selected) -->\r\n @if (i === selectedChapterIndex) {\r\n <div class=\"panel-list\">\r\n <!-- If chapter has pages, group by pages -->\r\n @if (hasPages(chapter)) {\r\n @for (page of getPages(chapter); track page.id) {\r\n <div class=\"page-group\">\r\n <div class=\"page-header\">\r\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\r\n </div>\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n } @else {\r\n <!-- No pages, show all panels directly -->\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanels(chapter); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (filteredChapters.length === 0) {\r\n <div class=\"empty-state\">\r\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"toc-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\r\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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"] }]
14666
+ 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"] }]
14548
14667
  }], propDecorators: { manifest: [{
14549
14668
  type: Input
14550
14669
  }], chapters: [{
@@ -14568,6 +14687,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
14568
14687
  args: ['window:keydown', ['$event']]
14569
14688
  }] } });
14570
14689
 
14690
+ /**
14691
+ * Translate a built-in UI string. Falls back to the given English text (with
14692
+ * `{{param}}` interpolation) when no TranslateService is provided or the key
14693
+ * is not loaded, so a public component embedded without ngx-translate never
14694
+ * shows a raw key.
14695
+ */
14696
+ function uiText(translate, key, english, params) {
14697
+ const value = translate?.instant(key, params);
14698
+ if (typeof value === 'string' && value !== key) {
14699
+ return value;
14700
+ }
14701
+ return (english ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
14702
+ }
14703
+
14571
14704
  /**
14572
14705
  * Paywall Overlay Component
14573
14706
  * Displays paywall gate UI with purchase options
@@ -14594,6 +14727,10 @@ const PAYWALL_TEXT_EN = {
14594
14727
  'paywall.go_back': 'Go Back',
14595
14728
  'paywall.maybe_later': 'Maybe Later',
14596
14729
  'paywall.secure_payment': 'Secure payment processing',
14730
+ 'paywall.reason_subscription_required': 'This part of the story is included with a subscription.',
14731
+ 'paywall.reason_purchase_required': 'This part of the story is available to buy.',
14732
+ 'paywall.reason_age_verification_required': 'This content is age-restricted. Confirm your age to continue.',
14733
+ 'paywall.reason_entitlement_required': 'This content requires an entitlement to access.',
14597
14734
  };
14598
14735
  /**
14599
14736
  * Paywall Overlay Component
@@ -14672,17 +14809,24 @@ class PaywallOverlayComponent {
14672
14809
  if (this.message) {
14673
14810
  return this.getLocalizedString(this.message);
14674
14811
  }
14812
+ // A known lock reason is translated; otherwise the gate's own sentence.
14813
+ const key = 'paywall.reason_' + this.gate?.lockReason;
14814
+ const translated = this.t(key);
14815
+ if (translated !== key) {
14816
+ return translated;
14817
+ }
14675
14818
  if (this.gate?.reason) {
14676
14819
  return this.gate.reason;
14677
14820
  }
14678
14821
  return this.t('paywall.message_default');
14679
14822
  }
14680
14823
  /**
14681
- * Handle purchase option selection
14824
+ * Handle purchase option selection: `purchase` carries the product id,
14825
+ * `action` is 'subscribe' for a subscription option, else 'purchase'.
14682
14826
  */
14683
- onPurchaseOption(productId) {
14684
- this.purchase.emit(productId);
14685
- this.action.emit('purchase');
14827
+ onPurchaseOption(option) {
14828
+ this.purchase.emit(option.productId);
14829
+ this.action.emit(option.type === 'subscription' ? 'subscribe' : 'purchase');
14686
14830
  }
14687
14831
  /**
14688
14832
  * Handle login action
@@ -14745,18 +14889,14 @@ class PaywallOverlayComponent {
14745
14889
  * Manifest-provided texts (title/message/gate.reason) bypass this.
14746
14890
  */
14747
14891
  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] ?? ''));
14892
+ return uiText(this.translate, key, PAYWALL_TEXT_EN[key], params);
14753
14893
  }
14754
14894
  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 -->\r\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\r\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\r\n <!-- Header -->\r\n <div class=\"paywall-header\">\r\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"t('paywall.close')\"\r\n (click)=\"onDismiss()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"paywall-content\">\r\n <!-- Icon -->\r\n <div class=\"paywall-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\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\"/>\r\n </svg>\r\n </div>\r\n\r\n <!-- Message -->\r\n <p class=\"paywall-message\">{{ getMessage() }}</p>\r\n\r\n <!-- Preview Info -->\r\n @if (gate?.preview && allowPreview) {\r\n <div class=\"preview-info\">\r\n @if (gate?.preview?.previewPanels) {\r\n <p class=\"preview-text\">\r\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\r\n </p>\r\n }\r\n @if (gate?.preview?.mode) {\r\n <p class=\"preview-mode\">\r\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Purchase Options -->\r\n @if (purchaseOptions.length > 0) {\r\n <div class=\"purchase-options\">\r\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\r\n @for (option of purchaseOptions; track option.productId) {\r\n <button\r\n class=\"purchase-option\"\r\n type=\"button\"\r\n (click)=\"onPurchaseOption(option.productId)\">\r\n <div class=\"option-header\">\r\n <span class=\"option-name\">{{ option.name }}</span>\r\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\r\n </div>\r\n @if (option.description) {\r\n <p class=\"option-description\">{{ option.description }}</p>\r\n }\r\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Login Option -->\r\n @if (showLogin && !purchaseOptions.length) {\r\n <button\r\n class=\"login-btn\"\r\n type=\"button\"\r\n (click)=\"onLogin()\">\r\n {{ t('paywall.sign_in') }}\r\n </button>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"paywall-actions\">\r\n @if (purchaseOptions.length === 0 && !showLogin) {\r\n <button\r\n class=\"action-btn primary\"\r\n type=\"button\"\r\n (click)=\"onDismiss()\">\r\n {{ t('paywall.go_back') }}\r\n </button>\r\n }\r\n @if (purchaseOptions.length > 0 || showLogin) {\r\n <button\r\n class=\"action-btn secondary\"\r\n type=\"button\"\r\n (click)=\"onDismiss()\">\r\n {{ t('paywall.maybe_later') }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Footer Info -->\r\n <div class=\"paywall-footer\">\r\n <p class=\"footer-text\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\r\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\"/>\r\n </svg>\r\n {{ t('paywall.secure_payment') }}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\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 }); }
14895
+ 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
14896
  }
14757
14897
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, decorators: [{
14758
14898
  type: Component,
14759
- args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\r\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\r\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\r\n <!-- Header -->\r\n <div class=\"paywall-header\">\r\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"t('paywall.close')\"\r\n (click)=\"onDismiss()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"paywall-content\">\r\n <!-- Icon -->\r\n <div class=\"paywall-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\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\"/>\r\n </svg>\r\n </div>\r\n\r\n <!-- Message -->\r\n <p class=\"paywall-message\">{{ getMessage() }}</p>\r\n\r\n <!-- Preview Info -->\r\n @if (gate?.preview && allowPreview) {\r\n <div class=\"preview-info\">\r\n @if (gate?.preview?.previewPanels) {\r\n <p class=\"preview-text\">\r\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\r\n </p>\r\n }\r\n @if (gate?.preview?.mode) {\r\n <p class=\"preview-mode\">\r\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Purchase Options -->\r\n @if (purchaseOptions.length > 0) {\r\n <div class=\"purchase-options\">\r\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\r\n @for (option of purchaseOptions; track option.productId) {\r\n <button\r\n class=\"purchase-option\"\r\n type=\"button\"\r\n (click)=\"onPurchaseOption(option.productId)\">\r\n <div class=\"option-header\">\r\n <span class=\"option-name\">{{ option.name }}</span>\r\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\r\n </div>\r\n @if (option.description) {\r\n <p class=\"option-description\">{{ option.description }}</p>\r\n }\r\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Login Option -->\r\n @if (showLogin && !purchaseOptions.length) {\r\n <button\r\n class=\"login-btn\"\r\n type=\"button\"\r\n (click)=\"onLogin()\">\r\n {{ t('paywall.sign_in') }}\r\n </button>\r\n }\r\n\r\n <!-- Actions -->\r\n <div class=\"paywall-actions\">\r\n @if (purchaseOptions.length === 0 && !showLogin) {\r\n <button\r\n class=\"action-btn primary\"\r\n type=\"button\"\r\n (click)=\"onDismiss()\">\r\n {{ t('paywall.go_back') }}\r\n </button>\r\n }\r\n @if (purchaseOptions.length > 0 || showLogin) {\r\n <button\r\n class=\"action-btn secondary\"\r\n type=\"button\"\r\n (click)=\"onDismiss()\">\r\n {{ t('paywall.maybe_later') }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Footer Info -->\r\n <div class=\"paywall-footer\">\r\n <p class=\"footer-text\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\r\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\"/>\r\n </svg>\r\n {{ t('paywall.secure_payment') }}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\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"] }]
14899
+ 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
14900
  }], propDecorators: { visible: [{
14761
14901
  type: Input
14762
14902
  }], gate: [{
@@ -15404,11 +15544,11 @@ class CharacterRosterComponent {
15404
15544
  }
15405
15545
  }
15406
15546
  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) {\r\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\r\n \r\n <!-- LIST VIEW -->\r\n @if (currentView === 'list') {\r\n <!-- Header -->\r\n <div class=\"roster-header\">\r\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"roster-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'character_roster.search' | translate\"\r\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Character grid -->\r\n <div class=\"roster-content\">\r\n @if (filteredCharacters.length > 0) {\r\n <div class=\"character-grid\" role=\"list\">\r\n @for (character of filteredCharacters; track character.id) {\r\n <button\r\n class=\"character-card\"\r\n type=\"button\"\r\n role=\"listitem\"\r\n (click)=\"selectCharacter(character)\">\r\n \r\n <!-- Avatar -->\r\n <div class=\"character-avatar\">\r\n @if (getAvatarUrl(character)) {\r\n <img\r\n [src]=\"getAvatarUrl(character)\"\r\n [alt]=\"getCharacterName(character)\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getInitials(character) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Info -->\r\n <div class=\"character-info\">\r\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\r\n @if (character['role']) {\r\n <p class=\"character-role\">{{ character['role'] }}</p>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"roster-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n <!-- DETAIL VIEW -->\r\n @if (currentView === 'detail' && selectedCharacter) {\r\n <!-- Header with back button -->\r\n <div class=\"roster-header\">\r\n <button\r\n class=\"back-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\r\n (click)=\"backToList()\">\r\n {{ 'character_roster.back_to_list' | translate }}\r\n </button>\r\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_sheet.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"sheet-content\">\r\n <!-- Portrait -->\r\n <div class=\"portrait-section\">\r\n <div class=\"portrait-container\">\r\n @if (getAvatarUrl(selectedCharacter)) {\r\n <img\r\n [src]=\"getAvatarUrl(selectedCharacter)\"\r\n [alt]=\"getCharacterName(selectedCharacter)\"\r\n class=\"portrait-image\" />\r\n } @else {\r\n <div class=\"portrait-placeholder\">\r\n {{ getInitials(selectedCharacter) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Role badge -->\r\n @if (selectedCharacter['role']) {\r\n <div class=\"role-badge\">\r\n {{ selectedCharacter['role'] }}\r\n </div>\r\n }\r\n\r\n <!-- Voice sample button -->\r\n @if (hasVoiceSample(selectedCharacter)) {\r\n <button\r\n class=\"voice-btn\"\r\n [class.playing]=\"isPlaying\"\r\n type=\"button\"\r\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\r\n (click)=\"playVoiceSample(selectedCharacter)\">\r\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\r\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Bio -->\r\n <div class=\"bio-section\">\r\n <h3 class=\"bio-title\">Biography</h3>\r\n @if (getCharacterBio(selectedCharacter)) {\r\n <div class=\"bio-content\">\r\n {{ getCharacterBio(selectedCharacter) }}\r\n </div>\r\n } @else {\r\n <p class=\"bio-empty\">No biography available.</p>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"sheet-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n </div>\r\n </div>\r\n}\r\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 }); }
15547
+ 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 }); }
15408
15548
  }
15409
15549
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CharacterRosterComponent, decorators: [{
15410
15550
  type: Component,
15411
- args: [{ selector: 'pw-character-roster', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\r\n \r\n <!-- LIST VIEW -->\r\n @if (currentView === 'list') {\r\n <!-- Header -->\r\n <div class=\"roster-header\">\r\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"roster-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'character_roster.search' | translate\"\r\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Character grid -->\r\n <div class=\"roster-content\">\r\n @if (filteredCharacters.length > 0) {\r\n <div class=\"character-grid\" role=\"list\">\r\n @for (character of filteredCharacters; track character.id) {\r\n <button\r\n class=\"character-card\"\r\n type=\"button\"\r\n role=\"listitem\"\r\n (click)=\"selectCharacter(character)\">\r\n \r\n <!-- Avatar -->\r\n <div class=\"character-avatar\">\r\n @if (getAvatarUrl(character)) {\r\n <img\r\n [src]=\"getAvatarUrl(character)\"\r\n [alt]=\"getCharacterName(character)\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getInitials(character) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Info -->\r\n <div class=\"character-info\">\r\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\r\n @if (character['role']) {\r\n <p class=\"character-role\">{{ character['role'] }}</p>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"roster-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n <!-- DETAIL VIEW -->\r\n @if (currentView === 'detail' && selectedCharacter) {\r\n <!-- Header with back button -->\r\n <div class=\"roster-header\">\r\n <button\r\n class=\"back-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\r\n (click)=\"backToList()\">\r\n {{ 'character_roster.back_to_list' | translate }}\r\n </button>\r\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_sheet.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"sheet-content\">\r\n <!-- Portrait -->\r\n <div class=\"portrait-section\">\r\n <div class=\"portrait-container\">\r\n @if (getAvatarUrl(selectedCharacter)) {\r\n <img\r\n [src]=\"getAvatarUrl(selectedCharacter)\"\r\n [alt]=\"getCharacterName(selectedCharacter)\"\r\n class=\"portrait-image\" />\r\n } @else {\r\n <div class=\"portrait-placeholder\">\r\n {{ getInitials(selectedCharacter) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Role badge -->\r\n @if (selectedCharacter['role']) {\r\n <div class=\"role-badge\">\r\n {{ selectedCharacter['role'] }}\r\n </div>\r\n }\r\n\r\n <!-- Voice sample button -->\r\n @if (hasVoiceSample(selectedCharacter)) {\r\n <button\r\n class=\"voice-btn\"\r\n [class.playing]=\"isPlaying\"\r\n type=\"button\"\r\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\r\n (click)=\"playVoiceSample(selectedCharacter)\">\r\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\r\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Bio -->\r\n <div class=\"bio-section\">\r\n <h3 class=\"bio-title\">Biography</h3>\r\n @if (getCharacterBio(selectedCharacter)) {\r\n <div class=\"bio-content\">\r\n {{ getCharacterBio(selectedCharacter) }}\r\n </div>\r\n } @else {\r\n <p class=\"bio-empty\">No biography available.</p>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"sheet-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n </div>\r\n </div>\r\n}\r\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"] }]
15551
+ 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"] }]
15412
15552
  }], propDecorators: { characters: [{
15413
15553
  type: Input
15414
15554
  }], locale: [{
@@ -15673,11 +15813,11 @@ class ExtrasViewerComponent {
15673
15813
  }
15674
15814
  }
15675
15815
  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) {\r\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\r\n <!-- Header -->\r\n <div class=\"extras-header\">\r\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Filters -->\r\n <div class=\"extras-filters\">\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'all'\"\r\n type=\"button\"\r\n (click)=\"setFilter('all')\">\r\n {{ 'extras.all' | translate }} ({{ extras.length }})\r\n </button>\r\n @if (getExtrasByType('cover') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'cover'\"\r\n type=\"button\"\r\n (click)=\"setFilter('cover')\">\r\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('art') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'art'\"\r\n type=\"button\"\r\n (click)=\"setFilter('art')\">\r\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('bts') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'bts'\"\r\n type=\"button\"\r\n (click)=\"setFilter('bts')\">\r\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('interview') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'interview'\"\r\n type=\"button\"\r\n (click)=\"setFilter('interview')\">\r\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('other') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'other'\"\r\n type=\"button\"\r\n (click)=\"setFilter('other')\">\r\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Gallery Grid -->\r\n <div class=\"extras-content\">\r\n @if (getFilteredExtras().length > 0) {\r\n <div class=\"extras-grid\" role=\"list\">\r\n @for (extra of getFilteredExtras(); track extra.id) {\r\n <div class=\"extra-card\" role=\"listitem\">\r\n <button\r\n class=\"extra-thumbnail\"\r\n [class.locked]=\"!isAccessible(extra)\"\r\n type=\"button\"\r\n (click)=\"openExtra(extra)\">\r\n \r\n @if (getThumbnailUrl(extra)) {\r\n <img\r\n [src]=\"getThumbnailUrl(extra)\"\r\n [alt]=\"getExtraTitle(extra)\"\r\n class=\"thumbnail-image\" />\r\n } @else {\r\n <div class=\"thumbnail-placeholder\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n }\r\n\r\n <!-- Lock overlay for gated content -->\r\n @if (!isAccessible(extra)) {\r\n <div class=\"lock-overlay\">\r\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Media type badge -->\r\n <div class=\"media-badge\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n </button>\r\n\r\n <div class=\"extra-info\">\r\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\r\n @if (getExtraDescription(extra)) {\r\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'extras.no_extras' | translate }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail View -->\r\n @if (selectedExtra) {\r\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\r\n <div class=\"detail-container\">\r\n <!-- Detail Header -->\r\n <div class=\"detail-header\">\r\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close_detail' | translate\"\r\n (click)=\"closeDetail()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Detail Content -->\r\n <div class=\"detail-content\">\r\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\r\n <img\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n [alt]=\"getExtraTitle(selectedExtra)\"\r\n class=\"detail-image\" />\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'video') {\r\n <video\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-video\">\r\n {{ 'extras.video_not_supported' | translate }}\r\n </video>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'audio') {\r\n <audio\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-audio\">\r\n {{ 'extras.audio_not_supported' | translate }}\r\n </audio>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\r\n <a\r\n class=\"action-btn detail-document-link\"\r\n [href]=\"getAssetUrl(selectedExtra)\"\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\">\r\n {{ 'extras.open_document' | translate }}\r\n </a>\r\n }\r\n\r\n @if (getExtraDescription(selectedExtra)) {\r\n <div class=\"detail-description\">\r\n {{ getExtraDescription(selectedExtra) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail Footer -->\r\n <div class=\"detail-footer\">\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"closeDetail()\">\r\n {{ 'extras.close_button' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Paywall Notice (for gated content without access) -->\r\n @if (!hasPremiumAccess && hasGatedExtras()) {\r\n <div class=\"paywall-notice\">\r\n <p class=\"notice-text\">\r\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n {{ 'extras.premium_notice' | translate }}\r\n </p>\r\n <button\r\n class=\"upgrade-btn\"\r\n type=\"button\"\r\n (click)=\"onPurchase()\">\r\n {{ 'extras.upgrade_now' | translate }}\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n}\r\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 }); }
15816
+ 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 }); }
15677
15817
  }
15678
15818
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ExtrasViewerComponent, decorators: [{
15679
15819
  type: Component,
15680
- args: [{ selector: 'pw-extras-viewer', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\r\n <!-- Header -->\r\n <div class=\"extras-header\">\r\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Filters -->\r\n <div class=\"extras-filters\">\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'all'\"\r\n type=\"button\"\r\n (click)=\"setFilter('all')\">\r\n {{ 'extras.all' | translate }} ({{ extras.length }})\r\n </button>\r\n @if (getExtrasByType('cover') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'cover'\"\r\n type=\"button\"\r\n (click)=\"setFilter('cover')\">\r\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('art') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'art'\"\r\n type=\"button\"\r\n (click)=\"setFilter('art')\">\r\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('bts') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'bts'\"\r\n type=\"button\"\r\n (click)=\"setFilter('bts')\">\r\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('interview') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'interview'\"\r\n type=\"button\"\r\n (click)=\"setFilter('interview')\">\r\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('other') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'other'\"\r\n type=\"button\"\r\n (click)=\"setFilter('other')\">\r\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Gallery Grid -->\r\n <div class=\"extras-content\">\r\n @if (getFilteredExtras().length > 0) {\r\n <div class=\"extras-grid\" role=\"list\">\r\n @for (extra of getFilteredExtras(); track extra.id) {\r\n <div class=\"extra-card\" role=\"listitem\">\r\n <button\r\n class=\"extra-thumbnail\"\r\n [class.locked]=\"!isAccessible(extra)\"\r\n type=\"button\"\r\n (click)=\"openExtra(extra)\">\r\n \r\n @if (getThumbnailUrl(extra)) {\r\n <img\r\n [src]=\"getThumbnailUrl(extra)\"\r\n [alt]=\"getExtraTitle(extra)\"\r\n class=\"thumbnail-image\" />\r\n } @else {\r\n <div class=\"thumbnail-placeholder\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n }\r\n\r\n <!-- Lock overlay for gated content -->\r\n @if (!isAccessible(extra)) {\r\n <div class=\"lock-overlay\">\r\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Media type badge -->\r\n <div class=\"media-badge\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n </button>\r\n\r\n <div class=\"extra-info\">\r\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\r\n @if (getExtraDescription(extra)) {\r\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'extras.no_extras' | translate }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail View -->\r\n @if (selectedExtra) {\r\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\r\n <div class=\"detail-container\">\r\n <!-- Detail Header -->\r\n <div class=\"detail-header\">\r\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close_detail' | translate\"\r\n (click)=\"closeDetail()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Detail Content -->\r\n <div class=\"detail-content\">\r\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\r\n <img\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n [alt]=\"getExtraTitle(selectedExtra)\"\r\n class=\"detail-image\" />\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'video') {\r\n <video\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-video\">\r\n {{ 'extras.video_not_supported' | translate }}\r\n </video>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'audio') {\r\n <audio\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-audio\">\r\n {{ 'extras.audio_not_supported' | translate }}\r\n </audio>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\r\n <a\r\n class=\"action-btn detail-document-link\"\r\n [href]=\"getAssetUrl(selectedExtra)\"\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\">\r\n {{ 'extras.open_document' | translate }}\r\n </a>\r\n }\r\n\r\n @if (getExtraDescription(selectedExtra)) {\r\n <div class=\"detail-description\">\r\n {{ getExtraDescription(selectedExtra) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail Footer -->\r\n <div class=\"detail-footer\">\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"closeDetail()\">\r\n {{ 'extras.close_button' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Paywall Notice (for gated content without access) -->\r\n @if (!hasPremiumAccess && hasGatedExtras()) {\r\n <div class=\"paywall-notice\">\r\n <p class=\"notice-text\">\r\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n {{ 'extras.premium_notice' | translate }}\r\n </p>\r\n <button\r\n class=\"upgrade-btn\"\r\n type=\"button\"\r\n (click)=\"onPurchase()\">\r\n {{ 'extras.upgrade_now' | translate }}\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n}\r\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"] }]
15820
+ 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"] }]
15681
15821
  }], propDecorators: { extras: [{
15682
15822
  type: Input
15683
15823
  }], initialExtraId: [{
@@ -15741,11 +15881,11 @@ class ActionModalComponent {
15741
15881
  }
15742
15882
  }
15743
15883
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ActionModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15744
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ActionModalComponent, isStandalone: true, selector: "pw-action-modal", inputs: { visible: "visible", locale: "locale", title: "title", content: "content" }, outputs: { close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\r\n <div class=\"action-modal-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); close.emit()\">\r\n <div class=\"action-modal-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"resolved(title)\">\r\n <div class=\"action-modal-header\">\r\n <h2 class=\"action-modal-title\">{{ resolved(title) }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n aria-label=\"Close\"\r\n (click)=\"close.emit()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n <!-- Plain text only: manifest content is untrusted, never innerHTML -->\r\n <div class=\"action-modal-content\">{{ resolved(content) }}</div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".action-modal-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}}.action-modal-container{width:90%;max-width:480px;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)}}.action-modal-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}.action-modal-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;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.action-modal-content{padding:20px 24px;color:#ffffffd9;font-size:15px;line-height:1.6;overflow-y:auto;white-space:pre-line}@media (prefers-reduced-motion: reduce){.action-modal-overlay,.action-modal-container{animation:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15884
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ActionModalComponent, isStandalone: true, selector: "pw-action-modal", inputs: { visible: "visible", locale: "locale", title: "title", content: "content" }, outputs: { close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"action-modal-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); close.emit()\">\n <div class=\"action-modal-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"resolved(title)\">\n <div class=\"action-modal-header\">\n <h2 class=\"action-modal-title\">{{ resolved(title) }}</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 <!-- Plain text only: manifest content is untrusted, never innerHTML -->\n <div class=\"action-modal-content\">{{ resolved(content) }}</div>\n </div>\n </div>\n}\n", styles: [".action-modal-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}}.action-modal-container{width:90%;max-width:480px;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)}}.action-modal-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}.action-modal-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;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.action-modal-content{padding:20px 24px;color:#ffffffd9;font-size:15px;line-height:1.6;overflow-y:auto;white-space:pre-line}@media (prefers-reduced-motion: reduce){.action-modal-overlay,.action-modal-container{animation:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15745
15885
  }
15746
15886
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ActionModalComponent, decorators: [{
15747
15887
  type: Component,
15748
- args: [{ selector: 'pw-action-modal', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"action-modal-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); close.emit()\">\r\n <div class=\"action-modal-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"resolved(title)\">\r\n <div class=\"action-modal-header\">\r\n <h2 class=\"action-modal-title\">{{ resolved(title) }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n aria-label=\"Close\"\r\n (click)=\"close.emit()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n <!-- Plain text only: manifest content is untrusted, never innerHTML -->\r\n <div class=\"action-modal-content\">{{ resolved(content) }}</div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".action-modal-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}}.action-modal-container{width:90%;max-width:480px;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)}}.action-modal-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}.action-modal-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;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.action-modal-content{padding:20px 24px;color:#ffffffd9;font-size:15px;line-height:1.6;overflow-y:auto;white-space:pre-line}@media (prefers-reduced-motion: reduce){.action-modal-overlay,.action-modal-container{animation:none}}\n"] }]
15888
+ args: [{ selector: 'pw-action-modal', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"action-modal-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); close.emit()\">\n <div class=\"action-modal-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"resolved(title)\">\n <div class=\"action-modal-header\">\n <h2 class=\"action-modal-title\">{{ resolved(title) }}</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 <!-- Plain text only: manifest content is untrusted, never innerHTML -->\n <div class=\"action-modal-content\">{{ resolved(content) }}</div>\n </div>\n </div>\n}\n", styles: [".action-modal-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}}.action-modal-container{width:90%;max-width:480px;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)}}.action-modal-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}.action-modal-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;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.action-modal-content{padding:20px 24px;color:#ffffffd9;font-size:15px;line-height:1.6;overflow-y:auto;white-space:pre-line}@media (prefers-reduced-motion: reduce){.action-modal-overlay,.action-modal-container{animation:none}}\n"] }]
15749
15889
  }], propDecorators: { visible: [{
15750
15890
  type: Input
15751
15891
  }], locale: [{
@@ -15765,12 +15905,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
15765
15905
  * Age Gate Component
15766
15906
  * Displays age verification UI for restricted content
15767
15907
  */
15908
+ /**
15909
+ * Built-in English UI strings (keys under `age_gate.`), used when no
15910
+ * TranslateService is provided or a key is not loaded. Must match the
15911
+ * "age_gate" section of assets/i18n/en.json.
15912
+ */
15913
+ const AGE_GATE_TEXT_EN = {
15914
+ title: 'Age Verification Required',
15915
+ close: 'Close',
15916
+ warning: 'This content is restricted to users {{age}} years of age or older.',
15917
+ prompt: 'Please enter your birth date:',
15918
+ month: 'Month',
15919
+ day: 'Day',
15920
+ year: 'Year',
15921
+ verify: 'Verify Age',
15922
+ privacy: 'Your information is private and will not be stored.',
15923
+ error_incomplete: 'Please enter your complete birth date.',
15924
+ error_month: 'Please enter a valid month (1-12).',
15925
+ error_day: 'Please enter a valid day (1-{{max}}).',
15926
+ error_year: 'Please enter a valid year (1900-{{max}}).',
15927
+ error_too_young: 'You must be at least {{age}} years old to access this content.',
15928
+ };
15768
15929
  /**
15769
15930
  * Age Gate Component
15770
15931
  * Modal overlay for age verification
15771
15932
  */
15772
15933
  class AgeGateComponent {
15773
15934
  constructor() {
15935
+ /** Optional: the gate is a public component and works (in English) without ngx-translate. */
15936
+ this.translate = inject(TranslateService, { optional: true });
15774
15937
  /**
15775
15938
  * Visibility state
15776
15939
  */
@@ -15821,7 +15984,11 @@ class AgeGateComponent {
15821
15984
  if (this.warningMessage) {
15822
15985
  return this.warningMessage;
15823
15986
  }
15824
- return `This content is restricted to users ${this.minimumAge} years of age or older.`;
15987
+ return this.t('warning', { age: this.minimumAge });
15988
+ }
15989
+ /** Translate an `age_gate.*` UI string (English fallback). */
15990
+ t(key, params) {
15991
+ return uiText(this.translate, 'age_gate.' + key, AGE_GATE_TEXT_EN[key], params);
15825
15992
  }
15826
15993
  /**
15827
15994
  * Handle verification submit
@@ -15830,7 +15997,7 @@ class AgeGateComponent {
15830
15997
  this.errorMessage = '';
15831
15998
  // Validate inputs
15832
15999
  if (!this.birthMonth || !this.birthDay || !this.birthYear) {
15833
- this.errorMessage = 'Please enter your complete birth date.';
16000
+ this.errorMessage = this.t('error_incomplete');
15834
16001
  return;
15835
16002
  }
15836
16003
  const month = parseInt(this.birthMonth, 10);
@@ -15838,30 +16005,30 @@ class AgeGateComponent {
15838
16005
  const year = parseInt(this.birthYear, 10);
15839
16006
  // Validate ranges
15840
16007
  if (month < 1 || month > 12) {
15841
- this.errorMessage = 'Please enter a valid month (1-12).';
16008
+ this.errorMessage = this.t('error_month');
15842
16009
  return;
15843
16010
  }
15844
16011
  if (day < 1 || day > 31) {
15845
- this.errorMessage = 'Please enter a valid day (1-31).';
16012
+ this.errorMessage = this.t('error_day', { max: 31 });
15846
16013
  return;
15847
16014
  }
15848
16015
  const currentYear = new Date().getFullYear();
15849
16016
  if (year < 1900 || year > currentYear) {
15850
- this.errorMessage = `Please enter a valid year (1900-${currentYear}).`;
16017
+ this.errorMessage = this.t('error_year', { max: currentYear });
15851
16018
  return;
15852
16019
  }
15853
16020
  // Reject calendar-impossible dates (31 Apr, 30 Feb, 29 Feb in non-leap
15854
16021
  // years): new Date() would silently roll them over into the next month.
15855
16022
  const daysInMonth = new Date(year, month, 0).getDate();
15856
16023
  if (day > daysInMonth) {
15857
- this.errorMessage = `Please enter a valid day (1-${daysInMonth}).`;
16024
+ this.errorMessage = this.t('error_day', { max: daysInMonth });
15858
16025
  return;
15859
16026
  }
15860
16027
  // Calculate age
15861
16028
  const birthDate = new Date(year, month - 1, day);
15862
16029
  const age = this.calculateAge(birthDate);
15863
16030
  if (age < this.minimumAge) {
15864
- this.errorMessage = `You must be at least ${this.minimumAge} years old to access this content.`;
16031
+ this.errorMessage = this.t('error_too_young', { age: this.minimumAge });
15865
16032
  this.verify.emit({
15866
16033
  verified: false,
15867
16034
  age,
@@ -15906,17 +16073,26 @@ class AgeGateComponent {
15906
16073
  return age;
15907
16074
  }
15908
16075
  /**
15909
- * Get month options
16076
+ * Month options, named in the current locale (Intl; cached per locale).
15910
16077
  */
15911
16078
  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
- }));
16079
+ if (this.monthCache?.locale !== this.locale) {
16080
+ let format;
16081
+ try {
16082
+ format = new Intl.DateTimeFormat(this.locale, { month: 'long', timeZone: 'UTC' });
16083
+ }
16084
+ catch {
16085
+ format = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' });
16086
+ }
16087
+ this.monthCache = {
16088
+ locale: this.locale,
16089
+ options: Array.from({ length: 12 }, (_, index) => ({
16090
+ value: String(index + 1),
16091
+ label: format.format(Date.UTC(2000, index, 1)),
16092
+ })),
16093
+ };
16094
+ }
16095
+ return this.monthCache.options;
15920
16096
  }
15921
16097
  /**
15922
16098
  * Get day options
@@ -15936,11 +16112,11 @@ class AgeGateComponent {
15936
16112
  return years;
15937
16113
  }
15938
16114
  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 -->\r\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\r\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Age Verification\">\r\n <!-- Header -->\r\n <div class=\"age-gate-header\">\r\n <div class=\"warning-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\r\n </svg>\r\n </div>\r\n <h2 class=\"age-gate-title\">Age Verification Required</h2>\r\n @if (allowDismiss) {\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n aria-label=\"Close\"\r\n (click)=\"onDismiss()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"age-gate-content\">\r\n <!-- Warning Message -->\r\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\r\n\r\n <!-- Birth Date Form -->\r\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"form-label\">Please enter your birth date:</div>\r\n\r\n <div class=\"date-inputs\">\r\n <!-- Month -->\r\n <div class=\"input-group\">\r\n <label for=\"birth-month\" class=\"input-label\">Month</label>\r\n <select\r\n id=\"birth-month\"\r\n class=\"date-input\"\r\n [(ngModel)]=\"birthMonth\"\r\n name=\"birthMonth\"\r\n required>\r\n <option value=\"\">--</option>\r\n @for (month of getMonthOptions(); track month.value) {\r\n <option [value]=\"month.value\">{{ month.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n <!-- Day -->\r\n <div class=\"input-group\">\r\n <label for=\"birth-day\" class=\"input-label\">Day</label>\r\n <select\r\n id=\"birth-day\"\r\n class=\"date-input\"\r\n [(ngModel)]=\"birthDay\"\r\n name=\"birthDay\"\r\n required>\r\n <option value=\"\">--</option>\r\n @for (day of getDayOptions(); track day) {\r\n <option [value]=\"day\">{{ day }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n <!-- Year -->\r\n <div class=\"input-group\">\r\n <label for=\"birth-year\" class=\"input-label\">Year</label>\r\n <select\r\n id=\"birth-year\"\r\n class=\"date-input\"\r\n [(ngModel)]=\"birthYear\"\r\n name=\"birthYear\"\r\n required>\r\n <option value=\"\">----</option>\r\n @for (year of getYearOptions(); track year) {\r\n <option [value]=\"year\">{{ year }}</option>\r\n }\r\n </select>\r\n </div>\r\n </div>\r\n\r\n <!-- Error Message -->\r\n @if (errorMessage) {\r\n <div class=\"error-message\" role=\"alert\">\r\n {{ errorMessage }}\r\n </div>\r\n }\r\n\r\n <!-- Submit Button -->\r\n <button\r\n type=\"submit\"\r\n class=\"verify-btn\">\r\n Verify Age\r\n </button>\r\n </form>\r\n\r\n <!-- Privacy Notice -->\r\n <div class=\"privacy-notice\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\r\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\"/>\r\n </svg>\r\n <span>Your information is private and will not be stored.</span>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\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 }); }
16115
+ 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$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 }); }
15940
16116
  }
15941
16117
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, decorators: [{
15942
16118
  type: Component,
15943
- args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\r\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\r\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Age Verification\">\r\n <!-- Header -->\r\n <div class=\"age-gate-header\">\r\n <div class=\"warning-icon\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\r\n </svg>\r\n </div>\r\n <h2 class=\"age-gate-title\">Age Verification Required</h2>\r\n @if (allowDismiss) {\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n aria-label=\"Close\"\r\n (click)=\"onDismiss()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"age-gate-content\">\r\n <!-- Warning Message -->\r\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\r\n\r\n <!-- Birth Date Form -->\r\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\r\n <div class=\"form-label\">Please enter your birth date:</div>\r\n\r\n <div class=\"date-inputs\">\r\n <!-- Month -->\r\n <div class=\"input-group\">\r\n <label for=\"birth-month\" class=\"input-label\">Month</label>\r\n <select\r\n id=\"birth-month\"\r\n class=\"date-input\"\r\n [(ngModel)]=\"birthMonth\"\r\n name=\"birthMonth\"\r\n required>\r\n <option value=\"\">--</option>\r\n @for (month of getMonthOptions(); track month.value) {\r\n <option [value]=\"month.value\">{{ month.label }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n <!-- Day -->\r\n <div class=\"input-group\">\r\n <label for=\"birth-day\" class=\"input-label\">Day</label>\r\n <select\r\n id=\"birth-day\"\r\n class=\"date-input\"\r\n [(ngModel)]=\"birthDay\"\r\n name=\"birthDay\"\r\n required>\r\n <option value=\"\">--</option>\r\n @for (day of getDayOptions(); track day) {\r\n <option [value]=\"day\">{{ day }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n <!-- Year -->\r\n <div class=\"input-group\">\r\n <label for=\"birth-year\" class=\"input-label\">Year</label>\r\n <select\r\n id=\"birth-year\"\r\n class=\"date-input\"\r\n [(ngModel)]=\"birthYear\"\r\n name=\"birthYear\"\r\n required>\r\n <option value=\"\">----</option>\r\n @for (year of getYearOptions(); track year) {\r\n <option [value]=\"year\">{{ year }}</option>\r\n }\r\n </select>\r\n </div>\r\n </div>\r\n\r\n <!-- Error Message -->\r\n @if (errorMessage) {\r\n <div class=\"error-message\" role=\"alert\">\r\n {{ errorMessage }}\r\n </div>\r\n }\r\n\r\n <!-- Submit Button -->\r\n <button\r\n type=\"submit\"\r\n class=\"verify-btn\">\r\n Verify Age\r\n </button>\r\n </form>\r\n\r\n <!-- Privacy Notice -->\r\n <div class=\"privacy-notice\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\r\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\"/>\r\n </svg>\r\n <span>Your information is private and will not be stored.</span>\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\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"] }]
16119
+ 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
16120
  }], propDecorators: { visible: [{
15945
16121
  type: Input
15946
16122
  }], minimumAge: [{
@@ -15967,8 +16143,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
15967
16143
  * priority. Opened from the toolbar's "Choices" button; follows the shell's
15968
16144
  * modal contract (visible input + close output).
15969
16145
  */
16146
+ /** English fallback for the `branch_chooser.*` keys (see assets/i18n/en.json). */
16147
+ const BRANCH_TEXT_EN = {
16148
+ title: 'Choose your path',
16149
+ close: 'Close',
16150
+ option: 'Option {{n}}',
16151
+ };
15970
16152
  class BranchChooserComponent {
15971
16153
  constructor() {
16154
+ this.translate = inject(TranslateService, { optional: true });
15972
16155
  /** Visible state */
15973
16156
  this.visible = false;
15974
16157
  /** Current locale (labels are already resolved by the shell). */
@@ -15980,6 +16163,10 @@ class BranchChooserComponent {
15980
16163
  /** Close requested without choosing. */
15981
16164
  this.close = new EventEmitter();
15982
16165
  }
16166
+ /** Translate a `branch_chooser.*` UI string (English fallback). */
16167
+ t(key, params) {
16168
+ return uiText(this.translate, 'branch_chooser.' + key, BRANCH_TEXT_EN[key], params);
16169
+ }
15983
16170
  onEscape() {
15984
16171
  if (this.visible) {
15985
16172
  this.close.emit();
@@ -15994,11 +16181,11 @@ class BranchChooserComponent {
15994
16181
  return choice.index;
15995
16182
  }
15996
16183
  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 }); }
16184
+ 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
16185
  }
15999
16186
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, decorators: [{
16000
16187
  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"] }]
16188
+ 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
16189
  }], propDecorators: { visible: [{
16003
16190
  type: Input
16004
16191
  }], locale: [{
@@ -16314,11 +16501,11 @@ class ShareModalComponent {
16314
16501
  }
16315
16502
  }
16316
16503
  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) {\r\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\r\n <!-- Header -->\r\n <div class=\"share-header\">\r\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'share.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"share-content\">\r\n @if (!shareable) {\r\n <!-- Not shareable message -->\r\n <div class=\"not-shareable\">\r\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\r\n <p>{{ 'share.not_shareable' | translate }}</p>\r\n </div>\r\n } @else {\r\n <!-- Copy Link -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\r\n <div class=\"link-container\">\r\n <input\r\n type=\"text\"\r\n class=\"link-input\"\r\n [value]=\"shareUrl\"\r\n readonly\r\n [attr.aria-label]=\"'share.share_url' | translate\" />\r\n <button\r\n class=\"copy-btn\"\r\n [class.success]=\"copySuccess\"\r\n type=\"button\"\r\n (click)=\"copyLink()\">\r\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\r\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Native Share (if supported) -->\r\n @if (isNativeShareSupported) {\r\n <div class=\"share-section\">\r\n <button\r\n class=\"native-share-btn\"\r\n type=\"button\"\r\n (click)=\"useNativeShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Social Share Buttons -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\r\n <div class=\"social-buttons\">\r\n <button\r\n class=\"social-btn twitter\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\r\n (click)=\"shareOnTwitter()\">\r\n <span class=\"social-icon\">\uD835\uDD4F</span>\r\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn facebook\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\r\n (click)=\"shareOnFacebook()\">\r\n <span class=\"social-icon\">f</span>\r\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn reddit\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\r\n (click)=\"shareOnReddit()\">\r\n <span class=\"social-icon\">\u25B2</span>\r\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn email\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\r\n (click)=\"shareViaEmail()\">\r\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\r\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- QR Code -->\r\n <div class=\"share-section\">\r\n <button\r\n class=\"qr-toggle-btn\"\r\n type=\"button\"\r\n (click)=\"toggleQrCode()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\r\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\r\n </button>\r\n\r\n @if (showQrCode) {\r\n <div class=\"qr-code-container\">\r\n @if (qrCodeDataUrl) {\r\n <img\r\n [src]=\"qrCodeDataUrl\"\r\n [alt]=\"'share.qr_code' | translate\"\r\n class=\"qr-code-image\" />\r\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\r\n } @else {\r\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Share Info -->\r\n <div class=\"share-info\">\r\n <p class=\"info-text\">\r\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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 }); }
16504
+ 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 }); }
16318
16505
  }
16319
16506
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ShareModalComponent, decorators: [{
16320
16507
  type: Component,
16321
- args: [{ selector: 'pw-share-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\r\n <!-- Header -->\r\n <div class=\"share-header\">\r\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'share.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"share-content\">\r\n @if (!shareable) {\r\n <!-- Not shareable message -->\r\n <div class=\"not-shareable\">\r\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\r\n <p>{{ 'share.not_shareable' | translate }}</p>\r\n </div>\r\n } @else {\r\n <!-- Copy Link -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\r\n <div class=\"link-container\">\r\n <input\r\n type=\"text\"\r\n class=\"link-input\"\r\n [value]=\"shareUrl\"\r\n readonly\r\n [attr.aria-label]=\"'share.share_url' | translate\" />\r\n <button\r\n class=\"copy-btn\"\r\n [class.success]=\"copySuccess\"\r\n type=\"button\"\r\n (click)=\"copyLink()\">\r\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\r\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Native Share (if supported) -->\r\n @if (isNativeShareSupported) {\r\n <div class=\"share-section\">\r\n <button\r\n class=\"native-share-btn\"\r\n type=\"button\"\r\n (click)=\"useNativeShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Social Share Buttons -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\r\n <div class=\"social-buttons\">\r\n <button\r\n class=\"social-btn twitter\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\r\n (click)=\"shareOnTwitter()\">\r\n <span class=\"social-icon\">\uD835\uDD4F</span>\r\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn facebook\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\r\n (click)=\"shareOnFacebook()\">\r\n <span class=\"social-icon\">f</span>\r\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn reddit\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\r\n (click)=\"shareOnReddit()\">\r\n <span class=\"social-icon\">\u25B2</span>\r\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn email\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\r\n (click)=\"shareViaEmail()\">\r\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\r\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- QR Code -->\r\n <div class=\"share-section\">\r\n <button\r\n class=\"qr-toggle-btn\"\r\n type=\"button\"\r\n (click)=\"toggleQrCode()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\r\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\r\n </button>\r\n\r\n @if (showQrCode) {\r\n <div class=\"qr-code-container\">\r\n @if (qrCodeDataUrl) {\r\n <img\r\n [src]=\"qrCodeDataUrl\"\r\n [alt]=\"'share.qr_code' | translate\"\r\n class=\"qr-code-image\" />\r\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\r\n } @else {\r\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Share Info -->\r\n <div class=\"share-info\">\r\n <p class=\"info-text\">\r\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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"] }]
16508
+ 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"] }]
16322
16509
  }], propDecorators: { shareUrl: [{
16323
16510
  type: Input
16324
16511
  }], shareTitle: [{
@@ -16557,11 +16744,11 @@ class CommentsDrawerComponent {
16557
16744
  }
16558
16745
  }
16559
16746
  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) {\r\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\r\n <!-- Header -->\r\n <div class=\"drawer-header\">\r\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'comments.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Comment Composer (Top) -->\r\n @if (isAuthenticated) {\r\n <div class=\"comment-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_comment' | translate\"\r\n rows=\"3\"\r\n [(ngModel)]=\"newCommentContent\"\r\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!newCommentContent.trim()\"\r\n (click)=\"submitComment()\">\r\n {{ 'comments.post_comment' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"auth-notice\">\r\n <p>{{ 'comments.sign_in' | translate }}</p>\r\n </div>\r\n }\r\n\r\n <!-- Comments List -->\r\n <div class=\"comments-content\">\r\n @if (loading && comments.length === 0) {\r\n <div class=\"loading-state\">\r\n <div class=\"loading-spinner\"></div>\r\n <p>{{ 'comments.loading' | translate }}</p>\r\n </div>\r\n } @else if (comments.length === 0) {\r\n <div class=\"empty-state\">\r\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n <p>{{ 'comments.no_comments' | translate }}</p>\r\n </div>\r\n } @else {\r\n <div class=\"comments-list\">\r\n @for (comment of comments; track comment.id) {\r\n <div class=\"comment-item\">\r\n <!-- Avatar -->\r\n <div class=\"comment-avatar\">\r\n @if (comment.authorAvatar) {\r\n <img\r\n [src]=\"comment.authorAvatar\"\r\n [alt]=\"comment.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(comment.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ comment.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\r\n </div>\r\n\r\n <p class=\"comment-text\">{{ comment.content }}</p>\r\n\r\n <!-- Actions -->\r\n <div class=\"comment-actions\">\r\n @if (isAuthenticated) {\r\n <button\r\n class=\"action-btn\"\r\n [class.active]=\"comment.isLiked\"\r\n type=\"button\"\r\n (click)=\"onLikeComment(comment)\">\r\n <pw-icon name=\"lucideHeart\" />\r\n @if (comment.likes && comment.likes > 0) {\r\n <span>{{ comment.likes }}</span>\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"startReply(comment)\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Replies -->\r\n @if (comment.replies && comment.replies.length > 0) {\r\n <div class=\"comment-replies\">\r\n @for (reply of comment.replies; track reply.id) {\r\n <div class=\"reply-item\">\r\n <div class=\"comment-avatar small\">\r\n @if (reply.authorAvatar) {\r\n <img\r\n [src]=\"reply.authorAvatar\"\r\n [alt]=\"reply.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(reply.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ reply.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\r\n </div>\r\n <p class=\"comment-text\">{{ reply.content }}</p>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Reply Composer -->\r\n @if (showReplyComposer && replyTarget?.id === comment.id) {\r\n <div class=\"reply-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_reply' | translate\"\r\n rows=\"2\"\r\n [(ngModel)]=\"replyContent\"\r\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"cancel-btn\"\r\n type=\"button\"\r\n (click)=\"cancelReply()\">\r\n {{ 'comments.cancel' | translate }}\r\n </button>\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!replyContent.trim()\"\r\n (click)=\"submitReply()\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Load More -->\r\n @if (hasMore) {\r\n <div class=\"load-more\">\r\n @if (loading) {\r\n <div class=\"loading-spinner small\"></div>\r\n } @else {\r\n <button\r\n class=\"load-more-btn\"\r\n type=\"button\"\r\n (click)=\"onLoadMore()\">\r\n {{ 'comments.load_more' | translate }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Footer Info -->\r\n <div class=\"drawer-footer\">\r\n <p class=\"footer-text\">\r\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\r\n @if (totalPages > 1) {\r\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\r\n }\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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 }); }
16747
+ 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 }); }
16561
16748
  }
16562
16749
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CommentsDrawerComponent, decorators: [{
16563
16750
  type: Component,
16564
- args: [{ selector: 'pw-comments-drawer', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\r\n <!-- Header -->\r\n <div class=\"drawer-header\">\r\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'comments.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Comment Composer (Top) -->\r\n @if (isAuthenticated) {\r\n <div class=\"comment-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_comment' | translate\"\r\n rows=\"3\"\r\n [(ngModel)]=\"newCommentContent\"\r\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!newCommentContent.trim()\"\r\n (click)=\"submitComment()\">\r\n {{ 'comments.post_comment' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"auth-notice\">\r\n <p>{{ 'comments.sign_in' | translate }}</p>\r\n </div>\r\n }\r\n\r\n <!-- Comments List -->\r\n <div class=\"comments-content\">\r\n @if (loading && comments.length === 0) {\r\n <div class=\"loading-state\">\r\n <div class=\"loading-spinner\"></div>\r\n <p>{{ 'comments.loading' | translate }}</p>\r\n </div>\r\n } @else if (comments.length === 0) {\r\n <div class=\"empty-state\">\r\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n <p>{{ 'comments.no_comments' | translate }}</p>\r\n </div>\r\n } @else {\r\n <div class=\"comments-list\">\r\n @for (comment of comments; track comment.id) {\r\n <div class=\"comment-item\">\r\n <!-- Avatar -->\r\n <div class=\"comment-avatar\">\r\n @if (comment.authorAvatar) {\r\n <img\r\n [src]=\"comment.authorAvatar\"\r\n [alt]=\"comment.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(comment.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ comment.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\r\n </div>\r\n\r\n <p class=\"comment-text\">{{ comment.content }}</p>\r\n\r\n <!-- Actions -->\r\n <div class=\"comment-actions\">\r\n @if (isAuthenticated) {\r\n <button\r\n class=\"action-btn\"\r\n [class.active]=\"comment.isLiked\"\r\n type=\"button\"\r\n (click)=\"onLikeComment(comment)\">\r\n <pw-icon name=\"lucideHeart\" />\r\n @if (comment.likes && comment.likes > 0) {\r\n <span>{{ comment.likes }}</span>\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"startReply(comment)\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Replies -->\r\n @if (comment.replies && comment.replies.length > 0) {\r\n <div class=\"comment-replies\">\r\n @for (reply of comment.replies; track reply.id) {\r\n <div class=\"reply-item\">\r\n <div class=\"comment-avatar small\">\r\n @if (reply.authorAvatar) {\r\n <img\r\n [src]=\"reply.authorAvatar\"\r\n [alt]=\"reply.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(reply.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ reply.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\r\n </div>\r\n <p class=\"comment-text\">{{ reply.content }}</p>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Reply Composer -->\r\n @if (showReplyComposer && replyTarget?.id === comment.id) {\r\n <div class=\"reply-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_reply' | translate\"\r\n rows=\"2\"\r\n [(ngModel)]=\"replyContent\"\r\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"cancel-btn\"\r\n type=\"button\"\r\n (click)=\"cancelReply()\">\r\n {{ 'comments.cancel' | translate }}\r\n </button>\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!replyContent.trim()\"\r\n (click)=\"submitReply()\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Load More -->\r\n @if (hasMore) {\r\n <div class=\"load-more\">\r\n @if (loading) {\r\n <div class=\"loading-spinner small\"></div>\r\n } @else {\r\n <button\r\n class=\"load-more-btn\"\r\n type=\"button\"\r\n (click)=\"onLoadMore()\">\r\n {{ 'comments.load_more' | translate }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Footer Info -->\r\n <div class=\"drawer-footer\">\r\n <p class=\"footer-text\">\r\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\r\n @if (totalPages > 1) {\r\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\r\n }\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\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"] }]
16751
+ 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"] }]
16565
16752
  }], propDecorators: { comments: [{
16566
16753
  type: Input
16567
16754
  }], visible: [{
@@ -16607,8 +16794,11 @@ function createTranslateLoader(http) {
16607
16794
  class PlayerShellComponent {
16608
16795
  constructor() {
16609
16796
  /**
16610
- * Emitted when a reader acts on the paywall overlay. The host owns
16611
- * checkout — the player never talks to a payment provider itself.
16797
+ * Emitted when a reader acts on the paywall overlay: `purchase` /
16798
+ * `subscribe` (with the chosen option's `productId` — a product id or a
16799
+ * subscription tier), `login` or `dismiss`. The host owns checkout — the
16800
+ * player never talks to a payment provider itself; call
16801
+ * `refreshEntitlements()` (or pass a new `entitlementSnapshot`) afterwards.
16612
16802
  */
16613
16803
  this.paywallAction = new EventEmitter();
16614
16804
  /**
@@ -16890,6 +17080,10 @@ class PlayerShellComponent {
16890
17080
  this.translationService = inject(TranslationService);
16891
17081
  this.canvasCamera = inject(CanvasCameraService);
16892
17082
  this.paywallService = inject(PaywallService);
17083
+ /** ngOnInit ran: later input changes are live updates, not initial values. */
17084
+ this.initialized = false;
17085
+ /** The state subscriptions are set up once per instance, not per (re)load. */
17086
+ this.stateSubscribed = false;
16893
17087
  }
16894
17088
  /**
16895
17089
  * Effective reduced-motion state for every transition the shell drives
@@ -16903,6 +17097,7 @@ class PlayerShellComponent {
16903
17097
  * Initialize component
16904
17098
  */
16905
17099
  ngOnInit() {
17100
+ this.initialized = true;
16906
17101
  this.toolbarVisible = this.showToolbar;
16907
17102
  this.watchReducedMotion();
16908
17103
  this.subscribeToVideoSignals();
@@ -16913,6 +17108,100 @@ class PlayerShellComponent {
16913
17108
  window.addEventListener('pagehide', this.onPageHide);
16914
17109
  }
16915
17110
  }
17111
+ /**
17112
+ * Inputs changed after init. The first round (before ngOnInit) is skipped:
17113
+ * ngOnInit reads every input itself, so nothing loads twice.
17114
+ */
17115
+ ngOnChanges(changes) {
17116
+ if (!this.initialized) {
17117
+ return;
17118
+ }
17119
+ if (changes['manifest'] || changes['manifestUrl']) {
17120
+ // A new work: reload re-reads every other input as well.
17121
+ void this.reload();
17122
+ return;
17123
+ }
17124
+ if (changes['locale']) {
17125
+ this.applyLocale();
17126
+ }
17127
+ if (changes['entitlementSnapshot']) {
17128
+ if (!this.entitlementSnapshot && !this.httpEntitlement) {
17129
+ this.paywallService.setSnapshot(null);
17130
+ }
17131
+ this.refreshEntitlements(this.entitlementSnapshot).catch(() => undefined);
17132
+ }
17133
+ if (changes['viewModeOverride']) {
17134
+ this.applyViewModeOverride();
17135
+ }
17136
+ if (changes['showToolbar']) {
17137
+ this.toolbarVisible = this.showToolbar;
17138
+ }
17139
+ if (changes['autoplay'] && this.autoplay !== this.autoplayEnabled) {
17140
+ this.onToggleAutoplay();
17141
+ }
17142
+ else if ((changes['secondsPerPanel'] || changes['reducedMotion']) && this.autoplayEnabled) {
17143
+ // Re-arm with the new timing / video start modes.
17144
+ this.startAutoplay();
17145
+ }
17146
+ this.cdr.markForCheck();
17147
+ }
17148
+ /**
17149
+ * Reload the work from the current `manifest` / `manifestUrl` inputs
17150
+ * (also the error screen's Retry). Story state of the previous load —
17151
+ * position, variables except persistent ones, open overlays, autoplay —
17152
+ * is reset; the analytics session of the previous work is closed.
17153
+ */
17154
+ async reload() {
17155
+ this.stopAutoplay();
17156
+ this.autoplayEnabled = false;
17157
+ this.videoSequencer.reset();
17158
+ this.panelAudio.stopAll();
17159
+ this.endAnalyticsSession();
17160
+ this.analyticsSessionStarted = this.analyticsSessionEnded = this.workCompleteTracked = false;
17161
+ this.lastTrackedPanelId = undefined;
17162
+ this.closePaywall();
17163
+ this.closeAgeGate();
17164
+ this.tocVisible = this.settingsVisible = this.languageModalVisible = this.charactersVisible = false;
17165
+ this.extrasVisible = this.shareVisible = this.commentsVisible = this.thumbnailsVisible = false;
17166
+ this.actionModalVisible = this.branchChooserVisible = false;
17167
+ this.variantOverrides.clear();
17168
+ this.variableStore.setDefinitions([]);
17169
+ for (const scope of ['global', 'chapter', 'page', 'session']) {
17170
+ this.variableStore.resetScope(scope);
17171
+ }
17172
+ this.currentChapter = this.currentPanel = this.currentPage = this.effectivePanel = undefined;
17173
+ this.viewMode = 'panel';
17174
+ this.visitedPanelIds = [];
17175
+ this.httpEntitlement = undefined;
17176
+ this.hasError = false;
17177
+ this.errorMessage = '';
17178
+ await this.initializePlayer();
17179
+ }
17180
+ /** Content locale + GUI language follow `locale`. */
17181
+ applyLocale() {
17182
+ this.playerState.setLocale(this.locale);
17183
+ this.translationService.setLanguage(this.locale);
17184
+ }
17185
+ /** Apply a changed `viewModeOverride` to the running view. */
17186
+ applyViewModeOverride() {
17187
+ const before = this.viewMode;
17188
+ if (this.viewModeOverride === 'panel' && this.viewMode === 'page') {
17189
+ this.viewMode = 'panel';
17190
+ this.videoSequencer.reset();
17191
+ }
17192
+ else {
17193
+ this.updateCanvasAvailability();
17194
+ }
17195
+ if (this.viewMode === before) {
17196
+ return;
17197
+ }
17198
+ if (this.viewMode === 'canvas') {
17199
+ this.jumpCameraToCurrentPanel();
17200
+ }
17201
+ if (this.autoplayEnabled) {
17202
+ this.startAutoplay();
17203
+ }
17204
+ }
16916
17205
  /**
16917
17206
  * Cleanup on destroy
16918
17207
  */
@@ -17029,8 +17318,7 @@ class PlayerShellComponent {
17029
17318
  this.variableStore.seed(context);
17030
17319
  }
17031
17320
  // Set initial locale and configure translations
17032
- this.playerState.setLocale(this.locale);
17033
- this.translationService.setLanguage(this.locale);
17321
+ this.applyLocale();
17034
17322
  // Check if page view is available
17035
17323
  const loadedManifest = this.manifestService.getManifest();
17036
17324
  if (loadedManifest?.chapters) {
@@ -17065,12 +17353,19 @@ class PlayerShellComponent {
17065
17353
  }
17066
17354
  // Subscribe to state changes
17067
17355
  this.subscribeToStateChanges();
17356
+ // The host's `autoplay` input decides at start; the reader's toolbar
17357
+ // toggle takes over afterwards.
17358
+ if (this.autoplay && !this.autoplayEnabled) {
17359
+ this.onToggleAutoplay();
17360
+ }
17068
17361
  this.loading = false;
17069
17362
  this.ready.emit();
17070
17363
  }
17071
17364
  catch (err) {
17072
17365
  this.handleError(err);
17073
17366
  }
17367
+ // Async completion (e.g. a reload from ngOnChanges): repaint the OnPush view.
17368
+ this.cdr.markForCheck();
17074
17369
  }
17075
17370
  /**
17076
17371
  * Configure the TrackingService from the manifest's `tracking` section:
@@ -17225,6 +17520,10 @@ class PlayerShellComponent {
17225
17520
  * Subscribe to state changes
17226
17521
  */
17227
17522
  subscribeToStateChanges() {
17523
+ if (this.stateSubscribed) {
17524
+ return;
17525
+ }
17526
+ this.stateSubscribed = true;
17228
17527
  // Listen to panel changes
17229
17528
  this.playerState.currentPanel$
17230
17529
  .pipe(takeUntil(this.destroy$))
@@ -17294,8 +17593,11 @@ class PlayerShellComponent {
17294
17593
  * @param transition - Optional transition to render for this panel change
17295
17594
  * (already resolved against the outputPresets default). Omitted for
17296
17595
  * non-adjacent jumps (TOC, initial load), which swap instantly.
17596
+ * @param mutations - Variable mutations of the traversed edge. Applied only
17597
+ * once the navigation is allowed (after the paywall / age gate), so a
17598
+ * blocked attempt never changes story state.
17297
17599
  */
17298
- async navigateToPanel(chapterId, panelId, transition, cameraMove) {
17600
+ async navigateToPanel(chapterId, panelId, transition, cameraMove, mutations) {
17299
17601
  try {
17300
17602
  this.viewportTransition = transition ?? null;
17301
17603
  // Check entitlement. A host-supplied adapter still wins — it is the
@@ -17304,10 +17606,11 @@ class PlayerShellComponent {
17304
17606
  if (this.entitlementAdapter) {
17305
17607
  const hasAccess = await this.entitlementAdapter.hasAccess(panelId);
17306
17608
  if (!hasAccess) {
17307
- this.openPaywall(this.paywallService.gateFor(panelId) ?? {
17609
+ this.openPaywall(this.paywallService.gateFor(panelId, this.locale) ?? {
17308
17610
  scope: 'panel',
17309
17611
  refId: panelId,
17310
- reason: 'This content requires an entitlement to access.',
17612
+ reason: PaywallService.readerMessage(null),
17613
+ lockReason: 'entitlement_required',
17311
17614
  });
17312
17615
  return;
17313
17616
  }
@@ -17318,10 +17621,10 @@ class PlayerShellComponent {
17318
17621
  // the last panel they were entitled to see.
17319
17622
  if (this.paywallService.evaluate(panelId).reason === 'age_verification_required') {
17320
17623
  // An age gate is answered in-player (birth date), not by checkout.
17321
- this.openAgeGate(chapterId, panelId, transition, cameraMove);
17624
+ this.openAgeGate(chapterId, panelId, transition, cameraMove, mutations);
17322
17625
  return;
17323
17626
  }
17324
- const gate = this.paywallService.gateFor(panelId);
17627
+ const gate = this.paywallService.gateFor(panelId, this.locale);
17325
17628
  if (gate) {
17326
17629
  this.openPaywall(gate);
17327
17630
  return;
@@ -17335,6 +17638,10 @@ class PlayerShellComponent {
17335
17638
  if (!panelData) {
17336
17639
  throw new Error(`Panel not found: ${panelId}`);
17337
17640
  }
17641
+ // Edge `action` mutations (schema: "applied when traversing this edge").
17642
+ if (mutations?.length) {
17643
+ this.variableStore.applyMutations(mutations, { chapterId });
17644
+ }
17338
17645
  const chapterChanged = this.currentChapter !== chapter;
17339
17646
  const leavingPanelId = this.getCurrentPanelId();
17340
17647
  this.currentChapter = chapter;
@@ -17469,7 +17776,8 @@ class PlayerShellComponent {
17469
17776
  buildExtrasList(manifest) {
17470
17777
  // Media (images / video / audio / thumbnails) resolve through the asset
17471
17778
  // catalog; see utils/extras-utils.ts for the ExtraBlock mapping.
17472
- this.extrasList = extrasFromManifest(manifest?.extras, (assetId) => this.manifestService.getAsset(assetId));
17779
+ // An extras-scoped paywall rule the reader does not satisfy locks its block.
17780
+ this.extrasList = extrasFromManifest(manifest?.extras, (assetId) => this.manifestService.getAsset(assetId)).map((extra) => (this.paywallService.isExtraLocked(extra.id) ? { ...extra, gated: true } : extra));
17473
17781
  }
17474
17782
  /**
17475
17783
  * Canvas view is available when the current chapter has a canvas layout
@@ -17529,12 +17837,7 @@ class PlayerShellComponent {
17529
17837
  return;
17530
17838
  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
17839
  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);
17840
+ await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove, result.action);
17538
17841
  }
17539
17842
  }
17540
17843
  catch (err) {
@@ -17663,16 +17966,7 @@ class PlayerShellComponent {
17663
17966
  else if (newMode === 'canvas') {
17664
17967
  this.viewMode = 'canvas';
17665
17968
  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
- }
17969
+ this.jumpCameraToCurrentPanel();
17676
17970
  }
17677
17971
  else {
17678
17972
  // Switch to panel view
@@ -17685,6 +17979,14 @@ class PlayerShellComponent {
17685
17979
  }
17686
17980
  console.log('View mode:', this.viewMode);
17687
17981
  }
17982
+ /** Land the canvas camera on the current panel (entering canvas view). */
17983
+ jumpCameraToCurrentPanel() {
17984
+ const panelId = this.getCurrentPanelId();
17985
+ const placement = panelId ? this.currentChapter?.canvas?.placements?.[panelId] : undefined;
17986
+ if (placement) {
17987
+ this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
17988
+ }
17989
+ }
17688
17990
  /**
17689
17991
  * Tap on a placed panel in canvas view. Traverses the matching out-edge of
17690
17992
  * the current panel when its condition passes (the graph still gates
@@ -17707,7 +18009,7 @@ class PlayerShellComponent {
17707
18009
  if (edge) {
17708
18010
  this.navigationAttempt.emit({ direction: 'panel', target: panelId });
17709
18011
  void this.navigateToPanel(this.currentChapter.id, panelId, edge.transition, edge.cameraMove ??
17710
- this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
18012
+ this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings), edge.action);
17711
18013
  return;
17712
18014
  }
17713
18015
  if (this.visitedPanelIds.includes(panelId)) {
@@ -17715,6 +18017,14 @@ class PlayerShellComponent {
17715
18017
  void this.navigateToPanel(this.currentChapter.id, panelId);
17716
18018
  }
17717
18019
  }
18020
+ /**
18021
+ * The manifest actually loaded — from `manifest` or fetched via
18022
+ * `manifestUrl`. Everything that lists the work (ToC, thumbnails) binds to
18023
+ * this, never to the `manifest` input, which is unset for URL loading.
18024
+ */
18025
+ get loadedManifest() {
18026
+ return this.manifestService.getManifest() ?? undefined;
18027
+ }
17718
18028
  /** Balloon defaults for the canvas stage (same source as the viewport). */
17719
18029
  get manifestBalloonConfig() {
17720
18030
  return this.manifestService.getManifest()?.settings?.typography?.balloon_config ?? null;
@@ -17728,12 +18038,9 @@ class PlayerShellComponent {
17728
18038
  return this.manifestService.getManifest()?.settings?.preload ?? null;
17729
18039
  }
17730
18040
  onLocaleChange(locale) {
17731
- // Update content locale
18041
+ // Content locale and GUI language stay in sync.
17732
18042
  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);
18043
+ this.applyLocale();
17737
18044
  }
17738
18045
  onToggleSpeech() {
17739
18046
  this.setSpeechEnabled(!this.speechEnabled);
@@ -18055,7 +18362,15 @@ class PlayerShellComponent {
18055
18362
  */
18056
18363
  handleKeyboard(event) {
18057
18364
  // Don't handle if user is typing in an input
18058
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
18365
+ if (event.target instanceof HTMLInputElement ||
18366
+ event.target instanceof HTMLTextAreaElement ||
18367
+ event.target instanceof HTMLSelectElement) {
18368
+ return;
18369
+ }
18370
+ // The paywall and the age gate block the story: no shortcut may move
18371
+ // past them. Their own Escape handling applies (paywall: "Maybe later"
18372
+ // dismisses; age gate: dismiss keeps the reader where they were).
18373
+ if (this.paywallVisible || this.ageGateVisible) {
18059
18374
  return;
18060
18375
  }
18061
18376
  // While a dialog is open the story behind it must not react: Escape
@@ -18192,6 +18507,9 @@ class PlayerShellComponent {
18192
18507
  * Handle swipe gesture
18193
18508
  */
18194
18509
  onSwipe(direction) {
18510
+ if (this.paywallVisible || this.ageGateVisible) {
18511
+ return;
18512
+ }
18195
18513
  if (direction === 'left') {
18196
18514
  // Swipe left = navigate forward
18197
18515
  if (this.viewMode === 'page') {
@@ -18589,9 +18907,11 @@ class PlayerShellComponent {
18589
18907
  */
18590
18908
  onPaywallAction(action) {
18591
18909
  const gate = this.paywallGate;
18910
+ const productId = action === 'purchase' || action === 'subscribe' ? this.paywallProductId : undefined;
18592
18911
  if (gate) {
18593
- this.paywallAction.emit({ action, gate });
18912
+ this.paywallAction.emit({ action, gate, ...(productId ? { productId } : {}) });
18594
18913
  }
18914
+ this.paywallProductId = undefined;
18595
18915
  this.closePaywall();
18596
18916
  }
18597
18917
  /**
@@ -18608,6 +18928,10 @@ class PlayerShellComponent {
18608
18928
  await this.httpEntitlement.resolveEntitlement({ workId: manifest?.meta?.id ?? '' });
18609
18929
  this.paywallService.setSnapshot(this.httpEntitlement.getSnapshot());
18610
18930
  }
18931
+ // What the host passed replaces the snapshot wholesale; an age check the
18932
+ // reader already passed on this device still counts.
18933
+ this.restoreAgeVerification();
18934
+ this.buildExtrasList(this.manifestService.getManifest() ?? null);
18611
18935
  const gatedPanelId = this.paywallGate?.refId ?? this.getCurrentPanelId();
18612
18936
  if (gatedPanelId && this.paywallService.canAccess(gatedPanelId)) {
18613
18937
  this.closePaywall();
@@ -18616,9 +18940,9 @@ class PlayerShellComponent {
18616
18940
  // ── Age gate ──────────────────────────────────────────────────────────────
18617
18941
  static { this.AGE_VERIFIED_STORAGE_KEY = 'pw-age-verified'; }
18618
18942
  /** Raise the age gate for a panel and remember where the reader was going. */
18619
- openAgeGate(chapterId, panelId, transition, cameraMove) {
18943
+ openAgeGate(chapterId, panelId, transition, cameraMove, mutations) {
18620
18944
  this.ageGateMinimumAge = this.paywallService.ruleFor(panelId)?.minimumAge ?? 18;
18621
- this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove };
18945
+ this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove, mutations };
18622
18946
  this.ageGateVisible = true;
18623
18947
  this.cdr.markForCheck();
18624
18948
  }
@@ -18648,8 +18972,11 @@ class PlayerShellComponent {
18648
18972
  this.ageGateVisible = false;
18649
18973
  this.pendingAgeGatedNavigation = null;
18650
18974
  this.cdr.markForCheck();
18651
- if (pending && this.paywallService.canAccess(pending.panelId)) {
18652
- await this.navigateToPanel(pending.chapterId, pending.panelId, pending.transition, pending.cameraMove);
18975
+ // Retry the move: navigateToPanel re-checks the gate, so a rule that
18976
+ // also needs a purchase / subscription now raises the paywall (with its
18977
+ // Buy / Subscribe options) instead of letting the reader through.
18978
+ if (pending) {
18979
+ await this.navigateToPanel(pending.chapterId, pending.panelId, pending.transition, pending.cameraMove, pending.mutations);
18653
18980
  }
18654
18981
  }
18655
18982
  persistAgeVerification(age) {
@@ -18725,41 +19052,43 @@ class PlayerShellComponent {
18725
19052
  bookmark.panelId === this.getCurrentPanelId();
18726
19053
  }
18727
19054
  // ── Branch choices ────────────────────────────────────────────────────────
18728
- /** More than one edge leaves the current panel — the toolbar shows "Choices". */
19055
+ /**
19056
+ * At least two paths out of the current panel are open right now (edge
19057
+ * conditions evaluated exactly like the chooser does) — the toolbar shows
19058
+ * "Choices". A closed conditional edge does not count.
19059
+ */
18729
19060
  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;
19061
+ return this.openEdges().length > 1;
18742
19062
  }
18743
- /** Outgoing edges whose conditions pass right now, with reader-facing labels. */
18744
- computeBranchChoices() {
19063
+ /** Outgoing edges of the current panel whose conditions pass right now. */
19064
+ openEdges() {
18745
19065
  const chapter = this.currentChapter;
18746
19066
  const panelId = this.getCurrentPanelId();
18747
- if (!chapter?.graph || !panelId) {
19067
+ if (!chapter || !panelId) {
18748
19068
  return [];
18749
19069
  }
19070
+ const outgoing = chapter.graph?.edges?.filter((edge) => edge.from === panelId) ?? [];
19071
+ if (!outgoing.some((edge) => edge.condition)) {
19072
+ // Nothing to evaluate: skip building a variable context on every check.
19073
+ return outgoing;
19074
+ }
18750
19075
  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) }));
19076
+ return outgoing.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context));
18755
19077
  }
18756
- branchLabel(edge, chapter, index) {
19078
+ /** Open outgoing edges with reader-facing labels. */
19079
+ computeBranchChoices() {
19080
+ const chapter = this.currentChapter;
19081
+ return chapter
19082
+ ? this.openEdges().map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter) }))
19083
+ : [];
19084
+ }
19085
+ branchLabel(edge, chapter) {
18757
19086
  const fromEdge = this.localizedText(edge.label);
18758
19087
  if (fromEdge) {
18759
19088
  return fromEdge;
18760
19089
  }
18761
- const fromTarget = this.localizedText(chapter.panels?.[edge.to]?.title);
18762
- return fromTarget || `Option ${index + 1}`;
19090
+ // Neither: the chooser renders a translated "Option N".
19091
+ return this.localizedText(chapter.panels?.[edge.to]?.title);
18763
19092
  }
18764
19093
  localizedText(value) {
18765
19094
  if (!value) {
@@ -18784,13 +19113,10 @@ class PlayerShellComponent {
18784
19113
  to: choice.edge.to,
18785
19114
  index: choice.index,
18786
19115
  });
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));
19116
+ 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
19117
  }
18792
19118
  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 }); }
19119
+ 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: "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 }); }
18794
19120
  }
18795
19121
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, decorators: [{
18796
19122
  type: Component,
@@ -18812,7 +19138,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
18812
19138
  ShareModalComponent,
18813
19139
  CommentsDrawerComponent,
18814
19140
  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"] }]
19141
+ ], 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
19142
  }], propDecorators: { manifest: [{
18817
19143
  type: Input
18818
19144
  }], manifestUrl: [{
@@ -18954,18 +19280,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
18954
19280
 
18955
19281
  class PlayerComponent {
18956
19282
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
18957
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PlayerComponent, isStandalone: true, selector: "pw-player", ngImport: i0, template: `
18958
- <p>
18959
- player works!
18960
- </p>
19283
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PlayerComponent, isStandalone: true, selector: "pw-player", ngImport: i0, template: `
19284
+ <p>
19285
+ player works!
19286
+ </p>
18961
19287
  `, isInline: true, styles: [""] }); }
18962
19288
  }
18963
19289
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerComponent, decorators: [{
18964
19290
  type: Component,
18965
- args: [{ selector: 'pw-player', imports: [], template: `
18966
- <p>
18967
- player works!
18968
- </p>
19291
+ args: [{ selector: 'pw-player', imports: [], template: `
19292
+ <p>
19293
+ player works!
19294
+ </p>
18969
19295
  ` }]
18970
19296
  }] });
18971
19297
 
@@ -18978,5 +19304,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
18978
19304
  * Generated bundle index. Do not edit.
18979
19305
  */
18980
19306
 
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 };
19307
+ 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
19308
  //# sourceMappingURL=panelwave-player.mjs.map