@panelwave/player 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +243 -24
- package/fesm2022/panelwave-player.mjs +501 -175
- package/fesm2022/panelwave-player.mjs.map +1 -1
- package/index.d.ts +181 -22
- package/package.json +1 -1
- package/src/assets/fonts/balloon/LICENSES.md +33 -33
- package/src/assets/fonts/balloon/balloon-fonts.css +227 -227
- package/src/assets/i18n/de.json +26 -1
- package/src/assets/i18n/en.json +26 -1
|
@@ -7053,12 +7053,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
7053
7053
|
* free preview.
|
|
7054
7054
|
* - Panel-scoped rules gate exactly their `targetPanelIds` — preview does NOT
|
|
7055
7055
|
* override an explicit panel gate.
|
|
7056
|
+
* - Chapter-scoped rules gate only the panels of their chapter(s) (`refId`);
|
|
7057
|
+
* their free preview counts panels WITHIN that chapter. They take
|
|
7058
|
+
* precedence over a work gate for those panels.
|
|
7059
|
+
* - Extras-scoped rules never gate panels — they gate the extras block named
|
|
7060
|
+
* by `refId` (see `isExtraLocked`).
|
|
7056
7061
|
* - A gated panel unlocks when the gating rule's requirement is satisfied by
|
|
7057
7062
|
* the reader's entitlement snapshot.
|
|
7058
7063
|
*/
|
|
7059
7064
|
const DEFAULT_MINIMUM_AGE = 18;
|
|
7060
|
-
/** Does the
|
|
7065
|
+
/** Does the rule carry an age requirement the reader has not met? */
|
|
7066
|
+
function ageMissing(rule, entitlement) {
|
|
7067
|
+
if (rule.entitlementType !== 'age_gate' && rule.minimumAge === undefined)
|
|
7068
|
+
return false;
|
|
7069
|
+
const min = rule.minimumAge ?? DEFAULT_MINIMUM_AGE;
|
|
7070
|
+
return !(entitlement.ageVerified && (entitlement.age ?? 0) >= min);
|
|
7071
|
+
}
|
|
7072
|
+
/**
|
|
7073
|
+
* Does the reader's snapshot satisfy this rule's requirement? A rule with a
|
|
7074
|
+
* `minimumAge` needs the age AND its entitlement.
|
|
7075
|
+
*/
|
|
7061
7076
|
function satisfiesRule(rule, entitlement) {
|
|
7077
|
+
return !ageMissing(rule, entitlement) && hasEntitlement(rule, entitlement);
|
|
7078
|
+
}
|
|
7079
|
+
/** The entitlement part of a rule (the age part is `ageMissing`). */
|
|
7080
|
+
function hasEntitlement(rule, entitlement) {
|
|
7062
7081
|
switch (rule.entitlementType) {
|
|
7063
7082
|
case 'free':
|
|
7064
7083
|
return true;
|
|
@@ -7074,13 +7093,17 @@ function satisfiesRule(rule, entitlement) {
|
|
|
7074
7093
|
return entitlement.purchasedProductIds.length > 0;
|
|
7075
7094
|
return required.some((id) => entitlement.purchasedProductIds.includes(id));
|
|
7076
7095
|
}
|
|
7077
|
-
case 'age_gate':
|
|
7078
|
-
|
|
7079
|
-
return entitlement.ageVerified && (entitlement.age ?? 0) >= min;
|
|
7080
|
-
}
|
|
7096
|
+
case 'age_gate':
|
|
7097
|
+
return true; // the age is the whole requirement (checked by ageMissing)
|
|
7081
7098
|
}
|
|
7082
7099
|
}
|
|
7083
|
-
|
|
7100
|
+
/**
|
|
7101
|
+
* Why a rule locks the reader out. The age comes first: a reader who fails
|
|
7102
|
+
* the age check is asked for it before being offered anything to buy.
|
|
7103
|
+
*/
|
|
7104
|
+
function lockReasonFor(rule, entitlement) {
|
|
7105
|
+
if (ageMissing(rule, entitlement))
|
|
7106
|
+
return 'age_verification_required';
|
|
7084
7107
|
switch (rule.entitlementType) {
|
|
7085
7108
|
case 'subscription':
|
|
7086
7109
|
return 'subscription_required';
|
|
@@ -7108,36 +7131,48 @@ function evaluatePanelAccess(rules, entitlement, panel) {
|
|
|
7108
7131
|
return {
|
|
7109
7132
|
panelId: panel.id,
|
|
7110
7133
|
locked: !ok,
|
|
7111
|
-
reason: ok ? null : lockReasonFor(panelRule),
|
|
7134
|
+
reason: ok ? null : lockReasonFor(panelRule, entitlement),
|
|
7112
7135
|
appliedRuleId: panelRule.id,
|
|
7113
7136
|
preview: false,
|
|
7114
7137
|
};
|
|
7115
7138
|
}
|
|
7116
|
-
// 2.
|
|
7139
|
+
// 2. Chapter gates: only the panels of the named chapter, with the free
|
|
7140
|
+
// preview counted within that chapter.
|
|
7141
|
+
const chapterRule = panel.chapterId === undefined
|
|
7142
|
+
? undefined
|
|
7143
|
+
: active.find((r) => r.scope === 'chapter' && (r.targetIds ?? []).includes(panel.chapterId));
|
|
7144
|
+
if (chapterRule) {
|
|
7145
|
+
return gateWithPreview(chapterRule, entitlement, panel.id, panel.chapterIndex ?? Number.MAX_SAFE_INTEGER);
|
|
7146
|
+
}
|
|
7147
|
+
// 3. Work/global gate with free preview.
|
|
7117
7148
|
const workRule = findWorkGateRule(active);
|
|
7118
7149
|
if (workRule) {
|
|
7119
|
-
|
|
7120
|
-
if (panel.index < previewCount) {
|
|
7121
|
-
return {
|
|
7122
|
-
panelId: panel.id,
|
|
7123
|
-
locked: false,
|
|
7124
|
-
reason: null,
|
|
7125
|
-
appliedRuleId: workRule.id,
|
|
7126
|
-
preview: true,
|
|
7127
|
-
};
|
|
7128
|
-
}
|
|
7129
|
-
const ok = satisfiesRule(workRule, entitlement);
|
|
7130
|
-
return {
|
|
7131
|
-
panelId: panel.id,
|
|
7132
|
-
locked: !ok,
|
|
7133
|
-
reason: ok ? null : lockReasonFor(workRule),
|
|
7134
|
-
appliedRuleId: workRule.id,
|
|
7135
|
-
preview: false,
|
|
7136
|
-
};
|
|
7150
|
+
return gateWithPreview(workRule, entitlement, panel.id, panel.index);
|
|
7137
7151
|
}
|
|
7138
|
-
//
|
|
7152
|
+
// 4. No gate at all — free content.
|
|
7139
7153
|
return { panelId: panel.id, locked: false, reason: null, appliedRuleId: null, preview: false };
|
|
7140
7154
|
}
|
|
7155
|
+
/** A work/chapter gate: panels before `previewPanelCount` (by `index`) are free. */
|
|
7156
|
+
function gateWithPreview(rule, entitlement, panelId, index) {
|
|
7157
|
+
if (index < (rule.previewPanelCount ?? 0)) {
|
|
7158
|
+
return { panelId, locked: false, reason: null, appliedRuleId: rule.id, preview: true };
|
|
7159
|
+
}
|
|
7160
|
+
const ok = satisfiesRule(rule, entitlement);
|
|
7161
|
+
return {
|
|
7162
|
+
panelId,
|
|
7163
|
+
locked: !ok,
|
|
7164
|
+
reason: ok ? null : lockReasonFor(rule, entitlement),
|
|
7165
|
+
appliedRuleId: rule.id,
|
|
7166
|
+
preview: false,
|
|
7167
|
+
};
|
|
7168
|
+
}
|
|
7169
|
+
/** Is the extras block `extraId` locked by an extras-scoped rule? */
|
|
7170
|
+
function isExtraLocked(rules, entitlement, extraId) {
|
|
7171
|
+
return rules.some((r) => r.isActive &&
|
|
7172
|
+
r.scope === 'extras' &&
|
|
7173
|
+
(r.targetIds ?? []).includes(extraId) &&
|
|
7174
|
+
!satisfiesRule(r, entitlement));
|
|
7175
|
+
}
|
|
7141
7176
|
/** Evaluate a whole work (panels in reading order). */
|
|
7142
7177
|
function evaluateWorkAccess(rules, entitlement, panels) {
|
|
7143
7178
|
return panels.map((panel) => evaluatePanelAccess(rules, entitlement, panel));
|
|
@@ -7164,8 +7199,10 @@ const KNOWN_ENTITLEMENT_TYPES = [
|
|
|
7164
7199
|
* `minimumAge` written by the CMS exporter. Prefer the CMS names and fall
|
|
7165
7200
|
* back to the originals, so a manifest from either era gates identically.
|
|
7166
7201
|
*
|
|
7167
|
-
*
|
|
7168
|
-
*
|
|
7202
|
+
* `ageGate` / `minimumAge` is an age check ON TOP OF the entitlement (schema
|
|
7203
|
+
* semantics). Only a rule that declares no entitlement at all but names an
|
|
7204
|
+
* age becomes a pure age gate; a custom marker plus an age stays a purchase
|
|
7205
|
+
* (or subscription) gate that also requires the age.
|
|
7169
7206
|
*/
|
|
7170
7207
|
function fromManifestRule(rule, index) {
|
|
7171
7208
|
const raw = rule;
|
|
@@ -7175,9 +7212,6 @@ function fromManifestRule(rule, index) {
|
|
|
7175
7212
|
if (declared && KNOWN_ENTITLEMENT_TYPES.includes(declared)) {
|
|
7176
7213
|
entitlementType = declared;
|
|
7177
7214
|
}
|
|
7178
|
-
else if (minimumAge !== undefined) {
|
|
7179
|
-
entitlementType = 'age_gate';
|
|
7180
|
-
}
|
|
7181
7215
|
else if (declared === 'premium') {
|
|
7182
7216
|
// Original format's subscription marker.
|
|
7183
7217
|
entitlementType = 'subscription';
|
|
@@ -7186,10 +7220,17 @@ function fromManifestRule(rule, index) {
|
|
|
7186
7220
|
// 'token', 'purchaseId', or any custom marker: treat as a purchase gate.
|
|
7187
7221
|
entitlementType = 'purchase';
|
|
7188
7222
|
}
|
|
7223
|
+
else if (minimumAge !== undefined) {
|
|
7224
|
+
// No entitlement declared, only an age: a pure age gate.
|
|
7225
|
+
entitlementType = 'age_gate';
|
|
7226
|
+
}
|
|
7189
7227
|
else {
|
|
7190
7228
|
entitlementType = 'free';
|
|
7191
7229
|
}
|
|
7192
|
-
|
|
7230
|
+
// Extras-scoped rules keep their scope: they gate an extras block, never
|
|
7231
|
+
// panels (mapping them onto 'work' used to lock the whole work).
|
|
7232
|
+
const scope = raw['scope'];
|
|
7233
|
+
const refId = raw['refId'];
|
|
7193
7234
|
return {
|
|
7194
7235
|
id: raw['id'] ?? `rule-${index}`,
|
|
7195
7236
|
scope: scope ?? 'work',
|
|
@@ -7203,9 +7244,12 @@ function fromManifestRule(rule, index) {
|
|
|
7203
7244
|
targetPanelIds: raw['targetPanelIds'] ??
|
|
7204
7245
|
// A panel-scoped rule from the original format names its single target
|
|
7205
7246
|
// in refId rather than a list.
|
|
7206
|
-
(
|
|
7207
|
-
|
|
7208
|
-
|
|
7247
|
+
(scope === 'panel' && refId ? [refId] : undefined),
|
|
7248
|
+
targetIds: (scope === 'chapter' || scope === 'extras') && refId ? [refId] : undefined,
|
|
7249
|
+
name: raw['name'],
|
|
7250
|
+
description: raw['description'],
|
|
7251
|
+
price: raw['price'],
|
|
7252
|
+
entitlementKey: raw['requireEntitlement'],
|
|
7209
7253
|
};
|
|
7210
7254
|
}
|
|
7211
7255
|
/** Normalise every rule in a manifest. Returns [] when the work has no paywall. */
|
|
@@ -7215,6 +7259,14 @@ function rulesFromManifest(manifest) {
|
|
|
7215
7259
|
return [];
|
|
7216
7260
|
return rules.map(fromManifestRule);
|
|
7217
7261
|
}
|
|
7262
|
+
/** Chapter id and within-chapter index per panel id (chapter-rule previews). */
|
|
7263
|
+
function chapterOrderFromManifest(manifest) {
|
|
7264
|
+
const out = new Map();
|
|
7265
|
+
for (const chapter of manifest?.chapters ?? []) {
|
|
7266
|
+
Object.keys(chapter.panels ?? {}).forEach((panelId, chapterIndex) => out.set(panelId, { chapterId: chapter.id, chapterIndex }));
|
|
7267
|
+
}
|
|
7268
|
+
return out;
|
|
7269
|
+
}
|
|
7218
7270
|
/**
|
|
7219
7271
|
* Global reading order for preview counting: chapters in array order, and
|
|
7220
7272
|
* within a chapter the panel declaration order.
|
|
@@ -7267,7 +7319,9 @@ class PaywallService {
|
|
|
7267
7319
|
constructor() {
|
|
7268
7320
|
this.rules = [];
|
|
7269
7321
|
this.readingOrder = new Map();
|
|
7322
|
+
this.chapterOrder = new Map();
|
|
7270
7323
|
this.manifest = null;
|
|
7324
|
+
this.products = new Map();
|
|
7271
7325
|
this.snapshotSubject = new BehaviorSubject(ANONYMOUS_READER);
|
|
7272
7326
|
this.snapshot$ = this.snapshotSubject.asObservable();
|
|
7273
7327
|
}
|
|
@@ -7276,6 +7330,17 @@ class PaywallService {
|
|
|
7276
7330
|
this.manifest = manifest;
|
|
7277
7331
|
this.rules = rulesFromManifest(manifest);
|
|
7278
7332
|
this.readingOrder = new Map(readingOrderFromManifest(manifest).map((panelId, index) => [panelId, index]));
|
|
7333
|
+
this.chapterOrder = chapterOrderFromManifest(manifest);
|
|
7334
|
+
this.products = PaywallService.productsFromManifest(manifest);
|
|
7335
|
+
}
|
|
7336
|
+
/** `paywall.products` by id (format 1.6); the first entry wins on duplicate ids. */
|
|
7337
|
+
static productsFromManifest(manifest) {
|
|
7338
|
+
const out = new Map();
|
|
7339
|
+
for (const product of manifest?.paywall?.products ?? []) {
|
|
7340
|
+
if (product?.id && !out.has(product.id))
|
|
7341
|
+
out.set(product.id, product);
|
|
7342
|
+
}
|
|
7343
|
+
return out;
|
|
7279
7344
|
}
|
|
7280
7345
|
/** Replace what the reader owns (after sign-in, or a completed purchase). */
|
|
7281
7346
|
setSnapshot(snapshot) {
|
|
@@ -7304,7 +7369,15 @@ class PaywallService {
|
|
|
7304
7369
|
return { panelId, locked: false, reason: null, appliedRuleId: null, preview: false };
|
|
7305
7370
|
}
|
|
7306
7371
|
const index = this.readingOrder.get(panelId) ?? Number.MAX_SAFE_INTEGER;
|
|
7307
|
-
return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
|
|
7372
|
+
return evaluatePanelAccess(this.rules, this.snapshotSubject.value, {
|
|
7373
|
+
id: panelId,
|
|
7374
|
+
index,
|
|
7375
|
+
...this.chapterOrder.get(panelId),
|
|
7376
|
+
});
|
|
7377
|
+
}
|
|
7378
|
+
/** Is this extras block locked by an extras-scoped rule the reader does not satisfy? */
|
|
7379
|
+
isExtraLocked(extraId) {
|
|
7380
|
+
return isExtraLocked(this.rules, this.snapshotSubject.value, extraId);
|
|
7308
7381
|
}
|
|
7309
7382
|
/** Convenience: may the reader open this panel? */
|
|
7310
7383
|
canAccess(panelId) {
|
|
@@ -7313,9 +7386,11 @@ class PaywallService {
|
|
|
7313
7386
|
/**
|
|
7314
7387
|
* Describe the gate blocking a panel, or null when it is not gated.
|
|
7315
7388
|
* The shape is the player's existing `PaywallGate`, so the overlay needs
|
|
7316
|
-
* no changes to render it.
|
|
7389
|
+
* no changes to render it. `locale` picks the language of product names
|
|
7390
|
+
* and descriptions from `paywall.products` (fallback: the work's default
|
|
7391
|
+
* locale).
|
|
7317
7392
|
*/
|
|
7318
|
-
gateFor(panelId) {
|
|
7393
|
+
gateFor(panelId, locale) {
|
|
7319
7394
|
const decision = this.evaluate(panelId);
|
|
7320
7395
|
if (!decision.locked)
|
|
7321
7396
|
return null;
|
|
@@ -7323,14 +7398,52 @@ class PaywallService {
|
|
|
7323
7398
|
const scope = rule?.scope === 'panel' ? 'panel' : rule?.scope === 'chapter' ? 'chapter' : 'work';
|
|
7324
7399
|
return {
|
|
7325
7400
|
scope,
|
|
7326
|
-
refId: rule?.scope === 'panel' ? panelId : undefined,
|
|
7401
|
+
refId: rule?.scope === 'panel' ? panelId : scope === 'chapter' ? this.chapterOrder.get(panelId)?.chapterId : undefined,
|
|
7327
7402
|
requireEntitlement: rule?.entitlementType,
|
|
7328
7403
|
reason: PaywallService.readerMessage(decision.reason),
|
|
7329
7404
|
preview: rule?.previewPanelCount
|
|
7330
7405
|
? { previewPanels: rule.previewPanelCount }
|
|
7331
7406
|
: undefined,
|
|
7407
|
+
ruleId: rule?.id,
|
|
7408
|
+
lockReason: decision.reason ?? undefined,
|
|
7409
|
+
options: rule
|
|
7410
|
+
? PaywallService.purchaseOptions(rule, this.products, locale ?? this.defaultLocale, this.defaultLocale)
|
|
7411
|
+
: [],
|
|
7332
7412
|
};
|
|
7333
7413
|
}
|
|
7414
|
+
/**
|
|
7415
|
+
* What the reader can buy to pass a rule: one option per required product
|
|
7416
|
+
* (purchase) or per subscription tier (subscription). Without explicit
|
|
7417
|
+
* ids, the rule's `requireEntitlement` key names the product, else the
|
|
7418
|
+
* rule id. Age gates and free rules offer nothing to buy.
|
|
7419
|
+
*
|
|
7420
|
+
* Labels (format 1.6): an option whose id has an entry in
|
|
7421
|
+
* `paywall.products` takes that entry's localized name, description and
|
|
7422
|
+
* price. Otherwise it falls back to the rule's name (a single option) or
|
|
7423
|
+
* the bare id (several options), and to the rule's description and price.
|
|
7424
|
+
*/
|
|
7425
|
+
static purchaseOptions(rule, products = new Map(), locale = 'en-US', fallbackLocale = 'en-US') {
|
|
7426
|
+
const type = rule.entitlementType === 'purchase' ? 'one-time' : rule.entitlementType;
|
|
7427
|
+
if (type !== 'one-time' && type !== 'subscription')
|
|
7428
|
+
return [];
|
|
7429
|
+
const listed = type === 'one-time' ? rule.requiredProductIds : rule.subscriptionTiers;
|
|
7430
|
+
const key = rule.entitlementKey !== rule.entitlementType ? rule.entitlementKey : undefined;
|
|
7431
|
+
const ids = listed?.length ? listed : [key ?? rule.id];
|
|
7432
|
+
return ids.map((productId) => {
|
|
7433
|
+
const product = products.get(productId);
|
|
7434
|
+
const name = product?.name ? resolveLocalizedString(product.name, locale, fallbackLocale) : '';
|
|
7435
|
+
const description = product?.description
|
|
7436
|
+
? resolveLocalizedString(product.description, locale, fallbackLocale)
|
|
7437
|
+
: '';
|
|
7438
|
+
return {
|
|
7439
|
+
productId,
|
|
7440
|
+
name: name || (ids.length > 1 ? productId : rule.name ?? ''),
|
|
7441
|
+
description: description || rule.description,
|
|
7442
|
+
price: product?.price ?? rule.price,
|
|
7443
|
+
type,
|
|
7444
|
+
};
|
|
7445
|
+
});
|
|
7446
|
+
}
|
|
7334
7447
|
/** The rule that gates a panel, for callers that need its price or name. */
|
|
7335
7448
|
ruleFor(panelId) {
|
|
7336
7449
|
const decision = this.evaluate(panelId);
|
|
@@ -7338,11 +7451,17 @@ class PaywallService {
|
|
|
7338
7451
|
return null;
|
|
7339
7452
|
return this.rules.find((r) => r.id === decision.appliedRuleId) ?? null;
|
|
7340
7453
|
}
|
|
7454
|
+
/** The work's default locale (product-label fallback). */
|
|
7455
|
+
get defaultLocale() {
|
|
7456
|
+
return this.manifest?.meta?.default_locale ?? 'en-US';
|
|
7457
|
+
}
|
|
7341
7458
|
/** Reset to a manifest-less, entitlement-less state. */
|
|
7342
7459
|
clear() {
|
|
7343
7460
|
this.manifest = null;
|
|
7344
7461
|
this.rules = [];
|
|
7462
|
+
this.products.clear();
|
|
7345
7463
|
this.readingOrder.clear();
|
|
7464
|
+
this.chapterOrder.clear();
|
|
7346
7465
|
this.snapshotSubject.next(ANONYMOUS_READER);
|
|
7347
7466
|
}
|
|
7348
7467
|
/**
|
|
@@ -9714,11 +9833,11 @@ class VideoLayerComponent {
|
|
|
9714
9833
|
this.frameStepHandle = null;
|
|
9715
9834
|
}
|
|
9716
9835
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: VideoLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
9717
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: VideoLayerComponent, isStandalone: true, selector: "pw-video-layer", inputs: { src: "src", reverseSrc: "reverseSrc", baseUrl: "baseUrl", poster: "poster", controls: "controls", playMode: "playMode", startMode: "startMode", muted: "muted", startAtMs: "startAtMs", loopFromMs: "loopFromMs", streaming: "streaming", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", ariaLabel: "ariaLabel", videoId: "videoId", panelId: "panelId", assetId: "assetId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { videoPlay: "videoPlay", videoPause: "videoPause", videoEnd: "videoEnd", videoLoop: "videoLoop", passComplete: "passComplete", videoError: "videoError" }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\
|
|
9836
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: VideoLayerComponent, isStandalone: true, selector: "pw-video-layer", inputs: { src: "src", reverseSrc: "reverseSrc", baseUrl: "baseUrl", poster: "poster", controls: "controls", playMode: "playMode", startMode: "startMode", muted: "muted", startAtMs: "startAtMs", loopFromMs: "loopFromMs", streaming: "streaming", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", ariaLabel: "ariaLabel", videoId: "videoId", panelId: "panelId", assetId: "assetId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { videoPlay: "videoPlay", videoPause: "videoPause", videoEnd: "videoEnd", videoLoop: "videoLoop", passComplete: "passComplete", videoError: "videoError" }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoElement"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"video-layer-container\"\n [class.interactive]=\"isKeyboardInteractive()\"\n [attr.tabindex]=\"isKeyboardInteractive() ? 0 : null\"\n [attr.role]=\"isKeyboardInteractive() ? 'button' : null\"\n [attr.aria-label]=\"ariaLabel || null\"\n (click)=\"onClick()\"\n (keydown)=\"onKeydown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\">\n <video\n #videoElement\n class=\"video-layer\"\n [src]=\"getVideoUrl()\"\n [poster]=\"getPosterUrl()\"\n [muted]=\"displayMuted\"\n [controls]=\"controls\"\n playsinline\n preload=\"metadata\"\n (loadedmetadata)=\"onLoadedMetadata()\"\n (error)=\"onError($event)\">\n Your browser does not support the video tag.\n </video>\n\n @if (showUnmuteButton) {\n <button\n type=\"button\"\n class=\"video-unmute-affordance\"\n aria-label=\"Unmute video\"\n (click)=\"onUnmuteClick($event)\">\n <span class=\"video-unmute-icon\" aria-hidden=\"true\">\uD83D\uDD0A</span>\n <span class=\"video-unmute-label\">Unmute</span>\n </button>\n }\n</div>\n", styles: [".video-layer-container{position:relative;width:100%;height:100%;background:#000}.video-layer{width:100%;height:100%;display:block;object-fit:contain}.video-layer::-webkit-media-controls-panel{background:linear-gradient(to bottom,transparent,rgba(0,0,0,.7))}.video-layer-container.interactive{cursor:pointer}.video-layer-container.interactive:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-affordance{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:1;color:#fff;background:#000000a6;border:1px solid rgba(255,255,255,.25);border-radius:4px;cursor:pointer;z-index:2}.video-unmute-affordance:hover{background:#000c}.video-unmute-affordance:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-icon{font-size:15px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
9718
9837
|
}
|
|
9719
9838
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: VideoLayerComponent, decorators: [{
|
|
9720
9839
|
type: Component,
|
|
9721
|
-
args: [{ selector: 'pw-video-layer', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\
|
|
9840
|
+
args: [{ selector: 'pw-video-layer', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"video-layer-container\"\n [class.interactive]=\"isKeyboardInteractive()\"\n [attr.tabindex]=\"isKeyboardInteractive() ? 0 : null\"\n [attr.role]=\"isKeyboardInteractive() ? 'button' : null\"\n [attr.aria-label]=\"ariaLabel || null\"\n (click)=\"onClick()\"\n (keydown)=\"onKeydown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\">\n <video\n #videoElement\n class=\"video-layer\"\n [src]=\"getVideoUrl()\"\n [poster]=\"getPosterUrl()\"\n [muted]=\"displayMuted\"\n [controls]=\"controls\"\n playsinline\n preload=\"metadata\"\n (loadedmetadata)=\"onLoadedMetadata()\"\n (error)=\"onError($event)\">\n Your browser does not support the video tag.\n </video>\n\n @if (showUnmuteButton) {\n <button\n type=\"button\"\n class=\"video-unmute-affordance\"\n aria-label=\"Unmute video\"\n (click)=\"onUnmuteClick($event)\">\n <span class=\"video-unmute-icon\" aria-hidden=\"true\">\uD83D\uDD0A</span>\n <span class=\"video-unmute-label\">Unmute</span>\n </button>\n }\n</div>\n", styles: [".video-layer-container{position:relative;width:100%;height:100%;background:#000}.video-layer{width:100%;height:100%;display:block;object-fit:contain}.video-layer::-webkit-media-controls-panel{background:linear-gradient(to bottom,transparent,rgba(0,0,0,.7))}.video-layer-container.interactive{cursor:pointer}.video-layer-container.interactive:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-affordance{position:absolute;bottom:12px;right:12px;display:inline-flex;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:1;color:#fff;background:#000000a6;border:1px solid rgba(255,255,255,.25);border-radius:4px;cursor:pointer;z-index:2}.video-unmute-affordance:hover{background:#000c}.video-unmute-affordance:focus-visible{outline:2px solid #4f9dff;outline-offset:2px}.video-unmute-icon{font-size:15px}\n"] }]
|
|
9722
9841
|
}], propDecorators: { src: [{
|
|
9723
9842
|
type: Input
|
|
9724
9843
|
}], reverseSrc: [{
|
|
@@ -10195,11 +10314,11 @@ class LayerRendererComponent {
|
|
|
10195
10314
|
return kind === 'hotspot' || kind === 'button';
|
|
10196
10315
|
}
|
|
10197
10316
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LayerRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10198
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LayerRendererComponent, isStandalone: true, selector: "pw-layer-renderer", inputs: { layer: "layer", locale: "locale", baseUrl: "baseUrl", targetWidth: "targetWidth", altText: "altText", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", panelId: "panelId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { layerClick: "layerClick" }, ngImport: i0, template: "<div \
|
|
10317
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LayerRendererComponent, isStandalone: true, selector: "pw-layer-renderer", inputs: { layer: "layer", locale: "locale", baseUrl: "baseUrl", targetWidth: "targetWidth", altText: "altText", viewMode: "viewMode", viewActive: "viewActive", reducedMotion: "reducedMotion", panelId: "panelId", placementId: "placementId", readingOrderIndex: "readingOrderIndex", placementZ: "placementZ", placementY: "placementY", placementX: "placementX" }, outputs: { layerClick: "layerClick" }, ngImport: i0, template: "<div \n class=\"layer-wrapper\"\n [ngStyle]=\"getLayerStyles()\"\n [style.transform]=\"getTransformStyle()\"\n [class.interactive]=\"isInteractive()\"\n [attr.data-layer-id]=\"layer.id\"\n [attr.data-layer-kind]=\"layer.kind\"\n [tabindex]=\"isInteractive() ? 0 : -1\"\n (click)=\"isInteractive() ? onLayerClick($event) : null\"\n (keydown.enter)=\"isInteractive() ? onLayerClick($any($event)) : null\">\n\n <!-- Image Layer -->\n @if (layer.kind === 'image') {\n <img \n [src]=\"getImageSrc()\"\n [alt]=\"getAltText()\"\n class=\"layer-image\"\n [attr.loading]=\"viewActive ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"viewActive ? 'high' : null\" />\n }\n\n <!-- Video Layer -->\n @if (layer.kind === 'video') {\n <pw-video-layer\n class=\"layer-video\"\n [src]=\"getVideoSrc()\"\n [reverseSrc]=\"getVideoReverseSrc()\"\n [baseUrl]=\"baseUrl\"\n [poster]=\"getVideoPoster()\"\n [controls]=\"getVideoControls()\"\n [playMode]=\"getVideoPlayMode()\"\n [startMode]=\"getVideoStartMode()\"\n [muted]=\"getVideoMuted()\"\n [startAtMs]=\"getVideoStartAtMs()\"\n [loopFromMs]=\"getVideoLoopFromMs()\"\n [streaming]=\"getVideoStreaming()\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"viewActive\"\n [reducedMotion]=\"reducedMotion\"\n [videoId]=\"layer.id\"\n [panelId]=\"panelId\"\n [assetId]=\"getVideoAssetId()\"\n [placementId]=\"placementId || panelId\"\n [readingOrderIndex]=\"readingOrderIndex\"\n [placementZ]=\"placementZ\"\n [placementY]=\"placementY\"\n [placementX]=\"placementX\"\n [ariaLabel]=\"getAltText()\">\n </pw-video-layer>\n }\n\n <!-- Text Layer -->\n @if (layer.kind === 'text') {\n <div \n class=\"layer-text\"\n [ngStyle]=\"getTextStyles()\"\n [innerHTML]=\"getTextContent()\">\n </div>\n }\n\n <!-- Hotspot Layer -->\n @if (layer.kind === 'hotspot') {\n <div \n class=\"layer-hotspot\"\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"'Interactive hotspot'\">\n <div class=\"hotspot-indicator\"></div>\n </div>\n }\n\n <!-- Button Layer -->\n @if (layer.kind === 'button') {\n <button \n class=\"layer-button\"\n type=\"button\">\n {{ getTextContent() }}\n </button>\n }\n\n <!-- SVG Layer -->\n @if (layer.kind === 'svg') {\n <div \n class=\"layer-svg\"\n [innerHTML]=\"getSvgContent()\">\n </div>\n }\n\n <!-- Shape Layer -->\n @if (layer.kind === 'shape') {\n <div class=\"layer-shape\" [attr.data-shape]=\"getShapeType()\">\n <!-- Shape rendering would be implemented here -->\n </div>\n }\n\n <!-- Group Layer (container for nested layers) -->\n @if (layer.kind === 'group') {\n <div class=\"layer-group\">\n <!-- Nested layers would be rendered here -->\n </div>\n }\n</div>\n", styles: [".layer-wrapper{position:absolute;pointer-events:auto}.layer-wrapper.interactive{cursor:pointer}.layer-image,.layer-video{width:100%;height:100%;object-fit:contain;display:block}.layer-text{width:100%;height:100%;overflow:hidden;word-wrap:break-word}.layer-hotspot{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#0078d71a;border:2px solid rgba(0,120,215,.5);border-radius:4px;transition:all .2s ease}.layer-hotspot:hover{background:#0078d733;border-color:#0078d7cc}.hotspot-indicator{width:12px;height:12px;background:#0078d7;border-radius:50%;animation:pulse 2s infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.5}}.layer-button{width:100%;height:100%;padding:12px 24px;background:#0078d7;color:#fff;border:none;border-radius:4px;font-size:16px;font-weight:500;cursor:pointer;transition:background .2s ease}.layer-button:hover{background:#005a9e}.layer-button:active{background:#004578}.layer-svg{width:100%;height:100%}.layer-svg svg{width:100%;height:100%}.layer-shape{width:100%;height:100%;background:#0000001a;border:1px solid rgba(0,0,0,.2)}.layer-group{width:100%;height:100%;position:relative}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: VideoLayerComponent, selector: "pw-video-layer", inputs: ["src", "reverseSrc", "baseUrl", "poster", "controls", "playMode", "startMode", "muted", "startAtMs", "loopFromMs", "streaming", "viewMode", "viewActive", "reducedMotion", "ariaLabel", "videoId", "panelId", "assetId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["videoPlay", "videoPause", "videoEnd", "videoLoop", "passComplete", "videoError"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
10199
10318
|
}
|
|
10200
10319
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LayerRendererComponent, decorators: [{
|
|
10201
10320
|
type: Component,
|
|
10202
|
-
args: [{ selector: 'pw-layer-renderer', imports: [CommonModule, VideoLayerComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \
|
|
10321
|
+
args: [{ selector: 'pw-layer-renderer', imports: [CommonModule, VideoLayerComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \n class=\"layer-wrapper\"\n [ngStyle]=\"getLayerStyles()\"\n [style.transform]=\"getTransformStyle()\"\n [class.interactive]=\"isInteractive()\"\n [attr.data-layer-id]=\"layer.id\"\n [attr.data-layer-kind]=\"layer.kind\"\n [tabindex]=\"isInteractive() ? 0 : -1\"\n (click)=\"isInteractive() ? onLayerClick($event) : null\"\n (keydown.enter)=\"isInteractive() ? onLayerClick($any($event)) : null\">\n\n <!-- Image Layer -->\n @if (layer.kind === 'image') {\n <img \n [src]=\"getImageSrc()\"\n [alt]=\"getAltText()\"\n class=\"layer-image\"\n [attr.loading]=\"viewActive ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"viewActive ? 'high' : null\" />\n }\n\n <!-- Video Layer -->\n @if (layer.kind === 'video') {\n <pw-video-layer\n class=\"layer-video\"\n [src]=\"getVideoSrc()\"\n [reverseSrc]=\"getVideoReverseSrc()\"\n [baseUrl]=\"baseUrl\"\n [poster]=\"getVideoPoster()\"\n [controls]=\"getVideoControls()\"\n [playMode]=\"getVideoPlayMode()\"\n [startMode]=\"getVideoStartMode()\"\n [muted]=\"getVideoMuted()\"\n [startAtMs]=\"getVideoStartAtMs()\"\n [loopFromMs]=\"getVideoLoopFromMs()\"\n [streaming]=\"getVideoStreaming()\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"viewActive\"\n [reducedMotion]=\"reducedMotion\"\n [videoId]=\"layer.id\"\n [panelId]=\"panelId\"\n [assetId]=\"getVideoAssetId()\"\n [placementId]=\"placementId || panelId\"\n [readingOrderIndex]=\"readingOrderIndex\"\n [placementZ]=\"placementZ\"\n [placementY]=\"placementY\"\n [placementX]=\"placementX\"\n [ariaLabel]=\"getAltText()\">\n </pw-video-layer>\n }\n\n <!-- Text Layer -->\n @if (layer.kind === 'text') {\n <div \n class=\"layer-text\"\n [ngStyle]=\"getTextStyles()\"\n [innerHTML]=\"getTextContent()\">\n </div>\n }\n\n <!-- Hotspot Layer -->\n @if (layer.kind === 'hotspot') {\n <div \n class=\"layer-hotspot\"\n role=\"button\"\n tabindex=\"0\"\n [attr.aria-label]=\"'Interactive hotspot'\">\n <div class=\"hotspot-indicator\"></div>\n </div>\n }\n\n <!-- Button Layer -->\n @if (layer.kind === 'button') {\n <button \n class=\"layer-button\"\n type=\"button\">\n {{ getTextContent() }}\n </button>\n }\n\n <!-- SVG Layer -->\n @if (layer.kind === 'svg') {\n <div \n class=\"layer-svg\"\n [innerHTML]=\"getSvgContent()\">\n </div>\n }\n\n <!-- Shape Layer -->\n @if (layer.kind === 'shape') {\n <div class=\"layer-shape\" [attr.data-shape]=\"getShapeType()\">\n <!-- Shape rendering would be implemented here -->\n </div>\n }\n\n <!-- Group Layer (container for nested layers) -->\n @if (layer.kind === 'group') {\n <div class=\"layer-group\">\n <!-- Nested layers would be rendered here -->\n </div>\n }\n</div>\n", styles: [".layer-wrapper{position:absolute;pointer-events:auto}.layer-wrapper.interactive{cursor:pointer}.layer-image,.layer-video{width:100%;height:100%;object-fit:contain;display:block}.layer-text{width:100%;height:100%;overflow:hidden;word-wrap:break-word}.layer-hotspot{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#0078d71a;border:2px solid rgba(0,120,215,.5);border-radius:4px;transition:all .2s ease}.layer-hotspot:hover{background:#0078d733;border-color:#0078d7cc}.hotspot-indicator{width:12px;height:12px;background:#0078d7;border-radius:50%;animation:pulse 2s infinite}@keyframes pulse{0%,to{transform:scale(1);opacity:1}50%{transform:scale(1.5);opacity:.5}}.layer-button{width:100%;height:100%;padding:12px 24px;background:#0078d7;color:#fff;border:none;border-radius:4px;font-size:16px;font-weight:500;cursor:pointer;transition:background .2s ease}.layer-button:hover{background:#005a9e}.layer-button:active{background:#004578}.layer-svg{width:100%;height:100%}.layer-svg svg{width:100%;height:100%}.layer-shape{width:100%;height:100%;background:#0000001a;border:1px solid rgba(0,0,0,.2)}.layer-group{width:100%;height:100%;position:relative}\n"] }]
|
|
10203
10322
|
}], propDecorators: { layer: [{
|
|
10204
10323
|
type: Input
|
|
10205
10324
|
}], locale: [{
|
|
@@ -10681,11 +10800,11 @@ class HotspotsOverlayComponent {
|
|
|
10681
10800
|
return Math.min(1, Math.max(0, v));
|
|
10682
10801
|
}
|
|
10683
10802
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: HotspotsOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10684
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: HotspotsOverlayComponent, isStandalone: true, selector: "pw-hotspots-overlay", inputs: { hotspots: "hotspots", locale: "locale", containerWidth: "containerWidth", containerHeight: "containerHeight", context: "context", interactive: "interactive", showIndicators: "showIndicators" }, outputs: { hotspotActivate: "hotspotActivate" }, usesOnChanges: true, ngImport: i0, template: "<svg\
|
|
10803
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: HotspotsOverlayComponent, isStandalone: true, selector: "pw-hotspots-overlay", inputs: { hotspots: "hotspots", locale: "locale", containerWidth: "containerWidth", containerHeight: "containerHeight", context: "context", interactive: "interactive", showIndicators: "showIndicators" }, outputs: { hotspotActivate: "hotspotActivate" }, usesOnChanges: true, ngImport: i0, template: "<svg\n class=\"hotspots-svg\"\n [class.non-interactive]=\"!interactive\"\n [attr.viewBox]=\"'0 0 ' + containerWidth + ' ' + containerHeight\"\n preserveAspectRatio=\"none\"\n aria-hidden=\"false\"\n>\n @for (h of visibleHotspots; track h.id) {\n <g\n class=\"hotspot-shape\"\n [class.indicator]=\"showIndicators\"\n [attr.role]=\"interactive ? 'button' : null\"\n [attr.tabindex]=\"interactive ? 0 : null\"\n [attr.aria-label]=\"resolveLabel(h)\"\n (click)=\"onShapeClick(h, $event)\"\n (keydown)=\"onKeyDown(h, $event)\"\n >\n @switch (h.shape.type) {\n @case ('rect') {\n <rect\n [attr.x]=\"rectAttrs($any(h.shape)).x\"\n [attr.y]=\"rectAttrs($any(h.shape)).y\"\n [attr.width]=\"rectAttrs($any(h.shape)).width\"\n [attr.height]=\"rectAttrs($any(h.shape)).height\"\n rx=\"6\"\n />\n }\n @case ('circle') {\n <circle\n [attr.cx]=\"circleAttrs($any(h.shape)).cx\"\n [attr.cy]=\"circleAttrs($any(h.shape)).cy\"\n [attr.r]=\"circleAttrs($any(h.shape)).r\"\n />\n }\n @case ('polygon') {\n <polygon [attr.points]=\"polygonPoints($any(h.shape))\" />\n }\n }\n </g>\n }\n</svg>\n", styles: [":host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:15}.hotspots-svg{width:100%;height:100%;display:block}.hotspot-shape rect,.hotspot-shape circle,.hotspot-shape polygon{fill:#fff0;stroke:transparent;stroke-width:2;pointer-events:auto;cursor:pointer}.hotspots-svg.non-interactive .hotspot-shape rect,.hotspots-svg.non-interactive .hotspot-shape circle,.hotspots-svg.non-interactive .hotspot-shape polygon{pointer-events:none;cursor:default}.hotspot-shape:hover rect,.hotspot-shape:focus-visible rect,.hotspot-shape:hover circle,.hotspot-shape:focus-visible circle,.hotspot-shape:hover polygon,.hotspot-shape:focus-visible polygon{stroke:#ffffffd9;fill:#ffffff14}.hotspot-shape:focus{outline:none}.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:hotspot-pulse 2.4s ease-in-out infinite}@keyframes hotspot-pulse{0%,to{stroke:#fff0}50%{stroke:#ffffff73}}@media (prefers-reduced-motion: reduce){.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
10685
10804
|
}
|
|
10686
10805
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: HotspotsOverlayComponent, decorators: [{
|
|
10687
10806
|
type: Component,
|
|
10688
|
-
args: [{ selector: 'pw-hotspots-overlay', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\
|
|
10807
|
+
args: [{ selector: 'pw-hotspots-overlay', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<svg\n class=\"hotspots-svg\"\n [class.non-interactive]=\"!interactive\"\n [attr.viewBox]=\"'0 0 ' + containerWidth + ' ' + containerHeight\"\n preserveAspectRatio=\"none\"\n aria-hidden=\"false\"\n>\n @for (h of visibleHotspots; track h.id) {\n <g\n class=\"hotspot-shape\"\n [class.indicator]=\"showIndicators\"\n [attr.role]=\"interactive ? 'button' : null\"\n [attr.tabindex]=\"interactive ? 0 : null\"\n [attr.aria-label]=\"resolveLabel(h)\"\n (click)=\"onShapeClick(h, $event)\"\n (keydown)=\"onKeyDown(h, $event)\"\n >\n @switch (h.shape.type) {\n @case ('rect') {\n <rect\n [attr.x]=\"rectAttrs($any(h.shape)).x\"\n [attr.y]=\"rectAttrs($any(h.shape)).y\"\n [attr.width]=\"rectAttrs($any(h.shape)).width\"\n [attr.height]=\"rectAttrs($any(h.shape)).height\"\n rx=\"6\"\n />\n }\n @case ('circle') {\n <circle\n [attr.cx]=\"circleAttrs($any(h.shape)).cx\"\n [attr.cy]=\"circleAttrs($any(h.shape)).cy\"\n [attr.r]=\"circleAttrs($any(h.shape)).r\"\n />\n }\n @case ('polygon') {\n <polygon [attr.points]=\"polygonPoints($any(h.shape))\" />\n }\n }\n </g>\n }\n</svg>\n", styles: [":host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:15}.hotspots-svg{width:100%;height:100%;display:block}.hotspot-shape rect,.hotspot-shape circle,.hotspot-shape polygon{fill:#fff0;stroke:transparent;stroke-width:2;pointer-events:auto;cursor:pointer}.hotspots-svg.non-interactive .hotspot-shape rect,.hotspots-svg.non-interactive .hotspot-shape circle,.hotspots-svg.non-interactive .hotspot-shape polygon{pointer-events:none;cursor:default}.hotspot-shape:hover rect,.hotspot-shape:focus-visible rect,.hotspot-shape:hover circle,.hotspot-shape:focus-visible circle,.hotspot-shape:hover polygon,.hotspot-shape:focus-visible polygon{stroke:#ffffffd9;fill:#ffffff14}.hotspot-shape:focus{outline:none}.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:hotspot-pulse 2.4s ease-in-out infinite}@keyframes hotspot-pulse{0%,to{stroke:#fff0}50%{stroke:#ffffff73}}@media (prefers-reduced-motion: reduce){.hotspot-shape.indicator rect,.hotspot-shape.indicator circle,.hotspot-shape.indicator polygon{animation:none}}\n"] }]
|
|
10689
10808
|
}], propDecorators: { hotspots: [{
|
|
10690
10809
|
type: Input
|
|
10691
10810
|
}], locale: [{
|
|
@@ -12228,11 +12347,11 @@ class ViewportComponent {
|
|
|
12228
12347
|
console.groupEnd();
|
|
12229
12348
|
}
|
|
12230
12349
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ViewportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
12231
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ViewportComponent, isStandalone: true, selector: "pw-viewport", inputs: { panel: "panel", page: "page", panels: "panels", viewMode: "viewMode", currentPanelId: "currentPanelId", locale: "locale", variableContext: "variableContext", panX: "panX", panY: "panY", zoom: "zoom", reducedMotion: "reducedMotion", transition: "transition", interactive: "interactive", showOverflowArrows: "showOverflowArrows", enableViewportCulling: "enableViewportCulling", enableLazyLoading: "enableLazyLoading", workBalloonConfig: "workBalloonConfig", characters: "characters", speechEnabled: "speechEnabled", graph: "graph", preload: "preload" }, outputs: { viewportClick: "viewportClick", performanceMetrics: "performanceMetrics", transformChange: "transformChange", layerClick: "layerClick", swipe: "swipe", navigatePrevious: "navigatePrevious", navigateNext: "navigateNext", panelFocus: "panelFocus", hotspotActivate: "hotspotActivate", deadClick: "deadClick" }, host: { listeners: { "window:resize": "onWindowResize()", "mousedown": "onMouseDown($event)", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)", "wheel": "onWheel($event)", "touchstart": "onTouchStart($event)", "touchmove": "onTouchMove($event)", "touchend": "onTouchEnd($event)", "touchcancel": "onTouchEnd($event)", "keydown.tab": "onKeydownTab($event)", "keydown.arrowRight": "onKeydownNext($event)", "keydown.arrowDown": "onKeydownNext($event)", "keydown.arrowLeft": "onKeydownPrevious($event)", "keydown.arrowUp": "onKeydownPrevious($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div \r\n class=\"viewport-container\"\r\n [class]=\"getViewModeClass()\"\r\n [class.reduced-motion]=\"reducedMotion\"\r\n [class.interactive]=\"interactive\"\r\n role=\"region\"\r\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\r\n tabindex=\"0\"\r\n (click)=\"onViewportClick($event)\"\r\n (keydown.enter)=\"onKeyboardActivate($event)\"\r\n (keydown.space)=\"onKeyboardActivate($event)\"\r\n (mousemove)=\"onViewportMouseMove($event)\"\r\n (mouseleave)=\"onViewportMouseLeave()\">\r\n \r\n <!-- Panel View -->\r\n @if (viewMode === 'panel') {\r\n <div\r\n class=\"viewport-content\"\r\n [style.transform]=\"getTransformStyle()\"\r\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\r\n [style.--pw-t-duration]=\"transitionDuration\"\r\n [style.--pw-t-easing]=\"transitionEasing\">\r\n\r\n <!-- Outgoing panel during a manifest edge transition -->\r\n @if (leavingPanel) {\r\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\r\n <div class=\"panel-container\">\r\n @for (layer of leavingPanel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\r\n [panelId]=\"leavingPanelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"leavingPanel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div [class]=\"'t-frame ' + enterAnimationClass\">\r\n @if (panel) {\r\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\r\n <!-- Render layers using LayerRenderer -->\r\n @for (layer of panel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"true\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\r\n [panelId]=\"currentPanelId || ''\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay (below speech bubbles) -->\r\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panel.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for panel view -->\r\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"no-panel\">\r\n <p>No panel to display</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Page View -->\r\n @if (viewMode === 'page' && page) {\r\n <div class=\"viewport-page\">\r\n <div class=\"page-stage\">\r\n <!-- Outgoing page during a Page.transitions in/out change -->\r\n @if (leavingPage) {\r\n <div\r\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\r\n [style.--pw-t-duration]=\"pageLeaveDuration\"\r\n [style.--pw-t-easing]=\"pageLeaveEasing\"\r\n aria-hidden=\"true\">\r\n <div class=\"page-canvas\">\r\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\r\n <div\r\n class=\"panel-container\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\">\r\n\r\n @if (getLeavingPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\">\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div\r\n [class]=\"'pt-frame ' + pageEnterClass\"\r\n [style.--pw-t-duration]=\"pageEnterDuration\"\r\n [style.--pw-t-easing]=\"pageEnterEasing\">\r\n <div\r\n class=\"page-canvas\"\r\n [attr.tabindex]=\"-1\"\r\n (click)=\"onPanelClickWithFocus($event)\"\r\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\r\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\r\n \r\n @for (placement of getSortedPanels(); track placement.panelId) {\r\n @if (isPanelVisible(placement)) {\r\n <div \r\n class=\"panel-container\"\r\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\"\r\n [attr.data-panel-id]=\"placement.panelId\"\r\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\r\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\r\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\r\n [attr.loading]=\"getImageLoadingStrategy()\"\r\n (click)=\"onPanelClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\r\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\r\n \r\n @if (getPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\"\r\n tabindex=\"0\"\r\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\r\n <!-- Render panel layers -->\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [altText]=\"getAltText(layer)\"\r\n [viewMode]=\"viewMode\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\"\r\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\r\n [placementZ]=\"placement.z || 0\"\r\n [placementY]=\"placement.y\"\r\n [placementX]=\"placement.x\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay for page view panels -->\r\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panelData.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for page view panels -->\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n \r\n <!-- Focus indicator for rotated panels -->\r\n @if (isPanelFocused(placement.panelId)) {\r\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (viewMode === 'page' && !page) {\r\n <div class=\"no-panel\">\r\n <p>No page to display</p>\r\n </div>\r\n }\r\n\r\n <!-- Overflow navigation arrows -->\r\n @if (showOverflowArrows && panel) {\r\n @if (hasOverflowLeft) {\r\n <button \r\n class=\"overflow-arrow overflow-left\"\r\n type=\"button\"\r\n aria-label=\"Navigate left\"\r\n (click)=\"navigateLeft()\">\r\n <pw-icon name=\"lucideChevronLeft\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowRight) {\r\n <button \r\n class=\"overflow-arrow overflow-right\"\r\n type=\"button\"\r\n aria-label=\"Navigate right\"\r\n (click)=\"navigateRight()\">\r\n <pw-icon name=\"lucideChevronRight\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowTop) {\r\n <button \r\n class=\"overflow-arrow overflow-top\"\r\n type=\"button\"\r\n aria-label=\"Navigate up\"\r\n (click)=\"navigateUp()\">\r\n <pw-icon name=\"lucideChevronUp\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowBottom) {\r\n <button \r\n class=\"overflow-arrow overflow-bottom\"\r\n type=\"button\"\r\n aria-label=\"Navigate down\"\r\n (click)=\"navigateDown()\">\r\n <pw-icon name=\"lucideChevronDown\" />\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Navigation arrows (hover zones) -->\r\n @if (viewMode === 'panel') {\r\n @if (showLeftArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-left\"\r\n type=\"button\"\r\n aria-label=\"Previous panel\"\r\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\r\n </button>\r\n }\r\n\r\n @if (showRightArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-right\"\r\n type=\"button\"\r\n aria-label=\"Next panel\"\r\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\r\n </button>\r\n }\r\n }\r\n</div>\r\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "baseUrl", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
12350
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ViewportComponent, isStandalone: true, selector: "pw-viewport", inputs: { panel: "panel", page: "page", panels: "panels", viewMode: "viewMode", currentPanelId: "currentPanelId", locale: "locale", variableContext: "variableContext", panX: "panX", panY: "panY", zoom: "zoom", reducedMotion: "reducedMotion", transition: "transition", interactive: "interactive", showOverflowArrows: "showOverflowArrows", enableViewportCulling: "enableViewportCulling", enableLazyLoading: "enableLazyLoading", workBalloonConfig: "workBalloonConfig", characters: "characters", speechEnabled: "speechEnabled", graph: "graph", preload: "preload" }, outputs: { viewportClick: "viewportClick", performanceMetrics: "performanceMetrics", transformChange: "transformChange", layerClick: "layerClick", swipe: "swipe", navigatePrevious: "navigatePrevious", navigateNext: "navigateNext", panelFocus: "panelFocus", hotspotActivate: "hotspotActivate", deadClick: "deadClick" }, host: { listeners: { "window:resize": "onWindowResize()", "mousedown": "onMouseDown($event)", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)", "wheel": "onWheel($event)", "touchstart": "onTouchStart($event)", "touchmove": "onTouchMove($event)", "touchend": "onTouchEnd($event)", "touchcancel": "onTouchEnd($event)", "keydown.tab": "onKeydownTab($event)", "keydown.arrowRight": "onKeydownNext($event)", "keydown.arrowDown": "onKeydownNext($event)", "keydown.arrowLeft": "onKeydownPrevious($event)", "keydown.arrowUp": "onKeydownPrevious($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <div class=\"panel-container\">\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div class=\"viewport-page\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\">\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel') {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Previous panel\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Next panel\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "baseUrl", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
12232
12351
|
}
|
|
12233
12352
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ViewportComponent, decorators: [{
|
|
12234
12353
|
type: Component,
|
|
12235
|
-
args: [{ selector: 'pw-viewport', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \r\n class=\"viewport-container\"\r\n [class]=\"getViewModeClass()\"\r\n [class.reduced-motion]=\"reducedMotion\"\r\n [class.interactive]=\"interactive\"\r\n role=\"region\"\r\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\r\n tabindex=\"0\"\r\n (click)=\"onViewportClick($event)\"\r\n (keydown.enter)=\"onKeyboardActivate($event)\"\r\n (keydown.space)=\"onKeyboardActivate($event)\"\r\n (mousemove)=\"onViewportMouseMove($event)\"\r\n (mouseleave)=\"onViewportMouseLeave()\">\r\n \r\n <!-- Panel View -->\r\n @if (viewMode === 'panel') {\r\n <div\r\n class=\"viewport-content\"\r\n [style.transform]=\"getTransformStyle()\"\r\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\r\n [style.--pw-t-duration]=\"transitionDuration\"\r\n [style.--pw-t-easing]=\"transitionEasing\">\r\n\r\n <!-- Outgoing panel during a manifest edge transition -->\r\n @if (leavingPanel) {\r\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\r\n <div class=\"panel-container\">\r\n @for (layer of leavingPanel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\r\n [panelId]=\"leavingPanelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"leavingPanel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div [class]=\"'t-frame ' + enterAnimationClass\">\r\n @if (panel) {\r\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\r\n <!-- Render layers using LayerRenderer -->\r\n @for (layer of panel.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"true\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\r\n [panelId]=\"currentPanelId || ''\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay (below speech bubbles) -->\r\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panel.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for panel view -->\r\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panel.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getContainerWidth()\"\r\n [containerHeight]=\"getContainerHeight()\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"no-panel\">\r\n <p>No panel to display</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n \r\n <!-- Page View -->\r\n @if (viewMode === 'page' && page) {\r\n <div class=\"viewport-page\">\r\n <div class=\"page-stage\">\r\n <!-- Outgoing page during a Page.transitions in/out change -->\r\n @if (leavingPage) {\r\n <div\r\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\r\n [style.--pw-t-duration]=\"pageLeaveDuration\"\r\n [style.--pw-t-easing]=\"pageLeaveEasing\"\r\n aria-hidden=\"true\">\r\n <div class=\"page-canvas\">\r\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\r\n <div\r\n class=\"panel-container\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\">\r\n\r\n @if (getLeavingPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\">\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [viewMode]=\"viewMode\"\r\n [viewActive]=\"false\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <div\r\n [class]=\"'pt-frame ' + pageEnterClass\"\r\n [style.--pw-t-duration]=\"pageEnterDuration\"\r\n [style.--pw-t-easing]=\"pageEnterEasing\">\r\n <div\r\n class=\"page-canvas\"\r\n [attr.tabindex]=\"-1\"\r\n (click)=\"onPanelClickWithFocus($event)\"\r\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\r\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\r\n \r\n @for (placement of getSortedPanels(); track placement.panelId) {\r\n @if (isPanelVisible(placement)) {\r\n <div \r\n class=\"panel-container\"\r\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\r\n [style.left.%]=\"toPercent(placement.x)\"\r\n [style.top.%]=\"toPercent(placement.y)\"\r\n [style.width.%]=\"toPercent(placement.w)\"\r\n [style.height.%]=\"toPercent(placement.h)\"\r\n [style.z-index]=\"placement.z || 0\"\r\n [style.transform]=\"getPanelTransform(placement)\"\r\n [style.transform-origin]=\"getTransformOrigin(placement)\"\r\n [attr.data-panel-id]=\"placement.panelId\"\r\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\r\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\r\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\r\n [attr.loading]=\"getImageLoadingStrategy()\"\r\n (click)=\"onPanelClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\r\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\r\n \r\n @if (getPanel(placement.panelId); as panelData) {\r\n <div class=\"panel-content\"\r\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\r\n [style.transform]=\"getPanelContentTransform(placement)\"\r\n tabindex=\"0\"\r\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\r\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\r\n <!-- Render panel layers -->\r\n @for (layer of panelData.layers; track layer.id) {\r\n <pw-layer-renderer\r\n [layer]=\"layer\"\r\n [locale]=\"locale\"\r\n [altText]=\"getAltText(layer)\"\r\n [viewMode]=\"viewMode\"\r\n [reducedMotion]=\"reducedMotion\"\r\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\r\n [panelId]=\"placement.panelId\"\r\n [placementId]=\"placement.panelId\"\r\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\r\n [placementZ]=\"placement.z || 0\"\r\n [placementY]=\"placement.y\"\r\n [placementX]=\"placement.x\"\r\n (layerClick)=\"layerClick.emit($event)\">\r\n </pw-layer-renderer>\r\n }\r\n\r\n <!-- Interactive hotspots overlay for page view panels -->\r\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\r\n <pw-hotspots-overlay\r\n [hotspots]=\"panelData.hotspots\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [context]=\"variableContext\"\r\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\r\n </pw-hotspots-overlay>\r\n }\r\n\r\n <!-- Speech bubbles overlay for page view panels -->\r\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\r\n <pw-speech-bubbles\r\n [bubbles]=\"panelData.speechBubbles\"\r\n [locale]=\"locale\"\r\n [containerWidth]=\"getPagePanelWidth(placement)\"\r\n [containerHeight]=\"getPagePanelHeight(placement)\"\r\n [readingScale]=\"getReadingScale()\"\r\n [workBalloonConfig]=\"workBalloonConfig\"\r\n [characters]=\"characters\">\r\n </pw-speech-bubbles>\r\n }\r\n </div>\r\n }\r\n \r\n <!-- Focus indicator for rotated panels -->\r\n @if (isPanelFocused(placement.panelId)) {\r\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (viewMode === 'page' && !page) {\r\n <div class=\"no-panel\">\r\n <p>No page to display</p>\r\n </div>\r\n }\r\n\r\n <!-- Overflow navigation arrows -->\r\n @if (showOverflowArrows && panel) {\r\n @if (hasOverflowLeft) {\r\n <button \r\n class=\"overflow-arrow overflow-left\"\r\n type=\"button\"\r\n aria-label=\"Navigate left\"\r\n (click)=\"navigateLeft()\">\r\n <pw-icon name=\"lucideChevronLeft\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowRight) {\r\n <button \r\n class=\"overflow-arrow overflow-right\"\r\n type=\"button\"\r\n aria-label=\"Navigate right\"\r\n (click)=\"navigateRight()\">\r\n <pw-icon name=\"lucideChevronRight\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowTop) {\r\n <button \r\n class=\"overflow-arrow overflow-top\"\r\n type=\"button\"\r\n aria-label=\"Navigate up\"\r\n (click)=\"navigateUp()\">\r\n <pw-icon name=\"lucideChevronUp\" />\r\n </button>\r\n }\r\n\r\n @if (hasOverflowBottom) {\r\n <button \r\n class=\"overflow-arrow overflow-bottom\"\r\n type=\"button\"\r\n aria-label=\"Navigate down\"\r\n (click)=\"navigateDown()\">\r\n <pw-icon name=\"lucideChevronDown\" />\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Navigation arrows (hover zones) -->\r\n @if (viewMode === 'panel') {\r\n @if (showLeftArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-left\"\r\n type=\"button\"\r\n aria-label=\"Previous panel\"\r\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\r\n </button>\r\n }\r\n\r\n @if (showRightArrow) {\r\n <button \r\n class=\"nav-arrow nav-arrow-right\"\r\n type=\"button\"\r\n aria-label=\"Next panel\"\r\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\r\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\r\n </button>\r\n }\r\n }\r\n</div>\r\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"] }]
|
|
12354
|
+
args: [{ selector: 'pw-viewport', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div \n class=\"viewport-container\"\n [class]=\"getViewModeClass()\"\n [class.reduced-motion]=\"reducedMotion\"\n [class.interactive]=\"interactive\"\n role=\"region\"\n [attr.aria-label]=\"viewMode === 'page' ? 'Page viewport' : 'Panel viewport'\"\n tabindex=\"0\"\n (click)=\"onViewportClick($event)\"\n (keydown.enter)=\"onKeyboardActivate($event)\"\n (keydown.space)=\"onKeyboardActivate($event)\"\n (mousemove)=\"onViewportMouseMove($event)\"\n (mouseleave)=\"onViewportMouseLeave()\">\n \n <!-- Panel View -->\n @if (viewMode === 'panel') {\n <div\n class=\"viewport-content\"\n [style.transform]=\"getTransformStyle()\"\n [style.transition]=\"reducedMotion ? 'none' : 'transform 0.3s ease'\"\n [style.--pw-t-duration]=\"transitionDuration\"\n [style.--pw-t-easing]=\"transitionEasing\">\n\n <!-- Outgoing panel during a manifest edge transition -->\n @if (leavingPanel) {\n <div [class]=\"'t-frame-leave ' + leaveAnimationClass\" aria-hidden=\"true\">\n <div class=\"panel-container\">\n @for (layer of leavingPanel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(leavingPanelId)\"\n [panelId]=\"leavingPanelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && leavingPanel.speechBubbles && leavingPanel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"leavingPanel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n </div>\n }\n\n <div [class]=\"'t-frame ' + enterAnimationClass\">\n @if (panel) {\n <div class=\"panel-container\" tabindex=\"0\" (click)=\"onPanelContainerClick($event, currentPanelId)\" (keydown.enter)=\"onPanelContainerClick($any($event), currentPanelId)\">\n <!-- Render layers using LayerRenderer -->\n @for (layer of panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"true\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(currentPanelId)\"\n [panelId]=\"currentPanelId || ''\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay (below speech bubbles) -->\n @if (interactive && panel.hotspots && panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, currentPanelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for panel view -->\n @if (speechEnabled && panel.speechBubbles && panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getContainerWidth()\"\n [containerHeight]=\"getContainerHeight()\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n } @else {\n <div class=\"no-panel\">\n <p>No panel to display</p>\n </div>\n }\n </div>\n </div>\n }\n \n <!-- Page View -->\n @if (viewMode === 'page' && page) {\n <div class=\"viewport-page\">\n <div class=\"page-stage\">\n <!-- Outgoing page during a Page.transitions in/out change -->\n @if (leavingPage) {\n <div\n [class]=\"'pt-frame-leave ' + pageLeaveClass + (pageLeaveAbove ? ' pt-frame-leave--above' : '')\"\n [style.--pw-t-duration]=\"pageLeaveDuration\"\n [style.--pw-t-easing]=\"pageLeaveEasing\"\n aria-hidden=\"true\">\n <div class=\"page-canvas\">\n @for (placement of getLeavingPagePlacements(); track placement.panelId) {\n <div\n class=\"panel-container\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\">\n\n @if (getLeavingPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\">\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [viewMode]=\"viewMode\"\n [viewActive]=\"false\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\">\n </pw-layer-renderer>\n }\n\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <div\n [class]=\"'pt-frame ' + pageEnterClass\"\n [style.--pw-t-duration]=\"pageEnterDuration\"\n [style.--pw-t-easing]=\"pageEnterEasing\">\n <div\n class=\"page-canvas\"\n [attr.tabindex]=\"-1\"\n (click)=\"onPanelClickWithFocus($event)\"\n (keydown.enter)=\"onCanvasKeyboardActivate($event)\"\n (keydown.space)=\"onCanvasKeyboardActivate($event)\">\n \n @for (placement of getSortedPanels(); track placement.panelId) {\n @if (isPanelVisible(placement)) {\n <div \n class=\"panel-container\"\n [class.panel-focused]=\"isPanelFocused(placement.panelId)\"\n [style.left.%]=\"toPercent(placement.x)\"\n [style.top.%]=\"toPercent(placement.y)\"\n [style.width.%]=\"toPercent(placement.w)\"\n [style.height.%]=\"toPercent(placement.h)\"\n [style.z-index]=\"placement.z || 0\"\n [style.transform]=\"getPanelTransform(placement)\"\n [style.transform-origin]=\"getTransformOrigin(placement)\"\n [attr.data-panel-id]=\"placement.panelId\"\n [attr.tabindex]=\"panelHasHotspots(placement.panelId) ? null : 0\"\n [attr.role]=\"panelHasHotspots(placement.panelId) ? null : 'button'\"\n [attr.aria-label]=\"panelHasHotspots(placement.panelId) ? null : 'Panel ' + placement.panelId\"\n [attr.loading]=\"getImageLoadingStrategy()\"\n (click)=\"onPanelClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelKeyboardActivate($event, placement.panelId)\"\n (keydown.space)=\"onPanelKeyboardActivate($event, placement.panelId)\">\n \n @if (getPanel(placement.panelId); as panelData) {\n <div class=\"panel-content\"\n [style.clip-path]=\"getPanelContentClipPath(placement)\"\n [style.transform]=\"getPanelContentTransform(placement)\"\n tabindex=\"0\"\n (click)=\"onPanelContainerClick($event, placement.panelId)\"\n (keydown.enter)=\"onPanelContainerClick($any($event), placement.panelId)\">\n <!-- Render panel layers -->\n @for (layer of panelData.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n [altText]=\"getAltText(layer)\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(placement.panelId)\"\n [panelId]=\"placement.panelId\"\n [placementId]=\"placement.panelId\"\n [readingOrderIndex]=\"readingOrderIndexOf(placement.panelId)\"\n [placementZ]=\"placement.z || 0\"\n [placementY]=\"placement.y\"\n [placementX]=\"placement.x\"\n (layerClick)=\"layerClick.emit($event)\">\n </pw-layer-renderer>\n }\n\n <!-- Interactive hotspots overlay for page view panels -->\n @if (interactive && panelData.hotspots && panelData.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"panelData.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [context]=\"variableContext\"\n (hotspotActivate)=\"onHotspotActivate($event, placement.panelId)\">\n </pw-hotspots-overlay>\n }\n\n <!-- Speech bubbles overlay for page view panels -->\n @if (speechEnabled && panelData.speechBubbles && panelData.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"panelData.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"getPagePanelWidth(placement)\"\n [containerHeight]=\"getPagePanelHeight(placement)\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n </div>\n }\n \n <!-- Focus indicator for rotated panels -->\n @if (isPanelFocused(placement.panelId)) {\n <div class=\"focus-rect\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n }\n </div>\n </div>\n </div>\n </div>\n }\n\n @if (viewMode === 'page' && !page) {\n <div class=\"no-panel\">\n <p>No page to display</p>\n </div>\n }\n\n <!-- Overflow navigation arrows -->\n @if (showOverflowArrows && panel) {\n @if (hasOverflowLeft) {\n <button \n class=\"overflow-arrow overflow-left\"\n type=\"button\"\n aria-label=\"Navigate left\"\n (click)=\"navigateLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n }\n\n @if (hasOverflowRight) {\n <button \n class=\"overflow-arrow overflow-right\"\n type=\"button\"\n aria-label=\"Navigate right\"\n (click)=\"navigateRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n }\n\n @if (hasOverflowTop) {\n <button \n class=\"overflow-arrow overflow-top\"\n type=\"button\"\n aria-label=\"Navigate up\"\n (click)=\"navigateUp()\">\n <pw-icon name=\"lucideChevronUp\" />\n </button>\n }\n\n @if (hasOverflowBottom) {\n <button \n class=\"overflow-arrow overflow-bottom\"\n type=\"button\"\n aria-label=\"Navigate down\"\n (click)=\"navigateDown()\">\n <pw-icon name=\"lucideChevronDown\" />\n </button>\n }\n }\n\n <!-- Navigation arrows (hover zones) -->\n @if (viewMode === 'panel') {\n @if (showLeftArrow) {\n <button \n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Previous panel\"\n (click)=\"onNavigatePrevious(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronLeft\" /></span>\n </button>\n }\n\n @if (showRightArrow) {\n <button \n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Next panel\"\n (click)=\"onNavigateNext(); $event.stopPropagation()\">\n <span class=\"arrow-icon\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n }\n }\n</div>\n", styles: [".viewport-container{position:relative;width:100%;height:100%;overflow:hidden;background-color:#f5f5f5;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.viewport-container:active{cursor:grabbing}.viewport-container.interactive{cursor:default}.viewport-content{position:absolute;top:50%;left:50%;transform-origin:center center;will-change:transform}.viewport-content.reduced-motion{transition:none!important}.panel-container{position:relative;background:#fff;box-shadow:0 2px 8px #0000001a;width:1280px;height:720px;max-width:100%;max-height:100%}.t-frame{position:relative;z-index:1}.t-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.t-fade-in{animation:pw-t-fade-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-in{animation:pw-t-zoom-in var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-right{animation:pw-t-from-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-left{animation:pw-t-from-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-bottom{animation:pw-t-from-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-from-top{animation:pw-t-from-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-left{animation:pw-t-to-left var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-right{animation:pw-t-to-right var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-top{animation:pw-t-to-top var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-to-bottom{animation:pw-t-to-bottom var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-in{0%{opacity:0}to{opacity:1}}@keyframes pw-t-zoom-in{0%{opacity:0;transform:scale(.94)}to{opacity:1;transform:scale(1)}}@keyframes pw-t-from-right{0%{transform:translate(100%)}to{transform:translate(0)}}@keyframes pw-t-from-left{0%{transform:translate(-100%)}to{transform:translate(0)}}@keyframes pw-t-from-bottom{0%{transform:translateY(100%)}to{transform:translateY(0)}}@keyframes pw-t-from-top{0%{transform:translateY(-100%)}to{transform:translateY(0)}}@keyframes pw-t-to-left{0%{transform:translate(0)}to{transform:translate(-100%)}}@keyframes pw-t-to-right{0%{transform:translate(0)}to{transform:translate(100%)}}@keyframes pw-t-to-top{0%{transform:translateY(0)}to{transform:translateY(-100%)}}@keyframes pw-t-to-bottom{0%{transform:translateY(0)}to{transform:translateY(100%)}}.layer{position:absolute;top:0;left:0}.layer-placeholder{padding:20px;background:#0078d71a;border:1px dashed rgba(0,120,215,.3);color:#0078d7;font-size:14px}.no-panel{display:flex;align-items:center;justify-content:center;min-width:400px;min-height:300px;color:#666}.reduced-motion *{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}.overflow-arrow{position:absolute;z-index:100;background:#0009;color:#fff;border:none;border-radius:4px;width:40px;height:40px;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;box-shadow:0 2px 8px #0000004d}.overflow-arrow:hover{background:#000c;transform:scale(1.1)}.overflow-arrow:active{transform:scale(.95)}.overflow-left{left:20px;top:50%;transform:translateY(-50%)}.overflow-left:hover{transform:translateY(-50%) scale(1.1)}.overflow-right{right:20px;top:50%;transform:translateY(-50%)}.overflow-right:hover{transform:translateY(-50%) scale(1.1)}.overflow-top{top:20px;left:50%;transform:translate(-50%)}.overflow-top:hover{transform:translate(-50%) scale(1.1)}.overflow-bottom{bottom:20px;left:50%;transform:translate(-50%)}.overflow-bottom:hover{transform:translate(-50%) scale(1.1)}.viewport-page{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:auto;padding:16px;box-sizing:border-box}.page-canvas{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;background:#1a1a1a;border-radius:4px;overflow:hidden}.page-stage{position:relative;width:100%;max-width:1400px;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px}.page-stage .page-canvas{max-width:none;height:100%;aspect-ratio:auto}.pt-frame{position:relative;z-index:1;width:100%;height:100%}.pt-frame-leave{position:absolute;inset:0;z-index:0;pointer-events:none}.pt-frame-leave--above{z-index:2}.pt-frame.t-fade-in,.pt-frame.t-zoom-in,.pt-frame.t-from-right,.pt-frame.t-from-left,.pt-frame.t-from-bottom,.pt-frame.t-from-top{will-change:transform,opacity}.t-fade-out{animation:pw-t-fade-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}.t-zoom-out{animation:pw-t-zoom-out var(--pw-t-duration, .4s) var(--pw-t-easing, ease) both}@keyframes pw-t-fade-out{0%{opacity:1}to{opacity:0}}@keyframes pw-t-zoom-out{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.06)}}.viewport-page .panel-container{position:absolute;overflow:hidden;background:#000;border:1px solid rgba(255,255,255,.1);border-radius:2px;transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;transform-origin:center center;outline:none}.viewport-page .panel-container:hover{border-color:#ffffff4d;cursor:pointer}.viewport-page .panel-container.panel-focused{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d74d,0 4px 12px #0006;z-index:9999!important}.focus-rect{position:absolute;inset:-4px;border:3px solid rgba(0,120,215,.9);border-radius:4px;pointer-events:none;animation:focusPulse 1.5s ease-in-out infinite}@keyframes focusPulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.02)}}@media (prefers-reduced-motion: reduce){.focus-rect{animation:none}.viewport-page .panel-container{transition:none}}.viewport-page .panel-content{position:relative;width:100%;height:100%}@media (max-width: 1024px){.page-canvas,.page-stage{max-width:900px}}@media (max-width: 768px){.viewport-page{padding:8px}.page-canvas,.page-stage{max-width:100%}}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:60px;height:120px;background:#0006;border:none;color:#fff;font-size:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:50;animation:fadeInArrow .2s ease}.nav-arrow:hover{background:#0009;width:70px}.nav-arrow:active{background:#000000b3;transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:0;border-radius:0 8px 8px 0}.nav-arrow-right{right:0;border-radius:8px 0 0 8px}.arrow-icon{font-size:48px;line-height:1;font-weight:300}@keyframes fadeInArrow{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.nav-arrow{animation:none;transition:none}}@media (max-width: 768px){.nav-arrow{width:50px;height:100px;font-size:36px}.nav-arrow:hover{width:55px}.arrow-icon{font-size:36px}}\n"] }]
|
|
12236
12355
|
}], propDecorators: { panel: [{
|
|
12237
12356
|
type: Input
|
|
12238
12357
|
}], page: [{
|
|
@@ -13416,11 +13535,11 @@ class CanvasStageComponent {
|
|
|
13416
13535
|
return Math.hypot(points[1].x - points[0].x, points[1].y - points[0].y);
|
|
13417
13536
|
}
|
|
13418
13537
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CanvasStageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
13419
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CanvasStageComponent, isStandalone: true, selector: "pw-canvas-stage", inputs: { canvas: "canvas", panels: "panels", graph: "graph", currentPanelId: "currentPanelId", visitedPanelIds: "visitedPanelIds", locale: "locale", variableContext: "variableContext", speechEnabled: "speechEnabled", workBalloonConfig: "workBalloonConfig", characters: "characters", reducedMotion: "reducedMotion", interactive: "interactive", preload: "preload" }, outputs: { panelTap: "panelTap", navigateNext: "navigateNext", navigatePrevious: "navigatePrevious", viewportClick: "viewportClick", hotspotActivate: "hotspotActivate", cameraChange: "cameraChange" }, usesOnChanges: true, ngImport: i0, template: "<div\
|
|
13538
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CanvasStageComponent, isStandalone: true, selector: "pw-canvas-stage", inputs: { canvas: "canvas", panels: "panels", graph: "graph", currentPanelId: "currentPanelId", visitedPanelIds: "visitedPanelIds", locale: "locale", variableContext: "variableContext", speechEnabled: "speechEnabled", workBalloonConfig: "workBalloonConfig", characters: "characters", reducedMotion: "reducedMotion", interactive: "interactive", preload: "preload" }, outputs: { panelTap: "panelTap", navigateNext: "navigateNext", navigatePrevious: "navigatePrevious", viewportClick: "viewportClick", hotspotActivate: "hotspotActivate", cameraChange: "cameraChange" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"canvas-viewport\"\n [class.reduced-motion]=\"reducedMotion\"\n [style]=\"backgroundStyle()\"\n role=\"region\"\n aria-label=\"Canvas viewport\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (wheel)=\"onWheel($event)\">\n\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\n\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\n @if (worldBackgroundStyle(); as bgStyle) {\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\n }\n\n <!-- Non-panel set dressing -->\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\n <img\n class=\"canvas-decoration\"\n [src]=\"assetUrl(decoration.assetId)\"\n [style]=\"decorationStyle(decoration)\"\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"lazy\" />\n }\n\n <!-- Placed panels (virtualized) -->\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\n <div\n class=\"canvas-panel\"\n [class.current]=\"isCurrent(entry.panelId)\"\n [class.unrevealed]=\"!entry.revealed\"\n [style]=\"placementStyle(entry)\"\n [attr.data-panel-id]=\"entry.panelId\"\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\n\n @if (entry.revealed) {\n @for (layer of entry.panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n viewMode=\"panel\"\n [viewActive]=\"isCurrent(entry.panelId)\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\n [panelId]=\"entry.panelId\">\n </pw-layer-renderer>\n }\n\n <!-- Hotspots are interactive on the CURRENT panel (the one the\n camera frames); other panels show them render-only, and a\n tap there keeps its panel-navigation meaning. -->\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"entry.panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [context]=\"variableContext\"\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\n </pw-hotspots-overlay>\n }\n\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"entry.panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n } @else {\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"], dependencies: [{ kind: "component", type: LayerRendererComponent, selector: "pw-layer-renderer", inputs: ["layer", "locale", "baseUrl", "targetWidth", "altText", "viewMode", "viewActive", "reducedMotion", "panelId", "placementId", "readingOrderIndex", "placementZ", "placementY", "placementX"], outputs: ["layerClick"] }, { kind: "component", type: SpeechBubblesComponent, selector: "pw-speech-bubbles", inputs: ["bubbles", "locale", "containerWidth", "containerHeight", "readingScale", "workBalloonConfig", "characters"], outputs: ["bubbleClick", "bubbleAudioPlay"] }, { kind: "component", type: HotspotsOverlayComponent, selector: "pw-hotspots-overlay", inputs: ["hotspots", "locale", "containerWidth", "containerHeight", "context", "interactive", "showIndicators"], outputs: ["hotspotActivate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
13420
13539
|
}
|
|
13421
13540
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CanvasStageComponent, decorators: [{
|
|
13422
13541
|
type: Component,
|
|
13423
|
-
args: [{ selector: 'pw-canvas-stage', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\
|
|
13542
|
+
args: [{ selector: 'pw-canvas-stage', imports: [LayerRendererComponent, SpeechBubblesComponent, HotspotsOverlayComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"canvas-viewport\"\n [class.reduced-motion]=\"reducedMotion\"\n [style]=\"backgroundStyle()\"\n role=\"region\"\n aria-label=\"Canvas viewport\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n (wheel)=\"onWheel($event)\">\n\n <div class=\"canvas-world\" [style.transform]=\"worldTransform\">\n\n <!-- Tiled/stretched backdrop covering the canvas bounds -->\n @if (worldBackgroundStyle(); as bgStyle) {\n <div class=\"canvas-backdrop\" [style]=\"bgStyle\" aria-hidden=\"true\"></div>\n }\n\n <!-- Non-panel set dressing -->\n @for (decoration of visibleDecorations; track decoration.assetId + decoration.x + decoration.y) {\n <img\n class=\"canvas-decoration\"\n [src]=\"assetUrl(decoration.assetId)\"\n [style]=\"decorationStyle(decoration)\"\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"lazy\" />\n }\n\n <!-- Placed panels (virtualized) -->\n @for (entry of stagePlacements; track trackPlacement($index, entry)) {\n <div\n class=\"canvas-panel\"\n [class.current]=\"isCurrent(entry.panelId)\"\n [class.unrevealed]=\"!entry.revealed\"\n [style]=\"placementStyle(entry)\"\n [attr.data-panel-id]=\"entry.panelId\"\n [attr.aria-hidden]=\"isCurrent(entry.panelId) ? null : true\"\n [attr.inert]=\"isCurrent(entry.panelId) ? null : ''\">\n\n @if (entry.revealed) {\n @for (layer of entry.panel.layers; track layer.id) {\n <pw-layer-renderer\n [layer]=\"layer\"\n [locale]=\"locale\"\n viewMode=\"panel\"\n [viewActive]=\"isCurrent(entry.panelId)\"\n [reducedMotion]=\"reducedMotion\"\n [targetWidth]=\"targetWidthFor(entry.panelId)\"\n [panelId]=\"entry.panelId\">\n </pw-layer-renderer>\n }\n\n <!-- Hotspots are interactive on the CURRENT panel (the one the\n camera frames); other panels show them render-only, and a\n tap there keeps its panel-navigation meaning. -->\n @if (entry.panel.hotspots && entry.panel.hotspots.length > 0) {\n <pw-hotspots-overlay\n [hotspots]=\"entry.panel.hotspots\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [context]=\"variableContext\"\n [interactive]=\"interactive && isCurrent(entry.panelId)\"\n (hotspotActivate)=\"hotspotActivate.emit({ hotspot: $event.hotspot, x: $event.x, y: $event.y, panelId: entry.panelId })\">\n </pw-hotspots-overlay>\n }\n\n @if (speechEnabled && entry.panel.speechBubbles && entry.panel.speechBubbles.length > 0) {\n <pw-speech-bubbles\n [bubbles]=\"entry.panel.speechBubbles\"\n [locale]=\"locale\"\n [containerWidth]=\"entry.placement.w\"\n [containerHeight]=\"entry.placement.h\"\n [readingScale]=\"getReadingScale()\"\n [workBalloonConfig]=\"workBalloonConfig\"\n [characters]=\"characters\">\n </pw-speech-bubbles>\n }\n } @else {\n <!-- Unrevealed panel: dark veil, no content (spoiler protection) -->\n <div class=\"canvas-panel-veil\" aria-hidden=\"true\"></div>\n }\n </div>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%}.canvas-viewport{position:relative;width:100%;height:100%;overflow:hidden;background-color:#101014;touch-action:none;cursor:grab;outline:none}.canvas-viewport:active{cursor:grabbing}.canvas-world{position:absolute;top:0;left:0;width:0;height:0;transform-origin:0 0;will-change:transform}.canvas-backdrop{position:absolute;pointer-events:none}.canvas-decoration{position:absolute;pointer-events:none;max-width:none}.canvas-panel{position:absolute;overflow:hidden;background:#000;contain:layout paint;content-visibility:auto;box-shadow:0 8px 40px #0000008c}.canvas-panel.current{box-shadow:0 0 0 2px #ffffff2e,0 8px 48px #0009}.canvas-panel-veil{position:absolute;inset:0;background:radial-gradient(ellipse at center,#1e1e26eb,#0a0a0efa)}.reduced-motion .canvas-world{will-change:auto}\n"] }]
|
|
13424
13543
|
}], propDecorators: { canvas: [{
|
|
13425
13544
|
type: Input
|
|
13426
13545
|
}], panels: [{
|
|
@@ -13735,11 +13854,11 @@ class ToolbarComponent {
|
|
|
13735
13854
|
this.close.emit();
|
|
13736
13855
|
}
|
|
13737
13856
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
13738
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ToolbarComponent, isStandalone: true, selector: "pw-toolbar", inputs: { locale: "locale", availableLocales: "availableLocales", viewMode: "viewMode", pageViewAvailable: "pageViewAvailable", speechEnabled: "speechEnabled", audioEnabled: "audioEnabled", sfxEnabled: "sfxEnabled", autoplayEnabled: "autoplayEnabled", secondsPerPanel: "secondsPerPanel", autoplayProgress: "autoplayProgress", thumbnailsVisible: "thumbnailsVisible", hasAlternatives: "hasAlternatives", hasBranches: "hasBranches", liked: "liked", bookmarked: "bookmarked", showSocial: "showSocial", visible: "visible" }, outputs: { toggleView: "toggleView", localeChange: "localeChange", toggleSpeech: "toggleSpeech", toggleAudio: "toggleAudio", toggleSfx: "toggleSfx", toggleAutoplay: "toggleAutoplay", secondsPerPanelChange: "secondsPerPanelChange", toggleThumbnails: "toggleThumbnails", openToc: "openToc", openSettings: "openSettings", openCharacters: "openCharacters", cycleAlternative: "cycleAlternative", showBranches: "showBranches", openExtras: "openExtras", like: "like", bookmark: "bookmark", share: "share", openComments: "openComments", close: "close", openLanguage: "openLanguage" }, ngImport: i0, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\r\n <div class=\"toolbar\">\r\n <!-- Left section: View and navigation controls -->\r\n <div class=\"toolbar-section toolbar-left\">\r\n <!-- View toggle (page/panel) -->\r\n <button\r\n class=\"toolbar-btn\"\r\n [class.disabled]=\"!pageViewAvailable\"\r\n [disabled]=\"!pageViewAvailable\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\r\n [attr.title]=\"'toolbar.view_toggle' | translate\"\r\n (click)=\"onToggleView()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\r\n </button>\r\n\r\n <!-- Language selector -->\r\n @if (availableLocales.length > 1) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\r\n (click)=\"openLanguage.emit()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\r\n <span class=\"btn-label\">{{ locale }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Center section: Content controls -->\r\n <div class=\"toolbar-section toolbar-center\">\r\n <!-- Speech bubbles toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"speechEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\r\n [attr.aria-pressed]=\"speechEnabled\"\r\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\r\n (click)=\"onToggleSpeech()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\r\n </button>\r\n\r\n <!-- Audio toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"audioEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\r\n [attr.aria-pressed]=\"audioEnabled\"\r\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\r\n (click)=\"onToggleAudio()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\r\n </button>\r\n\r\n <!-- SFX toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"sfxEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\r\n [attr.aria-pressed]=\"sfxEnabled\"\r\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\r\n (click)=\"onToggleSfx()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\r\n </button>\r\n\r\n <!-- Autoplay control -->\r\n <div class=\"toolbar-dropdown\">\r\n <button\r\n class=\"toolbar-btn toggle-btn autoplay-btn\"\r\n [class.active]=\"autoplayEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\r\n [attr.aria-pressed]=\"autoplayEnabled\"\r\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\r\n (click)=\"onToggleAutoplay()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-progress-bar\">\r\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\r\n </div>\r\n }\r\n </button>\r\n\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-controls\">\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(-1)\">\r\n -\r\n </button>\r\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(1)\">\r\n +\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Thumbnails toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"thumbnailsVisible\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\r\n [attr.aria-pressed]=\"thumbnailsVisible\"\r\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\r\n (click)=\"onToggleThumbnails()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Right section: Navigation and info controls -->\r\n <div class=\"toolbar-section toolbar-right\">\r\n <!-- Table of Contents -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\r\n [attr.title]=\"'toolbar.toc_open' | translate\"\r\n (click)=\"onOpenToc()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\r\n </button>\r\n\r\n <!-- Settings -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\r\n [attr.title]=\"'toolbar.settings_open' | translate\"\r\n (click)=\"onOpenSettings()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\r\n </button>\r\n\r\n <!-- Characters -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\r\n [attr.title]=\"'toolbar.characters_open' | translate\"\r\n (click)=\"onOpenCharacters()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\r\n </button>\r\n\r\n <!-- Alternatives (if available) -->\r\n @if (hasAlternatives) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\r\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\r\n (click)=\"onCycleAlternative()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Branch indicator (if choices ahead) -->\r\n @if (hasBranches) {\r\n <button\r\n class=\"toolbar-btn branch-indicator\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\r\n [attr.title]=\"'toolbar.choices_view' | translate\"\r\n (click)=\"onShowBranches()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Extras -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.extras' | translate\"\r\n [attr.title]=\"'toolbar.extras_open' | translate\"\r\n (click)=\"onOpenExtras()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Social cluster (rightmost) -->\r\n @if (showSocial) {\r\n <div class=\"toolbar-section toolbar-social\">\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"liked\"\r\n [attr.aria-pressed]=\"liked\"\r\n [attr.aria-label]=\"'toolbar.like' | translate\"\r\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\r\n (click)=\"onLike()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"bookmarked\"\r\n [attr.aria-pressed]=\"bookmarked\"\r\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\r\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\r\n (click)=\"onBookmark()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.share' | translate\"\r\n [attr.title]=\"'toolbar.share_panel' | translate\"\r\n (click)=\"onShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.comments' | translate\"\r\n [attr.title]=\"'toolbar.comments_view' | translate\"\r\n (click)=\"onOpenComments()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Close button -->\r\n <button\r\n class=\"toolbar-btn close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.close' | translate\"\r\n [attr.title]=\"'toolbar.close' | translate\"\r\n (click)=\"onClose()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\r\n </button>\r\n </div>\r\n</div>\r\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
13857
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ToolbarComponent, isStandalone: true, selector: "pw-toolbar", inputs: { locale: "locale", availableLocales: "availableLocales", viewMode: "viewMode", pageViewAvailable: "pageViewAvailable", speechEnabled: "speechEnabled", audioEnabled: "audioEnabled", sfxEnabled: "sfxEnabled", autoplayEnabled: "autoplayEnabled", secondsPerPanel: "secondsPerPanel", autoplayProgress: "autoplayProgress", thumbnailsVisible: "thumbnailsVisible", hasAlternatives: "hasAlternatives", hasBranches: "hasBranches", liked: "liked", bookmarked: "bookmarked", showSocial: "showSocial", visible: "visible" }, outputs: { toggleView: "toggleView", localeChange: "localeChange", toggleSpeech: "toggleSpeech", toggleAudio: "toggleAudio", toggleSfx: "toggleSfx", toggleAutoplay: "toggleAutoplay", secondsPerPanelChange: "secondsPerPanelChange", toggleThumbnails: "toggleThumbnails", openToc: "openToc", openSettings: "openSettings", openCharacters: "openCharacters", cycleAlternative: "cycleAlternative", showBranches: "showBranches", openExtras: "openExtras", like: "like", bookmark: "bookmark", share: "share", openComments: "openComments", close: "close", openLanguage: "openLanguage" }, ngImport: i0, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
13739
13858
|
}
|
|
13740
13859
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ToolbarComponent, decorators: [{
|
|
13741
13860
|
type: Component,
|
|
13742
|
-
args: [{ selector: 'pw-toolbar', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\r\n <div class=\"toolbar\">\r\n <!-- Left section: View and navigation controls -->\r\n <div class=\"toolbar-section toolbar-left\">\r\n <!-- View toggle (page/panel) -->\r\n <button\r\n class=\"toolbar-btn\"\r\n [class.disabled]=\"!pageViewAvailable\"\r\n [disabled]=\"!pageViewAvailable\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\r\n [attr.title]=\"'toolbar.view_toggle' | translate\"\r\n (click)=\"onToggleView()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\r\n </button>\r\n\r\n <!-- Language selector -->\r\n @if (availableLocales.length > 1) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\r\n (click)=\"openLanguage.emit()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\r\n <span class=\"btn-label\">{{ locale }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Center section: Content controls -->\r\n <div class=\"toolbar-section toolbar-center\">\r\n <!-- Speech bubbles toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"speechEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\r\n [attr.aria-pressed]=\"speechEnabled\"\r\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\r\n (click)=\"onToggleSpeech()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\r\n </button>\r\n\r\n <!-- Audio toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"audioEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\r\n [attr.aria-pressed]=\"audioEnabled\"\r\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\r\n (click)=\"onToggleAudio()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\r\n </button>\r\n\r\n <!-- SFX toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"sfxEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\r\n [attr.aria-pressed]=\"sfxEnabled\"\r\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\r\n (click)=\"onToggleSfx()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\r\n </button>\r\n\r\n <!-- Autoplay control -->\r\n <div class=\"toolbar-dropdown\">\r\n <button\r\n class=\"toolbar-btn toggle-btn autoplay-btn\"\r\n [class.active]=\"autoplayEnabled\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\r\n [attr.aria-pressed]=\"autoplayEnabled\"\r\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\r\n (click)=\"onToggleAutoplay()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-progress-bar\">\r\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\r\n </div>\r\n }\r\n </button>\r\n\r\n @if (autoplayEnabled) {\r\n <div class=\"autoplay-controls\">\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(-1)\">\r\n -\r\n </button>\r\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\r\n <button\r\n class=\"control-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\r\n (click)=\"adjustSecondsPerPanel(1)\">\r\n +\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Thumbnails toggle -->\r\n <button\r\n class=\"toolbar-btn toggle-btn\"\r\n [class.active]=\"thumbnailsVisible\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\r\n [attr.aria-pressed]=\"thumbnailsVisible\"\r\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\r\n (click)=\"onToggleThumbnails()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Right section: Navigation and info controls -->\r\n <div class=\"toolbar-section toolbar-right\">\r\n <!-- Table of Contents -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\r\n [attr.title]=\"'toolbar.toc_open' | translate\"\r\n (click)=\"onOpenToc()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\r\n </button>\r\n\r\n <!-- Settings -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\r\n [attr.title]=\"'toolbar.settings_open' | translate\"\r\n (click)=\"onOpenSettings()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\r\n </button>\r\n\r\n <!-- Characters -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\r\n [attr.title]=\"'toolbar.characters_open' | translate\"\r\n (click)=\"onOpenCharacters()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\r\n </button>\r\n\r\n <!-- Alternatives (if available) -->\r\n @if (hasAlternatives) {\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\r\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\r\n (click)=\"onCycleAlternative()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Branch indicator (if choices ahead) -->\r\n @if (hasBranches) {\r\n <button\r\n class=\"toolbar-btn branch-indicator\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\r\n [attr.title]=\"'toolbar.choices_view' | translate\"\r\n (click)=\"onShowBranches()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\r\n </button>\r\n }\r\n\r\n <!-- Extras -->\r\n <button\r\n class=\"toolbar-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.extras' | translate\"\r\n [attr.title]=\"'toolbar.extras_open' | translate\"\r\n (click)=\"onOpenExtras()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\r\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\r\n </button>\r\n </div>\r\n\r\n <!-- Social cluster (rightmost) -->\r\n @if (showSocial) {\r\n <div class=\"toolbar-section toolbar-social\">\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"liked\"\r\n [attr.aria-pressed]=\"liked\"\r\n [attr.aria-label]=\"'toolbar.like' | translate\"\r\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\r\n (click)=\"onLike()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [class.active]=\"bookmarked\"\r\n [attr.aria-pressed]=\"bookmarked\"\r\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\r\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\r\n (click)=\"onBookmark()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.share' | translate\"\r\n [attr.title]=\"'toolbar.share_panel' | translate\"\r\n (click)=\"onShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n </button>\r\n\r\n <button\r\n class=\"toolbar-btn icon-only\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.comments' | translate\"\r\n [attr.title]=\"'toolbar.comments_view' | translate\"\r\n (click)=\"onOpenComments()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Close button -->\r\n <button\r\n class=\"toolbar-btn close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toolbar.close' | translate\"\r\n [attr.title]=\"'toolbar.close' | translate\"\r\n (click)=\"onClose()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\r\n </button>\r\n </div>\r\n</div>\r\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"] }]
|
|
13861
|
+
args: [{ selector: 'pw-toolbar', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"toolbar-container\" [class.visible]=\"visible\">\n <div class=\"toolbar\">\n <!-- Left section: View and navigation controls -->\n <div class=\"toolbar-section toolbar-left\">\n <!-- View toggle (page/panel) -->\n <button\n class=\"toolbar-btn\"\n [class.disabled]=\"!pageViewAvailable\"\n [disabled]=\"!pageViewAvailable\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.view_toggle' | translate\"\n [attr.title]=\"'toolbar.view_toggle' | translate\"\n (click)=\"onToggleView()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"viewMode === 'page' ? 'lucideLayoutGrid' : viewMode === 'canvas' ? 'lucideGlobe' : 'lucideSquare'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.view' | translate }}</span>\n </button>\n\n <!-- Language selector -->\n @if (availableLocales.length > 1) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.select_language' | translate\"\n (click)=\"openLanguage.emit()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"btn-label\">{{ locale }}</span>\n </button>\n }\n </div>\n\n <!-- Center section: Content controls -->\n <div class=\"toolbar-section toolbar-center\">\n <!-- Speech bubbles toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"speechEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.speech_toggle' | translate\"\n [attr.aria-pressed]=\"speechEnabled\"\n [attr.title]=\"'toolbar.speech_toggle' | translate\"\n (click)=\"onToggleSpeech()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageSquare\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.speech' | translate }}</span>\n </button>\n\n <!-- Audio toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"audioEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.audio_toggle' | translate\"\n [attr.aria-pressed]=\"audioEnabled\"\n [attr.title]=\"'toolbar.audio_toggle' | translate\"\n (click)=\"onToggleAudio()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"audioEnabled ? 'lucideVolume2' : 'lucideVolumeX'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.audio' | translate }}</span>\n </button>\n\n <!-- SFX toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"sfxEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.sfx_toggle' | translate\"\n [attr.aria-pressed]=\"sfxEnabled\"\n [attr.title]=\"'toolbar.sfx_toggle' | translate\"\n (click)=\"onToggleSfx()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMusic\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.sfx' | translate }}</span>\n </button>\n\n <!-- Autoplay control -->\n <div class=\"toolbar-dropdown\">\n <button\n class=\"toolbar-btn toggle-btn autoplay-btn\"\n [class.active]=\"autoplayEnabled\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.autoplay_toggle' | translate\"\n [attr.aria-pressed]=\"autoplayEnabled\"\n [attr.title]=\"'toolbar.autoplay_toggle' | translate\"\n (click)=\"onToggleAutoplay()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"autoplayEnabled ? 'lucidePause' : 'lucidePlay'\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.autoplay' | translate }}</span>\n @if (autoplayEnabled) {\n <div class=\"autoplay-progress-bar\">\n <div class=\"autoplay-progress-fill\" [style.width.%]=\"autoplayProgress\"></div>\n </div>\n }\n </button>\n\n @if (autoplayEnabled) {\n <div class=\"autoplay-controls\">\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.decrease_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(-1)\">\n -\n </button>\n <span class=\"control-value\">{{ 'toolbar.seconds' | translate: {value: secondsPerPanel} }}</span>\n <button\n class=\"control-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.increase_speed' | translate\"\n (click)=\"adjustSecondsPerPanel(1)\">\n +\n </button>\n </div>\n }\n </div>\n\n <!-- Thumbnails toggle -->\n <button\n class=\"toolbar-btn toggle-btn\"\n [class.active]=\"thumbnailsVisible\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.thumbs_toggle' | translate\"\n [attr.aria-pressed]=\"thumbnailsVisible\"\n [attr.title]=\"'toolbar.thumbs_toggle' | translate\"\n (click)=\"onToggleThumbnails()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideImages\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.thumbs' | translate }}</span>\n </button>\n </div>\n\n <!-- Right section: Navigation and info controls -->\n <div class=\"toolbar-section toolbar-right\">\n <!-- Table of Contents -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.toc_open' | translate\"\n [attr.title]=\"'toolbar.toc_open' | translate\"\n (click)=\"onOpenToc()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideList\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.toc' | translate }}</span>\n </button>\n\n <!-- Settings -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.settings_open' | translate\"\n [attr.title]=\"'toolbar.settings_open' | translate\"\n (click)=\"onOpenSettings()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSettings\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.settings' | translate }}</span>\n </button>\n\n <!-- Characters -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.characters_open' | translate\"\n [attr.title]=\"'toolbar.characters_open' | translate\"\n (click)=\"onOpenCharacters()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideUsers\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.characters' | translate }}</span>\n </button>\n\n <!-- Alternatives (if available) -->\n @if (hasAlternatives) {\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.alt_label' | translate\"\n [attr.title]=\"'toolbar.alt_cycle' | translate\"\n (click)=\"onCycleAlternative()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideRefreshCw\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.alt' | translate }}</span>\n </button>\n }\n\n <!-- Branch indicator (if choices ahead) -->\n @if (hasBranches) {\n <button\n class=\"toolbar-btn branch-indicator\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.choices_label' | translate\"\n [attr.title]=\"'toolbar.choices_view' | translate\"\n (click)=\"onShowBranches()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSplit\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.choices' | translate }}</span>\n </button>\n }\n\n <!-- Extras -->\n <button\n class=\"toolbar-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.extras' | translate\"\n [attr.title]=\"'toolbar.extras_open' | translate\"\n (click)=\"onOpenExtras()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideSparkles\" /></span>\n <span class=\"btn-label\">{{ 'toolbar.extras' | translate }}</span>\n </button>\n </div>\n\n <!-- Social cluster (rightmost) -->\n @if (showSocial) {\n <div class=\"toolbar-section toolbar-social\">\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"liked\"\n [attr.aria-pressed]=\"liked\"\n [attr.aria-label]=\"'toolbar.like' | translate\"\n [attr.title]=\"(liked ? 'toolbar.like_remove' : 'toolbar.like_work') | translate\"\n (click)=\"onLike()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideHeart\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [class.active]=\"bookmarked\"\n [attr.aria-pressed]=\"bookmarked\"\n [attr.aria-label]=\"'toolbar.bookmark' | translate\"\n [attr.title]=\"(bookmarked ? 'toolbar.bookmark_remove' : 'toolbar.bookmark_panel') | translate\"\n (click)=\"onBookmark()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideBookmark\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.share' | translate\"\n [attr.title]=\"'toolbar.share_panel' | translate\"\n (click)=\"onShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n </button>\n\n <button\n class=\"toolbar-btn icon-only\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.comments' | translate\"\n [attr.title]=\"'toolbar.comments_view' | translate\"\n (click)=\"onOpenComments()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n </button>\n </div>\n }\n\n <!-- Close button -->\n <button\n class=\"toolbar-btn close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toolbar.close' | translate\"\n [attr.title]=\"'toolbar.close' | translate\"\n (click)=\"onClose()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideX\" /></span>\n </button>\n </div>\n</div>\n", styles: [".toolbar-container{position:fixed;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.9),rgba(0,0,0,.7) 70%,transparent);padding:24px 16px 16px;transform:translateY(100%);opacity:0;transition:all .3s cubic-bezier(.4,0,.2,1);pointer-events:none;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);overflow:visible}.toolbar-container.visible{transform:translateY(0);opacity:1;pointer-events:auto;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;align-items:center;gap:8px;max-width:1400px;margin:0 auto;overflow-x:auto;overflow-y:visible;padding:8px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent}.toolbar::-webkit-scrollbar{height:6px}.toolbar::-webkit-scrollbar-track{background:transparent}.toolbar::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.toolbar-section{display:flex;align-items:center;gap:6px;flex-shrink:0}.toolbar-left{margin-right:8px}.toolbar-center{flex:1 0 auto;justify-content:center}.toolbar-right{margin-left:8px}.toolbar-social{margin-left:16px;padding-left:16px;border-left:1px solid rgba(255,255,255,.2)}.toolbar-btn{display:flex;align-items:center;gap:6px;padding:8px 12px;min-height:40px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.toolbar-btn:hover{background:#fff3;border-color:#ffffff4d;transform:translateY(-1px)}.toolbar-btn:active{background:#ffffff26;transform:translateY(0)}.toolbar-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}.toolbar-btn:disabled:hover{background:#ffffff1a;border-color:#fff3}.toolbar-btn.toggle-btn.active{background:#0078d7cc;border-color:#0078d7}.toolbar-btn.toggle-btn.active:hover{background:#0078d7e6}.toolbar-btn.icon-only{padding:8px;min-width:40px;justify-content:center}.toolbar-btn.icon-only .btn-label{display:none}.toolbar-btn.close-btn{margin-left:8px;background:#ff00004d;border-color:#ff000080}.toolbar-btn.close-btn:hover{background:#ff000080;border-color:#ff0000b3}.toolbar-btn.branch-indicator{background:#ffa5004d;border-color:#ffa50080;animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}.btn-icon{font-size:18px;line-height:1;display:flex;align-items:center;justify-content:center;flex-shrink:0}.btn-label{font-size:13px;line-height:1}.toolbar-dropdown{position:relative;display:flex;flex-direction:column;align-items:stretch;gap:8px}.dropdown-menu{position:absolute;bottom:100%;left:0;margin-bottom:8px;min-width:120px;background:#1e1e1ef2;border:1px solid rgba(255,255,255,.2);border-radius:6px;padding:4px;box-shadow:0 4px 12px #0006;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:1000;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dropdown-item{display:block;width:100%;padding:8px 12px;font-size:14px;color:#ffffffe6;background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;transition:background .2s ease}.dropdown-item:hover{background:#ffffff1a}.dropdown-item.active{background:#0078d780;font-weight:600}.autoplay-controls{display:flex;align-items:center;gap:6px;padding:6px 8px;background:#0006;border:1px solid rgba(255,255,255,.2);border-radius:6px}.control-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease}.control-btn:hover{background:#fff3}.control-btn:active{background:#ffffff26;transform:scale(.95)}.control-value{min-width:40px;text-align:center;font-size:13px;font-weight:600;color:#ffffffe6}@media (max-width: 768px){.toolbar-container{padding:16px 8px 8px}.toolbar{gap:4px;padding:4px}.toolbar-section{gap:4px}.toolbar-btn{padding:6px 10px;min-height:36px;font-size:12px}.btn-icon{font-size:16px}.btn-label{font-size:11px}.toolbar-center .btn-label,.toolbar-right .btn-label{display:none}.toolbar-btn{padding:6px 8px;min-width:36px;justify-content:center}.toolbar-left .toolbar-btn{padding:6px 10px}.toolbar-left .btn-label{display:inline}.autoplay-controls{padding:4px 6px;gap:4px}.control-btn{width:24px;height:24px;font-size:14px}.control-value{min-width:32px;font-size:11px}.toolbar-social{margin-left:8px;padding-left:8px}}@media (min-width: 769px) and (max-width: 1024px){.toolbar-btn{padding:7px 11px;min-height:38px}.btn-icon{font-size:17px}.btn-label{font-size:12px}}@media (prefers-reduced-motion: reduce){.toolbar-container,.toolbar-btn,.dropdown-menu,.dropdown-item,.control-btn{transition:none;animation:none}.toolbar-container.visible{animation:none}.toolbar-btn:hover{transform:none}.toolbar-btn.branch-indicator{animation:none}}.toolbar-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.dropdown-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:-2px}@media (prefers-contrast: high){.toolbar-btn{border-width:2px}.toolbar-btn.toggle-btn.active{background:#0078d7}}.autoplay-btn{position:relative;overflow:visible}.autoplay-progress-bar{position:absolute;bottom:-4px;left:4px;right:4px;height:3px;background:#fff3;border-radius:2px;overflow:hidden}.autoplay-progress-fill{height:100%;background:linear-gradient(90deg,#0078d7cc,#00b4ffe6);border-radius:2px;transition:width .1s linear;box-shadow:0 0 8px #00b4ff80}\n"] }]
|
|
13743
13862
|
}], propDecorators: { locale: [{
|
|
13744
13863
|
type: Input
|
|
13745
13864
|
}], availableLocales: [{
|
|
@@ -13870,11 +13989,11 @@ class LanguageModalComponent {
|
|
|
13870
13989
|
}
|
|
13871
13990
|
}
|
|
13872
13991
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LanguageModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
13873
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LanguageModalComponent, isStandalone: true, selector: "pw-language-modal", inputs: { visible: "visible", currentLocale: "currentLocale", availableLocales: "availableLocales" }, outputs: { closeModal: "closeModal", localeSelected: "localeSelected" }, ngImport: i0, template: "@if (visible) {\
|
|
13992
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: LanguageModalComponent, isStandalone: true, selector: "pw-language-modal", inputs: { visible: "visible", currentLocale: "currentLocale", availableLocales: "availableLocales" }, outputs: { closeModal: "closeModal", localeSelected: "localeSelected" }, ngImport: i0, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
13874
13993
|
}
|
|
13875
13994
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: LanguageModalComponent, decorators: [{
|
|
13876
13995
|
type: Component,
|
|
13877
|
-
args: [{ selector: 'pw-language-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\
|
|
13996
|
+
args: [{ selector: 'pw-language-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div \n class=\"language-overlay\" \n (click)=\"onBackdropClick($event)\"\n (keydown)=\"onBackdropKeydown($event)\"\n tabindex=\"-1\">\n <div class=\"language-container\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"language-title\">\n <!-- Header -->\n <div class=\"language-header\">\n <h2 id=\"language-title\" class=\"language-title\">\n {{ 'toolbar.select_language' | translate }}\n </h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'settings.close' | translate\"\n (click)=\"closeModal.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Language list -->\n <div class=\"language-content\">\n <div class=\"language-list\">\n @for (locale of availableLocales; track locale) {\n <button\n class=\"language-item\"\n [class.active]=\"locale === currentLocale\"\n type=\"button\"\n (click)=\"onSelectLocale(locale)\">\n <span class=\"language-icon\"><pw-icon name=\"lucideGlobe\" /></span>\n <span class=\"language-name\">{{ locale }}</span>\n @if (locale === currentLocale) {\n <span class=\"language-check\"><pw-icon name=\"lucideCheck\" /></span>\n }\n </button>\n }\n </div>\n </div>\n </div>\n </div>\n}\n", styles: [".language-overlay{position:fixed;inset:0;background:#000c;display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.language-container{background:linear-gradient(135deg,#1a1a1a,#2d2d2d);border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 8px 32px #00000080;max-width:400px;width:100%;max-height:80vh;display:flex;flex-direction:column;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.language-header{display:flex;align-items:center;justify-content:space-between;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.language-title{margin:0;font-size:20px;font-weight:600;color:#fff}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;color:#ffffffb3;font-size:20px;cursor:pointer;border-radius:6px;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{transform:scale(.95)}.language-content{padding:16px;overflow-y:auto;flex:1}.language-list{display:flex;flex-direction:column;gap:8px}.language-item{display:flex;align-items:center;gap:12px;padding:16px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;color:#ffffffe6;font-size:16px;cursor:pointer;transition:all .2s ease;text-align:left}.language-item:hover{background:#ffffff1a;border-color:#fff3;transform:translate(4px)}.language-item:active{transform:translate(4px) scale(.98)}.language-item.active{background:#0078d74d;border-color:#0078d780;color:#fff}.language-icon{font-size:24px;flex-shrink:0}.language-name{flex:1;font-weight:500}.language-check{font-size:18px;color:#00c864;flex-shrink:0}.language-content::-webkit-scrollbar{width:8px}.language-content::-webkit-scrollbar-track{background:#ffffff0d;border-radius:4px}.language-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.language-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}@media (max-width: 768px){.language-container{max-width:100%;max-height:60vh}.language-header{padding:20px}.language-title{font-size:18px}.language-item{padding:14px;font-size:15px}}@media (prefers-reduced-motion: reduce){.language-overlay,.language-container,.language-item,.close-btn{animation:none;transition:none}}\n"] }]
|
|
13878
13997
|
}], propDecorators: { visible: [{
|
|
13879
13998
|
type: Input
|
|
13880
13999
|
}], currentLocale: [{
|
|
@@ -14216,11 +14335,11 @@ class ThumbnailStripComponent {
|
|
|
14216
14335
|
}
|
|
14217
14336
|
}
|
|
14218
14337
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ThumbnailStripComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14219
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ThumbnailStripComponent, isStandalone: true, selector: "pw-thumbnail-strip", inputs: { chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", baseUrl: "baseUrl", visible: "visible", lockedPanels: "lockedPanels" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (visible) {\
|
|
14338
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ThumbnailStripComponent, isStandalone: true, selector: "pw-thumbnail-strip", inputs: { chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", baseUrl: "baseUrl", visible: "visible", lockedPanels: "lockedPanels" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"thumbnail-strip\">\n <!-- Close button -->\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close thumbnail strip\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n\n <!-- Left arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Scroll left\"\n [disabled]=\"!canScrollLeft()\"\n [class.disabled]=\"!canScrollLeft()\"\n (click)=\"scrollLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n\n <!-- Right arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Scroll right\"\n [disabled]=\"!canScrollRight()\"\n [class.disabled]=\"!canScrollRight()\"\n (click)=\"scrollRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n\n <!-- Scroll container -->\n <div\n class=\"strip-scroll-container\"\n #scrollContainer\n (scroll)=\"onScroll($event)\"\n (mousedown)=\"onMouseDown($event)\"\n (mousemove)=\"onMouseMove($event)\"\n (mouseup)=\"onMouseUp()\"\n (mouseleave)=\"onMouseLeave()\">\n \n <!-- Content wrapper for virtual scrolling -->\n <div\n class=\"strip-content\"\n [style.width.px]=\"getTotalWidth()\">\n \n @for (item of getVisibleItems(); track item.panelId) {\n <div\n class=\"thumbnail-wrapper\"\n [style.left.px]=\"item.index * (itemWidth + itemGap)\"\n [class.chapter-start]=\"item.chapterStart\">\n \n <!-- Chapter separator -->\n @if (item.chapterStart && item.chapterTitle) {\n <div class=\"chapter-separator\">\n <span class=\"separator-title\">{{ item.chapterTitle }}</span>\n </div>\n }\n\n <!-- Thumbnail -->\n <button\n class=\"thumbnail-item\"\n [class.current]=\"item.isCurrentPanel\"\n [class.current-chapter]=\"item.isCurrentChapter\"\n [class.locked]=\"item.isLocked\"\n type=\"button\"\n [attr.aria-label]=\"'Panel ' + item.panelId\"\n [attr.aria-current]=\"item.isCurrentPanel ? 'true' : null\"\n (click)=\"onNavigate(item)\">\n \n @if (getThumbnailUrl(item)) {\n <img\n [src]=\"getThumbnailUrl(item)\"\n [alt]=\"'Panel ' + item.panelId\"\n class=\"thumbnail-image\"\n loading=\"lazy\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <span class=\"placeholder-text\">{{ item.panelId }}</span>\n </div>\n }\n\n <!-- Lock overlay -->\n @if (item.isLocked) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n </div>\n }\n\n <!-- Current indicator -->\n @if (item.isCurrentPanel) {\n <div class=\"current-indicator\"></div>\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!-- Navigation hints -->\n <div class=\"strip-hints\">\n <kbd>\u2190</kbd> <kbd>\u2192</kbd> Navigate\n <span class=\"separator\">\u2022</span>\n <kbd>Esc</kbd> Close\n </div>\n </div>\n}\n", styles: [".thumbnail-strip{position:fixed;bottom:0;left:0;right:0;z-index:120;background:linear-gradient(to top,#000000f2,#000c);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:12px 16px 16px;box-shadow:0 -4px 16px #0006;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.close-btn{position:absolute;top:8px;right:16px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#ffffffb3;background:#ffffff1a;border:none;border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:1}.close-btn:hover{background:#fff3;color:#fff}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#ffffffe6;background:#000000b3;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:2;-webkit-user-select:none;user-select:none}.nav-arrow:hover:not(.disabled){background:#000000d9;border-color:#fff6;transform:translateY(-50%) scale(1.1)}.nav-arrow:active:not(.disabled){transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:12px}.nav-arrow-right{right:12px}.nav-arrow.disabled{opacity:.3;cursor:not-allowed}.nav-arrow:disabled{opacity:.3;cursor:not-allowed}.strip-scroll-container{position:relative;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent;padding:8px 0;cursor:grab;-webkit-user-select:none;user-select:none}.strip-scroll-container:active{cursor:grabbing}.strip-scroll-container::-webkit-scrollbar{height:6px}.strip-scroll-container::-webkit-scrollbar-track{background:#ffffff0d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb:hover{background:#fff6}.strip-content{position:relative;height:140px;min-width:100%}.thumbnail-wrapper{position:absolute;top:0;width:120px;display:flex;flex-direction:column;gap:8px}.thumbnail-wrapper.chapter-start{padding-top:32px}.chapter-separator{position:absolute;top:0;left:0;right:0;display:flex;align-items:center;gap:8px}.chapter-separator:before,.chapter-separator:after{content:\"\";flex:1;height:1px;background:#fff3}.separator-title{font-size:11px;font-weight:600;color:#fff9;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;padding:0 8px}.thumbnail-item{position:relative;width:120px;height:100px;background:#ffffff0d;border:2px solid rgba(255,255,255,.2);border-radius:8px;cursor:pointer;overflow:hidden;transition:all .2s ease;padding:0}.thumbnail-item:hover:not(.locked){border-color:#fff6;transform:translateY(-4px);box-shadow:0 4px 12px #0006}.thumbnail-item:active:not(.locked){transform:translateY(-2px)}.thumbnail-item.current-chapter{border-color:#0078d766}.thumbnail-item.current{border-color:#0078d7cc;border-width:3px;box-shadow:0 0 0 1px #0078d74d,0 4px 12px #0078d766}.thumbnail-item.current:hover{border-color:#0078d7}.thumbnail-item.locked{opacity:.6;cursor:not-allowed}.thumbnail-item.locked:hover{transform:none;border-color:#fff3;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-text{font-size:12px;font-weight:600;color:#ffffff80}.lock-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:24px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.current-indicator{position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#0078d7cc,#00b4ffcc);animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.strip-hints{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:12px;font-size:11px;color:#ffffff80}.strip-hints kbd{padding:3px 6px;font-size:10px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:3px}.separator{opacity:.5}@media (max-width: 768px){.thumbnail-strip{padding:10px 12px 12px}.thumbnail-wrapper{width:100px}.thumbnail-item{width:100px;height:80px}.close-btn{width:24px;height:24px;font-size:16px}.nav-arrow{width:32px;height:32px;font-size:24px}.nav-arrow-left{left:8px}.nav-arrow-right{right:8px}.strip-content{height:120px}.separator-title{font-size:10px}.lock-icon{font-size:20px}.strip-hints{margin-top:8px;font-size:10px}}@media (max-width: 480px){.thumbnail-wrapper{width:80px}.thumbnail-item{width:80px;height:64px}.strip-content{height:100px}}.close-btn:focus-visible,.thumbnail-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.thumbnail-strip,.thumbnail-item,.current-indicator{animation:none;transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14220
14339
|
}
|
|
14221
14340
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ThumbnailStripComponent, decorators: [{
|
|
14222
14341
|
type: Component,
|
|
14223
|
-
args: [{ selector: 'pw-thumbnail-strip', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\
|
|
14342
|
+
args: [{ selector: 'pw-thumbnail-strip', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"thumbnail-strip\">\n <!-- Close button -->\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close thumbnail strip\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n\n <!-- Left arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-left\"\n type=\"button\"\n aria-label=\"Scroll left\"\n [disabled]=\"!canScrollLeft()\"\n [class.disabled]=\"!canScrollLeft()\"\n (click)=\"scrollLeft()\">\n <pw-icon name=\"lucideChevronLeft\" />\n </button>\n\n <!-- Right arrow navigation -->\n <button\n class=\"nav-arrow nav-arrow-right\"\n type=\"button\"\n aria-label=\"Scroll right\"\n [disabled]=\"!canScrollRight()\"\n [class.disabled]=\"!canScrollRight()\"\n (click)=\"scrollRight()\">\n <pw-icon name=\"lucideChevronRight\" />\n </button>\n\n <!-- Scroll container -->\n <div\n class=\"strip-scroll-container\"\n #scrollContainer\n (scroll)=\"onScroll($event)\"\n (mousedown)=\"onMouseDown($event)\"\n (mousemove)=\"onMouseMove($event)\"\n (mouseup)=\"onMouseUp()\"\n (mouseleave)=\"onMouseLeave()\">\n \n <!-- Content wrapper for virtual scrolling -->\n <div\n class=\"strip-content\"\n [style.width.px]=\"getTotalWidth()\">\n \n @for (item of getVisibleItems(); track item.panelId) {\n <div\n class=\"thumbnail-wrapper\"\n [style.left.px]=\"item.index * (itemWidth + itemGap)\"\n [class.chapter-start]=\"item.chapterStart\">\n \n <!-- Chapter separator -->\n @if (item.chapterStart && item.chapterTitle) {\n <div class=\"chapter-separator\">\n <span class=\"separator-title\">{{ item.chapterTitle }}</span>\n </div>\n }\n\n <!-- Thumbnail -->\n <button\n class=\"thumbnail-item\"\n [class.current]=\"item.isCurrentPanel\"\n [class.current-chapter]=\"item.isCurrentChapter\"\n [class.locked]=\"item.isLocked\"\n type=\"button\"\n [attr.aria-label]=\"'Panel ' + item.panelId\"\n [attr.aria-current]=\"item.isCurrentPanel ? 'true' : null\"\n (click)=\"onNavigate(item)\">\n \n @if (getThumbnailUrl(item)) {\n <img\n [src]=\"getThumbnailUrl(item)\"\n [alt]=\"'Panel ' + item.panelId\"\n class=\"thumbnail-image\"\n loading=\"lazy\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <span class=\"placeholder-text\">{{ item.panelId }}</span>\n </div>\n }\n\n <!-- Lock overlay -->\n @if (item.isLocked) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n </div>\n }\n\n <!-- Current indicator -->\n @if (item.isCurrentPanel) {\n <div class=\"current-indicator\"></div>\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!-- Navigation hints -->\n <div class=\"strip-hints\">\n <kbd>\u2190</kbd> <kbd>\u2192</kbd> Navigate\n <span class=\"separator\">\u2022</span>\n <kbd>Esc</kbd> Close\n </div>\n </div>\n}\n", styles: [".thumbnail-strip{position:fixed;bottom:0;left:0;right:0;z-index:120;background:linear-gradient(to top,#000000f2,#000c);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:12px 16px 16px;box-shadow:0 -4px 16px #0006;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.close-btn{position:absolute;top:8px;right:16px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#ffffffb3;background:#ffffff1a;border:none;border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:1}.close-btn:hover{background:#fff3;color:#fff}.nav-arrow{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;display:flex;align-items:center;justify-content:center;font-size:32px;font-weight:700;color:#ffffffe6;background:#000000b3;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;transition:all .2s ease;z-index:2;-webkit-user-select:none;user-select:none}.nav-arrow:hover:not(.disabled){background:#000000d9;border-color:#fff6;transform:translateY(-50%) scale(1.1)}.nav-arrow:active:not(.disabled){transform:translateY(-50%) scale(.95)}.nav-arrow-left{left:12px}.nav-arrow-right{right:12px}.nav-arrow.disabled{opacity:.3;cursor:not-allowed}.nav-arrow:disabled{opacity:.3;cursor:not-allowed}.strip-scroll-container{position:relative;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.3) transparent;padding:8px 0;cursor:grab;-webkit-user-select:none;user-select:none}.strip-scroll-container:active{cursor:grabbing}.strip-scroll-container::-webkit-scrollbar{height:6px}.strip-scroll-container::-webkit-scrollbar-track{background:#ffffff0d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb{background:#ffffff4d;border-radius:3px}.strip-scroll-container::-webkit-scrollbar-thumb:hover{background:#fff6}.strip-content{position:relative;height:140px;min-width:100%}.thumbnail-wrapper{position:absolute;top:0;width:120px;display:flex;flex-direction:column;gap:8px}.thumbnail-wrapper.chapter-start{padding-top:32px}.chapter-separator{position:absolute;top:0;left:0;right:0;display:flex;align-items:center;gap:8px}.chapter-separator:before,.chapter-separator:after{content:\"\";flex:1;height:1px;background:#fff3}.separator-title{font-size:11px;font-weight:600;color:#fff9;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;padding:0 8px}.thumbnail-item{position:relative;width:120px;height:100px;background:#ffffff0d;border:2px solid rgba(255,255,255,.2);border-radius:8px;cursor:pointer;overflow:hidden;transition:all .2s ease;padding:0}.thumbnail-item:hover:not(.locked){border-color:#fff6;transform:translateY(-4px);box-shadow:0 4px 12px #0006}.thumbnail-item:active:not(.locked){transform:translateY(-2px)}.thumbnail-item.current-chapter{border-color:#0078d766}.thumbnail-item.current{border-color:#0078d7cc;border-width:3px;box-shadow:0 0 0 1px #0078d74d,0 4px 12px #0078d766}.thumbnail-item.current:hover{border-color:#0078d7}.thumbnail-item.locked{opacity:.6;cursor:not-allowed}.thumbnail-item.locked:hover{transform:none;border-color:#fff3;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-text{font-size:12px;font-weight:600;color:#ffffff80}.lock-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:24px;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.current-indicator{position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#0078d7cc,#00b4ffcc);animation:pulse 2s ease-in-out infinite}@keyframes pulse{0%,to{opacity:1}50%{opacity:.6}}.strip-hints{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:12px;font-size:11px;color:#ffffff80}.strip-hints kbd{padding:3px 6px;font-size:10px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:3px}.separator{opacity:.5}@media (max-width: 768px){.thumbnail-strip{padding:10px 12px 12px}.thumbnail-wrapper{width:100px}.thumbnail-item{width:100px;height:80px}.close-btn{width:24px;height:24px;font-size:16px}.nav-arrow{width:32px;height:32px;font-size:24px}.nav-arrow-left{left:8px}.nav-arrow-right{right:8px}.strip-content{height:120px}.separator-title{font-size:10px}.lock-icon{font-size:20px}.strip-hints{margin-top:8px;font-size:10px}}@media (max-width: 480px){.thumbnail-wrapper{width:80px}.thumbnail-item{width:80px;height:64px}.strip-content{height:100px}}.close-btn:focus-visible,.thumbnail-item:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.thumbnail-strip,.thumbnail-item,.current-indicator{animation:none;transition:none}}\n"] }]
|
|
14224
14343
|
}], propDecorators: { chapters: [{
|
|
14225
14344
|
type: Input
|
|
14226
14345
|
}], currentChapterId: [{
|
|
@@ -14540,11 +14659,11 @@ class TocOverlayComponent {
|
|
|
14540
14659
|
return this.filteredChapters[this.selectedChapterIndex];
|
|
14541
14660
|
}
|
|
14542
14661
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TocOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14543
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: TocOverlayComponent, isStandalone: true, selector: "pw-toc-overlay", inputs: { manifest: "manifest", chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\r\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\r\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\r\n <!-- Header -->\r\n <div class=\"toc-header\">\r\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toc.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"toc-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'toc.search_chapters' | translate\"\r\n [attr.aria-label]=\"'toc.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"toc-content\">\r\n <!-- Chapter list -->\r\n <div class=\"chapter-list\" role=\"list\">\r\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\r\n <div\r\n class=\"chapter-item\"\r\n [class.selected]=\"i === selectedChapterIndex\"\r\n [class.current]=\"isCurrentChapter(chapter)\"\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"selectChapter(i)\"\r\n (keydown.enter)=\"selectChapter(i)\">\r\n \r\n <!-- Chapter header -->\r\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\r\n <span class=\"chapter-number\">{{ i + 1 }}</span>\r\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\r\n @if (isCurrentChapter(chapter)) {\r\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Panel list (if chapter selected) -->\r\n @if (i === selectedChapterIndex) {\r\n <div class=\"panel-list\">\r\n <!-- If chapter has pages, group by pages -->\r\n @if (hasPages(chapter)) {\r\n @for (page of getPages(chapter); track page.id) {\r\n <div class=\"page-group\">\r\n <div class=\"page-header\">\r\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\r\n </div>\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n } @else {\r\n <!-- No pages, show all panels directly -->\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanels(chapter); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (filteredChapters.length === 0) {\r\n <div class=\"empty-state\">\r\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"toc-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\r\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14662
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: TocOverlayComponent, isStandalone: true, selector: "pw-toc-overlay", inputs: { manifest: "manifest", chapters: "chapters", currentChapterId: "currentChapterId", currentPanelId: "currentPanelId", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { navigate: "navigate", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14544
14663
|
}
|
|
14545
14664
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TocOverlayComponent, decorators: [{
|
|
14546
14665
|
type: Component,
|
|
14547
|
-
args: [{ selector: 'pw-toc-overlay', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\r\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\r\n <!-- Header -->\r\n <div class=\"toc-header\">\r\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'toc.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"toc-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'toc.search_chapters' | translate\"\r\n [attr.aria-label]=\"'toc.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"toc-content\">\r\n <!-- Chapter list -->\r\n <div class=\"chapter-list\" role=\"list\">\r\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\r\n <div\r\n class=\"chapter-item\"\r\n [class.selected]=\"i === selectedChapterIndex\"\r\n [class.current]=\"isCurrentChapter(chapter)\"\r\n role=\"listitem\"\r\n tabindex=\"0\"\r\n (click)=\"selectChapter(i)\"\r\n (keydown.enter)=\"selectChapter(i)\">\r\n \r\n <!-- Chapter header -->\r\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\r\n <span class=\"chapter-number\">{{ i + 1 }}</span>\r\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\r\n @if (isCurrentChapter(chapter)) {\r\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Panel list (if chapter selected) -->\r\n @if (i === selectedChapterIndex) {\r\n <div class=\"panel-list\">\r\n <!-- If chapter has pages, group by pages -->\r\n @if (hasPages(chapter)) {\r\n @for (page of getPages(chapter); track page.id) {\r\n <div class=\"page-group\">\r\n <div class=\"page-header\">\r\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\r\n </div>\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n } @else {\r\n <!-- No pages, show all panels directly -->\r\n <div class=\"panels-grid\">\r\n @for (panelItem of getPanels(chapter); track panelItem.id) {\r\n <button\r\n class=\"panel-card\"\r\n [class.current]=\"isCurrentPanel(panelItem.id)\"\r\n type=\"button\"\r\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\r\n \r\n @if (getThumbnailUrl(panelItem.panel)) {\r\n <div class=\"panel-thumbnail\">\r\n <img \r\n [src]=\"getThumbnailUrl(panelItem.panel)\" \r\n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\r\n loading=\"lazy\" />\r\n @if (isCurrentPanel(panelItem.id)) {\r\n <div class=\"current-overlay\">\r\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"panel-thumbnail placeholder\">\r\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\r\n </div>\r\n }\r\n \r\n <div class=\"panel-info\">\r\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (filteredChapters.length === 0) {\r\n <div class=\"empty-state\">\r\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"toc-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\r\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"] }]
|
|
14666
|
+
args: [{ selector: 'pw-toc-overlay', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"toc-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onClose()\">\n <div class=\"toc-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Table of Contents\">\n <!-- Header -->\n <div class=\"toc-header\">\n <h2 class=\"toc-title\">{{ 'toc.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'toc.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"toc-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'toc.search_chapters' | translate\"\n [attr.aria-label]=\"'toc.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Content -->\n <div class=\"toc-content\">\n <!-- Chapter list -->\n <div class=\"chapter-list\" role=\"list\">\n @for (chapter of filteredChapters; track chapter.id; let i = $index) {\n <div\n class=\"chapter-item\"\n [class.selected]=\"i === selectedChapterIndex\"\n [class.current]=\"isCurrentChapter(chapter)\"\n role=\"listitem\"\n tabindex=\"0\"\n (click)=\"selectChapter(i)\"\n (keydown.enter)=\"selectChapter(i)\">\n \n <!-- Chapter header -->\n <div class=\"chapter-header\" tabindex=\"0\" (click)=\"navigateToChapter(chapter)\" (keydown.enter)=\"navigateToChapter(chapter)\">\n <span class=\"chapter-number\">{{ i + 1 }}</span>\n <h3 class=\"chapter-title\">{{ getChapterTitle(chapter) }}</h3>\n @if (isCurrentChapter(chapter)) {\n <span class=\"current-badge\">{{ 'toc.current' | translate }}</span>\n }\n </div>\n\n <!-- Panel list (if chapter selected) -->\n @if (i === selectedChapterIndex) {\n <div class=\"panel-list\">\n <!-- If chapter has pages, group by pages -->\n @if (hasPages(chapter)) {\n @for (page of getPages(chapter); track page.id) {\n <div class=\"page-group\">\n <div class=\"page-header\">\n <span class=\"page-title\">{{ getPageTitle(page) }}</span>\n </div>\n <div class=\"panels-grid\">\n @for (panelItem of getPanelsForPage(chapter, page); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <!-- No pages, show all panels directly -->\n <div class=\"panels-grid\">\n @for (panelItem of getPanels(chapter); track panelItem.id) {\n <button\n class=\"panel-card\"\n [class.current]=\"isCurrentPanel(panelItem.id)\"\n type=\"button\"\n (click)=\"navigateToPanel(chapter, panelItem.id)\">\n \n @if (getThumbnailUrl(panelItem.panel)) {\n <div class=\"panel-thumbnail\">\n <img \n [src]=\"getThumbnailUrl(panelItem.panel)\" \n [alt]=\"getPanelTitle(panelItem.panel, panelItem.id)\"\n loading=\"lazy\" />\n @if (isCurrentPanel(panelItem.id)) {\n <div class=\"current-overlay\">\n <span class=\"current-icon\"><pw-icon name=\"lucidePlay\" /></span>\n </div>\n }\n </div>\n } @else {\n <div class=\"panel-thumbnail placeholder\">\n <span class=\"placeholder-icon\"><pw-icon name=\"lucideImage\" /></span>\n </div>\n }\n \n <div class=\"panel-info\">\n <span class=\"panel-title\">{{ getPanelTitle(panelItem.panel, panelItem.id) }}</span>\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n @if (filteredChapters.length === 0) {\n <div class=\"empty-state\">\n <p>{{ 'toc.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"toc-footer\">\n <p class=\"footer-hint\">\n <kbd>\u2191</kbd> <kbd>\u2193</kbd> {{ 'toc.keyboard_hint_nav' | translate }}\n <kbd>Enter</kbd> {{ 'toc.keyboard_hint_select' | translate }}\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'toc.keyboard_hint_close' | translate }}\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".toc-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.toc-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.toc-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.toc-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.toc-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.toc-content{flex:1;overflow-y:auto;padding:16px 0}.toc-content::-webkit-scrollbar{width:8px}.toc-content::-webkit-scrollbar-track{background:#ffffff0d}.toc-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.toc-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.chapter-list{display:flex;flex-direction:column;gap:8px;padding:0 24px}.chapter-item{border-radius:8px;transition:background .2s ease}.chapter-item.selected{background:#ffffff0d}.chapter-item.current{background:#0078d71a}.chapter-header{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;border-radius:8px;transition:all .2s ease}.chapter-header:hover{background:#ffffff1a}.chapter-number{display:flex;align-items:center;justify-content:center;width:32px;height:32px;font-size:14px;font-weight:600;color:#fff;background:#ffffff1a;border-radius:6px;flex-shrink:0}.chapter-item.current .chapter-number{background:#0078d7cc}.chapter-title{flex:1;font-size:16px;font-weight:500;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.current-badge{padding:4px 8px;font-size:11px;font-weight:600;color:#fff;background:#0078d7cc;border-radius:4px;text-transform:uppercase;letter-spacing:.5px;flex-shrink:0}.panel-list{padding:12px 16px 16px 60px;display:flex;flex-direction:column;gap:16px}.page-group{display:flex;flex-direction:column;gap:12px}.page-header{padding:8px 12px;background:#ffffff0d;border-left:3px solid rgba(0,120,215,.6);border-radius:4px}.page-title{font-size:13px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.panels-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.panel-card{display:flex;flex-direction:column;background:#ffffff0d;border:2px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease;overflow:hidden;padding:0}.panel-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-2px);box-shadow:0 4px 12px #0000004d}.panel-card:active{transform:translateY(0)}.panel-card.current{border-color:#0078d7cc;box-shadow:0 0 0 1px #0078d74d}.panel-thumbnail{position:relative;width:100%;aspect-ratio:4 / 3;background:#0000004d;overflow:hidden;display:flex;align-items:center;justify-content:center}.panel-thumbnail img{width:100%;height:100%;object-fit:cover;display:block}.panel-thumbnail.placeholder{background:linear-gradient(135deg,#0078d71a,#00b4781a)}.placeholder-icon{font-size:32px;opacity:.4}.current-overlay{position:absolute;inset:0;background:#0078d74d;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.current-icon{font-size:24px;color:#fff;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.panel-info{padding:8px 10px;display:flex;flex-direction:column;gap:4px}.panel-title{font-size:12px;font-weight:500;color:#ffffffe6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left}.empty-state{padding:40px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.toc-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:16px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 768px){.toc-container{width:95%;max-height:90vh}.toc-header{padding:16px 20px}.toc-title{font-size:18px}.toc-search{padding:12px 20px}.chapter-list{padding:0 20px}.chapter-header{padding:10px 12px;gap:10px}.chapter-number{width:28px;height:28px;font-size:13px}.chapter-title{font-size:15px}.panel-list{padding:10px 12px 12px 48px;gap:12px}.page-header{padding:6px 10px}.page-title{font-size:12px}.panels-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}.panel-thumbnail{aspect-ratio:3 / 2}.placeholder-icon{font-size:24px}.current-icon{font-size:20px}.panel-info{padding:6px 8px}.panel-title{font-size:11px}.toc-footer{padding:12px 20px}.footer-hint{font-size:11px;gap:12px}}.close-btn:focus-visible,.search-input:focus-visible,.panel-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.toc-overlay,.toc-container,.close-btn,.chapter-header,.panel-card{animation:none;transition:none}}\n"] }]
|
|
14548
14667
|
}], propDecorators: { manifest: [{
|
|
14549
14668
|
type: Input
|
|
14550
14669
|
}], chapters: [{
|
|
@@ -14568,6 +14687,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
14568
14687
|
args: ['window:keydown', ['$event']]
|
|
14569
14688
|
}] } });
|
|
14570
14689
|
|
|
14690
|
+
/**
|
|
14691
|
+
* Translate a built-in UI string. Falls back to the given English text (with
|
|
14692
|
+
* `{{param}}` interpolation) when no TranslateService is provided or the key
|
|
14693
|
+
* is not loaded, so a public component embedded without ngx-translate never
|
|
14694
|
+
* shows a raw key.
|
|
14695
|
+
*/
|
|
14696
|
+
function uiText(translate, key, english, params) {
|
|
14697
|
+
const value = translate?.instant(key, params);
|
|
14698
|
+
if (typeof value === 'string' && value !== key) {
|
|
14699
|
+
return value;
|
|
14700
|
+
}
|
|
14701
|
+
return (english ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
|
|
14702
|
+
}
|
|
14703
|
+
|
|
14571
14704
|
/**
|
|
14572
14705
|
* Paywall Overlay Component
|
|
14573
14706
|
* Displays paywall gate UI with purchase options
|
|
@@ -14594,6 +14727,10 @@ const PAYWALL_TEXT_EN = {
|
|
|
14594
14727
|
'paywall.go_back': 'Go Back',
|
|
14595
14728
|
'paywall.maybe_later': 'Maybe Later',
|
|
14596
14729
|
'paywall.secure_payment': 'Secure payment processing',
|
|
14730
|
+
'paywall.reason_subscription_required': 'This part of the story is included with a subscription.',
|
|
14731
|
+
'paywall.reason_purchase_required': 'This part of the story is available to buy.',
|
|
14732
|
+
'paywall.reason_age_verification_required': 'This content is age-restricted. Confirm your age to continue.',
|
|
14733
|
+
'paywall.reason_entitlement_required': 'This content requires an entitlement to access.',
|
|
14597
14734
|
};
|
|
14598
14735
|
/**
|
|
14599
14736
|
* Paywall Overlay Component
|
|
@@ -14672,17 +14809,24 @@ class PaywallOverlayComponent {
|
|
|
14672
14809
|
if (this.message) {
|
|
14673
14810
|
return this.getLocalizedString(this.message);
|
|
14674
14811
|
}
|
|
14812
|
+
// A known lock reason is translated; otherwise the gate's own sentence.
|
|
14813
|
+
const key = 'paywall.reason_' + this.gate?.lockReason;
|
|
14814
|
+
const translated = this.t(key);
|
|
14815
|
+
if (translated !== key) {
|
|
14816
|
+
return translated;
|
|
14817
|
+
}
|
|
14675
14818
|
if (this.gate?.reason) {
|
|
14676
14819
|
return this.gate.reason;
|
|
14677
14820
|
}
|
|
14678
14821
|
return this.t('paywall.message_default');
|
|
14679
14822
|
}
|
|
14680
14823
|
/**
|
|
14681
|
-
* Handle purchase option selection
|
|
14824
|
+
* Handle purchase option selection: `purchase` carries the product id,
|
|
14825
|
+
* `action` is 'subscribe' for a subscription option, else 'purchase'.
|
|
14682
14826
|
*/
|
|
14683
|
-
onPurchaseOption(
|
|
14684
|
-
this.purchase.emit(productId);
|
|
14685
|
-
this.action.emit('purchase');
|
|
14827
|
+
onPurchaseOption(option) {
|
|
14828
|
+
this.purchase.emit(option.productId);
|
|
14829
|
+
this.action.emit(option.type === 'subscription' ? 'subscribe' : 'purchase');
|
|
14686
14830
|
}
|
|
14687
14831
|
/**
|
|
14688
14832
|
* Handle login action
|
|
@@ -14745,18 +14889,14 @@ class PaywallOverlayComponent {
|
|
|
14745
14889
|
* Manifest-provided texts (title/message/gate.reason) bypass this.
|
|
14746
14890
|
*/
|
|
14747
14891
|
t(key, params) {
|
|
14748
|
-
|
|
14749
|
-
if (typeof value === 'string' && value !== key) {
|
|
14750
|
-
return value;
|
|
14751
|
-
}
|
|
14752
|
-
return (PAYWALL_TEXT_EN[key] ?? key).replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, name) => String(params?.[name] ?? ''));
|
|
14892
|
+
return uiText(this.translate, key, PAYWALL_TEXT_EN[key], params);
|
|
14753
14893
|
}
|
|
14754
14894
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
14755
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\
|
|
14895
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PaywallOverlayComponent, isStandalone: true, selector: "pw-paywall-overlay", inputs: { visible: "visible", gate: "gate", purchaseOptions: "purchaseOptions", locale: "locale", title: "title", message: "message", allowPreview: "allowPreview", showLogin: "showLogin" }, outputs: { action: "action", purchase: "purchase", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
14756
14896
|
}
|
|
14757
14897
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PaywallOverlayComponent, decorators: [{
|
|
14758
14898
|
type: Component,
|
|
14759
|
-
args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\
|
|
14899
|
+
args: [{ selector: 'pw-paywall-overlay', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Paywall Overlay -->\n<div class=\"paywall-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"paywall-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"getTitle()\">\n <!-- Header -->\n <div class=\"paywall-header\">\n <h2 class=\"paywall-title\">{{ getTitle() }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('paywall.close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"paywall-content\">\n <!-- Icon -->\n <div class=\"paywall-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z\"/>\n </svg>\n </div>\n\n <!-- Message -->\n <p class=\"paywall-message\">{{ getMessage() }}</p>\n\n <!-- Preview Info -->\n @if (gate?.preview && allowPreview) {\n <div class=\"preview-info\">\n @if (gate?.preview?.previewPanels) {\n <p class=\"preview-text\">\n <pw-icon name=\"lucideCheck\" /> {{ t('paywall.preview_panels', { count: gate?.preview?.previewPanels }) }}\n </p>\n }\n @if (gate?.preview?.mode) {\n <p class=\"preview-mode\">\n {{ t('paywall.preview_mode', { mode: gate?.preview?.mode }) }}\n </p>\n }\n </div>\n }\n\n <!-- Purchase Options -->\n @if (purchaseOptions.length > 0) {\n <div class=\"purchase-options\">\n <h3 class=\"options-title\">{{ t('paywall.choose_option') }}</h3>\n @for (option of purchaseOptions; track option.productId) {\n <button\n class=\"purchase-option\"\n type=\"button\"\n (click)=\"onPurchaseOption(option)\">\n <div class=\"option-header\">\n <span class=\"option-name\">{{ option.name || getPurchaseTypeLabel(option.type) }}</span>\n @if (option.price) {\n <span class=\"option-price\">{{ formatPrice(option.price.amount, option.price.currency) }}</span>\n }\n </div>\n @if (option.description) {\n <p class=\"option-description\">{{ option.description }}</p>\n }\n @if (option.name) {\n <span class=\"option-type\">{{ getPurchaseTypeLabel(option.type) }}</span>\n }\n </button>\n }\n </div>\n }\n\n <!-- Login Option -->\n @if (showLogin) {\n <button\n class=\"login-btn\"\n type=\"button\"\n (click)=\"onLogin()\">\n {{ t('paywall.sign_in') }}\n </button>\n }\n\n <!-- Actions -->\n <div class=\"paywall-actions\">\n @if (purchaseOptions.length === 0 && !showLogin) {\n <button\n class=\"action-btn primary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.go_back') }}\n </button>\n }\n @if (purchaseOptions.length > 0 || showLogin) {\n <button\n class=\"action-btn secondary\"\n type=\"button\"\n (click)=\"onDismiss()\">\n {{ t('paywall.maybe_later') }}\n </button>\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"paywall-footer\">\n <p class=\"footer-text\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n {{ t('paywall.secure_payment') }}\n </p>\n </div>\n </div>\n </div>\n</div>\n", styles: [".paywall-overlay{position:fixed;inset:0;background:#000000bf;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.paywall-overlay.visible{opacity:1;visibility:visible}.paywall-modal{background:linear-gradient(135deg,#667eea,#764ba2);border-radius:16px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #0000004d;transform:scale(.9);transition:transform .3s ease}.paywall-overlay.visible .paywall-modal{transform:scale(1)}.paywall-header{display:flex;justify-content:space-between;align-items:center;padding:24px;border-bottom:1px solid rgba(255,255,255,.1)}.paywall-title{font-size:24px;font-weight:700;color:#fff;margin:0}.close-btn{background:#fff3;border:none;color:#fff;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#ffffff4d}.paywall-content{padding:32px 24px;text-align:center}.paywall-icon{width:80px;height:80px;margin:0 auto 24px;background:#ffffff26;border-radius:50%;display:flex;align-items:center;justify-content:center}.paywall-icon svg{width:48px;height:48px;color:#fff}.paywall-message{font-size:16px;line-height:1.5;color:#ffffffe6;margin:0 0 24px}.preview-info{background:#ffffff1a;border-radius:8px;padding:16px;margin-bottom:24px}.preview-text{font-size:14px;font-weight:600;color:#fff;margin:0 0 4px}.preview-mode{font-size:12px;color:#ffffffb3;margin:0}.purchase-options{margin-bottom:24px}.options-title{font-size:14px;font-weight:600;color:#fff;text-transform:uppercase;letter-spacing:.5px;margin:0 0 16px}.purchase-option{background:#fff;border:none;border-radius:12px;padding:20px;margin-bottom:12px;width:100%;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;text-align:left}.purchase-option:hover{transform:translateY(-2px);box-shadow:0 8px 20px #0003}.purchase-option:active{transform:translateY(0)}.option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.option-name{font-size:18px;font-weight:700;color:#333}.option-price{font-size:24px;font-weight:700;color:#667eea}.option-description{font-size:14px;color:#666;margin:0 0 8px}.option-type{display:inline-block;font-size:12px;font-weight:600;color:#4350c4;background:#667eea1a;padding:4px 12px;border-radius:12px}.login-btn{background:#fff;border:none;color:#4350c4;font-size:16px;font-weight:700;padding:16px 32px;border-radius:12px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%;margin-bottom:16px}.login-btn:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0003}.paywall-actions{display:flex;gap:12px;margin-bottom:24px}.action-btn{flex:1;border:none;font-size:16px;font-weight:600;padding:14px 24px;border-radius:12px;cursor:pointer;transition:transform .2s ease,opacity .2s ease}.action-btn.primary{background:#fff;color:#667eea}.action-btn.secondary{background:#fff3;color:#fff}.action-btn:hover{transform:translateY(-2px);opacity:.9}.paywall-footer{border-top:1px solid rgba(255,255,255,.1);padding-top:16px}.footer-text{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffffb3;margin:0}.footer-text svg{width:16px;height:16px}@media (max-width: 640px){.paywall-modal{max-width:100%;border-radius:12px}.paywall-header{padding:20px}.paywall-title{font-size:20px}.paywall-content{padding:24px 20px}.option-name{font-size:16px}.option-price{font-size:20px}}\n"] }]
|
|
14760
14900
|
}], propDecorators: { visible: [{
|
|
14761
14901
|
type: Input
|
|
14762
14902
|
}], gate: [{
|
|
@@ -15404,11 +15544,11 @@ class CharacterRosterComponent {
|
|
|
15404
15544
|
}
|
|
15405
15545
|
}
|
|
15406
15546
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CharacterRosterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15407
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CharacterRosterComponent, isStandalone: true, selector: "pw-character-roster", inputs: { characters: "characters", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { characterSelect: "characterSelect", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\r\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\r\n \r\n <!-- LIST VIEW -->\r\n @if (currentView === 'list') {\r\n <!-- Header -->\r\n <div class=\"roster-header\">\r\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"roster-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'character_roster.search' | translate\"\r\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Character grid -->\r\n <div class=\"roster-content\">\r\n @if (filteredCharacters.length > 0) {\r\n <div class=\"character-grid\" role=\"list\">\r\n @for (character of filteredCharacters; track character.id) {\r\n <button\r\n class=\"character-card\"\r\n type=\"button\"\r\n role=\"listitem\"\r\n (click)=\"selectCharacter(character)\">\r\n \r\n <!-- Avatar -->\r\n <div class=\"character-avatar\">\r\n @if (getAvatarUrl(character)) {\r\n <img\r\n [src]=\"getAvatarUrl(character)\"\r\n [alt]=\"getCharacterName(character)\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getInitials(character) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Info -->\r\n <div class=\"character-info\">\r\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\r\n @if (character['role']) {\r\n <p class=\"character-role\">{{ character['role'] }}</p>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"roster-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n <!-- DETAIL VIEW -->\r\n @if (currentView === 'detail' && selectedCharacter) {\r\n <!-- Header with back button -->\r\n <div class=\"roster-header\">\r\n <button\r\n class=\"back-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\r\n (click)=\"backToList()\">\r\n {{ 'character_roster.back_to_list' | translate }}\r\n </button>\r\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_sheet.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"sheet-content\">\r\n <!-- Portrait -->\r\n <div class=\"portrait-section\">\r\n <div class=\"portrait-container\">\r\n @if (getAvatarUrl(selectedCharacter)) {\r\n <img\r\n [src]=\"getAvatarUrl(selectedCharacter)\"\r\n [alt]=\"getCharacterName(selectedCharacter)\"\r\n class=\"portrait-image\" />\r\n } @else {\r\n <div class=\"portrait-placeholder\">\r\n {{ getInitials(selectedCharacter) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Role badge -->\r\n @if (selectedCharacter['role']) {\r\n <div class=\"role-badge\">\r\n {{ selectedCharacter['role'] }}\r\n </div>\r\n }\r\n\r\n <!-- Voice sample button -->\r\n @if (hasVoiceSample(selectedCharacter)) {\r\n <button\r\n class=\"voice-btn\"\r\n [class.playing]=\"isPlaying\"\r\n type=\"button\"\r\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\r\n (click)=\"playVoiceSample(selectedCharacter)\">\r\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\r\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Bio -->\r\n <div class=\"bio-section\">\r\n <h3 class=\"bio-title\">Biography</h3>\r\n @if (getCharacterBio(selectedCharacter)) {\r\n <div class=\"bio-content\">\r\n {{ getCharacterBio(selectedCharacter) }}\r\n </div>\r\n } @else {\r\n <p class=\"bio-empty\">No biography available.</p>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"sheet-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n </div>\r\n </div>\r\n}\r\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15547
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CharacterRosterComponent, isStandalone: true, selector: "pw-character-roster", inputs: { characters: "characters", locale: "locale", baseUrl: "baseUrl", visible: "visible" }, outputs: { characterSelect: "characterSelect", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15408
15548
|
}
|
|
15409
15549
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CharacterRosterComponent, decorators: [{
|
|
15410
15550
|
type: Component,
|
|
15411
|
-
args: [{ selector: 'pw-character-roster', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\r\n \r\n <!-- LIST VIEW -->\r\n @if (currentView === 'list') {\r\n <!-- Header -->\r\n <div class=\"roster-header\">\r\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Search -->\r\n <div class=\"roster-search\">\r\n <input\r\n type=\"text\"\r\n class=\"search-input\"\r\n [placeholder]=\"'character_roster.search' | translate\"\r\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\r\n [(ngModel)]=\"searchQuery\"\r\n (ngModelChange)=\"onSearchChange()\" />\r\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\r\n </div>\r\n\r\n <!-- Character grid -->\r\n <div class=\"roster-content\">\r\n @if (filteredCharacters.length > 0) {\r\n <div class=\"character-grid\" role=\"list\">\r\n @for (character of filteredCharacters; track character.id) {\r\n <button\r\n class=\"character-card\"\r\n type=\"button\"\r\n role=\"listitem\"\r\n (click)=\"selectCharacter(character)\">\r\n \r\n <!-- Avatar -->\r\n <div class=\"character-avatar\">\r\n @if (getAvatarUrl(character)) {\r\n <img\r\n [src]=\"getAvatarUrl(character)\"\r\n [alt]=\"getCharacterName(character)\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getInitials(character) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Info -->\r\n <div class=\"character-info\">\r\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\r\n @if (character['role']) {\r\n <p class=\"character-role\">{{ character['role'] }}</p>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"roster-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n <!-- DETAIL VIEW -->\r\n @if (currentView === 'detail' && selectedCharacter) {\r\n <!-- Header with back button -->\r\n <div class=\"roster-header\">\r\n <button\r\n class=\"back-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\r\n (click)=\"backToList()\">\r\n {{ 'character_roster.back_to_list' | translate }}\r\n </button>\r\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'character_sheet.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"sheet-content\">\r\n <!-- Portrait -->\r\n <div class=\"portrait-section\">\r\n <div class=\"portrait-container\">\r\n @if (getAvatarUrl(selectedCharacter)) {\r\n <img\r\n [src]=\"getAvatarUrl(selectedCharacter)\"\r\n [alt]=\"getCharacterName(selectedCharacter)\"\r\n class=\"portrait-image\" />\r\n } @else {\r\n <div class=\"portrait-placeholder\">\r\n {{ getInitials(selectedCharacter) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Role badge -->\r\n @if (selectedCharacter['role']) {\r\n <div class=\"role-badge\">\r\n {{ selectedCharacter['role'] }}\r\n </div>\r\n }\r\n\r\n <!-- Voice sample button -->\r\n @if (hasVoiceSample(selectedCharacter)) {\r\n <button\r\n class=\"voice-btn\"\r\n [class.playing]=\"isPlaying\"\r\n type=\"button\"\r\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\r\n (click)=\"playVoiceSample(selectedCharacter)\">\r\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\r\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Bio -->\r\n <div class=\"bio-section\">\r\n <h3 class=\"bio-title\">Biography</h3>\r\n @if (getCharacterBio(selectedCharacter)) {\r\n <div class=\"bio-content\">\r\n {{ getCharacterBio(selectedCharacter) }}\r\n </div>\r\n } @else {\r\n <p class=\"bio-empty\">No biography available.</p>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"sheet-footer\">\r\n <p class=\"footer-hint\">\r\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\r\n </p>\r\n </div>\r\n }\r\n\r\n </div>\r\n </div>\r\n}\r\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"] }]
|
|
15551
|
+
args: [{ selector: 'pw-character-roster', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"roster-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"roster-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"currentView === 'list' ? 'Character Roster' : 'Character Details'\">\n \n <!-- LIST VIEW -->\n @if (currentView === 'list') {\n <!-- Header -->\n <div class=\"roster-header\">\n <h2 class=\"roster-title\">{{ 'character_roster.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Search -->\n <div class=\"roster-search\">\n <input\n type=\"text\"\n class=\"search-input\"\n [placeholder]=\"'character_roster.search' | translate\"\n [attr.aria-label]=\"'character_roster.search_label' | translate\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\" />\n <span class=\"search-icon\"><pw-icon name=\"lucideSearch\" /></span>\n </div>\n\n <!-- Character grid -->\n <div class=\"roster-content\">\n @if (filteredCharacters.length > 0) {\n <div class=\"character-grid\" role=\"list\">\n @for (character of filteredCharacters; track character.id) {\n <button\n class=\"character-card\"\n type=\"button\"\n role=\"listitem\"\n (click)=\"selectCharacter(character)\">\n \n <!-- Avatar -->\n <div class=\"character-avatar\">\n @if (getAvatarUrl(character)) {\n <img\n [src]=\"getAvatarUrl(character)\"\n [alt]=\"getCharacterName(character)\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getInitials(character) }}\n </div>\n }\n </div>\n\n <!-- Info -->\n <div class=\"character-info\">\n <h3 class=\"character-name\">{{ getCharacterName(character) }}</h3>\n @if (character['role']) {\n <p class=\"character-role\">{{ character['role'] }}</p>\n }\n </div>\n </button>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'character_roster.no_results' | translate: {query: searchQuery} }}</p>\n </div>\n }\n </div>\n\n <!-- Footer -->\n <div class=\"roster-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_roster.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n <!-- DETAIL VIEW -->\n @if (currentView === 'detail' && selectedCharacter) {\n <!-- Header with back button -->\n <div class=\"roster-header\">\n <button\n class=\"back-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_roster.back_to_list' | translate\"\n (click)=\"backToList()\">\n {{ 'character_roster.back_to_list' | translate }}\n </button>\n <h2 class=\"roster-title\">{{ getCharacterName(selectedCharacter) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'character_sheet.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"sheet-content\">\n <!-- Portrait -->\n <div class=\"portrait-section\">\n <div class=\"portrait-container\">\n @if (getAvatarUrl(selectedCharacter)) {\n <img\n [src]=\"getAvatarUrl(selectedCharacter)\"\n [alt]=\"getCharacterName(selectedCharacter)\"\n class=\"portrait-image\" />\n } @else {\n <div class=\"portrait-placeholder\">\n {{ getInitials(selectedCharacter) }}\n </div>\n }\n </div>\n\n <!-- Role badge -->\n @if (selectedCharacter['role']) {\n <div class=\"role-badge\">\n {{ selectedCharacter['role'] }}\n </div>\n }\n\n <!-- Voice sample button -->\n @if (hasVoiceSample(selectedCharacter)) {\n <button\n class=\"voice-btn\"\n [class.playing]=\"isPlaying\"\n type=\"button\"\n [attr.aria-label]=\"(isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate\"\n (click)=\"playVoiceSample(selectedCharacter)\">\n <span class=\"voice-icon\"><pw-icon [name]=\"isPlaying ? 'lucidePause' : 'lucideVolume2'\" /></span>\n <span class=\"voice-label\">{{ (isPlaying ? 'character_sheet.stop_voice' : 'character_sheet.play_voice') | translate }}</span>\n </button>\n }\n </div>\n\n <!-- Bio -->\n <div class=\"bio-section\">\n <h3 class=\"bio-title\">Biography</h3>\n @if (getCharacterBio(selectedCharacter)) {\n <div class=\"bio-content\">\n {{ getCharacterBio(selectedCharacter) }}\n </div>\n } @else {\n <p class=\"bio-empty\">No biography available.</p>\n }\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"sheet-footer\">\n <p class=\"footer-hint\">\n <kbd>{{ 'common.esc' | translate }}</kbd> {{ 'character_sheet.keyboard_hint' | translate }}\n </p>\n </div>\n }\n\n </div>\n </div>\n}\n", styles: [".roster-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.roster-container{width:90%;max-width:900px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.roster-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.roster-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.roster-search{position:relative;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.search-input{width:100%;padding:10px 40px 10px 16px;font-size:14px;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;transition:all .2s ease}.search-input::placeholder{color:#fff6}.search-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.search-icon{position:absolute;right:36px;top:50%;transform:translateY(-50%);font-size:16px;pointer-events:none;opacity:.5}.roster-content{flex:1;overflow-y:auto;padding:24px}.roster-content::-webkit-scrollbar{width:8px}.roster-content::-webkit-scrollbar-track{background:#ffffff0d}.roster-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.roster-content::-webkit-scrollbar-thumb:hover{background:#ffffff4d}.character-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px}.character-card{display:flex;flex-direction:column;align-items:center;padding:20px;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:12px;cursor:pointer;transition:all .2s ease;text-align:center}.character-card:hover{background:#ffffff14;border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.character-card:active{transform:translateY(-2px)}.character-avatar{width:120px;height:120px;margin-bottom:16px;border-radius:50%;overflow:hidden;flex-shrink:0}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px;font-weight:700;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d);border:2px solid rgba(255,255,255,.2)}.character-info{width:100%}.character-name{font-size:16px;font-weight:600;color:#ffffffe6;margin:0 0 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.character-role{font-size:13px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.roster-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-hint{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;color:#ffffff80;margin:0}.footer-hint kbd{padding:4px 8px;font-size:11px;font-family:monospace;color:#fffc;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px}@media (max-width: 1024px){.character-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}.character-card{padding:16px}.character-avatar{width:100px;height:100px;margin-bottom:12px}.avatar-placeholder{font-size:30px}.character-name{font-size:15px}.character-role{font-size:12px}}@media (max-width: 768px){.roster-container{width:95%;max-height:90vh}.roster-header{padding:16px 20px}.roster-title{font-size:18px}.roster-search{padding:12px 20px}.roster-content{padding:20px}.character-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}.character-card{padding:14px}.character-avatar{width:90px;height:90px;margin-bottom:10px}.avatar-placeholder{font-size:28px}.character-name{font-size:14px}.character-role{font-size:11px}.empty-state{padding:40px 20px}.roster-footer{padding:12px 20px}}.close-btn:focus-visible,.search-input:focus-visible,.character-card:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}.back-btn{padding:8px 16px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;white-space:nowrap}.back-btn:hover{background:#ffffff26;border-color:#ffffff4d}.back-btn:active{transform:scale(.95)}.sheet-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.portrait-section{display:flex;flex-direction:column;align-items:center;gap:16px}.portrait-container{width:200px;height:200px;border-radius:50%;overflow:hidden;border:4px solid rgba(255,255,255,.2);background:#ffffff0d}.portrait-image{width:100%;height:100%;object-fit:cover}.portrait-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:700;color:#ffffff80;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.role-badge{padding:6px 16px;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:20px;color:#fff;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.voice-btn{display:flex;align-items:center;gap:8px;padding:12px 24px;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:8px;color:#fff;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease}.voice-btn:hover{background:#ffffff26;border-color:#ffffff4d}.voice-btn.playing{background:#0078d74d;border-color:#0078d780}.voice-icon{font-size:18px}.bio-section{display:flex;flex-direction:column;gap:12px}.bio-title{margin:0;font-size:18px;font-weight:600;color:#ffffffe6;border-bottom:1px solid rgba(255,255,255,.1);padding-bottom:8px}.bio-content{color:#fffc;font-size:15px;line-height:1.6;white-space:pre-wrap}.bio-empty{color:#ffffff80;font-style:italic;margin:0}.sheet-footer{padding:16px 24px;border-top:1px solid rgba(255,255,255,.1);background:#0003}@media (prefers-reduced-motion: reduce){.roster-overlay,.roster-container,.close-btn,.back-btn,.character-card,.voice-btn{animation:none;transition:none}}\n"] }]
|
|
15412
15552
|
}], propDecorators: { characters: [{
|
|
15413
15553
|
type: Input
|
|
15414
15554
|
}], locale: [{
|
|
@@ -15673,11 +15813,11 @@ class ExtrasViewerComponent {
|
|
|
15673
15813
|
}
|
|
15674
15814
|
}
|
|
15675
15815
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ExtrasViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15676
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ExtrasViewerComponent, isStandalone: true, selector: "pw-extras-viewer", inputs: { extras: "extras", initialExtraId: "initialExtraId", locale: "locale", baseUrl: "baseUrl", visible: "visible", hasPremiumAccess: "hasPremiumAccess" }, outputs: { purchase: "purchase", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\r\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\r\n <!-- Header -->\r\n <div class=\"extras-header\">\r\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Filters -->\r\n <div class=\"extras-filters\">\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'all'\"\r\n type=\"button\"\r\n (click)=\"setFilter('all')\">\r\n {{ 'extras.all' | translate }} ({{ extras.length }})\r\n </button>\r\n @if (getExtrasByType('cover') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'cover'\"\r\n type=\"button\"\r\n (click)=\"setFilter('cover')\">\r\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('art') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'art'\"\r\n type=\"button\"\r\n (click)=\"setFilter('art')\">\r\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('bts') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'bts'\"\r\n type=\"button\"\r\n (click)=\"setFilter('bts')\">\r\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('interview') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'interview'\"\r\n type=\"button\"\r\n (click)=\"setFilter('interview')\">\r\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('other') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'other'\"\r\n type=\"button\"\r\n (click)=\"setFilter('other')\">\r\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Gallery Grid -->\r\n <div class=\"extras-content\">\r\n @if (getFilteredExtras().length > 0) {\r\n <div class=\"extras-grid\" role=\"list\">\r\n @for (extra of getFilteredExtras(); track extra.id) {\r\n <div class=\"extra-card\" role=\"listitem\">\r\n <button\r\n class=\"extra-thumbnail\"\r\n [class.locked]=\"!isAccessible(extra)\"\r\n type=\"button\"\r\n (click)=\"openExtra(extra)\">\r\n \r\n @if (getThumbnailUrl(extra)) {\r\n <img\r\n [src]=\"getThumbnailUrl(extra)\"\r\n [alt]=\"getExtraTitle(extra)\"\r\n class=\"thumbnail-image\" />\r\n } @else {\r\n <div class=\"thumbnail-placeholder\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n }\r\n\r\n <!-- Lock overlay for gated content -->\r\n @if (!isAccessible(extra)) {\r\n <div class=\"lock-overlay\">\r\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Media type badge -->\r\n <div class=\"media-badge\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n </button>\r\n\r\n <div class=\"extra-info\">\r\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\r\n @if (getExtraDescription(extra)) {\r\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'extras.no_extras' | translate }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail View -->\r\n @if (selectedExtra) {\r\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\r\n <div class=\"detail-container\">\r\n <!-- Detail Header -->\r\n <div class=\"detail-header\">\r\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close_detail' | translate\"\r\n (click)=\"closeDetail()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Detail Content -->\r\n <div class=\"detail-content\">\r\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\r\n <img\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n [alt]=\"getExtraTitle(selectedExtra)\"\r\n class=\"detail-image\" />\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'video') {\r\n <video\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-video\">\r\n {{ 'extras.video_not_supported' | translate }}\r\n </video>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'audio') {\r\n <audio\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-audio\">\r\n {{ 'extras.audio_not_supported' | translate }}\r\n </audio>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\r\n <a\r\n class=\"action-btn detail-document-link\"\r\n [href]=\"getAssetUrl(selectedExtra)\"\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\">\r\n {{ 'extras.open_document' | translate }}\r\n </a>\r\n }\r\n\r\n @if (getExtraDescription(selectedExtra)) {\r\n <div class=\"detail-description\">\r\n {{ getExtraDescription(selectedExtra) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail Footer -->\r\n <div class=\"detail-footer\">\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"closeDetail()\">\r\n {{ 'extras.close_button' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Paywall Notice (for gated content without access) -->\r\n @if (!hasPremiumAccess && hasGatedExtras()) {\r\n <div class=\"paywall-notice\">\r\n <p class=\"notice-text\">\r\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n {{ 'extras.premium_notice' | translate }}\r\n </p>\r\n <button\r\n class=\"upgrade-btn\"\r\n type=\"button\"\r\n (click)=\"onPurchase()\">\r\n {{ 'extras.upgrade_now' | translate }}\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n}\r\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15816
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ExtrasViewerComponent, isStandalone: true, selector: "pw-extras-viewer", inputs: { extras: "extras", initialExtraId: "initialExtraId", locale: "locale", baseUrl: "baseUrl", visible: "visible", hasPremiumAccess: "hasPremiumAccess" }, outputs: { purchase: "purchase", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, usesOnChanges: true, ngImport: i0, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15677
15817
|
}
|
|
15678
15818
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ExtrasViewerComponent, decorators: [{
|
|
15679
15819
|
type: Component,
|
|
15680
|
-
args: [{ selector: 'pw-extras-viewer', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\r\n <!-- Header -->\r\n <div class=\"extras-header\">\r\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Filters -->\r\n <div class=\"extras-filters\">\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'all'\"\r\n type=\"button\"\r\n (click)=\"setFilter('all')\">\r\n {{ 'extras.all' | translate }} ({{ extras.length }})\r\n </button>\r\n @if (getExtrasByType('cover') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'cover'\"\r\n type=\"button\"\r\n (click)=\"setFilter('cover')\">\r\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('art') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'art'\"\r\n type=\"button\"\r\n (click)=\"setFilter('art')\">\r\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('bts') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'bts'\"\r\n type=\"button\"\r\n (click)=\"setFilter('bts')\">\r\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('interview') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'interview'\"\r\n type=\"button\"\r\n (click)=\"setFilter('interview')\">\r\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\r\n </button>\r\n }\r\n @if (getExtrasByType('other') > 0) {\r\n <button\r\n class=\"filter-btn\"\r\n [class.active]=\"filterType === 'other'\"\r\n type=\"button\"\r\n (click)=\"setFilter('other')\">\r\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Gallery Grid -->\r\n <div class=\"extras-content\">\r\n @if (getFilteredExtras().length > 0) {\r\n <div class=\"extras-grid\" role=\"list\">\r\n @for (extra of getFilteredExtras(); track extra.id) {\r\n <div class=\"extra-card\" role=\"listitem\">\r\n <button\r\n class=\"extra-thumbnail\"\r\n [class.locked]=\"!isAccessible(extra)\"\r\n type=\"button\"\r\n (click)=\"openExtra(extra)\">\r\n \r\n @if (getThumbnailUrl(extra)) {\r\n <img\r\n [src]=\"getThumbnailUrl(extra)\"\r\n [alt]=\"getExtraTitle(extra)\"\r\n class=\"thumbnail-image\" />\r\n } @else {\r\n <div class=\"thumbnail-placeholder\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n }\r\n\r\n <!-- Lock overlay for gated content -->\r\n @if (!isAccessible(extra)) {\r\n <div class=\"lock-overlay\">\r\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Media type badge -->\r\n <div class=\"media-badge\">\r\n <pw-icon [name]=\"getMediaIcon(extra)\" />\r\n </div>\r\n </button>\r\n\r\n <div class=\"extra-info\">\r\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\r\n @if (getExtraDescription(extra)) {\r\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <div class=\"empty-state\">\r\n <p>{{ 'extras.no_extras' | translate }}</p>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail View -->\r\n @if (selectedExtra) {\r\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\r\n <div class=\"detail-container\">\r\n <!-- Detail Header -->\r\n <div class=\"detail-header\">\r\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'extras.close_detail' | translate\"\r\n (click)=\"closeDetail()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Detail Content -->\r\n <div class=\"detail-content\">\r\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\r\n <img\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n [alt]=\"getExtraTitle(selectedExtra)\"\r\n class=\"detail-image\" />\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'video') {\r\n <video\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-video\">\r\n {{ 'extras.video_not_supported' | translate }}\r\n </video>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'audio') {\r\n <audio\r\n [src]=\"getAssetUrl(selectedExtra)\"\r\n controls\r\n class=\"detail-audio\">\r\n {{ 'extras.audio_not_supported' | translate }}\r\n </audio>\r\n }\r\n\r\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\r\n <a\r\n class=\"action-btn detail-document-link\"\r\n [href]=\"getAssetUrl(selectedExtra)\"\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\">\r\n {{ 'extras.open_document' | translate }}\r\n </a>\r\n }\r\n\r\n @if (getExtraDescription(selectedExtra)) {\r\n <div class=\"detail-description\">\r\n {{ getExtraDescription(selectedExtra) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Detail Footer -->\r\n <div class=\"detail-footer\">\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"closeDetail()\">\r\n {{ 'extras.close_button' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Paywall Notice (for gated content without access) -->\r\n @if (!hasPremiumAccess && hasGatedExtras()) {\r\n <div class=\"paywall-notice\">\r\n <p class=\"notice-text\">\r\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\r\n {{ 'extras.premium_notice' | translate }}\r\n </p>\r\n <button\r\n class=\"upgrade-btn\"\r\n type=\"button\"\r\n (click)=\"onPurchase()\">\r\n {{ 'extras.upgrade_now' | translate }}\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n}\r\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"] }]
|
|
15820
|
+
args: [{ selector: 'pw-extras-viewer', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"extras-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"extras-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Extras Viewer\">\n <!-- Header -->\n <div class=\"extras-header\">\n <h2 class=\"extras-title\">{{ 'extras.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Filters -->\n <div class=\"extras-filters\">\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'all'\"\n type=\"button\"\n (click)=\"setFilter('all')\">\n {{ 'extras.all' | translate }} ({{ extras.length }})\n </button>\n @if (getExtrasByType('cover') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'cover'\"\n type=\"button\"\n (click)=\"setFilter('cover')\">\n {{ getTypeName('cover') }} ({{ getExtrasByType('cover') }})\n </button>\n }\n @if (getExtrasByType('art') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'art'\"\n type=\"button\"\n (click)=\"setFilter('art')\">\n {{ getTypeName('art') }} ({{ getExtrasByType('art') }})\n </button>\n }\n @if (getExtrasByType('bts') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'bts'\"\n type=\"button\"\n (click)=\"setFilter('bts')\">\n {{ getTypeName('bts') }} ({{ getExtrasByType('bts') }})\n </button>\n }\n @if (getExtrasByType('interview') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'interview'\"\n type=\"button\"\n (click)=\"setFilter('interview')\">\n {{ getTypeName('interview') }} ({{ getExtrasByType('interview') }})\n </button>\n }\n @if (getExtrasByType('other') > 0) {\n <button\n class=\"filter-btn\"\n [class.active]=\"filterType === 'other'\"\n type=\"button\"\n (click)=\"setFilter('other')\">\n {{ getTypeName('other') }} ({{ getExtrasByType('other') }})\n </button>\n }\n </div>\n\n <!-- Gallery Grid -->\n <div class=\"extras-content\">\n @if (getFilteredExtras().length > 0) {\n <div class=\"extras-grid\" role=\"list\">\n @for (extra of getFilteredExtras(); track extra.id) {\n <div class=\"extra-card\" role=\"listitem\">\n <button\n class=\"extra-thumbnail\"\n [class.locked]=\"!isAccessible(extra)\"\n type=\"button\"\n (click)=\"openExtra(extra)\">\n \n @if (getThumbnailUrl(extra)) {\n <img\n [src]=\"getThumbnailUrl(extra)\"\n [alt]=\"getExtraTitle(extra)\"\n class=\"thumbnail-image\" />\n } @else {\n <div class=\"thumbnail-placeholder\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n }\n\n <!-- Lock overlay for gated content -->\n @if (!isAccessible(extra)) {\n <div class=\"lock-overlay\">\n <span class=\"lock-icon\"><pw-icon name=\"lucideLock\" /></span>\n <span class=\"lock-text\">{{ 'extras.locked_premium' | translate }}</span>\n </div>\n }\n\n <!-- Media type badge -->\n <div class=\"media-badge\">\n <pw-icon [name]=\"getMediaIcon(extra)\" />\n </div>\n </button>\n\n <div class=\"extra-info\">\n <h3 class=\"extra-title\">{{ getExtraTitle(extra) }}</h3>\n @if (getExtraDescription(extra)) {\n <p class=\"extra-description\">{{ getExtraDescription(extra) }}</p>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <div class=\"empty-state\">\n <p>{{ 'extras.no_extras' | translate }}</p>\n </div>\n }\n </div>\n\n <!-- Detail View -->\n @if (selectedExtra) {\n <div class=\"detail-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); closeDetail()\">\n <div class=\"detail-container\">\n <!-- Detail Header -->\n <div class=\"detail-header\">\n <h3 class=\"detail-title\">{{ getExtraTitle(selectedExtra) }}</h3>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'extras.close_detail' | translate\"\n (click)=\"closeDetail()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Detail Content -->\n <div class=\"detail-content\">\n @if ((selectedExtra.mediaType === 'image' || !selectedExtra.mediaType) && getAssetUrl(selectedExtra)) {\n <img\n [src]=\"getAssetUrl(selectedExtra)\"\n [alt]=\"getExtraTitle(selectedExtra)\"\n class=\"detail-image\" />\n }\n\n @if (selectedExtra.mediaType === 'video') {\n <video\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-video\">\n {{ 'extras.video_not_supported' | translate }}\n </video>\n }\n\n @if (selectedExtra.mediaType === 'audio') {\n <audio\n [src]=\"getAssetUrl(selectedExtra)\"\n controls\n class=\"detail-audio\">\n {{ 'extras.audio_not_supported' | translate }}\n </audio>\n }\n\n @if (selectedExtra.mediaType === 'document' && getAssetUrl(selectedExtra)) {\n <a\n class=\"action-btn detail-document-link\"\n [href]=\"getAssetUrl(selectedExtra)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\">\n {{ 'extras.open_document' | translate }}\n </a>\n }\n\n @if (getExtraDescription(selectedExtra)) {\n <div class=\"detail-description\">\n {{ getExtraDescription(selectedExtra) }}\n </div>\n }\n </div>\n\n <!-- Detail Footer -->\n <div class=\"detail-footer\">\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"closeDetail()\">\n {{ 'extras.close_button' | translate }}\n </button>\n </div>\n </div>\n </div>\n }\n\n <!-- Paywall Notice (for gated content without access) -->\n @if (!hasPremiumAccess && hasGatedExtras()) {\n <div class=\"paywall-notice\">\n <p class=\"notice-text\">\n <span class=\"notice-icon\"><pw-icon name=\"lucideLock\" /></span>\n {{ 'extras.premium_notice' | translate }}\n </p>\n <button\n class=\"upgrade-btn\"\n type=\"button\"\n (click)=\"onPurchase()\">\n {{ 'extras.upgrade_now' | translate }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n", styles: [".extras-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.extras-container{width:90%;max-width:1000px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.extras-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.extras-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.extras-filters{display:flex;gap:8px;padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);overflow-x:auto;flex-shrink:0;scrollbar-width:thin}.extras-filters::-webkit-scrollbar{height:4px}.extras-filters::-webkit-scrollbar-thumb{background:#fff3;border-radius:2px}.filter-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffb3;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:20px;cursor:pointer;transition:all .2s ease;white-space:nowrap;flex-shrink:0}.filter-btn:hover{background:#ffffff1a;border-color:#fff3}.filter-btn.active{color:#fff;background:#0078d780;border-color:#0078d7b3}.extras-content{flex:1;overflow-y:auto;padding:24px}.extras-content::-webkit-scrollbar{width:8px}.extras-content::-webkit-scrollbar-track{background:#ffffff0d}.extras-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.extras-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}.extra-card{display:flex;flex-direction:column;gap:12px}.extra-thumbnail{position:relative;aspect-ratio:3 / 4;background:#ffffff08;border:1px solid rgba(255,255,255,.1);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .2s ease;padding:0}.extra-thumbnail:hover{border-color:#fff3;transform:translateY(-4px);box-shadow:0 4px 12px #0000004d}.extra-thumbnail.locked{cursor:not-allowed}.extra-thumbnail.locked:hover{transform:none;box-shadow:none}.thumbnail-image{width:100%;height:100%;object-fit:cover;display:block}.thumbnail-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:48px;background:linear-gradient(135deg,#0078d71a,#00b4781a)}.lock-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.lock-icon{font-size:32px}.lock-text{font-size:12px;font-weight:600;color:#ffffffe6;text-transform:uppercase;letter-spacing:.5px}.media-badge{position:absolute;top:8px;right:8px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;font-size:14px;background:#0009;border-radius:6px;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.extra-info{display:flex;flex-direction:column;gap:4px}.extra-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.extra-description{font-size:12px;color:#fff9;margin:0;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.empty-state{padding:60px 24px;text-align:center}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.detail-overlay{position:fixed;inset:0;z-index:10;display:flex;align-items:center;justify-content:center;background:#000000e6;animation:fadeIn .2s ease}.detail-container{width:90%;max-width:800px;max-height:85vh;background:#1a1a1a;border-radius:12px;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}.detail-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.detail-title{font-size:18px;font-weight:600;color:#fff;margin:0}.detail-content{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;align-items:center}.detail-image,.detail-video{max-width:100%;max-height:60vh;border-radius:8px}.detail-audio{width:100%;max-width:500px}.detail-description{font-size:14px;line-height:1.6;color:#fffc;white-space:pre-wrap;text-align:center;max-width:600px}.detail-footer{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-btn{padding:10px 24px;font-size:14px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.action-btn:hover{background:#ffffff26;border-color:#ffffff4d}.paywall-notice{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 24px;background:#ffa5001a;border-top:1px solid rgba(255,165,0,.3);flex-shrink:0}.notice-text{display:flex;align-items:center;gap:8px;font-size:13px;color:#ffffffe6;margin:0}.notice-icon{font-size:16px}.upgrade-btn{padding:8px 20px;font-size:13px;font-weight:600;color:#fff;background:#ffa500cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.upgrade-btn:hover{background:#ffa500e6;transform:translateY(-1px);box-shadow:0 2px 8px #ffa5004d}@media (max-width: 768px){.extras-container{width:95%}.extras-header{padding:16px 20px}.extras-title{font-size:18px}.extras-filters{padding:12px 20px}.extras-content{padding:20px}.extras-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}.extra-title{font-size:13px}.extra-description{font-size:11px}.detail-container{width:95%}.detail-header{padding:14px 18px}.detail-title{font-size:16px}.detail-content{padding:16px}.paywall-notice{flex-direction:column;align-items:flex-start;gap:12px}.upgrade-btn{width:100%}}.close-btn:focus-visible,.filter-btn:focus-visible,.extra-thumbnail:focus-visible,.action-btn:focus-visible,.upgrade-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.extras-overlay,.extras-container,.detail-overlay,.detail-container,.close-btn,.filter-btn,.extra-thumbnail,.action-btn,.upgrade-btn{animation:none;transition:none}}\n"] }]
|
|
15681
15821
|
}], propDecorators: { extras: [{
|
|
15682
15822
|
type: Input
|
|
15683
15823
|
}], initialExtraId: [{
|
|
@@ -15741,11 +15881,11 @@ class ActionModalComponent {
|
|
|
15741
15881
|
}
|
|
15742
15882
|
}
|
|
15743
15883
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ActionModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15744
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ActionModalComponent, isStandalone: true, selector: "pw-action-modal", inputs: { visible: "visible", locale: "locale", title: "title", content: "content" }, outputs: { close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\
|
|
15884
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ActionModalComponent, isStandalone: true, selector: "pw-action-modal", inputs: { visible: "visible", locale: "locale", title: "title", content: "content" }, outputs: { close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"action-modal-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); close.emit()\">\n <div class=\"action-modal-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"resolved(title)\">\n <div class=\"action-modal-header\">\n <h2 class=\"action-modal-title\">{{ resolved(title) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n <!-- Plain text only: manifest content is untrusted, never innerHTML -->\n <div class=\"action-modal-content\">{{ resolved(content) }}</div>\n </div>\n </div>\n}\n", styles: [".action-modal-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.action-modal-container{width:90%;max-width:480px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.action-modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-modal-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.action-modal-content{padding:20px 24px;color:#ffffffd9;font-size:15px;line-height:1.6;overflow-y:auto;white-space:pre-line}@media (prefers-reduced-motion: reduce){.action-modal-overlay,.action-modal-container{animation:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15745
15885
|
}
|
|
15746
15886
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ActionModalComponent, decorators: [{
|
|
15747
15887
|
type: Component,
|
|
15748
|
-
args: [{ selector: 'pw-action-modal', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\
|
|
15888
|
+
args: [{ selector: 'pw-action-modal', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"action-modal-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); close.emit()\">\n <div class=\"action-modal-container\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"resolved(title)\">\n <div class=\"action-modal-header\">\n <h2 class=\"action-modal-title\">{{ resolved(title) }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n aria-label=\"Close\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n <!-- Plain text only: manifest content is untrusted, never innerHTML -->\n <div class=\"action-modal-content\">{{ resolved(content) }}</div>\n </div>\n </div>\n}\n", styles: [".action-modal-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.action-modal-container{width:90%;max-width:480px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.action-modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.action-modal-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.action-modal-content{padding:20px 24px;color:#ffffffd9;font-size:15px;line-height:1.6;overflow-y:auto;white-space:pre-line}@media (prefers-reduced-motion: reduce){.action-modal-overlay,.action-modal-container{animation:none}}\n"] }]
|
|
15749
15889
|
}], propDecorators: { visible: [{
|
|
15750
15890
|
type: Input
|
|
15751
15891
|
}], locale: [{
|
|
@@ -15765,12 +15905,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15765
15905
|
* Age Gate Component
|
|
15766
15906
|
* Displays age verification UI for restricted content
|
|
15767
15907
|
*/
|
|
15908
|
+
/**
|
|
15909
|
+
* Built-in English UI strings (keys under `age_gate.`), used when no
|
|
15910
|
+
* TranslateService is provided or a key is not loaded. Must match the
|
|
15911
|
+
* "age_gate" section of assets/i18n/en.json.
|
|
15912
|
+
*/
|
|
15913
|
+
const AGE_GATE_TEXT_EN = {
|
|
15914
|
+
title: 'Age Verification Required',
|
|
15915
|
+
close: 'Close',
|
|
15916
|
+
warning: 'This content is restricted to users {{age}} years of age or older.',
|
|
15917
|
+
prompt: 'Please enter your birth date:',
|
|
15918
|
+
month: 'Month',
|
|
15919
|
+
day: 'Day',
|
|
15920
|
+
year: 'Year',
|
|
15921
|
+
verify: 'Verify Age',
|
|
15922
|
+
privacy: 'Your information is private and will not be stored.',
|
|
15923
|
+
error_incomplete: 'Please enter your complete birth date.',
|
|
15924
|
+
error_month: 'Please enter a valid month (1-12).',
|
|
15925
|
+
error_day: 'Please enter a valid day (1-{{max}}).',
|
|
15926
|
+
error_year: 'Please enter a valid year (1900-{{max}}).',
|
|
15927
|
+
error_too_young: 'You must be at least {{age}} years old to access this content.',
|
|
15928
|
+
};
|
|
15768
15929
|
/**
|
|
15769
15930
|
* Age Gate Component
|
|
15770
15931
|
* Modal overlay for age verification
|
|
15771
15932
|
*/
|
|
15772
15933
|
class AgeGateComponent {
|
|
15773
15934
|
constructor() {
|
|
15935
|
+
/** Optional: the gate is a public component and works (in English) without ngx-translate. */
|
|
15936
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
15774
15937
|
/**
|
|
15775
15938
|
* Visibility state
|
|
15776
15939
|
*/
|
|
@@ -15821,7 +15984,11 @@ class AgeGateComponent {
|
|
|
15821
15984
|
if (this.warningMessage) {
|
|
15822
15985
|
return this.warningMessage;
|
|
15823
15986
|
}
|
|
15824
|
-
return
|
|
15987
|
+
return this.t('warning', { age: this.minimumAge });
|
|
15988
|
+
}
|
|
15989
|
+
/** Translate an `age_gate.*` UI string (English fallback). */
|
|
15990
|
+
t(key, params) {
|
|
15991
|
+
return uiText(this.translate, 'age_gate.' + key, AGE_GATE_TEXT_EN[key], params);
|
|
15825
15992
|
}
|
|
15826
15993
|
/**
|
|
15827
15994
|
* Handle verification submit
|
|
@@ -15830,7 +15997,7 @@ class AgeGateComponent {
|
|
|
15830
15997
|
this.errorMessage = '';
|
|
15831
15998
|
// Validate inputs
|
|
15832
15999
|
if (!this.birthMonth || !this.birthDay || !this.birthYear) {
|
|
15833
|
-
this.errorMessage =
|
|
16000
|
+
this.errorMessage = this.t('error_incomplete');
|
|
15834
16001
|
return;
|
|
15835
16002
|
}
|
|
15836
16003
|
const month = parseInt(this.birthMonth, 10);
|
|
@@ -15838,30 +16005,30 @@ class AgeGateComponent {
|
|
|
15838
16005
|
const year = parseInt(this.birthYear, 10);
|
|
15839
16006
|
// Validate ranges
|
|
15840
16007
|
if (month < 1 || month > 12) {
|
|
15841
|
-
this.errorMessage = '
|
|
16008
|
+
this.errorMessage = this.t('error_month');
|
|
15842
16009
|
return;
|
|
15843
16010
|
}
|
|
15844
16011
|
if (day < 1 || day > 31) {
|
|
15845
|
-
this.errorMessage = '
|
|
16012
|
+
this.errorMessage = this.t('error_day', { max: 31 });
|
|
15846
16013
|
return;
|
|
15847
16014
|
}
|
|
15848
16015
|
const currentYear = new Date().getFullYear();
|
|
15849
16016
|
if (year < 1900 || year > currentYear) {
|
|
15850
|
-
this.errorMessage =
|
|
16017
|
+
this.errorMessage = this.t('error_year', { max: currentYear });
|
|
15851
16018
|
return;
|
|
15852
16019
|
}
|
|
15853
16020
|
// Reject calendar-impossible dates (31 Apr, 30 Feb, 29 Feb in non-leap
|
|
15854
16021
|
// years): new Date() would silently roll them over into the next month.
|
|
15855
16022
|
const daysInMonth = new Date(year, month, 0).getDate();
|
|
15856
16023
|
if (day > daysInMonth) {
|
|
15857
|
-
this.errorMessage =
|
|
16024
|
+
this.errorMessage = this.t('error_day', { max: daysInMonth });
|
|
15858
16025
|
return;
|
|
15859
16026
|
}
|
|
15860
16027
|
// Calculate age
|
|
15861
16028
|
const birthDate = new Date(year, month - 1, day);
|
|
15862
16029
|
const age = this.calculateAge(birthDate);
|
|
15863
16030
|
if (age < this.minimumAge) {
|
|
15864
|
-
this.errorMessage =
|
|
16031
|
+
this.errorMessage = this.t('error_too_young', { age: this.minimumAge });
|
|
15865
16032
|
this.verify.emit({
|
|
15866
16033
|
verified: false,
|
|
15867
16034
|
age,
|
|
@@ -15906,17 +16073,26 @@ class AgeGateComponent {
|
|
|
15906
16073
|
return age;
|
|
15907
16074
|
}
|
|
15908
16075
|
/**
|
|
15909
|
-
*
|
|
16076
|
+
* Month options, named in the current locale (Intl; cached per locale).
|
|
15910
16077
|
*/
|
|
15911
16078
|
getMonthOptions() {
|
|
15912
|
-
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15917
|
-
|
|
15918
|
-
|
|
15919
|
-
|
|
16079
|
+
if (this.monthCache?.locale !== this.locale) {
|
|
16080
|
+
let format;
|
|
16081
|
+
try {
|
|
16082
|
+
format = new Intl.DateTimeFormat(this.locale, { month: 'long', timeZone: 'UTC' });
|
|
16083
|
+
}
|
|
16084
|
+
catch {
|
|
16085
|
+
format = new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' });
|
|
16086
|
+
}
|
|
16087
|
+
this.monthCache = {
|
|
16088
|
+
locale: this.locale,
|
|
16089
|
+
options: Array.from({ length: 12 }, (_, index) => ({
|
|
16090
|
+
value: String(index + 1),
|
|
16091
|
+
label: format.format(Date.UTC(2000, index, 1)),
|
|
16092
|
+
})),
|
|
16093
|
+
};
|
|
16094
|
+
}
|
|
16095
|
+
return this.monthCache.options;
|
|
15920
16096
|
}
|
|
15921
16097
|
/**
|
|
15922
16098
|
* Get day options
|
|
@@ -15936,11 +16112,11 @@ class AgeGateComponent {
|
|
|
15936
16112
|
return years;
|
|
15937
16113
|
}
|
|
15938
16114
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15939
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\
|
|
16115
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: AgeGateComponent, isStandalone: true, selector: "pw-age-gate", inputs: { visible: "visible", minimumAge: "minimumAge", locale: "locale", warningMessage: "warningMessage", allowDismiss: "allowDismiss" }, outputs: { verify: "verify", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1$2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15940
16116
|
}
|
|
15941
16117
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: AgeGateComponent, decorators: [{
|
|
15942
16118
|
type: Component,
|
|
15943
|
-
args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\
|
|
16119
|
+
args: [{ selector: 'pw-age-gate', imports: [FormsModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Age Gate Overlay -->\n<div class=\"age-gate-overlay\" [class.visible]=\"visible\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown.escape)=\"$event.stopPropagation(); onDismiss()\">\n <div class=\"age-gate-modal\" role=\"dialog\" aria-modal=\"true\" [attr.aria-label]=\"t('title')\">\n <!-- Header -->\n <div class=\"age-gate-header\">\n <div class=\"warning-icon\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/>\n </svg>\n </div>\n <h2 class=\"age-gate-title\">{{ t('title') }}</h2>\n @if (allowDismiss) {\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"onDismiss()\">\n <pw-icon name=\"lucideX\" />\n </button>\n }\n </div>\n\n <!-- Content -->\n <div class=\"age-gate-content\">\n <!-- Warning Message -->\n <p class=\"warning-message\">{{ getWarningMessage() }}</p>\n\n <!-- Birth Date Form -->\n <form class=\"birth-date-form\" (ngSubmit)=\"onSubmit()\">\n <div class=\"form-label\">{{ t('prompt') }}</div>\n\n <div class=\"date-inputs\">\n <!-- Month -->\n <div class=\"input-group\">\n <label for=\"birth-month\" class=\"input-label\">{{ t('month') }}</label>\n <select\n id=\"birth-month\"\n class=\"date-input\"\n [(ngModel)]=\"birthMonth\"\n name=\"birthMonth\"\n required>\n <option value=\"\">--</option>\n @for (month of getMonthOptions(); track month.value) {\n <option [value]=\"month.value\">{{ month.label }}</option>\n }\n </select>\n </div>\n\n <!-- Day -->\n <div class=\"input-group\">\n <label for=\"birth-day\" class=\"input-label\">{{ t('day') }}</label>\n <select\n id=\"birth-day\"\n class=\"date-input\"\n [(ngModel)]=\"birthDay\"\n name=\"birthDay\"\n required>\n <option value=\"\">--</option>\n @for (day of getDayOptions(); track day) {\n <option [value]=\"day\">{{ day }}</option>\n }\n </select>\n </div>\n\n <!-- Year -->\n <div class=\"input-group\">\n <label for=\"birth-year\" class=\"input-label\">{{ t('year') }}</label>\n <select\n id=\"birth-year\"\n class=\"date-input\"\n [(ngModel)]=\"birthYear\"\n name=\"birthYear\"\n required>\n <option value=\"\">----</option>\n @for (year of getYearOptions(); track year) {\n <option [value]=\"year\">{{ year }}</option>\n }\n </select>\n </div>\n </div>\n\n <!-- Error Message -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n {{ errorMessage }}\n </div>\n }\n\n <!-- Submit Button -->\n <button\n type=\"submit\"\n class=\"verify-btn\">\n {{ t('verify') }}\n </button>\n </form>\n\n <!-- Privacy Notice -->\n <div class=\"privacy-notice\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z\"/>\n </svg>\n <span>{{ t('privacy') }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".age-gate-overlay{position:fixed;inset:0;background:#000000d9;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;padding:20px}.age-gate-overlay.visible{opacity:1;visibility:visible}.age-gate-modal{background:#1a1a1a;border:2px solid #ffa500;border-radius:12px;max-width:500px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px #ffa5004d;transform:scale(.9);transition:transform .3s ease}.age-gate-overlay.visible .age-gate-modal{transform:scale(1)}.age-gate-header{position:relative;padding:32px 24px 24px;text-align:center;border-bottom:1px solid rgba(255,165,0,.2)}.warning-icon{width:64px;height:64px;margin:0 auto 16px;background:#ffa50026;border-radius:50%;display:flex;align-items:center;justify-content:center}.warning-icon svg{width:36px;height:36px;color:orange}.age-gate-title{font-size:24px;font-weight:700;color:orange;margin:0}.close-btn{position:absolute;top:16px;right:16px;background:#ffffff1a;border:none;color:orange;font-size:24px;width:36px;height:36px;border-radius:50%;cursor:pointer;transition:background .2s ease;display:flex;align-items:center;justify-content:center;padding:0}.close-btn:hover{background:#fff3}.age-gate-content{padding:32px 24px}.warning-message{font-size:16px;line-height:1.6;color:#ffffffe6;text-align:center;margin:0 0 32px}.birth-date-form{margin-bottom:24px}.form-label{font-size:14px;font-weight:600;color:#fffc;text-align:center;margin-bottom:16px}.date-inputs{display:grid;grid-template-columns:2fr 1fr 1.5fr;gap:12px;margin-bottom:16px}.input-group{display:flex;flex-direction:column}.input-label{font-size:12px;font-weight:600;color:#fff9;margin-bottom:8px;text-transform:uppercase;letter-spacing:.5px}.date-input{background:#ffffff1a;border:1px solid rgba(255,165,0,.3);color:#fff;font-size:16px;padding:12px;border-radius:8px;transition:border-color .2s ease,background .2s ease}.date-input:focus{outline:none;border-color:orange;background:#ffffff26}.date-input option{background:#1a1a1a;color:#fff}.error-message{background:#dc354526;border:1px solid #dc3545;border-radius:8px;color:#ff6b7a;font-size:14px;padding:12px;margin-bottom:16px;text-align:center}.verify-btn{background:linear-gradient(135deg,orange,#ff8c00);border:none;color:#fff;font-size:16px;font-weight:700;padding:16px 32px;border-radius:8px;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease;width:100%}.verify-btn:hover{transform:translateY(-2px);box-shadow:0 8px 20px #ffa5004d}.verify-btn:active{transform:translateY(0)}.verify-btn:disabled{opacity:.5;cursor:not-allowed}.privacy-notice{display:flex;align-items:center;justify-content:center;gap:8px;padding-top:24px;border-top:1px solid rgba(255,255,255,.1)}.privacy-notice svg{width:16px;height:16px;color:#ffffff80;flex-shrink:0}.privacy-notice span{font-size:12px;color:#ffffff80}@media (max-width: 640px){.age-gate-modal{max-width:100%;border-radius:8px}.age-gate-header{padding:24px 20px 20px}.age-gate-title{font-size:20px}.age-gate-content{padding:24px 20px}.date-inputs{grid-template-columns:1fr;gap:16px}}\n"] }]
|
|
15944
16120
|
}], propDecorators: { visible: [{
|
|
15945
16121
|
type: Input
|
|
15946
16122
|
}], minimumAge: [{
|
|
@@ -15967,8 +16143,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
15967
16143
|
* priority. Opened from the toolbar's "Choices" button; follows the shell's
|
|
15968
16144
|
* modal contract (visible input + close output).
|
|
15969
16145
|
*/
|
|
16146
|
+
/** English fallback for the `branch_chooser.*` keys (see assets/i18n/en.json). */
|
|
16147
|
+
const BRANCH_TEXT_EN = {
|
|
16148
|
+
title: 'Choose your path',
|
|
16149
|
+
close: 'Close',
|
|
16150
|
+
option: 'Option {{n}}',
|
|
16151
|
+
};
|
|
15970
16152
|
class BranchChooserComponent {
|
|
15971
16153
|
constructor() {
|
|
16154
|
+
this.translate = inject(TranslateService, { optional: true });
|
|
15972
16155
|
/** Visible state */
|
|
15973
16156
|
this.visible = false;
|
|
15974
16157
|
/** Current locale (labels are already resolved by the shell). */
|
|
@@ -15980,6 +16163,10 @@ class BranchChooserComponent {
|
|
|
15980
16163
|
/** Close requested without choosing. */
|
|
15981
16164
|
this.close = new EventEmitter();
|
|
15982
16165
|
}
|
|
16166
|
+
/** Translate a `branch_chooser.*` UI string (English fallback). */
|
|
16167
|
+
t(key, params) {
|
|
16168
|
+
return uiText(this.translate, 'branch_chooser.' + key, BRANCH_TEXT_EN[key], params);
|
|
16169
|
+
}
|
|
15983
16170
|
onEscape() {
|
|
15984
16171
|
if (this.visible) {
|
|
15985
16172
|
this.close.emit();
|
|
@@ -15994,11 +16181,11 @@ class BranchChooserComponent {
|
|
|
15994
16181
|
return choice.index;
|
|
15995
16182
|
}
|
|
15996
16183
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
15997
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">
|
|
16184
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: BranchChooserComponent, isStandalone: true, selector: "pw-branch-chooser", inputs: { visible: "visible", locale: "locale", choices: "choices" }, outputs: { choose: "choose", close: "close" }, host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"], dependencies: [{ kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
15998
16185
|
}
|
|
15999
16186
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: BranchChooserComponent, decorators: [{
|
|
16000
16187
|
type: Component,
|
|
16001
|
-
args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">
|
|
16188
|
+
args: [{ selector: 'pw-branch-chooser', imports: [PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div\n class=\"branch-chooser-overlay\"\n tabindex=\"-1\"\n (click)=\"onBackdropClick($event)\"\n (keydown.escape)=\"close.emit()\">\n <div\n class=\"branch-chooser-container\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"pw-branch-chooser-title\">\n <div class=\"branch-chooser-header\">\n <h2 id=\"pw-branch-chooser-title\" class=\"branch-chooser-title\">{{ t('title') }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"t('close')\"\n (click)=\"close.emit()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <ul class=\"branch-chooser-list\">\n @for (choice of choices; track choice.index) {\n <li>\n <button\n class=\"branch-chooser-option\"\n type=\"button\"\n (click)=\"choose.emit(choice)\">\n <span class=\"branch-chooser-index\" aria-hidden=\"true\">{{ choice.index + 1 }}</span>\n <span class=\"branch-chooser-label\">{{ choice.label || t('option', { n: choice.index + 1 }) }}</span>\n <span class=\"branch-chooser-arrow\" aria-hidden=\"true\"><pw-icon name=\"lucideChevronRight\" /></span>\n </button>\n </li>\n }\n </ul>\n </div>\n </div>\n}\n", styles: [".branch-chooser-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease;outline:none}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.branch-chooser-container{width:90%;max-width:420px;max-height:70vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.branch-chooser-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.branch-chooser-title{font-size:18px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:background .15s ease,color .15s ease}.close-btn:hover,.close-btn:focus-visible{background:#ffffff1a;color:#fff;outline:none}.branch-chooser-list{list-style:none;margin:0;padding:12px;overflow-y:auto;display:flex;flex-direction:column;gap:8px}.branch-chooser-option{width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;background:#ffffff0f;border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#fff;font:inherit;font-size:15px;text-align:left;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .15s ease}.branch-chooser-option:hover,.branch-chooser-option:focus-visible{background:#ffffff1f;border-color:#ffffff40;outline:none;transform:translate(2px)}.branch-chooser-index{flex-shrink:0;width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;border-radius:50%;background:#ffffff1f;font-size:13px;font-weight:600}.branch-chooser-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.branch-chooser-arrow{flex-shrink:0;display:inline-flex;color:#ffffff80}@media (prefers-reduced-motion: reduce){.branch-chooser-overlay,.branch-chooser-container{animation:none}.branch-chooser-option{transition:none}}\n"] }]
|
|
16002
16189
|
}], propDecorators: { visible: [{
|
|
16003
16190
|
type: Input
|
|
16004
16191
|
}], locale: [{
|
|
@@ -16314,11 +16501,11 @@ class ShareModalComponent {
|
|
|
16314
16501
|
}
|
|
16315
16502
|
}
|
|
16316
16503
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ShareModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16317
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ShareModalComponent, isStandalone: true, selector: "pw-share-modal", inputs: { shareUrl: "shareUrl", shareTitle: "shareTitle", shareDescription: "shareDescription", visible: "visible", shareable: "shareable" }, outputs: { close: "close", share: "share" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\r\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\r\n <!-- Header -->\r\n <div class=\"share-header\">\r\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'share.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"share-content\">\r\n @if (!shareable) {\r\n <!-- Not shareable message -->\r\n <div class=\"not-shareable\">\r\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\r\n <p>{{ 'share.not_shareable' | translate }}</p>\r\n </div>\r\n } @else {\r\n <!-- Copy Link -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\r\n <div class=\"link-container\">\r\n <input\r\n type=\"text\"\r\n class=\"link-input\"\r\n [value]=\"shareUrl\"\r\n readonly\r\n [attr.aria-label]=\"'share.share_url' | translate\" />\r\n <button\r\n class=\"copy-btn\"\r\n [class.success]=\"copySuccess\"\r\n type=\"button\"\r\n (click)=\"copyLink()\">\r\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\r\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Native Share (if supported) -->\r\n @if (isNativeShareSupported) {\r\n <div class=\"share-section\">\r\n <button\r\n class=\"native-share-btn\"\r\n type=\"button\"\r\n (click)=\"useNativeShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Social Share Buttons -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\r\n <div class=\"social-buttons\">\r\n <button\r\n class=\"social-btn twitter\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\r\n (click)=\"shareOnTwitter()\">\r\n <span class=\"social-icon\">\uD835\uDD4F</span>\r\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn facebook\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\r\n (click)=\"shareOnFacebook()\">\r\n <span class=\"social-icon\">f</span>\r\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn reddit\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\r\n (click)=\"shareOnReddit()\">\r\n <span class=\"social-icon\">\u25B2</span>\r\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn email\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\r\n (click)=\"shareViaEmail()\">\r\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\r\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- QR Code -->\r\n <div class=\"share-section\">\r\n <button\r\n class=\"qr-toggle-btn\"\r\n type=\"button\"\r\n (click)=\"toggleQrCode()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\r\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\r\n </button>\r\n\r\n @if (showQrCode) {\r\n <div class=\"qr-code-container\">\r\n @if (qrCodeDataUrl) {\r\n <img\r\n [src]=\"qrCodeDataUrl\"\r\n [alt]=\"'share.qr_code' | translate\"\r\n class=\"qr-code-image\" />\r\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\r\n } @else {\r\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Share Info -->\r\n <div class=\"share-info\">\r\n <p class=\"info-text\">\r\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16504
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: ShareModalComponent, isStandalone: true, selector: "pw-share-modal", inputs: { shareUrl: "shareUrl", shareTitle: "shareTitle", shareDescription: "shareDescription", visible: "visible", shareable: "shareable" }, outputs: { close: "close", share: "share" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16318
16505
|
}
|
|
16319
16506
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: ShareModalComponent, decorators: [{
|
|
16320
16507
|
type: Component,
|
|
16321
|
-
args: [{ selector: 'pw-share-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\r\n <!-- Header -->\r\n <div class=\"share-header\">\r\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'share.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"share-content\">\r\n @if (!shareable) {\r\n <!-- Not shareable message -->\r\n <div class=\"not-shareable\">\r\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\r\n <p>{{ 'share.not_shareable' | translate }}</p>\r\n </div>\r\n } @else {\r\n <!-- Copy Link -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\r\n <div class=\"link-container\">\r\n <input\r\n type=\"text\"\r\n class=\"link-input\"\r\n [value]=\"shareUrl\"\r\n readonly\r\n [attr.aria-label]=\"'share.share_url' | translate\" />\r\n <button\r\n class=\"copy-btn\"\r\n [class.success]=\"copySuccess\"\r\n type=\"button\"\r\n (click)=\"copyLink()\">\r\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\r\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Native Share (if supported) -->\r\n @if (isNativeShareSupported) {\r\n <div class=\"share-section\">\r\n <button\r\n class=\"native-share-btn\"\r\n type=\"button\"\r\n (click)=\"useNativeShare()\">\r\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\r\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- Social Share Buttons -->\r\n <div class=\"share-section\">\r\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\r\n <div class=\"social-buttons\">\r\n <button\r\n class=\"social-btn twitter\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\r\n (click)=\"shareOnTwitter()\">\r\n <span class=\"social-icon\">\uD835\uDD4F</span>\r\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn facebook\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\r\n (click)=\"shareOnFacebook()\">\r\n <span class=\"social-icon\">f</span>\r\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn reddit\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\r\n (click)=\"shareOnReddit()\">\r\n <span class=\"social-icon\">\u25B2</span>\r\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\r\n </button>\r\n\r\n <button\r\n class=\"social-btn email\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\r\n (click)=\"shareViaEmail()\">\r\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\r\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- QR Code -->\r\n <div class=\"share-section\">\r\n <button\r\n class=\"qr-toggle-btn\"\r\n type=\"button\"\r\n (click)=\"toggleQrCode()\">\r\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\r\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\r\n </button>\r\n\r\n @if (showQrCode) {\r\n <div class=\"qr-code-container\">\r\n @if (qrCodeDataUrl) {\r\n <img\r\n [src]=\"qrCodeDataUrl\"\r\n [alt]=\"'share.qr_code' | translate\"\r\n class=\"qr-code-image\" />\r\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\r\n } @else {\r\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Share Info -->\r\n <div class=\"share-info\">\r\n <p class=\"info-text\">\r\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"] }]
|
|
16508
|
+
args: [{ selector: 'pw-share-modal', imports: [TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"share-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"share-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Share\">\n <!-- Header -->\n <div class=\"share-header\">\n <h2 class=\"share-title\">{{ 'share.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'share.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"share-content\">\n @if (!shareable) {\n <!-- Not shareable message -->\n <div class=\"not-shareable\">\n <span class=\"warning-icon\"><pw-icon name=\"lucideTriangleAlert\" /></span>\n <p>{{ 'share.not_shareable' | translate }}</p>\n </div>\n } @else {\n <!-- Copy Link -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.copy_link' | translate }}</h3>\n <div class=\"link-container\">\n <input\n type=\"text\"\n class=\"link-input\"\n [value]=\"shareUrl\"\n readonly\n [attr.aria-label]=\"'share.share_url' | translate\" />\n <button\n class=\"copy-btn\"\n [class.success]=\"copySuccess\"\n type=\"button\"\n (click)=\"copyLink()\">\n <pw-icon [name]=\"copySuccess ? 'lucideCheck' : 'lucideClipboard'\" />\n {{ copySuccess ? ('share.copied' | translate) : ('share.copy' | translate) }}\n </button>\n </div>\n </div>\n\n <!-- Native Share (if supported) -->\n @if (isNativeShareSupported) {\n <div class=\"share-section\">\n <button\n class=\"native-share-btn\"\n type=\"button\"\n (click)=\"useNativeShare()\">\n <span class=\"btn-icon\"><pw-icon name=\"lucideShare2\" /></span>\n <span class=\"btn-label\">{{ 'share.share_via' | translate }}</span>\n </button>\n </div>\n }\n\n <!-- Social Share Buttons -->\n <div class=\"share-section\">\n <h3 class=\"section-title\">{{ 'share.social_media' | translate }}</h3>\n <div class=\"social-buttons\">\n <button\n class=\"social-btn twitter\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.twitter' | translate)\"\n (click)=\"shareOnTwitter()\">\n <span class=\"social-icon\">\uD835\uDD4F</span>\n <span class=\"social-label\">{{ 'share.twitter' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn facebook\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.facebook' | translate)\"\n (click)=\"shareOnFacebook()\">\n <span class=\"social-icon\">f</span>\n <span class=\"social-label\">{{ 'share.facebook' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn reddit\"\n type=\"button\"\n [attr.aria-label]=\"'Share on ' + ('share.reddit' | translate)\"\n (click)=\"shareOnReddit()\">\n <span class=\"social-icon\">\u25B2</span>\n <span class=\"social-label\">{{ 'share.reddit' | translate }}</span>\n </button>\n\n <button\n class=\"social-btn email\"\n type=\"button\"\n [attr.aria-label]=\"'Share via ' + ('share.email' | translate)\"\n (click)=\"shareViaEmail()\">\n <span class=\"social-icon\"><pw-icon name=\"lucideMail\" /></span>\n <span class=\"social-label\">{{ 'share.email' | translate }}</span>\n </button>\n </div>\n </div>\n\n <!-- QR Code -->\n <div class=\"share-section\">\n <button\n class=\"qr-toggle-btn\"\n type=\"button\"\n (click)=\"toggleQrCode()\">\n <span class=\"btn-icon\"><pw-icon [name]=\"showQrCode ? 'lucideChevronDown' : 'lucideChevronRight'\" /></span>\n <span class=\"btn-label\">{{ showQrCode ? ('share.hide_qr' | translate) : ('share.show_qr' | translate) }}</span>\n </button>\n\n @if (showQrCode) {\n <div class=\"qr-code-container\">\n @if (qrCodeDataUrl) {\n <img\n [src]=\"qrCodeDataUrl\"\n [alt]=\"'share.qr_code' | translate\"\n class=\"qr-code-image\" />\n <p class=\"qr-hint\">{{ 'share.scan_mobile' | translate }}</p>\n } @else {\n <div class=\"qr-loading\">{{ 'share.generating_qr' | translate }}</div>\n }\n </div>\n }\n </div>\n\n <!-- Share Info -->\n <div class=\"share-info\">\n <p class=\"info-text\">\n <pw-icon name=\"lucideInfo\" /> {{ 'share.share_info' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n}\n", styles: [".share-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:#000000b3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.share-container{width:90%;max-width:500px;max-height:85vh;background:#1a1a1a;border-radius:12px;box-shadow:0 8px 32px #0006;display:flex;flex-direction:column;overflow:hidden;animation:slideUp .3s cubic-bezier(.4,0,.2,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.share-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.share-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.close-btn:active{background:#ffffff26;transform:scale(.95)}.share-content{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:24px}.share-content::-webkit-scrollbar{width:8px}.share-content::-webkit-scrollbar-track{background:#ffffff0d}.share-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.not-shareable{display:flex;flex-direction:column;align-items:center;gap:16px;padding:40px 20px;text-align:center}.warning-icon{font-size:48px}.not-shareable p{font-size:14px;color:#ffffffb3;margin:0}.share-section{display:flex;flex-direction:column;gap:12px}.section-title{font-size:14px;font-weight:600;color:#ffffffe6;margin:0;text-transform:uppercase;letter-spacing:.5px}.link-container{display:flex;gap:8px}.link-input{flex:1;padding:10px 14px;font-size:13px;font-family:monospace;color:#fffc;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:6px;outline:none}.link-input:focus{border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.copy-btn{padding:10px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease;white-space:nowrap}.copy-btn:hover{background:#ffffff26;border-color:#ffffff4d}.copy-btn.success{background:#00b4784d;border-color:#00b47880;color:#fff}.native-share-btn{display:flex;align-items:center;justify-content:center;gap:8px;padding:12px 20px;font-size:14px;font-weight:500;color:#ffffffe6;background:#0078d74d;border:1px solid rgba(0,120,215,.5);border-radius:8px;cursor:pointer;transition:all .2s ease}.native-share-btn:hover{background:#0078d766;border-color:#0078d799;transform:translateY(-1px)}.social-buttons{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.social-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;cursor:pointer;transition:all .2s ease}.social-btn:hover{background:#ffffff1a;border-color:#fff3;transform:translateY(-2px)}.social-btn:active{transform:translateY(0)}.social-icon{font-size:24px;font-weight:700;line-height:1}.social-label{font-size:12px;font-weight:500;color:#fffc}.social-btn.twitter{background:#1d9bf01a;border-color:#1d9bf04d}.social-btn.twitter:hover{background:#1d9bf033;border-color:#1d9bf080}.social-btn.facebook{background:#1877f21a;border-color:#1877f24d}.social-btn.facebook:hover{background:#1877f233;border-color:#1877f280}.social-btn.reddit{background:#ff45001a;border-color:#ff45004d}.social-btn.reddit:hover{background:#ff450033;border-color:#ff450080}.social-btn.email{background:#8080801a;border-color:#8080804d}.social-btn.email:hover{background:#80808033;border-color:#80808080}.qr-toggle-btn{display:flex;align-items:center;gap:8px;padding:10px 14px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:6px;cursor:pointer;transition:all .2s ease}.qr-toggle-btn:hover{background:#ffffff1a;border-color:#fff3}.btn-icon{font-size:14px;line-height:1}.btn-label{line-height:1}.qr-code-container{display:flex;flex-direction:column;align-items:center;gap:12px;padding:20px;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:8px;animation:fadeIn .2s ease}.qr-code-image{width:200px;height:200px;border-radius:8px;background:#fff;padding:8px}.qr-hint{font-size:12px;color:#fff9;margin:0}.qr-loading{padding:40px;font-size:13px;color:#ffffff80}.share-info{padding:12px 16px;background:#0078d71a;border:1px solid rgba(0,120,215,.2);border-radius:6px}.info-text{font-size:12px;line-height:1.5;color:#ffffffb3;margin:0}@media (max-width: 480px){.share-container{width:95%}.share-header{padding:16px 20px}.share-title{font-size:18px}.share-content{padding:20px;gap:20px}.social-buttons{grid-template-columns:1fr}.qr-code-image{width:180px;height:180px}}.close-btn:focus-visible,.copy-btn:focus-visible,.native-share-btn:focus-visible,.social-btn:focus-visible,.qr-toggle-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.share-overlay,.share-container,.qr-code-container,.close-btn,.copy-btn,.social-btn,.qr-toggle-btn{animation:none;transition:none}}\n"] }]
|
|
16322
16509
|
}], propDecorators: { shareUrl: [{
|
|
16323
16510
|
type: Input
|
|
16324
16511
|
}], shareTitle: [{
|
|
@@ -16557,11 +16744,11 @@ class CommentsDrawerComponent {
|
|
|
16557
16744
|
}
|
|
16558
16745
|
}
|
|
16559
16746
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CommentsDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
16560
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CommentsDrawerComponent, isStandalone: true, selector: "pw-comments-drawer", inputs: { comments: "comments", visible: "visible", loading: "loading", currentPage: "currentPage", totalPages: "totalPages", hasMore: "hasMore", isAuthenticated: "isAuthenticated" }, outputs: { postComment: "postComment", loadMore: "loadMore", likeComment: "likeComment", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\r\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\r\n <!-- Header -->\r\n <div class=\"drawer-header\">\r\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'comments.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Comment Composer (Top) -->\r\n @if (isAuthenticated) {\r\n <div class=\"comment-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_comment' | translate\"\r\n rows=\"3\"\r\n [(ngModel)]=\"newCommentContent\"\r\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!newCommentContent.trim()\"\r\n (click)=\"submitComment()\">\r\n {{ 'comments.post_comment' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"auth-notice\">\r\n <p>{{ 'comments.sign_in' | translate }}</p>\r\n </div>\r\n }\r\n\r\n <!-- Comments List -->\r\n <div class=\"comments-content\">\r\n @if (loading && comments.length === 0) {\r\n <div class=\"loading-state\">\r\n <div class=\"loading-spinner\"></div>\r\n <p>{{ 'comments.loading' | translate }}</p>\r\n </div>\r\n } @else if (comments.length === 0) {\r\n <div class=\"empty-state\">\r\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n <p>{{ 'comments.no_comments' | translate }}</p>\r\n </div>\r\n } @else {\r\n <div class=\"comments-list\">\r\n @for (comment of comments; track comment.id) {\r\n <div class=\"comment-item\">\r\n <!-- Avatar -->\r\n <div class=\"comment-avatar\">\r\n @if (comment.authorAvatar) {\r\n <img\r\n [src]=\"comment.authorAvatar\"\r\n [alt]=\"comment.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(comment.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ comment.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\r\n </div>\r\n\r\n <p class=\"comment-text\">{{ comment.content }}</p>\r\n\r\n <!-- Actions -->\r\n <div class=\"comment-actions\">\r\n @if (isAuthenticated) {\r\n <button\r\n class=\"action-btn\"\r\n [class.active]=\"comment.isLiked\"\r\n type=\"button\"\r\n (click)=\"onLikeComment(comment)\">\r\n <pw-icon name=\"lucideHeart\" />\r\n @if (comment.likes && comment.likes > 0) {\r\n <span>{{ comment.likes }}</span>\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"startReply(comment)\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Replies -->\r\n @if (comment.replies && comment.replies.length > 0) {\r\n <div class=\"comment-replies\">\r\n @for (reply of comment.replies; track reply.id) {\r\n <div class=\"reply-item\">\r\n <div class=\"comment-avatar small\">\r\n @if (reply.authorAvatar) {\r\n <img\r\n [src]=\"reply.authorAvatar\"\r\n [alt]=\"reply.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(reply.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ reply.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\r\n </div>\r\n <p class=\"comment-text\">{{ reply.content }}</p>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Reply Composer -->\r\n @if (showReplyComposer && replyTarget?.id === comment.id) {\r\n <div class=\"reply-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_reply' | translate\"\r\n rows=\"2\"\r\n [(ngModel)]=\"replyContent\"\r\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"cancel-btn\"\r\n type=\"button\"\r\n (click)=\"cancelReply()\">\r\n {{ 'comments.cancel' | translate }}\r\n </button>\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!replyContent.trim()\"\r\n (click)=\"submitReply()\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Load More -->\r\n @if (hasMore) {\r\n <div class=\"load-more\">\r\n @if (loading) {\r\n <div class=\"loading-spinner small\"></div>\r\n } @else {\r\n <button\r\n class=\"load-more-btn\"\r\n type=\"button\"\r\n (click)=\"onLoadMore()\">\r\n {{ 'comments.load_more' | translate }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Footer Info -->\r\n <div class=\"drawer-footer\">\r\n <p class=\"footer-text\">\r\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\r\n @if (totalPages > 1) {\r\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\r\n }\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16747
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: CommentsDrawerComponent, isStandalone: true, selector: "pw-comments-drawer", inputs: { comments: "comments", visible: "visible", loading: "loading", currentPage: "currentPage", totalPages: "totalPages", hasMore: "hasMore", isAuthenticated: "isAuthenticated" }, outputs: { postComment: "postComment", loadMore: "loadMore", likeComment: "likeComment", close: "close" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, ngImport: i0, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
16561
16748
|
}
|
|
16562
16749
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: CommentsDrawerComponent, decorators: [{
|
|
16563
16750
|
type: Component,
|
|
16564
|
-
args: [{ selector: 'pw-comments-drawer', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\r\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\r\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\r\n <!-- Header -->\r\n <div class=\"drawer-header\">\r\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\r\n <button\r\n class=\"close-btn\"\r\n type=\"button\"\r\n [attr.aria-label]=\"'comments.close' | translate\"\r\n (click)=\"onClose()\">\r\n <pw-icon name=\"lucideX\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Comment Composer (Top) -->\r\n @if (isAuthenticated) {\r\n <div class=\"comment-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_comment' | translate\"\r\n rows=\"3\"\r\n [(ngModel)]=\"newCommentContent\"\r\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!newCommentContent.trim()\"\r\n (click)=\"submitComment()\">\r\n {{ 'comments.post_comment' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"auth-notice\">\r\n <p>{{ 'comments.sign_in' | translate }}</p>\r\n </div>\r\n }\r\n\r\n <!-- Comments List -->\r\n <div class=\"comments-content\">\r\n @if (loading && comments.length === 0) {\r\n <div class=\"loading-state\">\r\n <div class=\"loading-spinner\"></div>\r\n <p>{{ 'comments.loading' | translate }}</p>\r\n </div>\r\n } @else if (comments.length === 0) {\r\n <div class=\"empty-state\">\r\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\r\n <p>{{ 'comments.no_comments' | translate }}</p>\r\n </div>\r\n } @else {\r\n <div class=\"comments-list\">\r\n @for (comment of comments; track comment.id) {\r\n <div class=\"comment-item\">\r\n <!-- Avatar -->\r\n <div class=\"comment-avatar\">\r\n @if (comment.authorAvatar) {\r\n <img\r\n [src]=\"comment.authorAvatar\"\r\n [alt]=\"comment.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(comment.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Content -->\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ comment.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\r\n </div>\r\n\r\n <p class=\"comment-text\">{{ comment.content }}</p>\r\n\r\n <!-- Actions -->\r\n <div class=\"comment-actions\">\r\n @if (isAuthenticated) {\r\n <button\r\n class=\"action-btn\"\r\n [class.active]=\"comment.isLiked\"\r\n type=\"button\"\r\n (click)=\"onLikeComment(comment)\">\r\n <pw-icon name=\"lucideHeart\" />\r\n @if (comment.likes && comment.likes > 0) {\r\n <span>{{ comment.likes }}</span>\r\n }\r\n </button>\r\n\r\n <button\r\n class=\"action-btn\"\r\n type=\"button\"\r\n (click)=\"startReply(comment)\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Replies -->\r\n @if (comment.replies && comment.replies.length > 0) {\r\n <div class=\"comment-replies\">\r\n @for (reply of comment.replies; track reply.id) {\r\n <div class=\"reply-item\">\r\n <div class=\"comment-avatar small\">\r\n @if (reply.authorAvatar) {\r\n <img\r\n [src]=\"reply.authorAvatar\"\r\n [alt]=\"reply.author\"\r\n class=\"avatar-image\" />\r\n } @else {\r\n <div class=\"avatar-placeholder\">\r\n {{ getAuthorInitials(reply.author) }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"comment-content\">\r\n <div class=\"comment-header\">\r\n <span class=\"comment-author\">{{ reply.author }}</span>\r\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\r\n </div>\r\n <p class=\"comment-text\">{{ reply.content }}</p>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Reply Composer -->\r\n @if (showReplyComposer && replyTarget?.id === comment.id) {\r\n <div class=\"reply-composer\">\r\n <textarea\r\n class=\"composer-input\"\r\n [placeholder]=\"'comments.write_reply' | translate\"\r\n rows=\"2\"\r\n [(ngModel)]=\"replyContent\"\r\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\r\n <div class=\"composer-actions\">\r\n <button\r\n class=\"cancel-btn\"\r\n type=\"button\"\r\n (click)=\"cancelReply()\">\r\n {{ 'comments.cancel' | translate }}\r\n </button>\r\n <button\r\n class=\"post-btn\"\r\n type=\"button\"\r\n [disabled]=\"!replyContent.trim()\"\r\n (click)=\"submitReply()\">\r\n {{ 'comments.reply' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Load More -->\r\n @if (hasMore) {\r\n <div class=\"load-more\">\r\n @if (loading) {\r\n <div class=\"loading-spinner small\"></div>\r\n } @else {\r\n <button\r\n class=\"load-more-btn\"\r\n type=\"button\"\r\n (click)=\"onLoadMore()\">\r\n {{ 'comments.load_more' | translate }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n }\r\n </div>\r\n\r\n <!-- Footer Info -->\r\n <div class=\"drawer-footer\">\r\n <p class=\"footer-text\">\r\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\r\n @if (totalPages > 1) {\r\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\r\n }\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"] }]
|
|
16751
|
+
args: [{ selector: 'pw-comments-drawer', imports: [FormsModule, TranslateModule, PwIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible) {\n <div class=\"drawer-overlay\" tabindex=\"-1\" (click)=\"onBackdropClick($event)\" (keydown)=\"onBackdropKeydown($event)\">\n <div class=\"drawer-container\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Comments\">\n <!-- Header -->\n <div class=\"drawer-header\">\n <h2 class=\"drawer-title\">{{ 'comments.title' | translate }}</h2>\n <button\n class=\"close-btn\"\n type=\"button\"\n [attr.aria-label]=\"'comments.close' | translate\"\n (click)=\"onClose()\">\n <pw-icon name=\"lucideX\" />\n </button>\n </div>\n\n <!-- Comment Composer (Top) -->\n @if (isAuthenticated) {\n <div class=\"comment-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_comment' | translate\"\n rows=\"3\"\n [(ngModel)]=\"newCommentContent\"\n [attr.aria-label]=\"'comments.new_comment_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!newCommentContent.trim()\"\n (click)=\"submitComment()\">\n {{ 'comments.post_comment' | translate }}\n </button>\n </div>\n </div>\n } @else {\n <div class=\"auth-notice\">\n <p>{{ 'comments.sign_in' | translate }}</p>\n </div>\n }\n\n <!-- Comments List -->\n <div class=\"comments-content\">\n @if (loading && comments.length === 0) {\n <div class=\"loading-state\">\n <div class=\"loading-spinner\"></div>\n <p>{{ 'comments.loading' | translate }}</p>\n </div>\n } @else if (comments.length === 0) {\n <div class=\"empty-state\">\n <span class=\"empty-icon\"><pw-icon name=\"lucideMessageCircle\" /></span>\n <p>{{ 'comments.no_comments' | translate }}</p>\n </div>\n } @else {\n <div class=\"comments-list\">\n @for (comment of comments; track comment.id) {\n <div class=\"comment-item\">\n <!-- Avatar -->\n <div class=\"comment-avatar\">\n @if (comment.authorAvatar) {\n <img\n [src]=\"comment.authorAvatar\"\n [alt]=\"comment.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(comment.author) }}\n </div>\n }\n </div>\n\n <!-- Content -->\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ comment.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(comment.timestamp) }}</span>\n </div>\n\n <p class=\"comment-text\">{{ comment.content }}</p>\n\n <!-- Actions -->\n <div class=\"comment-actions\">\n @if (isAuthenticated) {\n <button\n class=\"action-btn\"\n [class.active]=\"comment.isLiked\"\n type=\"button\"\n (click)=\"onLikeComment(comment)\">\n <pw-icon name=\"lucideHeart\" />\n @if (comment.likes && comment.likes > 0) {\n <span>{{ comment.likes }}</span>\n }\n </button>\n\n <button\n class=\"action-btn\"\n type=\"button\"\n (click)=\"startReply(comment)\">\n {{ 'comments.reply' | translate }}\n </button>\n }\n </div>\n\n <!-- Replies -->\n @if (comment.replies && comment.replies.length > 0) {\n <div class=\"comment-replies\">\n @for (reply of comment.replies; track reply.id) {\n <div class=\"reply-item\">\n <div class=\"comment-avatar small\">\n @if (reply.authorAvatar) {\n <img\n [src]=\"reply.authorAvatar\"\n [alt]=\"reply.author\"\n class=\"avatar-image\" />\n } @else {\n <div class=\"avatar-placeholder\">\n {{ getAuthorInitials(reply.author) }}\n </div>\n }\n </div>\n\n <div class=\"comment-content\">\n <div class=\"comment-header\">\n <span class=\"comment-author\">{{ reply.author }}</span>\n <span class=\"comment-timestamp\">{{ formatTimestamp(reply.timestamp) }}</span>\n </div>\n <p class=\"comment-text\">{{ reply.content }}</p>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Reply Composer -->\n @if (showReplyComposer && replyTarget?.id === comment.id) {\n <div class=\"reply-composer\">\n <textarea\n class=\"composer-input\"\n [placeholder]=\"'comments.write_reply' | translate\"\n rows=\"2\"\n [(ngModel)]=\"replyContent\"\n [attr.aria-label]=\"'comments.reply_label' | translate\"></textarea>\n <div class=\"composer-actions\">\n <button\n class=\"cancel-btn\"\n type=\"button\"\n (click)=\"cancelReply()\">\n {{ 'comments.cancel' | translate }}\n </button>\n <button\n class=\"post-btn\"\n type=\"button\"\n [disabled]=\"!replyContent.trim()\"\n (click)=\"submitReply()\">\n {{ 'comments.reply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n\n <!-- Load More -->\n @if (hasMore) {\n <div class=\"load-more\">\n @if (loading) {\n <div class=\"loading-spinner small\"></div>\n } @else {\n <button\n class=\"load-more-btn\"\n type=\"button\"\n (click)=\"onLoadMore()\">\n {{ 'comments.load_more' | translate }}\n </button>\n }\n </div>\n }\n }\n </div>\n\n <!-- Footer Info -->\n <div class=\"drawer-footer\">\n <p class=\"footer-text\">\n {{ comments.length }} {{ comments.length === 1 ? ('comments.comment_count_singular' | translate) : ('comments.comment_count_plural' | translate) }}\n @if (totalPages > 1) {\n <span> \u2022 {{ 'comments.page_of' | translate: {current: currentPage, total: totalPages} }}</span>\n }\n </p>\n </div>\n </div>\n </div>\n}\n", styles: [".drawer-overlay{position:fixed;inset:0;z-index:200;display:flex;align-items:stretch;justify-content:flex-end;background:#00000080;animation:fadeIn .2s ease}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.drawer-container{width:90%;max-width:450px;background:#1a1a1a;box-shadow:-4px 0 16px #0000004d;display:flex;flex-direction:column;overflow:hidden;animation:slideInRight .3s cubic-bezier(.4,0,.2,1)}@keyframes slideInRight{0%{transform:translate(100%)}to{transform:translate(0)}}.drawer-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.drawer-title{font-size:20px;font-weight:600;color:#fff;margin:0}.close-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:20px;color:#ffffffb3;background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.close-btn:hover{background:#ffffff1a;color:#fff}.comment-composer{padding:16px 24px;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0}.composer-input{width:100%;padding:12px;font-size:14px;line-height:1.5;color:#fff;background:#ffffff0d;border:1px solid rgba(255,255,255,.2);border-radius:8px;outline:none;resize:vertical;font-family:inherit;transition:all .2s ease}.composer-input::placeholder{color:#fff6}.composer-input:focus{background:#ffffff14;border-color:#0078d7cc;box-shadow:0 0 0 3px #0078d733}.composer-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}.post-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#fff;background:#0078d7cc;border:none;border-radius:6px;cursor:pointer;transition:all .2s ease}.post-btn:hover:not(:disabled){background:#0078d7e6;transform:translateY(-1px)}.post-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.cancel-btn{padding:8px 16px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.cancel-btn:hover{background:#ffffff26}.auth-notice{padding:16px 24px;background:#ffa5001a;border-bottom:1px solid rgba(255,165,0,.2);flex-shrink:0}.auth-notice p{font-size:13px;color:#fffc;margin:0;text-align:center}.comments-content{flex:1;overflow-y:auto;padding:16px 24px}.comments-content::-webkit-scrollbar{width:8px}.comments-content::-webkit-scrollbar-track{background:#ffffff0d}.comments-content::-webkit-scrollbar-thumb{background:#fff3;border-radius:4px}.loading-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px}.loading-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}.loading-spinner.small{width:24px;height:24px;border-width:2px}@keyframes spin{to{transform:rotate(360deg)}}.loading-state p{font-size:14px;color:#ffffff80;margin:0}.empty-state{display:flex;flex-direction:column;align-items:center;gap:16px;padding:60px 20px;text-align:center}.empty-icon{font-size:48px}.empty-state p{font-size:14px;color:#ffffff80;margin:0}.comments-list{display:flex;flex-direction:column;gap:20px}.comment-item{display:flex;gap:12px}.comment-avatar{width:40px;height:40px;flex-shrink:0;border-radius:50%;overflow:hidden}.comment-avatar.small{width:32px;height:32px}.avatar-image{width:100%;height:100%;object-fit:cover;display:block}.avatar-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;color:#ffffffe6;background:linear-gradient(135deg,#0078d74d,#00b4784d)}.comment-avatar.small .avatar-placeholder{font-size:12px}.comment-content{flex:1;min-width:0}.comment-header{display:flex;align-items:baseline;gap:8px;margin-bottom:6px}.comment-author{font-size:14px;font-weight:600;color:#ffffffe6}.comment-timestamp{font-size:12px;color:#ffffff80}.comment-text{font-size:14px;line-height:1.5;color:#fffc;margin:0 0 8px;word-wrap:break-word}.comment-actions{display:flex;gap:12px;margin-top:8px}.action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:12px;font-weight:500;color:#fff9;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:all .2s ease}.action-btn:hover{color:#ffffffe6;background:#ffffff1a}.action-btn.active{color:#ff4d6d}.comment-replies{margin-top:12px;padding-left:12px;border-left:2px solid rgba(255,255,255,.1);display:flex;flex-direction:column;gap:16px}.reply-item{display:flex;gap:10px}.reply-composer{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.1)}.load-more{display:flex;justify-content:center;padding:16px 0;margin-top:16px;border-top:1px solid rgba(255,255,255,.1)}.load-more-btn{padding:10px 20px;font-size:13px;font-weight:500;color:#ffffffe6;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:6px;cursor:pointer;transition:all .2s ease}.load-more-btn:hover{background:#ffffff26;border-color:#ffffff4d}.drawer-footer{padding:12px 24px;border-top:1px solid rgba(255,255,255,.1);flex-shrink:0}.footer-text{font-size:12px;color:#ffffff80;margin:0;text-align:center}@media (max-width: 768px){.drawer-container{width:100%;max-width:none}.drawer-header{padding:16px 20px}.drawer-title{font-size:18px}.comment-composer,.auth-notice,.comments-content{padding:14px 20px}.comment-avatar{width:36px;height:36px}.comment-avatar.small{width:28px;height:28px}.avatar-placeholder{font-size:13px}.comment-avatar.small .avatar-placeholder{font-size:11px}}.close-btn:focus-visible,.composer-input:focus-visible,.post-btn:focus-visible,.cancel-btn:focus-visible,.action-btn:focus-visible,.load-more-btn:focus-visible{outline:2px solid rgba(0,120,215,.8);outline-offset:2px}@media (prefers-reduced-motion: reduce){.drawer-overlay,.drawer-container,.loading-spinner,.close-btn,.post-btn,.cancel-btn,.action-btn,.load-more-btn{animation:none;transition:none}}\n"] }]
|
|
16565
16752
|
}], propDecorators: { comments: [{
|
|
16566
16753
|
type: Input
|
|
16567
16754
|
}], visible: [{
|
|
@@ -16607,8 +16794,11 @@ function createTranslateLoader(http) {
|
|
|
16607
16794
|
class PlayerShellComponent {
|
|
16608
16795
|
constructor() {
|
|
16609
16796
|
/**
|
|
16610
|
-
* Emitted when a reader acts on the paywall overlay
|
|
16611
|
-
*
|
|
16797
|
+
* Emitted when a reader acts on the paywall overlay: `purchase` /
|
|
16798
|
+
* `subscribe` (with the chosen option's `productId` — a product id or a
|
|
16799
|
+
* subscription tier), `login` or `dismiss`. The host owns checkout — the
|
|
16800
|
+
* player never talks to a payment provider itself; call
|
|
16801
|
+
* `refreshEntitlements()` (or pass a new `entitlementSnapshot`) afterwards.
|
|
16612
16802
|
*/
|
|
16613
16803
|
this.paywallAction = new EventEmitter();
|
|
16614
16804
|
/**
|
|
@@ -16890,6 +17080,10 @@ class PlayerShellComponent {
|
|
|
16890
17080
|
this.translationService = inject(TranslationService);
|
|
16891
17081
|
this.canvasCamera = inject(CanvasCameraService);
|
|
16892
17082
|
this.paywallService = inject(PaywallService);
|
|
17083
|
+
/** ngOnInit ran: later input changes are live updates, not initial values. */
|
|
17084
|
+
this.initialized = false;
|
|
17085
|
+
/** The state subscriptions are set up once per instance, not per (re)load. */
|
|
17086
|
+
this.stateSubscribed = false;
|
|
16893
17087
|
}
|
|
16894
17088
|
/**
|
|
16895
17089
|
* Effective reduced-motion state for every transition the shell drives
|
|
@@ -16903,6 +17097,7 @@ class PlayerShellComponent {
|
|
|
16903
17097
|
* Initialize component
|
|
16904
17098
|
*/
|
|
16905
17099
|
ngOnInit() {
|
|
17100
|
+
this.initialized = true;
|
|
16906
17101
|
this.toolbarVisible = this.showToolbar;
|
|
16907
17102
|
this.watchReducedMotion();
|
|
16908
17103
|
this.subscribeToVideoSignals();
|
|
@@ -16913,6 +17108,100 @@ class PlayerShellComponent {
|
|
|
16913
17108
|
window.addEventListener('pagehide', this.onPageHide);
|
|
16914
17109
|
}
|
|
16915
17110
|
}
|
|
17111
|
+
/**
|
|
17112
|
+
* Inputs changed after init. The first round (before ngOnInit) is skipped:
|
|
17113
|
+
* ngOnInit reads every input itself, so nothing loads twice.
|
|
17114
|
+
*/
|
|
17115
|
+
ngOnChanges(changes) {
|
|
17116
|
+
if (!this.initialized) {
|
|
17117
|
+
return;
|
|
17118
|
+
}
|
|
17119
|
+
if (changes['manifest'] || changes['manifestUrl']) {
|
|
17120
|
+
// A new work: reload re-reads every other input as well.
|
|
17121
|
+
void this.reload();
|
|
17122
|
+
return;
|
|
17123
|
+
}
|
|
17124
|
+
if (changes['locale']) {
|
|
17125
|
+
this.applyLocale();
|
|
17126
|
+
}
|
|
17127
|
+
if (changes['entitlementSnapshot']) {
|
|
17128
|
+
if (!this.entitlementSnapshot && !this.httpEntitlement) {
|
|
17129
|
+
this.paywallService.setSnapshot(null);
|
|
17130
|
+
}
|
|
17131
|
+
this.refreshEntitlements(this.entitlementSnapshot).catch(() => undefined);
|
|
17132
|
+
}
|
|
17133
|
+
if (changes['viewModeOverride']) {
|
|
17134
|
+
this.applyViewModeOverride();
|
|
17135
|
+
}
|
|
17136
|
+
if (changes['showToolbar']) {
|
|
17137
|
+
this.toolbarVisible = this.showToolbar;
|
|
17138
|
+
}
|
|
17139
|
+
if (changes['autoplay'] && this.autoplay !== this.autoplayEnabled) {
|
|
17140
|
+
this.onToggleAutoplay();
|
|
17141
|
+
}
|
|
17142
|
+
else if ((changes['secondsPerPanel'] || changes['reducedMotion']) && this.autoplayEnabled) {
|
|
17143
|
+
// Re-arm with the new timing / video start modes.
|
|
17144
|
+
this.startAutoplay();
|
|
17145
|
+
}
|
|
17146
|
+
this.cdr.markForCheck();
|
|
17147
|
+
}
|
|
17148
|
+
/**
|
|
17149
|
+
* Reload the work from the current `manifest` / `manifestUrl` inputs
|
|
17150
|
+
* (also the error screen's Retry). Story state of the previous load —
|
|
17151
|
+
* position, variables except persistent ones, open overlays, autoplay —
|
|
17152
|
+
* is reset; the analytics session of the previous work is closed.
|
|
17153
|
+
*/
|
|
17154
|
+
async reload() {
|
|
17155
|
+
this.stopAutoplay();
|
|
17156
|
+
this.autoplayEnabled = false;
|
|
17157
|
+
this.videoSequencer.reset();
|
|
17158
|
+
this.panelAudio.stopAll();
|
|
17159
|
+
this.endAnalyticsSession();
|
|
17160
|
+
this.analyticsSessionStarted = this.analyticsSessionEnded = this.workCompleteTracked = false;
|
|
17161
|
+
this.lastTrackedPanelId = undefined;
|
|
17162
|
+
this.closePaywall();
|
|
17163
|
+
this.closeAgeGate();
|
|
17164
|
+
this.tocVisible = this.settingsVisible = this.languageModalVisible = this.charactersVisible = false;
|
|
17165
|
+
this.extrasVisible = this.shareVisible = this.commentsVisible = this.thumbnailsVisible = false;
|
|
17166
|
+
this.actionModalVisible = this.branchChooserVisible = false;
|
|
17167
|
+
this.variantOverrides.clear();
|
|
17168
|
+
this.variableStore.setDefinitions([]);
|
|
17169
|
+
for (const scope of ['global', 'chapter', 'page', 'session']) {
|
|
17170
|
+
this.variableStore.resetScope(scope);
|
|
17171
|
+
}
|
|
17172
|
+
this.currentChapter = this.currentPanel = this.currentPage = this.effectivePanel = undefined;
|
|
17173
|
+
this.viewMode = 'panel';
|
|
17174
|
+
this.visitedPanelIds = [];
|
|
17175
|
+
this.httpEntitlement = undefined;
|
|
17176
|
+
this.hasError = false;
|
|
17177
|
+
this.errorMessage = '';
|
|
17178
|
+
await this.initializePlayer();
|
|
17179
|
+
}
|
|
17180
|
+
/** Content locale + GUI language follow `locale`. */
|
|
17181
|
+
applyLocale() {
|
|
17182
|
+
this.playerState.setLocale(this.locale);
|
|
17183
|
+
this.translationService.setLanguage(this.locale);
|
|
17184
|
+
}
|
|
17185
|
+
/** Apply a changed `viewModeOverride` to the running view. */
|
|
17186
|
+
applyViewModeOverride() {
|
|
17187
|
+
const before = this.viewMode;
|
|
17188
|
+
if (this.viewModeOverride === 'panel' && this.viewMode === 'page') {
|
|
17189
|
+
this.viewMode = 'panel';
|
|
17190
|
+
this.videoSequencer.reset();
|
|
17191
|
+
}
|
|
17192
|
+
else {
|
|
17193
|
+
this.updateCanvasAvailability();
|
|
17194
|
+
}
|
|
17195
|
+
if (this.viewMode === before) {
|
|
17196
|
+
return;
|
|
17197
|
+
}
|
|
17198
|
+
if (this.viewMode === 'canvas') {
|
|
17199
|
+
this.jumpCameraToCurrentPanel();
|
|
17200
|
+
}
|
|
17201
|
+
if (this.autoplayEnabled) {
|
|
17202
|
+
this.startAutoplay();
|
|
17203
|
+
}
|
|
17204
|
+
}
|
|
16916
17205
|
/**
|
|
16917
17206
|
* Cleanup on destroy
|
|
16918
17207
|
*/
|
|
@@ -17029,8 +17318,7 @@ class PlayerShellComponent {
|
|
|
17029
17318
|
this.variableStore.seed(context);
|
|
17030
17319
|
}
|
|
17031
17320
|
// Set initial locale and configure translations
|
|
17032
|
-
this.
|
|
17033
|
-
this.translationService.setLanguage(this.locale);
|
|
17321
|
+
this.applyLocale();
|
|
17034
17322
|
// Check if page view is available
|
|
17035
17323
|
const loadedManifest = this.manifestService.getManifest();
|
|
17036
17324
|
if (loadedManifest?.chapters) {
|
|
@@ -17065,12 +17353,19 @@ class PlayerShellComponent {
|
|
|
17065
17353
|
}
|
|
17066
17354
|
// Subscribe to state changes
|
|
17067
17355
|
this.subscribeToStateChanges();
|
|
17356
|
+
// The host's `autoplay` input decides at start; the reader's toolbar
|
|
17357
|
+
// toggle takes over afterwards.
|
|
17358
|
+
if (this.autoplay && !this.autoplayEnabled) {
|
|
17359
|
+
this.onToggleAutoplay();
|
|
17360
|
+
}
|
|
17068
17361
|
this.loading = false;
|
|
17069
17362
|
this.ready.emit();
|
|
17070
17363
|
}
|
|
17071
17364
|
catch (err) {
|
|
17072
17365
|
this.handleError(err);
|
|
17073
17366
|
}
|
|
17367
|
+
// Async completion (e.g. a reload from ngOnChanges): repaint the OnPush view.
|
|
17368
|
+
this.cdr.markForCheck();
|
|
17074
17369
|
}
|
|
17075
17370
|
/**
|
|
17076
17371
|
* Configure the TrackingService from the manifest's `tracking` section:
|
|
@@ -17225,6 +17520,10 @@ class PlayerShellComponent {
|
|
|
17225
17520
|
* Subscribe to state changes
|
|
17226
17521
|
*/
|
|
17227
17522
|
subscribeToStateChanges() {
|
|
17523
|
+
if (this.stateSubscribed) {
|
|
17524
|
+
return;
|
|
17525
|
+
}
|
|
17526
|
+
this.stateSubscribed = true;
|
|
17228
17527
|
// Listen to panel changes
|
|
17229
17528
|
this.playerState.currentPanel$
|
|
17230
17529
|
.pipe(takeUntil(this.destroy$))
|
|
@@ -17294,8 +17593,11 @@ class PlayerShellComponent {
|
|
|
17294
17593
|
* @param transition - Optional transition to render for this panel change
|
|
17295
17594
|
* (already resolved against the outputPresets default). Omitted for
|
|
17296
17595
|
* non-adjacent jumps (TOC, initial load), which swap instantly.
|
|
17596
|
+
* @param mutations - Variable mutations of the traversed edge. Applied only
|
|
17597
|
+
* once the navigation is allowed (after the paywall / age gate), so a
|
|
17598
|
+
* blocked attempt never changes story state.
|
|
17297
17599
|
*/
|
|
17298
|
-
async navigateToPanel(chapterId, panelId, transition, cameraMove) {
|
|
17600
|
+
async navigateToPanel(chapterId, panelId, transition, cameraMove, mutations) {
|
|
17299
17601
|
try {
|
|
17300
17602
|
this.viewportTransition = transition ?? null;
|
|
17301
17603
|
// Check entitlement. A host-supplied adapter still wins — it is the
|
|
@@ -17304,10 +17606,11 @@ class PlayerShellComponent {
|
|
|
17304
17606
|
if (this.entitlementAdapter) {
|
|
17305
17607
|
const hasAccess = await this.entitlementAdapter.hasAccess(panelId);
|
|
17306
17608
|
if (!hasAccess) {
|
|
17307
|
-
this.openPaywall(this.paywallService.gateFor(panelId) ?? {
|
|
17609
|
+
this.openPaywall(this.paywallService.gateFor(panelId, this.locale) ?? {
|
|
17308
17610
|
scope: 'panel',
|
|
17309
17611
|
refId: panelId,
|
|
17310
|
-
reason:
|
|
17612
|
+
reason: PaywallService.readerMessage(null),
|
|
17613
|
+
lockReason: 'entitlement_required',
|
|
17311
17614
|
});
|
|
17312
17615
|
return;
|
|
17313
17616
|
}
|
|
@@ -17318,10 +17621,10 @@ class PlayerShellComponent {
|
|
|
17318
17621
|
// the last panel they were entitled to see.
|
|
17319
17622
|
if (this.paywallService.evaluate(panelId).reason === 'age_verification_required') {
|
|
17320
17623
|
// An age gate is answered in-player (birth date), not by checkout.
|
|
17321
|
-
this.openAgeGate(chapterId, panelId, transition, cameraMove);
|
|
17624
|
+
this.openAgeGate(chapterId, panelId, transition, cameraMove, mutations);
|
|
17322
17625
|
return;
|
|
17323
17626
|
}
|
|
17324
|
-
const gate = this.paywallService.gateFor(panelId);
|
|
17627
|
+
const gate = this.paywallService.gateFor(panelId, this.locale);
|
|
17325
17628
|
if (gate) {
|
|
17326
17629
|
this.openPaywall(gate);
|
|
17327
17630
|
return;
|
|
@@ -17335,6 +17638,10 @@ class PlayerShellComponent {
|
|
|
17335
17638
|
if (!panelData) {
|
|
17336
17639
|
throw new Error(`Panel not found: ${panelId}`);
|
|
17337
17640
|
}
|
|
17641
|
+
// Edge `action` mutations (schema: "applied when traversing this edge").
|
|
17642
|
+
if (mutations?.length) {
|
|
17643
|
+
this.variableStore.applyMutations(mutations, { chapterId });
|
|
17644
|
+
}
|
|
17338
17645
|
const chapterChanged = this.currentChapter !== chapter;
|
|
17339
17646
|
const leavingPanelId = this.getCurrentPanelId();
|
|
17340
17647
|
this.currentChapter = chapter;
|
|
@@ -17469,7 +17776,8 @@ class PlayerShellComponent {
|
|
|
17469
17776
|
buildExtrasList(manifest) {
|
|
17470
17777
|
// Media (images / video / audio / thumbnails) resolve through the asset
|
|
17471
17778
|
// catalog; see utils/extras-utils.ts for the ExtraBlock mapping.
|
|
17472
|
-
|
|
17779
|
+
// An extras-scoped paywall rule the reader does not satisfy locks its block.
|
|
17780
|
+
this.extrasList = extrasFromManifest(manifest?.extras, (assetId) => this.manifestService.getAsset(assetId)).map((extra) => (this.paywallService.isExtraLocked(extra.id) ? { ...extra, gated: true } : extra));
|
|
17473
17781
|
}
|
|
17474
17782
|
/**
|
|
17475
17783
|
* Canvas view is available when the current chapter has a canvas layout
|
|
@@ -17529,12 +17837,7 @@ class PlayerShellComponent {
|
|
|
17529
17837
|
return;
|
|
17530
17838
|
const result = this.flowEngine.getNextPanel(this.currentChapter.graph, currentPanelId, context, this.flowEngine.getDefaultTransition(this.manifestService.getManifest()?.settings), this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
|
|
17531
17839
|
if (result.nextPanelId) {
|
|
17532
|
-
|
|
17533
|
-
// edge") were returned by the flow engine but never applied here.
|
|
17534
|
-
if (result.action?.length) {
|
|
17535
|
-
this.variableStore.applyMutations(result.action, { chapterId: this.currentChapter.id });
|
|
17536
|
-
}
|
|
17537
|
-
await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove);
|
|
17840
|
+
await this.navigateToPanel(this.currentChapter.id, result.nextPanelId, result.transition, result.cameraMove, result.action);
|
|
17538
17841
|
}
|
|
17539
17842
|
}
|
|
17540
17843
|
catch (err) {
|
|
@@ -17663,16 +17966,7 @@ class PlayerShellComponent {
|
|
|
17663
17966
|
else if (newMode === 'canvas') {
|
|
17664
17967
|
this.viewMode = 'canvas';
|
|
17665
17968
|
this.videoSequencer.reset();
|
|
17666
|
-
|
|
17667
|
-
if (this.currentChapter) {
|
|
17668
|
-
const panelId = this.getCurrentPanelId();
|
|
17669
|
-
if (panelId) {
|
|
17670
|
-
const placement = this.currentChapter.canvas?.placements?.[panelId];
|
|
17671
|
-
if (placement) {
|
|
17672
|
-
this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
|
|
17673
|
-
}
|
|
17674
|
-
}
|
|
17675
|
-
}
|
|
17969
|
+
this.jumpCameraToCurrentPanel();
|
|
17676
17970
|
}
|
|
17677
17971
|
else {
|
|
17678
17972
|
// Switch to panel view
|
|
@@ -17685,6 +17979,14 @@ class PlayerShellComponent {
|
|
|
17685
17979
|
}
|
|
17686
17980
|
console.log('View mode:', this.viewMode);
|
|
17687
17981
|
}
|
|
17982
|
+
/** Land the canvas camera on the current panel (entering canvas view). */
|
|
17983
|
+
jumpCameraToCurrentPanel() {
|
|
17984
|
+
const panelId = this.getCurrentPanelId();
|
|
17985
|
+
const placement = panelId ? this.currentChapter?.canvas?.placements?.[panelId] : undefined;
|
|
17986
|
+
if (placement) {
|
|
17987
|
+
this.canvasCamera.jumpTo(this.canvasCamera.frameForPlacement(placement));
|
|
17988
|
+
}
|
|
17989
|
+
}
|
|
17688
17990
|
/**
|
|
17689
17991
|
* Tap on a placed panel in canvas view. Traverses the matching out-edge of
|
|
17690
17992
|
* the current panel when its condition passes (the graph still gates
|
|
@@ -17707,7 +18009,7 @@ class PlayerShellComponent {
|
|
|
17707
18009
|
if (edge) {
|
|
17708
18010
|
this.navigationAttempt.emit({ direction: 'panel', target: panelId });
|
|
17709
18011
|
void this.navigateToPanel(this.currentChapter.id, panelId, edge.transition, edge.cameraMove ??
|
|
17710
|
-
this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings));
|
|
18012
|
+
this.flowEngine.getDefaultCameraMove(this.manifestService.getManifest()?.settings), edge.action);
|
|
17711
18013
|
return;
|
|
17712
18014
|
}
|
|
17713
18015
|
if (this.visitedPanelIds.includes(panelId)) {
|
|
@@ -17715,6 +18017,14 @@ class PlayerShellComponent {
|
|
|
17715
18017
|
void this.navigateToPanel(this.currentChapter.id, panelId);
|
|
17716
18018
|
}
|
|
17717
18019
|
}
|
|
18020
|
+
/**
|
|
18021
|
+
* The manifest actually loaded — from `manifest` or fetched via
|
|
18022
|
+
* `manifestUrl`. Everything that lists the work (ToC, thumbnails) binds to
|
|
18023
|
+
* this, never to the `manifest` input, which is unset for URL loading.
|
|
18024
|
+
*/
|
|
18025
|
+
get loadedManifest() {
|
|
18026
|
+
return this.manifestService.getManifest() ?? undefined;
|
|
18027
|
+
}
|
|
17718
18028
|
/** Balloon defaults for the canvas stage (same source as the viewport). */
|
|
17719
18029
|
get manifestBalloonConfig() {
|
|
17720
18030
|
return this.manifestService.getManifest()?.settings?.typography?.balloon_config ?? null;
|
|
@@ -17728,12 +18038,9 @@ class PlayerShellComponent {
|
|
|
17728
18038
|
return this.manifestService.getManifest()?.settings?.preload ?? null;
|
|
17729
18039
|
}
|
|
17730
18040
|
onLocaleChange(locale) {
|
|
17731
|
-
//
|
|
18041
|
+
// Content locale and GUI language stay in sync.
|
|
17732
18042
|
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);
|
|
18043
|
+
this.applyLocale();
|
|
17737
18044
|
}
|
|
17738
18045
|
onToggleSpeech() {
|
|
17739
18046
|
this.setSpeechEnabled(!this.speechEnabled);
|
|
@@ -18055,7 +18362,15 @@ class PlayerShellComponent {
|
|
|
18055
18362
|
*/
|
|
18056
18363
|
handleKeyboard(event) {
|
|
18057
18364
|
// Don't handle if user is typing in an input
|
|
18058
|
-
if (event.target instanceof HTMLInputElement ||
|
|
18365
|
+
if (event.target instanceof HTMLInputElement ||
|
|
18366
|
+
event.target instanceof HTMLTextAreaElement ||
|
|
18367
|
+
event.target instanceof HTMLSelectElement) {
|
|
18368
|
+
return;
|
|
18369
|
+
}
|
|
18370
|
+
// The paywall and the age gate block the story: no shortcut may move
|
|
18371
|
+
// past them. Their own Escape handling applies (paywall: "Maybe later"
|
|
18372
|
+
// dismisses; age gate: dismiss keeps the reader where they were).
|
|
18373
|
+
if (this.paywallVisible || this.ageGateVisible) {
|
|
18059
18374
|
return;
|
|
18060
18375
|
}
|
|
18061
18376
|
// While a dialog is open the story behind it must not react: Escape
|
|
@@ -18192,6 +18507,9 @@ class PlayerShellComponent {
|
|
|
18192
18507
|
* Handle swipe gesture
|
|
18193
18508
|
*/
|
|
18194
18509
|
onSwipe(direction) {
|
|
18510
|
+
if (this.paywallVisible || this.ageGateVisible) {
|
|
18511
|
+
return;
|
|
18512
|
+
}
|
|
18195
18513
|
if (direction === 'left') {
|
|
18196
18514
|
// Swipe left = navigate forward
|
|
18197
18515
|
if (this.viewMode === 'page') {
|
|
@@ -18589,9 +18907,11 @@ class PlayerShellComponent {
|
|
|
18589
18907
|
*/
|
|
18590
18908
|
onPaywallAction(action) {
|
|
18591
18909
|
const gate = this.paywallGate;
|
|
18910
|
+
const productId = action === 'purchase' || action === 'subscribe' ? this.paywallProductId : undefined;
|
|
18592
18911
|
if (gate) {
|
|
18593
|
-
this.paywallAction.emit({ action, gate });
|
|
18912
|
+
this.paywallAction.emit({ action, gate, ...(productId ? { productId } : {}) });
|
|
18594
18913
|
}
|
|
18914
|
+
this.paywallProductId = undefined;
|
|
18595
18915
|
this.closePaywall();
|
|
18596
18916
|
}
|
|
18597
18917
|
/**
|
|
@@ -18608,6 +18928,10 @@ class PlayerShellComponent {
|
|
|
18608
18928
|
await this.httpEntitlement.resolveEntitlement({ workId: manifest?.meta?.id ?? '' });
|
|
18609
18929
|
this.paywallService.setSnapshot(this.httpEntitlement.getSnapshot());
|
|
18610
18930
|
}
|
|
18931
|
+
// What the host passed replaces the snapshot wholesale; an age check the
|
|
18932
|
+
// reader already passed on this device still counts.
|
|
18933
|
+
this.restoreAgeVerification();
|
|
18934
|
+
this.buildExtrasList(this.manifestService.getManifest() ?? null);
|
|
18611
18935
|
const gatedPanelId = this.paywallGate?.refId ?? this.getCurrentPanelId();
|
|
18612
18936
|
if (gatedPanelId && this.paywallService.canAccess(gatedPanelId)) {
|
|
18613
18937
|
this.closePaywall();
|
|
@@ -18616,9 +18940,9 @@ class PlayerShellComponent {
|
|
|
18616
18940
|
// ── Age gate ──────────────────────────────────────────────────────────────
|
|
18617
18941
|
static { this.AGE_VERIFIED_STORAGE_KEY = 'pw-age-verified'; }
|
|
18618
18942
|
/** Raise the age gate for a panel and remember where the reader was going. */
|
|
18619
|
-
openAgeGate(chapterId, panelId, transition, cameraMove) {
|
|
18943
|
+
openAgeGate(chapterId, panelId, transition, cameraMove, mutations) {
|
|
18620
18944
|
this.ageGateMinimumAge = this.paywallService.ruleFor(panelId)?.minimumAge ?? 18;
|
|
18621
|
-
this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove };
|
|
18945
|
+
this.pendingAgeGatedNavigation = { chapterId, panelId, transition, cameraMove, mutations };
|
|
18622
18946
|
this.ageGateVisible = true;
|
|
18623
18947
|
this.cdr.markForCheck();
|
|
18624
18948
|
}
|
|
@@ -18648,8 +18972,11 @@ class PlayerShellComponent {
|
|
|
18648
18972
|
this.ageGateVisible = false;
|
|
18649
18973
|
this.pendingAgeGatedNavigation = null;
|
|
18650
18974
|
this.cdr.markForCheck();
|
|
18651
|
-
|
|
18652
|
-
|
|
18975
|
+
// Retry the move: navigateToPanel re-checks the gate, so a rule that
|
|
18976
|
+
// also needs a purchase / subscription now raises the paywall (with its
|
|
18977
|
+
// Buy / Subscribe options) instead of letting the reader through.
|
|
18978
|
+
if (pending) {
|
|
18979
|
+
await this.navigateToPanel(pending.chapterId, pending.panelId, pending.transition, pending.cameraMove, pending.mutations);
|
|
18653
18980
|
}
|
|
18654
18981
|
}
|
|
18655
18982
|
persistAgeVerification(age) {
|
|
@@ -18725,41 +19052,43 @@ class PlayerShellComponent {
|
|
|
18725
19052
|
bookmark.panelId === this.getCurrentPanelId();
|
|
18726
19053
|
}
|
|
18727
19054
|
// ── Branch choices ────────────────────────────────────────────────────────
|
|
18728
|
-
/**
|
|
19055
|
+
/**
|
|
19056
|
+
* At least two paths out of the current panel are open right now (edge
|
|
19057
|
+
* conditions evaluated exactly like the chooser does) — the toolbar shows
|
|
19058
|
+
* "Choices". A closed conditional edge does not count.
|
|
19059
|
+
*/
|
|
18729
19060
|
get hasBranchesAhead() {
|
|
18730
|
-
|
|
18731
|
-
const edges = this.currentChapter?.graph?.edges;
|
|
18732
|
-
if (!panelId || !edges) {
|
|
18733
|
-
return false;
|
|
18734
|
-
}
|
|
18735
|
-
let count = 0;
|
|
18736
|
-
for (const edge of edges) {
|
|
18737
|
-
if (edge.from === panelId && ++count > 1) {
|
|
18738
|
-
return true;
|
|
18739
|
-
}
|
|
18740
|
-
}
|
|
18741
|
-
return false;
|
|
19061
|
+
return this.openEdges().length > 1;
|
|
18742
19062
|
}
|
|
18743
|
-
/** Outgoing edges whose conditions pass right now
|
|
18744
|
-
|
|
19063
|
+
/** Outgoing edges of the current panel whose conditions pass right now. */
|
|
19064
|
+
openEdges() {
|
|
18745
19065
|
const chapter = this.currentChapter;
|
|
18746
19066
|
const panelId = this.getCurrentPanelId();
|
|
18747
|
-
if (!chapter
|
|
19067
|
+
if (!chapter || !panelId) {
|
|
18748
19068
|
return [];
|
|
18749
19069
|
}
|
|
19070
|
+
const outgoing = chapter.graph?.edges?.filter((edge) => edge.from === panelId) ?? [];
|
|
19071
|
+
if (!outgoing.some((edge) => edge.condition)) {
|
|
19072
|
+
// Nothing to evaluate: skip building a variable context on every check.
|
|
19073
|
+
return outgoing;
|
|
19074
|
+
}
|
|
18750
19075
|
const context = this.variableStore.createContext(chapter.id);
|
|
18751
|
-
return
|
|
18752
|
-
.filter((edge) => edge.from === panelId)
|
|
18753
|
-
.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context))
|
|
18754
|
-
.map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter, index) }));
|
|
19076
|
+
return outgoing.filter((edge) => !edge.condition || evaluateJsonLogic(edge.condition, context));
|
|
18755
19077
|
}
|
|
18756
|
-
|
|
19078
|
+
/** Open outgoing edges with reader-facing labels. */
|
|
19079
|
+
computeBranchChoices() {
|
|
19080
|
+
const chapter = this.currentChapter;
|
|
19081
|
+
return chapter
|
|
19082
|
+
? this.openEdges().map((edge, index) => ({ edge, index, label: this.branchLabel(edge, chapter) }))
|
|
19083
|
+
: [];
|
|
19084
|
+
}
|
|
19085
|
+
branchLabel(edge, chapter) {
|
|
18757
19086
|
const fromEdge = this.localizedText(edge.label);
|
|
18758
19087
|
if (fromEdge) {
|
|
18759
19088
|
return fromEdge;
|
|
18760
19089
|
}
|
|
18761
|
-
|
|
18762
|
-
return
|
|
19090
|
+
// Neither: the chooser renders a translated "Option N".
|
|
19091
|
+
return this.localizedText(chapter.panels?.[edge.to]?.title);
|
|
18763
19092
|
}
|
|
18764
19093
|
localizedText(value) {
|
|
18765
19094
|
if (!value) {
|
|
@@ -18784,13 +19113,10 @@ class PlayerShellComponent {
|
|
|
18784
19113
|
to: choice.edge.to,
|
|
18785
19114
|
index: choice.index,
|
|
18786
19115
|
});
|
|
18787
|
-
|
|
18788
|
-
this.variableStore.applyMutations(choice.edge.action, { chapterId: chapter.id });
|
|
18789
|
-
}
|
|
18790
|
-
await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings));
|
|
19116
|
+
await this.navigateToPanel(chapter.id, choice.edge.to, choice.edge.transition ?? this.flowEngine.getDefaultTransition(settings), choice.edge.cameraMove ?? this.flowEngine.getDefaultCameraMove(settings), choice.edge.action);
|
|
18791
19117
|
}
|
|
18792
19118
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
18793
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, providers: [CanvasCameraService], ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "panels", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelFocus", "hotspotActivate", "deadClick"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
19119
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.14", type: PlayerShellComponent, isStandalone: true, selector: "pw-player-shell", inputs: { manifest: "manifest", manifestUrl: "manifestUrl", entitlementAdapter: "entitlementAdapter", entitlementSnapshot: "entitlementSnapshot", entitlementEndpoint: "entitlementEndpoint", readerToken: "readerToken", initialVariables: "initialVariables", locale: "locale", initialChapterId: "initialChapterId", initialPanelId: "initialPanelId", autoplay: "autoplay", secondsPerPanel: "secondsPerPanel", reducedMotion: "reducedMotion", showToolbar: "showToolbar", viewModeOverride: "viewModeOverride" }, outputs: { paywallAction: "paywallAction", ageVerified: "ageVerified", likeChange: "likeChange", bookmarkChange: "bookmarkChange", ready: "ready", panelChange: "panelChange", chapterChange: "chapterChange", error: "error", localeChange: "localeChange", variableChange: "variableChange", navigationAttempt: "navigationAttempt", cameraChange: "cameraChange" }, host: { listeners: { "window:keydown": "handleKeyboard($event)" } }, providers: [CanvasCameraService], usesOnChanges: true, ngImport: i0, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: ViewportComponent, selector: "pw-viewport", inputs: ["panel", "page", "panels", "viewMode", "currentPanelId", "locale", "variableContext", "panX", "panY", "zoom", "reducedMotion", "transition", "interactive", "showOverflowArrows", "enableViewportCulling", "enableLazyLoading", "workBalloonConfig", "characters", "speechEnabled", "graph", "preload"], outputs: ["viewportClick", "performanceMetrics", "transformChange", "layerClick", "swipe", "navigatePrevious", "navigateNext", "panelFocus", "hotspotActivate", "deadClick"] }, { kind: "component", type: CanvasStageComponent, selector: "pw-canvas-stage", inputs: ["canvas", "panels", "graph", "currentPanelId", "visitedPanelIds", "locale", "variableContext", "speechEnabled", "workBalloonConfig", "characters", "reducedMotion", "interactive", "preload"], outputs: ["panelTap", "navigateNext", "navigatePrevious", "viewportClick", "hotspotActivate", "cameraChange"] }, { kind: "component", type: ToolbarComponent, selector: "pw-toolbar", inputs: ["locale", "availableLocales", "viewMode", "pageViewAvailable", "speechEnabled", "audioEnabled", "sfxEnabled", "autoplayEnabled", "secondsPerPanel", "autoplayProgress", "thumbnailsVisible", "hasAlternatives", "hasBranches", "liked", "bookmarked", "showSocial", "visible"], outputs: ["toggleView", "localeChange", "toggleSpeech", "toggleAudio", "toggleSfx", "toggleAutoplay", "secondsPerPanelChange", "toggleThumbnails", "openToc", "openSettings", "openCharacters", "cycleAlternative", "showBranches", "openExtras", "like", "bookmark", "share", "openComments", "close", "openLanguage"] }, { kind: "component", type: ThumbnailStripComponent, selector: "pw-thumbnail-strip", inputs: ["chapters", "currentChapterId", "currentPanelId", "baseUrl", "visible", "lockedPanels"], outputs: ["navigate", "close"] }, { kind: "component", type: TocOverlayComponent, selector: "pw-toc-overlay", inputs: ["manifest", "chapters", "currentChapterId", "currentPanelId", "locale", "baseUrl", "visible"], outputs: ["navigate", "close"] }, { kind: "component", type: PaywallOverlayComponent, selector: "pw-paywall-overlay", inputs: ["visible", "gate", "purchaseOptions", "locale", "title", "message", "allowPreview", "showLogin"], outputs: ["action", "purchase", "close"] }, { kind: "component", type: SettingsModalComponent, selector: "pw-settings-modal", inputs: ["preferences", "availableLocales", "locale", "variables", "variableValues", "visible"], outputs: ["preferencesChange", "localeChange", "variableChange", "close"] }, { kind: "component", type: LanguageModalComponent, selector: "pw-language-modal", inputs: ["visible", "currentLocale", "availableLocales"], outputs: ["closeModal", "localeSelected"] }, { kind: "component", type: CharacterRosterComponent, selector: "pw-character-roster", inputs: ["characters", "locale", "baseUrl", "visible"], outputs: ["characterSelect", "close"] }, { kind: "component", type: ExtrasViewerComponent, selector: "pw-extras-viewer", inputs: ["extras", "initialExtraId", "locale", "baseUrl", "visible", "hasPremiumAccess"], outputs: ["purchase", "close"] }, { kind: "component", type: ActionModalComponent, selector: "pw-action-modal", inputs: ["visible", "locale", "title", "content"], outputs: ["close"] }, { kind: "component", type: AgeGateComponent, selector: "pw-age-gate", inputs: ["visible", "minimumAge", "locale", "warningMessage", "allowDismiss"], outputs: ["verify", "close"] }, { kind: "component", type: BranchChooserComponent, selector: "pw-branch-chooser", inputs: ["visible", "locale", "choices"], outputs: ["choose", "close"] }, { kind: "component", type: ShareModalComponent, selector: "pw-share-modal", inputs: ["shareUrl", "shareTitle", "shareDescription", "visible", "shareable"], outputs: ["close", "share"] }, { kind: "component", type: CommentsDrawerComponent, selector: "pw-comments-drawer", inputs: ["comments", "visible", "loading", "currentPage", "totalPages", "hasMore", "isAuthenticated"], outputs: ["postComment", "loadMore", "likeComment", "close"] }, { kind: "component", type: PwIconComponent, selector: "pw-icon", inputs: ["name"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
18794
19120
|
}
|
|
18795
19121
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerShellComponent, decorators: [{
|
|
18796
19122
|
type: Component,
|
|
@@ -18812,7 +19138,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
18812
19138
|
ShareModalComponent,
|
|
18813
19139
|
CommentsDrawerComponent,
|
|
18814
19140
|
PwIconComponent
|
|
18815
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"ngOnInit()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"manifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [locale]=\"locale\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"manifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
|
|
19141
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"player-shell\" [class.has-error]=\"hasError\">\n <!-- Loading state -->\n @if (loading) {\n <div class=\"loading-container\">\n <div class=\"loading-spinner\"></div>\n <p class=\"loading-text\">{{ 'loading.text' | translate }}</p>\n </div>\n }\n\n <!-- Error state -->\n @if (hasError && !loading) {\n <div class=\"error-container\">\n <div class=\"error-icon\"><pw-icon name=\"lucideTriangleAlert\" /></div>\n <h2 class=\"error-title\">{{ 'error.title' | translate }}</h2>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button class=\"retry-button\" type=\"button\" (click)=\"reload()\">\n {{ 'error.retry' | translate }}\n </button>\n </div>\n }\n\n <!-- Main player -->\n @if (!loading && !hasError && currentPanel) {\n <div class=\"player-content\">\n <!-- Viewport (panel / page view) -->\n @if (viewMode !== 'canvas') {\n <pw-viewport\n [transition]=\"viewportTransition\"\n [panel]=\"effectivePanel || currentPanel\"\n [page]=\"currentPage || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [viewMode]=\"viewMode\"\n [reducedMotion]=\"motionReduced\"\n [locale]=\"locale\"\n [interactive]=\"true\"\n [showOverflowArrows]=\"true\"\n [speechEnabled]=\"speechEnabled\"\n [preload]=\"manifestPreloadSettings\"\n [variableContext]=\"variableContext\"\n (viewportClick)=\"onViewportClick()\"\n (swipe)=\"onSwipe($event)\"\n (navigatePrevious)=\"navigatePrevious()\"\n (navigateNext)=\"navigateNext()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (deadClick)=\"onDeadClick($event)\">\n </pw-viewport>\n }\n\n <!-- Canvas stage (infinite-canvas view, schema 1.4) -->\n @if (viewMode === 'canvas') {\n <pw-canvas-stage\n [canvas]=\"currentChapter?.canvas || null\"\n [panels]=\"resolvedPanels\"\n [graph]=\"currentChapter?.graph || null\"\n [currentPanelId]=\"getCurrentPanelId() || null\"\n [visitedPanelIds]=\"visitedPanelIds\"\n [locale]=\"locale\"\n [variableContext]=\"variableContext\"\n [speechEnabled]=\"speechEnabled\"\n [workBalloonConfig]=\"manifestBalloonConfig\"\n [characters]=\"manifestCharacters\"\n [reducedMotion]=\"motionReduced\"\n [interactive]=\"true\"\n [preload]=\"manifestPreloadSettings\"\n (panelTap)=\"onCanvasPanelTap($event)\"\n (navigateNext)=\"navigateNext()\"\n (navigatePrevious)=\"navigatePrevious()\"\n (viewportClick)=\"onViewportClick()\"\n (hotspotActivate)=\"onHotspotActivate($event)\"\n (cameraChange)=\"cameraChange.emit($event)\">\n </pw-canvas-stage>\n }\n\n <!-- Full Toolbar -->\n <pw-toolbar\n [visible]=\"toolbarVisible\"\n [locale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n [viewMode]=\"viewMode\"\n [pageViewAvailable]=\"pageViewAvailable\"\n [speechEnabled]=\"speechEnabled\"\n [audioEnabled]=\"audioEnabled\"\n [sfxEnabled]=\"sfxEnabled\"\n [autoplayEnabled]=\"autoplayEnabled\"\n [autoplayProgress]=\"autoplayProgress\"\n [secondsPerPanel]=\"secondsPerPanel\"\n [thumbnailsVisible]=\"thumbnailsVisible\"\n [hasAlternatives]=\"!!(currentPanel && currentPanel.variants && currentPanel.variants.length)\"\n [hasBranches]=\"hasBranchesAhead\"\n [showSocial]=\"true\"\n [liked]=\"liked\"\n [bookmarked]=\"bookmarked\"\n (toggleView)=\"onToggleView()\"\n (localeChange)=\"onLocaleChange($event)\"\n (toggleSpeech)=\"onToggleSpeech()\"\n (toggleAudio)=\"onToggleAudio()\"\n (toggleSfx)=\"onToggleSfx()\"\n (toggleAutoplay)=\"onToggleAutoplay()\"\n (secondsPerPanelChange)=\"onSecondsPerPanelChange($event)\"\n (toggleThumbnails)=\"onToggleThumbnails()\"\n (openToc)=\"onOpenToc()\"\n (openSettings)=\"onOpenSettings()\"\n (openLanguage)=\"languageModalVisible = true\"\n (openCharacters)=\"onOpenCharacters()\"\n (cycleAlternative)=\"onCycleAlternative()\"\n (showBranches)=\"onShowBranches()\"\n (openExtras)=\"onOpenExtras()\"\n (like)=\"onLike()\"\n (bookmark)=\"onBookmark()\"\n (share)=\"onShare()\"\n (openComments)=\"onOpenComments()\"\n (close)=\"toggleToolbar()\">\n </pw-toolbar>\n\n <!-- Thumbnail Strip -->\n <pw-thumbnail-strip\n [visible]=\"thumbnailsVisible\"\n [chapters]=\"loadedManifest?.chapters || []\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [baseUrl]=\"''\"\n [lockedPanels]=\"[]\"\n (navigate)=\"onThumbnailNavigate($event)\"\n (close)=\"onThumbnailsClose()\">\n </pw-thumbnail-strip>\n\n <!-- Active variant indicator -->\n @if (activeVariantId) {\n <div class=\"variant-active\" role=\"status\" aria-live=\"polite\"\n [attr.aria-label]=\"'Active panel variant: ' + activeVariantId\">\n {{ activeVariantId }}\n </div>\n }\n\n <!-- PanelWave floating icon -->\n @if (!toolbarVisible) {\n <button \n class=\"floating-icon\"\n type=\"button\"\n aria-label=\"Show toolbar\"\n (click)=\"toggleToolbar()\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z\"/>\n </svg>\n </button>\n }\n\n <!-- Modals -->\n @if (paywallVisible && paywallGate) {\n <pw-paywall-overlay\n [visible]=\"paywallVisible\"\n [gate]=\"paywallGate\"\n [purchaseOptions]=\"paywallGate.options || []\"\n [locale]=\"locale\"\n (purchase)=\"paywallProductId = $event\"\n (action)=\"onPaywallAction($event)\"\n (close)=\"closePaywall()\">\n </pw-paywall-overlay>\n }\n\n @if (ageGateVisible) {\n <pw-age-gate\n [visible]=\"ageGateVisible\"\n [minimumAge]=\"ageGateMinimumAge\"\n [locale]=\"locale\"\n [allowDismiss]=\"true\"\n (verify)=\"onAgeGateVerify($event)\"\n (close)=\"closeAgeGate()\">\n </pw-age-gate>\n }\n\n @if (branchChooserVisible) {\n <pw-branch-chooser\n [visible]=\"branchChooserVisible\"\n [locale]=\"locale\"\n [choices]=\"branchChoices\"\n (choose)=\"onBranchChosen($event)\"\n (close)=\"branchChooserVisible = false\">\n </pw-branch-chooser>\n }\n\n @if (tocVisible) {\n <pw-toc-overlay\n [visible]=\"tocVisible\"\n [manifest]=\"loadedManifest\"\n [currentChapterId]=\"currentChapter?.id\"\n [currentPanelId]=\"getCurrentPanelId()\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (navigate)=\"onTocNavigate($event)\"\n (close)=\"tocVisible = false\">\n </pw-toc-overlay>\n }\n\n @if (settingsVisible) {\n <pw-settings-modal\n [visible]=\"settingsVisible\"\n [variables]=\"variableDefinitions\"\n [variableValues]=\"settingsVariableValues\"\n [availableLocales]=\"availableLocales\"\n [locale]=\"locale\"\n [preferences]=\"settingsPreferences\"\n (preferencesChange)=\"onPreferencesChange($event)\"\n (variableChange)=\"onSettingsVariableChange($event)\"\n (close)=\"settingsVisible = false\">\n </pw-settings-modal>\n }\n\n @if (languageModalVisible) {\n <pw-language-modal\n [visible]=\"languageModalVisible\"\n [currentLocale]=\"locale\"\n [availableLocales]=\"availableLocales\"\n (localeSelected)=\"onLocaleChange($event)\"\n (closeModal)=\"languageModalVisible = false\">\n </pw-language-modal>\n }\n\n @if (charactersVisible) {\n <pw-character-roster\n [visible]=\"charactersVisible\"\n [characters]=\"characters\"\n [locale]=\"locale\"\n [baseUrl]=\"''\"\n (close)=\"charactersVisible = false\">\n </pw-character-roster>\n }\n\n @if (extrasVisible) {\n <pw-extras-viewer\n [visible]=\"extrasVisible\"\n [extras]=\"extrasList\"\n [initialExtraId]=\"extrasInitialId\"\n [locale]=\"locale\"\n (close)=\"extrasVisible = false; extrasInitialId = undefined\">\n </pw-extras-viewer>\n }\n\n @if (actionModalVisible) {\n <pw-action-modal\n [visible]=\"actionModalVisible\"\n [locale]=\"locale\"\n [title]=\"actionModalTitle\"\n [content]=\"actionModalContent\"\n (close)=\"actionModalVisible = false\">\n </pw-action-modal>\n }\n\n @if (shareVisible) {\n <pw-share-modal\n [visible]=\"shareVisible\"\n [shareable]=\"currentPanel.shareable ?? true\"\n (close)=\"shareVisible = false\">\n </pw-share-modal>\n }\n\n @if (commentsVisible) {\n <pw-comments-drawer\n [visible]=\"commentsVisible\"\n (close)=\"commentsVisible = false\">\n </pw-comments-drawer>\n }\n </div>\n }\n</div>\n", styles: [".player-shell{position:relative;width:100%;height:100%;overflow:hidden;background:#000;color:#fff}.player-shell.has-error{background:#1a1a1a}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}.loading-spinner{width:48px;height:48px;border:4px solid rgba(255,255,255,.1);border-top-color:#fff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.loading-text{font-size:16px;color:#ffffffb3;margin:0}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:24px;text-align:center;gap:16px}.error-icon{font-size:64px;line-height:1}.error-title{font-size:24px;font-weight:600;margin:0;color:#fff}.error-message{font-size:16px;color:#ffffffb3;margin:0;max-width:500px}.retry-button{margin-top:16px;padding:12px 24px;font-size:16px;font-weight:500;color:#fff;background:#0078d7;border:none;border-radius:4px;cursor:pointer;transition:background .2s ease}.retry-button:hover{background:#005a9e}.retry-button:active{background:#004578}.player-content{position:relative;width:100%;height:100%}.toolbar-container{position:absolute;bottom:0;left:0;right:0;z-index:100;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:24px 16px 16px;animation:slideUp .3s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}.toolbar{display:flex;justify-content:center;align-items:center;gap:12px;max-width:1200px;margin:0 auto}.toolbar-button{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:14px;font-weight:500;color:#fff;background:#ffffff1a;border:1px solid rgba(255,255,255,.2);border-radius:4px;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.toolbar-button:hover{background:#fff3;border-color:#ffffff4d}.toolbar-button:active{background:#ffffff26;transform:scale(.98)}.button-icon{font-size:18px;line-height:1}.button-label{font-size:14px}.variant-active{position:absolute;bottom:20px;left:20px;z-index:50;padding:4px 10px;font-size:12px;font-weight:600;color:#fff;background:#0078d7d9;border-radius:12px;box-shadow:0 2px 8px #0000004d;pointer-events:none}.floating-icon{position:absolute;bottom:20px;right:20px;z-index:50;width:56px;height:56px;display:flex;align-items:center;justify-content:center;color:#fff;background:#0078d7e6;border:none;border-radius:50%;box-shadow:0 4px 12px #0000004d;cursor:pointer;transition:all .2s ease;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.floating-icon:hover{background:#0078d7;transform:scale(1.1);box-shadow:0 6px 16px #0006}.floating-icon:active{transform:scale(1.05)}.floating-icon svg{width:24px;height:24px}@media (max-width: 768px){.toolbar-container{padding:16px 12px 12px}.toolbar{gap:8px}.toolbar-button{padding:8px 12px;font-size:12px}.button-label{display:none}.button-icon{font-size:16px}.floating-icon{width:48px;height:48px;bottom:16px;right:16px}.floating-icon svg{width:20px;height:20px}}@media (prefers-reduced-motion: reduce){.toolbar-container{animation:none}.loading-spinner{animation:none;border-top-color:#ffffff4d}.toolbar-button:active,.floating-icon:hover{transform:none}}\n"] }]
|
|
18816
19142
|
}], propDecorators: { manifest: [{
|
|
18817
19143
|
type: Input
|
|
18818
19144
|
}], manifestUrl: [{
|
|
@@ -18954,18 +19280,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
18954
19280
|
|
|
18955
19281
|
class PlayerComponent {
|
|
18956
19282
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
18957
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PlayerComponent, isStandalone: true, selector: "pw-player", ngImport: i0, template: `
|
|
18958
|
-
<p>
|
|
18959
|
-
player works!
|
|
18960
|
-
</p>
|
|
19283
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: PlayerComponent, isStandalone: true, selector: "pw-player", ngImport: i0, template: `
|
|
19284
|
+
<p>
|
|
19285
|
+
player works!
|
|
19286
|
+
</p>
|
|
18961
19287
|
`, isInline: true, styles: [""] }); }
|
|
18962
19288
|
}
|
|
18963
19289
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: PlayerComponent, decorators: [{
|
|
18964
19290
|
type: Component,
|
|
18965
|
-
args: [{ selector: 'pw-player', imports: [], template: `
|
|
18966
|
-
<p>
|
|
18967
|
-
player works!
|
|
18968
|
-
</p>
|
|
19291
|
+
args: [{ selector: 'pw-player', imports: [], template: `
|
|
19292
|
+
<p>
|
|
19293
|
+
player works!
|
|
19294
|
+
</p>
|
|
18969
19295
|
` }]
|
|
18970
19296
|
}] });
|
|
18971
19297
|
|
|
@@ -18978,5 +19304,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImpo
|
|
|
18978
19304
|
* Generated bundle index. Do not edit.
|
|
18979
19305
|
*/
|
|
18980
19306
|
|
|
18981
|
-
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
|
|
19307
|
+
export { ANONYMOUS_READER, ActionModalComponent, AgeGateComponent, AudioEngineService, BranchChooserComponent, CANVAS_PANEL_BUDGET, CanvasCameraService, CanvasStageComponent, ComicBalloon, DEFAULT_BALLOON_CONFIG, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_VIDEO_MUTED, DEFAULT_VIDEO_PLAY_MODE, DEFAULT_VIDEO_START_MODE, EntitlementError, EntitlementService, ExportService, FlowEngineService, HotspotActionService, HotspotsOverlayComponent, HttpEntitlementAdapter, ImageCacheService, ManifestService, MockEntitlementAdapter, NullEntitlementAdapter, PANEL_AUDIO_LEAVE_FADE_MS, PanelAudioService, PaywallOverlayComponent, PaywallService, PlayerComponent, PlayerEvent, PlayerService, PlayerShellComponent, PlayerStateService, PluginHostService, PluginSandboxComponent, PreloadService, PwIconComponent, RESERVED_NAMESPACES, SpeechBubblesComponent, TrackingService, TranslationService, UserGestureService, VISIBILITY_THRESHOLD, VariableStoreService, VideoControllerService, VideoLayerComponent, VideoSequencerService, VisibilityService, applyVariantOverrides, balloonConfigToRenderOptions, balloonConfigToTailOptions, busForRole, chapterOrderFromManifest, createBalloon, evaluatePanelAccess, evaluateWorkAccess, findWorkGateRule, fromManifestRule, isExtraLocked, mergeBalloonConfig, readingOrderFromManifest, resolveMuted, resolvePanelVariant, resolvePanels, resolvePlayMode, resolveStartMode, resolveVideoConfig, rulesFromManifest, satisfiesRule, selectVariant, toEntitlementStatus };
|
|
18982
19308
|
//# sourceMappingURL=panelwave-player.mjs.map
|