@flyos/design-system 1.3.0 → 1.5.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.5.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
@@ -1207,9 +1207,14 @@ const flyStandaloneAuthGuard = () => {
1207
1207
  * token — attaching it would exfiltrate the token to whatever host the app happens to call.
1208
1208
  *
1209
1209
  * Requests that MUST NOT carry the bearer even same-origin: the STS token endpoint (its own client
1210
- * auth) and the pre-auth BFF endpoints under `/api/{appId}/auth/` (they authenticate via the HttpOnly
1211
- * cookie). The exclusion runs on the origin-NORMALIZED pathname, so an absolute same-origin URL to
1212
- * the BFF is excluded exactly like its relative spelling.
1210
+ * auth), the pre-auth BFF endpoints under `/api/{appId}/auth/` (they authenticate via the HttpOnly
1211
+ * cookie), and any path the app declared in `anonymousPaths` (an `[AllowAnonymous]` surface such as
1212
+ * a public portal). The exclusion runs on the origin-NORMALIZED pathname, so an absolute same-origin
1213
+ * URL to the BFF is excluded exactly like its relative spelling.
1214
+ *
1215
+ * The same exclusion governs the 401 handling: a `401` from an excluded path never starts a login.
1216
+ * That matters most for `anonymousPaths` — without it, a stray 401 on a public surface would bounce
1217
+ * an anonymous visitor into a PKCE login they cannot complete.
1213
1218
  *
1214
1219
  * Ordering: register this AFTER any offline/sample-data interceptor and BEFORE `stepUpInterceptor`
1215
1220
  * (a `401 step_up_required` arrives while the session is valid, so this interceptor passes it through
@@ -1217,8 +1222,11 @@ const flyStandaloneAuthGuard = () => {
1217
1222
  */
1218
1223
  function flyStandaloneAuthInterceptor(req, next) {
1219
1224
  const auth = inject(FlyStandaloneAuthService);
1220
- const appId = inject(FLY_STANDALONE_AUTH_CONFIG).appId;
1221
- const authPaths = ['/connect/token', `/api/${appId}/auth/`];
1225
+ const config = inject(FLY_STANDALONE_AUTH_CONFIG);
1226
+ const appId = config.appId;
1227
+ // App-declared anonymous surfaces join the built-in pre-auth paths: no bearer, and no
1228
+ // login redirect on a 401 (see `FlyStandaloneAuthConfig.anonymousPaths`).
1229
+ const authPaths = ['/connect/token', `/api/${appId}/auth/`, ...(config.anonymousPaths ?? [])];
1222
1230
  // Normalize BEFORE any decision: resolving against the app's own origin collapses a relative URL
1223
1231
  // and its absolute same-origin spelling onto one shape, and exposes a foreign origin for what it is.
1224
1232
  let sameOrigin = false;
@@ -2697,14 +2705,18 @@ function normalizeLocaleJson(raw) {
2697
2705
  /**
2698
2706
  * Shared I18nService for the shell and Business Apps.
2699
2707
  *
2700
- * **Merge order** (later keys win): DS baseline → shell layer remote bundles
2701
- * in registration order.
2708
+ * **Merge order** (later keys win): DS baseline → remote bundles in registration
2709
+ * order shell layer.
2702
2710
  *
2703
2711
  * - Baseline: {@link DS_BASELINE_LOCALES} — built-in strings for DS components
2704
2712
  * (markdown editor, entity lookup) so they render localized labels even in a
2705
2713
  * 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`.
2714
+ * - Remotes: `loadBundle()` per manifest `localeBaseUrl`. May ADD any key and may
2715
+ * override the baseline, but cannot redefine a key the shell owns — the dictionary
2716
+ * is global, so allowing that let one remote restyle the whole desktop's wording.
2717
+ * - Shell: `setShellTranslations()` after loading `locale/{lang}.json` and API
2718
+ * overrides. Highest priority, which also makes the per-tenant
2719
+ * `/api/i18n/desktop-shell/{lang}` overrides authoritative over every app.
2708
2720
  */
2709
2721
  class I18nService {
2710
2722
  // NOTE (NG0200 / federation): the ErrorHandler ↔ I18nService cycle is broken on the
@@ -2719,20 +2731,43 @@ class I18nService {
2719
2731
  _bundleOrder = signal([], ...(ngDevMode ? [{ debugName: "_bundleOrder" }] : /* istanbul ignore next */ []));
2720
2732
  _locale = signal('en', ...(ngDevMode ? [{ debugName: "_locale" }] : /* istanbul ignore next */ []));
2721
2733
  _version = signal(0, ...(ngDevMode ? [{ debugName: "_version" }] : /* istanbul ignore next */ []));
2734
+ /** `${bundleId}|${locale}` pairs already reported by {@link warnOnSuppressedShellKeys}. */
2735
+ _warnedCollisions = new Set();
2722
2736
  /** Built-in DS strings for the active locale (falls back to `en` for any
2723
2737
  * locale we don't ship). Lowest-priority layer — always overridable. */
2724
2738
  _baseline = computed(() => DS_BASELINE_LOCALES[this._locale()] ?? DS_BASELINE_LOCALES['en'] ?? {}, ...(ngDevMode ? [{ debugName: "_baseline" }] : /* istanbul ignore next */ []));
2739
+ /**
2740
+ * Merge order, lowest priority first: DS baseline → remote bundles (registration
2741
+ * order) → shell.
2742
+ *
2743
+ * The shell layer is LAST, and that is the whole point. Translations live in one
2744
+ * flat, global dictionary with no per-app scoping, so when remote bundles were
2745
+ * merged last, any remote that happened to define a key the shell also defines
2746
+ * silently rewrote that string for the ENTIRE desktop — every other app included.
2747
+ *
2748
+ * That was not hypothetical: the Circles remote's bundle shares 63 keys with the
2749
+ * shell and differed on 27 of them, one being `common.label.dashboard`, which is
2750
+ * how it came to own the Arabic display name of a Core App it has nothing to do
2751
+ * with. The symptom is near-impossible to chase from the outside — editing the
2752
+ * shell's own `locale/{lang}.json` simply has no effect at runtime, with nothing
2753
+ * logged.
2754
+ *
2755
+ * A remote may still override the DS baseline (bundles are merged after it), which
2756
+ * is the layer explicitly documented as always-overridable, and it may still add
2757
+ * any key the shell does not define. What it can no longer do is redefine a string
2758
+ * the shell owns. {@link loadBundle} warns in dev when a bundle tries.
2759
+ */
2725
2760
  _merged = computed(() => {
2726
2761
  const shell = this._shell();
2727
2762
  const order = this._bundleOrder();
2728
2763
  const bundles = this._bundles();
2729
- let out = { ...this._baseline(), ...shell };
2764
+ let out = { ...this._baseline() };
2730
2765
  for (const id of order) {
2731
2766
  const b = bundles[id];
2732
2767
  if (b)
2733
2768
  out = { ...out, ...b };
2734
2769
  }
2735
- return out;
2770
+ return { ...out, ...shell };
2736
2771
  }, ...(ngDevMode ? [{ debugName: "_merged" }] : /* istanbul ignore next */ []));
2737
2772
  locale = this._locale.asReadonly();
2738
2773
  version = this._version.asReadonly();
@@ -2746,6 +2781,17 @@ class I18nService {
2746
2781
  this._shell.set(data);
2747
2782
  this._locale.set(lang);
2748
2783
  this.bump();
2784
+ // Bundles routinely land BEFORE this — the shell kicks off remote locale loads
2785
+ // and its own fetch together, and the remotes often win the race. Re-checking
2786
+ // here is what makes the diagnostic order-independent; without it the warning
2787
+ // fired only when the shell happened to arrive first, which in practice was
2788
+ // never.
2789
+ const bundles = this._bundles();
2790
+ for (const id of this._bundleOrder()) {
2791
+ const b = bundles[id];
2792
+ if (b)
2793
+ this.warnOnSuppressedShellKeys(id, b);
2794
+ }
2749
2795
  }
2750
2796
  setLanguage(lang) {
2751
2797
  this._locale.set(lang);
@@ -2771,6 +2817,7 @@ class I18nService {
2771
2817
  const data = normalizeLocaleJson(await resp.json());
2772
2818
  if (abortSignal?.aborted)
2773
2819
  return false;
2820
+ this.warnOnSuppressedShellKeys(id, data);
2774
2821
  this._bundles.update((b) => ({ ...b, [id]: data }));
2775
2822
  this._bundleOrder.update((order) => (order.includes(id) ? order : [...order, id]));
2776
2823
  this._locale.set(lang);
@@ -2785,6 +2832,45 @@ class I18nService {
2785
2832
  return false;
2786
2833
  }
2787
2834
  }
2835
+ /**
2836
+ * Dev-only: report keys a bundle defines that the shell already owns.
2837
+ *
2838
+ * These are now silently ignored (the shell wins), so without this the app would
2839
+ * simply not use strings the remote shipped and nobody would know which ones. The
2840
+ * previous precedence was worse — it applied them, changing wording across the
2841
+ * whole desktop, also silently. Either way the failure is invisible, which is the
2842
+ * argument for saying it out loud exactly once, when the bundle loads.
2843
+ *
2844
+ * A hit is not automatically a bug: an app legitimately reuses shared keys it does
2845
+ * not intend to redefine, so only keys whose VALUE differs are worth reporting.
2846
+ * The fix on the app's side is to namespace the key under its own app id.
2847
+ */
2848
+ warnOnSuppressedShellKeys(id, data) {
2849
+ if (!isDevMode())
2850
+ return;
2851
+ const shell = this._shell();
2852
+ // Nothing to conflict with yet — a standalone consumer, or a bundle that raced
2853
+ // ahead of setShellTranslations. Not a collision, so stay quiet; the shell
2854
+ // re-runs this check for every stored bundle once it lands.
2855
+ if (Object.keys(shell).length === 0)
2856
+ return;
2857
+ const suppressed = Object.keys(data).filter((k) => k in shell && shell[k] !== data[k]);
2858
+ if (suppressed.length === 0)
2859
+ return;
2860
+ // Both entry points can reach the same (bundle, locale) pair — a bundle stored
2861
+ // before the shell is checked again when the shell lands, and every locale
2862
+ // switch replays both. Report each pair once so a real signal doesn't drown in
2863
+ // repeats.
2864
+ const seen = `${id}|${this._locale()}`;
2865
+ if (this._warnedCollisions.has(seen))
2866
+ return;
2867
+ this._warnedCollisions.add(seen);
2868
+ const sample = suppressed.slice(0, 10).join(', ');
2869
+ const more = suppressed.length > 10 ? `, …and ${suppressed.length - 10} more` : '';
2870
+ console.warn(`[I18n] Locale bundle "${id}" redefines ${suppressed.length} key(s) the shell owns; ` +
2871
+ `the shell's values win and the bundle's are ignored. Namespace these under "${id}." ` +
2872
+ `if they are meant to be app-specific: ${sample}${more}`);
2873
+ }
2788
2874
  removeBundle(id) {
2789
2875
  this._bundles.update((b) => {
2790
2876
  const next = { ...b };
@@ -3994,6 +4080,290 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
3994
4080
  args: [{ providedIn: 'root' }]
3995
4081
  }] });
3996
4082
 
4083
+ /** `POST /api/users/resolve` accepts at most this many ids per call (platform contract). */
4084
+ const RESOLVE_BATCH_SIZE = 200;
4085
+ /**
4086
+ * **`FlyUserDirectoryService`** — the one id→person adapter for the platform user
4087
+ * directory (`GET /api/users/lookup?q=`, `POST /api/users/resolve`).
4088
+ *
4089
+ * Every app that renders a *list* of people — comment authors, contributors,
4090
+ * approvers, credits, audit rows — needs the same thing: turn a `userId` into a
4091
+ * name without issuing one request per row. Before this service each app wrote
4092
+ * its own copy (Circles `UserResolverService`, Thoughts `UserLookupService`,
4093
+ * plus hand-rolled `/api/users/resolve` calls in the shell and admin portal),
4094
+ * and every copy re-derived the same read-through cache. This is that cache,
4095
+ * once, in the design system.
4096
+ *
4097
+ * The HTTP call rides the **host's** `HttpClient`, so the host's gateway routing,
4098
+ * auth interceptor, and correlation headers all apply — the same arrangement
4099
+ * `fly-entity-lookup` uses. The DS never configures a base URL of its own.
4100
+ *
4101
+ * ## `label()` is read-through — hosts do not prime
4102
+ *
4103
+ * {@link label} is a *signal-backed* accessor: on a cache miss it queues the id
4104
+ * and returns `null`, then the queued ids are resolved in ONE batched request a
4105
+ * microtask later. Because the read happens inside Angular's reactive context,
4106
+ * a template calling `label(id)` re-renders by itself when the answer lands —
4107
+ * no `computed` shadow state, no `prime()` call at each fetch site.
4108
+ *
4109
+ * That last point is the whole reason this exists. The prime-then-read shape it
4110
+ * replaces required every host to (a) keep the raw page separate from the mapped
4111
+ * page, (b) derive the mapped page through a `computed`, and (c) remember to
4112
+ * prime on *every* fetch path — root load, load-more, load-replies, refetch. Miss
4113
+ * one path and that path silently renders GUIDs, with nothing failing. Making the
4114
+ * cache read-through deletes the whole bug class.
4115
+ *
4116
+ * ## Failure is graceful, and never a retry storm
4117
+ *
4118
+ * A failed lookup caches nothing, so callers fall back to whatever they render
4119
+ * for an unresolved id (typically the raw id). Deliberate asymmetry in retry
4120
+ * behaviour, because a render-driven read must not hammer a broken endpoint:
4121
+ *
4122
+ * - **Render-driven** ({@link label}) attempts an id **once**. After a failure it
4123
+ * stays unresolved for the session rather than re-requesting on every
4124
+ * change-detection pass.
4125
+ * - **Explicit** ({@link prime}, {@link resolve}, {@link invalidate}) always
4126
+ * re-attempts. A host re-fetching a page therefore retries naturally, and a
4127
+ * retry button can call {@link invalidate}.
4128
+ *
4129
+ * Unknown / cross-tenant ids are omitted by the endpoint rather than erroring, so
4130
+ * they simply never resolve — which is also the correct behaviour for a
4131
+ * standalone External App where `/api/users/*` isn't reachable at all.
4132
+ *
4133
+ * @example Comment thread — one binding, no plumbing
4134
+ * ```html
4135
+ * <fly-comment-thread
4136
+ * [rootPage]="rootPage()"
4137
+ * [resolveDisplayName]="directory.label" />
4138
+ * ```
4139
+ * ```ts
4140
+ * protected readonly directory = inject(FlyUserDirectoryService);
4141
+ * ```
4142
+ *
4143
+ * @example People picker — search + prefill
4144
+ * ```html
4145
+ * <fly-people-picker
4146
+ * [searchFn]="directory.search()"
4147
+ * [initialSelection]="selected()" />
4148
+ * ```
4149
+ */
4150
+ class FlyUserDirectoryService {
4151
+ http = inject(HttpClient);
4152
+ /** id → person, for everything resolved so far. Signal-backed so {@link label} is reactive. */
4153
+ entries = signal({}, ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
4154
+ /** Ids with a request in the air — so a re-render cannot duplicate an in-flight lookup. */
4155
+ inFlight = new Set();
4156
+ /**
4157
+ * Ids {@link label} has already attempted at least once. Checked ONLY on the
4158
+ * render-driven path: it is what stops a persistently failing endpoint from
4159
+ * being re-requested on every change-detection pass. Explicit callers ignore it.
4160
+ */
4161
+ attempted = new Set();
4162
+ /** Ids queued by {@link label}, awaiting the batched microtask flush. */
4163
+ queued = new Set();
4164
+ flushScheduled = false;
4165
+ // ── Read ─────────────────────────────────────────────────────────────────────
4166
+ /**
4167
+ * Display name for `id`, or `null` when it is not (yet) resolved — on a miss the
4168
+ * id is queued for the next batched resolve. Null rather than the raw id on
4169
+ * purpose: a GUID is not a name, and the caller should render its own fallback
4170
+ * (or localized placeholder) instead of the DS guessing one.
4171
+ *
4172
+ * Bound as a value (`[resolveDisplayName]="directory.label"`), so it is an arrow
4173
+ * property rather than a method — `this` stays correct without the host wrapping
4174
+ * it in a closure.
4175
+ */
4176
+ label = (id) => {
4177
+ if (!id)
4178
+ return null;
4179
+ const hit = this.entries()[id];
4180
+ if (hit)
4181
+ return hit.displayName;
4182
+ this.queueForRender(id);
4183
+ return null;
4184
+ };
4185
+ /**
4186
+ * The full resolved person for `id` (name, email, avatar), or `null` when not yet
4187
+ * known. Same read-through queueing as {@link label} — use it when a surface can
4188
+ * render an avatar rather than just a name.
4189
+ */
4190
+ option = (id) => {
4191
+ if (!id)
4192
+ return null;
4193
+ const hit = this.entries()[id];
4194
+ if (hit)
4195
+ return hit;
4196
+ this.queueForRender(id);
4197
+ return null;
4198
+ };
4199
+ // ── Explicit population ──────────────────────────────────────────────────────
4200
+ /**
4201
+ * Eagerly resolves any ids not already known, merging them into the cache.
4202
+ * Optional — {@link label} resolves on demand — but useful to warm the cache
4203
+ * before a render, or to force a retry after a failed auto-attempt (explicit
4204
+ * calls are not subject to the once-only rule; see the class doc).
4205
+ */
4206
+ prime(ids) {
4207
+ const fresh = this.distinct(ids).filter(id => !this.entries()[id] && !this.inFlight.has(id));
4208
+ if (fresh.length === 0)
4209
+ return;
4210
+ this.fetch(fresh);
4211
+ }
4212
+ /**
4213
+ * Bulk-resolves ids to picker options — for prefilling `fly-people-picker`'s
4214
+ * `[initialSelection]` from stored ids. Requests are chunked to the endpoint's
4215
+ * {@link RESOLVE_BATCH_SIZE} cap rather than truncated, so a large selection
4216
+ * resolves fully instead of silently losing its tail. Resolved rows also land in
4217
+ * the cache, so a later {@link label} for the same id is free. Empty in → empty
4218
+ * out with no request.
4219
+ */
4220
+ resolve(ids) {
4221
+ const distinct = this.distinct(ids);
4222
+ if (distinct.length === 0)
4223
+ return of([]);
4224
+ return new Observable(subscriber => {
4225
+ const chunks = this.chunk(distinct);
4226
+ const collected = [];
4227
+ let remaining = chunks.length;
4228
+ let failed = false;
4229
+ for (const batch of chunks) {
4230
+ this.post(batch).subscribe({
4231
+ next: rows => {
4232
+ this.merge(rows);
4233
+ collected.push(...rows);
4234
+ },
4235
+ error: (err) => {
4236
+ if (failed)
4237
+ return;
4238
+ failed = true;
4239
+ subscriber.error(err);
4240
+ },
4241
+ complete: () => {
4242
+ if (failed)
4243
+ return;
4244
+ if (--remaining === 0) {
4245
+ subscriber.next(collected);
4246
+ subscriber.complete();
4247
+ }
4248
+ },
4249
+ });
4250
+ }
4251
+ });
4252
+ }
4253
+ /**
4254
+ * Drops cached entries so the next read re-resolves them — the retry hook for a
4255
+ * host that surfaces a "couldn't load names" affordance. Omit `ids` to clear the
4256
+ * whole cache.
4257
+ */
4258
+ invalidate(ids) {
4259
+ if (!ids) {
4260
+ this.entries.set({});
4261
+ this.inFlight.clear();
4262
+ this.attempted.clear();
4263
+ this.queued.clear();
4264
+ return;
4265
+ }
4266
+ const targets = this.distinct(ids);
4267
+ if (targets.length === 0)
4268
+ return;
4269
+ targets.forEach(id => {
4270
+ this.inFlight.delete(id);
4271
+ this.attempted.delete(id);
4272
+ });
4273
+ this.entries.update(prev => {
4274
+ const next = { ...prev };
4275
+ targets.forEach(id => delete next[id]);
4276
+ return next;
4277
+ });
4278
+ }
4279
+ // ── Search ───────────────────────────────────────────────────────────────────
4280
+ /**
4281
+ * A `searchFn` over the platform users directory — bind straight to
4282
+ * `<fly-people-picker [searchFn]>`. Results are cached too, so picking someone
4283
+ * makes their name available to {@link label} without another round-trip.
4284
+ */
4285
+ search() {
4286
+ return (query) => this.http
4287
+ .get('/api/users/lookup', { params: { q: query } })
4288
+ .pipe(map(res => (res.data ?? []).map(toOption)), map(rows => {
4289
+ this.merge(rows);
4290
+ return rows;
4291
+ }));
4292
+ }
4293
+ // ── Internals ────────────────────────────────────────────────────────────────
4294
+ /** Queues a render-driven miss, honouring the once-only rule. */
4295
+ queueForRender(id) {
4296
+ if (this.entries()[id] || this.inFlight.has(id) || this.attempted.has(id) || this.queued.has(id))
4297
+ return;
4298
+ this.queued.add(id);
4299
+ if (this.flushScheduled)
4300
+ return;
4301
+ this.flushScheduled = true;
4302
+ // A microtask, not a synchronous call: `label()` runs during template
4303
+ // evaluation, and firing the request (and its signal write) inline would
4304
+ // mutate state mid-change-detection. Deferring also batches every miss in
4305
+ // the pass into ONE request — a 20-comment page costs one round-trip.
4306
+ queueMicrotask(() => this.flush());
4307
+ }
4308
+ flush() {
4309
+ this.flushScheduled = false;
4310
+ const batch = [...this.queued];
4311
+ this.queued.clear();
4312
+ if (batch.length === 0)
4313
+ return;
4314
+ batch.forEach(id => this.attempted.add(id));
4315
+ this.fetch(batch);
4316
+ }
4317
+ /** Issues the (chunked) resolve calls for `ids` and merges whatever comes back. */
4318
+ fetch(ids) {
4319
+ for (const batch of this.chunk(ids)) {
4320
+ batch.forEach(id => this.inFlight.add(id));
4321
+ this.post(batch).subscribe({
4322
+ next: rows => this.merge(rows),
4323
+ // Unknown / cross-tenant ids are omitted by the endpoint rather than
4324
+ // erroring, so reaching here means a transport-level failure. Cache
4325
+ // nothing — a wrong answer is worse than an unresolved one.
4326
+ error: () => batch.forEach(id => this.inFlight.delete(id)),
4327
+ complete: () => batch.forEach(id => this.inFlight.delete(id)),
4328
+ });
4329
+ }
4330
+ }
4331
+ post(ids) {
4332
+ return this.http
4333
+ .post('/api/users/resolve', { ids })
4334
+ .pipe(map(res => (res.data ?? []).map(toOption)));
4335
+ }
4336
+ merge(rows) {
4337
+ if (rows.length === 0)
4338
+ return;
4339
+ this.entries.update(prev => {
4340
+ const next = { ...prev };
4341
+ rows.forEach(r => (next[r.id] = r));
4342
+ return next;
4343
+ });
4344
+ }
4345
+ distinct(ids) {
4346
+ return [...new Set(ids.filter((id) => !!id))];
4347
+ }
4348
+ chunk(ids) {
4349
+ const out = [];
4350
+ for (let i = 0; i < ids.length; i += RESOLVE_BATCH_SIZE) {
4351
+ out.push(ids.slice(i, i + RESOLVE_BATCH_SIZE));
4352
+ }
4353
+ return out;
4354
+ }
4355
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyUserDirectoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4356
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyUserDirectoryService, providedIn: 'root' });
4357
+ }
4358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyUserDirectoryService, decorators: [{
4359
+ type: Injectable,
4360
+ args: [{ providedIn: 'root' }]
4361
+ }] });
4362
+ /** `UserLookupDto`/`UserResolvedDto` → the picker option shape, 1:1. */
4363
+ function toOption(u) {
4364
+ return { id: u.id, displayName: u.displayName, email: u.email, avatarUrl: u.avatarUrl };
4365
+ }
4366
+
3997
4367
  /**
3998
4368
  * The page's CSP nonce, read once at module init from `<meta name="csp-nonce">`.
3999
4369
  *
@@ -8779,11 +9149,11 @@ class FlyBlockUiComponent {
8779
9149
  return k && k.length > 0 ? k : 'common.loading';
8780
9150
  }, ...(ngDevMode ? [{ debugName: "resolvedMessageKey" }] : /* istanbul ignore next */ []));
8781
9151
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyBlockUiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8782
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyBlockUiComponent, isStandalone: true, selector: "fly-block-ui", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: true, transformFunction: null }, messageKey: { classPropertyName: "messageKey", publicName: "messageKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (active()) {\r\n <div\r\n class=\"fly-block-ui\"\r\n role=\"status\"\r\n aria-live=\"polite\"\r\n aria-busy=\"true\"\r\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\r\n <div class=\"fly-block-ui__card\">\r\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\r\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9152
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyBlockUiComponent, isStandalone: true, selector: "fly-block-ui", inputs: { active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: true, transformFunction: null }, messageKey: { classPropertyName: "messageKey", publicName: "messageKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8783
9153
  }
8784
9154
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyBlockUiComponent, decorators: [{
8785
9155
  type: Component,
8786
- args: [{ selector: 'fly-block-ui', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (active()) {\r\n <div\r\n class=\"fly-block-ui\"\r\n role=\"status\"\r\n aria-live=\"polite\"\r\n aria-busy=\"true\"\r\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\r\n <div class=\"fly-block-ui__card\">\r\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\r\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\r\n </div>\r\n </div>\r\n}\r\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"] }]
9156
+ args: [{ selector: 'fly-block-ui', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (active()) {\n <div\n class=\"fly-block-ui\"\n role=\"status\"\n aria-live=\"polite\"\n aria-busy=\"true\"\n [attr.aria-label]=\"resolvedMessageKey() | translate\">\n <div class=\"fly-block-ui__card\">\n <i class=\"pi pi-spin pi-spinner fly-block-ui__spinner\" aria-hidden=\"true\"></i>\n <span class=\"fly-block-ui__text\">{{ resolvedMessageKey() | translate }}</span>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.fly-block-ui{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--surface-ground, #0c0c12) 58%,transparent);-webkit-backdrop-filter:blur(6px) saturate(120%);backdrop-filter:blur(6px) saturate(120%)}.fly-block-ui__card{display:flex;flex-direction:column;align-items:center;gap:12px;padding:24px 36px;border-radius:12px;background:var(--glass-bg, rgba(255, 255, 255, .14));border:1px solid var(--glass-border, rgba(255, 255, 255, .22));box-shadow:0 8px 32px #0000002e}.fly-block-ui__spinner{font-size:2rem;color:var(--primary-color)}.fly-block-ui__text{font-size:.875rem;color:var(--text-color-secondary)}\n"] }]
8787
9157
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: true }] }], messageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageKey", required: false }] }] } });
8788
9158
 
8789
9159
  /**
@@ -9562,11 +9932,11 @@ class FlyDrawerComponent {
9562
9932
  }
9563
9933
  }
9564
9934
  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 });
9935
+ 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
9936
  }
9567
9937
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDrawerComponent, decorators: [{
9568
9938
  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"] }]
9939
+ 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
9940
  }], 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
9941
  type: HostListener,
9572
9942
  args: ['document:keydown.escape']
@@ -12928,6 +13298,13 @@ const ROOT_DRAFT_KEY = '__root__';
12928
13298
  * eager load-all) — the explicit fix for the legacy unpaged-children scale bug
12929
13299
  * — and each level pages independently via `loadMore`.
12930
13300
  *
13301
+ * The `comments` core app owns no user profile data, so `FlyComment` carries
13302
+ * `authorUserId` and an OPTIONAL `authorDisplayName`. Rather than make every host
13303
+ * map names in itself (and render GUIDs when it forgets), bind
13304
+ * `[resolveDisplayName]` to `FlyUserDirectoryService.label` — see
13305
+ * {@link FlyCommentThreadComponent.resolveDisplayName}. Still transport-agnostic:
13306
+ * the resolver is a function the host hands over, not a call this component makes.
13307
+ *
12931
13308
  * i18n is self-sufficient through the `comment.*` keys in `DS_BASELINE_LOCALES`
12932
13309
  * (en/ar/fr/ur), overridable by any consumer key of the same name. RTL works via
12933
13310
  * logical CSS. Styling uses DS theme tokens (`--surface-*`, `--label-*`,
@@ -12940,6 +13317,7 @@ const ROOT_DRAFT_KEY = '__root__';
12940
13317
  * [rootPage]="rootPage()"
12941
13318
  * [repliesByParent]="repliesByParent()"
12942
13319
  * [currentUserId]="me.id"
13320
+ * [resolveDisplayName]="directory.label"
12943
13321
  * [canModerateThread]="canModerate()"
12944
13322
  * [isLocked]="thread().isLocked"
12945
13323
  * (loadReplies)="onLoadReplies($event)"
@@ -12976,6 +13354,23 @@ class FlyCommentThreadComponent {
12976
13354
  /** Thread-scoped moderator permission (lock/unlock). Additive — distinct from the
12977
13355
  * per-comment `canModerate` flag, which gates delete-any on that specific comment. */
12978
13356
  canModerateThread = input(false, ...(ngDevMode ? [{ debugName: "canModerateThread" }] : /* istanbul ignore next */ []));
13357
+ /**
13358
+ * Optional id→name lookup, for the common case where the backend DTO carries only
13359
+ * `authorUserId`. The `comments` core app owns no user profile data, so without
13360
+ * this every host had to map display names into `authorDisplayName` itself — and
13361
+ * a host that forgot rendered raw GUIDs at the reader.
13362
+ *
13363
+ * Bind `FlyUserDirectoryService.label` (a signal-backed read-through cache that
13364
+ * resolves on demand and batches). Because the component reads it during render,
13365
+ * the name appears by itself once the lookup lands — the host keeps a plain
13366
+ * page signal, with no `computed` derivation and no priming.
13367
+ *
13368
+ * The component still never calls an API: this is a function the HOST supplies,
13369
+ * exactly like `fly-entity-lookup`'s `LOOKUP_APP_NAME_RESOLVER`. Absent (or
13370
+ * returning `null`), rendering falls back as before. See {@link authorLabel} for
13371
+ * the resolution order.
13372
+ */
13373
+ resolveDisplayName = input(null, ...(ngDevMode ? [{ debugName: "resolveDisplayName" }] : /* istanbul ignore next */ []));
12979
13374
  // ── Outputs ──────────────────────────────────────────────────────────────────
12980
13375
  /** Expand a collapsed reply subtree — always page 1 of that parent. */
12981
13376
  loadReplies = output();
@@ -13036,6 +13431,25 @@ class FlyCommentThreadComponent {
13036
13431
  const me = this.currentUserId();
13037
13432
  return !!me && me === comment.authorUserId;
13038
13433
  }
13434
+ /**
13435
+ * The name to render for a comment's author. Resolution order:
13436
+ * explicit `authorDisplayName` (a host that already knows the name, or a
13437
+ * backend that returns one, always wins) → {@link resolveDisplayName} →
13438
+ * the raw `authorUserId`.
13439
+ *
13440
+ * The raw id remains the last resort rather than a placeholder: it is stable,
13441
+ * unique, and lets a reader correlate two comments by the same author even
13442
+ * when the directory is unreachable.
13443
+ */
13444
+ authorLabel(comment) {
13445
+ if (comment.authorDisplayName)
13446
+ return comment.authorDisplayName;
13447
+ return this.resolveDisplayName()?.(comment.authorUserId) || comment.authorUserId;
13448
+ }
13449
+ /** First character of {@link authorLabel}, for the avatar-initials fallback. */
13450
+ authorInitial(comment) {
13451
+ return this.authorLabel(comment).charAt(0);
13452
+ }
13039
13453
  canReport(comment) {
13040
13454
  return !this.isReadonly() && !comment.isDeleted && !this.isOwn(comment) && !!this.currentUserId();
13041
13455
  }
@@ -13206,16 +13620,16 @@ class FlyCommentThreadComponent {
13206
13620
  this._drafts.set(map);
13207
13621
  }
13208
13622
  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 });
13623
+ 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
13624
  }
13211
13625
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyCommentThreadComponent, decorators: [{
13212
13626
  type: Component,
13213
- args: [{ selector: 'fly-comment-thread', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
13627
+ args: [{ selector: 'fly-comment-thread', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe, FlyTooltipDirective], changeDetection: ChangeDetectionStrategy.OnPush, host: {
13214
13628
  class: 'fly-comment-thread',
13215
13629
  '[class.fly-comment-thread--locked]': 'isLocked()',
13216
13630
  '[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"] }] } });
13631
+ }, 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"] }]
13632
+ }], 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
13633
 
13220
13634
  // ── Vendored synchronous SHA-256 (FIPS 180-4) ────────────────────────────────
13221
13635
  // Why this exists: `fly-captcha`'s PoW solver must brute-force up to
@@ -15456,32 +15870,56 @@ function isChunkLoadError(err) {
15456
15870
  return looksLikeModuleUrl && looksLikeFetchStatus;
15457
15871
  }
15458
15872
  /**
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).
15873
+ * sessionStorage key recording WHEN this tab session last spent its chunk-error
15874
+ * reload (epoch ms). It persists across the reload it triggers (an in-memory
15875
+ * flag resets on reload and cannot guard a loop).
15462
15876
  */
15463
15877
  const FLY_CHUNK_RELOAD_FLAG = 'fly:chunk-reload-attempted';
15464
15878
  /**
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.
15879
+ * How long a spent self-heal attempt stays spent. Within the window a repeat
15880
+ * failure falls through to the caller's error UI (loop guard); after it the
15881
+ * heal re-arms. A permanent one-shot stranded long-lived tabs on environments
15882
+ * that redeploy repeatedly (e.g. a Helm upgrade→rollback→retry cycle flips the
15883
+ * served build several times in an afternoon — each flip needs its own reload,
15884
+ * but only the first ever got one). 10 min is far above any error→reload→error
15885
+ * loop period (seconds) while well below the gap between distinct redeploys.
15886
+ */
15887
+ const FLY_RELOAD_REARM_MS = 10 * 60 * 1000;
15888
+ /**
15889
+ * Self-heal for a stale client after a redeploy. Permits ONE reload per
15890
+ * {@link FLY_RELOAD_REARM_MS} window per tab session, so a reload that keeps
15891
+ * failing (a chunk truly missing from the current build, or a genuinely-down
15892
+ * host) does NOT loop — but a LATER redeploy re-arms it.
15468
15893
  *
15469
15894
  * @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
15895
+ * `false` when the window's reload was already spent or sessionStorage is
15471
15896
  * unavailable — callers should surface a fallback / stop rather than spin.
15472
15897
  */
15473
15898
  function reloadOnceForChunkError() {
15899
+ if (!trySpendHealAttempt(FLY_CHUNK_RELOAD_FLAG))
15900
+ return false;
15901
+ window.location.reload();
15902
+ return true;
15903
+ }
15904
+ /**
15905
+ * Records a heal attempt under `key` unless one was already recorded within the
15906
+ * last {@link FLY_RELOAD_REARM_MS}. A legacy `'1'` value (pre-timestamp builds)
15907
+ * parses to epoch 1 and therefore counts as expired — one extra reload on
15908
+ * upgrade, never a loop. Returns `false` on sessionStorage failure (private
15909
+ * mode / quota / non-browser): cannot record the attempt, so cannot prove a
15910
+ * reload wouldn't loop — prefer the caller's fallback over a spin.
15911
+ */
15912
+ function trySpendHealAttempt(key) {
15474
15913
  try {
15475
- if (sessionStorage.getItem(FLY_CHUNK_RELOAD_FLAG))
15914
+ const last = Number(sessionStorage.getItem(key));
15915
+ if (Number.isFinite(last) && last > 0 && Date.now() - last < FLY_RELOAD_REARM_MS) {
15476
15916
  return false;
15477
- sessionStorage.setItem(FLY_CHUNK_RELOAD_FLAG, '1');
15917
+ }
15918
+ sessionStorage.setItem(key, String(Date.now()));
15478
15919
  }
15479
15920
  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
15921
  return false;
15483
15922
  }
15484
- window.location.reload();
15485
15923
  return true;
15486
15924
  }
15487
15925
  /**
@@ -15508,10 +15946,11 @@ function isNativeFederationCacheError(err) {
15508
15946
  return /Unable to resolve specifier ['"]?@nf-internal\/chunk-/i.test(msg);
15509
15947
  }
15510
15948
  /**
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.
15949
+ * sessionStorage key recording WHEN this tab session last spent its
15950
+ * Native-Federation cache-heal (revalidation sweep + reload; epoch ms)
15951
+ * separate from {@link FLY_CHUNK_RELOAD_FLAG} so a plain chunk-404 reload and a
15952
+ * shared-bundle revalidation sweep each get one independent attempt per
15953
+ * {@link FLY_RELOAD_REARM_MS} window.
15515
15954
  */
15516
15955
  const FLY_NF_CACHE_HEAL_FLAG = 'nf-cache-heal-attempted';
15517
15956
  /**
@@ -15530,14 +15969,8 @@ const FLY_NF_CACHE_HEAL_FLAG = 'nf-cache-heal-attempted';
15530
15969
  * sessionStorage is unavailable (caller should surface a fallback).
15531
15970
  */
15532
15971
  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 {
15972
+ if (!trySpendHealAttempt(FLY_NF_CACHE_HEAL_FLAG))
15539
15973
  return false;
15540
- }
15541
15974
  const reload = () => {
15542
15975
  try {
15543
15976
  window.location.reload();
@@ -15964,6 +16397,13 @@ class FlyModerationQueueComponent {
15964
16397
  error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
15965
16398
  /** The currently applied status filter; `null` = "All". Host-controlled, like `[sort]` on `fly-comment-thread`. */
15966
16399
  status = input(null, ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
16400
+ /**
16401
+ * Optional id→name lookup for the reporter column, mirroring
16402
+ * `fly-comment-thread`'s input of the same name. Bind
16403
+ * `FlyUserDirectoryService.label`; absent, rows fall back to the raw id.
16404
+ * See {@link reporterLabel} for the resolution order.
16405
+ */
16406
+ resolveDisplayName = input(null, ...(ngDevMode ? [{ debugName: "resolveDisplayName" }] : /* istanbul ignore next */ []));
15967
16407
  // ── Outputs ──────────────────────────────────────────────────────────────────
15968
16408
  /** A status-filter pick (page resets to 1) or a `loadMore` (next page at the current filter). */
15969
16409
  loadPage = output();
@@ -16008,6 +16448,16 @@ class FlyModerationQueueComponent {
16008
16448
  subjectDisplay(r) {
16009
16449
  return r.subjectLabel || `${r.subjectType} #${this.shortId(r.subjectId)}`;
16010
16450
  }
16451
+ /**
16452
+ * Who filed the report. Resolution order: explicit `reporterDisplayName` →
16453
+ * {@link resolveDisplayName} → the raw `reporterUserId`. Same contract as
16454
+ * `fly-comment-thread`'s `authorLabel`.
16455
+ */
16456
+ reporterLabel(r) {
16457
+ if (r.reporterDisplayName)
16458
+ return r.reporterDisplayName;
16459
+ return this.resolveDisplayName()?.(r.reporterUserId) || r.reporterUserId;
16460
+ }
16011
16461
  formatDate(iso) {
16012
16462
  const d = new Date(iso);
16013
16463
  return Number.isNaN(d.getTime()) ? '' : d.toLocaleString();
@@ -16124,12 +16574,12 @@ class FlyModerationQueueComponent {
16124
16574
  btn?.focus({ preventScroll: true });
16125
16575
  }
16126
16576
  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 });
16577
+ 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
16578
  }
16129
16579
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyModerationQueueComponent, decorators: [{
16130
16580
  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: [{
16581
+ 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"] }]
16582
+ }], 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
16583
  type: HostListener,
16134
16584
  args: ['document:keydown.escape']
16135
16585
  }] } });
@@ -16279,11 +16729,11 @@ class FlyPeoplePickerComponent {
16279
16729
  this.selectionChange.emit(this._selected().map((o) => o.id));
16280
16730
  }
16281
16731
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyPeoplePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16282
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyPeoplePickerComponent, isStandalone: true, selector: "fly-people-picker", inputs: { searchFn: { classPropertyName: "searchFn", publicName: "searchFn", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, excludeIds: { classPropertyName: "excludeIds", publicName: "excludeIds", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { classAttribute: "fly-people-picker" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: FlyTypeaheadComponent, descendants: true }], ngImport: i0, template: "@if (selected().length > 0) {\r\n <ul class=\"fly-people-picker__chips\" role=\"list\">\r\n @for (o of selected(); track o.id) {\r\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\r\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\r\n @if (o.avatarUrl) {\r\n <img [src]=\"o.avatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-people-picker__chip-remove\"\r\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\r\n (click)=\"remove(o.id)\">\r\n \u2715\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n@if (showSearch()) {\r\n <fly-typeahead\r\n [searchFn]=\"typeaheadSearchFn\"\r\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\r\n [allowFreeText]=\"false\"\r\n (selected)=\"onPicked($event)\" />\r\n} @else {\r\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\r\n {{ 'people_picker.change' | translate }}\r\n </button>\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));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: FlyTypeaheadComponent, selector: "fly-typeahead", inputs: ["searchFn", "debounceMs", "placeholder", "ariaLabel", "value", "allowFreeText", "openOnFocus"], outputs: ["valueChange", "selected", "cleared"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16732
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyPeoplePickerComponent, isStandalone: true, selector: "fly-people-picker", inputs: { searchFn: { classPropertyName: "searchFn", publicName: "searchFn", isSignal: true, isRequired: true, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, excludeIds: { classPropertyName: "excludeIds", publicName: "excludeIds", isSignal: true, isRequired: false, transformFunction: null }, initialSelection: { classPropertyName: "initialSelection", publicName: "initialSelection", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, host: { classAttribute: "fly-people-picker" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: FlyTypeaheadComponent, descendants: true }], ngImport: i0, template: "@if (selected().length > 0) {\n <ul class=\"fly-people-picker__chips\" role=\"list\">\n @for (o of selected(); track o.id) {\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\n @if (o.avatarUrl) {\n <img [src]=\"o.avatarUrl\" alt=\"\" />\n } @else {\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\n }\n </span>\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\n <button\n type=\"button\"\n class=\"fly-people-picker__chip-remove\"\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\n (click)=\"remove(o.id)\">\n \u2715\n </button>\n </li>\n }\n </ul>\n}\n\n@if (showSearch()) {\n <fly-typeahead\n [searchFn]=\"typeaheadSearchFn\"\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\n [allowFreeText]=\"false\"\n (selected)=\"onPicked($event)\" />\n} @else {\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\n {{ 'people_picker.change' | translate }}\n </button>\n}\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));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: FlyTypeaheadComponent, selector: "fly-typeahead", inputs: ["searchFn", "debounceMs", "placeholder", "ariaLabel", "value", "allowFreeText", "openOnFocus"], outputs: ["valueChange", "selected", "cleared"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16283
16733
  }
16284
16734
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyPeoplePickerComponent, decorators: [{
16285
16735
  type: Component,
16286
- args: [{ selector: 'fly-people-picker', standalone: true, imports: [CommonModule, TranslatePipe, FlyTypeaheadComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-people-picker' }, template: "@if (selected().length > 0) {\r\n <ul class=\"fly-people-picker__chips\" role=\"list\">\r\n @for (o of selected(); track o.id) {\r\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\r\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\r\n @if (o.avatarUrl) {\r\n <img [src]=\"o.avatarUrl\" alt=\"\" />\r\n } @else {\r\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\r\n }\r\n </span>\r\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fly-people-picker__chip-remove\"\r\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\r\n (click)=\"remove(o.id)\">\r\n \u2715\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n\r\n@if (showSearch()) {\r\n <fly-typeahead\r\n [searchFn]=\"typeaheadSearchFn\"\r\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\r\n [allowFreeText]=\"false\"\r\n (selected)=\"onPicked($event)\" />\r\n} @else {\r\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\r\n {{ 'people_picker.change' | translate }}\r\n </button>\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));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"] }]
16736
+ args: [{ selector: 'fly-people-picker', standalone: true, imports: [CommonModule, TranslatePipe, FlyTypeaheadComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'fly-people-picker' }, template: "@if (selected().length > 0) {\n <ul class=\"fly-people-picker__chips\" role=\"list\">\n @for (o of selected(); track o.id) {\n <li class=\"fly-people-picker__chip\" role=\"listitem\">\n <span class=\"fly-people-picker__chip-avatar\" aria-hidden=\"true\">\n @if (o.avatarUrl) {\n <img [src]=\"o.avatarUrl\" alt=\"\" />\n } @else {\n <span class=\"fly-people-picker__chip-initials\">{{ initialsFor(o.displayName) }}</span>\n }\n </span>\n <span class=\"fly-people-picker__chip-name\" [title]=\"o.email || o.displayName\">{{ o.displayName }}</span>\n <button\n type=\"button\"\n class=\"fly-people-picker__chip-remove\"\n [attr.aria-label]=\"'people_picker.remove' | translate: { name: o.displayName }\"\n (click)=\"remove(o.id)\">\n \u2715\n </button>\n </li>\n }\n </ul>\n}\n\n@if (showSearch()) {\n <fly-typeahead\n [searchFn]=\"typeaheadSearchFn\"\n [placeholder]=\"placeholder() || ('people_picker.search_placeholder' | translate)\"\n [allowFreeText]=\"false\"\n (selected)=\"onPicked($event)\" />\n} @else {\n <button type=\"button\" class=\"fly-people-picker__change-btn\" (click)=\"change()\">\n {{ 'people_picker.change' | translate }}\n </button>\n}\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));--_fill: var(--fill-tertiary, #f3f4f6);--_accent: var(--accent);--_danger: var(--system-red, #ef4444);--_radius: 999px;display:flex;flex-direction:column;gap:8px;inline-size:100%;color:var(--_text);font-size:13px}.fly-people-picker__chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:6px}.fly-people-picker__chip{display:inline-flex;align-items:center;gap:6px;padding-block:3px;padding-inline:3px 6px;border-radius:var(--_radius);background:var(--_fill);max-inline-size:220px}.fly-people-picker__chip-avatar{inline-size:22px;block-size:22px;border-radius:50%;overflow:hidden;flex:none;background:var(--_surface);display:flex;align-items:center;justify-content:center}.fly-people-picker__chip-avatar img{inline-size:100%;block-size:100%;object-fit:cover}.fly-people-picker__chip-initials{font-size:9px;font-weight:700;color:var(--_text-subtle);text-transform:uppercase}.fly-people-picker__chip-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fly-people-picker__chip-remove{border:none;background:transparent;padding:2px;margin-inline-start:2px;border-radius:50%;color:var(--_text-subtle);font-size:10px;line-height:1;cursor:pointer;flex:none}.fly-people-picker__chip-remove:hover{background:color-mix(in srgb,var(--_danger) 18%,transparent);color:var(--_danger)}.fly-people-picker__change-btn{align-self:flex-start;border:1px solid var(--_border);border-radius:8px;background:var(--_surface);padding:5px 12px;color:var(--_text);font:inherit;font-weight:600;cursor:pointer}.fly-people-picker__change-btn:hover{background:var(--_surface-hover)}\n"] }]
16287
16737
  }], ctorParameters: () => [], propDecorators: { searchFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchFn", required: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], excludeIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "excludeIds", required: false }] }], initialSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialSelection", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], typeahead: [{
16288
16738
  type: ViewChild,
16289
16739
  args: [FlyTypeaheadComponent]
@@ -18547,30 +18997,30 @@ class FlyDetailCardComponent {
18547
18997
  /** Drop the body padding — for a child that brings its own list/table chrome. */
18548
18998
  flush = input(false, ...(ngDevMode ? [{ debugName: "flush" }] : /* istanbul ignore next */ []));
18549
18999
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18550
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailCardComponent, isStandalone: true, selector: "fly-detail-card", inputs: { titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, hasProjectedTitle: { classPropertyName: "hasProjectedTitle", publicName: "hasProjectedTitle", isSignal: true, isRequired: false, transformFunction: null }, flush: { classPropertyName: "flush", publicName: "flush", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
18551
- @if (titleKey() || hasProjectedTitle()) {
18552
- <fly-section-header [titleKey]="titleKey()">
18553
- <ng-content select="[card-title]" ngProjectAs="[section-title]" />
18554
- <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
18555
- </fly-section-header>
18556
- }
18557
- <div class="dc__body" [class.dc__body--flush]="flush()">
18558
- <ng-content />
18559
- </div>
19000
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailCardComponent, isStandalone: true, selector: "fly-detail-card", inputs: { titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, hasProjectedTitle: { classPropertyName: "hasProjectedTitle", publicName: "hasProjectedTitle", isSignal: true, isRequired: false, transformFunction: null }, flush: { classPropertyName: "flush", publicName: "flush", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
19001
+ @if (titleKey() || hasProjectedTitle()) {
19002
+ <fly-section-header [titleKey]="titleKey()">
19003
+ <ng-content select="[card-title]" ngProjectAs="[section-title]" />
19004
+ <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
19005
+ </fly-section-header>
19006
+ }
19007
+ <div class="dc__body" [class.dc__body--flush]="flush()">
19008
+ <ng-content />
19009
+ </div>
18560
19010
  `, isInline: true, styles: [":host{display:block;background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow-card);overflow:hidden}.dc__body{padding:var(--sp-3) var(--sp-4) var(--sp-4)}.dc__body--flush{padding:0}\n"], dependencies: [{ kind: "component", type: FlySectionHeaderComponent, selector: "fly-section-header", inputs: ["titleKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18561
19011
  }
18562
19012
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailCardComponent, decorators: [{
18563
19013
  type: Component,
18564
- args: [{ selector: 'fly-detail-card', standalone: true, imports: [FlySectionHeaderComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
18565
- @if (titleKey() || hasProjectedTitle()) {
18566
- <fly-section-header [titleKey]="titleKey()">
18567
- <ng-content select="[card-title]" ngProjectAs="[section-title]" />
18568
- <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
18569
- </fly-section-header>
18570
- }
18571
- <div class="dc__body" [class.dc__body--flush]="flush()">
18572
- <ng-content />
18573
- </div>
19014
+ args: [{ selector: 'fly-detail-card', standalone: true, imports: [FlySectionHeaderComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
19015
+ @if (titleKey() || hasProjectedTitle()) {
19016
+ <fly-section-header [titleKey]="titleKey()">
19017
+ <ng-content select="[card-title]" ngProjectAs="[section-title]" />
19018
+ <ng-content select="[card-actions]" ngProjectAs="[section-actions]" />
19019
+ </fly-section-header>
19020
+ }
19021
+ <div class="dc__body" [class.dc__body--flush]="flush()">
19022
+ <ng-content />
19023
+ </div>
18574
19024
  `, styles: [":host{display:block;background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow-card);overflow:hidden}.dc__body{padding:var(--sp-3) var(--sp-4) var(--sp-4)}.dc__body--flush{padding:0}\n"] }]
18575
19025
  }], propDecorators: { titleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleKey", required: false }] }], hasProjectedTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasProjectedTitle", required: false }] }], flush: [{ type: i0.Input, args: [{ isSignal: true, alias: "flush", required: false }] }] } });
18576
19026
 
@@ -18698,108 +19148,108 @@ class FlyDetailShellComponent {
18698
19148
  buttons?.[next]?.focus();
18699
19149
  }
18700
19150
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18701
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailShellComponent, isStandalone: true, selector: "fly-detail-shell", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, sectionsLabelKey: { classPropertyName: "sectionsLabelKey", publicName: "sectionsLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, viewQueries: [{ propertyName: "rail", first: true, predicate: ["rail"], descendants: true, isSignal: true }], ngImport: i0, template: `
18702
- <div class="ds__layout">
18703
- <aside class="ds__aside">
18704
- <div class="ds__pinned">
18705
- <ng-content select="[detail-aside]" />
18706
- </div>
18707
-
18708
- @if (sections().length > 0) {
18709
- <div
18710
- #rail
18711
- class="ds__rail"
18712
- role="tablist"
18713
- tabindex="-1"
18714
- [attr.aria-orientation]="'vertical'"
18715
- [attr.aria-label]="sectionsLabelKey() | translate"
18716
- (keydown)="onKey($event)"
18717
- >
18718
- @for (s of sections(); track s.id) {
18719
- <button
18720
- type="button"
18721
- class="ds__tab"
18722
- role="tab"
18723
- [id]="tabId(s.id)"
18724
- [class.ds__tab--active]="activeId() === s.id"
18725
- [attr.aria-selected]="activeId() === s.id"
18726
- [attr.aria-controls]="panelId()"
18727
- [attr.tabindex]="activeId() === s.id ? 0 : -1"
18728
- [attr.title]="s.labelKey | translate"
18729
- (click)="select(s.id)"
18730
- >
18731
- @if (s.icon) {
18732
- <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
18733
- }
18734
- <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
18735
- </button>
18736
- }
18737
- </div>
18738
- }
18739
- </aside>
18740
-
18741
- <div
18742
- class="ds__panel"
18743
- [id]="panelId()"
18744
- [attr.role]="sections().length ? 'tabpanel' : null"
18745
- [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
18746
- >
18747
- <ng-content />
18748
- </div>
18749
- </div>
19151
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: FlyDetailShellComponent, isStandalone: true, selector: "fly-detail-shell", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, sectionsLabelKey: { classPropertyName: "sectionsLabelKey", publicName: "sectionsLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selectedChange" }, viewQueries: [{ propertyName: "rail", first: true, predicate: ["rail"], descendants: true, isSignal: true }], ngImport: i0, template: `
19152
+ <div class="ds__layout">
19153
+ <aside class="ds__aside">
19154
+ <div class="ds__pinned">
19155
+ <ng-content select="[detail-aside]" />
19156
+ </div>
19157
+
19158
+ @if (sections().length > 0) {
19159
+ <div
19160
+ #rail
19161
+ class="ds__rail"
19162
+ role="tablist"
19163
+ tabindex="-1"
19164
+ [attr.aria-orientation]="'vertical'"
19165
+ [attr.aria-label]="sectionsLabelKey() | translate"
19166
+ (keydown)="onKey($event)"
19167
+ >
19168
+ @for (s of sections(); track s.id) {
19169
+ <button
19170
+ type="button"
19171
+ class="ds__tab"
19172
+ role="tab"
19173
+ [id]="tabId(s.id)"
19174
+ [class.ds__tab--active]="activeId() === s.id"
19175
+ [attr.aria-selected]="activeId() === s.id"
19176
+ [attr.aria-controls]="panelId()"
19177
+ [attr.tabindex]="activeId() === s.id ? 0 : -1"
19178
+ [attr.title]="s.labelKey | translate"
19179
+ (click)="select(s.id)"
19180
+ >
19181
+ @if (s.icon) {
19182
+ <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
19183
+ }
19184
+ <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
19185
+ </button>
19186
+ }
19187
+ </div>
19188
+ }
19189
+ </aside>
19190
+
19191
+ <div
19192
+ class="ds__panel"
19193
+ [id]="panelId()"
19194
+ [attr.role]="sections().length ? 'tabpanel' : null"
19195
+ [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
19196
+ >
19197
+ <ng-content />
19198
+ </div>
19199
+ </div>
18750
19200
  `, isInline: true, styles: [":host{display:block;container-type:inline-size}.ds__layout{display:grid;grid-template-columns:var(--fly-detail-aside, 300px) minmax(0,1fr);gap:var(--sp-4);align-items:start}.ds__aside{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__aside .ds__rail{order:-1}.ds__pinned{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__panel{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0;align-self:stretch}.ds__panel>:only-child{flex:1}.ds__rail{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-2);background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow-card)}.ds__tab{display:flex;align-items:center;gap:var(--sp-3);width:100%;padding:var(--sp-2);border:0;background:transparent;border-radius:var(--r-md);color:var(--ink-2);cursor:pointer;font-family:inherit;font-size:var(--text-sm);text-align:start;white-space:nowrap;overflow:hidden;transition:background var(--t-state),color var(--t-state)}.ds__tab:hover{background:var(--bg-hover);color:var(--ink)}.ds__tab:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ds__tab--active{background:var(--accent-fill);color:var(--on-accent-fill);font-weight:var(--fw-semibold)}.ds__tab-ico{font-size:var(--text-md);flex-shrink:0;width:20px;text-align:center}.ds__tab-label{min-width:0;overflow:hidden;text-overflow:ellipsis}@container (max-width: 980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-direction:row;flex-wrap:wrap}.ds__tab{width:auto}}@media(max-width:980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-direction:row;flex-wrap:wrap}.ds__tab{width:auto}}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18751
19201
  }
18752
19202
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyDetailShellComponent, decorators: [{
18753
19203
  type: Component,
18754
- args: [{ selector: 'fly-detail-shell', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
18755
- <div class="ds__layout">
18756
- <aside class="ds__aside">
18757
- <div class="ds__pinned">
18758
- <ng-content select="[detail-aside]" />
18759
- </div>
18760
-
18761
- @if (sections().length > 0) {
18762
- <div
18763
- #rail
18764
- class="ds__rail"
18765
- role="tablist"
18766
- tabindex="-1"
18767
- [attr.aria-orientation]="'vertical'"
18768
- [attr.aria-label]="sectionsLabelKey() | translate"
18769
- (keydown)="onKey($event)"
18770
- >
18771
- @for (s of sections(); track s.id) {
18772
- <button
18773
- type="button"
18774
- class="ds__tab"
18775
- role="tab"
18776
- [id]="tabId(s.id)"
18777
- [class.ds__tab--active]="activeId() === s.id"
18778
- [attr.aria-selected]="activeId() === s.id"
18779
- [attr.aria-controls]="panelId()"
18780
- [attr.tabindex]="activeId() === s.id ? 0 : -1"
18781
- [attr.title]="s.labelKey | translate"
18782
- (click)="select(s.id)"
18783
- >
18784
- @if (s.icon) {
18785
- <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
18786
- }
18787
- <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
18788
- </button>
18789
- }
18790
- </div>
18791
- }
18792
- </aside>
18793
-
18794
- <div
18795
- class="ds__panel"
18796
- [id]="panelId()"
18797
- [attr.role]="sections().length ? 'tabpanel' : null"
18798
- [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
18799
- >
18800
- <ng-content />
18801
- </div>
18802
- </div>
19204
+ args: [{ selector: 'fly-detail-shell', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
19205
+ <div class="ds__layout">
19206
+ <aside class="ds__aside">
19207
+ <div class="ds__pinned">
19208
+ <ng-content select="[detail-aside]" />
19209
+ </div>
19210
+
19211
+ @if (sections().length > 0) {
19212
+ <div
19213
+ #rail
19214
+ class="ds__rail"
19215
+ role="tablist"
19216
+ tabindex="-1"
19217
+ [attr.aria-orientation]="'vertical'"
19218
+ [attr.aria-label]="sectionsLabelKey() | translate"
19219
+ (keydown)="onKey($event)"
19220
+ >
19221
+ @for (s of sections(); track s.id) {
19222
+ <button
19223
+ type="button"
19224
+ class="ds__tab"
19225
+ role="tab"
19226
+ [id]="tabId(s.id)"
19227
+ [class.ds__tab--active]="activeId() === s.id"
19228
+ [attr.aria-selected]="activeId() === s.id"
19229
+ [attr.aria-controls]="panelId()"
19230
+ [attr.tabindex]="activeId() === s.id ? 0 : -1"
19231
+ [attr.title]="s.labelKey | translate"
19232
+ (click)="select(s.id)"
19233
+ >
19234
+ @if (s.icon) {
19235
+ <span class="pi {{ s.icon }} ds__tab-ico" aria-hidden="true"></span>
19236
+ }
19237
+ <span class="ds__tab-label">{{ s.labelKey | translate }}</span>
19238
+ </button>
19239
+ }
19240
+ </div>
19241
+ }
19242
+ </aside>
19243
+
19244
+ <div
19245
+ class="ds__panel"
19246
+ [id]="panelId()"
19247
+ [attr.role]="sections().length ? 'tabpanel' : null"
19248
+ [attr.aria-labelledby]="activeId() ? tabId(activeId()!) : null"
19249
+ >
19250
+ <ng-content />
19251
+ </div>
19252
+ </div>
18803
19253
  `, styles: [":host{display:block;container-type:inline-size}.ds__layout{display:grid;grid-template-columns:var(--fly-detail-aside, 300px) minmax(0,1fr);gap:var(--sp-4);align-items:start}.ds__aside{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__aside .ds__rail{order:-1}.ds__pinned{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0}.ds__panel{display:flex;flex-direction:column;gap:var(--sp-4);min-width:0;align-self:stretch}.ds__panel>:only-child{flex:1}.ds__rail{display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-2);background:var(--bg-2);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow-card)}.ds__tab{display:flex;align-items:center;gap:var(--sp-3);width:100%;padding:var(--sp-2);border:0;background:transparent;border-radius:var(--r-md);color:var(--ink-2);cursor:pointer;font-family:inherit;font-size:var(--text-sm);text-align:start;white-space:nowrap;overflow:hidden;transition:background var(--t-state),color var(--t-state)}.ds__tab:hover{background:var(--bg-hover);color:var(--ink)}.ds__tab:focus-visible{outline:2px solid var(--focus-ring);outline-offset:2px}.ds__tab--active{background:var(--accent-fill);color:var(--on-accent-fill);font-weight:var(--fw-semibold)}.ds__tab-ico{font-size:var(--text-md);flex-shrink:0;width:20px;text-align:center}.ds__tab-label{min-width:0;overflow:hidden;text-overflow:ellipsis}@container (max-width: 980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-direction:row;flex-wrap:wrap}.ds__tab{width:auto}}@media(max-width:980px){.ds__layout{grid-template-columns:minmax(0,1fr)}.ds__rail{flex-direction:row;flex-wrap:wrap}.ds__tab{width:auto}}\n"] }]
18804
19254
  }], propDecorators: { rail: [{ type: i0.ViewChild, args: ['rail', { isSignal: true }] }], sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], sectionsLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "sectionsLabelKey", required: false }] }] } });
18805
19255
 
@@ -19164,11 +19614,11 @@ class FlyAppHomeComponent {
19164
19614
  return `fly-app-home-sec-${index}`;
19165
19615
  }
19166
19616
  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 });
19617
+ 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
19618
  }
19169
19619
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyAppHomeComponent, decorators: [{
19170
19620
  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"] }]
19621
+ 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
19622
  }], 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
19623
 
19174
19624
  /**
@@ -20134,7 +20584,7 @@ class FlyFormSectionComponent {
20134
20584
  }
20135
20585
  </div>
20136
20586
  <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 });
20587
+ `, 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
20588
  }
20139
20589
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: FlyFormSectionComponent, decorators: [{
20140
20590
  type: Component,
@@ -20145,7 +20595,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImpo
20145
20595
  }
20146
20596
  </div>
20147
20597
  <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"] }]
20598
+ `, 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
20599
  }], propDecorators: { labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }] } });
20150
20600
  /**
20151
20601
  * Responsive field grid (port of the legacy `.dr__grid/--2/--3`): 1–3 equal
@@ -21399,5 +21849,5 @@ const AUDIENCE_ERROR_CODES = {
21399
21849
  * Generated bundle index. Do not edit.
21400
21850
  */
21401
21851
 
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 };
21852
+ 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
21853
  //# sourceMappingURL=flyos-design-system.mjs.map