@flyos/design-system 1.3.0 → 1.4.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.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, computed, Injectable, inject, TemplateRef, ViewContainerRef, effect, Input, Directive, ErrorHandler, Pipe, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideAppInitializer, PLATFORM_ID, DestroyRef, ElementRef, input, output, afterNextRender, HostListener, ViewChild, NgZone, viewChild, DOCUMENT as DOCUMENT$1, EventEmitter, Output, forwardRef, model, viewChildren, untracked, Injector, ViewEncapsulation, Renderer2, isDevMode, contentChildren, contentChild } from '@angular/core';
3
- import { catchError, throwError, Subject, from, firstValueFrom, of, ReplaySubject, retry, timer, map } from 'rxjs';
2
+ import { InjectionToken, signal, computed, Injectable, inject, TemplateRef, ViewContainerRef, effect, Input, Directive, ErrorHandler, isDevMode, Pipe, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideAppInitializer, PLATFORM_ID, DestroyRef, ElementRef, input, output, afterNextRender, HostListener, ViewChild, NgZone, viewChild, DOCUMENT as DOCUMENT$1, EventEmitter, Output, forwardRef, model, viewChildren, untracked, Injector, ViewEncapsulation, Renderer2, contentChildren, contentChild } from '@angular/core';
3
+ import { catchError, throwError, Subject, from, firstValueFrom, of, Observable, map, ReplaySubject, retry, timer } from 'rxjs';
4
4
  import { HttpClient, HttpParams, HttpHeaders, HttpErrorResponse, HttpEventType } from '@angular/common/http';
5
5
  import { switchMap, debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
6
6
  import { HubConnectionBuilder, HttpTransportType, LogLevel, HubConnectionState } from '@microsoft/signalr';
@@ -48,7 +48,7 @@ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
48
48
  // tools/publish-library.ps1 at bump time, and asserted by the spec beside this file.
49
49
  // Used only for the diagnostic message; the duplicate-instance detection itself is
50
50
  // version-agnostic, so a stale literal misnames a fork rather than hiding one.
51
- const FLY_DS_VERSION = '1.3.0';
51
+ const FLY_DS_VERSION = '1.4.0';
52
52
  const FLY_DS_REGISTRY_KEY = '__FLY_DS_INSTANCES__';
53
53
  /**
54
54
  * Records this design-system instance on the shared `scope` and returns the
@@ -2697,14 +2697,18 @@ function normalizeLocaleJson(raw) {
2697
2697
  /**
2698
2698
  * Shared I18nService for the shell and Business Apps.
2699
2699
  *
2700
- * **Merge order** (later keys win): DS baseline → shell layer remote bundles
2701
- * in registration order.
2700
+ * **Merge order** (later keys win): DS baseline → remote bundles in registration
2701
+ * order shell layer.
2702
2702
  *
2703
2703
  * - Baseline: {@link DS_BASELINE_LOCALES} — built-in strings for DS components
2704
2704
  * (markdown editor, entity lookup) so they render localized labels even in a
2705
2705
  * standalone consumer that never populated the shell layer. Always overridable.
2706
- * - Shell: `setShellTranslations()` after loading `locale/{lang}.json` and API overrides.
2707
- * - Remotes: `loadBundle()` per manifest `localeBaseUrl`.
2706
+ * - Remotes: `loadBundle()` per manifest `localeBaseUrl`. May ADD any key and may
2707
+ * override the baseline, but cannot redefine a key the shell owns — the dictionary
2708
+ * is global, so allowing that let one remote restyle the whole desktop's wording.
2709
+ * - Shell: `setShellTranslations()` after loading `locale/{lang}.json` and API
2710
+ * overrides. Highest priority, which also makes the per-tenant
2711
+ * `/api/i18n/desktop-shell/{lang}` overrides authoritative over every app.
2708
2712
  */
2709
2713
  class I18nService {
2710
2714
  // NOTE (NG0200 / federation): the ErrorHandler ↔ I18nService cycle is broken on the
@@ -2719,20 +2723,43 @@ class I18nService {
2719
2723
  _bundleOrder = signal([], ...(ngDevMode ? [{ debugName: "_bundleOrder" }] : /* istanbul ignore next */ []));
2720
2724
  _locale = signal('en', ...(ngDevMode ? [{ debugName: "_locale" }] : /* istanbul ignore next */ []));
2721
2725
  _version = signal(0, ...(ngDevMode ? [{ debugName: "_version" }] : /* istanbul ignore next */ []));
2726
+ /** `${bundleId}|${locale}` pairs already reported by {@link warnOnSuppressedShellKeys}. */
2727
+ _warnedCollisions = new Set();
2722
2728
  /** Built-in DS strings for the active locale (falls back to `en` for any
2723
2729
  * locale we don't ship). Lowest-priority layer — always overridable. */
2724
2730
  _baseline = computed(() => DS_BASELINE_LOCALES[this._locale()] ?? DS_BASELINE_LOCALES['en'] ?? {}, ...(ngDevMode ? [{ debugName: "_baseline" }] : /* istanbul ignore next */ []));
2731
+ /**
2732
+ * Merge order, lowest priority first: DS baseline → remote bundles (registration
2733
+ * order) → shell.
2734
+ *
2735
+ * The shell layer is LAST, and that is the whole point. Translations live in one
2736
+ * flat, global dictionary with no per-app scoping, so when remote bundles were
2737
+ * merged last, any remote that happened to define a key the shell also defines
2738
+ * silently rewrote that string for the ENTIRE desktop — every other app included.
2739
+ *
2740
+ * That was not hypothetical: the Circles remote's bundle shares 63 keys with the
2741
+ * shell and differed on 27 of them, one being `common.label.dashboard`, which is
2742
+ * how it came to own the Arabic display name of a Core App it has nothing to do
2743
+ * with. The symptom is near-impossible to chase from the outside — editing the
2744
+ * shell's own `locale/{lang}.json` simply has no effect at runtime, with nothing
2745
+ * logged.
2746
+ *
2747
+ * A remote may still override the DS baseline (bundles are merged after it), which
2748
+ * is the layer explicitly documented as always-overridable, and it may still add
2749
+ * any key the shell does not define. What it can no longer do is redefine a string
2750
+ * the shell owns. {@link loadBundle} warns in dev when a bundle tries.
2751
+ */
2725
2752
  _merged = computed(() => {
2726
2753
  const shell = this._shell();
2727
2754
  const order = this._bundleOrder();
2728
2755
  const bundles = this._bundles();
2729
- let out = { ...this._baseline(), ...shell };
2756
+ let out = { ...this._baseline() };
2730
2757
  for (const id of order) {
2731
2758
  const b = bundles[id];
2732
2759
  if (b)
2733
2760
  out = { ...out, ...b };
2734
2761
  }
2735
- return out;
2762
+ return { ...out, ...shell };
2736
2763
  }, ...(ngDevMode ? [{ debugName: "_merged" }] : /* istanbul ignore next */ []));
2737
2764
  locale = this._locale.asReadonly();
2738
2765
  version = this._version.asReadonly();
@@ -2746,6 +2773,17 @@ class I18nService {
2746
2773
  this._shell.set(data);
2747
2774
  this._locale.set(lang);
2748
2775
  this.bump();
2776
+ // Bundles routinely land BEFORE this — the shell kicks off remote locale loads
2777
+ // and its own fetch together, and the remotes often win the race. Re-checking
2778
+ // here is what makes the diagnostic order-independent; without it the warning
2779
+ // fired only when the shell happened to arrive first, which in practice was
2780
+ // never.
2781
+ const bundles = this._bundles();
2782
+ for (const id of this._bundleOrder()) {
2783
+ const b = bundles[id];
2784
+ if (b)
2785
+ this.warnOnSuppressedShellKeys(id, b);
2786
+ }
2749
2787
  }
2750
2788
  setLanguage(lang) {
2751
2789
  this._locale.set(lang);
@@ -2771,6 +2809,7 @@ class I18nService {
2771
2809
  const data = normalizeLocaleJson(await resp.json());
2772
2810
  if (abortSignal?.aborted)
2773
2811
  return false;
2812
+ this.warnOnSuppressedShellKeys(id, data);
2774
2813
  this._bundles.update((b) => ({ ...b, [id]: data }));
2775
2814
  this._bundleOrder.update((order) => (order.includes(id) ? order : [...order, id]));
2776
2815
  this._locale.set(lang);
@@ -2785,6 +2824,45 @@ class I18nService {
2785
2824
  return false;
2786
2825
  }
2787
2826
  }
2827
+ /**
2828
+ * Dev-only: report keys a bundle defines that the shell already owns.
2829
+ *
2830
+ * These are now silently ignored (the shell wins), so without this the app would
2831
+ * simply not use strings the remote shipped and nobody would know which ones. The
2832
+ * previous precedence was worse — it applied them, changing wording across the
2833
+ * whole desktop, also silently. Either way the failure is invisible, which is the
2834
+ * argument for saying it out loud exactly once, when the bundle loads.
2835
+ *
2836
+ * A hit is not automatically a bug: an app legitimately reuses shared keys it does
2837
+ * not intend to redefine, so only keys whose VALUE differs are worth reporting.
2838
+ * The fix on the app's side is to namespace the key under its own app id.
2839
+ */
2840
+ warnOnSuppressedShellKeys(id, data) {
2841
+ if (!isDevMode())
2842
+ return;
2843
+ const shell = this._shell();
2844
+ // Nothing to conflict with yet — a standalone consumer, or a bundle that raced
2845
+ // ahead of setShellTranslations. Not a collision, so stay quiet; the shell
2846
+ // re-runs this check for every stored bundle once it lands.
2847
+ if (Object.keys(shell).length === 0)
2848
+ return;
2849
+ const suppressed = Object.keys(data).filter((k) => k in shell && shell[k] !== data[k]);
2850
+ if (suppressed.length === 0)
2851
+ return;
2852
+ // Both entry points can reach the same (bundle, locale) pair — a bundle stored
2853
+ // before the shell is checked again when the shell lands, and every locale
2854
+ // switch replays both. Report each pair once so a real signal doesn't drown in
2855
+ // repeats.
2856
+ const seen = `${id}|${this._locale()}`;
2857
+ if (this._warnedCollisions.has(seen))
2858
+ return;
2859
+ this._warnedCollisions.add(seen);
2860
+ const sample = suppressed.slice(0, 10).join(', ');
2861
+ const more = suppressed.length > 10 ? `, …and ${suppressed.length - 10} more` : '';
2862
+ console.warn(`[I18n] Locale bundle "${id}" redefines ${suppressed.length} key(s) the shell owns; ` +
2863
+ `the shell's values win and the bundle's are ignored. Namespace these under "${id}." ` +
2864
+ `if they are meant to be app-specific: ${sample}${more}`);
2865
+ }
2788
2866
  removeBundle(id) {
2789
2867
  this._bundles.update((b) => {
2790
2868
  const next = { ...b };
@@ -3994,6 +4072,290 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
3994
4072
  args: [{ providedIn: 'root' }]
3995
4073
  }] });
3996
4074
 
4075
+ /** `POST /api/users/resolve` accepts at most this many ids per call (platform contract). */
4076
+ const RESOLVE_BATCH_SIZE = 200;
4077
+ /**
4078
+ * **`FlyUserDirectoryService`** — the one id→person adapter for the platform user
4079
+ * directory (`GET /api/users/lookup?q=`, `POST /api/users/resolve`).
4080
+ *
4081
+ * Every app that renders a *list* of people — comment authors, contributors,
4082
+ * approvers, credits, audit rows — needs the same thing: turn a `userId` into a
4083
+ * name without issuing one request per row. Before this service each app wrote
4084
+ * its own copy (Circles `UserResolverService`, Thoughts `UserLookupService`,
4085
+ * plus hand-rolled `/api/users/resolve` calls in the shell and admin portal),
4086
+ * and every copy re-derived the same read-through cache. This is that cache,
4087
+ * once, in the design system.
4088
+ *
4089
+ * The HTTP call rides the **host's** `HttpClient`, so the host's gateway routing,
4090
+ * auth interceptor, and correlation headers all apply — the same arrangement
4091
+ * `fly-entity-lookup` uses. The DS never configures a base URL of its own.
4092
+ *
4093
+ * ## `label()` is read-through — hosts do not prime
4094
+ *
4095
+ * {@link label} is a *signal-backed* accessor: on a cache miss it queues the id
4096
+ * and returns `null`, then the queued ids are resolved in ONE batched request a
4097
+ * microtask later. Because the read happens inside Angular's reactive context,
4098
+ * a template calling `label(id)` re-renders by itself when the answer lands —
4099
+ * no `computed` shadow state, no `prime()` call at each fetch site.
4100
+ *
4101
+ * That last point is the whole reason this exists. The prime-then-read shape it
4102
+ * replaces required every host to (a) keep the raw page separate from the mapped
4103
+ * page, (b) derive the mapped page through a `computed`, and (c) remember to
4104
+ * prime on *every* fetch path — root load, load-more, load-replies, refetch. Miss
4105
+ * one path and that path silently renders GUIDs, with nothing failing. Making the
4106
+ * cache read-through deletes the whole bug class.
4107
+ *
4108
+ * ## Failure is graceful, and never a retry storm
4109
+ *
4110
+ * A failed lookup caches nothing, so callers fall back to whatever they render
4111
+ * for an unresolved id (typically the raw id). Deliberate asymmetry in retry
4112
+ * behaviour, because a render-driven read must not hammer a broken endpoint:
4113
+ *
4114
+ * - **Render-driven** ({@link label}) attempts an id **once**. After a failure it
4115
+ * stays unresolved for the session rather than re-requesting on every
4116
+ * change-detection pass.
4117
+ * - **Explicit** ({@link prime}, {@link resolve}, {@link invalidate}) always
4118
+ * re-attempts. A host re-fetching a page therefore retries naturally, and a
4119
+ * retry button can call {@link invalidate}.
4120
+ *
4121
+ * Unknown / cross-tenant ids are omitted by the endpoint rather than erroring, so
4122
+ * they simply never resolve — which is also the correct behaviour for a
4123
+ * standalone External App where `/api/users/*` isn't reachable at all.
4124
+ *
4125
+ * @example Comment thread — one binding, no plumbing
4126
+ * ```html
4127
+ * <fly-comment-thread
4128
+ * [rootPage]="rootPage()"
4129
+ * [resolveDisplayName]="directory.label" />
4130
+ * ```
4131
+ * ```ts
4132
+ * protected readonly directory = inject(FlyUserDirectoryService);
4133
+ * ```
4134
+ *
4135
+ * @example People picker — search + prefill
4136
+ * ```html
4137
+ * <fly-people-picker
4138
+ * [searchFn]="directory.search()"
4139
+ * [initialSelection]="selected()" />
4140
+ * ```
4141
+ */
4142
+ class FlyUserDirectoryService {
4143
+ http = inject(HttpClient);
4144
+ /** id → person, for everything resolved so far. Signal-backed so {@link label} is reactive. */
4145
+ entries = signal({}, ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
4146
+ /** Ids with a request in the air — so a re-render cannot duplicate an in-flight lookup. */
4147
+ inFlight = new Set();
4148
+ /**
4149
+ * Ids {@link label} has already attempted at least once. Checked ONLY on the
4150
+ * render-driven path: it is what stops a persistently failing endpoint from
4151
+ * being re-requested on every change-detection pass. Explicit callers ignore it.
4152
+ */
4153
+ attempted = new Set();
4154
+ /** Ids queued by {@link label}, awaiting the batched microtask flush. */
4155
+ queued = new Set();
4156
+ flushScheduled = false;
4157
+ // ── Read ─────────────────────────────────────────────────────────────────────
4158
+ /**
4159
+ * Display name for `id`, or `null` when it is not (yet) resolved — on a miss the
4160
+ * id is queued for the next batched resolve. Null rather than the raw id on
4161
+ * purpose: a GUID is not a name, and the caller should render its own fallback
4162
+ * (or localized placeholder) instead of the DS guessing one.
4163
+ *
4164
+ * Bound as a value (`[resolveDisplayName]="directory.label"`), so it is an arrow
4165
+ * property rather than a method — `this` stays correct without the host wrapping
4166
+ * it in a closure.
4167
+ */
4168
+ label = (id) => {
4169
+ if (!id)
4170
+ return null;
4171
+ const hit = this.entries()[id];
4172
+ if (hit)
4173
+ return hit.displayName;
4174
+ this.queueForRender(id);
4175
+ return null;
4176
+ };
4177
+ /**
4178
+ * The full resolved person for `id` (name, email, avatar), or `null` when not yet
4179
+ * known. Same read-through queueing as {@link label} — use it when a surface can
4180
+ * render an avatar rather than just a name.
4181
+ */
4182
+ option = (id) => {
4183
+ if (!id)
4184
+ return null;
4185
+ const hit = this.entries()[id];
4186
+ if (hit)
4187
+ return hit;
4188
+ this.queueForRender(id);
4189
+ return null;
4190
+ };
4191
+ // ── Explicit population ──────────────────────────────────────────────────────
4192
+ /**
4193
+ * Eagerly resolves any ids not already known, merging them into the cache.
4194
+ * Optional — {@link label} resolves on demand — but useful to warm the cache
4195
+ * before a render, or to force a retry after a failed auto-attempt (explicit
4196
+ * calls are not subject to the once-only rule; see the class doc).
4197
+ */
4198
+ prime(ids) {
4199
+ const fresh = this.distinct(ids).filter(id => !this.entries()[id] && !this.inFlight.has(id));
4200
+ if (fresh.length === 0)
4201
+ return;
4202
+ this.fetch(fresh);
4203
+ }
4204
+ /**
4205
+ * Bulk-resolves ids to picker options — for prefilling `fly-people-picker`'s
4206
+ * `[initialSelection]` from stored ids. Requests are chunked to the endpoint's
4207
+ * {@link RESOLVE_BATCH_SIZE} cap rather than truncated, so a large selection
4208
+ * resolves fully instead of silently losing its tail. Resolved rows also land in
4209
+ * the cache, so a later {@link label} for the same id is free. Empty in → empty
4210
+ * out with no request.
4211
+ */
4212
+ resolve(ids) {
4213
+ const distinct = this.distinct(ids);
4214
+ if (distinct.length === 0)
4215
+ return of([]);
4216
+ return new Observable(subscriber => {
4217
+ const chunks = this.chunk(distinct);
4218
+ const collected = [];
4219
+ let remaining = chunks.length;
4220
+ let failed = false;
4221
+ for (const batch of chunks) {
4222
+ this.post(batch).subscribe({
4223
+ next: rows => {
4224
+ this.merge(rows);
4225
+ collected.push(...rows);
4226
+ },
4227
+ error: (err) => {
4228
+ if (failed)
4229
+ return;
4230
+ failed = true;
4231
+ subscriber.error(err);
4232
+ },
4233
+ complete: () => {
4234
+ if (failed)
4235
+ return;
4236
+ if (--remaining === 0) {
4237
+ subscriber.next(collected);
4238
+ subscriber.complete();
4239
+ }
4240
+ },
4241
+ });
4242
+ }
4243
+ });
4244
+ }
4245
+ /**
4246
+ * Drops cached entries so the next read re-resolves them — the retry hook for a
4247
+ * host that surfaces a "couldn't load names" affordance. Omit `ids` to clear the
4248
+ * whole cache.
4249
+ */
4250
+ invalidate(ids) {
4251
+ if (!ids) {
4252
+ this.entries.set({});
4253
+ this.inFlight.clear();
4254
+ this.attempted.clear();
4255
+ this.queued.clear();
4256
+ return;
4257
+ }
4258
+ const targets = this.distinct(ids);
4259
+ if (targets.length === 0)
4260
+ return;
4261
+ targets.forEach(id => {
4262
+ this.inFlight.delete(id);
4263
+ this.attempted.delete(id);
4264
+ });
4265
+ this.entries.update(prev => {
4266
+ const next = { ...prev };
4267
+ targets.forEach(id => delete next[id]);
4268
+ return next;
4269
+ });
4270
+ }
4271
+ // ── Search ───────────────────────────────────────────────────────────────────
4272
+ /**
4273
+ * A `searchFn` over the platform users directory — bind straight to
4274
+ * `<fly-people-picker [searchFn]>`. Results are cached too, so picking someone
4275
+ * makes their name available to {@link label} without another round-trip.
4276
+ */
4277
+ search() {
4278
+ return (query) => this.http
4279
+ .get('/api/users/lookup', { params: { q: query } })
4280
+ .pipe(map(res => (res.data ?? []).map(toOption)), map(rows => {
4281
+ this.merge(rows);
4282
+ return rows;
4283
+ }));
4284
+ }
4285
+ // ── Internals ────────────────────────────────────────────────────────────────
4286
+ /** Queues a render-driven miss, honouring the once-only rule. */
4287
+ queueForRender(id) {
4288
+ if (this.entries()[id] || this.inFlight.has(id) || this.attempted.has(id) || this.queued.has(id))
4289
+ return;
4290
+ this.queued.add(id);
4291
+ if (this.flushScheduled)
4292
+ return;
4293
+ this.flushScheduled = true;
4294
+ // A microtask, not a synchronous call: `label()` runs during template
4295
+ // evaluation, and firing the request (and its signal write) inline would
4296
+ // mutate state mid-change-detection. Deferring also batches every miss in
4297
+ // the pass into ONE request — a 20-comment page costs one round-trip.
4298
+ queueMicrotask(() => this.flush());
4299
+ }
4300
+ flush() {
4301
+ this.flushScheduled = false;
4302
+ const batch = [...this.queued];
4303
+ this.queued.clear();
4304
+ if (batch.length === 0)
4305
+ return;
4306
+ batch.forEach(id => this.attempted.add(id));
4307
+ this.fetch(batch);
4308
+ }
4309
+ /** Issues the (chunked) resolve calls for `ids` and merges whatever comes back. */
4310
+ fetch(ids) {
4311
+ for (const batch of this.chunk(ids)) {
4312
+ batch.forEach(id => this.inFlight.add(id));
4313
+ this.post(batch).subscribe({
4314
+ next: rows => this.merge(rows),
4315
+ // Unknown / cross-tenant ids are omitted by the endpoint rather than
4316
+ // erroring, so reaching here means a transport-level failure. Cache
4317
+ // nothing — a wrong answer is worse than an unresolved one.
4318
+ error: () => batch.forEach(id => this.inFlight.delete(id)),
4319
+ complete: () => batch.forEach(id => this.inFlight.delete(id)),
4320
+ });
4321
+ }
4322
+ }
4323
+ post(ids) {
4324
+ return this.http
4325
+ .post('/api/users/resolve', { ids })
4326
+ .pipe(map(res => (res.data ?? []).map(toOption)));
4327
+ }
4328
+ merge(rows) {
4329
+ if (rows.length === 0)
4330
+ return;
4331
+ this.entries.update(prev => {
4332
+ const next = { ...prev };
4333
+ rows.forEach(r => (next[r.id] = r));
4334
+ return next;
4335
+ });
4336
+ }
4337
+ distinct(ids) {
4338
+ return [...new Set(ids.filter((id) => !!id))];
4339
+ }
4340
+ chunk(ids) {
4341
+ const out = [];
4342
+ for (let i = 0; i < ids.length; i += RESOLVE_BATCH_SIZE) {
4343
+ out.push(ids.slice(i, i + RESOLVE_BATCH_SIZE));
4344
+ }
4345
+ return out;
4346
+ }
4347
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyUserDirectoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4348
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyUserDirectoryService, providedIn: 'root' });
4349
+ }
4350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyUserDirectoryService, decorators: [{
4351
+ type: Injectable,
4352
+ args: [{ providedIn: 'root' }]
4353
+ }] });
4354
+ /** `UserLookupDto`/`UserResolvedDto` → the picker option shape, 1:1. */
4355
+ function toOption(u) {
4356
+ return { id: u.id, displayName: u.displayName, email: u.email, avatarUrl: u.avatarUrl };
4357
+ }
4358
+
3997
4359
  /**
3998
4360
  * The page's CSP nonce, read once at module init from `<meta name="csp-nonce">`.
3999
4361
  *
@@ -9562,11 +9924,11 @@ class FlyDrawerComponent {
9562
9924
  }
9563
9925
  }
9564
9926
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9565
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDrawerComponent, isStandalone: true, selector: "fly-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, dismissOnScrim: { classPropertyName: "dismissOnScrim", publicName: "dismissOnScrim", isSignal: true, isRequired: false, transformFunction: null }, dismissOnEscape: { classPropertyName: "dismissOnEscape", publicName: "dismissOnEscape", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, hideCloseButton: { classPropertyName: "hideCloseButton", publicName: "hideCloseButton", isSignal: true, isRequired: false, transformFunction: null }, bodyPadding: { classPropertyName: "bodyPadding", publicName: "bodyPadding", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", openChange: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "class.fly-drawer--fixed": "position() === 'fixed'" } }, ngImport: i0, template: "<!--\r\n Windowed overlay drawer. Renders only while mounted (open or sliding out).\r\n Scrim + panel are absolutely positioned within the nearest positioned\r\n ancestor (the consuming app root must be position:relative).\r\n-->\r\n@if (rendered()) {\r\n <div\r\n class=\"fly-drawer__scrim\"\r\n [class.fly-drawer__scrim--leaving]=\"leaving()\"\r\n (click)=\"onScrimClick()\"\r\n aria-hidden=\"true\"\r\n ></div>\r\n\r\n <div\r\n class=\"fly-drawer__panel\"\r\n [ngClass]=\"panelClass()\"\r\n [class.fly-drawer__panel--leaving]=\"leaving()\"\r\n [style.--fly-drawer-width]=\"width()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n [attr.aria-label]=\"!labelledBy() && ariaLabel() ? (ariaLabel()! | translate) : null\"\r\n cdkTrapFocus\r\n (animationend)=\"onPanelAnimationEnd($event.animationName)\"\r\n >\r\n <!-- Header: custom slot, else heading + close \u2715. -->\r\n <ng-content select=\"[flyDrawerHeader]\">\r\n @if (heading()) {\r\n <header class=\"fly-drawer__header\">\r\n <h2 class=\"fly-drawer__title\" [id]=\"headingId\">{{ heading()! | translate }}</h2>\r\n @if (!hideCloseButton()) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-drawer__close\"\r\n (click)=\"requestClose()\"\r\n [attr.aria-label]=\"'common.action.close' | translate\"\r\n >\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </header>\r\n }\r\n </ng-content>\r\n\r\n <div class=\"fly-drawer__body\" [ngClass]=\"'fly-drawer__body--pad-' + bodyPadding()\">\r\n <ng-content></ng-content>\r\n </div>\r\n\r\n <ng-content select=\"[flyDrawerFooter]\"></ng-content>\r\n </div>\r\n}\r\n", styles: [":host{position:absolute;inset:0;z-index:60;pointer-events:none}:host(.fly-drawer--fixed){position:fixed;block-size:100dvh;z-index:var(--z-overlay, 100)}.fly-drawer__scrim{position:absolute;inset:0;pointer-events:auto;background:#00000061;opacity:1;animation:fly-drawer-scrim-in .24s ease both}.fly-drawer__scrim--leaving{animation-name:fly-drawer-scrim-out;animation-duration:.2s}.fly-drawer__panel{--fly-drawer-offset: 100%;position:absolute;inset-block:0;inset-inline-end:0;pointer-events:auto;display:flex;flex-direction:column;min-block-size:0;inline-size:var(--fly-drawer-width, min(480px, 100%));max-inline-size:100%;background:var(--surface-overlay, #fff);-webkit-backdrop-filter:blur(30px) saturate(150%);backdrop-filter:blur(30px) saturate(150%);border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:-16px 0 48px #00000038;animation:fly-drawer-in .28s cubic-bezier(.32,.72,0,1) both;will-change:transform}.fly-drawer__panel--sm{inline-size:var(--fly-drawer-width, min(360px, 100%))}.fly-drawer__panel--md{inline-size:var(--fly-drawer-width, min(480px, 100%))}.fly-drawer__panel--lg{inline-size:var(--fly-drawer-width, min(640px, 100%))}.fly-drawer__panel--xl{inline-size:var(--fly-drawer-width, min(960px, 94%))}.fly-drawer__panel--side-start{--fly-drawer-offset: -100%;inset-inline:0 auto;border-inline-start:none;border-inline-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:16px 0 48px #00000038}.fly-drawer__panel--leaving{animation-name:fly-drawer-out;animation-duration:.24s;animation-timing-function:cubic-bezier(.4,0,1,1)}@keyframes fly-drawer-in{0%{transform:translate(var(--fly-drawer-offset))}to{transform:translate(0)}}@keyframes fly-drawer-out{0%{transform:translate(0)}to{transform:translate(var(--fly-drawer-offset))}}@keyframes fly-drawer-scrim-in{0%{opacity:0}to{opacity:1}}@keyframes fly-drawer-scrim-out{0%{opacity:1}to{opacity:0}}:host-context([dir=rtl]) .fly-drawer__panel{--fly-drawer-offset: -100%;box-shadow:16px 0 48px #00000038;border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context([dir=rtl]) .fly-drawer__panel--side-start{--fly-drawer-offset: 100%}.fly-drawer__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-block-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));flex:0 0 auto}.fly-drawer__title{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1d1d1f)}.fly-drawer__close{display:inline-flex;align-items:center;justify-content:center;inline-size:30px;block-size:30px;border:none;border-radius:8px;background:transparent;color:var(--label-secondary, #6e6e73);cursor:pointer}.fly-drawer__close:hover{background:#0000000f}.fly-drawer__body{flex:1 1 auto;min-block-size:0;overflow-y:auto;padding:var(--fly-drawer-body-padding, 16px 18px)}.fly-drawer__body--pad-none{--fly-drawer-body-padding: 0}.fly-drawer__body--pad-sm{--fly-drawer-body-padding: 12px 14px}.fly-drawer__body--pad-md{--fly-drawer-body-padding: 16px 18px}.fly-drawer__body--pad-lg{--fly-drawer-body-padding: 20px 22px}:host ::ng-deep [flyDrawerFooter]{flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-block-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context(html.dark-theme) .fly-drawer__close:hover{background:#ffffff14}@media(prefers-reduced-motion:reduce){.fly-drawer__scrim,.fly-drawer__panel,.fly-drawer__scrim--leaving,.fly-drawer__panel--leaving{animation:none}}@media(forced-colors:active){.fly-drawer__panel{border:1px solid CanvasText}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9927
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDrawerComponent, isStandalone: true, selector: "fly-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, dismissOnScrim: { classPropertyName: "dismissOnScrim", publicName: "dismissOnScrim", isSignal: true, isRequired: false, transformFunction: null }, dismissOnEscape: { classPropertyName: "dismissOnEscape", publicName: "dismissOnEscape", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, hideCloseButton: { classPropertyName: "hideCloseButton", publicName: "hideCloseButton", isSignal: true, isRequired: false, transformFunction: null }, bodyPadding: { classPropertyName: "bodyPadding", publicName: "bodyPadding", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", openChange: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "class.fly-drawer--fixed": "position() === 'fixed'" } }, ngImport: i0, template: "<!--\r\n Windowed overlay drawer. Renders only while mounted (open or sliding out).\r\n Scrim + panel are absolutely positioned within the nearest positioned\r\n ancestor (the consuming app root must be position:relative).\r\n-->\r\n@if (rendered()) {\r\n <div\r\n class=\"fly-drawer__scrim\"\r\n [class.fly-drawer__scrim--leaving]=\"leaving()\"\r\n (click)=\"onScrimClick()\"\r\n aria-hidden=\"true\"\r\n ></div>\r\n\r\n <div\r\n class=\"fly-drawer__panel\"\r\n [ngClass]=\"panelClass()\"\r\n [class.fly-drawer__panel--leaving]=\"leaving()\"\r\n [style.--fly-drawer-width]=\"width()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n [attr.aria-label]=\"!labelledBy() && ariaLabel() ? (ariaLabel()! | translate) : null\"\r\n cdkTrapFocus\r\n (animationend)=\"onPanelAnimationEnd($event.animationName)\"\r\n >\r\n <!-- Header: custom slot, else heading + close \u2715. -->\r\n <ng-content select=\"[flyDrawerHeader]\">\r\n @if (heading()) {\r\n <header class=\"fly-drawer__header\">\r\n <h2 class=\"fly-drawer__title\" [id]=\"headingId\">{{ heading()! | translate }}</h2>\r\n @if (!hideCloseButton()) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-drawer__close\"\r\n (click)=\"requestClose()\"\r\n [attr.aria-label]=\"'common.action.close' | translate\"\r\n >\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </header>\r\n }\r\n </ng-content>\r\n\r\n <div class=\"fly-drawer__body\" [ngClass]=\"'fly-drawer__body--pad-' + bodyPadding()\">\r\n <ng-content></ng-content>\r\n </div>\r\n\r\n <ng-content select=\"[flyDrawerFooter]\"></ng-content>\r\n </div>\r\n}\r\n", styles: [":host{position:absolute;inset:0;z-index:60;pointer-events:none}:host(.fly-drawer--fixed){position:fixed;block-size:100dvh;z-index:var(--z-overlay, 100)}.fly-drawer__scrim{position:absolute;inset:0;pointer-events:auto;background:#00000061;opacity:1;animation:fly-drawer-scrim-in .24s ease both}.fly-drawer__scrim--leaving{animation-name:fly-drawer-scrim-out;animation-duration:.2s}.fly-drawer__panel{--fly-drawer-offset: 100%;position:absolute;inset-block:0;inset-inline-end:0;pointer-events:auto;display:flex;flex-direction:column;min-block-size:0;inline-size:var(--fly-drawer-width, min(480px, 100%));max-inline-size:100%;background:var(--surface-overlay, #fff);-webkit-backdrop-filter:blur(30px) saturate(150%);backdrop-filter:blur(30px) saturate(150%);border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:-16px 0 48px #00000038;animation:fly-drawer-in .28s cubic-bezier(.32,.72,0,1) both;will-change:transform}.fly-drawer__panel--sm{inline-size:var(--fly-drawer-width, min(360px, 100%))}.fly-drawer__panel--md{inline-size:var(--fly-drawer-width, min(480px, 100%))}.fly-drawer__panel--lg{inline-size:var(--fly-drawer-width, min(640px, 100%))}.fly-drawer__panel--xl{inline-size:var(--fly-drawer-width, min(960px, 94%))}.fly-drawer__panel--side-start{--fly-drawer-offset: -100%;inset-inline:0 auto;border-inline-start:none;border-inline-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:16px 0 48px #00000038}.fly-drawer__panel--leaving{animation-name:fly-drawer-out;animation-duration:.24s;animation-timing-function:cubic-bezier(.4,0,1,1)}@keyframes fly-drawer-in{0%{transform:translate(var(--fly-drawer-offset))}to{transform:translate(0)}}@keyframes fly-drawer-out{0%{transform:translate(0)}to{transform:translate(var(--fly-drawer-offset))}}@keyframes fly-drawer-scrim-in{0%{opacity:0}to{opacity:1}}@keyframes fly-drawer-scrim-out{0%{opacity:1}to{opacity:0}}:host-context([dir=rtl]) .fly-drawer__panel{--fly-drawer-offset: -100%;box-shadow:16px 0 48px #00000038;border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context([dir=rtl]) .fly-drawer__panel--side-start{--fly-drawer-offset: 100%}.fly-drawer__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-block-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));flex:0 0 auto}.fly-drawer__title{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1d1d1f)}.fly-drawer__close{display:inline-flex;align-items:center;justify-content:center;inline-size:30px;block-size:30px;border:none;border-radius:8px;background:transparent;color:var(--label-secondary, #6e6e73);cursor:pointer}.fly-drawer__close:hover{background:#0000000f}.fly-drawer__body{flex:1 1 auto;min-block-size:0;overflow-y:auto;padding:var(--fly-drawer-body-padding, 16px 18px);container-type:inline-size}.fly-drawer__body--pad-none{--fly-drawer-body-padding: 0}.fly-drawer__body--pad-sm{--fly-drawer-body-padding: 12px 14px}.fly-drawer__body--pad-md{--fly-drawer-body-padding: 16px 18px}.fly-drawer__body--pad-lg{--fly-drawer-body-padding: 20px 22px}:host ::ng-deep [flyDrawerFooter]{flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-block-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context(html.dark-theme) .fly-drawer__close:hover{background:#ffffff14}@media(prefers-reduced-motion:reduce){.fly-drawer__scrim,.fly-drawer__panel,.fly-drawer__scrim--leaving,.fly-drawer__panel--leaving{animation:none}}@media(forced-colors:active){.fly-drawer__panel{border:1px solid CanvasText}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: i2.CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9566
9928
  }
9567
9929
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDrawerComponent, decorators: [{
9568
9930
  type: Component,
9569
- args: [{ selector: 'fly-drawer', standalone: true, imports: [CommonModule, A11yModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class.fly-drawer--fixed]': "position() === 'fixed'" }, template: "<!--\r\n Windowed overlay drawer. Renders only while mounted (open or sliding out).\r\n Scrim + panel are absolutely positioned within the nearest positioned\r\n ancestor (the consuming app root must be position:relative).\r\n-->\r\n@if (rendered()) {\r\n <div\r\n class=\"fly-drawer__scrim\"\r\n [class.fly-drawer__scrim--leaving]=\"leaving()\"\r\n (click)=\"onScrimClick()\"\r\n aria-hidden=\"true\"\r\n ></div>\r\n\r\n <div\r\n class=\"fly-drawer__panel\"\r\n [ngClass]=\"panelClass()\"\r\n [class.fly-drawer__panel--leaving]=\"leaving()\"\r\n [style.--fly-drawer-width]=\"width()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n [attr.aria-label]=\"!labelledBy() && ariaLabel() ? (ariaLabel()! | translate) : null\"\r\n cdkTrapFocus\r\n (animationend)=\"onPanelAnimationEnd($event.animationName)\"\r\n >\r\n <!-- Header: custom slot, else heading + close \u2715. -->\r\n <ng-content select=\"[flyDrawerHeader]\">\r\n @if (heading()) {\r\n <header class=\"fly-drawer__header\">\r\n <h2 class=\"fly-drawer__title\" [id]=\"headingId\">{{ heading()! | translate }}</h2>\r\n @if (!hideCloseButton()) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-drawer__close\"\r\n (click)=\"requestClose()\"\r\n [attr.aria-label]=\"'common.action.close' | translate\"\r\n >\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </header>\r\n }\r\n </ng-content>\r\n\r\n <div class=\"fly-drawer__body\" [ngClass]=\"'fly-drawer__body--pad-' + bodyPadding()\">\r\n <ng-content></ng-content>\r\n </div>\r\n\r\n <ng-content select=\"[flyDrawerFooter]\"></ng-content>\r\n </div>\r\n}\r\n", styles: [":host{position:absolute;inset:0;z-index:60;pointer-events:none}:host(.fly-drawer--fixed){position:fixed;block-size:100dvh;z-index:var(--z-overlay, 100)}.fly-drawer__scrim{position:absolute;inset:0;pointer-events:auto;background:#00000061;opacity:1;animation:fly-drawer-scrim-in .24s ease both}.fly-drawer__scrim--leaving{animation-name:fly-drawer-scrim-out;animation-duration:.2s}.fly-drawer__panel{--fly-drawer-offset: 100%;position:absolute;inset-block:0;inset-inline-end:0;pointer-events:auto;display:flex;flex-direction:column;min-block-size:0;inline-size:var(--fly-drawer-width, min(480px, 100%));max-inline-size:100%;background:var(--surface-overlay, #fff);-webkit-backdrop-filter:blur(30px) saturate(150%);backdrop-filter:blur(30px) saturate(150%);border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:-16px 0 48px #00000038;animation:fly-drawer-in .28s cubic-bezier(.32,.72,0,1) both;will-change:transform}.fly-drawer__panel--sm{inline-size:var(--fly-drawer-width, min(360px, 100%))}.fly-drawer__panel--md{inline-size:var(--fly-drawer-width, min(480px, 100%))}.fly-drawer__panel--lg{inline-size:var(--fly-drawer-width, min(640px, 100%))}.fly-drawer__panel--xl{inline-size:var(--fly-drawer-width, min(960px, 94%))}.fly-drawer__panel--side-start{--fly-drawer-offset: -100%;inset-inline:0 auto;border-inline-start:none;border-inline-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:16px 0 48px #00000038}.fly-drawer__panel--leaving{animation-name:fly-drawer-out;animation-duration:.24s;animation-timing-function:cubic-bezier(.4,0,1,1)}@keyframes fly-drawer-in{0%{transform:translate(var(--fly-drawer-offset))}to{transform:translate(0)}}@keyframes fly-drawer-out{0%{transform:translate(0)}to{transform:translate(var(--fly-drawer-offset))}}@keyframes fly-drawer-scrim-in{0%{opacity:0}to{opacity:1}}@keyframes fly-drawer-scrim-out{0%{opacity:1}to{opacity:0}}:host-context([dir=rtl]) .fly-drawer__panel{--fly-drawer-offset: -100%;box-shadow:16px 0 48px #00000038;border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context([dir=rtl]) .fly-drawer__panel--side-start{--fly-drawer-offset: 100%}.fly-drawer__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-block-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));flex:0 0 auto}.fly-drawer__title{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1d1d1f)}.fly-drawer__close{display:inline-flex;align-items:center;justify-content:center;inline-size:30px;block-size:30px;border:none;border-radius:8px;background:transparent;color:var(--label-secondary, #6e6e73);cursor:pointer}.fly-drawer__close:hover{background:#0000000f}.fly-drawer__body{flex:1 1 auto;min-block-size:0;overflow-y:auto;padding:var(--fly-drawer-body-padding, 16px 18px)}.fly-drawer__body--pad-none{--fly-drawer-body-padding: 0}.fly-drawer__body--pad-sm{--fly-drawer-body-padding: 12px 14px}.fly-drawer__body--pad-md{--fly-drawer-body-padding: 16px 18px}.fly-drawer__body--pad-lg{--fly-drawer-body-padding: 20px 22px}:host ::ng-deep [flyDrawerFooter]{flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-block-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context(html.dark-theme) .fly-drawer__close:hover{background:#ffffff14}@media(prefers-reduced-motion:reduce){.fly-drawer__scrim,.fly-drawer__panel,.fly-drawer__scrim--leaving,.fly-drawer__panel--leaving{animation:none}}@media(forced-colors:active){.fly-drawer__panel{border:1px solid CanvasText}}\n"] }]
9931
+ args: [{ selector: 'fly-drawer', standalone: true, imports: [CommonModule, A11yModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class.fly-drawer--fixed]': "position() === 'fixed'" }, template: "<!--\r\n Windowed overlay drawer. Renders only while mounted (open or sliding out).\r\n Scrim + panel are absolutely positioned within the nearest positioned\r\n ancestor (the consuming app root must be position:relative).\r\n-->\r\n@if (rendered()) {\r\n <div\r\n class=\"fly-drawer__scrim\"\r\n [class.fly-drawer__scrim--leaving]=\"leaving()\"\r\n (click)=\"onScrimClick()\"\r\n aria-hidden=\"true\"\r\n ></div>\r\n\r\n <div\r\n class=\"fly-drawer__panel\"\r\n [ngClass]=\"panelClass()\"\r\n [class.fly-drawer__panel--leaving]=\"leaving()\"\r\n [style.--fly-drawer-width]=\"width()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-labelledby]=\"labelledBy()\"\r\n [attr.aria-label]=\"!labelledBy() && ariaLabel() ? (ariaLabel()! | translate) : null\"\r\n cdkTrapFocus\r\n (animationend)=\"onPanelAnimationEnd($event.animationName)\"\r\n >\r\n <!-- Header: custom slot, else heading + close \u2715. -->\r\n <ng-content select=\"[flyDrawerHeader]\">\r\n @if (heading()) {\r\n <header class=\"fly-drawer__header\">\r\n <h2 class=\"fly-drawer__title\" [id]=\"headingId\">{{ heading()! | translate }}</h2>\r\n @if (!hideCloseButton()) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-drawer__close\"\r\n (click)=\"requestClose()\"\r\n [attr.aria-label]=\"'common.action.close' | translate\"\r\n >\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </header>\r\n }\r\n </ng-content>\r\n\r\n <div class=\"fly-drawer__body\" [ngClass]=\"'fly-drawer__body--pad-' + bodyPadding()\">\r\n <ng-content></ng-content>\r\n </div>\r\n\r\n <ng-content select=\"[flyDrawerFooter]\"></ng-content>\r\n </div>\r\n}\r\n", styles: [":host{position:absolute;inset:0;z-index:60;pointer-events:none}:host(.fly-drawer--fixed){position:fixed;block-size:100dvh;z-index:var(--z-overlay, 100)}.fly-drawer__scrim{position:absolute;inset:0;pointer-events:auto;background:#00000061;opacity:1;animation:fly-drawer-scrim-in .24s ease both}.fly-drawer__scrim--leaving{animation-name:fly-drawer-scrim-out;animation-duration:.2s}.fly-drawer__panel{--fly-drawer-offset: 100%;position:absolute;inset-block:0;inset-inline-end:0;pointer-events:auto;display:flex;flex-direction:column;min-block-size:0;inline-size:var(--fly-drawer-width, min(480px, 100%));max-inline-size:100%;background:var(--surface-overlay, #fff);-webkit-backdrop-filter:blur(30px) saturate(150%);backdrop-filter:blur(30px) saturate(150%);border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:-16px 0 48px #00000038;animation:fly-drawer-in .28s cubic-bezier(.32,.72,0,1) both;will-change:transform}.fly-drawer__panel--sm{inline-size:var(--fly-drawer-width, min(360px, 100%))}.fly-drawer__panel--md{inline-size:var(--fly-drawer-width, min(480px, 100%))}.fly-drawer__panel--lg{inline-size:var(--fly-drawer-width, min(640px, 100%))}.fly-drawer__panel--xl{inline-size:var(--fly-drawer-width, min(960px, 94%))}.fly-drawer__panel--side-start{--fly-drawer-offset: -100%;inset-inline:0 auto;border-inline-start:none;border-inline-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));box-shadow:16px 0 48px #00000038}.fly-drawer__panel--leaving{animation-name:fly-drawer-out;animation-duration:.24s;animation-timing-function:cubic-bezier(.4,0,1,1)}@keyframes fly-drawer-in{0%{transform:translate(var(--fly-drawer-offset))}to{transform:translate(0)}}@keyframes fly-drawer-out{0%{transform:translate(0)}to{transform:translate(var(--fly-drawer-offset))}}@keyframes fly-drawer-scrim-in{0%{opacity:0}to{opacity:1}}@keyframes fly-drawer-scrim-out{0%{opacity:1}to{opacity:0}}:host-context([dir=rtl]) .fly-drawer__panel{--fly-drawer-offset: -100%;box-shadow:16px 0 48px #00000038;border-inline-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context([dir=rtl]) .fly-drawer__panel--side-start{--fly-drawer-offset: 100%}.fly-drawer__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-block-end:1px solid var(--surface-border, rgba(0, 0, 0, .08));flex:0 0 auto}.fly-drawer__title{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1d1d1f)}.fly-drawer__close{display:inline-flex;align-items:center;justify-content:center;inline-size:30px;block-size:30px;border:none;border-radius:8px;background:transparent;color:var(--label-secondary, #6e6e73);cursor:pointer}.fly-drawer__close:hover{background:#0000000f}.fly-drawer__body{flex:1 1 auto;min-block-size:0;overflow-y:auto;padding:var(--fly-drawer-body-padding, 16px 18px);container-type:inline-size}.fly-drawer__body--pad-none{--fly-drawer-body-padding: 0}.fly-drawer__body--pad-sm{--fly-drawer-body-padding: 12px 14px}.fly-drawer__body--pad-md{--fly-drawer-body-padding: 16px 18px}.fly-drawer__body--pad-lg{--fly-drawer-body-padding: 20px 22px}:host ::ng-deep [flyDrawerFooter]{flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 18px;border-block-start:1px solid var(--surface-border, rgba(0, 0, 0, .08))}:host-context(html.dark-theme) .fly-drawer__close:hover{background:#ffffff14}@media(prefers-reduced-motion:reduce){.fly-drawer__scrim,.fly-drawer__panel,.fly-drawer__scrim--leaving,.fly-drawer__panel--leaving{animation:none}}@media(forced-colors:active){.fly-drawer__panel{border:1px solid CanvasText}}\n"] }]
9570
9932
  }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], dismissOnScrim: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissOnScrim", required: false }] }], dismissOnEscape: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissOnEscape", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hideCloseButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideCloseButton", required: false }] }], bodyPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "bodyPadding", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], openChange: [{ type: i0.Output, args: ["openChange"] }], onEscape: [{
9571
9933
  type: HostListener,
9572
9934
  args: ['document:keydown.escape']
@@ -12928,6 +13290,13 @@ const ROOT_DRAFT_KEY = '__root__';
12928
13290
  * eager load-all) — the explicit fix for the legacy unpaged-children scale bug
12929
13291
  * — and each level pages independently via `loadMore`.
12930
13292
  *
13293
+ * The `comments` core app owns no user profile data, so `FlyComment` carries
13294
+ * `authorUserId` and an OPTIONAL `authorDisplayName`. Rather than make every host
13295
+ * map names in itself (and render GUIDs when it forgets), bind
13296
+ * `[resolveDisplayName]` to `FlyUserDirectoryService.label` — see
13297
+ * {@link FlyCommentThreadComponent.resolveDisplayName}. Still transport-agnostic:
13298
+ * the resolver is a function the host hands over, not a call this component makes.
13299
+ *
12931
13300
  * i18n is self-sufficient through the `comment.*` keys in `DS_BASELINE_LOCALES`
12932
13301
  * (en/ar/fr/ur), overridable by any consumer key of the same name. RTL works via
12933
13302
  * logical CSS. Styling uses DS theme tokens (`--surface-*`, `--label-*`,
@@ -12940,6 +13309,7 @@ const ROOT_DRAFT_KEY = '__root__';
12940
13309
  * [rootPage]="rootPage()"
12941
13310
  * [repliesByParent]="repliesByParent()"
12942
13311
  * [currentUserId]="me.id"
13312
+ * [resolveDisplayName]="directory.label"
12943
13313
  * [canModerateThread]="canModerate()"
12944
13314
  * [isLocked]="thread().isLocked"
12945
13315
  * (loadReplies)="onLoadReplies($event)"
@@ -12976,6 +13346,23 @@ class FlyCommentThreadComponent {
12976
13346
  /** Thread-scoped moderator permission (lock/unlock). Additive — distinct from the
12977
13347
  * per-comment `canModerate` flag, which gates delete-any on that specific comment. */
12978
13348
  canModerateThread = input(false, ...(ngDevMode ? [{ debugName: "canModerateThread" }] : /* istanbul ignore next */ []));
13349
+ /**
13350
+ * Optional id→name lookup, for the common case where the backend DTO carries only
13351
+ * `authorUserId`. The `comments` core app owns no user profile data, so without
13352
+ * this every host had to map display names into `authorDisplayName` itself — and
13353
+ * a host that forgot rendered raw GUIDs at the reader.
13354
+ *
13355
+ * Bind `FlyUserDirectoryService.label` (a signal-backed read-through cache that
13356
+ * resolves on demand and batches). Because the component reads it during render,
13357
+ * the name appears by itself once the lookup lands — the host keeps a plain
13358
+ * page signal, with no `computed` derivation and no priming.
13359
+ *
13360
+ * The component still never calls an API: this is a function the HOST supplies,
13361
+ * exactly like `fly-entity-lookup`'s `LOOKUP_APP_NAME_RESOLVER`. Absent (or
13362
+ * returning `null`), rendering falls back as before. See {@link authorLabel} for
13363
+ * the resolution order.
13364
+ */
13365
+ resolveDisplayName = input(null, ...(ngDevMode ? [{ debugName: "resolveDisplayName" }] : /* istanbul ignore next */ []));
12979
13366
  // ── Outputs ──────────────────────────────────────────────────────────────────
12980
13367
  /** Expand a collapsed reply subtree — always page 1 of that parent. */
12981
13368
  loadReplies = output();
@@ -13036,6 +13423,25 @@ class FlyCommentThreadComponent {
13036
13423
  const me = this.currentUserId();
13037
13424
  return !!me && me === comment.authorUserId;
13038
13425
  }
13426
+ /**
13427
+ * The name to render for a comment's author. Resolution order:
13428
+ * explicit `authorDisplayName` (a host that already knows the name, or a
13429
+ * backend that returns one, always wins) → {@link resolveDisplayName} →
13430
+ * the raw `authorUserId`.
13431
+ *
13432
+ * The raw id remains the last resort rather than a placeholder: it is stable,
13433
+ * unique, and lets a reader correlate two comments by the same author even
13434
+ * when the directory is unreachable.
13435
+ */
13436
+ authorLabel(comment) {
13437
+ if (comment.authorDisplayName)
13438
+ return comment.authorDisplayName;
13439
+ return this.resolveDisplayName()?.(comment.authorUserId) || comment.authorUserId;
13440
+ }
13441
+ /** First character of {@link authorLabel}, for the avatar-initials fallback. */
13442
+ authorInitial(comment) {
13443
+ return this.authorLabel(comment).charAt(0);
13444
+ }
13039
13445
  canReport(comment) {
13040
13446
  return !this.isReadonly() && !comment.isDeleted && !this.isOwn(comment) && !!this.currentUserId();
13041
13447
  }
@@ -13206,16 +13612,16 @@ class FlyCommentThreadComponent {
13206
13612
  this._drafts.set(map);
13207
13613
  }
13208
13614
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyCommentThreadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13209
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyCommentThreadComponent, isStandalone: true, selector: "fly-comment-thread", inputs: { rootPage: { classPropertyName: "rootPage", publicName: "rootPage", isSignal: true, isRequired: true, transformFunction: null }, repliesByParent: { classPropertyName: "repliesByParent", publicName: "repliesByParent", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, isLocked: { classPropertyName: "isLocked", publicName: "isLocked", isSignal: true, isRequired: false, transformFunction: null }, currentUserId: { classPropertyName: "currentUserId", publicName: "currentUserId", isSignal: true, isRequired: false, transformFunction: null }, isReadonly: { classPropertyName: "isReadonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, canModerateThread: { classPropertyName: "canModerateThread", publicName: "canModerateThread", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadReplies: "loadReplies", loadMore: "loadMore", submit: "submit", edit: "edit", delete: "delete", report: "report", lockToggle: "lockToggle", sortChange: "sortChange" }, host: { properties: { "class.fly-comment-thread--locked": "isLocked()", "class.fly-comment-thread--readonly": "isReadonly()" }, classAttribute: "fly-comment-thread" }, ngImport: i0, template: "<div class=\"fly-comment-thread__body\">\r\n <!-- \u2500\u2500 Header: count + sort + lock toggle \u2500\u2500 -->\r\n <header class=\"fly-comment-thread__header\">\r\n <span class=\"fly-comment-thread__count\">\r\n {{ 'comment.count' | translate: { n: rootPage().total } }}\r\n </span>\r\n\r\n <div class=\"fly-comment-thread__header-actions\">\r\n <div class=\"fly-comment-thread__sort\" role=\"group\" [attr.aria-label]=\"'comment.sort_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__sort-btn\"\r\n [class.fly-comment-thread__sort-btn--active]=\"sort() === 'newest'\"\r\n (click)=\"onSortPick('newest')\">\r\n {{ 'comment.sort_newest' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__sort-btn\"\r\n [class.fly-comment-thread__sort-btn--active]=\"sort() === 'oldest'\"\r\n (click)=\"onSortPick('oldest')\">\r\n {{ 'comment.sort_oldest' | translate }}\r\n </button>\r\n </div>\r\n\r\n @if (canModerateThread() && !isReadonly()) {\r\n <button type=\"button\" class=\"fly-comment-thread__lock-btn\" (click)=\"toggleLock()\">\r\n {{ (isLocked() ? 'comment.unlock' : 'comment.lock') | translate }}\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n @if (isLocked()) {\r\n <p class=\"fly-comment-thread__locked-notice\">{{ 'comment.locked_notice' | translate }}</p>\r\n }\r\n\r\n <!-- \u2500\u2500 Root composer \u2500\u2500 -->\r\n @if (canReply()) {\r\n <div class=\"fly-comment-thread__composer\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [id]=\"fieldId('root-composer')\"\r\n [attr.aria-label]=\"'comment.composer_placeholder' | translate\"\r\n [placeholder]=\"'comment.composer_placeholder' | translate\"\r\n [ngModel]=\"rootDraft()\"\r\n (ngModelChange)=\"onRootDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!rootDraft().trim()\"\r\n (click)=\"submitRoot()\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- \u2500\u2500 Root comments \u2500\u2500 -->\r\n @if (rootPage().items.length === 0) {\r\n <p class=\"fly-comment-thread__empty\">{{ 'comment.empty' | translate }}</p>\r\n } @else {\r\n <ul class=\"fly-comment-thread__list\" role=\"list\">\r\n @for (c of rootPage().items; track c.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: c }\"></ng-container>\r\n }\r\n </ul>\r\n\r\n @if (rootPage().hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more\" (click)=\"loadMoreRoot()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Recursive comment node template \u2500\u2500 -->\r\n<ng-template #commentNode let-comment>\r\n <li class=\"fly-comment-thread__item\" [attr.data-depth]=\"comment.depth\">\r\n <div class=\"fly-comment-thread__row\">\r\n <div class=\"fly-comment-thread__avatar\" aria-hidden=\"true\">\r\n @if (comment.authorAvatarUrl) {\r\n <img [src]=\"comment.authorAvatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-comment-thread__avatar-fallback\">{{ (comment.authorDisplayName || comment.authorUserId)[0] }}</span>\r\n }\r\n </div>\r\n\r\n <div class=\"fly-comment-thread__content\">\r\n <div class=\"fly-comment-thread__meta\">\r\n <span class=\"fly-comment-thread__author\">{{ comment.authorDisplayName || comment.authorUserId }}</span>\r\n <span class=\"fly-comment-thread__timestamp\">{{ formatDate(comment.createdAt) }}</span>\r\n @if (comment.isEdited && !comment.isDeleted) {\r\n <span class=\"fly-comment-thread__edited\">{{ 'comment.edited' | translate }}</span>\r\n }\r\n </div>\r\n\r\n @if (comment.isDeleted) {\r\n <p class=\"fly-comment-thread__tombstone\">{{ 'comment.deleted' | translate }}</p>\r\n } @else if (editingId() === comment.id) {\r\n <div class=\"fly-comment-thread__edit-box\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [ngModel]=\"comment.body\"\r\n (ngModelChange)=\"onEditDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelEdit()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" (click)=\"saveEdit(comment)\">\r\n {{ 'comment.save' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <p class=\"fly-comment-thread__text\">{{ comment.body }}</p>\r\n }\r\n\r\n @if (!isReadonly() && !comment.isDeleted && editingId() !== comment.id) {\r\n <div class=\"fly-comment-thread__actions\">\r\n @if (canReply()) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn\" (click)=\"openReplyBox(comment.id)\">\r\n {{ 'comment.reply' | translate }}\r\n </button>\r\n }\r\n @if (canEditComment(comment)) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn\" (click)=\"startEdit(comment)\">\r\n {{ 'comment.edit' | translate }}\r\n </button>\r\n }\r\n @if (canDeleteComment(comment)) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn fly-comment-thread__action-btn--danger\" (click)=\"requestDelete(comment)\">\r\n {{ 'comment.delete' | translate }}\r\n </button>\r\n }\r\n @if (canReport(comment)) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn\" (click)=\"openReport(comment)\">\r\n {{ 'comment.report' | translate }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (confirmingDeleteId() === comment.id) {\r\n <div class=\"fly-comment-thread__confirm\">\r\n <span>{{ 'comment.delete_confirm' | translate }}</span>\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelDelete()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__danger-btn\" (click)=\"confirmDelete(comment)\">\r\n {{ 'comment.delete' | translate }}\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (isReplyBoxOpen(comment.id)) {\r\n <div class=\"fly-comment-thread__composer fly-comment-thread__composer--reply\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"2\"\r\n [attr.aria-label]=\"'comment.reply_placeholder' | translate\"\r\n [placeholder]=\"'comment.reply_placeholder' | translate\"\r\n [ngModel]=\"replyDraft(comment.id)\"\r\n (ngModelChange)=\"onReplyDraftChange(comment.id, $event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReplyBox(comment.id)\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!replyDraft(comment.id).trim()\"\r\n (click)=\"submitReply(comment.id)\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (comment.replyCount > 0) {\r\n <button type=\"button\" class=\"fly-comment-thread__toggle-replies\" (click)=\"toggleReplies(comment)\">\r\n {{ (isExpanded(comment.id) ? 'comment.hide_replies' : 'comment.show_replies') | translate: { n: comment.replyCount } }}\r\n </button>\r\n }\r\n\r\n @if (isExpanded(comment.id)) {\r\n <ul class=\"fly-comment-thread__list fly-comment-thread__list--nested\" role=\"list\">\r\n @for (child of repliesFor(comment.id)?.items ?? []; track child.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: child }\"></ng-container>\r\n }\r\n </ul>\r\n @if (repliesFor(comment.id)?.hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more fly-comment-thread__load-more--nested\" (click)=\"loadMoreReplies(comment.id)\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </li>\r\n</ng-template>\r\n\r\n<!-- \u2500\u2500 Report dialog \u2500\u2500 -->\r\n@if (reportingId()) {\r\n <div class=\"fly-comment-thread__overlay\">\r\n <div class=\"fly-comment-thread__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeReport()\"\r\n (keydown.enter)=\"closeReport()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div class=\"fly-comment-thread__dialog\" role=\"dialog\" [attr.aria-label]=\"'comment.report_title' | translate\">\r\n <h3 class=\"fly-comment-thread__dialog-title\">{{ 'comment.report_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-comment-thread__reason-group\">\r\n <legend>{{ 'comment.report_reason_label' | translate }}</legend>\r\n @for (reason of reportReasons; track reason) {\r\n <label class=\"fly-comment-thread__reason-option\">\r\n <input\r\n type=\"radio\"\r\n name=\"fly-comment-report-reason\"\r\n [value]=\"reason\"\r\n [checked]=\"reportReason() === reason\"\r\n (change)=\"onReportReasonChange(reason)\" />\r\n <span>{{ 'comment.reason.' + reason | translate }}</span>\r\n </label>\r\n }\r\n </fieldset>\r\n\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'comment.report_detail_label' | translate\"\r\n [placeholder]=\"'comment.report_detail_label' | translate\"\r\n [ngModel]=\"reportDetail()\"\r\n (ngModelChange)=\"onReportDetailChange($event)\"></textarea>\r\n\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReport()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" [disabled]=\"!reportReason()\" (click)=\"submitReport()\">\r\n {{ 'comment.report_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-comment-thread__body{display:flex;flex-direction:column;gap:16px}.fly-comment-thread__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-comment-thread__count{font-weight:700;font-size:14px}.fly-comment-thread__header-actions{display:flex;align-items:center;gap:10px}.fly-comment-thread__sort{display:inline-flex;border:1px solid var(--_border);border-radius:var(--_radius);overflow:hidden}.fly-comment-thread__sort-btn{padding:5px 12px;border:none;border-inline-end:1px solid var(--_border);background:transparent;color:var(--_text-subtle);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__sort-btn:last-child{border-inline-end:none}.fly-comment-thread__sort-btn--active{background:var(--_accent);color:#fff}.fly-comment-thread__lock-btn{padding:5px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__lock-btn:hover{background:var(--_surface-hover)}.fly-comment-thread__locked-notice{margin:0;padding:8px 12px;border-radius:var(--_radius);background:var(--_fill);color:var(--_text-subtle);font-size:12px}.fly-comment-thread__empty{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-comment-thread__composer{display:flex;flex-direction:column;gap:8px}.fly-comment-thread__composer--reply{margin-block-start:8px}.fly-comment-thread__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-comment-thread__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-comment-thread__composer-actions{display:flex;justify-content:flex-end;gap:8px}.fly-comment-thread__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__submit-btn:hover:not(:disabled){opacity:.9}.fly-comment-thread__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-comment-thread__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-comment-thread__cancel-btn:hover{background:var(--_surface-hover)}.fly-comment-thread__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__danger-btn:hover{opacity:.9}.fly-comment-thread__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}.fly-comment-thread__list--nested{margin-block-start:10px;margin-inline-start:20px;padding-inline-start:12px;border-inline-start:2px solid var(--_border);gap:12px}.fly-comment-thread__item{display:block}.fly-comment-thread__row{display:flex;gap:10px;align-items:flex-start}.fly-comment-thread__avatar{inline-size:32px;block-size:32px;border-radius:50%;overflow:hidden;flex:none;background:var(--_fill);display:flex;align-items:center;justify-content:center}.fly-comment-thread__avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-comment-thread__avatar-fallback{font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-comment-thread__content{flex:1 1 auto;min-inline-size:0;display:flex;flex-direction:column;gap:4px}.fly-comment-thread__meta{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.fly-comment-thread__author{font-weight:700;color:var(--_text)}.fly-comment-thread__timestamp{font-size:11px;color:var(--_text-tertiary)}.fly-comment-thread__edited{font-size:11px;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__text{margin:0;color:var(--_text);line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}.fly-comment-thread__tombstone{margin:0;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__edit-box{display:flex;flex-direction:column;gap:6px}.fly-comment-thread__actions{display:flex;gap:12px;margin-block-start:2px}.fly-comment-thread__action-btn{border:none;background:transparent;padding:0;color:var(--_text-subtle);font:inherit;font-size:12px;font-weight:600;cursor:pointer}.fly-comment-thread__action-btn:hover{color:var(--_accent);text-decoration:underline}.fly-comment-thread__action-btn--danger:hover{color:var(--_danger)}.fly-comment-thread__confirm{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--_radius);background:var(--_fill);font-size:12px;color:var(--_text-subtle)}.fly-comment-thread__toggle-replies{align-self:flex-start;border:none;background:transparent;padding:0;margin-block-start:4px;color:var(--_accent);font:inherit;font-size:12px;font-weight:700;cursor:pointer}.fly-comment-thread__toggle-replies:hover{text-decoration:underline}.fly-comment-thread__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__load-more:hover{background:var(--_surface-hover)}.fly-comment-thread__load-more--nested{margin-inline-start:20px}.fly-comment-thread__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-comment-thread__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-comment-thread__dialog{position:relative;z-index:1;inline-size:min(420px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-comment-thread__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-comment-thread__reason-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-comment-thread__reason-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-comment-thread__reason-option{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-comment-thread__reason-option input{accent-color:var(--_accent);cursor:pointer}:host(.fly-comment-thread--readonly) .fly-comment-thread__composer,:host(.fly-comment-thread--readonly) .fly-comment-thread__actions{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13615
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyCommentThreadComponent, isStandalone: true, selector: "fly-comment-thread", inputs: { rootPage: { classPropertyName: "rootPage", publicName: "rootPage", isSignal: true, isRequired: true, transformFunction: null }, repliesByParent: { classPropertyName: "repliesByParent", publicName: "repliesByParent", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, isLocked: { classPropertyName: "isLocked", publicName: "isLocked", isSignal: true, isRequired: false, transformFunction: null }, currentUserId: { classPropertyName: "currentUserId", publicName: "currentUserId", isSignal: true, isRequired: false, transformFunction: null }, isReadonly: { classPropertyName: "isReadonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, canModerateThread: { classPropertyName: "canModerateThread", publicName: "canModerateThread", isSignal: true, isRequired: false, transformFunction: null }, resolveDisplayName: { classPropertyName: "resolveDisplayName", publicName: "resolveDisplayName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadReplies: "loadReplies", loadMore: "loadMore", submit: "submit", edit: "edit", delete: "delete", report: "report", lockToggle: "lockToggle", sortChange: "sortChange" }, host: { properties: { "class.fly-comment-thread--locked": "isLocked()", "class.fly-comment-thread--readonly": "isReadonly()" }, classAttribute: "fly-comment-thread" }, ngImport: i0, template: "<div class=\"fly-comment-thread__body\">\r\n <!-- \u2500\u2500 Header: count + one combined sort/lock icon-button toolbar \u2500\u2500 -->\r\n <header class=\"fly-comment-thread__header\">\r\n <span class=\"fly-comment-thread__count\">\r\n {{ 'comment.count' | translate: { n: rootPage().total } }}\r\n </span>\r\n\r\n <div class=\"fly-comment-thread__toolbar\" role=\"group\" [attr.aria-label]=\"'comment.sort_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [class.fly-comment-thread__icon-btn--active]=\"sort() === 'newest'\"\r\n [attr.aria-pressed]=\"sort() === 'newest'\"\r\n [flyTooltip]=\"'comment.sort_newest' | translate\"\r\n [attr.aria-label]=\"'comment.sort_newest' | translate\"\r\n (click)=\"onSortPick('newest')\">\r\n <i class=\"pi pi-sort-amount-down\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [class.fly-comment-thread__icon-btn--active]=\"sort() === 'oldest'\"\r\n [attr.aria-pressed]=\"sort() === 'oldest'\"\r\n [flyTooltip]=\"'comment.sort_oldest' | translate\"\r\n [attr.aria-label]=\"'comment.sort_oldest' | translate\"\r\n (click)=\"onSortPick('oldest')\">\r\n <i class=\"pi pi-sort-amount-up\" aria-hidden=\"true\"></i>\r\n </button>\r\n\r\n @if (canModerateThread() && !isReadonly()) {\r\n <span class=\"fly-comment-thread__toolbar-sep\" aria-hidden=\"true\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [class.fly-comment-thread__icon-btn--active]=\"isLocked()\"\r\n [attr.aria-pressed]=\"isLocked()\"\r\n [flyTooltip]=\"(isLocked() ? 'comment.unlock' : 'comment.lock') | translate\"\r\n [attr.aria-label]=\"(isLocked() ? 'comment.unlock' : 'comment.lock') | translate\"\r\n (click)=\"toggleLock()\">\r\n <i class=\"pi\" [class.pi-lock]=\"!isLocked()\" [class.pi-lock-open]=\"isLocked()\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n @if (isLocked()) {\r\n <p class=\"fly-comment-thread__locked-notice\">{{ 'comment.locked_notice' | translate }}</p>\r\n }\r\n\r\n <!-- \u2500\u2500 Root composer \u2500\u2500 -->\r\n @if (canReply()) {\r\n <div class=\"fly-comment-thread__composer\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [id]=\"fieldId('root-composer')\"\r\n [attr.aria-label]=\"'comment.composer_placeholder' | translate\"\r\n [placeholder]=\"'comment.composer_placeholder' | translate\"\r\n [ngModel]=\"rootDraft()\"\r\n (ngModelChange)=\"onRootDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!rootDraft().trim()\"\r\n (click)=\"submitRoot()\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- \u2500\u2500 Root comments \u2500\u2500 -->\r\n @if (rootPage().items.length === 0) {\r\n <p class=\"fly-comment-thread__empty\">{{ 'comment.empty' | translate }}</p>\r\n } @else {\r\n <ul class=\"fly-comment-thread__list\" role=\"list\">\r\n @for (c of rootPage().items; track c.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: c }\"></ng-container>\r\n }\r\n </ul>\r\n\r\n @if (rootPage().hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more\" (click)=\"loadMoreRoot()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Recursive comment node template \u2500\u2500 -->\r\n<ng-template #commentNode let-comment>\r\n <li class=\"fly-comment-thread__item\" [attr.data-depth]=\"comment.depth\">\r\n <div class=\"fly-comment-thread__row\">\r\n <div class=\"fly-comment-thread__avatar\" aria-hidden=\"true\">\r\n @if (comment.authorAvatarUrl) {\r\n <img [src]=\"comment.authorAvatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-comment-thread__avatar-fallback\">{{ authorInitial(comment) }}</span>\r\n }\r\n </div>\r\n\r\n <div class=\"fly-comment-thread__content\">\r\n <div class=\"fly-comment-thread__meta\">\r\n <span class=\"fly-comment-thread__author\">{{ authorLabel(comment) }}</span>\r\n <span class=\"fly-comment-thread__timestamp\">{{ formatDate(comment.createdAt) }}</span>\r\n @if (comment.isEdited && !comment.isDeleted) {\r\n <span class=\"fly-comment-thread__edited\">{{ 'comment.edited' | translate }}</span>\r\n }\r\n </div>\r\n\r\n @if (comment.isDeleted) {\r\n <p class=\"fly-comment-thread__tombstone\">{{ 'comment.deleted' | translate }}</p>\r\n } @else if (editingId() === comment.id) {\r\n <div class=\"fly-comment-thread__edit-box\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [ngModel]=\"comment.body\"\r\n (ngModelChange)=\"onEditDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelEdit()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" (click)=\"saveEdit(comment)\">\r\n {{ 'comment.save' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"fly-comment-thread__text-row\">\r\n <p class=\"fly-comment-thread__text\">{{ comment.body }}</p>\r\n @if (!isReadonly()) {\r\n <div class=\"fly-comment-thread__actions\">\r\n @if (canReply()) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [flyTooltip]=\"'comment.reply' | translate\"\r\n [attr.aria-label]=\"'comment.reply' | translate\"\r\n (click)=\"openReplyBox(comment.id)\">\r\n <i class=\"pi pi-reply\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n @if (canEditComment(comment)) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [flyTooltip]=\"'comment.edit' | translate\"\r\n [attr.aria-label]=\"'comment.edit' | translate\"\r\n (click)=\"startEdit(comment)\">\r\n <i class=\"pi pi-pencil\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n @if (canDeleteComment(comment)) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn fly-comment-thread__icon-btn--danger\"\r\n [flyTooltip]=\"'comment.delete' | translate\"\r\n [attr.aria-label]=\"'comment.delete' | translate\"\r\n (click)=\"requestDelete(comment)\">\r\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n @if (canReport(comment)) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [flyTooltip]=\"'comment.report' | translate\"\r\n [attr.aria-label]=\"'comment.report' | translate\"\r\n (click)=\"openReport(comment)\">\r\n <i class=\"pi pi-flag\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (confirmingDeleteId() === comment.id) {\r\n <div class=\"fly-comment-thread__confirm\">\r\n <span>{{ 'comment.delete_confirm' | translate }}</span>\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelDelete()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__danger-btn\" (click)=\"confirmDelete(comment)\">\r\n {{ 'comment.delete' | translate }}\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (isReplyBoxOpen(comment.id)) {\r\n <div class=\"fly-comment-thread__composer fly-comment-thread__composer--reply\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"2\"\r\n [attr.aria-label]=\"'comment.reply_placeholder' | translate\"\r\n [placeholder]=\"'comment.reply_placeholder' | translate\"\r\n [ngModel]=\"replyDraft(comment.id)\"\r\n (ngModelChange)=\"onReplyDraftChange(comment.id, $event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReplyBox(comment.id)\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!replyDraft(comment.id).trim()\"\r\n (click)=\"submitReply(comment.id)\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (comment.replyCount > 0) {\r\n <button type=\"button\" class=\"fly-comment-thread__toggle-replies\" (click)=\"toggleReplies(comment)\">\r\n {{ (isExpanded(comment.id) ? 'comment.hide_replies' : 'comment.show_replies') | translate: { n: comment.replyCount } }}\r\n </button>\r\n }\r\n\r\n @if (isExpanded(comment.id)) {\r\n <ul class=\"fly-comment-thread__list fly-comment-thread__list--nested\" role=\"list\">\r\n @for (child of repliesFor(comment.id)?.items ?? []; track child.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: child }\"></ng-container>\r\n }\r\n </ul>\r\n @if (repliesFor(comment.id)?.hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more fly-comment-thread__load-more--nested\" (click)=\"loadMoreReplies(comment.id)\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </li>\r\n</ng-template>\r\n\r\n<!-- \u2500\u2500 Report dialog \u2500\u2500 -->\r\n@if (reportingId()) {\r\n <div class=\"fly-comment-thread__overlay\">\r\n <div class=\"fly-comment-thread__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeReport()\"\r\n (keydown.enter)=\"closeReport()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div class=\"fly-comment-thread__dialog\" role=\"dialog\" [attr.aria-label]=\"'comment.report_title' | translate\">\r\n <h3 class=\"fly-comment-thread__dialog-title\">{{ 'comment.report_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-comment-thread__reason-group\">\r\n <legend>{{ 'comment.report_reason_label' | translate }}</legend>\r\n @for (reason of reportReasons; track reason) {\r\n <label class=\"fly-comment-thread__reason-option\">\r\n <input\r\n type=\"radio\"\r\n name=\"fly-comment-report-reason\"\r\n [value]=\"reason\"\r\n [checked]=\"reportReason() === reason\"\r\n (change)=\"onReportReasonChange(reason)\" />\r\n <span>{{ 'comment.reason.' + reason | translate }}</span>\r\n </label>\r\n }\r\n </fieldset>\r\n\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'comment.report_detail_label' | translate\"\r\n [placeholder]=\"'comment.report_detail_label' | translate\"\r\n [ngModel]=\"reportDetail()\"\r\n (ngModelChange)=\"onReportDetailChange($event)\"></textarea>\r\n\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReport()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" [disabled]=\"!reportReason()\" (click)=\"submitReport()\">\r\n {{ 'comment.report_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-comment-thread__body{display:flex;flex-direction:column;gap:16px}.fly-comment-thread__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-comment-thread__count{font-weight:700;font-size:14px}.fly-comment-thread__toolbar{display:inline-flex;align-items:center;gap:2px;padding:2px;border:1px solid var(--_border);border-radius:calc(var(--_radius) + 2px);background:var(--_surface)}.fly-comment-thread__toolbar-sep{inline-size:1px;block-size:18px;margin-inline:2px;background:var(--_border)}.fly-comment-thread__icon-btn{display:inline-flex;align-items:center;justify-content:center;inline-size:26px;block-size:26px;flex:none;padding:0;border:none;border-radius:50%;background:transparent;color:var(--_text-subtle);cursor:pointer;transition:background var(--t-state, .15s),color var(--t-state, .15s)}.fly-comment-thread__icon-btn .pi{font-size:13px}.fly-comment-thread__icon-btn:hover{background:var(--_surface-hover);color:var(--_text)}.fly-comment-thread__icon-btn:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-comment-thread__icon-btn--active{background:var(--_accent);color:#fff}.fly-comment-thread__icon-btn--active:hover{background:var(--_accent);opacity:.9}.fly-comment-thread__icon-btn--danger:hover{background:var(--_danger);color:#fff}.fly-comment-thread__locked-notice{margin:0;padding:8px 12px;border-radius:var(--_radius);background:var(--_fill);color:var(--_text-subtle);font-size:12px}.fly-comment-thread__empty{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-comment-thread__composer{display:flex;flex-direction:column;gap:8px}.fly-comment-thread__composer--reply{margin-block-start:8px}.fly-comment-thread__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-comment-thread__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-comment-thread__composer-actions{display:flex;justify-content:flex-end;gap:8px}.fly-comment-thread__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__submit-btn:hover:not(:disabled){opacity:.9}.fly-comment-thread__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-comment-thread__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-comment-thread__cancel-btn:hover{background:var(--_surface-hover)}.fly-comment-thread__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__danger-btn:hover{opacity:.9}.fly-comment-thread__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}.fly-comment-thread__list--nested{margin-block-start:10px;margin-inline-start:20px;padding-inline-start:12px;border-inline-start:2px solid var(--_border);gap:12px}.fly-comment-thread__item{display:block}.fly-comment-thread__row{display:flex;gap:10px;align-items:flex-start}.fly-comment-thread__avatar{inline-size:32px;block-size:32px;border-radius:50%;overflow:hidden;flex:none;background:var(--_fill);display:flex;align-items:center;justify-content:center}.fly-comment-thread__avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-comment-thread__avatar-fallback{font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-comment-thread__content{flex:1 1 auto;min-inline-size:0;display:flex;flex-direction:column;gap:4px}.fly-comment-thread__meta{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.fly-comment-thread__author{font-weight:700;color:var(--_text)}.fly-comment-thread__timestamp{font-size:11px;color:var(--_text-tertiary)}.fly-comment-thread__edited{font-size:11px;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__text-row{display:flex;align-items:flex-start;gap:4px}.fly-comment-thread__text{flex:1 1 auto;min-inline-size:0;margin:0;color:var(--_text);line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}.fly-comment-thread__tombstone{margin:0;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__edit-box{display:flex;flex-direction:column;gap:6px}.fly-comment-thread__actions{display:flex;align-items:center;gap:2px;flex:none;margin-inline-start:auto}.fly-comment-thread__confirm{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--_radius);background:var(--_fill);font-size:12px;color:var(--_text-subtle)}.fly-comment-thread__toggle-replies{align-self:flex-start;border:none;background:transparent;padding:0;margin-block-start:4px;color:var(--_accent);font:inherit;font-size:12px;font-weight:700;cursor:pointer}.fly-comment-thread__toggle-replies:hover{text-decoration:underline}.fly-comment-thread__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__load-more:hover{background:var(--_surface-hover)}.fly-comment-thread__load-more--nested{margin-inline-start:20px}.fly-comment-thread__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-comment-thread__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-comment-thread__dialog{position:relative;z-index:1;inline-size:min(420px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-comment-thread__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-comment-thread__reason-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-comment-thread__reason-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-comment-thread__reason-option{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-comment-thread__reason-option input{accent-color:var(--_accent);cursor:pointer}:host(.fly-comment-thread--readonly) .fly-comment-thread__composer,:host(.fly-comment-thread--readonly) .fly-comment-thread__actions{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: FlyTooltipDirective, selector: "[flyTooltip], [data-tooltip]", inputs: ["flyTooltip", "flyTooltipPlacement", "flyTooltipDisabled", "flyTooltipDelay"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13210
13616
  }
13211
13617
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyCommentThreadComponent, decorators: [{
13212
13618
  type: Component,
13213
- args: [{ selector: 'fly-comment-thread', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
13619
+ args: [{ selector: 'fly-comment-thread', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe, FlyTooltipDirective], changeDetection: ChangeDetectionStrategy.OnPush, host: {
13214
13620
  class: 'fly-comment-thread',
13215
13621
  '[class.fly-comment-thread--locked]': 'isLocked()',
13216
13622
  '[class.fly-comment-thread--readonly]': 'isReadonly()',
13217
- }, template: "<div class=\"fly-comment-thread__body\">\r\n <!-- \u2500\u2500 Header: count + sort + lock toggle \u2500\u2500 -->\r\n <header class=\"fly-comment-thread__header\">\r\n <span class=\"fly-comment-thread__count\">\r\n {{ 'comment.count' | translate: { n: rootPage().total } }}\r\n </span>\r\n\r\n <div class=\"fly-comment-thread__header-actions\">\r\n <div class=\"fly-comment-thread__sort\" role=\"group\" [attr.aria-label]=\"'comment.sort_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__sort-btn\"\r\n [class.fly-comment-thread__sort-btn--active]=\"sort() === 'newest'\"\r\n (click)=\"onSortPick('newest')\">\r\n {{ 'comment.sort_newest' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__sort-btn\"\r\n [class.fly-comment-thread__sort-btn--active]=\"sort() === 'oldest'\"\r\n (click)=\"onSortPick('oldest')\">\r\n {{ 'comment.sort_oldest' | translate }}\r\n </button>\r\n </div>\r\n\r\n @if (canModerateThread() && !isReadonly()) {\r\n <button type=\"button\" class=\"fly-comment-thread__lock-btn\" (click)=\"toggleLock()\">\r\n {{ (isLocked() ? 'comment.unlock' : 'comment.lock') | translate }}\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n @if (isLocked()) {\r\n <p class=\"fly-comment-thread__locked-notice\">{{ 'comment.locked_notice' | translate }}</p>\r\n }\r\n\r\n <!-- \u2500\u2500 Root composer \u2500\u2500 -->\r\n @if (canReply()) {\r\n <div class=\"fly-comment-thread__composer\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [id]=\"fieldId('root-composer')\"\r\n [attr.aria-label]=\"'comment.composer_placeholder' | translate\"\r\n [placeholder]=\"'comment.composer_placeholder' | translate\"\r\n [ngModel]=\"rootDraft()\"\r\n (ngModelChange)=\"onRootDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!rootDraft().trim()\"\r\n (click)=\"submitRoot()\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- \u2500\u2500 Root comments \u2500\u2500 -->\r\n @if (rootPage().items.length === 0) {\r\n <p class=\"fly-comment-thread__empty\">{{ 'comment.empty' | translate }}</p>\r\n } @else {\r\n <ul class=\"fly-comment-thread__list\" role=\"list\">\r\n @for (c of rootPage().items; track c.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: c }\"></ng-container>\r\n }\r\n </ul>\r\n\r\n @if (rootPage().hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more\" (click)=\"loadMoreRoot()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Recursive comment node template \u2500\u2500 -->\r\n<ng-template #commentNode let-comment>\r\n <li class=\"fly-comment-thread__item\" [attr.data-depth]=\"comment.depth\">\r\n <div class=\"fly-comment-thread__row\">\r\n <div class=\"fly-comment-thread__avatar\" aria-hidden=\"true\">\r\n @if (comment.authorAvatarUrl) {\r\n <img [src]=\"comment.authorAvatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-comment-thread__avatar-fallback\">{{ (comment.authorDisplayName || comment.authorUserId)[0] }}</span>\r\n }\r\n </div>\r\n\r\n <div class=\"fly-comment-thread__content\">\r\n <div class=\"fly-comment-thread__meta\">\r\n <span class=\"fly-comment-thread__author\">{{ comment.authorDisplayName || comment.authorUserId }}</span>\r\n <span class=\"fly-comment-thread__timestamp\">{{ formatDate(comment.createdAt) }}</span>\r\n @if (comment.isEdited && !comment.isDeleted) {\r\n <span class=\"fly-comment-thread__edited\">{{ 'comment.edited' | translate }}</span>\r\n }\r\n </div>\r\n\r\n @if (comment.isDeleted) {\r\n <p class=\"fly-comment-thread__tombstone\">{{ 'comment.deleted' | translate }}</p>\r\n } @else if (editingId() === comment.id) {\r\n <div class=\"fly-comment-thread__edit-box\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [ngModel]=\"comment.body\"\r\n (ngModelChange)=\"onEditDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelEdit()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" (click)=\"saveEdit(comment)\">\r\n {{ 'comment.save' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <p class=\"fly-comment-thread__text\">{{ comment.body }}</p>\r\n }\r\n\r\n @if (!isReadonly() && !comment.isDeleted && editingId() !== comment.id) {\r\n <div class=\"fly-comment-thread__actions\">\r\n @if (canReply()) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn\" (click)=\"openReplyBox(comment.id)\">\r\n {{ 'comment.reply' | translate }}\r\n </button>\r\n }\r\n @if (canEditComment(comment)) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn\" (click)=\"startEdit(comment)\">\r\n {{ 'comment.edit' | translate }}\r\n </button>\r\n }\r\n @if (canDeleteComment(comment)) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn fly-comment-thread__action-btn--danger\" (click)=\"requestDelete(comment)\">\r\n {{ 'comment.delete' | translate }}\r\n </button>\r\n }\r\n @if (canReport(comment)) {\r\n <button type=\"button\" class=\"fly-comment-thread__action-btn\" (click)=\"openReport(comment)\">\r\n {{ 'comment.report' | translate }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (confirmingDeleteId() === comment.id) {\r\n <div class=\"fly-comment-thread__confirm\">\r\n <span>{{ 'comment.delete_confirm' | translate }}</span>\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelDelete()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__danger-btn\" (click)=\"confirmDelete(comment)\">\r\n {{ 'comment.delete' | translate }}\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (isReplyBoxOpen(comment.id)) {\r\n <div class=\"fly-comment-thread__composer fly-comment-thread__composer--reply\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"2\"\r\n [attr.aria-label]=\"'comment.reply_placeholder' | translate\"\r\n [placeholder]=\"'comment.reply_placeholder' | translate\"\r\n [ngModel]=\"replyDraft(comment.id)\"\r\n (ngModelChange)=\"onReplyDraftChange(comment.id, $event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReplyBox(comment.id)\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!replyDraft(comment.id).trim()\"\r\n (click)=\"submitReply(comment.id)\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (comment.replyCount > 0) {\r\n <button type=\"button\" class=\"fly-comment-thread__toggle-replies\" (click)=\"toggleReplies(comment)\">\r\n {{ (isExpanded(comment.id) ? 'comment.hide_replies' : 'comment.show_replies') | translate: { n: comment.replyCount } }}\r\n </button>\r\n }\r\n\r\n @if (isExpanded(comment.id)) {\r\n <ul class=\"fly-comment-thread__list fly-comment-thread__list--nested\" role=\"list\">\r\n @for (child of repliesFor(comment.id)?.items ?? []; track child.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: child }\"></ng-container>\r\n }\r\n </ul>\r\n @if (repliesFor(comment.id)?.hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more fly-comment-thread__load-more--nested\" (click)=\"loadMoreReplies(comment.id)\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </li>\r\n</ng-template>\r\n\r\n<!-- \u2500\u2500 Report dialog \u2500\u2500 -->\r\n@if (reportingId()) {\r\n <div class=\"fly-comment-thread__overlay\">\r\n <div class=\"fly-comment-thread__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeReport()\"\r\n (keydown.enter)=\"closeReport()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div class=\"fly-comment-thread__dialog\" role=\"dialog\" [attr.aria-label]=\"'comment.report_title' | translate\">\r\n <h3 class=\"fly-comment-thread__dialog-title\">{{ 'comment.report_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-comment-thread__reason-group\">\r\n <legend>{{ 'comment.report_reason_label' | translate }}</legend>\r\n @for (reason of reportReasons; track reason) {\r\n <label class=\"fly-comment-thread__reason-option\">\r\n <input\r\n type=\"radio\"\r\n name=\"fly-comment-report-reason\"\r\n [value]=\"reason\"\r\n [checked]=\"reportReason() === reason\"\r\n (change)=\"onReportReasonChange(reason)\" />\r\n <span>{{ 'comment.reason.' + reason | translate }}</span>\r\n </label>\r\n }\r\n </fieldset>\r\n\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'comment.report_detail_label' | translate\"\r\n [placeholder]=\"'comment.report_detail_label' | translate\"\r\n [ngModel]=\"reportDetail()\"\r\n (ngModelChange)=\"onReportDetailChange($event)\"></textarea>\r\n\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReport()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" [disabled]=\"!reportReason()\" (click)=\"submitReport()\">\r\n {{ 'comment.report_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-comment-thread__body{display:flex;flex-direction:column;gap:16px}.fly-comment-thread__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-comment-thread__count{font-weight:700;font-size:14px}.fly-comment-thread__header-actions{display:flex;align-items:center;gap:10px}.fly-comment-thread__sort{display:inline-flex;border:1px solid var(--_border);border-radius:var(--_radius);overflow:hidden}.fly-comment-thread__sort-btn{padding:5px 12px;border:none;border-inline-end:1px solid var(--_border);background:transparent;color:var(--_text-subtle);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__sort-btn:last-child{border-inline-end:none}.fly-comment-thread__sort-btn--active{background:var(--_accent);color:#fff}.fly-comment-thread__lock-btn{padding:5px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__lock-btn:hover{background:var(--_surface-hover)}.fly-comment-thread__locked-notice{margin:0;padding:8px 12px;border-radius:var(--_radius);background:var(--_fill);color:var(--_text-subtle);font-size:12px}.fly-comment-thread__empty{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-comment-thread__composer{display:flex;flex-direction:column;gap:8px}.fly-comment-thread__composer--reply{margin-block-start:8px}.fly-comment-thread__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-comment-thread__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-comment-thread__composer-actions{display:flex;justify-content:flex-end;gap:8px}.fly-comment-thread__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__submit-btn:hover:not(:disabled){opacity:.9}.fly-comment-thread__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-comment-thread__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-comment-thread__cancel-btn:hover{background:var(--_surface-hover)}.fly-comment-thread__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__danger-btn:hover{opacity:.9}.fly-comment-thread__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}.fly-comment-thread__list--nested{margin-block-start:10px;margin-inline-start:20px;padding-inline-start:12px;border-inline-start:2px solid var(--_border);gap:12px}.fly-comment-thread__item{display:block}.fly-comment-thread__row{display:flex;gap:10px;align-items:flex-start}.fly-comment-thread__avatar{inline-size:32px;block-size:32px;border-radius:50%;overflow:hidden;flex:none;background:var(--_fill);display:flex;align-items:center;justify-content:center}.fly-comment-thread__avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-comment-thread__avatar-fallback{font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-comment-thread__content{flex:1 1 auto;min-inline-size:0;display:flex;flex-direction:column;gap:4px}.fly-comment-thread__meta{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.fly-comment-thread__author{font-weight:700;color:var(--_text)}.fly-comment-thread__timestamp{font-size:11px;color:var(--_text-tertiary)}.fly-comment-thread__edited{font-size:11px;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__text{margin:0;color:var(--_text);line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}.fly-comment-thread__tombstone{margin:0;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__edit-box{display:flex;flex-direction:column;gap:6px}.fly-comment-thread__actions{display:flex;gap:12px;margin-block-start:2px}.fly-comment-thread__action-btn{border:none;background:transparent;padding:0;color:var(--_text-subtle);font:inherit;font-size:12px;font-weight:600;cursor:pointer}.fly-comment-thread__action-btn:hover{color:var(--_accent);text-decoration:underline}.fly-comment-thread__action-btn--danger:hover{color:var(--_danger)}.fly-comment-thread__confirm{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--_radius);background:var(--_fill);font-size:12px;color:var(--_text-subtle)}.fly-comment-thread__toggle-replies{align-self:flex-start;border:none;background:transparent;padding:0;margin-block-start:4px;color:var(--_accent);font:inherit;font-size:12px;font-weight:700;cursor:pointer}.fly-comment-thread__toggle-replies:hover{text-decoration:underline}.fly-comment-thread__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__load-more:hover{background:var(--_surface-hover)}.fly-comment-thread__load-more--nested{margin-inline-start:20px}.fly-comment-thread__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-comment-thread__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-comment-thread__dialog{position:relative;z-index:1;inline-size:min(420px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-comment-thread__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-comment-thread__reason-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-comment-thread__reason-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-comment-thread__reason-option{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-comment-thread__reason-option input{accent-color:var(--_accent);cursor:pointer}:host(.fly-comment-thread--readonly) .fly-comment-thread__composer,:host(.fly-comment-thread--readonly) .fly-comment-thread__actions{display:none}\n"] }]
13218
- }], propDecorators: { rootPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootPage", required: true }] }], repliesByParent: [{ type: i0.Input, args: [{ isSignal: true, alias: "repliesByParent", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], isLocked: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLocked", required: false }] }], currentUserId: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentUserId", required: false }] }], isReadonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], canModerateThread: [{ type: i0.Input, args: [{ isSignal: true, alias: "canModerateThread", required: false }] }], loadReplies: [{ type: i0.Output, args: ["loadReplies"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], submit: [{ type: i0.Output, args: ["submit"] }], edit: [{ type: i0.Output, args: ["edit"] }], delete: [{ type: i0.Output, args: ["delete"] }], report: [{ type: i0.Output, args: ["report"] }], lockToggle: [{ type: i0.Output, args: ["lockToggle"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }] } });
13623
+ }, template: "<div class=\"fly-comment-thread__body\">\r\n <!-- \u2500\u2500 Header: count + one combined sort/lock icon-button toolbar \u2500\u2500 -->\r\n <header class=\"fly-comment-thread__header\">\r\n <span class=\"fly-comment-thread__count\">\r\n {{ 'comment.count' | translate: { n: rootPage().total } }}\r\n </span>\r\n\r\n <div class=\"fly-comment-thread__toolbar\" role=\"group\" [attr.aria-label]=\"'comment.sort_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [class.fly-comment-thread__icon-btn--active]=\"sort() === 'newest'\"\r\n [attr.aria-pressed]=\"sort() === 'newest'\"\r\n [flyTooltip]=\"'comment.sort_newest' | translate\"\r\n [attr.aria-label]=\"'comment.sort_newest' | translate\"\r\n (click)=\"onSortPick('newest')\">\r\n <i class=\"pi pi-sort-amount-down\" aria-hidden=\"true\"></i>\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [class.fly-comment-thread__icon-btn--active]=\"sort() === 'oldest'\"\r\n [attr.aria-pressed]=\"sort() === 'oldest'\"\r\n [flyTooltip]=\"'comment.sort_oldest' | translate\"\r\n [attr.aria-label]=\"'comment.sort_oldest' | translate\"\r\n (click)=\"onSortPick('oldest')\">\r\n <i class=\"pi pi-sort-amount-up\" aria-hidden=\"true\"></i>\r\n </button>\r\n\r\n @if (canModerateThread() && !isReadonly()) {\r\n <span class=\"fly-comment-thread__toolbar-sep\" aria-hidden=\"true\"></span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [class.fly-comment-thread__icon-btn--active]=\"isLocked()\"\r\n [attr.aria-pressed]=\"isLocked()\"\r\n [flyTooltip]=\"(isLocked() ? 'comment.unlock' : 'comment.lock') | translate\"\r\n [attr.aria-label]=\"(isLocked() ? 'comment.unlock' : 'comment.lock') | translate\"\r\n (click)=\"toggleLock()\">\r\n <i class=\"pi\" [class.pi-lock]=\"!isLocked()\" [class.pi-lock-open]=\"isLocked()\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n @if (isLocked()) {\r\n <p class=\"fly-comment-thread__locked-notice\">{{ 'comment.locked_notice' | translate }}</p>\r\n }\r\n\r\n <!-- \u2500\u2500 Root composer \u2500\u2500 -->\r\n @if (canReply()) {\r\n <div class=\"fly-comment-thread__composer\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [id]=\"fieldId('root-composer')\"\r\n [attr.aria-label]=\"'comment.composer_placeholder' | translate\"\r\n [placeholder]=\"'comment.composer_placeholder' | translate\"\r\n [ngModel]=\"rootDraft()\"\r\n (ngModelChange)=\"onRootDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!rootDraft().trim()\"\r\n (click)=\"submitRoot()\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- \u2500\u2500 Root comments \u2500\u2500 -->\r\n @if (rootPage().items.length === 0) {\r\n <p class=\"fly-comment-thread__empty\">{{ 'comment.empty' | translate }}</p>\r\n } @else {\r\n <ul class=\"fly-comment-thread__list\" role=\"list\">\r\n @for (c of rootPage().items; track c.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: c }\"></ng-container>\r\n }\r\n </ul>\r\n\r\n @if (rootPage().hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more\" (click)=\"loadMoreRoot()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Recursive comment node template \u2500\u2500 -->\r\n<ng-template #commentNode let-comment>\r\n <li class=\"fly-comment-thread__item\" [attr.data-depth]=\"comment.depth\">\r\n <div class=\"fly-comment-thread__row\">\r\n <div class=\"fly-comment-thread__avatar\" aria-hidden=\"true\">\r\n @if (comment.authorAvatarUrl) {\r\n <img [src]=\"comment.authorAvatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-comment-thread__avatar-fallback\">{{ authorInitial(comment) }}</span>\r\n }\r\n </div>\r\n\r\n <div class=\"fly-comment-thread__content\">\r\n <div class=\"fly-comment-thread__meta\">\r\n <span class=\"fly-comment-thread__author\">{{ authorLabel(comment) }}</span>\r\n <span class=\"fly-comment-thread__timestamp\">{{ formatDate(comment.createdAt) }}</span>\r\n @if (comment.isEdited && !comment.isDeleted) {\r\n <span class=\"fly-comment-thread__edited\">{{ 'comment.edited' | translate }}</span>\r\n }\r\n </div>\r\n\r\n @if (comment.isDeleted) {\r\n <p class=\"fly-comment-thread__tombstone\">{{ 'comment.deleted' | translate }}</p>\r\n } @else if (editingId() === comment.id) {\r\n <div class=\"fly-comment-thread__edit-box\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [ngModel]=\"comment.body\"\r\n (ngModelChange)=\"onEditDraftChange($event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelEdit()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" (click)=\"saveEdit(comment)\">\r\n {{ 'comment.save' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"fly-comment-thread__text-row\">\r\n <p class=\"fly-comment-thread__text\">{{ comment.body }}</p>\r\n @if (!isReadonly()) {\r\n <div class=\"fly-comment-thread__actions\">\r\n @if (canReply()) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [flyTooltip]=\"'comment.reply' | translate\"\r\n [attr.aria-label]=\"'comment.reply' | translate\"\r\n (click)=\"openReplyBox(comment.id)\">\r\n <i class=\"pi pi-reply\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n @if (canEditComment(comment)) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [flyTooltip]=\"'comment.edit' | translate\"\r\n [attr.aria-label]=\"'comment.edit' | translate\"\r\n (click)=\"startEdit(comment)\">\r\n <i class=\"pi pi-pencil\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n @if (canDeleteComment(comment)) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn fly-comment-thread__icon-btn--danger\"\r\n [flyTooltip]=\"'comment.delete' | translate\"\r\n [attr.aria-label]=\"'comment.delete' | translate\"\r\n (click)=\"requestDelete(comment)\">\r\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n @if (canReport(comment)) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__icon-btn\"\r\n [flyTooltip]=\"'comment.report' | translate\"\r\n [attr.aria-label]=\"'comment.report' | translate\"\r\n (click)=\"openReport(comment)\">\r\n <i class=\"pi pi-flag\" aria-hidden=\"true\"></i>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (confirmingDeleteId() === comment.id) {\r\n <div class=\"fly-comment-thread__confirm\">\r\n <span>{{ 'comment.delete_confirm' | translate }}</span>\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"cancelDelete()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__danger-btn\" (click)=\"confirmDelete(comment)\">\r\n {{ 'comment.delete' | translate }}\r\n </button>\r\n </div>\r\n }\r\n\r\n @if (isReplyBoxOpen(comment.id)) {\r\n <div class=\"fly-comment-thread__composer fly-comment-thread__composer--reply\">\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"2\"\r\n [attr.aria-label]=\"'comment.reply_placeholder' | translate\"\r\n [placeholder]=\"'comment.reply_placeholder' | translate\"\r\n [ngModel]=\"replyDraft(comment.id)\"\r\n (ngModelChange)=\"onReplyDraftChange(comment.id, $event)\"></textarea>\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReplyBox(comment.id)\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-comment-thread__submit-btn\"\r\n [disabled]=\"!replyDraft(comment.id).trim()\"\r\n (click)=\"submitReply(comment.id)\">\r\n {{ 'comment.post' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n @if (comment.replyCount > 0) {\r\n <button type=\"button\" class=\"fly-comment-thread__toggle-replies\" (click)=\"toggleReplies(comment)\">\r\n {{ (isExpanded(comment.id) ? 'comment.hide_replies' : 'comment.show_replies') | translate: { n: comment.replyCount } }}\r\n </button>\r\n }\r\n\r\n @if (isExpanded(comment.id)) {\r\n <ul class=\"fly-comment-thread__list fly-comment-thread__list--nested\" role=\"list\">\r\n @for (child of repliesFor(comment.id)?.items ?? []; track child.id) {\r\n <ng-container [ngTemplateOutlet]=\"commentNode\" [ngTemplateOutletContext]=\"{ $implicit: child }\"></ng-container>\r\n }\r\n </ul>\r\n @if (repliesFor(comment.id)?.hasMore) {\r\n <button type=\"button\" class=\"fly-comment-thread__load-more fly-comment-thread__load-more--nested\" (click)=\"loadMoreReplies(comment.id)\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </li>\r\n</ng-template>\r\n\r\n<!-- \u2500\u2500 Report dialog \u2500\u2500 -->\r\n@if (reportingId()) {\r\n <div class=\"fly-comment-thread__overlay\">\r\n <div class=\"fly-comment-thread__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeReport()\"\r\n (keydown.enter)=\"closeReport()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div class=\"fly-comment-thread__dialog\" role=\"dialog\" [attr.aria-label]=\"'comment.report_title' | translate\">\r\n <h3 class=\"fly-comment-thread__dialog-title\">{{ 'comment.report_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-comment-thread__reason-group\">\r\n <legend>{{ 'comment.report_reason_label' | translate }}</legend>\r\n @for (reason of reportReasons; track reason) {\r\n <label class=\"fly-comment-thread__reason-option\">\r\n <input\r\n type=\"radio\"\r\n name=\"fly-comment-report-reason\"\r\n [value]=\"reason\"\r\n [checked]=\"reportReason() === reason\"\r\n (change)=\"onReportReasonChange(reason)\" />\r\n <span>{{ 'comment.reason.' + reason | translate }}</span>\r\n </label>\r\n }\r\n </fieldset>\r\n\r\n <textarea\r\n class=\"fly-comment-thread__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'comment.report_detail_label' | translate\"\r\n [placeholder]=\"'comment.report_detail_label' | translate\"\r\n [ngModel]=\"reportDetail()\"\r\n (ngModelChange)=\"onReportDetailChange($event)\"></textarea>\r\n\r\n <div class=\"fly-comment-thread__composer-actions\">\r\n <button type=\"button\" class=\"fly-comment-thread__cancel-btn\" (click)=\"closeReport()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-comment-thread__submit-btn\" [disabled]=\"!reportReason()\" (click)=\"submitReport()\">\r\n {{ 'comment.report_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 8px;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-comment-thread__body{display:flex;flex-direction:column;gap:16px}.fly-comment-thread__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-comment-thread__count{font-weight:700;font-size:14px}.fly-comment-thread__toolbar{display:inline-flex;align-items:center;gap:2px;padding:2px;border:1px solid var(--_border);border-radius:calc(var(--_radius) + 2px);background:var(--_surface)}.fly-comment-thread__toolbar-sep{inline-size:1px;block-size:18px;margin-inline:2px;background:var(--_border)}.fly-comment-thread__icon-btn{display:inline-flex;align-items:center;justify-content:center;inline-size:26px;block-size:26px;flex:none;padding:0;border:none;border-radius:50%;background:transparent;color:var(--_text-subtle);cursor:pointer;transition:background var(--t-state, .15s),color var(--t-state, .15s)}.fly-comment-thread__icon-btn .pi{font-size:13px}.fly-comment-thread__icon-btn:hover{background:var(--_surface-hover);color:var(--_text)}.fly-comment-thread__icon-btn:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-comment-thread__icon-btn--active{background:var(--_accent);color:#fff}.fly-comment-thread__icon-btn--active:hover{background:var(--_accent);opacity:.9}.fly-comment-thread__icon-btn--danger:hover{background:var(--_danger);color:#fff}.fly-comment-thread__locked-notice{margin:0;padding:8px 12px;border-radius:var(--_radius);background:var(--_fill);color:var(--_text-subtle);font-size:12px}.fly-comment-thread__empty{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-comment-thread__composer{display:flex;flex-direction:column;gap:8px}.fly-comment-thread__composer--reply{margin-block-start:8px}.fly-comment-thread__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-comment-thread__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-comment-thread__composer-actions{display:flex;justify-content:flex-end;gap:8px}.fly-comment-thread__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__submit-btn:hover:not(:disabled){opacity:.9}.fly-comment-thread__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-comment-thread__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-comment-thread__cancel-btn:hover{background:var(--_surface-hover)}.fly-comment-thread__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__danger-btn:hover{opacity:.9}.fly-comment-thread__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:14px}.fly-comment-thread__list--nested{margin-block-start:10px;margin-inline-start:20px;padding-inline-start:12px;border-inline-start:2px solid var(--_border);gap:12px}.fly-comment-thread__item{display:block}.fly-comment-thread__row{display:flex;gap:10px;align-items:flex-start}.fly-comment-thread__avatar{inline-size:32px;block-size:32px;border-radius:50%;overflow:hidden;flex:none;background:var(--_fill);display:flex;align-items:center;justify-content:center}.fly-comment-thread__avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-comment-thread__avatar-fallback{font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-comment-thread__content{flex:1 1 auto;min-inline-size:0;display:flex;flex-direction:column;gap:4px}.fly-comment-thread__meta{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.fly-comment-thread__author{font-weight:700;color:var(--_text)}.fly-comment-thread__timestamp{font-size:11px;color:var(--_text-tertiary)}.fly-comment-thread__edited{font-size:11px;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__text-row{display:flex;align-items:flex-start;gap:4px}.fly-comment-thread__text{flex:1 1 auto;min-inline-size:0;margin:0;color:var(--_text);line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}.fly-comment-thread__tombstone{margin:0;color:var(--_text-tertiary);font-style:italic}.fly-comment-thread__edit-box{display:flex;flex-direction:column;gap:6px}.fly-comment-thread__actions{display:flex;align-items:center;gap:2px;flex:none;margin-inline-start:auto}.fly-comment-thread__confirm{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--_radius);background:var(--_fill);font-size:12px;color:var(--_text-subtle)}.fly-comment-thread__toggle-replies{align-self:flex-start;border:none;background:transparent;padding:0;margin-block-start:4px;color:var(--_accent);font:inherit;font-size:12px;font-weight:700;cursor:pointer}.fly-comment-thread__toggle-replies:hover{text-decoration:underline}.fly-comment-thread__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-comment-thread__load-more:hover{background:var(--_surface-hover)}.fly-comment-thread__load-more--nested{margin-inline-start:20px}.fly-comment-thread__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-comment-thread__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-comment-thread__dialog{position:relative;z-index:1;inline-size:min(420px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-comment-thread__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-comment-thread__reason-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-comment-thread__reason-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-comment-thread__reason-option{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-comment-thread__reason-option input{accent-color:var(--_accent);cursor:pointer}:host(.fly-comment-thread--readonly) .fly-comment-thread__composer,:host(.fly-comment-thread--readonly) .fly-comment-thread__actions{display:none}\n"] }]
13624
+ }], propDecorators: { rootPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootPage", required: true }] }], repliesByParent: [{ type: i0.Input, args: [{ isSignal: true, alias: "repliesByParent", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], isLocked: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLocked", required: false }] }], currentUserId: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentUserId", required: false }] }], isReadonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], canModerateThread: [{ type: i0.Input, args: [{ isSignal: true, alias: "canModerateThread", required: false }] }], resolveDisplayName: [{ type: i0.Input, args: [{ isSignal: true, alias: "resolveDisplayName", required: false }] }], loadReplies: [{ type: i0.Output, args: ["loadReplies"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], submit: [{ type: i0.Output, args: ["submit"] }], edit: [{ type: i0.Output, args: ["edit"] }], delete: [{ type: i0.Output, args: ["delete"] }], report: [{ type: i0.Output, args: ["report"] }], lockToggle: [{ type: i0.Output, args: ["lockToggle"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }] } });
13219
13625
 
13220
13626
  // ── Vendored synchronous SHA-256 (FIPS 180-4) ────────────────────────────────
13221
13627
  // Why this exists: `fly-captcha`'s PoW solver must brute-force up to
@@ -15456,32 +15862,56 @@ function isChunkLoadError(err) {
15456
15862
  return looksLikeModuleUrl && looksLikeFetchStatus;
15457
15863
  }
15458
15864
  /**
15459
- * sessionStorage key recording that this tab session's single chunk-error
15460
- * reload was already spent. It persists across the reload it triggers (an
15461
- * in-memory flag resets on reload and cannot guard a loop).
15865
+ * sessionStorage key recording WHEN this tab session last spent its chunk-error
15866
+ * reload (epoch ms). It persists across the reload it triggers (an in-memory
15867
+ * flag resets on reload and cannot guard a loop).
15462
15868
  */
15463
15869
  const FLY_CHUNK_RELOAD_FLAG = 'fly:chunk-reload-attempted';
15464
15870
  /**
15465
- * One-shot self-heal for a stale client after a redeploy. Permits exactly ONE
15466
- * reload per tab session, so a reload that keeps failing (a chunk truly missing
15467
- * from the current build, or a genuinely-down host) does NOT loop.
15871
+ * How long a spent self-heal attempt stays spent. Within the window a repeat
15872
+ * failure falls through to the caller's error UI (loop guard); after it the
15873
+ * heal re-arms. A permanent one-shot stranded long-lived tabs on environments
15874
+ * that redeploy repeatedly (e.g. a Helm upgrade→rollback→retry cycle flips the
15875
+ * served build several times in an afternoon — each flip needs its own reload,
15876
+ * but only the first ever got one). 10 min is far above any error→reload→error
15877
+ * loop period (seconds) while well below the gap between distinct redeploys.
15878
+ */
15879
+ const FLY_RELOAD_REARM_MS = 10 * 60 * 1000;
15880
+ /**
15881
+ * Self-heal for a stale client after a redeploy. Permits ONE reload per
15882
+ * {@link FLY_RELOAD_REARM_MS} window per tab session, so a reload that keeps
15883
+ * failing (a chunk truly missing from the current build, or a genuinely-down
15884
+ * host) does NOT loop — but a LATER redeploy re-arms it.
15468
15885
  *
15469
15886
  * @returns `true` when a reload was initiated (the page will navigate away);
15470
- * `false` when the session's reload was already spent or sessionStorage is
15887
+ * `false` when the window's reload was already spent or sessionStorage is
15471
15888
  * unavailable — callers should surface a fallback / stop rather than spin.
15472
15889
  */
15473
15890
  function reloadOnceForChunkError() {
15891
+ if (!trySpendHealAttempt(FLY_CHUNK_RELOAD_FLAG))
15892
+ return false;
15893
+ window.location.reload();
15894
+ return true;
15895
+ }
15896
+ /**
15897
+ * Records a heal attempt under `key` unless one was already recorded within the
15898
+ * last {@link FLY_RELOAD_REARM_MS}. A legacy `'1'` value (pre-timestamp builds)
15899
+ * parses to epoch 1 and therefore counts as expired — one extra reload on
15900
+ * upgrade, never a loop. Returns `false` on sessionStorage failure (private
15901
+ * mode / quota / non-browser): cannot record the attempt, so cannot prove a
15902
+ * reload wouldn't loop — prefer the caller's fallback over a spin.
15903
+ */
15904
+ function trySpendHealAttempt(key) {
15474
15905
  try {
15475
- if (sessionStorage.getItem(FLY_CHUNK_RELOAD_FLAG))
15906
+ const last = Number(sessionStorage.getItem(key));
15907
+ if (Number.isFinite(last) && last > 0 && Date.now() - last < FLY_RELOAD_REARM_MS) {
15476
15908
  return false;
15477
- sessionStorage.setItem(FLY_CHUNK_RELOAD_FLAG, '1');
15909
+ }
15910
+ sessionStorage.setItem(key, String(Date.now()));
15478
15911
  }
15479
15912
  catch {
15480
- // Private mode / quota / non-browser: cannot record the attempt, so cannot
15481
- // prove a reload wouldn't loop. Prefer the fallback over a spin.
15482
15913
  return false;
15483
15914
  }
15484
- window.location.reload();
15485
15915
  return true;
15486
15916
  }
15487
15917
  /**
@@ -15508,10 +15938,11 @@ function isNativeFederationCacheError(err) {
15508
15938
  return /Unable to resolve specifier ['"]?@nf-internal\/chunk-/i.test(msg);
15509
15939
  }
15510
15940
  /**
15511
- * sessionStorage key recording that this tab session's single Native-Federation
15512
- * cache-heal (revalidation sweep + reload) was already spent separate from
15513
- * {@link FLY_CHUNK_RELOAD_FLAG} so a plain chunk-404 reload and a shared-bundle
15514
- * revalidation sweep each get one independent attempt.
15941
+ * sessionStorage key recording WHEN this tab session last spent its
15942
+ * Native-Federation cache-heal (revalidation sweep + reload; epoch ms)
15943
+ * separate from {@link FLY_CHUNK_RELOAD_FLAG} so a plain chunk-404 reload and a
15944
+ * shared-bundle revalidation sweep each get one independent attempt per
15945
+ * {@link FLY_RELOAD_REARM_MS} window.
15515
15946
  */
15516
15947
  const FLY_NF_CACHE_HEAL_FLAG = 'nf-cache-heal-attempted';
15517
15948
  /**
@@ -15530,14 +15961,8 @@ const FLY_NF_CACHE_HEAL_FLAG = 'nf-cache-heal-attempted';
15530
15961
  * sessionStorage is unavailable (caller should surface a fallback).
15531
15962
  */
15532
15963
  function healNativeFederationCacheOnce() {
15533
- try {
15534
- if (sessionStorage.getItem(FLY_NF_CACHE_HEAL_FLAG))
15535
- return false;
15536
- sessionStorage.setItem(FLY_NF_CACHE_HEAL_FLAG, '1');
15537
- }
15538
- catch {
15964
+ if (!trySpendHealAttempt(FLY_NF_CACHE_HEAL_FLAG))
15539
15965
  return false;
15540
- }
15541
15966
  const reload = () => {
15542
15967
  try {
15543
15968
  window.location.reload();
@@ -15964,6 +16389,13 @@ class FlyModerationQueueComponent {
15964
16389
  error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
15965
16390
  /** The currently applied status filter; `null` = "All". Host-controlled, like `[sort]` on `fly-comment-thread`. */
15966
16391
  status = input(null, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
16392
+ /**
16393
+ * Optional id→name lookup for the reporter column, mirroring
16394
+ * `fly-comment-thread`'s input of the same name. Bind
16395
+ * `FlyUserDirectoryService.label`; absent, rows fall back to the raw id.
16396
+ * See {@link reporterLabel} for the resolution order.
16397
+ */
16398
+ resolveDisplayName = input(null, ...(ngDevMode ? [{ debugName: "resolveDisplayName" }] : /* istanbul ignore next */ []));
15967
16399
  // ── Outputs ──────────────────────────────────────────────────────────────────
15968
16400
  /** A status-filter pick (page resets to 1) or a `loadMore` (next page at the current filter). */
15969
16401
  loadPage = output();
@@ -16008,6 +16440,16 @@ class FlyModerationQueueComponent {
16008
16440
  subjectDisplay(r) {
16009
16441
  return r.subjectLabel || `${r.subjectType} #${this.shortId(r.subjectId)}`;
16010
16442
  }
16443
+ /**
16444
+ * Who filed the report. Resolution order: explicit `reporterDisplayName` →
16445
+ * {@link resolveDisplayName} → the raw `reporterUserId`. Same contract as
16446
+ * `fly-comment-thread`'s `authorLabel`.
16447
+ */
16448
+ reporterLabel(r) {
16449
+ if (r.reporterDisplayName)
16450
+ return r.reporterDisplayName;
16451
+ return this.resolveDisplayName()?.(r.reporterUserId) || r.reporterUserId;
16452
+ }
16011
16453
  formatDate(iso) {
16012
16454
  const d = new Date(iso);
16013
16455
  return Number.isNaN(d.getTime()) ? '' : d.toLocaleString();
@@ -16124,12 +16566,12 @@ class FlyModerationQueueComponent {
16124
16566
  btn?.focus({ preventScroll: true });
16125
16567
  }
16126
16568
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyModerationQueueComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16127
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyModerationQueueComponent, isStandalone: true, selector: "fly-moderation-queue", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadPage: "loadPage", resolve: "resolve", lockThread: "lockThread", openSubject: "openSubject" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, classAttribute: "fly-moderation-queue" }, ngImport: i0, template: "<div class=\"fly-moderation-queue__body\">\r\n <!-- \u2500\u2500 Header: count + status filter \u2500\u2500 -->\r\n <header class=\"fly-moderation-queue__header\">\r\n <span class=\"fly-moderation-queue__count\">\r\n {{ 'moderation.count' | translate: { n: page().total } }}\r\n </span>\r\n\r\n <div class=\"fly-moderation-queue__status-filter\" role=\"group\" [attr.aria-label]=\"'moderation.status_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === null\"\r\n [attr.aria-pressed]=\"status() === null\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(null)\">\r\n {{ 'moderation.status_all' | translate }}\r\n </button>\r\n @for (s of statuses; track s) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === s\"\r\n [attr.aria-pressed]=\"status() === s\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(s)\">\r\n {{ 'moderation.status.' + s | translate }}\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n <!-- \u2500\u2500 States \u2500\u2500 -->\r\n @if (error()) {\r\n <p class=\"fly-moderation-queue__error\" role=\"alert\">{{ error() }}</p>\r\n } @else if (loading() && page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__status-msg\">{{ 'moderation.loading' | translate }}</p>\r\n } @else if (page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__empty\">{{ 'moderation.empty' | translate }}</p>\r\n } @else {\r\n <!-- \u2500\u2500 Report table \u2500\u2500 -->\r\n <div class=\"fly-moderation-queue__table-wrap\">\r\n <table class=\"fly-moderation-queue__table\">\r\n <caption class=\"fly-moderation-queue__sr-only\">{{ 'moderation.table_caption' | translate }}</caption>\r\n <thead>\r\n <tr>\r\n <th scope=\"col\">{{ 'moderation.col_reason' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reporter' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reported' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_excerpt' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_subject' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_status' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_actions' | translate }}</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (r of page().items; track r.id) {\r\n <tr>\r\n <td class=\"fly-moderation-queue__reason-cell\">\r\n <span class=\"fly-moderation-queue__reason\">{{ 'comment.reason.' + r.reason | translate }}</span>\r\n @if (r.detail) {\r\n <span class=\"fly-moderation-queue__detail\" [title]=\"r.detail\">{{ r.detail }}</span>\r\n }\r\n </td>\r\n <td>{{ r.reporterDisplayName || r.reporterUserId }}</td>\r\n <td>{{ formatDate(r.createdAt) }}</td>\r\n <td class=\"fly-moderation-queue__excerpt\">{{ r.commentExcerpt || '\u2014' }}</td>\r\n <td>\r\n <button type=\"button\" class=\"fly-moderation-queue__subject-link\" [title]=\"subjectHref(r)\" (click)=\"onOpenSubject(r)\">\r\n {{ subjectDisplay(r) }}\r\n </button>\r\n </td>\r\n <td>\r\n <span class=\"fly-moderation-queue__badge\" [class]=\"'fly-moderation-queue__badge--' + r.status\">\r\n {{ 'moderation.status.' + r.status | translate }}\r\n </span>\r\n @if (r.resolutionNote) {\r\n <div class=\"fly-moderation-queue__resolution-note\">{{ r.resolutionNote }}</div>\r\n }\r\n </td>\r\n <td class=\"fly-moderation-queue__actions\">\r\n @if (isResolvable(r.status)) {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"openResolve(r)\">\r\n {{ 'moderation.resolve' | translate }}\r\n </button>\r\n }\r\n @if (confirmingLockId() === r.id) {\r\n <span class=\"fly-moderation-queue__lock-confirm\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"cancelLock()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-moderation-queue__danger-btn\" (click)=\"confirmLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n </span>\r\n } @else {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"requestLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n @if (page().hasMore) {\r\n <button type=\"button\" class=\"fly-moderation-queue__load-more\" [disabled]=\"loading()\" (click)=\"loadMore()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Resolve dialog \u2500\u2500 -->\r\n@if (resolvingReport(); as r) {\r\n <div class=\"fly-moderation-queue__overlay\">\r\n <div class=\"fly-moderation-queue__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeResolve()\"\r\n (keydown.enter)=\"closeResolve()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div\r\n class=\"fly-moderation-queue__dialog\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'moderation.resolve_title' | translate\">\r\n <h3 class=\"fly-moderation-queue__dialog-title\">{{ 'moderation.resolve_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-moderation-queue__resolution-group\">\r\n <legend>{{ 'moderation.resolution_label' | translate }}</legend>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'ActionTaken'\"\r\n (change)=\"onResolutionStatusChange('ActionTaken')\" />\r\n <span>{{ 'moderation.resolution.ActionTaken' | translate }}</span>\r\n </label>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'Dismissed'\"\r\n (change)=\"onResolutionStatusChange('Dismissed')\" />\r\n <span>{{ 'moderation.resolution.Dismissed' | translate }}</span>\r\n </label>\r\n </fieldset>\r\n\r\n @if (resolutionStatus() === 'ActionTaken') {\r\n <label class=\"fly-moderation-queue__checkbox-row\">\r\n <input type=\"checkbox\" [checked]=\"deleteComment()\" (change)=\"onDeleteCommentToggle($event)\" />\r\n <span>{{ 'moderation.delete_comment_label' | translate }}</span>\r\n </label>\r\n }\r\n\r\n <textarea\r\n class=\"fly-moderation-queue__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'moderation.resolution_note_label' | translate\"\r\n [placeholder]=\"'moderation.resolution_note_label' | translate\"\r\n [ngModel]=\"resolutionNote()\"\r\n (ngModelChange)=\"onResolutionNoteChange($event)\"></textarea>\r\n\r\n <div class=\"fly-moderation-queue__dialog-actions\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"closeResolve()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__submit-btn\"\r\n [disabled]=\"!resolutionStatus()\"\r\n (click)=\"submitResolve(r)\">\r\n {{ 'moderation.resolve_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_blue: var(--system-blue, #0a84ff);--_orange: var(--system-orange, #ff9f0a);--_green: var(--system-green, #32d74b);--_radius: 8px;--_blue-text: #1d4ed8;--_orange-text: #9a3412;--_green-text: #166534;--_danger-text: #b91c1c;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-moderation-queue__body{display:flex;flex-direction:column;gap:16px}.fly-moderation-queue__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-moderation-queue__count{font-weight:700;font-size:14px}.fly-moderation-queue__status-filter{display:inline-flex;flex-wrap:wrap;border:1px solid var(--_border);border-radius:var(--_radius);overflow:hidden}.fly-moderation-queue__status-btn{padding:5px 12px;border:none;border-inline-end:1px solid var(--_border);background:transparent;color:var(--_text-subtle);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__status-btn:last-child{border-inline-end:none}.fly-moderation-queue__status-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__status-btn--active{background:var(--_accent);color:#fff}.fly-moderation-queue__empty,.fly-moderation-queue__status-msg{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-moderation-queue__error{margin:0;padding:8px 12px;border-radius:var(--_radius);background:color-mix(in srgb,var(--_danger) 12%,transparent);color:var(--_danger);font-size:12px}.fly-moderation-queue__sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.fly-moderation-queue__table-wrap{overflow-x:auto}.fly-moderation-queue__table{inline-size:100%;border-collapse:collapse;font-size:12px}.fly-moderation-queue__table th{text-align:start;padding:6px 10px;color:var(--_text-subtle);font-weight:600;border-block-end:1px solid var(--_border);white-space:nowrap}.fly-moderation-queue__table td{padding:8px 10px;border-block-end:1px solid var(--_border);vertical-align:top}.fly-moderation-queue__reason-cell{display:table-cell}.fly-moderation-queue__reason{display:block;font-weight:600}.fly-moderation-queue__detail{display:block;margin-block-start:2px;color:var(--_text-tertiary);font-size:11px;max-inline-size:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__excerpt{max-inline-size:260px;color:var(--_text-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__subject-link{border:none;background:transparent;padding:0;color:var(--_accent);font:inherit;font-weight:600;cursor:pointer;text-align:start}.fly-moderation-queue__subject-link:hover{text-decoration:underline}.fly-moderation-queue__badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:700;white-space:nowrap}.fly-moderation-queue__badge--Open{background:color-mix(in srgb,var(--_blue) 12%,#fff);color:var(--_blue-text)}.fly-moderation-queue__badge--UnderReview{background:color-mix(in srgb,var(--_orange) 12%,#fff);color:var(--_orange-text)}.fly-moderation-queue__badge--ActionTaken{background:color-mix(in srgb,var(--_danger) 12%,#fff);color:var(--_danger-text)}.fly-moderation-queue__badge--Dismissed{background:color-mix(in srgb,var(--_green) 12%,#fff);color:var(--_green-text)}.fly-moderation-queue__resolution-note{margin-block-start:4px;color:var(--_text-tertiary);font-size:11px;max-inline-size:200px}.fly-moderation-queue__actions{display:flex;flex-direction:column;align-items:flex-start;gap:6px;white-space:nowrap}.fly-moderation-queue__action-btn{border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:4px 10px;color:var(--_text);font:inherit;font-size:11px;font-weight:600;cursor:pointer}.fly-moderation-queue__action-btn:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__action-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__lock-confirm{display:inline-flex;align-items:center;gap:6px}.fly-moderation-queue__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__load-more:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__load-more:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-moderation-queue__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-moderation-queue__dialog{position:relative;z-index:1;inline-size:min(440px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-moderation-queue__dialog:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}.fly-moderation-queue__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-moderation-queue__resolution-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-moderation-queue__resolution-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-moderation-queue__resolution-option,.fly-moderation-queue__checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-moderation-queue__resolution-option input,.fly-moderation-queue__checkbox-row input{accent-color:var(--_accent);cursor:pointer}.fly-moderation-queue__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-moderation-queue__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-moderation-queue__dialog-actions{display:flex;justify-content:flex-end;gap:8px}.fly-moderation-queue__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__submit-btn:hover:not(:disabled){opacity:.9}.fly-moderation-queue__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-moderation-queue__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-moderation-queue__cancel-btn:hover{background:var(--_surface-hover)}.fly-moderation-queue__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__danger-btn:hover{opacity:.9}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16569
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyModerationQueueComponent, isStandalone: true, selector: "fly-moderation-queue", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, resolveDisplayName: { classPropertyName: "resolveDisplayName", publicName: "resolveDisplayName", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadPage: "loadPage", resolve: "resolve", lockThread: "lockThread", openSubject: "openSubject" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, classAttribute: "fly-moderation-queue" }, ngImport: i0, template: "<div class=\"fly-moderation-queue__body\">\r\n <!-- \u2500\u2500 Header: count + status filter \u2500\u2500 -->\r\n <header class=\"fly-moderation-queue__header\">\r\n <span class=\"fly-moderation-queue__count\">\r\n {{ 'moderation.count' | translate: { n: page().total } }}\r\n </span>\r\n\r\n <div class=\"fly-moderation-queue__status-filter\" role=\"group\" [attr.aria-label]=\"'moderation.status_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === null\"\r\n [attr.aria-pressed]=\"status() === null\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(null)\">\r\n {{ 'moderation.status_all' | translate }}\r\n </button>\r\n @for (s of statuses; track s) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === s\"\r\n [attr.aria-pressed]=\"status() === s\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(s)\">\r\n {{ 'moderation.status.' + s | translate }}\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n <!-- \u2500\u2500 States \u2500\u2500 -->\r\n @if (error()) {\r\n <p class=\"fly-moderation-queue__error\" role=\"alert\">{{ error() }}</p>\r\n } @else if (loading() && page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__status-msg\">{{ 'moderation.loading' | translate }}</p>\r\n } @else if (page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__empty\">{{ 'moderation.empty' | translate }}</p>\r\n } @else {\r\n <!-- \u2500\u2500 Report table \u2500\u2500 -->\r\n <div class=\"fly-moderation-queue__table-wrap\">\r\n <table class=\"fly-moderation-queue__table\">\r\n <caption class=\"fly-moderation-queue__sr-only\">{{ 'moderation.table_caption' | translate }}</caption>\r\n <thead>\r\n <tr>\r\n <th scope=\"col\">{{ 'moderation.col_reason' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reporter' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reported' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_excerpt' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_subject' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_status' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_actions' | translate }}</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (r of page().items; track r.id) {\r\n <tr>\r\n <td class=\"fly-moderation-queue__reason-cell\">\r\n <span class=\"fly-moderation-queue__reason\">{{ 'comment.reason.' + r.reason | translate }}</span>\r\n @if (r.detail) {\r\n <span class=\"fly-moderation-queue__detail\" [title]=\"r.detail\">{{ r.detail }}</span>\r\n }\r\n </td>\r\n <td>{{ reporterLabel(r) }}</td>\r\n <td>{{ formatDate(r.createdAt) }}</td>\r\n <td class=\"fly-moderation-queue__excerpt\">{{ r.commentExcerpt || '\u2014' }}</td>\r\n <td>\r\n <button type=\"button\" class=\"fly-moderation-queue__subject-link\" [title]=\"subjectHref(r)\" (click)=\"onOpenSubject(r)\">\r\n {{ subjectDisplay(r) }}\r\n </button>\r\n </td>\r\n <td>\r\n <span class=\"fly-moderation-queue__badge\" [class]=\"'fly-moderation-queue__badge--' + r.status\">\r\n {{ 'moderation.status.' + r.status | translate }}\r\n </span>\r\n @if (r.resolutionNote) {\r\n <div class=\"fly-moderation-queue__resolution-note\">{{ r.resolutionNote }}</div>\r\n }\r\n </td>\r\n <td class=\"fly-moderation-queue__actions\">\r\n @if (isResolvable(r.status)) {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"openResolve(r)\">\r\n {{ 'moderation.resolve' | translate }}\r\n </button>\r\n }\r\n @if (confirmingLockId() === r.id) {\r\n <span class=\"fly-moderation-queue__lock-confirm\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"cancelLock()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-moderation-queue__danger-btn\" (click)=\"confirmLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n </span>\r\n } @else {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"requestLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n @if (page().hasMore) {\r\n <button type=\"button\" class=\"fly-moderation-queue__load-more\" [disabled]=\"loading()\" (click)=\"loadMore()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Resolve dialog \u2500\u2500 -->\r\n@if (resolvingReport(); as r) {\r\n <div class=\"fly-moderation-queue__overlay\">\r\n <div class=\"fly-moderation-queue__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeResolve()\"\r\n (keydown.enter)=\"closeResolve()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div\r\n class=\"fly-moderation-queue__dialog\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'moderation.resolve_title' | translate\">\r\n <h3 class=\"fly-moderation-queue__dialog-title\">{{ 'moderation.resolve_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-moderation-queue__resolution-group\">\r\n <legend>{{ 'moderation.resolution_label' | translate }}</legend>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'ActionTaken'\"\r\n (change)=\"onResolutionStatusChange('ActionTaken')\" />\r\n <span>{{ 'moderation.resolution.ActionTaken' | translate }}</span>\r\n </label>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'Dismissed'\"\r\n (change)=\"onResolutionStatusChange('Dismissed')\" />\r\n <span>{{ 'moderation.resolution.Dismissed' | translate }}</span>\r\n </label>\r\n </fieldset>\r\n\r\n @if (resolutionStatus() === 'ActionTaken') {\r\n <label class=\"fly-moderation-queue__checkbox-row\">\r\n <input type=\"checkbox\" [checked]=\"deleteComment()\" (change)=\"onDeleteCommentToggle($event)\" />\r\n <span>{{ 'moderation.delete_comment_label' | translate }}</span>\r\n </label>\r\n }\r\n\r\n <textarea\r\n class=\"fly-moderation-queue__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'moderation.resolution_note_label' | translate\"\r\n [placeholder]=\"'moderation.resolution_note_label' | translate\"\r\n [ngModel]=\"resolutionNote()\"\r\n (ngModelChange)=\"onResolutionNoteChange($event)\"></textarea>\r\n\r\n <div class=\"fly-moderation-queue__dialog-actions\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"closeResolve()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__submit-btn\"\r\n [disabled]=\"!resolutionStatus()\"\r\n (click)=\"submitResolve(r)\">\r\n {{ 'moderation.resolve_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_blue: var(--system-blue, #0a84ff);--_orange: var(--system-orange, #ff9f0a);--_green: var(--system-green, #32d74b);--_radius: 8px;--_blue-text: #1d4ed8;--_orange-text: #9a3412;--_green-text: #166534;--_danger-text: #b91c1c;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-moderation-queue__body{display:flex;flex-direction:column;gap:16px}.fly-moderation-queue__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-moderation-queue__count{font-weight:700;font-size:14px}.fly-moderation-queue__status-filter{display:inline-flex;flex-wrap:wrap;border:1px solid var(--_border);border-radius:var(--_radius);overflow:hidden}.fly-moderation-queue__status-btn{padding:5px 12px;border:none;border-inline-end:1px solid var(--_border);background:transparent;color:var(--_text-subtle);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__status-btn:last-child{border-inline-end:none}.fly-moderation-queue__status-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__status-btn--active{background:var(--_accent);color:#fff}.fly-moderation-queue__empty,.fly-moderation-queue__status-msg{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-moderation-queue__error{margin:0;padding:8px 12px;border-radius:var(--_radius);background:color-mix(in srgb,var(--_danger) 12%,transparent);color:var(--_danger);font-size:12px}.fly-moderation-queue__sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.fly-moderation-queue__table-wrap{overflow-x:auto}.fly-moderation-queue__table{inline-size:100%;border-collapse:collapse;font-size:12px}.fly-moderation-queue__table th{text-align:start;padding:6px 10px;color:var(--_text-subtle);font-weight:600;border-block-end:1px solid var(--_border);white-space:nowrap}.fly-moderation-queue__table td{padding:8px 10px;border-block-end:1px solid var(--_border);vertical-align:top}.fly-moderation-queue__reason-cell{display:table-cell}.fly-moderation-queue__reason{display:block;font-weight:600}.fly-moderation-queue__detail{display:block;margin-block-start:2px;color:var(--_text-tertiary);font-size:11px;max-inline-size:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__excerpt{max-inline-size:260px;color:var(--_text-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__subject-link{border:none;background:transparent;padding:0;color:var(--_accent);font:inherit;font-weight:600;cursor:pointer;text-align:start}.fly-moderation-queue__subject-link:hover{text-decoration:underline}.fly-moderation-queue__badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:700;white-space:nowrap}.fly-moderation-queue__badge--Open{background:color-mix(in srgb,var(--_blue) 12%,#fff);color:var(--_blue-text)}.fly-moderation-queue__badge--UnderReview{background:color-mix(in srgb,var(--_orange) 12%,#fff);color:var(--_orange-text)}.fly-moderation-queue__badge--ActionTaken{background:color-mix(in srgb,var(--_danger) 12%,#fff);color:var(--_danger-text)}.fly-moderation-queue__badge--Dismissed{background:color-mix(in srgb,var(--_green) 12%,#fff);color:var(--_green-text)}.fly-moderation-queue__resolution-note{margin-block-start:4px;color:var(--_text-tertiary);font-size:11px;max-inline-size:200px}.fly-moderation-queue__actions{display:flex;flex-direction:column;align-items:flex-start;gap:6px;white-space:nowrap}.fly-moderation-queue__action-btn{border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:4px 10px;color:var(--_text);font:inherit;font-size:11px;font-weight:600;cursor:pointer}.fly-moderation-queue__action-btn:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__action-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__lock-confirm{display:inline-flex;align-items:center;gap:6px}.fly-moderation-queue__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__load-more:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__load-more:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-moderation-queue__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-moderation-queue__dialog{position:relative;z-index:1;inline-size:min(440px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-moderation-queue__dialog:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}.fly-moderation-queue__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-moderation-queue__resolution-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-moderation-queue__resolution-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-moderation-queue__resolution-option,.fly-moderation-queue__checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-moderation-queue__resolution-option input,.fly-moderation-queue__checkbox-row input{accent-color:var(--_accent);cursor:pointer}.fly-moderation-queue__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-moderation-queue__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-moderation-queue__dialog-actions{display:flex;justify-content:flex-end;gap:8px}.fly-moderation-queue__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__submit-btn:hover:not(:disabled){opacity:.9}.fly-moderation-queue__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-moderation-queue__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-moderation-queue__cancel-btn:hover{background:var(--_surface-hover)}.fly-moderation-queue__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__danger-btn:hover{opacity:.9}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16128
16570
  }
16129
16571
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyModerationQueueComponent, decorators: [{
16130
16572
  type: Component,
16131
- args: [{ selector: 'fly-moderation-queue', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-moderation-queue' }, template: "<div class=\"fly-moderation-queue__body\">\r\n <!-- \u2500\u2500 Header: count + status filter \u2500\u2500 -->\r\n <header class=\"fly-moderation-queue__header\">\r\n <span class=\"fly-moderation-queue__count\">\r\n {{ 'moderation.count' | translate: { n: page().total } }}\r\n </span>\r\n\r\n <div class=\"fly-moderation-queue__status-filter\" role=\"group\" [attr.aria-label]=\"'moderation.status_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === null\"\r\n [attr.aria-pressed]=\"status() === null\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(null)\">\r\n {{ 'moderation.status_all' | translate }}\r\n </button>\r\n @for (s of statuses; track s) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === s\"\r\n [attr.aria-pressed]=\"status() === s\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(s)\">\r\n {{ 'moderation.status.' + s | translate }}\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n <!-- \u2500\u2500 States \u2500\u2500 -->\r\n @if (error()) {\r\n <p class=\"fly-moderation-queue__error\" role=\"alert\">{{ error() }}</p>\r\n } @else if (loading() && page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__status-msg\">{{ 'moderation.loading' | translate }}</p>\r\n } @else if (page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__empty\">{{ 'moderation.empty' | translate }}</p>\r\n } @else {\r\n <!-- \u2500\u2500 Report table \u2500\u2500 -->\r\n <div class=\"fly-moderation-queue__table-wrap\">\r\n <table class=\"fly-moderation-queue__table\">\r\n <caption class=\"fly-moderation-queue__sr-only\">{{ 'moderation.table_caption' | translate }}</caption>\r\n <thead>\r\n <tr>\r\n <th scope=\"col\">{{ 'moderation.col_reason' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reporter' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reported' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_excerpt' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_subject' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_status' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_actions' | translate }}</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (r of page().items; track r.id) {\r\n <tr>\r\n <td class=\"fly-moderation-queue__reason-cell\">\r\n <span class=\"fly-moderation-queue__reason\">{{ 'comment.reason.' + r.reason | translate }}</span>\r\n @if (r.detail) {\r\n <span class=\"fly-moderation-queue__detail\" [title]=\"r.detail\">{{ r.detail }}</span>\r\n }\r\n </td>\r\n <td>{{ r.reporterDisplayName || r.reporterUserId }}</td>\r\n <td>{{ formatDate(r.createdAt) }}</td>\r\n <td class=\"fly-moderation-queue__excerpt\">{{ r.commentExcerpt || '\u2014' }}</td>\r\n <td>\r\n <button type=\"button\" class=\"fly-moderation-queue__subject-link\" [title]=\"subjectHref(r)\" (click)=\"onOpenSubject(r)\">\r\n {{ subjectDisplay(r) }}\r\n </button>\r\n </td>\r\n <td>\r\n <span class=\"fly-moderation-queue__badge\" [class]=\"'fly-moderation-queue__badge--' + r.status\">\r\n {{ 'moderation.status.' + r.status | translate }}\r\n </span>\r\n @if (r.resolutionNote) {\r\n <div class=\"fly-moderation-queue__resolution-note\">{{ r.resolutionNote }}</div>\r\n }\r\n </td>\r\n <td class=\"fly-moderation-queue__actions\">\r\n @if (isResolvable(r.status)) {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"openResolve(r)\">\r\n {{ 'moderation.resolve' | translate }}\r\n </button>\r\n }\r\n @if (confirmingLockId() === r.id) {\r\n <span class=\"fly-moderation-queue__lock-confirm\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"cancelLock()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-moderation-queue__danger-btn\" (click)=\"confirmLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n </span>\r\n } @else {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"requestLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n @if (page().hasMore) {\r\n <button type=\"button\" class=\"fly-moderation-queue__load-more\" [disabled]=\"loading()\" (click)=\"loadMore()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Resolve dialog \u2500\u2500 -->\r\n@if (resolvingReport(); as r) {\r\n <div class=\"fly-moderation-queue__overlay\">\r\n <div class=\"fly-moderation-queue__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeResolve()\"\r\n (keydown.enter)=\"closeResolve()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div\r\n class=\"fly-moderation-queue__dialog\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'moderation.resolve_title' | translate\">\r\n <h3 class=\"fly-moderation-queue__dialog-title\">{{ 'moderation.resolve_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-moderation-queue__resolution-group\">\r\n <legend>{{ 'moderation.resolution_label' | translate }}</legend>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'ActionTaken'\"\r\n (change)=\"onResolutionStatusChange('ActionTaken')\" />\r\n <span>{{ 'moderation.resolution.ActionTaken' | translate }}</span>\r\n </label>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'Dismissed'\"\r\n (change)=\"onResolutionStatusChange('Dismissed')\" />\r\n <span>{{ 'moderation.resolution.Dismissed' | translate }}</span>\r\n </label>\r\n </fieldset>\r\n\r\n @if (resolutionStatus() === 'ActionTaken') {\r\n <label class=\"fly-moderation-queue__checkbox-row\">\r\n <input type=\"checkbox\" [checked]=\"deleteComment()\" (change)=\"onDeleteCommentToggle($event)\" />\r\n <span>{{ 'moderation.delete_comment_label' | translate }}</span>\r\n </label>\r\n }\r\n\r\n <textarea\r\n class=\"fly-moderation-queue__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'moderation.resolution_note_label' | translate\"\r\n [placeholder]=\"'moderation.resolution_note_label' | translate\"\r\n [ngModel]=\"resolutionNote()\"\r\n (ngModelChange)=\"onResolutionNoteChange($event)\"></textarea>\r\n\r\n <div class=\"fly-moderation-queue__dialog-actions\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"closeResolve()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__submit-btn\"\r\n [disabled]=\"!resolutionStatus()\"\r\n (click)=\"submitResolve(r)\">\r\n {{ 'moderation.resolve_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_blue: var(--system-blue, #0a84ff);--_orange: var(--system-orange, #ff9f0a);--_green: var(--system-green, #32d74b);--_radius: 8px;--_blue-text: #1d4ed8;--_orange-text: #9a3412;--_green-text: #166534;--_danger-text: #b91c1c;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-moderation-queue__body{display:flex;flex-direction:column;gap:16px}.fly-moderation-queue__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-moderation-queue__count{font-weight:700;font-size:14px}.fly-moderation-queue__status-filter{display:inline-flex;flex-wrap:wrap;border:1px solid var(--_border);border-radius:var(--_radius);overflow:hidden}.fly-moderation-queue__status-btn{padding:5px 12px;border:none;border-inline-end:1px solid var(--_border);background:transparent;color:var(--_text-subtle);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__status-btn:last-child{border-inline-end:none}.fly-moderation-queue__status-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__status-btn--active{background:var(--_accent);color:#fff}.fly-moderation-queue__empty,.fly-moderation-queue__status-msg{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-moderation-queue__error{margin:0;padding:8px 12px;border-radius:var(--_radius);background:color-mix(in srgb,var(--_danger) 12%,transparent);color:var(--_danger);font-size:12px}.fly-moderation-queue__sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.fly-moderation-queue__table-wrap{overflow-x:auto}.fly-moderation-queue__table{inline-size:100%;border-collapse:collapse;font-size:12px}.fly-moderation-queue__table th{text-align:start;padding:6px 10px;color:var(--_text-subtle);font-weight:600;border-block-end:1px solid var(--_border);white-space:nowrap}.fly-moderation-queue__table td{padding:8px 10px;border-block-end:1px solid var(--_border);vertical-align:top}.fly-moderation-queue__reason-cell{display:table-cell}.fly-moderation-queue__reason{display:block;font-weight:600}.fly-moderation-queue__detail{display:block;margin-block-start:2px;color:var(--_text-tertiary);font-size:11px;max-inline-size:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__excerpt{max-inline-size:260px;color:var(--_text-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__subject-link{border:none;background:transparent;padding:0;color:var(--_accent);font:inherit;font-weight:600;cursor:pointer;text-align:start}.fly-moderation-queue__subject-link:hover{text-decoration:underline}.fly-moderation-queue__badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:700;white-space:nowrap}.fly-moderation-queue__badge--Open{background:color-mix(in srgb,var(--_blue) 12%,#fff);color:var(--_blue-text)}.fly-moderation-queue__badge--UnderReview{background:color-mix(in srgb,var(--_orange) 12%,#fff);color:var(--_orange-text)}.fly-moderation-queue__badge--ActionTaken{background:color-mix(in srgb,var(--_danger) 12%,#fff);color:var(--_danger-text)}.fly-moderation-queue__badge--Dismissed{background:color-mix(in srgb,var(--_green) 12%,#fff);color:var(--_green-text)}.fly-moderation-queue__resolution-note{margin-block-start:4px;color:var(--_text-tertiary);font-size:11px;max-inline-size:200px}.fly-moderation-queue__actions{display:flex;flex-direction:column;align-items:flex-start;gap:6px;white-space:nowrap}.fly-moderation-queue__action-btn{border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:4px 10px;color:var(--_text);font:inherit;font-size:11px;font-weight:600;cursor:pointer}.fly-moderation-queue__action-btn:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__action-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__lock-confirm{display:inline-flex;align-items:center;gap:6px}.fly-moderation-queue__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__load-more:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__load-more:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-moderation-queue__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-moderation-queue__dialog{position:relative;z-index:1;inline-size:min(440px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-moderation-queue__dialog:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}.fly-moderation-queue__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-moderation-queue__resolution-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-moderation-queue__resolution-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-moderation-queue__resolution-option,.fly-moderation-queue__checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-moderation-queue__resolution-option input,.fly-moderation-queue__checkbox-row input{accent-color:var(--_accent);cursor:pointer}.fly-moderation-queue__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-moderation-queue__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-moderation-queue__dialog-actions{display:flex;justify-content:flex-end;gap:8px}.fly-moderation-queue__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__submit-btn:hover:not(:disabled){opacity:.9}.fly-moderation-queue__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-moderation-queue__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-moderation-queue__cancel-btn:hover{background:var(--_surface-hover)}.fly-moderation-queue__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__danger-btn:hover{opacity:.9}\n"] }]
16132
- }], propDecorators: { page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], loadPage: [{ type: i0.Output, args: ["loadPage"] }], resolve: [{ type: i0.Output, args: ["resolve"] }], lockThread: [{ type: i0.Output, args: ["lockThread"] }], openSubject: [{ type: i0.Output, args: ["openSubject"] }], onEscape: [{
16573
+ args: [{ selector: 'fly-moderation-queue', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-moderation-queue' }, template: "<div class=\"fly-moderation-queue__body\">\r\n <!-- \u2500\u2500 Header: count + status filter \u2500\u2500 -->\r\n <header class=\"fly-moderation-queue__header\">\r\n <span class=\"fly-moderation-queue__count\">\r\n {{ 'moderation.count' | translate: { n: page().total } }}\r\n </span>\r\n\r\n <div class=\"fly-moderation-queue__status-filter\" role=\"group\" [attr.aria-label]=\"'moderation.status_label' | translate\">\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === null\"\r\n [attr.aria-pressed]=\"status() === null\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(null)\">\r\n {{ 'moderation.status_all' | translate }}\r\n </button>\r\n @for (s of statuses; track s) {\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__status-btn\"\r\n [class.fly-moderation-queue__status-btn--active]=\"status() === s\"\r\n [attr.aria-pressed]=\"status() === s\"\r\n [disabled]=\"loading()\"\r\n (click)=\"onStatusPick(s)\">\r\n {{ 'moderation.status.' + s | translate }}\r\n </button>\r\n }\r\n </div>\r\n </header>\r\n\r\n <!-- \u2500\u2500 States \u2500\u2500 -->\r\n @if (error()) {\r\n <p class=\"fly-moderation-queue__error\" role=\"alert\">{{ error() }}</p>\r\n } @else if (loading() && page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__status-msg\">{{ 'moderation.loading' | translate }}</p>\r\n } @else if (page().items.length === 0) {\r\n <p class=\"fly-moderation-queue__empty\">{{ 'moderation.empty' | translate }}</p>\r\n } @else {\r\n <!-- \u2500\u2500 Report table \u2500\u2500 -->\r\n <div class=\"fly-moderation-queue__table-wrap\">\r\n <table class=\"fly-moderation-queue__table\">\r\n <caption class=\"fly-moderation-queue__sr-only\">{{ 'moderation.table_caption' | translate }}</caption>\r\n <thead>\r\n <tr>\r\n <th scope=\"col\">{{ 'moderation.col_reason' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reporter' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_reported' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_excerpt' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_subject' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_status' | translate }}</th>\r\n <th scope=\"col\">{{ 'moderation.col_actions' | translate }}</th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n @for (r of page().items; track r.id) {\r\n <tr>\r\n <td class=\"fly-moderation-queue__reason-cell\">\r\n <span class=\"fly-moderation-queue__reason\">{{ 'comment.reason.' + r.reason | translate }}</span>\r\n @if (r.detail) {\r\n <span class=\"fly-moderation-queue__detail\" [title]=\"r.detail\">{{ r.detail }}</span>\r\n }\r\n </td>\r\n <td>{{ reporterLabel(r) }}</td>\r\n <td>{{ formatDate(r.createdAt) }}</td>\r\n <td class=\"fly-moderation-queue__excerpt\">{{ r.commentExcerpt || '\u2014' }}</td>\r\n <td>\r\n <button type=\"button\" class=\"fly-moderation-queue__subject-link\" [title]=\"subjectHref(r)\" (click)=\"onOpenSubject(r)\">\r\n {{ subjectDisplay(r) }}\r\n </button>\r\n </td>\r\n <td>\r\n <span class=\"fly-moderation-queue__badge\" [class]=\"'fly-moderation-queue__badge--' + r.status\">\r\n {{ 'moderation.status.' + r.status | translate }}\r\n </span>\r\n @if (r.resolutionNote) {\r\n <div class=\"fly-moderation-queue__resolution-note\">{{ r.resolutionNote }}</div>\r\n }\r\n </td>\r\n <td class=\"fly-moderation-queue__actions\">\r\n @if (isResolvable(r.status)) {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"openResolve(r)\">\r\n {{ 'moderation.resolve' | translate }}\r\n </button>\r\n }\r\n @if (confirmingLockId() === r.id) {\r\n <span class=\"fly-moderation-queue__lock-confirm\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"cancelLock()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button type=\"button\" class=\"fly-moderation-queue__danger-btn\" (click)=\"confirmLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n </span>\r\n } @else {\r\n <button type=\"button\" class=\"fly-moderation-queue__action-btn\" [disabled]=\"loading()\" (click)=\"requestLock(r)\">\r\n {{ 'moderation.lock_thread' | translate }}\r\n </button>\r\n }\r\n </td>\r\n </tr>\r\n }\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n @if (page().hasMore) {\r\n <button type=\"button\" class=\"fly-moderation-queue__load-more\" [disabled]=\"loading()\" (click)=\"loadMore()\">\r\n {{ 'comment.load_more' | translate }}\r\n </button>\r\n }\r\n }\r\n</div>\r\n\r\n<!-- \u2500\u2500 Resolve dialog \u2500\u2500 -->\r\n@if (resolvingReport(); as r) {\r\n <div class=\"fly-moderation-queue__overlay\">\r\n <div class=\"fly-moderation-queue__overlay-backdrop\" role=\"button\" tabindex=\"0\"\r\n [attr.aria-label]=\"'common.action.cancel' | translate\"\r\n (click)=\"closeResolve()\"\r\n (keydown.enter)=\"closeResolve()\"\r\n (keydown.space)=\"onOverlayBackdropSpace($event)\"></div>\r\n <div\r\n class=\"fly-moderation-queue__dialog\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'moderation.resolve_title' | translate\">\r\n <h3 class=\"fly-moderation-queue__dialog-title\">{{ 'moderation.resolve_title' | translate }}</h3>\r\n\r\n <fieldset class=\"fly-moderation-queue__resolution-group\">\r\n <legend>{{ 'moderation.resolution_label' | translate }}</legend>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'ActionTaken'\"\r\n (change)=\"onResolutionStatusChange('ActionTaken')\" />\r\n <span>{{ 'moderation.resolution.ActionTaken' | translate }}</span>\r\n </label>\r\n <label class=\"fly-moderation-queue__resolution-option\">\r\n <input\r\n type=\"radio\"\r\n [name]=\"resolutionGroupName\"\r\n [checked]=\"resolutionStatus() === 'Dismissed'\"\r\n (change)=\"onResolutionStatusChange('Dismissed')\" />\r\n <span>{{ 'moderation.resolution.Dismissed' | translate }}</span>\r\n </label>\r\n </fieldset>\r\n\r\n @if (resolutionStatus() === 'ActionTaken') {\r\n <label class=\"fly-moderation-queue__checkbox-row\">\r\n <input type=\"checkbox\" [checked]=\"deleteComment()\" (change)=\"onDeleteCommentToggle($event)\" />\r\n <span>{{ 'moderation.delete_comment_label' | translate }}</span>\r\n </label>\r\n }\r\n\r\n <textarea\r\n class=\"fly-moderation-queue__textarea\"\r\n rows=\"3\"\r\n [attr.aria-label]=\"'moderation.resolution_note_label' | translate\"\r\n [placeholder]=\"'moderation.resolution_note_label' | translate\"\r\n [ngModel]=\"resolutionNote()\"\r\n (ngModelChange)=\"onResolutionNoteChange($event)\"></textarea>\r\n\r\n <div class=\"fly-moderation-queue__dialog-actions\">\r\n <button type=\"button\" class=\"fly-moderation-queue__cancel-btn\" (click)=\"closeResolve()\">\r\n {{ 'common.action.cancel' | translate }}\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"fly-moderation-queue__submit-btn\"\r\n [disabled]=\"!resolutionStatus()\"\r\n (click)=\"submitResolve(r)\">\r\n {{ 'moderation.resolve_submit' | translate }}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host,:host *,:host *:before,:host *:after{box-sizing:border-box}:host{--_surface: var(--surface-card, #fff);--_surface-hover: var(--surface-hover, #f1f5f9);--_border: var(--separator, var(--surface-border, #e2e8f0));--_text: var(--label-primary, var(--text-color, #0f172a));--_text-subtle: var(--label-secondary, var(--text-color-secondary, #64748b));--_text-tertiary: var(--label-tertiary, #94a3b8);--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_blue: var(--system-blue, #0a84ff);--_orange: var(--system-orange, #ff9f0a);--_green: var(--system-green, #32d74b);--_radius: 8px;--_blue-text: #1d4ed8;--_orange-text: #9a3412;--_green-text: #166534;--_danger-text: #b91c1c;display:block;inline-size:100%;color:var(--_text);font-size:13px}.fly-moderation-queue__body{display:flex;flex-direction:column;gap:16px}.fly-moderation-queue__header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.fly-moderation-queue__count{font-weight:700;font-size:14px}.fly-moderation-queue__status-filter{display:inline-flex;flex-wrap:wrap;border:1px solid var(--_border);border-radius:var(--_radius);overflow:hidden}.fly-moderation-queue__status-btn{padding:5px 12px;border:none;border-inline-end:1px solid var(--_border);background:transparent;color:var(--_text-subtle);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__status-btn:last-child{border-inline-end:none}.fly-moderation-queue__status-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__status-btn--active{background:var(--_accent);color:#fff}.fly-moderation-queue__empty,.fly-moderation-queue__status-msg{margin:0;color:var(--_text-subtle);text-align:center;padding:16px 0}.fly-moderation-queue__error{margin:0;padding:8px 12px;border-radius:var(--_radius);background:color-mix(in srgb,var(--_danger) 12%,transparent);color:var(--_danger);font-size:12px}.fly-moderation-queue__sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.fly-moderation-queue__table-wrap{overflow-x:auto}.fly-moderation-queue__table{inline-size:100%;border-collapse:collapse;font-size:12px}.fly-moderation-queue__table th{text-align:start;padding:6px 10px;color:var(--_text-subtle);font-weight:600;border-block-end:1px solid var(--_border);white-space:nowrap}.fly-moderation-queue__table td{padding:8px 10px;border-block-end:1px solid var(--_border);vertical-align:top}.fly-moderation-queue__reason-cell{display:table-cell}.fly-moderation-queue__reason{display:block;font-weight:600}.fly-moderation-queue__detail{display:block;margin-block-start:2px;color:var(--_text-tertiary);font-size:11px;max-inline-size:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__excerpt{max-inline-size:260px;color:var(--_text-subtle);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-moderation-queue__subject-link{border:none;background:transparent;padding:0;color:var(--_accent);font:inherit;font-weight:600;cursor:pointer;text-align:start}.fly-moderation-queue__subject-link:hover{text-decoration:underline}.fly-moderation-queue__badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:700;white-space:nowrap}.fly-moderation-queue__badge--Open{background:color-mix(in srgb,var(--_blue) 12%,#fff);color:var(--_blue-text)}.fly-moderation-queue__badge--UnderReview{background:color-mix(in srgb,var(--_orange) 12%,#fff);color:var(--_orange-text)}.fly-moderation-queue__badge--ActionTaken{background:color-mix(in srgb,var(--_danger) 12%,#fff);color:var(--_danger-text)}.fly-moderation-queue__badge--Dismissed{background:color-mix(in srgb,var(--_green) 12%,#fff);color:var(--_green-text)}.fly-moderation-queue__resolution-note{margin-block-start:4px;color:var(--_text-tertiary);font-size:11px;max-inline-size:200px}.fly-moderation-queue__actions{display:flex;flex-direction:column;align-items:flex-start;gap:6px;white-space:nowrap}.fly-moderation-queue__action-btn{border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:4px 10px;color:var(--_text);font:inherit;font-size:11px;font-weight:600;cursor:pointer}.fly-moderation-queue__action-btn:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__action-btn:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__lock-confirm{display:inline-flex;align-items:center;gap:6px}.fly-moderation-queue__load-more{align-self:flex-start;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);padding:6px 14px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__load-more:hover:not(:disabled){background:var(--_surface-hover)}.fly-moderation-queue__load-more:disabled{opacity:.6;cursor:not-allowed}.fly-moderation-queue__overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:1000}.fly-moderation-queue__overlay-backdrop{position:absolute;inset:0;background:#00000073;border:none;padding:0;margin:0;cursor:pointer}.fly-moderation-queue__dialog{position:relative;z-index:1;inline-size:min(440px,90vw);max-block-size:85vh;overflow-y:auto;background:var(--_surface);border-radius:12px;padding:20px;display:flex;flex-direction:column;gap:14px;box-shadow:0 12px 32px #00000040}.fly-moderation-queue__dialog:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}.fly-moderation-queue__dialog-title{margin:0;font-size:16px;font-weight:700}.fly-moderation-queue__resolution-group{display:flex;flex-direction:column;gap:8px;margin:0;padding:0;border:none}.fly-moderation-queue__resolution-group legend{font-weight:600;padding:0;margin-block-end:4px}.fly-moderation-queue__resolution-option,.fly-moderation-queue__checkbox-row{display:flex;align-items:center;gap:8px;cursor:pointer}.fly-moderation-queue__resolution-option input,.fly-moderation-queue__checkbox-row input{accent-color:var(--_accent);cursor:pointer}.fly-moderation-queue__textarea{inline-size:100%;padding:8px 12px;border:1px solid var(--_border);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);font:inherit;line-height:1.5;resize:vertical}.fly-moderation-queue__textarea:focus-visible{outline:2px solid var(--_accent);outline-offset:1px}.fly-moderation-queue__dialog-actions{display:flex;justify-content:flex-end;gap:8px}.fly-moderation-queue__submit-btn{padding:6px 16px;border:none;border-radius:var(--_radius);background:var(--_accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__submit-btn:hover:not(:disabled){opacity:.9}.fly-moderation-queue__submit-btn:disabled{opacity:.5;cursor:not-allowed}.fly-moderation-queue__cancel-btn{padding:6px 14px;border:1px solid var(--_border);border-radius:var(--_radius);background:transparent;color:var(--_text-subtle);font:inherit;cursor:pointer}.fly-moderation-queue__cancel-btn:hover{background:var(--_surface-hover)}.fly-moderation-queue__danger-btn{padding:6px 14px;border:none;border-radius:var(--_radius);background:var(--_danger);color:#fff;font:inherit;font-weight:600;cursor:pointer}.fly-moderation-queue__danger-btn:hover{opacity:.9}\n"] }]
16574
+ }], propDecorators: { page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], resolveDisplayName: [{ type: i0.Input, args: [{ isSignal: true, alias: "resolveDisplayName", required: false }] }], loadPage: [{ type: i0.Output, args: ["loadPage"] }], resolve: [{ type: i0.Output, args: ["resolve"] }], lockThread: [{ type: i0.Output, args: ["lockThread"] }], openSubject: [{ type: i0.Output, args: ["openSubject"] }], onEscape: [{
16133
16575
  type: HostListener,
16134
16576
  args: ['document:keydown.escape']
16135
16577
  }] } });
@@ -19164,11 +19606,11 @@ class FlyAppHomeComponent {
19164
19606
  return `fly-app-home-sec-${index}`;
19165
19607
  }
19166
19608
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyAppHomeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19167
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyAppHomeComponent, isStandalone: true, selector: "fly-app-home", inputs: { brandLabelKey: { classPropertyName: "brandLabelKey", publicName: "brandLabelKey", isSignal: true, isRequired: false, transformFunction: null }, titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, subtitleKey: { classPropertyName: "subtitleKey", publicName: "subtitleKey", isSignal: true, isRequired: false, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, modulesLabelKey: { classPropertyName: "modulesLabelKey", publicName: "modulesLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { moduleSelected: "moduleSelected", brandSelected: "brandSelected" }, queries: [{ propertyName: "icons", predicate: FlyModuleIconDirective, isSignal: true }], ngImport: i0, template: "<div class=\"ah\">\n <header class=\"ah__util\">\n <button type=\"button\" class=\"ah__brand\" (click)=\"brandSelected.emit()\">\n <span class=\"ah__brand-mark\"><ng-content select=\"[app-home-brand]\" /></span>\n @if (brandLabelKey(); as key) {\n <span class=\"ah__brand-name\">{{ key | translate }}</span>\n }\n </button>\n <ng-content select=\"[app-home-actions]\" />\n </header>\n\n <main class=\"ah__main\">\n @if (titleKey() || subtitleKey()) {\n <section class=\"ah__hero\">\n @if (titleKey(); as key) {\n <h1 class=\"ah__title\">{{ key | translate }}</h1>\n }\n @if (subtitleKey(); as key) {\n <p class=\"ah__sub\">{{ key | translate }}</p>\n }\n </section>\n }\n\n @for (row of rows(); track row.index) {\n <section\n class=\"ah__sec\"\n [class.ah__sec--secondary]=\"row.index > 0\"\n [attr.aria-label]=\"row.section.titleKey ? null : (modulesLabelKey() | translate)\"\n >\n @if (row.section.titleKey; as titleKey) {\n @if (row.section.collapsible) {\n <button\n type=\"button\"\n class=\"ah__sec-hd\"\n (click)=\"toggle(row.index)\"\n [attr.aria-expanded]=\"row.expanded\"\n [attr.aria-controls]=\"sectionId(row.index)\"\n >\n <!-- Points end-ward when shut, down when open. `--open` wins over the RTL\n flip below because a downward chevron has no direction to mirror. -->\n <svg\n class=\"ah__sec-chev\"\n [class.ah__sec-chev--open]=\"row.expanded\"\n width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"\n >\n <polyline points=\"9 6 15 12 9 18\" />\n </svg>\n <span class=\"ah__sec-title\">{{ titleKey | translate }}</span>\n <span class=\"ah__sec-count\">{{ row.section.modules.length }}</span>\n @if (row.hintKey; as hint) {\n <span class=\"ah__sec-hint\">{{ hint | translate }}</span>\n }\n </button>\n } @else {\n <h2 class=\"ah__sec-title ah__sec-title--static\">{{ titleKey | translate }}</h2>\n }\n }\n\n @if (row.expanded) {\n <div class=\"ah__grid\" [class.ah__grid--compact]=\"row.layout === 'compact'\" [id]=\"sectionId(row.index)\">\n @for (mod of row.section.modules; track mod.key) {\n <button\n type=\"button\"\n class=\"ah__card\"\n [class.ah__card--compact]=\"row.layout === 'compact'\"\n [disabled]=\"mod.disabled\"\n (click)=\"select(mod)\"\n >\n <span class=\"ah__card-ico\">\n @if (iconFor(mod.key); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else if (mod.iconClass) {\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\n }\n </span>\n\n @if (row.layout === 'compact') {\n <span class=\"ah__card-main\">\n <span class=\"ah__card-title\">{{ mod.labelKey | translate }}</span>\n @if (mod.descKey; as descKey) {\n <span class=\"ah__card-desc\">{{ descKey | translate }}</span>\n }\n </span>\n <svg\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\n stroke-linejoin=\"round\" aria-hidden=\"true\"\n >\n <polyline points=\"9 6 15 12 9 18\" />\n </svg>\n } @else {\n <span class=\"ah__card-title ah__card-title--feature\">{{ mod.labelKey | translate }}</span>\n @if (mod.descKey; as descKey) {\n <span class=\"ah__card-desc ah__card-desc--feature\">{{ descKey | translate }}</span>\n }\n <span class=\"ah__card-open\">\n {{ 'ui.action.open' | translate }}\n <svg\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\n stroke-linejoin=\"round\" aria-hidden=\"true\"\n >\n <polyline points=\"9 6 15 12 9 18\" />\n </svg>\n </span>\n }\n </button>\n }\n </div>\n }\n </section>\n }\n </main>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-height:100%;container-type:inline-size}.ah{display:flex;flex-direction:column;flex:1;min-height:100%}.ah__util{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-4) var(--sp-6)}.ah__brand{display:inline-flex;align-items:center;gap:var(--sp-2);padding:0;border:0;background:none;color:var(--ink);font-family:inherit;cursor:pointer}.ah__brand:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__brand-name{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__main{flex:1;inline-size:100%;max-inline-size:1120px;margin-inline:auto;padding:56px 40px;display:flex;flex-direction:column;justify-content:center;gap:44px}.ah__hero{max-inline-size:640px}.ah__title{margin:0 0 var(--sp-3);font-size:44px;font-weight:var(--fw-semibold);letter-spacing:-.03em;text-wrap:balance;background:linear-gradient(180deg,var(--ink),var(--ink-2));-webkit-background-clip:text;background-clip:text;color:transparent}.ah__sub{margin:0;max-inline-size:560px;font-size:var(--text-lg);line-height:1.55;color:var(--ink-3);text-wrap:pretty}.ah__sec{display:flex;flex-direction:column;gap:var(--sp-4)}.ah__sec--secondary{padding-block-start:28px;border-block-start:1px solid var(--line)}.ah__sec-hd{display:flex;align-items:center;gap:var(--sp-3);inline-size:100%;padding:0;background:none;border:0;cursor:pointer;font-family:inherit;color:var(--ink);text-align:start}.ah__sec-hd:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__sec-chev{flex-shrink:0;color:var(--ink-3);transition:transform var(--t-overlay)}.ah__sec-title{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__sec-title--static{margin:0;color:var(--ink-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide);text-transform:uppercase}.ah__sec-count{display:inline-grid;place-items:center;min-inline-size:22px;block-size:20px;padding-inline:6px;border-radius:999px;background:var(--bg-3);border:1px solid var(--line);font-size:var(--text-2xs);font-weight:var(--fw-medium);color:var(--ink-3)}.ah__sec-hint{margin-inline-start:auto;font-size:var(--text-xs);color:var(--ink-3)}.ah__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3)}.ah__card{display:flex;flex-direction:column;align-items:stretch;position:relative;overflow:hidden;text-align:start;padding:22px 22px 18px;background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);color:inherit;font-family:inherit;cursor:pointer;transition:transform var(--t-overlay),box-shadow var(--t-overlay),border-color var(--t-overlay)}.ah__card:before{content:\"\";position:absolute;inset:0;pointer-events:none;border-radius:inherit;padding:1px;background:var(--grad);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity var(--t-overlay)}.ah__card:hover:not(:disabled){transform:translateY(-2px);border-color:transparent;box-shadow:var(--shadow-filter)}.ah__card:hover:not(:disabled):before{opacity:1}.ah__card:focus-visible{outline:0;border-color:transparent;box-shadow:0 0 0 3px var(--accent-soft),var(--shadow-filter)}.ah__card:focus-visible:before{opacity:1}.ah__card:disabled{cursor:default;opacity:.5}.ah__card-ico{display:grid;place-items:center;inline-size:40px;block-size:40px;margin-block-end:18px;border-radius:11px;background:var(--bg-3);border:0;color:var(--ink);font-size:var(--icon-lg);transition:background var(--t-overlay),color var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-ico,.ah__card:focus-visible .ah__card-ico{background:var(--grad);color:var(--ink-inverse)}.ah__card-title{font-size:var(--text-sm);font-weight:var(--fw-semibold);letter-spacing:-.01em;color:var(--ink)}.ah__card-title--feature{margin-block-end:4px;font-size:18px;letter-spacing:-.015em}.ah__card-desc{font-size:var(--text-xs);line-height:1.45;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ah__card-desc--feature{margin-block-end:18px;font-size:var(--text-sm);line-height:1.5;text-wrap:pretty;overflow:visible;white-space:normal}.ah__card-open{display:inline-flex;align-items:center;gap:4px;margin-block-start:auto;font-size:var(--text-2xs);color:var(--ink-3);transition:color var(--t-overlay),gap var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-open,.ah__card:focus-visible .ah__card-open{color:var(--accent);gap:8px}.ah__card-arrow{flex-shrink:0;transition:color var(--t-overlay),transform var(--t-overlay)}.ah__card--compact{flex-direction:row;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4)}.ah__card--compact .ah__card-ico{inline-size:34px;block-size:34px;margin-block-end:0;flex-shrink:0;border-radius:9px;font-size:var(--icon-md)}.ah__card-main{display:flex;flex-direction:column;gap:2px;flex:1;min-inline-size:0}.ah__card--compact .ah__card-arrow{color:var(--ink-4)}.ah__card--compact:hover:not(:disabled) .ah__card-arrow{color:var(--accent);transform:translate(2px)}:host-context([dir=rtl]) .ah__card-arrow{transform:scaleX(-1)}:host-context([dir=rtl]) .ah__sec-chev:not(.ah__sec-chev--open){transform:scaleX(-1)}:host-context([dir=rtl]) .ah__card--compact:hover:not(:disabled) .ah__card-arrow{transform:scaleX(-1) translate(2px)}.ah__sec-chev--open{transform:rotate(90deg)}@container (width <= 900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@media(width<=900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@container (width <= 560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}@media(width<=560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19609
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyAppHomeComponent, isStandalone: true, selector: "fly-app-home", inputs: { brandLabelKey: { classPropertyName: "brandLabelKey", publicName: "brandLabelKey", isSignal: true, isRequired: false, transformFunction: null }, titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, subtitleKey: { classPropertyName: "subtitleKey", publicName: "subtitleKey", isSignal: true, isRequired: false, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, modulesLabelKey: { classPropertyName: "modulesLabelKey", publicName: "modulesLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { moduleSelected: "moduleSelected", brandSelected: "brandSelected" }, queries: [{ propertyName: "icons", predicate: FlyModuleIconDirective, isSignal: true }], ngImport: i0, template: "<div class=\"ah\">\r\n <header class=\"ah__util\">\r\n <button type=\"button\" class=\"ah__brand\" (click)=\"brandSelected.emit()\">\r\n <span class=\"ah__brand-mark\"><ng-content select=\"[app-home-brand]\" /></span>\r\n @if (brandLabelKey(); as key) {\r\n <span class=\"ah__brand-name\">{{ key | translate }}</span>\r\n }\r\n </button>\r\n <ng-content select=\"[app-home-actions]\" />\r\n </header>\r\n\r\n <main class=\"ah__main\">\r\n @if (titleKey() || subtitleKey()) {\r\n <section class=\"ah__hero\">\r\n @if (titleKey(); as key) {\r\n <h1 class=\"ah__title\">{{ key | translate }}</h1>\r\n }\r\n @if (subtitleKey(); as key) {\r\n <p class=\"ah__sub\">{{ key | translate }}</p>\r\n }\r\n </section>\r\n }\r\n\r\n @for (row of rows(); track row.index) {\r\n <section\r\n class=\"ah__sec\"\r\n [class.ah__sec--secondary]=\"row.index > 0\"\r\n [attr.aria-label]=\"row.section.titleKey ? null : (modulesLabelKey() | translate)\"\r\n >\r\n @if (row.section.titleKey; as titleKey) {\r\n @if (row.section.collapsible) {\r\n <button\r\n type=\"button\"\r\n class=\"ah__sec-hd\"\r\n (click)=\"toggle(row.index)\"\r\n [attr.aria-expanded]=\"row.expanded\"\r\n [attr.aria-controls]=\"sectionId(row.index)\"\r\n >\r\n <!-- Points end-ward when shut, down when open. `--open` wins over the RTL\r\n flip below because a downward chevron has no direction to mirror. -->\r\n <svg\r\n class=\"ah__sec-chev\"\r\n [class.ah__sec-chev--open]=\"row.expanded\"\r\n width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\r\n stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"\r\n >\r\n <polyline points=\"9 6 15 12 9 18\" />\r\n </svg>\r\n <span class=\"ah__sec-title\">{{ titleKey | translate }}</span>\r\n <span class=\"ah__sec-count\">{{ row.section.modules.length }}</span>\r\n @if (row.hintKey; as hint) {\r\n <span class=\"ah__sec-hint\">{{ hint | translate }}</span>\r\n }\r\n </button>\r\n } @else {\r\n <h2 class=\"ah__sec-title ah__sec-title--static\">{{ titleKey | translate }}</h2>\r\n }\r\n }\r\n\r\n @if (row.expanded) {\r\n <div class=\"ah__grid\" [class.ah__grid--compact]=\"row.layout === 'compact'\" [id]=\"sectionId(row.index)\">\r\n @for (mod of row.section.modules; track mod.key) {\r\n <button\r\n type=\"button\"\r\n class=\"ah__card\"\r\n [class.ah__card--compact]=\"row.layout === 'compact'\"\r\n [disabled]=\"mod.disabled\"\r\n (click)=\"select(mod)\"\r\n >\r\n <span class=\"ah__card-ico\">\r\n @if (iconFor(mod.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (mod.iconClass) {\r\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n @if (row.layout === 'compact') {\r\n <span class=\"ah__card-main\">\r\n <span class=\"ah__card-title\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"ah__card-desc\">{{ descKey | translate }}</span>\r\n }\r\n </span>\r\n <svg\r\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\" aria-hidden=\"true\"\r\n >\r\n <polyline points=\"9 6 15 12 9 18\" />\r\n </svg>\r\n } @else {\r\n <span class=\"ah__card-title ah__card-title--feature\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"ah__card-desc ah__card-desc--feature\">{{ descKey | translate }}</span>\r\n }\r\n <span class=\"ah__card-open\">\r\n {{ 'ui.action.open' | translate }}\r\n <svg\r\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\" aria-hidden=\"true\"\r\n >\r\n <polyline points=\"9 6 15 12 9 18\" />\r\n </svg>\r\n </span>\r\n }\r\n </button>\r\n }\r\n </div>\r\n }\r\n </section>\r\n }\r\n </main>\r\n</div>\r\n", styles: [":host{display:flex;flex-direction:column;min-height:100%;container-type:inline-size}.ah{display:flex;flex-direction:column;flex:1;min-height:100%}.ah__util{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-4) var(--sp-6)}.ah__brand{display:inline-flex;align-items:center;gap:var(--sp-2);padding:0;border:0;background:none;color:var(--ink);font-family:inherit;cursor:pointer}.ah__brand:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__brand-name{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__main{flex:1;inline-size:100%;max-inline-size:1120px;margin-inline:auto;padding:56px 40px;display:flex;flex-direction:column;justify-content:center;gap:44px}.ah__hero{max-inline-size:640px}.ah__title{margin:0 0 var(--sp-3);font-size:44px;font-weight:var(--fw-semibold);letter-spacing:-.03em;text-wrap:balance;background:linear-gradient(180deg,var(--ink),var(--ink-2));-webkit-background-clip:text;background-clip:text;color:transparent}.ah__sub{margin:0;max-inline-size:560px;font-size:var(--text-lg);line-height:1.55;color:var(--ink-3);text-wrap:pretty}.ah__sec{display:flex;flex-direction:column;gap:var(--sp-4)}.ah__sec--secondary{padding-block-start:28px;border-block-start:1px solid var(--line)}.ah__sec-hd{display:flex;align-items:center;gap:var(--sp-3);inline-size:100%;padding:0;background:none;border:0;cursor:pointer;font-family:inherit;color:var(--ink);text-align:start}.ah__sec-hd:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__sec-chev{flex-shrink:0;color:var(--ink-3);transition:transform var(--t-overlay)}.ah__sec-title{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__sec-title--static{margin:0;color:var(--ink-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide);text-transform:uppercase}.ah__sec-count{display:inline-grid;place-items:center;min-inline-size:22px;block-size:20px;padding-inline:6px;border-radius:999px;background:var(--bg-3);border:1px solid var(--line);font-size:var(--text-2xs);font-weight:var(--fw-medium);color:var(--ink-3)}.ah__sec-hint{margin-inline-start:auto;font-size:var(--text-xs);color:var(--ink-3)}.ah__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3)}.ah__card{display:flex;flex-direction:column;align-items:stretch;position:relative;overflow:hidden;text-align:start;padding:22px 22px 18px;background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);color:inherit;font-family:inherit;cursor:pointer;transition:transform var(--t-overlay),box-shadow var(--t-overlay),border-color var(--t-overlay)}.ah__card:before{content:\"\";position:absolute;inset:0;pointer-events:none;border-radius:inherit;padding:1px;background:var(--grad);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity var(--t-overlay)}.ah__card:hover:not(:disabled){transform:translateY(-2px);border-color:transparent;box-shadow:var(--shadow-filter)}.ah__card:hover:not(:disabled):before{opacity:1}.ah__card:focus-visible{outline:0;border-color:transparent;box-shadow:0 0 0 3px var(--accent-soft),var(--shadow-filter)}.ah__card:focus-visible:before{opacity:1}.ah__card:disabled{cursor:default;opacity:.5}.ah__card-ico{display:grid;place-items:center;inline-size:40px;block-size:40px;margin-block-end:18px;border-radius:11px;background:var(--bg-3);border:0;color:var(--ink);font-size:var(--icon-lg);transition:background var(--t-overlay),color var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-ico,.ah__card:focus-visible .ah__card-ico{background:var(--grad);color:var(--ink-inverse)}.ah__card-title{font-size:var(--text-sm);font-weight:var(--fw-semibold);letter-spacing:-.01em;color:var(--ink)}.ah__card-title--feature{margin-block-end:4px;font-size:18px;letter-spacing:-.015em}.ah__card-desc{font-size:var(--text-xs);line-height:1.45;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ah__card-desc--feature{margin-block-end:18px;font-size:var(--text-sm);line-height:1.5;text-wrap:pretty;overflow:visible;white-space:normal}.ah__card-open{display:inline-flex;align-items:center;gap:4px;margin-block-start:auto;font-size:var(--text-2xs);color:var(--ink-3);transition:color var(--t-overlay),gap var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-open,.ah__card:focus-visible .ah__card-open{color:var(--accent);gap:8px}.ah__card-arrow{flex-shrink:0;transition:color var(--t-overlay),transform var(--t-overlay)}.ah__card--compact{flex-direction:row;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4)}.ah__card--compact .ah__card-ico{inline-size:34px;block-size:34px;margin-block-end:0;flex-shrink:0;border-radius:9px;font-size:var(--icon-md)}.ah__card-main{display:flex;flex-direction:column;gap:2px;flex:1;min-inline-size:0}.ah__card--compact .ah__card-arrow{color:var(--ink-4)}.ah__card--compact:hover:not(:disabled) .ah__card-arrow{color:var(--accent);transform:translate(2px)}:host-context([dir=rtl]) .ah__card-arrow{transform:scaleX(-1)}:host-context([dir=rtl]) .ah__sec-chev:not(.ah__sec-chev--open){transform:scaleX(-1)}:host-context([dir=rtl]) .ah__card--compact:hover:not(:disabled) .ah__card-arrow{transform:scaleX(-1) translate(2px)}.ah__sec-chev--open{transform:rotate(90deg)}@container (width <= 900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@media(width<=900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@container (width <= 560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}@media(width<=560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19168
19610
  }
19169
19611
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyAppHomeComponent, decorators: [{
19170
19612
  type: Component,
19171
- args: [{ selector: 'fly-app-home', standalone: true, imports: [NgTemplateOutlet, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ah\">\n <header class=\"ah__util\">\n <button type=\"button\" class=\"ah__brand\" (click)=\"brandSelected.emit()\">\n <span class=\"ah__brand-mark\"><ng-content select=\"[app-home-brand]\" /></span>\n @if (brandLabelKey(); as key) {\n <span class=\"ah__brand-name\">{{ key | translate }}</span>\n }\n </button>\n <ng-content select=\"[app-home-actions]\" />\n </header>\n\n <main class=\"ah__main\">\n @if (titleKey() || subtitleKey()) {\n <section class=\"ah__hero\">\n @if (titleKey(); as key) {\n <h1 class=\"ah__title\">{{ key | translate }}</h1>\n }\n @if (subtitleKey(); as key) {\n <p class=\"ah__sub\">{{ key | translate }}</p>\n }\n </section>\n }\n\n @for (row of rows(); track row.index) {\n <section\n class=\"ah__sec\"\n [class.ah__sec--secondary]=\"row.index > 0\"\n [attr.aria-label]=\"row.section.titleKey ? null : (modulesLabelKey() | translate)\"\n >\n @if (row.section.titleKey; as titleKey) {\n @if (row.section.collapsible) {\n <button\n type=\"button\"\n class=\"ah__sec-hd\"\n (click)=\"toggle(row.index)\"\n [attr.aria-expanded]=\"row.expanded\"\n [attr.aria-controls]=\"sectionId(row.index)\"\n >\n <!-- Points end-ward when shut, down when open. `--open` wins over the RTL\n flip below because a downward chevron has no direction to mirror. -->\n <svg\n class=\"ah__sec-chev\"\n [class.ah__sec-chev--open]=\"row.expanded\"\n width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"\n >\n <polyline points=\"9 6 15 12 9 18\" />\n </svg>\n <span class=\"ah__sec-title\">{{ titleKey | translate }}</span>\n <span class=\"ah__sec-count\">{{ row.section.modules.length }}</span>\n @if (row.hintKey; as hint) {\n <span class=\"ah__sec-hint\">{{ hint | translate }}</span>\n }\n </button>\n } @else {\n <h2 class=\"ah__sec-title ah__sec-title--static\">{{ titleKey | translate }}</h2>\n }\n }\n\n @if (row.expanded) {\n <div class=\"ah__grid\" [class.ah__grid--compact]=\"row.layout === 'compact'\" [id]=\"sectionId(row.index)\">\n @for (mod of row.section.modules; track mod.key) {\n <button\n type=\"button\"\n class=\"ah__card\"\n [class.ah__card--compact]=\"row.layout === 'compact'\"\n [disabled]=\"mod.disabled\"\n (click)=\"select(mod)\"\n >\n <span class=\"ah__card-ico\">\n @if (iconFor(mod.key); as tpl) {\n <ng-container *ngTemplateOutlet=\"tpl\" />\n } @else if (mod.iconClass) {\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\n }\n </span>\n\n @if (row.layout === 'compact') {\n <span class=\"ah__card-main\">\n <span class=\"ah__card-title\">{{ mod.labelKey | translate }}</span>\n @if (mod.descKey; as descKey) {\n <span class=\"ah__card-desc\">{{ descKey | translate }}</span>\n }\n </span>\n <svg\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\n stroke-linejoin=\"round\" aria-hidden=\"true\"\n >\n <polyline points=\"9 6 15 12 9 18\" />\n </svg>\n } @else {\n <span class=\"ah__card-title ah__card-title--feature\">{{ mod.labelKey | translate }}</span>\n @if (mod.descKey; as descKey) {\n <span class=\"ah__card-desc ah__card-desc--feature\">{{ descKey | translate }}</span>\n }\n <span class=\"ah__card-open\">\n {{ 'ui.action.open' | translate }}\n <svg\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\n stroke-linejoin=\"round\" aria-hidden=\"true\"\n >\n <polyline points=\"9 6 15 12 9 18\" />\n </svg>\n </span>\n }\n </button>\n }\n </div>\n }\n </section>\n }\n </main>\n</div>\n", styles: [":host{display:flex;flex-direction:column;min-height:100%;container-type:inline-size}.ah{display:flex;flex-direction:column;flex:1;min-height:100%}.ah__util{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-4) var(--sp-6)}.ah__brand{display:inline-flex;align-items:center;gap:var(--sp-2);padding:0;border:0;background:none;color:var(--ink);font-family:inherit;cursor:pointer}.ah__brand:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__brand-name{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__main{flex:1;inline-size:100%;max-inline-size:1120px;margin-inline:auto;padding:56px 40px;display:flex;flex-direction:column;justify-content:center;gap:44px}.ah__hero{max-inline-size:640px}.ah__title{margin:0 0 var(--sp-3);font-size:44px;font-weight:var(--fw-semibold);letter-spacing:-.03em;text-wrap:balance;background:linear-gradient(180deg,var(--ink),var(--ink-2));-webkit-background-clip:text;background-clip:text;color:transparent}.ah__sub{margin:0;max-inline-size:560px;font-size:var(--text-lg);line-height:1.55;color:var(--ink-3);text-wrap:pretty}.ah__sec{display:flex;flex-direction:column;gap:var(--sp-4)}.ah__sec--secondary{padding-block-start:28px;border-block-start:1px solid var(--line)}.ah__sec-hd{display:flex;align-items:center;gap:var(--sp-3);inline-size:100%;padding:0;background:none;border:0;cursor:pointer;font-family:inherit;color:var(--ink);text-align:start}.ah__sec-hd:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__sec-chev{flex-shrink:0;color:var(--ink-3);transition:transform var(--t-overlay)}.ah__sec-title{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__sec-title--static{margin:0;color:var(--ink-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide);text-transform:uppercase}.ah__sec-count{display:inline-grid;place-items:center;min-inline-size:22px;block-size:20px;padding-inline:6px;border-radius:999px;background:var(--bg-3);border:1px solid var(--line);font-size:var(--text-2xs);font-weight:var(--fw-medium);color:var(--ink-3)}.ah__sec-hint{margin-inline-start:auto;font-size:var(--text-xs);color:var(--ink-3)}.ah__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3)}.ah__card{display:flex;flex-direction:column;align-items:stretch;position:relative;overflow:hidden;text-align:start;padding:22px 22px 18px;background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);color:inherit;font-family:inherit;cursor:pointer;transition:transform var(--t-overlay),box-shadow var(--t-overlay),border-color var(--t-overlay)}.ah__card:before{content:\"\";position:absolute;inset:0;pointer-events:none;border-radius:inherit;padding:1px;background:var(--grad);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity var(--t-overlay)}.ah__card:hover:not(:disabled){transform:translateY(-2px);border-color:transparent;box-shadow:var(--shadow-filter)}.ah__card:hover:not(:disabled):before{opacity:1}.ah__card:focus-visible{outline:0;border-color:transparent;box-shadow:0 0 0 3px var(--accent-soft),var(--shadow-filter)}.ah__card:focus-visible:before{opacity:1}.ah__card:disabled{cursor:default;opacity:.5}.ah__card-ico{display:grid;place-items:center;inline-size:40px;block-size:40px;margin-block-end:18px;border-radius:11px;background:var(--bg-3);border:0;color:var(--ink);font-size:var(--icon-lg);transition:background var(--t-overlay),color var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-ico,.ah__card:focus-visible .ah__card-ico{background:var(--grad);color:var(--ink-inverse)}.ah__card-title{font-size:var(--text-sm);font-weight:var(--fw-semibold);letter-spacing:-.01em;color:var(--ink)}.ah__card-title--feature{margin-block-end:4px;font-size:18px;letter-spacing:-.015em}.ah__card-desc{font-size:var(--text-xs);line-height:1.45;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ah__card-desc--feature{margin-block-end:18px;font-size:var(--text-sm);line-height:1.5;text-wrap:pretty;overflow:visible;white-space:normal}.ah__card-open{display:inline-flex;align-items:center;gap:4px;margin-block-start:auto;font-size:var(--text-2xs);color:var(--ink-3);transition:color var(--t-overlay),gap var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-open,.ah__card:focus-visible .ah__card-open{color:var(--accent);gap:8px}.ah__card-arrow{flex-shrink:0;transition:color var(--t-overlay),transform var(--t-overlay)}.ah__card--compact{flex-direction:row;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4)}.ah__card--compact .ah__card-ico{inline-size:34px;block-size:34px;margin-block-end:0;flex-shrink:0;border-radius:9px;font-size:var(--icon-md)}.ah__card-main{display:flex;flex-direction:column;gap:2px;flex:1;min-inline-size:0}.ah__card--compact .ah__card-arrow{color:var(--ink-4)}.ah__card--compact:hover:not(:disabled) .ah__card-arrow{color:var(--accent);transform:translate(2px)}:host-context([dir=rtl]) .ah__card-arrow{transform:scaleX(-1)}:host-context([dir=rtl]) .ah__sec-chev:not(.ah__sec-chev--open){transform:scaleX(-1)}:host-context([dir=rtl]) .ah__card--compact:hover:not(:disabled) .ah__card-arrow{transform:scaleX(-1) translate(2px)}.ah__sec-chev--open{transform:rotate(90deg)}@container (width <= 900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@media(width<=900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@container (width <= 560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}@media(width<=560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}\n"] }]
19613
+ args: [{ selector: 'fly-app-home', standalone: true, imports: [NgTemplateOutlet, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"ah\">\r\n <header class=\"ah__util\">\r\n <button type=\"button\" class=\"ah__brand\" (click)=\"brandSelected.emit()\">\r\n <span class=\"ah__brand-mark\"><ng-content select=\"[app-home-brand]\" /></span>\r\n @if (brandLabelKey(); as key) {\r\n <span class=\"ah__brand-name\">{{ key | translate }}</span>\r\n }\r\n </button>\r\n <ng-content select=\"[app-home-actions]\" />\r\n </header>\r\n\r\n <main class=\"ah__main\">\r\n @if (titleKey() || subtitleKey()) {\r\n <section class=\"ah__hero\">\r\n @if (titleKey(); as key) {\r\n <h1 class=\"ah__title\">{{ key | translate }}</h1>\r\n }\r\n @if (subtitleKey(); as key) {\r\n <p class=\"ah__sub\">{{ key | translate }}</p>\r\n }\r\n </section>\r\n }\r\n\r\n @for (row of rows(); track row.index) {\r\n <section\r\n class=\"ah__sec\"\r\n [class.ah__sec--secondary]=\"row.index > 0\"\r\n [attr.aria-label]=\"row.section.titleKey ? null : (modulesLabelKey() | translate)\"\r\n >\r\n @if (row.section.titleKey; as titleKey) {\r\n @if (row.section.collapsible) {\r\n <button\r\n type=\"button\"\r\n class=\"ah__sec-hd\"\r\n (click)=\"toggle(row.index)\"\r\n [attr.aria-expanded]=\"row.expanded\"\r\n [attr.aria-controls]=\"sectionId(row.index)\"\r\n >\r\n <!-- Points end-ward when shut, down when open. `--open` wins over the RTL\r\n flip below because a downward chevron has no direction to mirror. -->\r\n <svg\r\n class=\"ah__sec-chev\"\r\n [class.ah__sec-chev--open]=\"row.expanded\"\r\n width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\r\n stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"\r\n >\r\n <polyline points=\"9 6 15 12 9 18\" />\r\n </svg>\r\n <span class=\"ah__sec-title\">{{ titleKey | translate }}</span>\r\n <span class=\"ah__sec-count\">{{ row.section.modules.length }}</span>\r\n @if (row.hintKey; as hint) {\r\n <span class=\"ah__sec-hint\">{{ hint | translate }}</span>\r\n }\r\n </button>\r\n } @else {\r\n <h2 class=\"ah__sec-title ah__sec-title--static\">{{ titleKey | translate }}</h2>\r\n }\r\n }\r\n\r\n @if (row.expanded) {\r\n <div class=\"ah__grid\" [class.ah__grid--compact]=\"row.layout === 'compact'\" [id]=\"sectionId(row.index)\">\r\n @for (mod of row.section.modules; track mod.key) {\r\n <button\r\n type=\"button\"\r\n class=\"ah__card\"\r\n [class.ah__card--compact]=\"row.layout === 'compact'\"\r\n [disabled]=\"mod.disabled\"\r\n (click)=\"select(mod)\"\r\n >\r\n <span class=\"ah__card-ico\">\r\n @if (iconFor(mod.key); as tpl) {\r\n <ng-container *ngTemplateOutlet=\"tpl\" />\r\n } @else if (mod.iconClass) {\r\n <i [class]=\"mod.iconClass\" aria-hidden=\"true\"></i>\r\n }\r\n </span>\r\n\r\n @if (row.layout === 'compact') {\r\n <span class=\"ah__card-main\">\r\n <span class=\"ah__card-title\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"ah__card-desc\">{{ descKey | translate }}</span>\r\n }\r\n </span>\r\n <svg\r\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\" aria-hidden=\"true\"\r\n >\r\n <polyline points=\"9 6 15 12 9 18\" />\r\n </svg>\r\n } @else {\r\n <span class=\"ah__card-title ah__card-title--feature\">{{ mod.labelKey | translate }}</span>\r\n @if (mod.descKey; as descKey) {\r\n <span class=\"ah__card-desc ah__card-desc--feature\">{{ descKey | translate }}</span>\r\n }\r\n <span class=\"ah__card-open\">\r\n {{ 'ui.action.open' | translate }}\r\n <svg\r\n class=\"ah__card-arrow\" width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\"\r\n stroke=\"currentColor\" stroke-width=\"2.6\" stroke-linecap=\"round\"\r\n stroke-linejoin=\"round\" aria-hidden=\"true\"\r\n >\r\n <polyline points=\"9 6 15 12 9 18\" />\r\n </svg>\r\n </span>\r\n }\r\n </button>\r\n }\r\n </div>\r\n }\r\n </section>\r\n }\r\n </main>\r\n</div>\r\n", styles: [":host{display:flex;flex-direction:column;min-height:100%;container-type:inline-size}.ah{display:flex;flex-direction:column;flex:1;min-height:100%}.ah__util{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-4);padding:var(--sp-4) var(--sp-6)}.ah__brand{display:inline-flex;align-items:center;gap:var(--sp-2);padding:0;border:0;background:none;color:var(--ink);font-family:inherit;cursor:pointer}.ah__brand:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__brand-name{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__main{flex:1;inline-size:100%;max-inline-size:1120px;margin-inline:auto;padding:56px 40px;display:flex;flex-direction:column;justify-content:center;gap:44px}.ah__hero{max-inline-size:640px}.ah__title{margin:0 0 var(--sp-3);font-size:44px;font-weight:var(--fw-semibold);letter-spacing:-.03em;text-wrap:balance;background:linear-gradient(180deg,var(--ink),var(--ink-2));-webkit-background-clip:text;background-clip:text;color:transparent}.ah__sub{margin:0;max-inline-size:560px;font-size:var(--text-lg);line-height:1.55;color:var(--ink-3);text-wrap:pretty}.ah__sec{display:flex;flex-direction:column;gap:var(--sp-4)}.ah__sec--secondary{padding-block-start:28px;border-block-start:1px solid var(--line)}.ah__sec-hd{display:flex;align-items:center;gap:var(--sp-3);inline-size:100%;padding:0;background:none;border:0;cursor:pointer;font-family:inherit;color:var(--ink);text-align:start}.ah__sec-hd:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ah__sec-chev{flex-shrink:0;color:var(--ink-3);transition:transform var(--t-overlay)}.ah__sec-title{font-size:var(--text-md);font-weight:var(--fw-semibold);letter-spacing:-.01em}.ah__sec-title--static{margin:0;color:var(--ink-3);font-size:var(--text-xs);letter-spacing:var(--tracking-wide);text-transform:uppercase}.ah__sec-count{display:inline-grid;place-items:center;min-inline-size:22px;block-size:20px;padding-inline:6px;border-radius:999px;background:var(--bg-3);border:1px solid var(--line);font-size:var(--text-2xs);font-weight:var(--fw-medium);color:var(--ink-3)}.ah__sec-hint{margin-inline-start:auto;font-size:var(--text-xs);color:var(--ink-3)}.ah__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-3)}.ah__card{display:flex;flex-direction:column;align-items:stretch;position:relative;overflow:hidden;text-align:start;padding:22px 22px 18px;background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);color:inherit;font-family:inherit;cursor:pointer;transition:transform var(--t-overlay),box-shadow var(--t-overlay),border-color var(--t-overlay)}.ah__card:before{content:\"\";position:absolute;inset:0;pointer-events:none;border-radius:inherit;padding:1px;background:var(--grad);-webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);-webkit-mask-composite:xor;mask-composite:exclude;opacity:0;transition:opacity var(--t-overlay)}.ah__card:hover:not(:disabled){transform:translateY(-2px);border-color:transparent;box-shadow:var(--shadow-filter)}.ah__card:hover:not(:disabled):before{opacity:1}.ah__card:focus-visible{outline:0;border-color:transparent;box-shadow:0 0 0 3px var(--accent-soft),var(--shadow-filter)}.ah__card:focus-visible:before{opacity:1}.ah__card:disabled{cursor:default;opacity:.5}.ah__card-ico{display:grid;place-items:center;inline-size:40px;block-size:40px;margin-block-end:18px;border-radius:11px;background:var(--bg-3);border:0;color:var(--ink);font-size:var(--icon-lg);transition:background var(--t-overlay),color var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-ico,.ah__card:focus-visible .ah__card-ico{background:var(--grad);color:var(--ink-inverse)}.ah__card-title{font-size:var(--text-sm);font-weight:var(--fw-semibold);letter-spacing:-.01em;color:var(--ink)}.ah__card-title--feature{margin-block-end:4px;font-size:18px;letter-spacing:-.015em}.ah__card-desc{font-size:var(--text-xs);line-height:1.45;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ah__card-desc--feature{margin-block-end:18px;font-size:var(--text-sm);line-height:1.5;text-wrap:pretty;overflow:visible;white-space:normal}.ah__card-open{display:inline-flex;align-items:center;gap:4px;margin-block-start:auto;font-size:var(--text-2xs);color:var(--ink-3);transition:color var(--t-overlay),gap var(--t-overlay)}.ah__card:hover:not(:disabled) .ah__card-open,.ah__card:focus-visible .ah__card-open{color:var(--accent);gap:8px}.ah__card-arrow{flex-shrink:0;transition:color var(--t-overlay),transform var(--t-overlay)}.ah__card--compact{flex-direction:row;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4)}.ah__card--compact .ah__card-ico{inline-size:34px;block-size:34px;margin-block-end:0;flex-shrink:0;border-radius:9px;font-size:var(--icon-md)}.ah__card-main{display:flex;flex-direction:column;gap:2px;flex:1;min-inline-size:0}.ah__card--compact .ah__card-arrow{color:var(--ink-4)}.ah__card--compact:hover:not(:disabled) .ah__card-arrow{color:var(--accent);transform:translate(2px)}:host-context([dir=rtl]) .ah__card-arrow{transform:scaleX(-1)}:host-context([dir=rtl]) .ah__sec-chev:not(.ah__sec-chev--open){transform:scaleX(-1)}:host-context([dir=rtl]) .ah__card--compact:hover:not(:disabled) .ah__card-arrow{transform:scaleX(-1) translate(2px)}.ah__sec-chev--open{transform:rotate(90deg)}@container (width <= 900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@media(width<=900px){.ah__grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ah__main{padding:40px 24px;gap:36px}.ah__title{font-size:36px}.ah__util{padding:var(--sp-3) var(--sp-5)}}@container (width <= 560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}@media(width<=560px){.ah__grid{grid-template-columns:1fr}.ah__title{font-size:30px}.ah__sub{font-size:var(--text-sm)}.ah__sec-hint{display:none}}\n"] }]
19172
19614
  }], propDecorators: { brandLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "brandLabelKey", required: false }] }], titleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleKey", required: false }] }], subtitleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitleKey", required: false }] }], sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }], modulesLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "modulesLabelKey", required: false }] }], moduleSelected: [{ type: i0.Output, args: ["moduleSelected"] }], brandSelected: [{ type: i0.Output, args: ["brandSelected"] }], icons: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => FlyModuleIconDirective), { isSignal: true }] }] } });
19173
19615
 
19174
19616
  /**
@@ -20134,7 +20576,7 @@ class FlyFormSectionComponent {
20134
20576
  }
20135
20577
  </div>
20136
20578
  <div class="sec__c"><ng-content /></div>
20137
- `, isInline: true, styles: [":host{display:grid;grid-template-columns:140px 1fr;gap:24px;padding:22px 0;border-bottom:1px solid var(--line-3)}:host(:last-of-type){border-bottom:0}.sec__l{font-size:var(--text-xs);font-weight:var(--fw-medium);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding-top:8px;position:sticky;top:8px;align-self:start}.sec__c{min-width:0}@media(width<=720px){:host{grid-template-columns:1fr;gap:8px}.sec__l{padding-top:0;position:static}}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20579
+ `, isInline: true, styles: [":host{display:grid;grid-template-columns:140px 1fr;gap:24px;padding:22px 0;border-bottom:1px solid var(--line-3)}:host(:last-of-type){border-bottom:0}.sec__l{font-size:var(--text-xs);font-weight:var(--fw-medium);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding-top:8px;position:sticky;top:8px;align-self:start}.sec__c{min-width:0}@container (width <= 720px){:host{grid-template-columns:1fr;gap:8px}.sec__l{padding-top:0;position:static}}@media(width<=720px){:host{grid-template-columns:1fr;gap:8px}.sec__l{padding-top:0;position:static}}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20138
20580
  }
20139
20581
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyFormSectionComponent, decorators: [{
20140
20582
  type: Component,
@@ -20145,7 +20587,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
20145
20587
  }
20146
20588
  </div>
20147
20589
  <div class="sec__c"><ng-content /></div>
20148
- `, styles: [":host{display:grid;grid-template-columns:140px 1fr;gap:24px;padding:22px 0;border-bottom:1px solid var(--line-3)}:host(:last-of-type){border-bottom:0}.sec__l{font-size:var(--text-xs);font-weight:var(--fw-medium);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding-top:8px;position:sticky;top:8px;align-self:start}.sec__c{min-width:0}@media(width<=720px){:host{grid-template-columns:1fr;gap:8px}.sec__l{padding-top:0;position:static}}\n"] }]
20590
+ `, styles: [":host{display:grid;grid-template-columns:140px 1fr;gap:24px;padding:22px 0;border-bottom:1px solid var(--line-3)}:host(:last-of-type){border-bottom:0}.sec__l{font-size:var(--text-xs);font-weight:var(--fw-medium);letter-spacing:var(--tracking-wide);text-transform:uppercase;color:var(--ink-3);padding-top:8px;position:sticky;top:8px;align-self:start}.sec__c{min-width:0}@container (width <= 720px){:host{grid-template-columns:1fr;gap:8px}.sec__l{padding-top:0;position:static}}@media(width<=720px){:host{grid-template-columns:1fr;gap:8px}.sec__l{padding-top:0;position:static}}\n"] }]
20149
20591
  }], propDecorators: { labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }] } });
20150
20592
  /**
20151
20593
  * Responsive field grid (port of the legacy `.dr__grid/--2/--3`): 1–3 equal
@@ -21399,5 +21841,5 @@ const AUDIENCE_ERROR_CODES = {
21399
21841
  * Generated bundle index. Do not edit.
21400
21842
  */
21401
21843
 
21402
- export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, BilingualFieldComponent, CRON_MODES, CRON_WEEKDAYS, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DashboardKpiComponent, DialogResult, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLYOS_LAUNCH_REQUEST_EVENT, FLYOS_REMOTE_ROUTE_EVENT, FLY_ACCENT_PROPERTY, FLY_ADMIN_ROLES, FLY_CHUNK_RELOAD_FLAG, FLY_COMPACT_THRESHOLD, FLY_COUNTRIES, FLY_EMOJI_BY_ID, FLY_EMOJI_CATEGORIES, FLY_EMOJI_CATEGORY_BY_ID, FLY_EMOJI_DEFAULT_CATEGORIES, FLY_EMOJI_PACK, FLY_EMOJI_PACK_MISSING_MESSAGE, FLY_EMOJI_PREVIEW_COUNT, FLY_EMPTY_VALUE, FLY_LOCALE_CATALOG, FLY_NF_CACHE_HEAL_FLAG, FLY_RELOAD_DEBOUNCE_MS, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_ROUTES, FLY_SEARCH_DEBOUNCE_MS, FLY_SKIN_TONES, FLY_STANDALONE_AUTH_CONFIG, FLY_THEME_MODE_IDS, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, FlyAuthLiveRefresh, FlyBadgeWallComponent, FlyBlockUiComponent, FlyBreadcrumbComponent, FlyButtonComponent, FlyBytesPipe, FlyCaptchaComponent, FlyCardActionsComponent, FlyCardBodyComponent, FlyCardComponent, FlyCardFooterComponent, FlyCardGridComponent, FlyCardMediaComponent, FlyCardMetaComponent, FlyCardStatusComponent, FlyCardTitleComponent, FlyCellDirective, FlyCheckboxComponent, FlyChipComponent, FlyChunkReloadErrorHandler, FlyClickOutsideDirective, FlyCollapsibleComponent, FlyCommentThreadComponent, FlyCompactNumberPipe, FlyConfirmDialogComponent, FlyControlDirective, FlyCronBuilderComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLeaderboardComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStateMessageComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, GANTT_ZOOMS, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, OverlayStack, PRESENCE_COLORS, PageHeaderComponent, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, STATE_DEFAULT_ICONS, STATE_DEFAULT_MESSAGE_KEYS, STEP_UP_CODE, STEP_UP_REAUTH_HANDLER, STEP_UP_RETURN_KEY, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, StatusBadgeComponent, StepUpService, ToastHostComponent, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext, canGoPrev, captureFocus, clampPage, clampSliderValue, connectRemoteLaunch, enterActivatesNatively, filterGroup, filterSuggestions, findLocaleByDialect, findLocaleByPrefix, firstEnabledIndex, firstModuleIndex, flattenModules, flyAdminGuard, flyCaptchaBase64Utf8, flyCaptchaBuildToken, flyCaptchaSha256Hex, flyCompactNumber, flyDebounced, flyDecimalNumber, flyDownloadBlob, flyDuration, flyEmojiFor, flyEmojiIsTonable, flyEmojiPackUrl, flyExportFileName, flyFirstEnabledIndex, flyFormatBytes, flyFormatDate, flyFormatDateTime, flyFormatTime, flyFullNumber, flyNextEnabledIndex, flyNextSegmentIndex, flyRelativeTime, flyResolveActiveTab, flyRoleGuard, flySignedCompact, flySkinToneLabelKey, flyStandaloneAuthGuard, flyStandaloneAuthInterceptor, flyToDateOnly, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, loadRemoteStyles, matchFlyRoutePattern, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeRoles, overlayStack, parseCron, parseStepUpChallenge, parseTags, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
21844
+ export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, BilingualFieldComponent, CRON_MODES, CRON_WEEKDAYS, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DashboardKpiComponent, DialogResult, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLYOS_LAUNCH_REQUEST_EVENT, FLYOS_REMOTE_ROUTE_EVENT, FLY_ACCENT_PROPERTY, FLY_ADMIN_ROLES, FLY_CHUNK_RELOAD_FLAG, FLY_COMPACT_THRESHOLD, FLY_COUNTRIES, FLY_EMOJI_BY_ID, FLY_EMOJI_CATEGORIES, FLY_EMOJI_CATEGORY_BY_ID, FLY_EMOJI_DEFAULT_CATEGORIES, FLY_EMOJI_PACK, FLY_EMOJI_PACK_MISSING_MESSAGE, FLY_EMOJI_PREVIEW_COUNT, FLY_EMPTY_VALUE, FLY_LOCALE_CATALOG, FLY_NF_CACHE_HEAL_FLAG, FLY_RELOAD_DEBOUNCE_MS, FLY_RELOAD_REARM_MS, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_ROUTES, FLY_SEARCH_DEBOUNCE_MS, FLY_SKIN_TONES, FLY_STANDALONE_AUTH_CONFIG, FLY_THEME_MODE_IDS, FLY_WINDOW_HELP_HINT_EVENT, FLY_WINDOW_PAGE_TITLE_EVENT, FlyAchievementProgressComponent, FlyActionMenuComponent, FlyActionStackComponent, FlyAgentDraggableDirective, FlyAnimatedEmojiComponent, FlyAppHomeComponent, FlyAppTopbarComponent, FlyAuthLiveRefresh, FlyBadgeWallComponent, FlyBlockUiComponent, FlyBreadcrumbComponent, FlyButtonComponent, FlyBytesPipe, FlyCaptchaComponent, FlyCardActionsComponent, FlyCardBodyComponent, FlyCardComponent, FlyCardFooterComponent, FlyCardGridComponent, FlyCardMediaComponent, FlyCardMetaComponent, FlyCardStatusComponent, FlyCardTitleComponent, FlyCellDirective, FlyCheckboxComponent, FlyChipComponent, FlyChunkReloadErrorHandler, FlyClickOutsideDirective, FlyCollapsibleComponent, FlyCommentThreadComponent, FlyCompactNumberPipe, FlyConfirmDialogComponent, FlyControlDirective, FlyCronBuilderComponent, FlyDataTableComponent, FlyDatePipe, FlyDateTimePipe, FlyDebouncer, FlyDecimalNumberPipe, FlyDetailCardComponent, FlyDetailShellComponent, FlyDrawerComponent, FlyDurationPipe, FlyDynamicFormComponent, FlyEmojiPickerComponent, FlyFieldComponent, FlyFileUploadComponent, FlyFilterPanelComponent, FlyFormBannerComponent, FlyFormFooterComponent, FlyFormGridComponent, FlyFormSectionComponent, FlyFullNumberPipe, FlyGanttComponent, FlyHubClient, FlyIconButtonComponent, FlyIfAdminDirective, FlyIfRoleDirective, FlyImageUploadComponent, FlyLeaderboardComponent, FlyMetaListComponent, FlyModalComponent, FlyModerationQueueComponent, FlyModuleIconDirective, FlyPaginationComponent, FlyPeoplePickerComponent, FlyPointsTierComponent, FlyProgressComponent, FlyRelativeTimePipe, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySearchInputComponent, FlySectionHeaderComponent, FlySecureSrcDirective, FlySegmentedComponent, FlySelectComponent, FlySkeletonComponent, FlySliderComponent, FlySparklineComponent, FlySpinnerComponent, FlyStandaloneAuthCallbackComponent, FlyStandaloneAuthService, FlyStateMessageComponent, FlyDynamicFormComponent as FlySurveyFormComponent, FlyTabComponent, FlyTabsComponent, FlyTagsInputComponent, FlyThemeService, FlyTimePipe, FlyToastService, FlyToggleComponent, FlyTooltipDirective, FlyTreeNavComponent, FlyTypeaheadComponent, FlyUserDirectoryService, FlyWindowHelpService, FlyWindowTitleService, FlyosPendingLaunchesGlobalKey, FlyosShellHandlesLaunchRequestsGlobalKey, FlyosShellOwnsHistoryGlobalKey, GANTT_ZOOMS, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, OverlayStack, PRESENCE_COLORS, PageHeaderComponent, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, STATE_DEFAULT_ICONS, STATE_DEFAULT_MESSAGE_KEYS, STEP_UP_CODE, STEP_UP_REAUTH_HANDLER, STEP_UP_RETURN_KEY, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, StatusBadgeComponent, StepUpService, ToastHostComponent, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, applySkinTone, applySuggestion, ariaSort, buildCron, canConfirm, canGoNext, canGoPrev, captureFocus, clampPage, clampSliderValue, connectRemoteLaunch, enterActivatesNatively, filterGroup, filterSuggestions, findLocaleByDialect, findLocaleByPrefix, firstEnabledIndex, firstModuleIndex, flattenModules, flyAdminGuard, flyCaptchaBase64Utf8, flyCaptchaBuildToken, flyCaptchaSha256Hex, flyCompactNumber, flyDebounced, flyDecimalNumber, flyDownloadBlob, flyDuration, flyEmojiFor, flyEmojiIsTonable, flyEmojiPackUrl, flyExportFileName, flyFirstEnabledIndex, flyFormatBytes, flyFormatDate, flyFormatDateTime, flyFormatTime, flyFullNumber, flyNextEnabledIndex, flyNextSegmentIndex, flyRelativeTime, flyResolveActiveTab, flyRoleGuard, flySignedCompact, flySkinToneLabelKey, flyStandaloneAuthGuard, flyStandaloneAuthInterceptor, flyToDateOnly, hasAnyRoleIn, healNativeFederationCacheOnce, initialExpanded, isAccentColor, isChunkLoadError, isFlyAuthStorePopulated, isNativeFederationCacheError, isRtlLocale, isRtlLocaleEntry, isValidCron, isValidSingleField, loadRemoteStyles, matchFlyRoutePattern, nextEnabledIndex, nextModuleIndex, nextSegmentIndex, nextSort, normalizeFlyTheme, normalizeRoles, overlayStack, parseCron, parseStepUpChallenge, parseTags, presenceColorFor, printConsoleSecurityWarning, provideFlyChunkReloadRecovery, provideFlyEmojiPack, provideFlyStandaloneAuth, reloadOnceForChunkError, requestAppLaunch, resolveActiveModule, resolveActiveSection, resolveActiveTab, resolveCellValue, resolveStateIcon, resolveStateMessageKey, restoreFocus, sameTags, sectionHintKey, sectionLayout, sliderTrackGradient, sparklinePath, sparklinePoints, stepUpInterceptor, stepUpReturnKey, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload, warnIfEmbeddedSessionMissing };
21403
21845
  //# sourceMappingURL=flyos-design-system.mjs.map