@panelwave/player 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -20
- package/fesm2022/panelwave-player.mjs +511 -167
- package/fesm2022/panelwave-player.mjs.map +1 -1
- package/index.d.ts +212 -27
- package/package.json +4 -4
- package/src/assets/i18n/de.json +26 -1
- package/src/assets/i18n/en.json +26 -1
|
@@ -4,12 +4,11 @@ import { BehaviorSubject, throwError, of, Subject, takeUntil } from 'rxjs';
|
|
|
4
4
|
import { HttpClient } from '@angular/common/http';
|
|
5
5
|
import { map, catchError, distinctUntilChanged } from 'rxjs/operators';
|
|
6
6
|
import jsonLogic from 'json-logic-js';
|
|
7
|
-
import
|
|
8
|
-
import { TranslateService, TranslateModule } from '@ngx-translate/core';
|
|
7
|
+
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
|
9
8
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
10
9
|
import * as i1 from '@angular/common';
|
|
11
10
|
import { CommonModule } from '@angular/common';
|
|
12
|
-
import * as i1$
|
|
11
|
+
import * as i1$1 from '@angular/forms';
|
|
13
12
|
import { FormsModule } from '@angular/forms';
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -7053,12 +7052,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
7053
7052
|
* free preview.
|
|
7054
7053
|
* - Panel-scoped rules gate exactly their `targetPanelIds` — preview does NOT
|
|
7055
7054
|
* override an explicit panel gate.
|
|
7055
|
+
* - Chapter-scoped rules gate only the panels of their chapter(s) (`refId`);
|
|
7056
|
+
* their free preview counts panels WITHIN that chapter. They take
|
|
7057
|
+
* precedence over a work gate for those panels.
|
|
7058
|
+
* - Extras-scoped rules never gate panels — they gate the extras block named
|
|
7059
|
+
* by `refId` (see `isExtraLocked`).
|
|
7056
7060
|
* - A gated panel unlocks when the gating rule's requirement is satisfied by
|
|
7057
7061
|
* the reader's entitlement snapshot.
|
|
7058
7062
|
*/
|
|
7059
7063
|
const DEFAULT_MINIMUM_AGE = 18;
|
|
7060
|
-
/** Does the
|
|
7064
|
+
/** Does the rule carry an age requirement the reader has not met? */
|
|
7065
|
+
function ageMissing(rule, entitlement) {
|
|
7066
|
+
if (rule.entitlementType !== 'age_gate' && rule.minimumAge === undefined)
|
|
7067
|
+
return false;
|
|
7068
|
+
const min = rule.minimumAge ?? DEFAULT_MINIMUM_AGE;
|
|
7069
|
+
return !(entitlement.ageVerified && (entitlement.age ?? 0) >= min);
|
|
7070
|
+
}
|
|
7071
|
+
/**
|
|
7072
|
+
* Does the reader's snapshot satisfy this rule's requirement? A rule with a
|
|
7073
|
+
* `minimumAge` needs the age AND its entitlement.
|
|
7074
|
+
*/
|
|
7061
7075
|
function satisfiesRule(rule, entitlement) {
|
|
7076
|
+
return !ageMissing(rule, entitlement) && hasEntitlement(rule, entitlement);
|
|
7077
|
+
}
|
|
7078
|
+
/** The entitlement part of a rule (the age part is `ageMissing`). */
|
|
7079
|
+
function hasEntitlement(rule, entitlement) {
|
|
7062
7080
|
switch (rule.entitlementType) {
|
|
7063
7081
|
case 'free':
|
|
7064
7082
|
return true;
|
|
@@ -7074,13 +7092,17 @@ function satisfiesRule(rule, entitlement) {
|
|
|
7074
7092
|
return entitlement.purchasedProductIds.length > 0;
|
|
7075
7093
|
return required.some((id) => entitlement.purchasedProductIds.includes(id));
|
|
7076
7094
|
}
|
|
7077
|
-
case 'age_gate':
|
|
7078
|
-
|
|
7079
|
-
return entitlement.ageVerified && (entitlement.age ?? 0) >= min;
|
|
7080
|
-
}
|
|
7095
|
+
case 'age_gate':
|
|
7096
|
+
return true; // the age is the whole requirement (checked by ageMissing)
|
|
7081
7097
|
}
|
|
7082
7098
|
}
|
|
7083
|
-
|
|
7099
|
+
/**
|
|
7100
|
+
* Why a rule locks the reader out. The age comes first: a reader who fails
|
|
7101
|
+
* the age check is asked for it before being offered anything to buy.
|
|
7102
|
+
*/
|
|
7103
|
+
function lockReasonFor(rule, entitlement) {
|
|
7104
|
+
if (ageMissing(rule, entitlement))
|
|
7105
|
+
return 'age_verification_required';
|
|
7084
7106
|
switch (rule.entitlementType) {
|
|
7085
7107
|
case 'subscription':
|
|
7086
7108
|
return 'subscription_required';
|
|
@@ -7108,36 +7130,48 @@ function evaluatePanelAccess(rules, entitlement, panel) {
|
|
|
7108
7130
|
return {
|
|
7109
7131
|
panelId: panel.id,
|
|
7110
7132
|
locked: !ok,
|
|
7111
|
-
reason: ok ? null : lockReasonFor(panelRule),
|
|
7133
|
+
reason: ok ? null : lockReasonFor(panelRule, entitlement),
|
|
7112
7134
|
appliedRuleId: panelRule.id,
|
|
7113
7135
|
preview: false,
|
|
7114
7136
|
};
|
|
7115
7137
|
}
|
|
7116
|
-
// 2.
|
|
7138
|
+
// 2. Chapter gates: only the panels of the named chapter, with the free
|
|
7139
|
+
// preview counted within that chapter.
|
|
7140
|
+
const chapterRule = panel.chapterId === undefined
|
|
7141
|
+
? undefined
|
|
7142
|
+
: active.find((r) => r.scope === 'chapter' && (r.targetIds ?? []).includes(panel.chapterId));
|
|
7143
|
+
if (chapterRule) {
|
|
7144
|
+
return gateWithPreview(chapterRule, entitlement, panel.id, panel.chapterIndex ?? Number.MAX_SAFE_INTEGER);
|
|
7145
|
+
}
|
|
7146
|
+
// 3. Work/global gate with free preview.
|
|
7117
7147
|
const workRule = findWorkGateRule(active);
|
|
7118
7148
|
if (workRule) {
|
|
7119
|
-
|
|
7120
|
-
if (panel.index < previewCount) {
|
|
7121
|
-
return {
|
|
7122
|
-
panelId: panel.id,
|
|
7123
|
-
locked: false,
|
|
7124
|
-
reason: null,
|
|
7125
|
-
appliedRuleId: workRule.id,
|
|
7126
|
-
preview: true,
|
|
7127
|
-
};
|
|
7128
|
-
}
|
|
7129
|
-
const ok = satisfiesRule(workRule, entitlement);
|
|
7130
|
-
return {
|
|
7131
|
-
panelId: panel.id,
|
|
7132
|
-
locked: !ok,
|
|
7133
|
-
reason: ok ? null : lockReasonFor(workRule),
|
|
7134
|
-
appliedRuleId: workRule.id,
|
|
7135
|
-
preview: false,
|
|
7136
|
-
};
|
|
7149
|
+
return gateWithPreview(workRule, entitlement, panel.id, panel.index);
|
|
7137
7150
|
}
|
|
7138
|
-
//
|
|
7151
|
+
// 4. No gate at all — free content.
|
|
7139
7152
|
return { panelId: panel.id, locked: false, reason: null, appliedRuleId: null, preview: false };
|
|
7140
7153
|
}
|
|
7154
|
+
/** A work/chapter gate: panels before `previewPanelCount` (by `index`) are free. */
|
|
7155
|
+
function gateWithPreview(rule, entitlement, panelId, index) {
|
|
7156
|
+
if (index < (rule.previewPanelCount ?? 0)) {
|
|
7157
|
+
return { panelId, locked: false, reason: null, appliedRuleId: rule.id, preview: true };
|
|
7158
|
+
}
|
|
7159
|
+
const ok = satisfiesRule(rule, entitlement);
|
|
7160
|
+
return {
|
|
7161
|
+
panelId,
|
|
7162
|
+
locked: !ok,
|
|
7163
|
+
reason: ok ? null : lockReasonFor(rule, entitlement),
|
|
7164
|
+
appliedRuleId: rule.id,
|
|
7165
|
+
preview: false,
|
|
7166
|
+
};
|
|
7167
|
+
}
|
|
7168
|
+
/** Is the extras block `extraId` locked by an extras-scoped rule? */
|
|
7169
|
+
function isExtraLocked(rules, entitlement, extraId) {
|
|
7170
|
+
return rules.some((r) => r.isActive &&
|
|
7171
|
+
r.scope === 'extras' &&
|
|
7172
|
+
(r.targetIds ?? []).includes(extraId) &&
|
|
7173
|
+
!satisfiesRule(r, entitlement));
|
|
7174
|
+
}
|
|
7141
7175
|
/** Evaluate a whole work (panels in reading order). */
|
|
7142
7176
|
function evaluateWorkAccess(rules, entitlement, panels) {
|
|
7143
7177
|
return panels.map((panel) => evaluatePanelAccess(rules, entitlement, panel));
|
|
@@ -7164,8 +7198,10 @@ const KNOWN_ENTITLEMENT_TYPES = [
|
|
|
7164
7198
|
* `minimumAge` written by the CMS exporter. Prefer the CMS names and fall
|
|
7165
7199
|
* back to the originals, so a manifest from either era gates identically.
|
|
7166
7200
|
*
|
|
7167
|
-
*
|
|
7168
|
-
*
|
|
7201
|
+
* `ageGate` / `minimumAge` is an age check ON TOP OF the entitlement (schema
|
|
7202
|
+
* semantics). Only a rule that declares no entitlement at all but names an
|
|
7203
|
+
* age becomes a pure age gate; a custom marker plus an age stays a purchase
|
|
7204
|
+
* (or subscription) gate that also requires the age.
|
|
7169
7205
|
*/
|
|
7170
7206
|
function fromManifestRule(rule, index) {
|
|
7171
7207
|
const raw = rule;
|
|
@@ -7175,9 +7211,6 @@ function fromManifestRule(rule, index) {
|
|
|
7175
7211
|
if (declared && KNOWN_ENTITLEMENT_TYPES.includes(declared)) {
|
|
7176
7212
|
entitlementType = declared;
|
|
7177
7213
|
}
|
|
7178
|
-
else if (minimumAge !== undefined) {
|
|
7179
|
-
entitlementType = 'age_gate';
|
|
7180
|
-
}
|
|
7181
7214
|
else if (declared === 'premium') {
|
|
7182
7215
|
// Original format's subscription marker.
|
|
7183
7216
|
entitlementType = 'subscription';
|
|
@@ -7186,10 +7219,17 @@ function fromManifestRule(rule, index) {
|
|
|
7186
7219
|
// 'token', 'purchaseId', or any custom marker: treat as a purchase gate.
|
|
7187
7220
|
entitlementType = 'purchase';
|
|
7188
7221
|
}
|
|
7222
|
+
else if (minimumAge !== undefined) {
|
|
7223
|
+
// No entitlement declared, only an age: a pure age gate.
|
|
7224
|
+
entitlementType = 'age_gate';
|
|
7225
|
+
}
|
|
7189
7226
|
else {
|
|
7190
7227
|
entitlementType = 'free';
|
|
7191
7228
|
}
|
|
7192
|
-
|
|
7229
|
+
// Extras-scoped rules keep their scope: they gate an extras block, never
|
|
7230
|
+
// panels (mapping them onto 'work' used to lock the whole work).
|
|
7231
|
+
const scope = raw['scope'];
|
|
7232
|
+
const refId = raw['refId'];
|
|
7193
7233
|
return {
|
|
7194
7234
|
id: raw['id'] ?? `rule-${index}`,
|
|
7195
7235
|
scope: scope ?? 'work',
|
|
@@ -7203,9 +7243,12 @@ function fromManifestRule(rule, index) {
|
|
|
7203
7243
|
targetPanelIds: raw['targetPanelIds'] ??
|
|
7204
7244
|
// A panel-scoped rule from the original format names its single target
|
|
7205
7245
|
// in refId rather than a list.
|
|
7206
|
-
(
|
|
7207
|
-
|
|
7208
|
-
|
|
7246
|
+
(scope === 'panel' && refId ? [refId] : undefined),
|
|
7247
|
+
targetIds: (scope === 'chapter' || scope === 'extras') && refId ? [refId] : undefined,
|
|
7248
|
+
name: raw['name'],
|
|
7249
|
+
description: raw['description'],
|
|
7250
|
+
price: raw['price'],
|
|
7251
|
+
entitlementKey: raw['requireEntitlement'],
|
|
7209
7252
|
};
|
|
7210
7253
|
}
|
|
7211
7254
|
/** Normalise every rule in a manifest. Returns [] when the work has no paywall. */
|
|
@@ -7215,6 +7258,14 @@ function rulesFromManifest(manifest) {
|
|
|
7215
7258
|
return [];
|
|
7216
7259
|
return rules.map(fromManifestRule);
|
|
7217
7260
|
}
|
|
7261
|
+
/** Chapter id and within-chapter index per panel id (chapter-rule previews). */
|
|
7262
|
+
function chapterOrderFromManifest(manifest) {
|
|
7263
|
+
const out = new Map();
|
|
7264
|
+
for (const chapter of manifest?.chapters ?? []) {
|
|
7265
|
+
Object.keys(chapter.panels ?? {}).forEach((panelId, chapterIndex) => out.set(panelId, { chapterId: chapter.id, chapterIndex }));
|
|
7266
|
+
}
|
|
7267
|
+
return out;
|
|
7268
|
+
}
|
|
7218
7269
|
/**
|
|
7219
7270
|
* Global reading order for preview counting: chapters in array order, and
|
|
7220
7271
|
* within a chapter the panel declaration order.
|
|
@@ -7267,7 +7318,9 @@ class PaywallService {
|
|
|
7267
7318
|
constructor() {
|
|
7268
7319
|
this.rules = [];
|
|
7269
7320
|
this.readingOrder = new Map();
|
|
7321
|
+
this.chapterOrder = new Map();
|
|
7270
7322
|
this.manifest = null;
|
|
7323
|
+
this.products = new Map();
|
|
7271
7324
|
this.snapshotSubject = new BehaviorSubject(ANONYMOUS_READER);
|
|
7272
7325
|
this.snapshot$ = this.snapshotSubject.asObservable();
|
|
7273
7326
|
}
|
|
@@ -7276,6 +7329,17 @@ class PaywallService {
|
|
|
7276
7329
|
this.manifest = manifest;
|
|
7277
7330
|
this.rules = rulesFromManifest(manifest);
|
|
7278
7331
|
this.readingOrder = new Map(readingOrderFromManifest(manifest).map((panelId, index) => [panelId, index]));
|
|
7332
|
+
this.chapterOrder = chapterOrderFromManifest(manifest);
|
|
7333
|
+
this.products = PaywallService.productsFromManifest(manifest);
|
|
7334
|
+
}
|
|
7335
|
+
/** `paywall.products` by id (format 1.6); the first entry wins on duplicate ids. */
|
|
7336
|
+
static productsFromManifest(manifest) {
|
|
7337
|
+
const out = new Map();
|
|
7338
|
+
for (const product of manifest?.paywall?.products ?? []) {
|
|
7339
|
+
if (product?.id && !out.has(product.id))
|
|
7340
|
+
out.set(product.id, product);
|
|
7341
|
+
}
|
|
7342
|
+
return out;
|
|
7279
7343
|
}
|
|
7280
7344
|
/** Replace what the reader owns (after sign-in, or a completed purchase). */
|
|
7281
7345
|
setSnapshot(snapshot) {
|
|
@@ -7304,7 +7368,15 @@ class PaywallService {
|
|
|
7304
7368
|
return { panelId, locked: false, reason: null, appliedRuleId: null, preview: false };
|
|
7305
7369
|
}
|
|
7306
7370
|
const index = this.readingOrder.get(panelId) ?? Number.MAX_SAFE_INTEGER;
|
|
7307
|
-
return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
|
|
7371
|
+
return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
|
|
7372
|
+
id: panelId,
|
|
7373
|
+
index,
|
|
7374
|
+
...this.chapterOrder.get(panelId),
|
|
7375
|
+
});
|
|
7376
|
+
}
|
|
7377
|
+
/** Is this extras block locked by an extras-scoped rule the reader does not satisfy? */
|
|
7378
|
+
isExtraLocked(extraId) {
|
|
7379
|
+
return isExtraLocked(this.rules, this.snapshotSubject.value, extraId);
|
|
7308
7380
|
}
|
|
7309
7381
|
/** Convenience: may the reader open this panel? */
|
|
7310
7382
|
canAccess(panelId) {
|
|
@@ -7313,9 +7385,11 @@ class PaywallService {
|
|
|
7313
7385
|
/**
|
|
7314
7386
|
* Describe the gate blocking a panel, or null when it is not gated.
|
|
7315
7387
|
* The shape is the player's existing `PaywallGate`, so the overlay needs
|
|
7316
|
-
* no changes to render it.
|
|
7388
|
+
* no changes to render it. `locale` picks the language of product names
|
|
7389
|
+
* and descriptions from `paywall.products` (fallback: the work's default
|
|
7390
|
+
* locale).
|
|
7317
7391
|
*/
|
|
7318
|
-
gateFor(panelId) {
|
|
7392
|
+
gateFor(panelId, locale) {
|
|
7319
7393
|
const decision = this.evaluate(panelId);
|
|
7320
7394
|
if (!decision.locked)
|
|
7321
7395
|
return null;
|
|
@@ -7323,14 +7397,52 @@ class PaywallService {
|
|
|
7323
7397
|
const scope = rule?.scope === 'panel' ? 'panel' : rule?.scope === 'chapter' ? 'chapter' : 'work';
|
|
7324
7398
|
return {
|
|
7325
7399
|
scope,
|
|
7326
|
-
refId: rule?.scope === 'panel' ? panelId : undefined,
|
|
7400
|
+
refId: rule?.scope === 'panel' ? panelId : scope === 'chapter' ? this.chapterOrder.get(panelId)?.chapterId : undefined,
|
|
7327
7401
|
requireEntitlement: rule?.entitlementType,
|
|
7328
7402
|
reason: PaywallService.readerMessage(decision.reason),
|
|
7329
7403
|
preview: rule?.previewPanelCount
|
|
7330
7404
|
? { previewPanels: rule.previewPanelCount }
|
|
7331
7405
|
: undefined,
|
|
7406
|
+
ruleId: rule?.id,
|
|
7407
|
+
lockReason: decision.reason ?? undefined,
|
|
7408
|
+
options: rule
|
|
7409
|
+
? PaywallService.purchaseOptions(rule, this.products, locale ?? this.defaultLocale, this.defaultLocale)
|
|
7410
|
+
: [],
|
|
7332
7411
|
};
|
|
7333
7412
|
}
|
|
7413
|
+
/**
|
|
7414
|
+
* What the reader can buy to pass a rule: one option per required product
|
|
7415
|
+
* (purchase) or per subscription tier (subscription). Without explicit
|
|
7416
|
+
* ids, the rule's `requireEntitlement` key names the product, else the
|
|
7417
|
+
* rule id. Age gates and free rules offer nothing to buy.
|
|
7418
|
+
*
|
|
7419
|
+
* Labels (format 1.6): an option whose id has an entry in
|
|
7420
|
+
* `paywall.products` takes that entry's localized name, description and
|
|
7421
|
+
* price. Otherwise it falls back to the rule's name (a single option) or
|
|
7422
|
+
* the bare id (several options), and to the rule's description and price.
|
|
7423
|
+
*/
|
|
7424
|
+
static purchaseOptions(rule, products = new Map(), locale = 'en-US', fallbackLocale = 'en-US') {
|
|
7425
|
+
const type = rule.entitlementType === 'purchase' ? 'one-time' : rule.entitlementType;
|
|
7426
|
+
if (type !== 'one-time' && type !== 'subscription')
|
|
7427
|
+
return [];
|
|
7428
|
+
const listed = type === 'one-time' ? rule.requiredProductIds : rule.subscriptionTiers;
|
|
7429
|
+
const key = rule.entitlementKey !== rule.entitlementType ? rule.entitlementKey : undefined;
|
|
7430
|
+
const ids = listed?.length ? listed : [key ?? rule.id];
|
|
7431
|
+
return ids.map((productId) => {
|
|
7432
|
+
const product = products.get(productId);
|
|
7433
|
+
const name = product?.name ? resolveLocalizedString(product.name, locale, fallbackLocale) : '';
|
|
7434
|
+
const description = product?.description
|
|
7435
|
+
? resolveLocalizedString(product.description, locale, fallbackLocale)
|
|
7436
|
+
: '';
|
|
7437
|
+
return {
|
|
7438
|
+
productId,
|
|
7439
|
+
name: name || (ids.length > 1 ? productId : rule.name ?? ''),
|
|
7440
|
+
description: description || rule.description,
|
|
7441
|
+
price: product?.price ?? rule.price,
|
|
7442
|
+
type,
|
|
7443
|
+
};
|
|
7444
|
+
});
|
|
7445
|
+
}
|
|
7334
7446
|
/** The rule that gates a panel, for callers that need its price or name. */
|
|
7335
7447
|
ruleFor(panelId) {
|
|
7336
7448
|
const decision = this.evaluate(panelId);
|
|
@@ -7338,11 +7450,17 @@ class PaywallService {
|
|
|
7338
7450
|
return null;
|
|
7339
7451
|
return this.rules.find((r) => r.id === decision.appliedRuleId) ?? null;
|
|
7340
7452
|
}
|
|
7453
|
+
/** The work's default locale (product-label fallback). */
|
|
7454
|
+
get defaultLocale() {
|
|
7455
|
+
return this.manifest?.meta?.default_locale ?? 'en-US';
|
|
7456
|
+
}
|
|
7341
7457
|
/** Reset to a manifest-less, entitlement-less state. */
|
|
7342
7458
|
clear() {
|
|
7343
7459
|
this.manifest = null;
|
|
7344
7460
|
this.rules = [];
|
|
7461
|
+
this.products.clear();
|
|
7345
7462
|
this.readingOrder.clear();
|
|
7463
|
+
this.chapterOrder.clear();
|
|
7346
7464
|
this.snapshotSubject.next(ANONYMOUS_READER);
|
|
7347
7465
|
}
|
|
7348
7466
|
/**
|
|
@@ -8648,8 +8766,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
8648
8766
|
class TranslationService {
|
|
8649
8767
|
constructor() {
|
|
8650
8768
|
this.translate = inject(TranslateService);
|
|
8651
|
-
//
|
|
8652
|
-
|
|
8769
|
+
// Fallback language - translations will be loaded via HTTP.
|
|
8770
|
+
// setFallbackLang / getCurrentLang / getFallbackLang exist in
|
|
8771
|
+
// ngx-translate 17 and 18 (the default* aliases are gone in 18).
|
|
8772
|
+
this.translate.setFallbackLang('en');
|
|
8653
8773
|
this.translate.use('en');
|
|
8654
8774
|
}
|
|
8655
8775
|
/**
|
|
@@ -8670,7 +8790,7 @@ class TranslationService {
|
|
|
8670
8790
|
* Get current GUI language
|
|
8671
8791
|
*/
|
|
8672
8792
|
getCurrentLanguage() {
|
|
8673
|
-
return this.translate.
|
|
8793
|
+
return this.translate.getCurrentLang() || this.translate.getFallbackLang() || 'en';
|
|
8674
8794
|
}
|
|
8675
8795
|
/**
|
|
8676
8796
|
* Extract language code from locale code
|
|
@@ -13735,11 +13855,11 @@ class ToolbarComponent {
|
|
|
13735
13855
|
this.close.emit();
|
|
13736
13856
|
}
|
|
13737
13857
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
13738
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ToolbarComponent, isStandalone: true, selector: "pw-toolbar", inputs: { locale: "locale", availableLocales: "availableLocales", viewMode: "viewMode", pageViewAvailable: "pageViewAvailable", speechEnabled: "speechEnabled", audioEnabled: "audioEnabled", sfxEnabled: "sfxEnabled", autoplayEnabled: "autoplayEnabled", secondsPerPanel: "secondsPerPanel", autoplayProgress: "autoplayProgress", thumbnailsVisible: "thumbnailsVisible", hasAlternatives: "hasAlternatives", hasBranches: "hasBranches", liked: "liked", bookmarked: "bookmarked", showSocial: "showSocial", visible: "visible" }, outputs: { toggleView: "toggleView", localeChange: "localeChange", toggleSpeech: "toggleSpeech", toggleAudio: "toggleAudio", toggleSfx: "toggleSfx", toggleAutoplay: "toggleAutoplay", secondsPerPanelChange: "secondsPerPanelChange", toggleThumbnails: "toggleThumbnails", openToc: "openToc", openSettings: "openSettings", openCharacters: "openCharacters", cycleAlternative: "cycleAlternative", showBranches: "showBranches", openExtras: "openExtras", like: "like", bookmark: "bookmark", share: "share", openComments: "openComments", close: "close", openLanguage: "openLanguage" }, ngImport: i0, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
13858
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ToolbarComponent, isStandalone: true, selector: "pw-toolbar", inputs: { locale: "locale", availableLocales: "availableLocales", viewMode: "viewMode", pageViewAvailable: "pageViewAvailable", speechEnabled: "speechEnabled", audioEnabled: "audioEnabled", sfxEnabled: "sfxEnabled", autoplayEnabled: "autoplayEnabled", secondsPerPanel: "secondsPerPanel", autoplayProgress: "autoplayProgress", thumbnailsVisible: "thumbnailsVisible", hasAlternatives: "hasAlternatives", hasBranches: "hasBranches", liked: "liked", bookmarked: "bookmarked", showSocial: "showSocial", visible: "visible" }, outputs: { toggleView: "toggleView", localeChange: "localeChange", toggleSpeech: "toggleSpeech", toggleAudio: "toggleAudio", toggleSfx: "toggleSfx", toggleAutoplay: "toggleAutoplay", secondsPerPanelChange: "secondsPerPanelChange", toggleThumbnails: "toggleThumbnails", openToc: "openToc", openSettings: "openSettings", openCharacters: "openCharacters", cycleAlternative: "cycleAlternative", showBranches: "showBranches", openExtras: "openExtras", like: "like", bookmark: "bookmark", share: "share", openComments: "openComments", close: "close", openLanguage: "openLanguage" }, ngImport: i0, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
13739
13859
|
}
|
|
13740
13860
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ToolbarComponent, decorators: [{
|
|
13741
13861
|
type: Component,
|
|
13742
|
-
args: [{ selector: 'pw-toolbar', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"] }]
|
|
13862
|
+
args: [{ selector: 'pw-toolbar', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"] }]
|
|
13743
13863
|
}], propDecorators: { locale: [{
|
|
13744
13864
|
type: Input
|
|
13745
13865
|
}], availableLocales: [{
|
|
@@ -13870,11 +13990,11 @@ class LanguageModalComponent {
|
|
|
13870
13990
|
}
|
|
13871
13991
|
}
|
|
13872
13992
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LanguageModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
13873
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LanguageModalComponent, isStandalone: true, selector: "pw-language-modal", inputs: { visible: "visible", currentLocale: "currentLocale", availableLocales: "availableLocales" }, outputs: { closeModal: "closeModal", localeSelected: "localeSelected" }, ngImport: i0, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "
|
|
13993
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LanguageModalComponent, isStandalone: true, selector: "pw-language-modal", inputs: { visible: "visible", currentLocale: "currentLocale", availableLocales: "availableLocales" }, outputs: { closeModal: "closeModal", localeSelected: "localeSelected" }, ngImport: i0, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
13874
13994
|
}
|
|
13875
13995
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LanguageModalComponent, decorators: [{
|
|
13876
13996
|
type: Component,
|
|
13877
|
-
args: [{ selector: 'pw-language-modal', imports: [
|
|
13997
|
+
args: [{ selector: 'pw-language-modal', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"] }]
|
|
13878
13998
|
}], propDecorators: { visible: [{
|
|
13879
13999
|
type: Input
|
|
13880
14000
|
}], currentLocale: [{
|
|
@@ -14540,11 +14660,11 @@ class TocOverlayComponent {
|
|
|
14540
14660
|
return this.filteredChapters[this.selectedChapterIndex];
|
|
14541
14661
|
}
|
|
14542
14662
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TocOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14543
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: TocOverlayComponent, isStandalone: true, selector: "pw-toc-overlay", inputs: { manifest: "manifest", chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14663
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: TocOverlayComponent, isStandalone: true, selector: "pw-toc-overlay", inputs: { manifest: "manifest", chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14544
14664
|
}
|
|
14545
14665
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TocOverlayComponent, decorators: [{
|
|
14546
14666
|
type: Component,
|
|
14547
|
-
args: [{ selector: 'pw-toc-overlay', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"] }]
|
|
14667
|
+
args: [{ selector: 'pw-toc-overlay', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"] }]
|
|
14548
14668
|
}], propDecorators: { manifest: [{
|
|
14549
14669
|
type: Input
|
|
14550
14670
|
}], chapters: [{
|
|
@@ -14568,6 +14688,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
14568
14688
|
args: ['window:keydown', ['$event']]
|
|
14569
14689
|
}] } });
|
|
14570
14690
|
|
|
14691
|
+
/**
|
|
14692
|
+
* Translate a built-in UI string. Falls back to the given English text (with
|
|
14693
|
+
* `{{param}}` interpolation) when no TranslateService is provided or the key
|
|
14694
|
+
* is not loaded, so a public component embedded without ngx-translate never
|
|
14695
|
+
* shows a raw key.
|
|
14696
|
+
*/
|
|
14697
|
+
function uiText(translate, key, english, params) {
|
|
14698
|
+
const value = translate?.instant(key, params);
|
|
14699
|
+
if (typeof value === 'string' && value !== key) {
|
|
14700
|
+
return value;
|
|
14701
|
+
}
|
|
14702
|
+
return (english ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
|
|
14703
|
+
}
|
|
14704
|
+
|
|
14571
14705
|
/**
|
|
14572
14706
|
* Paywall Overlay Component
|
|
14573
14707
|
* Displays paywall gate UI with purchase options
|
|
@@ -14594,6 +14728,10 @@ const PAYWALL_TEXT_EN = {
|
|
|
14594
14728
|
'paywall.go_back': 'Go Back',
|
|
14595
14729
|
'paywall.maybe_later': 'Maybe Later',
|
|
14596
14730
|
'paywall.secure_payment': 'Secure payment processing',
|
|
14731
|
+
'paywall.reason_subscription_required': 'This part of the story is included with a subscription.',
|
|
14732
|
+
'paywall.reason_purchase_required': 'This part of the story is available to buy.',
|
|
14733
|
+
'paywall.reason_age_verification_required': 'This content is age-restricted. Confirm your age to continue.',
|
|
14734
|
+
'paywall.reason_entitlement_required': 'This content requires an entitlement to access.',
|
|
14597
14735
|
};
|
|
14598
14736
|
/**
|
|
14599
14737
|
* Paywall Overlay Component
|
|
@@ -14672,17 +14810,24 @@ class PaywallOverlayComponent {
|
|
|
14672
14810
|
if (this.message) {
|
|
14673
14811
|
return this.getLocalizedString(this.message);
|
|
14674
14812
|
}
|
|
14813
|
+
// A known lock reason is translated; otherwise the gate's own sentence.
|
|
14814
|
+
const key = 'paywall.reason_' + this.gate?.lockReason;
|
|
14815
|
+
const translated = this.t(key);
|
|
14816
|
+
if (translated !== key) {
|
|
14817
|
+
return translated;
|
|
14818
|
+
}
|
|
14675
14819
|
if (this.gate?.reason) {
|
|
14676
14820
|
return this.gate.reason;
|
|
14677
14821
|
}
|
|
14678
14822
|
return this.t('paywall.message_default');
|
|
14679
14823
|
}
|
|
14680
14824
|
/**
|
|
14681
|
-
* Handle purchase option selection
|
|
14825
|
+
* Handle purchase option selection: `purchase` carries the product id,
|
|
14826
|
+
* `action` is 'subscribe' for a subscription option, else 'purchase'.
|
|
14682
14827
|
*/
|
|
14683
|
-
onPurchaseOption(
|
|
14684
|
-
this.purchase.emit(productId);
|
|
14685
|
-
this.action.emit('purchase');
|
|
14828
|
+
onPurchaseOption(option) {
|
|
14829
|
+
this.purchase.emit(option.productId);
|
|
14830
|
+
this.action.emit(option.type === 'subscription' ? 'subscribe' : 'purchase');
|
|
14686
14831
|
}
|
|
14687
14832
|
/**
|
|
14688
14833
|
* Handle login action
|
|
@@ -14745,18 +14890,14 @@ class PaywallOverlayComponent {
|
|
|
14745
14890
|
* Manifest-provided texts (title/message/gate.reason) bypass this.
|
|
14746
14891
|
*/
|
|
14747
14892
|
t(key, params) {
|
|
14748
|
-
|
|
14749
|
-
if (typeof value === 'string' && value !== key) {
|
|
14750
|
-
return value;
|
|
14751
|
-
}
|
|
14752
|
-
return (PAYWALL_TEXT_EN[key] ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
|
|
14893
|
+
return uiText(this.translate, key, PAYWALL_TEXT_EN[key], params);
|
|
14753
14894
|
}
|
|
14754
14895
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14755
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option
|
|
14896
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14756
14897
|
}
|
|
14757
14898
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, decorators: [{
|
|
14758
14899
|
type: Component,
|
|
14759
|
-
args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option
|
|
14900
|
+
args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"] }]
|
|
14760
14901
|
}], propDecorators: { visible: [{
|
|
14761
14902
|
type: Input
|
|
14762
14903
|
}], gate: [{
|
|
@@ -15079,11 +15220,11 @@ class SettingsModalComponent {
|
|
|
15079
15220
|
return variable.max;
|
|
15080
15221
|
}
|
|
15081
15222
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SettingsModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15082
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: SettingsModalComponent, isStandalone: true, selector: "pw-settings-modal", inputs: { preferences: "preferences", availableLocales: "availableLocales", locale: "locale", variables: "variables", variableValues: "variableValues", visible: "visible" }, outputs: { preferencesChange: "preferencesChange", localeChange: "localeChange", variableChange: "variableChange", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15223
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: SettingsModalComponent, isStandalone: true, selector: "pw-settings-modal", inputs: { preferences: "preferences", availableLocales: "availableLocales", locale: "locale", variables: "variables", variableValues: "variableValues", visible: "visible" }, outputs: { preferencesChange: "preferencesChange", localeChange: "localeChange", variableChange: "variableChange", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15083
15224
|
}
|
|
15084
15225
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: SettingsModalComponent, decorators: [{
|
|
15085
15226
|
type: Component,
|
|
15086
|
-
args: [{ selector: 'pw-settings-modal', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"] }]
|
|
15227
|
+
args: [{ selector: 'pw-settings-modal', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"settings-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); cancel()\">\n <div class=\"settings-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Settings\">\n <!-- Header -->\n <div class=\"settings-header\">\n <h2 class=\"settings-title\">{{ 'settings.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"cancel()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Tabs -->\n <div class=\"settings-tabs\" role=\"tablist\">\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'preferences'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'preferences'\"\n (click)=\"switchTab('preferences')\">\n {{ 'settings.preferences' | translate }}\n </button>\n <button\n class=\"tab-btn\"\n [class.active]=\"activeTab === 'variables'\"\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'variables'\"\n (click)=\"switchTab('variables')\">\n {{ 'settings.variables' | translate }}\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"settings-content\">\n <!-- Preferences Tab -->\n @if (activeTab === 'preferences') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.language' | translate }}</h3>\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"locale-select\">{{ 'settings.interface_language' | translate }}</label>\n <select\n id=\"locale-select\"\n class=\"setting-select\"\n [ngModel]=\"locale\"\n (ngModelChange)=\"updateLocale($event)\">\n @for (loc of availableLocales; track loc) {\n <option [value]=\"loc\">{{ loc }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.display' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.mangaMode\"\n (ngModelChange)=\"updatePreference('mangaMode', $event)\" />\n <span>{{ 'settings.manga_mode' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.reducedMotion\"\n (ngModelChange)=\"updatePreference('reducedMotion', $event)\" />\n <span>{{ 'settings.reduced_motion' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.highContrast\"\n (ngModelChange)=\"updatePreference('highContrast', $event)\" />\n <span>{{ 'settings.high_contrast' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.content' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.speech\"\n (ngModelChange)=\"updatePreference('speech', $event)\" />\n <span>{{ 'settings.show_speech_bubbles' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.audio' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.audio\"\n (ngModelChange)=\"updatePreference('audio', $event)\" />\n <span>{{ 'settings.enable_audio' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.sfx\"\n (ngModelChange)=\"updatePreference('sfx', $event)\" />\n <span>{{ 'settings.enable_sfx' | translate }}</span>\n </label>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.autoplay' | translate }}</h3>\n \n <div class=\"setting-item\">\n <label class=\"setting-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [ngModel]=\"preferences.autoplay\"\n (ngModelChange)=\"updatePreference('autoplay', $event)\" />\n <span>{{ 'settings.enable_autoplay' | translate }}</span>\n </label>\n </div>\n\n <div class=\"setting-item\">\n <label class=\"setting-label\" for=\"seconds-input\">{{ 'settings.seconds_per_panel' | translate }}</label>\n <div class=\"number-input\">\n <input\n id=\"seconds-input\"\n type=\"number\"\n class=\"setting-number\"\n min=\"0.5\"\n max=\"120\"\n step=\"0.5\"\n [ngModel]=\"preferences.secondsPerPanel\"\n (ngModelChange)=\"updatePreference('secondsPerPanel', $event)\" />\n <span class=\"input-unit\">{{ 'settings.seconds_unit' | translate }}</span>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetPreferences()\">\n {{ 'settings.reset_defaults' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- Variables Tab -->\n @if (activeTab === 'variables') {\n <div class=\"tab-panel\" role=\"tabpanel\">\n @if (getPublicVariables().length > 0) {\n <div class=\"settings-section\">\n <h3 class=\"section-title\">{{ 'settings.public_variables' | translate }}</h3>\n \n @for (variable of getPublicVariables(); track variable.id) {\n <div class=\"setting-item variable-item\">\n <span class=\"setting-label\">{{ variable.id }}</span>\n \n <!-- Boolean Variable -->\n @if (isBoolean(variable)) {\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n class=\"setting-checkbox\"\n [checked]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).checked)\" />\n <span>{{ getVariableValue(variable) ? ('settings.enabled' | translate) : ('settings.disabled' | translate) }}</span>\n </label>\n }\n\n <!-- Number Variable -->\n @if (isNumber(variable)) {\n <div class=\"number-input\">\n <input\n type=\"number\"\n class=\"setting-number\"\n [min]=\"getMin(variable)\"\n [max]=\"getMax(variable)\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).valueAsNumber)\" />\n <button\n class=\"reset-btn-small\"\n type=\"button\"\n [attr.title]=\"'settings.reset_to_default' | translate\"\n (click)=\"resetVariable(variable)\">\n <pw-icon name=\"lucideRotateCcw\" />\n </button>\n </div>\n }\n\n <!-- Enum Variable -->\n @if (isEnum(variable)) {\n <select\n class=\"setting-select\"\n [value]=\"getVariableValue(variable)\"\n (change)=\"updateVariable(variable.id, $any($event.target).value)\">\n @for (option of getEnumOptions(variable); track option) {\n <option [value]=\"option\">{{ option }}</option>\n }\n </select>\n }\n\n <!-- String Variable -->\n @if (isString(variable)) {\n <input\n type=\"text\"\n class=\"setting-text\"\n [value]=\"getVariableValue(variable)\"\n (input)=\"updateVariable(variable.id, $any($event.target).value)\" />\n }\n </div>\n }\n </div>\n\n <div class=\"settings-actions\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"resetAllVariables()\">\n {{ 'settings.reset_all_variables' | translate }}\n </button>\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'settings.no_variables' | translate }}</p>\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"settings-footer\">\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"cancel()\">\n {{ 'settings.cancel' | translate }}\n </button>\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"save()\">\n {{ 'settings.save_changes' | translate }}\n </button>\n </div>\n </div>\n </div>\n}\n", styles: [".settings-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.settings-container{width:90%;max-width:700px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.settings-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.settings-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.settings-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.tab-btn{flex:1;padding:14px 20px;font-size:15px;font-weight:500;color:#fff9;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:all .2s ease}.tab-btn:hover{color:#fffc;background:#ffffff0d}.tab-btn.active{color:#fff;border-bottom-color:#0078d7cc}.settings-content{flex:1;overflow-y:auto;padding:24px}.settings-content::-webkit-scrollbar{width:8px}.settings-content::-webkit-scrollbar-track{background:#ffffff0d}.settings-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.tab-panel{display:flex;flex-direction:column;gap:24px}.settings-section{display:flex;flex-direction:column;gap:16px}.section-title{font-size:16px;font-weight:600;color:#ffffffe6;margin:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.1)}.setting-item{display:flex;flex-direction:column;gap:8px}.setting-item.variable-item{padding:12px;background:#ffffff05;border-radius:8px}.setting-label{font-size:14px;color:#fffc;display:flex;align-items:center;gap:10px;cursor:pointer;-webkit-user-select:none;user-select:none}.setting-label:has(.setting-checkbox){padding:8px;border-radius:6px;transition:background .2s ease}.setting-label:has(.setting-checkbox):hover{background:#ffffff0d}.setting-checkbox{width:20px;height:20px;cursor:pointer;accent-color:rgba(0,120,215,.8)}.setting-select{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;cursor:pointer;transition:all .2s ease}.setting-select:hover{background:#ffffff14;border-color:#ffffff4d}.setting-select:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.number-input{display:flex;align-items:center;gap:8px}.setting-number{flex:1;padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-number:hover{background:#ffffff14;border-color:#ffffff4d}.setting-number:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.input-unit{font-size:13px;color:#ffffff80;white-space:nowrap}.setting-text{padding:10px 14px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none;transition:all .2s ease}.setting-text:hover{background:#ffffff14;border-color:#ffffff4d}.setting-text:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.toggle-label{display:flex;align-items:center;gap:10px;padding:8px;border-radius:6px;cursor:pointer;transition:background .2s ease}.toggle-label:hover{background:#ffffff0d}.reset-btn-small{width:36px;height:36px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff9;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;flex-shrink:0}.reset-btn-small:hover{color:#fff;background:#ffffff1a;border-color:#ffffff4d}.settings-actions{display:flex;justify-content:flex-end;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.settings-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 20px;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all .2s ease;border:none}.action-btn.primary{color:#fff;background:#0078d7cc}.action-btn.primary:hover{background:#0078d7e6;transform:translateY(-1px);box-shadow:0 2px 8px #0078d74d}.action-btn.primary:active{transform:translateY(0)}.action-btn.secondary{color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2)}.action-btn.secondary:hover{background:#ffffff26;border-color:#ffffff4d}.action-btn.secondary:active{background:#ffffff1a}@media (max-width: 768px){.settings-container{width:95%;max-height:90vh}.settings-header{padding:16px 20px}.settings-title{font-size:18px}.settings-content{padding:20px}.tab-panel{gap:20px}.settings-section{gap:14px}.section-title{font-size:15px}.setting-label{font-size:13px}.settings-footer{padding:14px 20px;flex-wrap:wrap}.action-btn{flex:1;min-width:120px}}.close-btn:focus-visible,.tab-btn:focus-visible,.setting-select:focus-visible,.setting-number:focus-visible,.setting-text:focus-visible,.action-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.settings-overlay,.settings-container,.close-btn,.tab-btn,.setting-label,.action-btn{animation:none;transition:none}}\n"] }]
|
|
15087
15228
|
}], propDecorators: { preferences: [{
|
|
15088
15229
|
type: Input
|
|
15089
15230
|
}], availableLocales: [{
|
|
@@ -15404,11 +15545,11 @@ class CharacterRosterComponent {
|
|
|
15404
15545
|
}
|
|
15405
15546
|
}
|
|
15406
15547
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CharacterRosterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15407
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CharacterRosterComponent, isStandalone: true, selector: "pw-character-roster", inputs: { characters: "characters", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { characterSelect: "characterSelect", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15548
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CharacterRosterComponent, isStandalone: true, selector: "pw-character-roster", inputs: { characters: "characters", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { characterSelect: "characterSelect", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15408
15549
|
}
|
|
15409
15550
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CharacterRosterComponent, decorators: [{
|
|
15410
15551
|
type: Component,
|
|
15411
|
-
args: [{ selector: 'pw-character-roster', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"] }]
|
|
15552
|
+
args: [{ selector: 'pw-character-roster', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"] }]
|
|
15412
15553
|
}], propDecorators: { characters: [{
|
|
15413
15554
|
type: Input
|
|
15414
15555
|
}], locale: [{
|
|
@@ -15673,11 +15814,11 @@ class ExtrasViewerComponent {
|
|
|
15673
15814
|
}
|
|
15674
15815
|
}
|
|
15675
15816
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ExtrasViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15676
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ExtrasViewerComponent, isStandalone: true, selector: "pw-extras-viewer", inputs: { extras: "extras", initialExtraId: "initialExtraId", locale: "locale", baseUrl: "baseUrl", visible: "visible", hasPremiumAccess: "hasPremiumAccess" }, outputs: { purchase: "purchase", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15817
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ExtrasViewerComponent, isStandalone: true, selector: "pw-extras-viewer", inputs: { extras: "extras", initialExtraId: "initialExtraId", locale: "locale", baseUrl: "baseUrl", visible: "visible", hasPremiumAccess: "hasPremiumAccess" }, outputs: { purchase: "purchase", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15677
15818
|
}
|
|
15678
15819
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ExtrasViewerComponent, decorators: [{
|
|
15679
15820
|
type: Component,
|
|
15680
|
-
args: [{ selector: 'pw-extras-viewer', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"] }]
|
|
15821
|
+
args: [{ selector: 'pw-extras-viewer', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"] }]
|
|
15681
15822
|
}], propDecorators: { extras: [{
|
|
15682
15823
|
type: Input
|
|
15683
15824
|
}], initialExtraId: [{
|
|
@@ -15765,12 +15906,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15765
15906
|
* Age Gate Component
|
|
15766
15907
|
* Displays age verification UI for restricted content
|
|
15767
15908
|
*/
|
|
15909
|
+
/**
|
|
15910
|
+
* Built-in English UI strings (keys under `age_gate.`), used when no
|
|
15911
|
+
* TranslateService is provided or a key is not loaded. Must match the
|
|
15912
|
+
* "age_gate" section of assets/i18n/en.json.
|
|
15913
|
+
*/
|
|
15914
|
+
const AGE_GATE_TEXT_EN = {
|
|
15915
|
+
title: 'Age Verification Required',
|
|
15916
|
+
close: 'Close',
|
|
15917
|
+
warning: 'This content is restricted to users {{age}} years of age or older.',
|
|
15918
|
+
prompt: 'Please enter your birth date:',
|
|
15919
|
+
month: 'Month',
|
|
15920
|
+
day: 'Day',
|
|
15921
|
+
year: 'Year',
|
|
15922
|
+
verify: 'Verify Age',
|
|
15923
|
+
privacy: 'Your information is private and will not be stored.',
|
|
15924
|
+
error_incomplete: 'Please enter your complete birth date.',
|
|
15925
|
+
error_month: 'Please enter a valid month (1-12).',
|
|
15926
|
+
error_day: 'Please enter a valid day (1-{{max}}).',
|
|
15927
|
+
error_year: 'Please enter a valid year (1900-{{max}}).',
|
|
15928
|
+
error_too_young: 'You must be at least {{age}} years old to access this content.',
|
|
15929
|
+
};
|
|
15768
15930
|
/**
|
|
15769
15931
|
* Age Gate Component
|
|
15770
15932
|
* Modal overlay for age verification
|
|
15771
15933
|
*/
|
|
15772
15934
|
class AgeGateComponent {
|
|
15773
15935
|
constructor() {
|
|
15936
|
+
/** Optional: the gate is a public component and works (in English) without ngx-translate. */
|
|
15937
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
15774
15938
|
/**
|
|
15775
15939
|
* Visibility state
|
|
15776
15940
|
*/
|
|
@@ -15821,7 +15985,11 @@ class AgeGateComponent {
|
|
|
15821
15985
|
if (this.warningMessage) {
|
|
15822
15986
|
return this.warningMessage;
|
|
15823
15987
|
}
|
|
15824
|
-
return
|
|
15988
|
+
return this.t('warning', { age: this.minimumAge });
|
|
15989
|
+
}
|
|
15990
|
+
/** Translate an `age_gate.*` UI string (English fallback). */
|
|
15991
|
+
t(key, params) {
|
|
15992
|
+
return uiText(this.translate, 'age_gate.' + key, AGE_GATE_TEXT_EN[key], params);
|
|
15825
15993
|
}
|
|
15826
15994
|
/**
|
|
15827
15995
|
* Handle verification submit
|
|
@@ -15830,7 +15998,7 @@ class AgeGateComponent {
|
|
|
15830
15998
|
this.errorMessage = '';
|
|
15831
15999
|
// Validate inputs
|
|
15832
16000
|
if (!this.birthMonth || !this.birthDay || !this.birthYear) {
|
|
15833
|
-
this.errorMessage =
|
|
16001
|
+
this.errorMessage = this.t('error_incomplete');
|
|
15834
16002
|
return;
|
|
15835
16003
|
}
|
|
15836
16004
|
const month = parseInt(this.birthMonth, 10);
|
|
@@ -15838,30 +16006,30 @@ class AgeGateComponent {
|
|
|
15838
16006
|
const year = parseInt(this.birthYear, 10);
|
|
15839
16007
|
// Validate ranges
|
|
15840
16008
|
if (month < 1 || month > 12) {
|
|
15841
|
-
this.errorMessage = '
|
|
16009
|
+
this.errorMessage = this.t('error_month');
|
|
15842
16010
|
return;
|
|
15843
16011
|
}
|
|
15844
16012
|
if (day < 1 || day > 31) {
|
|
15845
|
-
this.errorMessage = '
|
|
16013
|
+
this.errorMessage = this.t('error_day', { max: 31 });
|
|
15846
16014
|
return;
|
|
15847
16015
|
}
|
|
15848
16016
|
const currentYear = new Date().getFullYear();
|
|
15849
16017
|
if (year < 1900 || year > currentYear) {
|
|
15850
|
-
this.errorMessage =
|
|
16018
|
+
this.errorMessage = this.t('error_year', { max: currentYear });
|
|
15851
16019
|
return;
|
|
15852
16020
|
}
|
|
15853
16021
|
// Reject calendar-impossible dates (31 Apr, 30 Feb, 29 Feb in non-leap
|
|
15854
16022
|
// years): new Date() would silently roll them over into the next month.
|
|
15855
16023
|
const daysInMonth = new Date(year, month, 0).getDate();
|
|
15856
16024
|
if (day > daysInMonth) {
|
|
15857
|
-
this.errorMessage =
|
|
16025
|
+
this.errorMessage = this.t('error_day', { max: daysInMonth });
|
|
15858
16026
|
return;
|
|
15859
16027
|
}
|
|
15860
16028
|
// Calculate age
|
|
15861
16029
|
const birthDate = new Date(year, month - 1, day);
|
|
15862
16030
|
const age = this.calculateAge(birthDate);
|
|
15863
16031
|
if (age < this.minimumAge) {
|
|
15864
|
-
this.errorMessage =
|
|
16032
|
+
this.errorMessage = this.t('error_too_young', { age: this.minimumAge });
|
|
15865
16033
|
this.verify.emit({
|
|
15866
16034
|
verified: false,
|
|
15867
16035
|
age,
|
|
@@ -15906,17 +16074,26 @@ class AgeGateComponent {
|
|
|
15906
16074
|
return age;
|
|
15907
16075
|
}
|
|
15908
16076
|
/**
|
|
15909
|
-
*
|
|
16077
|
+
* Month options, named in the current locale (Intl; cached per locale).
|
|
15910
16078
|
*/
|
|
15911
16079
|
getMonthOptions() {
|
|
15912
|
-
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
16080
|
+
if (this.monthCache?.locale !== this.locale) {
|
|
16081
|
+
let format;
|
|
16082
|
+
try {
|
|
16083
|
+
format = new Intl.DateTimeFormat(this.locale, { month: 'long', timeZone: 'UTC' });
|
|
16084
|
+
}
|
|
16085
|
+
catch {
|
|
16086
|
+
format = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' });
|
|
16087
|
+
}
|
|
16088
|
+
this.monthCache = {
|
|
16089
|
+
locale: this.locale,
|
|
16090
|
+
options: Array.from({ length: 12 }, (_, index) => ({
|
|
16091
|
+
value: String(index + 1),
|
|
16092
|
+
label: format.format(Date.UTC(2000, index, 1)),
|
|
16093
|
+
})),
|
|
16094
|
+
};
|
|
16095
|
+
}
|
|
16096
|
+
return this.monthCache.options;
|
|
15920
16097
|
}
|
|
15921
16098
|
/**
|
|
15922
16099
|
* Get day options
|
|
@@ -15936,11 +16113,11 @@ class AgeGateComponent {
|
|
|
15936
16113
|
return years;
|
|
15937
16114
|
}
|
|
15938
16115
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15939
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"
|
|
16116
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15940
16117
|
}
|
|
15941
16118
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, decorators: [{
|
|
15942
16119
|
type: Component,
|
|
15943
|
-
args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" aria-label=\"
|
|
16120
|
+
args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"] }]
|
|
15944
16121
|
}], propDecorators: { visible: [{
|
|
15945
16122
|
type: Input
|
|
15946
16123
|
}], minimumAge: [{
|
|
@@ -15967,8 +16144,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15967
16144
|
* priority. Opened from the toolbar's "Choices" button; follows the shell's
|
|
15968
16145
|
* modal contract (visible input + close output).
|
|
15969
16146
|
*/
|
|
16147
|
+
/** English fallback for the `branch_chooser.*` keys (see assets/i18n/en.json). */
|
|
16148
|
+
const BRANCH_TEXT_EN = {
|
|
16149
|
+
title: 'Choose your path',
|
|
16150
|
+
close: 'Close',
|
|
16151
|
+
option: 'Option {{n}}',
|
|
16152
|
+
};
|
|
15970
16153
|
class BranchChooserComponent {
|
|
15971
16154
|
constructor() {
|
|
16155
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
15972
16156
|
/** Visible state */
|
|
15973
16157
|
this.visible = false;
|
|
15974
16158
|
/** Current locale (labels are already resolved by the shell). */
|
|
@@ -15980,6 +16164,10 @@ class BranchChooserComponent {
|
|
|
15980
16164
|
/** Close requested without choosing. */
|
|
15981
16165
|
this.close = new EventEmitter();
|
|
15982
16166
|
}
|
|
16167
|
+
/** Translate a `branch_chooser.*` UI string (English fallback). */
|
|
16168
|
+
t(key, params) {
|
|
16169
|
+
return uiText(this.translate, 'branch_chooser.' + key, BRANCH_TEXT_EN[key], params);
|
|
16170
|
+
}
|
|
15983
16171
|
onEscape() {
|
|
15984
16172
|
if (this.visible) {
|
|
15985
16173
|
this.close.emit();
|
|
@@ -15994,11 +16182,11 @@ class BranchChooserComponent {
|
|
|
15994
16182
|
return choice.index;
|
|
15995
16183
|
}
|
|
15996
16184
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15997
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">
|
|
16185
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15998
16186
|
}
|
|
15999
16187
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, decorators: [{
|
|
16000
16188
|
type: Component,
|
|
16001
|
-
args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">
|
|
16189
|
+
args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"] }]
|
|
16002
16190
|
}], propDecorators: { visible: [{
|
|
16003
16191
|
type: Input
|
|
16004
16192
|
}], locale: [{
|
|
@@ -16314,11 +16502,11 @@ class ShareModalComponent {
|
|
|
16314
16502
|
}
|
|
16315
16503
|
}
|
|
16316
16504
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ShareModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16317
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ShareModalComponent, isStandalone: true, selector: "pw-share-modal", inputs: { shareUrl: "shareUrl", shareTitle: "shareTitle", shareDescription: "shareDescription", visible: "visible", shareable: "shareable" }, outputs: { close: "close", share: "share" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16505
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ShareModalComponent, isStandalone: true, selector: "pw-share-modal", inputs: { shareUrl: "shareUrl", shareTitle: "shareTitle", shareDescription: "shareDescription", visible: "visible", shareable: "shareable" }, outputs: { close: "close", share: "share" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16318
16506
|
}
|
|
16319
16507
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ShareModalComponent, decorators: [{
|
|
16320
16508
|
type: Component,
|
|
16321
|
-
args: [{ selector: 'pw-share-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"] }]
|
|
16509
|
+
args: [{ selector: 'pw-share-modal', imports: [TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"] }]
|
|
16322
16510
|
}], propDecorators: { shareUrl: [{
|
|
16323
16511
|
type: Input
|
|
16324
16512
|
}], shareTitle: [{
|
|
@@ -16557,11 +16745,11 @@ class CommentsDrawerComponent {
|
|
|
16557
16745
|
}
|
|
16558
16746
|
}
|
|
16559
16747
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CommentsDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16560
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CommentsDrawerComponent, isStandalone: true, selector: "pw-comments-drawer", inputs: { comments: "comments", visible: "visible", loading: "loading", currentPage: "currentPage", totalPages: "totalPages", hasMore: "hasMore", isAuthenticated: "isAuthenticated" }, outputs: { postComment: "postComment", loadMore: "loadMore", likeComment: "likeComment", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16748
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CommentsDrawerComponent, isStandalone: true, selector: "pw-comments-drawer", inputs: { comments: "comments", visible: "visible", loading: "loading", currentPage: "currentPage", totalPages: "totalPages", hasMore: "hasMore", isAuthenticated: "isAuthenticated" }, outputs: { postComment: "postComment", loadMore: "loadMore", likeComment: "likeComment", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16561
16749
|
}
|
|
16562
16750
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CommentsDrawerComponent, decorators: [{
|
|
16563
16751
|
type: Component,
|
|
16564
|
-
args: [{ selector: 'pw-comments-drawer', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"] }]
|
|
16752
|
+
args: [{ selector: 'pw-comments-drawer', imports: [FormsModule, TranslatePipe, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"] }]
|
|
16565
16753
|
}], propDecorators: { comments: [{
|
|
16566
16754
|
type: Input
|
|
16567
16755
|
}], visible: [{
|
|
@@ -16607,8 +16795,11 @@ function createTranslateLoader(http) {
|
|
|
16607
16795
|
class PlayerShellComponent {
|
|
16608
16796
|
constructor() {
|
|
16609
16797
|
/**
|
|
16610
|
-
* Emitted when a reader acts on the paywall overlay
|
|
16611
|
-
*
|
|
16798
|
+
* Emitted when a reader acts on the paywall overlay: `purchase` /
|
|
16799
|
+
* `subscribe` (with the chosen option's `productId` — a product id or a
|
|
16800
|
+
* subscription tier), `login` or `dismiss`. The host owns checkout — the
|
|
16801
|
+
* player never talks to a payment provider itself; call
|
|
16802
|
+
* `refreshEntitlements()` (or pass a new `entitlementSnapshot`) afterwards.
|
|
16612
16803
|
*/
|
|
16613
16804
|
this.paywallAction = new EventEmitter();
|
|
16614
16805
|
/**
|
|
@@ -16669,7 +16860,8 @@ class PlayerShellComponent {
|
|
|
16669
16860
|
*/
|
|
16670
16861
|
this.ready = new EventEmitter();
|
|
16671
16862
|
/**
|
|
16672
|
-
*
|
|
16863
|
+
* The current panel changed. Carries the panel's id (`panelId`, its key in
|
|
16864
|
+
* `chapter.panels`) and the id of the panel before it (`previousPanelId`).
|
|
16673
16865
|
*/
|
|
16674
16866
|
this.panelChange = new EventEmitter();
|
|
16675
16867
|
/**
|
|
@@ -16890,6 +17082,10 @@ class PlayerShellComponent {
|
|
|
16890
17082
|
this.translationService = inject(TranslationService);
|
|
16891
17083
|
this.canvasCamera = inject(CanvasCameraService);
|
|
16892
17084
|
this.paywallService = inject(PaywallService);
|
|
17085
|
+
/** ngOnInit ran: later input changes are live updates, not initial values. */
|
|
17086
|
+
this.initialized = false;
|
|
17087
|
+
/** The state subscriptions are set up once per instance, not per (re)load. */
|
|
17088
|
+
this.stateSubscribed = false;
|
|
16893
17089
|
}
|
|
16894
17090
|
/**
|
|
16895
17091
|
* Effective reduced-motion state for every transition the shell drives
|
|
@@ -16903,6 +17099,7 @@ class PlayerShellComponent {
|
|
|
16903
17099
|
* Initialize component
|
|
16904
17100
|
*/
|
|
16905
17101
|
ngOnInit() {
|
|
17102
|
+
this.initialized = true;
|
|
16906
17103
|
this.toolbarVisible = this.showToolbar;
|
|
16907
17104
|
this.watchReducedMotion();
|
|
16908
17105
|
this.subscribeToVideoSignals();
|
|
@@ -16913,6 +17110,101 @@ class PlayerShellComponent {
|
|
|
16913
17110
|
window.addEventListener('pagehide', this.onPageHide);
|
|
16914
17111
|
}
|
|
16915
17112
|
}
|
|
17113
|
+
/**
|
|
17114
|
+
* Inputs changed after init. The first round (before ngOnInit) is skipped:
|
|
17115
|
+
* ngOnInit reads every input itself, so nothing loads twice.
|
|
17116
|
+
*/
|
|
17117
|
+
ngOnChanges(changes) {
|
|
17118
|
+
if (!this.initialized) {
|
|
17119
|
+
return;
|
|
17120
|
+
}
|
|
17121
|
+
if (changes['manifest'] || changes['manifestUrl']) {
|
|
17122
|
+
// A new work: reload re-reads every other input as well.
|
|
17123
|
+
void this.reload();
|
|
17124
|
+
return;
|
|
17125
|
+
}
|
|
17126
|
+
if (changes['locale']) {
|
|
17127
|
+
this.applyLocale();
|
|
17128
|
+
}
|
|
17129
|
+
if (changes['entitlementSnapshot']) {
|
|
17130
|
+
if (!this.entitlementSnapshot && !this.httpEntitlement) {
|
|
17131
|
+
this.paywallService.setSnapshot(null);
|
|
17132
|
+
}
|
|
17133
|
+
this.refreshEntitlements(this.entitlementSnapshot).catch(() => undefined);
|
|
17134
|
+
}
|
|
17135
|
+
if (changes['viewModeOverride']) {
|
|
17136
|
+
this.applyViewModeOverride();
|
|
17137
|
+
}
|
|
17138
|
+
if (changes['showToolbar']) {
|
|
17139
|
+
this.toolbarVisible = this.showToolbar;
|
|
17140
|
+
}
|
|
17141
|
+
if (changes['autoplay'] && this.autoplay !== this.autoplayEnabled) {
|
|
17142
|
+
this.onToggleAutoplay();
|
|
17143
|
+
}
|
|
17144
|
+
else if ((changes['secondsPerPanel'] || changes['reducedMotion']) && this.autoplayEnabled) {
|
|
17145
|
+
// Re-arm with the new timing / video start modes.
|
|
17146
|
+
this.startAutoplay();
|
|
17147
|
+
}
|
|
17148
|
+
this.cdr.markForCheck();
|
|
17149
|
+
}
|
|
17150
|
+
/**
|
|
17151
|
+
* Reload the work from the current `manifest` / `manifestUrl` inputs
|
|
17152
|
+
* (also the error screen's Retry). Story state of the previous load —
|
|
17153
|
+
* position, variables except persistent ones, open overlays, autoplay —
|
|
17154
|
+
* is reset; the analytics session of the previous work is closed.
|
|
17155
|
+
*/
|
|
17156
|
+
async reload() {
|
|
17157
|
+
this.stopAutoplay();
|
|
17158
|
+
this.autoplayEnabled = false;
|
|
17159
|
+
this.videoSequencer.reset();
|
|
17160
|
+
this.panelAudio.stopAll();
|
|
17161
|
+
this.endAnalyticsSession();
|
|
17162
|
+
this.analyticsSessionStarted = this.analyticsSessionEnded = this.workCompleteTracked = false;
|
|
17163
|
+
this.lastTrackedPanelId = undefined;
|
|
17164
|
+
this.lastPanelChangeId = undefined;
|
|
17165
|
+
this.closePaywall();
|
|
17166
|
+
this.closeAgeGate();
|
|
17167
|
+
this.tocVisible = this.settingsVisible = this.languageModalVisible = this.charactersVisible = false;
|
|
17168
|
+
this.extrasVisible = this.shareVisible = this.commentsVisible = this.thumbnailsVisible = false;
|
|
17169
|
+
this.actionModalVisible = this.branchChooserVisible = false;
|
|
17170
|
+
this.variantOverrides.clear();
|
|
17171
|
+
this.variableStore.setDefinitions([]);
|
|
17172
|
+
for (const scope of ['global', 'chapter', 'page', 'session']) {
|
|
17173
|
+
this.variableStore.resetScope(scope);
|
|
17174
|
+
}
|
|
17175
|
+
this.currentChapter = this.currentPanel = this.currentPage = this.effectivePanel = undefined;
|
|
17176
|
+
this.viewMode = 'panel';
|
|
17177
|
+
this.visitedPanelIds = [];
|
|
17178
|
+
this.httpEntitlement = undefined;
|
|
17179
|
+
this.hasError = false;
|
|
17180
|
+
this.errorMessage = '';
|
|
17181
|
+
await this.initializePlayer();
|
|
17182
|
+
}
|
|
17183
|
+
/** Content locale + GUI language follow `locale`. */
|
|
17184
|
+
applyLocale() {
|
|
17185
|
+
this.playerState.setLocale(this.locale);
|
|
17186
|
+
this.translationService.setLanguage(this.locale);
|
|
17187
|
+
}
|
|
17188
|
+
/** Apply a changed `viewModeOverride` to the running view. */
|
|
17189
|
+
applyViewModeOverride() {
|
|
17190
|
+
const before = this.viewMode;
|
|
17191
|
+
if (this.viewModeOverride === 'panel' && this.viewMode === 'page') {
|
|
17192
|
+
this.viewMode = 'panel';
|
|
17193
|
+
this.videoSequencer.reset();
|
|
17194
|
+
}
|
|
17195
|
+
else {
|
|
17196
|
+
this.updateCanvasAvailability();
|
|
17197
|
+
}
|
|
17198
|
+
if (this.viewMode === before) {
|
|
17199
|
+
return;
|
|
17200
|
+
}
|
|
17201
|
+
if (this.viewMode === 'canvas') {
|
|
17202
|
+
this.jumpCameraToCurrentPanel();
|
|
17203
|
+
}
|
|
17204
|
+
if (this.autoplayEnabled) {
|
|
17205
|
+
this.startAutoplay();
|
|
17206
|
+
}
|
|
17207
|
+
}
|
|
16916
17208
|
/**
|
|
16917
17209
|
* Cleanup on destroy
|
|
16918
17210
|
*/
|
|
@@ -17029,8 +17321,7 @@ class PlayerShellComponent {
|
|
|
17029
17321
|
this.variableStore.seed(context);
|
|
17030
17322
|
}
|
|
17031
17323
|
// Set initial locale and configure translations
|
|
17032
|
-
this.
|
|
17033
|
-
this.translationService.setLanguage(this.locale);
|
|
17324
|
+
this.applyLocale();
|
|
17034
17325
|
// Check if page view is available
|
|
17035
17326
|
const loadedManifest = this.manifestService.getManifest();
|
|
17036
17327
|
if (loadedManifest?.chapters) {
|
|
@@ -17065,12 +17356,19 @@ class PlayerShellComponent {
|
|
|
17065
17356
|
}
|
|
17066
17357
|
// Subscribe to state changes
|
|
17067
17358
|
this.subscribeToStateChanges();
|
|
17359
|
+
// The host's `autoplay` input decides at start; the reader's toolbar
|
|
17360
|
+
// toggle takes over afterwards.
|
|
17361
|
+
if (this.autoplay && !this.autoplayEnabled) {
|
|
17362
|
+
this.onToggleAutoplay();
|
|
17363
|
+
}
|
|
17068
17364
|
this.loading = false;
|
|
17069
17365
|
this.ready.emit();
|
|
17070
17366
|
}
|
|
17071
17367
|
catch (err) {
|
|
17072
17368
|
this.handleError(err);
|
|
17073
17369
|
}
|
|
17370
|
+
// Async completion (e.g. a reload from ngOnChanges): repaint the OnPush view.
|
|
17371
|
+
this.cdr.markForCheck();
|
|
17074
17372
|
}
|
|
17075
17373
|
/**
|
|
17076
17374
|
* Configure the TrackingService from the manifest's `tracking` section:
|
|
@@ -17175,9 +17473,7 @@ class PlayerShellComponent {
|
|
|
17175
17473
|
* one-time `work_complete` signal.
|
|
17176
17474
|
*/
|
|
17177
17475
|
trackPanelView(panel, chapter) {
|
|
17178
|
-
|
|
17179
|
-
// (same identity lookup as getCurrentPanelId).
|
|
17180
|
-
const panelId = Object.entries(chapter.panels ?? {}).find(([, p]) => p === panel)?.[0];
|
|
17476
|
+
const panelId = this.panelIdOf(panel, chapter);
|
|
17181
17477
|
if (!panelId || panelId === this.lastTrackedPanelId) {
|
|
17182
17478
|
return;
|
|
17183
17479
|
}
|
|
@@ -17195,6 +17491,14 @@ class PlayerShellComponent {
|
|
|
17195
17491
|
});
|
|
17196
17492
|
}
|
|
17197
17493
|
}
|
|
17494
|
+
/**
|
|
17495
|
+
* A panel's id: its key in `chapter.panels`. Panels don't reliably carry
|
|
17496
|
+
* their own id (`Panel.id` is optional in the format), so resolve it by
|
|
17497
|
+
* identity, as getCurrentPanelId() does.
|
|
17498
|
+
*/
|
|
17499
|
+
panelIdOf(panel, chapter) {
|
|
17500
|
+
return Object.entries(chapter.panels ?? {}).find(([, p]) => p === panel)?.[0];
|
|
17501
|
+
}
|
|
17198
17502
|
/** An end panel (no outgoing edges) of the manifest's last chapter. */
|
|
17199
17503
|
isEndOfWork(panelId, chapter) {
|
|
17200
17504
|
const chapters = this.manifestService.getManifest()?.chapters ?? [];
|
|
@@ -17225,6 +17529,10 @@ class PlayerShellComponent {
|
|
|
17225
17529
|
* Subscribe to state changes
|
|
17226
17530
|
*/
|
|
17227
17531
|
subscribeToStateChanges() {
|
|
17532
|
+
if (this.stateSubscribed) {
|
|
17533
|
+
return;
|
|
17534
|
+
}
|
|
17535
|
+
this.stateSubscribed = true;
|
|
17228
17536
|
// Listen to panel changes
|
|
17229
17537
|
this.playerState.currentPanel$
|
|
17230
17538
|
.pipe(takeUntil(this.destroy$))
|
|
@@ -17234,7 +17542,16 @@ class PlayerShellComponent {
|
|
|
17234
17542
|
// Resolve variants before emitting so consumers (and the
|
|
17235
17543
|
// tracked event) see the effective panel state.
|
|
17236
17544
|
this.refreshResolvedPanels();
|
|
17237
|
-
|
|
17545
|
+
// Panels are keyed by id in chapter.panels; the lookup always hits
|
|
17546
|
+
// for a panel the shell navigated to ('' only guards the type).
|
|
17547
|
+
const panelId = this.panelIdOf(panel, this.currentChapter) ?? '';
|
|
17548
|
+
this.panelChange.emit({
|
|
17549
|
+
panel,
|
|
17550
|
+
chapter: this.currentChapter,
|
|
17551
|
+
panelId,
|
|
17552
|
+
previousPanelId: this.lastPanelChangeId,
|
|
17553
|
+
});
|
|
17554
|
+
this.lastPanelChangeId = panelId || undefined;
|
|
17238
17555
|
this.trackPanelView(panel, this.currentChapter);
|
|
17239
17556
|
}
|
|
17240
17557
|
});
|
|
@@ -17294,8 +17611,11 @@ class PlayerShellComponent {
|
|
|
17294
17611
|
* @param transition - Optional transition to render for this panel change
|
|
17295
17612
|
* (already resolved against the outputPresets default). Omitted for
|
|
17296
17613
|
* non-adjacent jumps (TOC, initial load), which swap instantly.
|
|
17614
|
+
* @param mutations - Variable mutations of the traversed edge. Applied only
|
|
17615
|
+
* once the navigation is allowed (after the paywall / age gate), so a
|
|
17616
|
+
* blocked attempt never changes story state.
|
|
17297
17617
|
*/
|
|
17298
|
-
async navigateToPanel(chapterId, panelId, transition, cameraMove) {
|
|
17618
|
+
async navigateToPanel(chapterId, panelId, transition, cameraMove, mutations) {
|
|
17299
17619
|
try {
|
|
17300
17620
|
this.viewportTransition = transition ?? null;
|
|
17301
17621
|
// Check entitlement. A host-supplied adapter still wins — it is the
|
|
@@ -17304,10 +17624,11 @@ class PlayerShellComponent {
|
|
|
17304
17624
|
if (this.entitlementAdapter) {
|
|
17305
17625
|
const hasAccess = await this.entitlementAdapter.hasAccess(panelId);
|
|
17306
17626
|
if (!hasAccess) {
|
|
17307
|
-
this.openPaywall(this.paywallService.gateFor(panelId) ?? {
|
|
17627
|
+
this.openPaywall(this.paywallService.gateFor(panelId, this.locale) ?? {
|
|
17308
17628
|
scope: 'panel',
|
|
17309
17629
|
refId: panelId,
|
|
17310
|
-
reason:
|
|
17630
|
+
reason: PaywallService.readerMessage(null),
|
|
17631
|
+
lockReason: 'entitlement_required',
|
|
17311
17632
|
});
|
|
17312
17633
|
return;
|
|
17313
17634
|
}
|
|
@@ -17318,10 +17639,10 @@ class PlayerShellComponent {
|
|
|
17318
17639
|
// the last panel they were entitled to see.
|
|
17319
17640
|
if (this.paywallService.evaluate(panelId).reason === 'age_verification_required') {
|
|
17320
17641
|
// An age gate is answered in-player (birth date), not by checkout.
|
|
17321
|
-
this.openAgeGate(chapterId, panelId, transition, cameraMove);
|
|
17642
|
+
this.openAgeGate(chapterId, panelId, transition, cameraMove, mutations);
|
|
17322
17643
|
return;
|
|
17323
17644
|
}
|
|
17324
|
-
const gate = this.paywallService.gateFor(panelId);
|
|
17645
|
+
const gate = this.paywallService.gateFor(panelId, this.locale);
|
|
17325
17646
|
if (gate) {
|
|
17326
17647
|
this.openPaywall(gate);
|
|
17327
17648
|
return;
|
|
@@ -17335,6 +17656,10 @@ class PlayerShellComponent {
|
|
|
17335
17656
|
if (!panelData) {
|
|
17336
17657
|
throw new Error(`Panel not found: ${panelId}`);
|
|
17337
17658
|
}
|
|
17659
|
+
// Edge `action` mutations (schema: "applied when traversing this edge").
|
|
17660
|
+
if (mutations?.length) {
|
|
17661
|
+
this.variableStore.applyMutations(mutations, { chapterId });
|
|
17662
|
+
}
|
|
17338
17663
|
const chapterChanged = this.currentChapter !== chapter;
|
|
17339
17664
|
const leavingPanelId = this.getCurrentPanelId();
|
|
17340
17665
|
this.currentChapter = chapter;
|
|
@@ -17469,7 +17794,8 @@ class PlayerShellComponent {
|
|
|
17469
17794
|
buildExtrasList(manifest) {
|
|
17470
17795
|
// Media (images / video / audio / thumbnails) resolve through the asset
|
|
17471
17796
|
// catalog; see utils/extras-utils.ts for the ExtraBlock mapping.
|
|
17472
|
-
|
|
17797
|
+
// An extras-scoped paywall rule the reader does not satisfy locks its block.
|
|
17798
|
+
this.extrasList = extrasFromManifest(manifest?.extras, (assetId) => this.manifestService.getAsset(assetId)).map((extra) => (this.paywallService.isExtraLocked(extra.id) ? { ...extra, gated: true } : extra));
|
|
17473
17799
|
}
|
|
17474
17800
|
/**
|
|
17475
17801
|
* Canvas view is available when the current chapter has a canvas layout
|
|
@@ -17529,12 +17855,7 @@ class PlayerShellComponent {
|
|
|
17529
17855
|
return;
|
|
17530
17856
|
const result = this.flowEngine.getNextPanel(this.currentChapter.graph, currentPanelId, context, this.flowEngine.getDefaultTransition(this.manifestService.getManifest()?.settings), this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
|
|
17531
17857
|
if (result.nextPanelId) {
|
|
17532
|
-
|
|
17533
|
-
// edge") were returned by the flow engine but never applied here.
|
|
17534
|
-
if (result.action?.length) {
|
|
17535
|
-
this.variableStore.applyMutations(result.action, { chapterId: this.currentChapter.id });
|
|
17536
|
-
}
|
|
17537
|
-
await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove);
|
|
17858
|
+
await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove, result.action);
|
|
17538
17859
|
}
|
|
17539
17860
|
}
|
|
17540
17861
|
catch (err) {
|
|
@@ -17663,16 +17984,7 @@ class PlayerShellComponent {
|
|
|
17663
17984
|
else if (newMode === 'canvas') {
|
|
17664
17985
|
this.viewMode = 'canvas';
|
|
17665
17986
|
this.videoSequencer.reset();
|
|
17666
|
-
|
|
17667
|
-
if (this.currentChapter) {
|
|
17668
|
-
const panelId = this.getCurrentPanelId();
|
|
17669
|
-
if (panelId) {
|
|
17670
|
-
const placement = this.currentChapter.canvas?.placements?.[panelId];
|
|
17671
|
-
if (placement) {
|
|
17672
|
-
this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
|
|
17673
|
-
}
|
|
17674
|
-
}
|
|
17675
|
-
}
|
|
17987
|
+
this.jumpCameraToCurrentPanel();
|
|
17676
17988
|
}
|
|
17677
17989
|
else {
|
|
17678
17990
|
// Switch to panel view
|
|
@@ -17685,6 +17997,14 @@ class PlayerShellComponent {
|
|
|
17685
17997
|
}
|
|
17686
17998
|
console.log('View mode:', this.viewMode);
|
|
17687
17999
|
}
|
|
18000
|
+
/** Land the canvas camera on the current panel (entering canvas view). */
|
|
18001
|
+
jumpCameraToCurrentPanel() {
|
|
18002
|
+
const panelId = this.getCurrentPanelId();
|
|
18003
|
+
const placement = panelId ? this.currentChapter?.canvas?.placements?.[panelId] : undefined;
|
|
18004
|
+
if (placement) {
|
|
18005
|
+
this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
|
|
18006
|
+
}
|
|
18007
|
+
}
|
|
17688
18008
|
/**
|
|
17689
18009
|
* Tap on a placed panel in canvas view. Traverses the matching out-edge of
|
|
17690
18010
|
* the current panel when its condition passes (the graph still gates
|
|
@@ -17707,7 +18027,7 @@ class PlayerShellComponent {
|
|
|
17707
18027
|
if (edge) {
|
|
17708
18028
|
this.navigationAttempt.emit({ direction: 'panel', target: panelId });
|
|
17709
18029
|
void this.navigateToPanel(this.currentChapter.id, panelId, edge.transition, edge.cameraMove ??
|
|
17710
|
-
this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
|
|
18030
|
+
this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings), edge.action);
|
|
17711
18031
|
return;
|
|
17712
18032
|
}
|
|
17713
18033
|
if (this.visitedPanelIds.includes(panelId)) {
|
|
@@ -17715,6 +18035,14 @@ class PlayerShellComponent {
|
|
|
17715
18035
|
void this.navigateToPanel(this.currentChapter.id, panelId);
|
|
17716
18036
|
}
|
|
17717
18037
|
}
|
|
18038
|
+
/**
|
|
18039
|
+
* The manifest actually loaded — from `manifest` or fetched via
|
|
18040
|
+
* `manifestUrl`. Everything that lists the work (ToC, thumbnails) binds to
|
|
18041
|
+
* this, never to the `manifest` input, which is unset for URL loading.
|
|
18042
|
+
*/
|
|
18043
|
+
get loadedManifest() {
|
|
18044
|
+
return this.manifestService.getManifest() ?? undefined;
|
|
18045
|
+
}
|
|
17718
18046
|
/** Balloon defaults for the canvas stage (same source as the viewport). */
|
|
17719
18047
|
get manifestBalloonConfig() {
|
|
17720
18048
|
return this.manifestService.getManifest()?.settings?.typography?.balloon_config ?? null;
|
|
@@ -17728,12 +18056,9 @@ class PlayerShellComponent {
|
|
|
17728
18056
|
return this.manifestService.getManifest()?.settings?.preload ?? null;
|
|
17729
18057
|
}
|
|
17730
18058
|
onLocaleChange(locale) {
|
|
17731
|
-
//
|
|
18059
|
+
// Content locale and GUI language stay in sync.
|
|
17732
18060
|
this.locale = locale;
|
|
17733
|
-
this.
|
|
17734
|
-
// Update GUI language (sync with content locale)
|
|
17735
|
-
this.translationService.setLanguage(locale);
|
|
17736
|
-
console.log('Locale changed to:', locale);
|
|
18061
|
+
this.applyLocale();
|
|
17737
18062
|
}
|
|
17738
18063
|
onToggleSpeech() {
|
|
17739
18064
|
this.setSpeechEnabled(!this.speechEnabled);
|
|
@@ -18055,7 +18380,15 @@ class PlayerShellComponent {
|
|
|
18055
18380
|
*/
|
|
18056
18381
|
handleKeyboard(event) {
|
|
18057
18382
|
// Don't handle if user is typing in an input
|
|
18058
|
-
if (event.target instanceof HTMLInputElement ||
|
|
18383
|
+
if (event.target instanceof HTMLInputElement ||
|
|
18384
|
+
event.target instanceof HTMLTextAreaElement ||
|
|
18385
|
+
event.target instanceof HTMLSelectElement) {
|
|
18386
|
+
return;
|
|
18387
|
+
}
|
|
18388
|
+
// The paywall and the age gate block the story: no shortcut may move
|
|
18389
|
+
// past them. Their own Escape handling applies (paywall: "Maybe later"
|
|
18390
|
+
// dismisses; age gate: dismiss keeps the reader where they were).
|
|
18391
|
+
if (this.paywallVisible || this.ageGateVisible) {
|
|
18059
18392
|
return;
|
|
18060
18393
|
}
|
|
18061
18394
|
// While a dialog is open the story behind it must not react: Escape
|
|
@@ -18192,6 +18525,9 @@ class PlayerShellComponent {
|
|
|
18192
18525
|
* Handle swipe gesture
|
|
18193
18526
|
*/
|
|
18194
18527
|
onSwipe(direction) {
|
|
18528
|
+
if (this.paywallVisible || this.ageGateVisible) {
|
|
18529
|
+
return;
|
|
18530
|
+
}
|
|
18195
18531
|
if (direction === 'left') {
|
|
18196
18532
|
// Swipe left = navigate forward
|
|
18197
18533
|
if (this.viewMode === 'page') {
|
|
@@ -18589,9 +18925,11 @@ class PlayerShellComponent {
|
|
|
18589
18925
|
*/
|
|
18590
18926
|
onPaywallAction(action) {
|
|
18591
18927
|
const gate = this.paywallGate;
|
|
18928
|
+
const productId = action === 'purchase' || action === 'subscribe' ? this.paywallProductId : undefined;
|
|
18592
18929
|
if (gate) {
|
|
18593
|
-
this.paywallAction.emit({ action, gate });
|
|
18930
|
+
this.paywallAction.emit({ action, gate, ...(productId ? { productId } : {}) });
|
|
18594
18931
|
}
|
|
18932
|
+
this.paywallProductId = undefined;
|
|
18595
18933
|
this.closePaywall();
|
|
18596
18934
|
}
|
|
18597
18935
|
/**
|
|
@@ -18608,6 +18946,10 @@ class PlayerShellComponent {
|
|
|
18608
18946
|
await this.httpEntitlement.resolveEntitlement({ workId: manifest?.meta?.id ?? '' });
|
|
18609
18947
|
this.paywallService.setSnapshot(this.httpEntitlement.getSnapshot());
|
|
18610
18948
|
}
|
|
18949
|
+
// What the host passed replaces the snapshot wholesale; an age check the
|
|
18950
|
+
// reader already passed on this device still counts.
|
|
18951
|
+
this.restoreAgeVerification();
|
|
18952
|
+
this.buildExtrasList(this.manifestService.getManifest() ?? null);
|
|
18611
18953
|
const gatedPanelId = this.paywallGate?.refId ?? this.getCurrentPanelId();
|
|
18612
18954
|
if (gatedPanelId && this.paywallService.canAccess(gatedPanelId)) {
|
|
18613
18955
|
this.closePaywall();
|
|
@@ -18616,9 +18958,9 @@ class PlayerShellComponent {
|
|
|
18616
18958
|
// ── Age gate ──────────────────────────────────────────────────────────────
|
|
18617
18959
|
static { this.AGE_VERIFIED_STORAGE_KEY = 'pw-age-verified'; }
|
|
18618
18960
|
/** Raise the age gate for a panel and remember where the reader was going. */
|
|
18619
|
-
openAgeGate(chapterId, panelId, transition, cameraMove) {
|
|
18961
|
+
openAgeGate(chapterId, panelId, transition, cameraMove, mutations) {
|
|
18620
18962
|
this.ageGateMinimumAge = this.paywallService.ruleFor(panelId)?.minimumAge ?? 18;
|
|
18621
|
-
this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove };
|
|
18963
|
+
this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove, mutations };
|
|
18622
18964
|
this.ageGateVisible = true;
|
|
18623
18965
|
this.cdr.markForCheck();
|
|
18624
18966
|
}
|
|
@@ -18648,8 +18990,11 @@ class PlayerShellComponent {
|
|
|
18648
18990
|
this.ageGateVisible = false;
|
|
18649
18991
|
this.pendingAgeGatedNavigation = null;
|
|
18650
18992
|
this.cdr.markForCheck();
|
|
18651
|
-
|
|
18652
|
-
|
|
18993
|
+
// Retry the move: navigateToPanel re-checks the gate, so a rule that
|
|
18994
|
+
// also needs a purchase / subscription now raises the paywall (with its
|
|
18995
|
+
// Buy / Subscribe options) instead of letting the reader through.
|
|
18996
|
+
if (pending) {
|
|
18997
|
+
await this.navigateToPanel(pending.chapterId, pending.panelId, pending.transition, pending.cameraMove, pending.mutations);
|
|
18653
18998
|
}
|
|
18654
18999
|
}
|
|
18655
19000
|
persistAgeVerification(age) {
|
|
@@ -18725,41 +19070,43 @@ class PlayerShellComponent {
|
|
|
18725
19070
|
bookmark.panelId === this.getCurrentPanelId();
|
|
18726
19071
|
}
|
|
18727
19072
|
// ── Branch choices ────────────────────────────────────────────────────────
|
|
18728
|
-
/**
|
|
19073
|
+
/**
|
|
19074
|
+
* At least two paths out of the current panel are open right now (edge
|
|
19075
|
+
* conditions evaluated exactly like the chooser does) — the toolbar shows
|
|
19076
|
+
* "Choices". A closed conditional edge does not count.
|
|
19077
|
+
*/
|
|
18729
19078
|
get hasBranchesAhead() {
|
|
18730
|
-
|
|
18731
|
-
const edges = this.currentChapter?.graph?.edges;
|
|
18732
|
-
if (!panelId || !edges) {
|
|
18733
|
-
return false;
|
|
18734
|
-
}
|
|
18735
|
-
let count = 0;
|
|
18736
|
-
for (const edge of edges) {
|
|
18737
|
-
if (edge.from === panelId && ++count > 1) {
|
|
18738
|
-
return true;
|
|
18739
|
-
}
|
|
18740
|
-
}
|
|
18741
|
-
return false;
|
|
19079
|
+
return this.openEdges().length > 1;
|
|
18742
19080
|
}
|
|
18743
|
-
/** Outgoing edges whose conditions pass right now
|
|
18744
|
-
|
|
19081
|
+
/** Outgoing edges of the current panel whose conditions pass right now. */
|
|
19082
|
+
openEdges() {
|
|
18745
19083
|
const chapter = this.currentChapter;
|
|
18746
19084
|
const panelId = this.getCurrentPanelId();
|
|
18747
|
-
if (!chapter
|
|
19085
|
+
if (!chapter || !panelId) {
|
|
18748
19086
|
return [];
|
|
18749
19087
|
}
|
|
19088
|
+
const outgoing = chapter.graph?.edges?.filter((edge) => edge.from === panelId) ?? [];
|
|
19089
|
+
if (!outgoing.some((edge) => edge.condition)) {
|
|
19090
|
+
// Nothing to evaluate: skip building a variable context on every check.
|
|
19091
|
+
return outgoing;
|
|
19092
|
+
}
|
|
18750
19093
|
const context = this.variableStore.createContext(chapter.id);
|
|
18751
|
-
return
|
|
18752
|
-
.filter((edge) => edge.from === panelId)
|
|
18753
|
-
.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context))
|
|
18754
|
-
.map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter, index) }));
|
|
19094
|
+
return outgoing.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context));
|
|
18755
19095
|
}
|
|
18756
|
-
|
|
19096
|
+
/** Open outgoing edges with reader-facing labels. */
|
|
19097
|
+
computeBranchChoices() {
|
|
19098
|
+
const chapter = this.currentChapter;
|
|
19099
|
+
return chapter
|
|
19100
|
+
? this.openEdges().map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter) }))
|
|
19101
|
+
: [];
|
|
19102
|
+
}
|
|
19103
|
+
branchLabel(edge, chapter) {
|
|
18757
19104
|
const fromEdge = this.localizedText(edge.label);
|
|
18758
19105
|
if (fromEdge) {
|
|
18759
19106
|
return fromEdge;
|
|
18760
19107
|
}
|
|
18761
|
-
|
|
18762
|
-
return
|
|
19108
|
+
// Neither: the chooser renders a translated "Option N".
|
|
19109
|
+
return this.localizedText(chapter.panels?.[edge.to]?.title);
|
|
18763
19110
|
}
|
|
18764
19111
|
localizedText(value) {
|
|
18765
19112
|
if (!value) {
|
|
@@ -18784,18 +19131,15 @@ class PlayerShellComponent {
|
|
|
18784
19131
|
to: choice.edge.to,
|
|
18785
19132
|
index: choice.index,
|
|
18786
19133
|
});
|
|
18787
|
-
|
|
18788
|
-
this.variableStore.applyMutations(choice.edge.action, { chapterId: chapter.id });
|
|
18789
|
-
}
|
|
18790
|
-
await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings));
|
|
19134
|
+
await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings), choice.edge.action);
|
|
18791
19135
|
}
|
|
18792
19136
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
18793
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, providers: [CanvasCameraService], ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "panels", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelFocus", "hotspotActivate", "deadClick"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
19137
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, providers: [CanvasCameraService], usesOnChanges: true, ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "panels", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelFocus", "hotspotActivate", "deadClick"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
18794
19138
|
}
|
|
18795
19139
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, decorators: [{
|
|
18796
19140
|
type: Component,
|
|
18797
19141
|
args: [{ selector: 'pw-player-shell', providers: [CanvasCameraService], imports: [
|
|
18798
|
-
|
|
19142
|
+
TranslatePipe,
|
|
18799
19143
|
ViewportComponent,
|
|
18800
19144
|
CanvasStageComponent,
|
|
18801
19145
|
ToolbarComponent,
|
|
@@ -18812,7 +19156,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
18812
19156
|
ShareModalComponent,
|
|
18813
19157
|
CommentsDrawerComponent,
|
|
18814
19158
|
PwIconComponent
|
|
18815
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
|
|
19159
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
|
|
18816
19160
|
}], propDecorators: { manifest: [{
|
|
18817
19161
|
type: Input
|
|
18818
19162
|
}], manifestUrl: [{
|
|
@@ -18978,5 +19322,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
18978
19322
|
* Generated bundle index. Do not edit.
|
|
18979
19323
|
*/
|
|
18980
19324
|
|
|
18981
|
-
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
|
|
19325
|
+
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, chapterOrderFromManifest, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, isExtraLocked, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
|
|
18982
19326
|
//# sourceMappingURL=panelwave-player.mjs.map
|