@mohamedatia/fly-design-system 2.3.2 → 2.6.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,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, Output, Input, forwardRef, Injector, viewChild, effect, afterNextRender, ViewEncapsulation, model } from '@angular/core';
2
+ import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, Output, Input, forwardRef, Injector, viewChild, effect, afterNextRender, ViewEncapsulation, model, Directive } from '@angular/core';
3
3
  import * as i1$1 from '@angular/common';
4
- import { isPlatformBrowser, CommonModule } from '@angular/common';
5
- import { Router } from '@angular/router';
4
+ import { isPlatformBrowser, CommonModule, DOCUMENT as DOCUMENT$1 } from '@angular/common';
5
+ import { Router, NavigationEnd } from '@angular/router';
6
6
  import { of, ReplaySubject, Subject } from 'rxjs';
7
7
  import * as i1 from '@angular/forms';
8
8
  import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
@@ -29,6 +29,22 @@ const WINDOW_DATA = new InjectionToken('WINDOW_DATA');
29
29
  */
30
30
  const LAUNCH_CONTEXT = new InjectionToken('LAUNCH_CONTEXT');
31
31
 
32
+ /**
33
+ * Generic share / ACL panel models — hosts map domain DTOs to these shapes.
34
+ *
35
+ * v2.0.0: introduces the {@link SharePrincipal} discriminated union to replace the v1
36
+ * "set-which-is-set" discriminator on {@link SharePermissionEntry} (`grantedToUserId` /
37
+ * `grantedToOuId` / `grantedToAppId`). The new shape compiles invalid combinations away
38
+ * (a permission row carries exactly one principal, never overlapping ids) and reserves
39
+ * a `role` kind for the planned apps-chart role-as-principal flow.
40
+ */
41
+ /**
42
+ * Stable identifiers for platform-managed system charts. Hosts that don't recognise the
43
+ * value just fall through to the default selection — these constants are advisory.
44
+ */
45
+ const SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT = 'default-company';
46
+ const SHARE_ORG_CHART_SYSTEM_KEY_APPS = 'apps';
47
+
32
48
  /** Discriminator string values — re-exported so hosts can build kind menus dynamically. */
33
49
  const AUDIENCE_TERM_KINDS = [
34
50
  'users',
@@ -359,6 +375,152 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
359
375
  type: Injectable
360
376
  }] });
361
377
 
378
+ /**
379
+ * FlyOS standard navigation surface for Business / Supporting App remotes.
380
+ *
381
+ * Problem this solves
382
+ * -------------------
383
+ * A federated remote ships with its own Angular `Routes` table that works
384
+ * perfectly in **standalone** mode (e.g. `http://localhost:7202/signals/:id`).
385
+ * Once the same remote is mounted inside the FlyOS desktop shell via
386
+ * `*ngComponentOutlet`, `inject(Router)` resolves to the **host shell's**
387
+ * Router — which has no knowledge of `/signals/:id`. Calls like
388
+ * `router.navigate(['/signals', id])` then silently no-op and the click
389
+ * "does nothing."
390
+ *
391
+ * The fix
392
+ * -------
393
+ * Inject `FlyRemoteRouter` instead of `Router`. It mirrors `Router.navigate` /
394
+ * `Router.navigateByUrl` and:
395
+ *
396
+ * - **Standalone** (no `WINDOW_DATA`): delegates to the real Angular Router,
397
+ * so the URL bar, browser back button, and `RouterLink` continue to work
398
+ * exactly as before.
399
+ * - **Embedded** (mounted in the shell via `WINDOW_DATA`): writes the target
400
+ * URL to an internal signal (`url`, `segments`) and does **not** touch the
401
+ * host's Router. The remote's root component watches the signal and renders
402
+ * the right view (typically a `@switch` on the first segment, plus an inline
403
+ * overlay for detail pages).
404
+ *
405
+ * Usage in a list page
406
+ * --------------------
407
+ * ```ts
408
+ * private readonly router = inject(FlyRemoteRouter);
409
+ *
410
+ * openDetail(item: Signal): void {
411
+ * this.router.navigate(['/signals', item.id]); // works in both modes
412
+ * }
413
+ * ```
414
+ *
415
+ * Usage in the remote's root component (embedded dispatcher)
416
+ * ----------------------------------------------------------
417
+ * ```ts
418
+ * private readonly router = inject(FlyRemoteRouter);
419
+ *
420
+ * readonly view = computed(() => {
421
+ * if (!this.router.isEmbedded) return null; // standalone: <router-outlet> handles it
422
+ * const [head, id] = this.router.segments();
423
+ * if (head === 'signals' && id) return { kind: 'signal-detail', id };
424
+ * if (head === 'signals') return { kind: 'signals-list' };
425
+ * return { kind: head ?? 'home' };
426
+ * });
427
+ * ```
428
+ *
429
+ * Notes
430
+ * -----
431
+ * - `WINDOW_DATA` is the canonical "I am running inside the shell" marker; the
432
+ * shell provides it per window, and it is `null` everywhere else.
433
+ * - `Router` is injected as `optional`. A remote that does not register a Router
434
+ * (e.g. a tiny chromeless utility app) still gets a working `navigate` that
435
+ * updates the `url` signal in embedded mode.
436
+ */
437
+ class FlyRemoteRouter {
438
+ windowData = inject(WINDOW_DATA, { optional: true });
439
+ router = inject(Router, { optional: true });
440
+ /** True when this remote is rendered inside the FlyOS desktop shell. */
441
+ isEmbedded = this.windowData != null;
442
+ _url = signal('/', ...(ngDevMode ? [{ debugName: "_url" }] : /* istanbul ignore next */ []));
443
+ /**
444
+ * Current logical URL of the remote — `/signals/abc` etc.
445
+ *
446
+ * Standalone: mirrors `Router.url` and is updated on every `NavigationEnd`.
447
+ * Embedded: updated by `navigate` / `navigateByUrl` only.
448
+ */
449
+ url = this._url.asReadonly();
450
+ /**
451
+ * `url` parsed into path segments, e.g. `'/signals/abc'` → `['signals', 'abc']`.
452
+ * Query string and hash are stripped. Empty segments removed.
453
+ */
454
+ segments = computed(() => {
455
+ const u = this._url();
456
+ return u.split('?')[0].split('#')[0].split('/').filter(Boolean);
457
+ }, ...(ngDevMode ? [{ debugName: "segments" }] : /* istanbul ignore next */ []));
458
+ constructor() {
459
+ if (!this.isEmbedded && this.router) {
460
+ // Seed with the current standalone URL so consumers can read `segments()`
461
+ // synchronously during initial render, before any NavigationEnd fires.
462
+ this._url.set(this.router.url);
463
+ this.router.events.subscribe(event => {
464
+ if (event instanceof NavigationEnd) {
465
+ this._url.set(event.urlAfterRedirects);
466
+ }
467
+ });
468
+ }
469
+ }
470
+ /**
471
+ * Navigate to a route, identified by an array of commands as you would pass
472
+ * to Angular's `Router.navigate`. In embedded mode `extras` is ignored — the
473
+ * shell owns history and query-param handling for the window's URL.
474
+ */
475
+ navigate(commands, extras) {
476
+ if (!this.isEmbedded && this.router) {
477
+ return this.router.navigate(commands, extras);
478
+ }
479
+ this._url.set(this.buildUrl(commands));
480
+ return Promise.resolve(true);
481
+ }
482
+ /**
483
+ * Navigate to a route given as a full URL string. Mirrors `Router.navigateByUrl`.
484
+ */
485
+ navigateByUrl(url, extras) {
486
+ if (!this.isEmbedded && this.router) {
487
+ return this.router.navigateByUrl(url, extras);
488
+ }
489
+ this._url.set(url.startsWith('/') ? url : '/' + url);
490
+ return Promise.resolve(true);
491
+ }
492
+ /**
493
+ * Convenience: pop the last two segments (the typical "go back from detail
494
+ * to list" gesture). In standalone mode this falls through to `history.back()`
495
+ * so the browser's back stack is respected.
496
+ */
497
+ back() {
498
+ if (!this.isEmbedded) {
499
+ if (typeof history !== 'undefined')
500
+ history.back();
501
+ return;
502
+ }
503
+ const segs = [...this.segments()];
504
+ if (segs.length === 0)
505
+ return;
506
+ segs.pop();
507
+ this._url.set('/' + segs.join('/'));
508
+ }
509
+ buildUrl(commands) {
510
+ const parts = commands
511
+ .map(c => (c == null ? '' : String(c)))
512
+ .map(s => s.replace(/^\/+|\/+$/g, ''))
513
+ .filter(Boolean);
514
+ return '/' + parts.join('/');
515
+ }
516
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyRemoteRouter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
517
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyRemoteRouter, providedIn: 'root' });
518
+ }
519
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyRemoteRouter, decorators: [{
520
+ type: Injectable,
521
+ args: [{ providedIn: 'root' }]
522
+ }], ctorParameters: () => [] });
523
+
362
524
  /**
363
525
  * Translates a key using the shared I18nService.
364
526
  *
@@ -603,11 +765,11 @@ class ContextMenuComponent {
603
765
  this.previouslyFocused = null;
604
766
  }
605
767
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
606
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ContextMenuComponent, isStandalone: true, selector: "fly-context-menu", inputs: { x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: true, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: true, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { action: "action", closed: "closed" }, host: { listeners: { "document:mousedown": "onClickOutside($event)", "document:keydown.escape": "onEscape()", "document:contextmenu": "onContextMenu($event)", "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["contextMenu"], descendants: true }], ngImport: i0, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().left\"\n [style.top.px]=\"clampedPos().top\"\n role=\"menu\"\n [attr.aria-label]=\"'shell.context_menu' | translate\">\n\n @for (section of sections(); track $index) {\n @if ($index > 0) {\n <div class=\"menu-divider\"></div>\n }\n @if (section.label) {\n <div class=\"menu-section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <button\n type=\"button\"\n class=\"vos-btn sm rect menu-item\"\n role=\"menuitem\"\n (click)=\"onAction(item.id)\">\n <i [class]=\"'pi ' + item.icon\" aria-hidden=\"true\"></i>\n <span>{{ item.label }}</span>\n </button>\n }\n }\n</div>\n", styles: [":host{display:contents}.context-menu{position:fixed;min-width:200px;padding:6px;border-radius:14px;background:var(--glass-bg, rgba(30, 30, 34, .72));backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border:1px solid var(--glass-border, rgba(255, 255, 255, .15));box-shadow:0 12px 40px #00000059,0 2px 8px #0003,inset 0 .5px #ffffff1a;animation:menuEnter .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}@keyframes menuEnter{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.menu-section-label{padding:6px 12px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-color-secondary, rgba(255, 255, 255, .45));pointer-events:none}.menu-item{gap:10px;width:100%;padding:8px 12px;color:var(--text-color, #fff);font-size:var(--link-font-size, 13px);font-weight:var(--btn-symbol-font-weight, 510);justify-content:flex-start}.menu-item i{font-size:14px;width:18px;text-align:center;opacity:.7}.menu-item:hover{background:var(--primary-color, #E8732A);color:#fff}.menu-item:hover i{opacity:1}.menu-item:focus-visible{background:var(--primary-color, #E8732A);outline:2px solid var(--focus-ring);outline-offset:-2px}.menu-divider{height:1px;margin:4px 8px;background:var(--glass-border, rgba(255, 255, 255, .12))}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
768
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: ContextMenuComponent, isStandalone: true, selector: "fly-context-menu", inputs: { x: { classPropertyName: "x", publicName: "x", isSignal: true, isRequired: true, transformFunction: null }, y: { classPropertyName: "y", publicName: "y", isSignal: true, isRequired: true, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { action: "action", closed: "closed" }, host: { listeners: { "document:mousedown": "onClickOutside($event)", "document:keydown.escape": "onEscape()", "document:contextmenu": "onContextMenu($event)", "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "menuEl", first: true, predicate: ["contextMenu"], descendants: true }], ngImport: i0, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().left\"\n [style.top.px]=\"clampedPos().top\"\n role=\"menu\"\n [attr.aria-label]=\"'shell.context_menu' | translate\">\n\n @for (section of sections(); track $index) {\n @if ($index > 0) {\n <div class=\"menu-divider\"></div>\n }\n @if (section.label) {\n <div class=\"menu-section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <button\n type=\"button\"\n class=\"vos-btn sm rect menu-item\"\n role=\"menuitem\"\n (click)=\"onAction(item.id)\">\n <i [class]=\"'pi ' + item.icon\" aria-hidden=\"true\"></i>\n <span>{{ item.label }}</span>\n </button>\n }\n }\n</div>\n", styles: [":host{display:contents}.context-menu{position:fixed;min-width:200px;padding:6px;border-radius:14px;background:var(--glass-bg, rgba(30, 30, 34, .72));-webkit-backdrop-filter:blur(40px) saturate(180%);backdrop-filter:blur(40px) saturate(180%);border:1px solid var(--glass-border, rgba(255, 255, 255, .15));box-shadow:0 12px 40px #00000059,0 2px 8px #0003,inset 0 .5px #ffffff1a;animation:menuEnter .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}@keyframes menuEnter{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.menu-section-label{padding:6px 12px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-color-secondary, rgba(255, 255, 255, .45));pointer-events:none}.menu-item{gap:10px;width:100%;padding:8px 12px;color:var(--text-color, #fff);font-size:var(--link-font-size, 13px);font-weight:var(--btn-symbol-font-weight, 510);justify-content:flex-start}.menu-item i{font-size:14px;width:18px;text-align:center;opacity:.7}.menu-item:hover{background:var(--primary-color, #E8732A);color:#fff}.menu-item:hover i{opacity:1}.menu-item:focus-visible{background:var(--primary-color, #E8732A);outline:2px solid var(--focus-ring);outline-offset:-2px}.menu-divider{height:1px;margin:4px 8px;background:var(--glass-border, rgba(255, 255, 255, .12))}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
607
769
  }
608
770
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ContextMenuComponent, decorators: [{
609
771
  type: Component,
610
- args: [{ selector: 'fly-context-menu', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().left\"\n [style.top.px]=\"clampedPos().top\"\n role=\"menu\"\n [attr.aria-label]=\"'shell.context_menu' | translate\">\n\n @for (section of sections(); track $index) {\n @if ($index > 0) {\n <div class=\"menu-divider\"></div>\n }\n @if (section.label) {\n <div class=\"menu-section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <button\n type=\"button\"\n class=\"vos-btn sm rect menu-item\"\n role=\"menuitem\"\n (click)=\"onAction(item.id)\">\n <i [class]=\"'pi ' + item.icon\" aria-hidden=\"true\"></i>\n <span>{{ item.label }}</span>\n </button>\n }\n }\n</div>\n", styles: [":host{display:contents}.context-menu{position:fixed;min-width:200px;padding:6px;border-radius:14px;background:var(--glass-bg, rgba(30, 30, 34, .72));backdrop-filter:blur(40px) saturate(180%);-webkit-backdrop-filter:blur(40px) saturate(180%);border:1px solid var(--glass-border, rgba(255, 255, 255, .15));box-shadow:0 12px 40px #00000059,0 2px 8px #0003,inset 0 .5px #ffffff1a;animation:menuEnter .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}@keyframes menuEnter{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.menu-section-label{padding:6px 12px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-color-secondary, rgba(255, 255, 255, .45));pointer-events:none}.menu-item{gap:10px;width:100%;padding:8px 12px;color:var(--text-color, #fff);font-size:var(--link-font-size, 13px);font-weight:var(--btn-symbol-font-weight, 510);justify-content:flex-start}.menu-item i{font-size:14px;width:18px;text-align:center;opacity:.7}.menu-item:hover{background:var(--primary-color, #E8732A);color:#fff}.menu-item:hover i{opacity:1}.menu-item:focus-visible{background:var(--primary-color, #E8732A);outline:2px solid var(--focus-ring);outline-offset:-2px}.menu-divider{height:1px;margin:4px 8px;background:var(--glass-border, rgba(255, 255, 255, .12))}\n"] }]
772
+ args: [{ selector: 'fly-context-menu', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n #contextMenu\n class=\"context-menu\"\n [style.left.px]=\"clampedPos().left\"\n [style.top.px]=\"clampedPos().top\"\n role=\"menu\"\n [attr.aria-label]=\"'shell.context_menu' | translate\">\n\n @for (section of sections(); track $index) {\n @if ($index > 0) {\n <div class=\"menu-divider\"></div>\n }\n @if (section.label) {\n <div class=\"menu-section-label\">{{ section.label }}</div>\n }\n @for (item of section.items; track item.id) {\n <button\n type=\"button\"\n class=\"vos-btn sm rect menu-item\"\n role=\"menuitem\"\n (click)=\"onAction(item.id)\">\n <i [class]=\"'pi ' + item.icon\" aria-hidden=\"true\"></i>\n <span>{{ item.label }}</span>\n </button>\n }\n }\n</div>\n", styles: [":host{display:contents}.context-menu{position:fixed;min-width:200px;padding:6px;border-radius:14px;background:var(--glass-bg, rgba(30, 30, 34, .72));-webkit-backdrop-filter:blur(40px) saturate(180%);backdrop-filter:blur(40px) saturate(180%);border:1px solid var(--glass-border, rgba(255, 255, 255, .15));box-shadow:0 12px 40px #00000059,0 2px 8px #0003,inset 0 .5px #ffffff1a;animation:menuEnter .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}@keyframes menuEnter{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.menu-section-label{padding:6px 12px 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-color-secondary, rgba(255, 255, 255, .45));pointer-events:none}.menu-item{gap:10px;width:100%;padding:8px 12px;color:var(--text-color, #fff);font-size:var(--link-font-size, 13px);font-weight:var(--btn-symbol-font-weight, 510);justify-content:flex-start}.menu-item i{font-size:14px;width:18px;text-align:center;opacity:.7}.menu-item:hover{background:var(--primary-color, #E8732A);color:#fff}.menu-item:hover i{opacity:1}.menu-item:focus-visible{background:var(--primary-color, #E8732A);outline:2px solid var(--focus-ring);outline-offset:-2px}.menu-divider{height:1px;margin:4px 8px;background:var(--glass-border, rgba(255, 255, 255, .12))}\n"] }]
611
773
  }], propDecorators: { menuEl: [{
612
774
  type: ViewChild,
613
775
  args: ['contextMenu']
@@ -660,29 +822,65 @@ class MessageBoxService {
660
822
  message = signal('', ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
661
823
  icon = signal(MessageBoxIcon.None, ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
662
824
  buttons = signal([], ...(ngDevMode ? [{ debugName: "buttons" }] : /* istanbul ignore next */ []));
825
+ /**
826
+ * Active "Don't ask again" config — populated only by `showAcknowledged()`.
827
+ * `show()` callers cannot reach this signal because the type system rejects
828
+ * `dontAskAgain` on the base `MessageBoxOptions`.
829
+ */
830
+ dontAskAgain = signal(undefined, ...(ngDevMode ? [{ debugName: "dontAskAgain" }] : /* istanbul ignore next */ []));
831
+ /**
832
+ * Component-side checkbox state. The component (`MessageBoxComponent`) wires
833
+ * its own signal to this one; the service explicitly re-seeds it on every
834
+ * `_open()` so reusing a constant `options` literal across two opens does
835
+ * NOT leak the previous user's checkbox state through signal `===` dedupe.
836
+ * Exposed for the component to read/write; not for general callers.
837
+ */
838
+ dontAskAgainChecked = signal(false, ...(ngDevMode ? [{ debugName: "dontAskAgainChecked" }] : /* istanbul ignore next */ []));
663
839
  resolver = null;
664
840
  show(options) {
665
- // Dismiss any pending dialog so its Promise resolves rather than leaking
841
+ return this._open(options).then((r) => r.result);
842
+ }
843
+ /**
844
+ * Opens a message box with a "Don't ask again" checkbox and resolves to both
845
+ * the dialog result and the checkbox state. Throws when `options.dontAskAgain`
846
+ * is missing — the checkbox config is mandatory for this entry point. The
847
+ * compile-time signature already guarantees presence; the runtime check is
848
+ * defense-in-depth for callers that bypass the type system via `as never`.
849
+ */
850
+ showAcknowledged(options) {
851
+ if (!options.dontAskAgain) {
852
+ throw new Error('MessageBoxService.showAcknowledged() requires options.dontAskAgain — use show() for plain dialogs.');
853
+ }
854
+ return this._open(options);
855
+ }
856
+ resolve(result, dontAskAgain = false) {
857
+ this.visible.set(false);
666
858
  if (this.resolver) {
667
- this.resolver(DialogResult.None);
859
+ this.resolver({ result, dontAskAgain });
668
860
  this.resolver = null;
669
861
  }
862
+ }
863
+ _open(options) {
864
+ // Dismiss any pending dialog so its Promise resolves rather than leaking.
865
+ if (this.resolver) {
866
+ this.resolver({ result: DialogResult.None, dontAskAgain: false });
867
+ this.resolver = null;
868
+ }
869
+ const daa = 'dontAskAgain' in options ? options.dontAskAgain : undefined;
670
870
  this.title.set(options.title);
671
871
  this.message.set(options.message);
672
872
  this.icon.set(options.icon ?? MessageBoxIcon.None);
673
873
  this.buttons.set(this.resolveButtons(options.buttons ?? MessageBoxButtons.OK));
874
+ this.dontAskAgain.set(daa);
875
+ // Explicit re-seed: signal `set` dedupes by `===`, so an `effect()` on
876
+ // `dontAskAgain` would not re-fire when a caller passes the SAME options
877
+ // literal twice. Set the checkbox state directly each open.
878
+ this.dontAskAgainChecked.set(daa?.defaultChecked ?? false);
674
879
  this.visible.set(true);
675
880
  return new Promise((resolve) => {
676
881
  this.resolver = resolve;
677
882
  });
678
883
  }
679
- resolve(result) {
680
- this.visible.set(false);
681
- if (this.resolver) {
682
- this.resolver(result);
683
- this.resolver = null;
684
- }
685
- }
686
884
  resolveButtons(buttons) {
687
885
  const t = (key) => this.i18n.t(key);
688
886
  switch (buttons) {
@@ -730,6 +928,19 @@ class MessageBoxComponent {
730
928
  elRef = inject(ElementRef);
731
929
  MessageBoxIcon = MessageBoxIcon;
732
930
  DialogResult = DialogResult;
931
+ /** Mirror of the service's `dontAskAgain` config so the template can read it directly. */
932
+ dontAskAgainConfig = computed(() => this.service.dontAskAgain(), ...(ngDevMode ? [{ debugName: "dontAskAgainConfig" }] : /* istanbul ignore next */ []));
933
+ /**
934
+ * User-driven checkbox state, owned by the service and re-seeded on every
935
+ * `_open()` so reusing a constant options literal cannot leak prior state.
936
+ */
937
+ dontAskAgainChecked = this.service.dontAskAgainChecked;
938
+ /**
939
+ * Concatenated id list for the dialog's `aria-describedby`. Always includes
940
+ * the message; appends the checkbox label id when the checkbox is rendered
941
+ * so screen readers announce the checkbox as part of the description.
942
+ */
943
+ ariaDescribedBy = computed(() => this.dontAskAgainConfig() ? 'mb-message mb-dont-ask' : 'mb-message', ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
733
944
  previouslyFocused = null;
734
945
  ngAfterViewInit() {
735
946
  // Store focus origin so we can restore it on close
@@ -738,18 +949,22 @@ class MessageBoxComponent {
738
949
  }
739
950
  onEscape() {
740
951
  if (this.service.visible()) {
741
- this.service.resolve(DialogResult.Cancel);
952
+ this.service.resolve(DialogResult.Cancel, this.dontAskAgainChecked());
742
953
  this.restoreFocus();
743
954
  }
744
955
  }
745
956
  onBackdropClick() {
746
- this.service.resolve(DialogResult.Cancel);
957
+ this.service.resolve(DialogResult.Cancel, this.dontAskAgainChecked());
747
958
  this.restoreFocus();
748
959
  }
749
960
  onButtonClick(result) {
750
- this.service.resolve(result);
961
+ this.service.resolve(result, this.dontAskAgainChecked());
751
962
  this.restoreFocus();
752
963
  }
964
+ onDontAskAgainToggle(ev) {
965
+ const target = ev.target;
966
+ this.dontAskAgainChecked.set(!!target?.checked);
967
+ }
753
968
  iconClass() {
754
969
  switch (this.service.icon()) {
755
970
  case MessageBoxIcon.Information: return 'pi pi-info-circle';
@@ -772,11 +987,11 @@ class MessageBoxComponent {
772
987
  this.previouslyFocused = null;
773
988
  }
774
989
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
775
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessageBoxComponent, isStandalone: true, selector: "fly-message-box", host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (service.visible()) {\n <div\n class=\"mb-backdrop\"\n (click)=\"onBackdropClick()\"\n role=\"presentation\">\n <div\n class=\"mb-dialog\"\n [class.mb-info]=\"service.icon() === MessageBoxIcon.Information\"\n [class.mb-warning]=\"service.icon() === MessageBoxIcon.Warning\"\n [class.mb-danger]=\"service.icon() === MessageBoxIcon.Error\"\n [class.mb-question]=\"service.icon() === MessageBoxIcon.Question\"\n (click)=\"$event.stopPropagation()\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"mb-title\"\n aria-describedby=\"mb-message\">\n\n @if (iconClass()) {\n <div class=\"mb-icon-wrap\">\n <i [class]=\"iconClass() + ' mb-icon'\" aria-hidden=\"true\"></i>\n </div>\n }\n\n <div class=\"mb-title\" id=\"mb-title\">{{ service.title() }}</div>\n <div class=\"mb-message\" id=\"mb-message\">{{ service.message() }}</div>\n\n <div class=\"mb-actions\">\n @for (btn of service.buttons(); track btn.result) {\n <button\n type=\"button\"\n class=\"vos-btn sm mb-btn\"\n [class.mb-btn--primary]=\"btn.variant === 'primary'\"\n [class.mb-btn--danger]=\"btn.variant === 'danger'\"\n (click)=\"onButtonClick(btn.result)\">\n {{ btn.label }}\n </button>\n }\n </div>\n\n </div>\n </div>\n}\n", styles: [".mb-backdrop{position:absolute;inset:0;z-index:5100;background:#00000073;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);animation:mbFadeIn .15s ease both}@keyframes mbFadeIn{0%{opacity:0}to{opacity:1}}.mb-dialog{background:var(--surface-card, rgba(30, 30, 30, .98));border:1px solid var(--surface-border);border-radius:16px;padding:28px 28px 20px;width:400px;max-width:90%;display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;box-shadow:0 20px 60px #0006;animation:mbScaleIn .2s cubic-bezier(.22,1,.36,1) both}@keyframes mbScaleIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.mb-icon-wrap{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:6px}.mb-icon{font-size:24px}.mb-info .mb-icon-wrap{background:#3b82f61f}.mb-info .mb-icon{color:#3b82f6}.mb-warning .mb-icon-wrap{background:#f59e0b1f}.mb-warning .mb-icon{color:#f59e0b}.mb-danger .mb-icon-wrap{background:#ef44441f}.mb-danger .mb-icon{color:#ef4444}.mb-question .mb-icon-wrap{background:#8b5cf61f}.mb-question .mb-icon{color:#8b5cf6}.mb-title{font-size:15px;font-weight:700;color:var(--text-color)}.mb-message{font-size:13px;color:var(--text-color-secondary);line-height:1.55;max-width:340px;white-space:pre-line}.mb-actions{display:flex;gap:10px;margin-top:16px;width:100%;justify-content:center;flex-wrap:wrap}.mb-btn{min-width:90px}.mb-btn--primary{background:var(--primary-color, #E8732A);color:#fff}.mb-btn--primary:hover{filter:brightness(1.1)}.mb-btn--danger{background:#ef4444;color:#fff}.mb-btn--danger:hover{background:#dc2626}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
990
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: MessageBoxComponent, isStandalone: true, selector: "fly-message-box", host: { listeners: { "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "@if (service.visible()) {\r\n <div\r\n class=\"mb-backdrop\"\r\n (click)=\"onBackdropClick()\"\r\n role=\"presentation\">\r\n <div\r\n class=\"mb-dialog\"\r\n [class.mb-info]=\"service.icon() === MessageBoxIcon.Information\"\r\n [class.mb-warning]=\"service.icon() === MessageBoxIcon.Warning\"\r\n [class.mb-danger]=\"service.icon() === MessageBoxIcon.Error\"\r\n [class.mb-question]=\"service.icon() === MessageBoxIcon.Question\"\r\n (click)=\"$event.stopPropagation()\"\r\n role=\"alertdialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby=\"mb-title\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\">\r\n\r\n @if (iconClass()) {\r\n <div class=\"mb-icon-wrap\">\r\n <i [class]=\"iconClass() + ' mb-icon'\" aria-hidden=\"true\"></i>\r\n </div>\r\n }\r\n\r\n <div class=\"mb-title\" id=\"mb-title\">{{ service.title() }}</div>\r\n <div class=\"mb-message\" id=\"mb-message\">{{ service.message() }}</div>\r\n\r\n @if (dontAskAgainConfig(); as daa) {\r\n <label class=\"mb-dont-ask-again\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"dontAskAgainChecked()\"\r\n (change)=\"onDontAskAgainToggle($event)\" />\r\n <span id=\"mb-dont-ask\">{{ daa.labelKey | translate }}</span>\r\n </label>\r\n }\r\n\r\n <div class=\"mb-actions\">\r\n @for (btn of service.buttons(); track btn.result) {\r\n <button\r\n type=\"button\"\r\n class=\"vos-btn sm mb-btn\"\r\n [class.mb-btn--primary]=\"btn.variant === 'primary'\"\r\n [class.mb-btn--danger]=\"btn.variant === 'danger'\"\r\n (click)=\"onButtonClick(btn.result)\">\r\n {{ btn.label }}\r\n </button>\r\n }\r\n </div>\r\n\r\n </div>\r\n </div>\r\n}\r\n", styles: [".mb-backdrop{position:absolute;inset:0;z-index:5100;background:#00000073;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);animation:mbFadeIn .15s ease both}@keyframes mbFadeIn{0%{opacity:0}to{opacity:1}}.mb-dialog{background:var(--surface-card, rgba(30, 30, 30, .98));border:1px solid var(--surface-border);border-radius:16px;padding:28px 28px 20px;width:400px;max-width:90%;display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;box-shadow:0 20px 60px #0006;animation:mbScaleIn .2s cubic-bezier(.22,1,.36,1) both}@keyframes mbScaleIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.mb-icon-wrap{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:6px}.mb-icon{font-size:24px}.mb-info .mb-icon-wrap{background:#3b82f61f}.mb-info .mb-icon{color:#3b82f6}.mb-warning .mb-icon-wrap{background:#f59e0b1f}.mb-warning .mb-icon{color:#f59e0b}.mb-danger .mb-icon-wrap{background:#ef44441f}.mb-danger .mb-icon{color:#ef4444}.mb-question .mb-icon-wrap{background:#8b5cf61f}.mb-question .mb-icon{color:#8b5cf6}.mb-title{font-size:15px;font-weight:700;color:var(--text-color)}.mb-message{font-size:13px;color:var(--text-color-secondary);line-height:1.55;max-width:340px;white-space:pre-line}.mb-dont-ask-again{display:flex;align-items:center;gap:8px;margin-block-start:12px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;align-self:flex-start;-webkit-user-select:none;user-select:none}.mb-dont-ask-again input[type=checkbox]{margin:0;cursor:pointer}.mb-dont-ask-again span{line-height:1.3}.mb-actions{display:flex;gap:10px;margin-top:16px;width:100%;justify-content:center;flex-wrap:wrap}.mb-btn{min-width:90px}.mb-btn--primary{background:var(--primary-color, #E8732A);color:#fff}.mb-btn--primary:hover{filter:brightness(1.1)}.mb-btn--danger{background:#ef4444;color:#fff}.mb-btn--danger:hover{background:#dc2626}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
776
991
  }
777
992
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: MessageBoxComponent, decorators: [{
778
993
  type: Component,
779
- args: [{ selector: 'fly-message-box', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (service.visible()) {\n <div\n class=\"mb-backdrop\"\n (click)=\"onBackdropClick()\"\n role=\"presentation\">\n <div\n class=\"mb-dialog\"\n [class.mb-info]=\"service.icon() === MessageBoxIcon.Information\"\n [class.mb-warning]=\"service.icon() === MessageBoxIcon.Warning\"\n [class.mb-danger]=\"service.icon() === MessageBoxIcon.Error\"\n [class.mb-question]=\"service.icon() === MessageBoxIcon.Question\"\n (click)=\"$event.stopPropagation()\"\n role=\"alertdialog\"\n aria-modal=\"true\"\n aria-labelledby=\"mb-title\"\n aria-describedby=\"mb-message\">\n\n @if (iconClass()) {\n <div class=\"mb-icon-wrap\">\n <i [class]=\"iconClass() + ' mb-icon'\" aria-hidden=\"true\"></i>\n </div>\n }\n\n <div class=\"mb-title\" id=\"mb-title\">{{ service.title() }}</div>\n <div class=\"mb-message\" id=\"mb-message\">{{ service.message() }}</div>\n\n <div class=\"mb-actions\">\n @for (btn of service.buttons(); track btn.result) {\n <button\n type=\"button\"\n class=\"vos-btn sm mb-btn\"\n [class.mb-btn--primary]=\"btn.variant === 'primary'\"\n [class.mb-btn--danger]=\"btn.variant === 'danger'\"\n (click)=\"onButtonClick(btn.result)\">\n {{ btn.label }}\n </button>\n }\n </div>\n\n </div>\n </div>\n}\n", styles: [".mb-backdrop{position:absolute;inset:0;z-index:5100;background:#00000073;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);animation:mbFadeIn .15s ease both}@keyframes mbFadeIn{0%{opacity:0}to{opacity:1}}.mb-dialog{background:var(--surface-card, rgba(30, 30, 30, .98));border:1px solid var(--surface-border);border-radius:16px;padding:28px 28px 20px;width:400px;max-width:90%;display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;box-shadow:0 20px 60px #0006;animation:mbScaleIn .2s cubic-bezier(.22,1,.36,1) both}@keyframes mbScaleIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.mb-icon-wrap{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:6px}.mb-icon{font-size:24px}.mb-info .mb-icon-wrap{background:#3b82f61f}.mb-info .mb-icon{color:#3b82f6}.mb-warning .mb-icon-wrap{background:#f59e0b1f}.mb-warning .mb-icon{color:#f59e0b}.mb-danger .mb-icon-wrap{background:#ef44441f}.mb-danger .mb-icon{color:#ef4444}.mb-question .mb-icon-wrap{background:#8b5cf61f}.mb-question .mb-icon{color:#8b5cf6}.mb-title{font-size:15px;font-weight:700;color:var(--text-color)}.mb-message{font-size:13px;color:var(--text-color-secondary);line-height:1.55;max-width:340px;white-space:pre-line}.mb-actions{display:flex;gap:10px;margin-top:16px;width:100%;justify-content:center;flex-wrap:wrap}.mb-btn{min-width:90px}.mb-btn--primary{background:var(--primary-color, #E8732A);color:#fff}.mb-btn--primary:hover{filter:brightness(1.1)}.mb-btn--danger{background:#ef4444;color:#fff}.mb-btn--danger:hover{background:#dc2626}\n"] }]
994
+ args: [{ selector: 'fly-message-box', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (service.visible()) {\r\n <div\r\n class=\"mb-backdrop\"\r\n (click)=\"onBackdropClick()\"\r\n role=\"presentation\">\r\n <div\r\n class=\"mb-dialog\"\r\n [class.mb-info]=\"service.icon() === MessageBoxIcon.Information\"\r\n [class.mb-warning]=\"service.icon() === MessageBoxIcon.Warning\"\r\n [class.mb-danger]=\"service.icon() === MessageBoxIcon.Error\"\r\n [class.mb-question]=\"service.icon() === MessageBoxIcon.Question\"\r\n (click)=\"$event.stopPropagation()\"\r\n role=\"alertdialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby=\"mb-title\"\r\n [attr.aria-describedby]=\"ariaDescribedBy()\">\r\n\r\n @if (iconClass()) {\r\n <div class=\"mb-icon-wrap\">\r\n <i [class]=\"iconClass() + ' mb-icon'\" aria-hidden=\"true\"></i>\r\n </div>\r\n }\r\n\r\n <div class=\"mb-title\" id=\"mb-title\">{{ service.title() }}</div>\r\n <div class=\"mb-message\" id=\"mb-message\">{{ service.message() }}</div>\r\n\r\n @if (dontAskAgainConfig(); as daa) {\r\n <label class=\"mb-dont-ask-again\">\r\n <input\r\n type=\"checkbox\"\r\n [checked]=\"dontAskAgainChecked()\"\r\n (change)=\"onDontAskAgainToggle($event)\" />\r\n <span id=\"mb-dont-ask\">{{ daa.labelKey | translate }}</span>\r\n </label>\r\n }\r\n\r\n <div class=\"mb-actions\">\r\n @for (btn of service.buttons(); track btn.result) {\r\n <button\r\n type=\"button\"\r\n class=\"vos-btn sm mb-btn\"\r\n [class.mb-btn--primary]=\"btn.variant === 'primary'\"\r\n [class.mb-btn--danger]=\"btn.variant === 'danger'\"\r\n (click)=\"onButtonClick(btn.result)\">\r\n {{ btn.label }}\r\n </button>\r\n }\r\n </div>\r\n\r\n </div>\r\n </div>\r\n}\r\n", styles: [".mb-backdrop{position:absolute;inset:0;z-index:5100;background:#00000073;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);animation:mbFadeIn .15s ease both}@keyframes mbFadeIn{0%{opacity:0}to{opacity:1}}.mb-dialog{background:var(--surface-card, rgba(30, 30, 30, .98));border:1px solid var(--surface-border);border-radius:16px;padding:28px 28px 20px;width:400px;max-width:90%;display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;box-shadow:0 20px 60px #0006;animation:mbScaleIn .2s cubic-bezier(.22,1,.36,1) both}@keyframes mbScaleIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}.mb-icon-wrap{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:6px}.mb-icon{font-size:24px}.mb-info .mb-icon-wrap{background:#3b82f61f}.mb-info .mb-icon{color:#3b82f6}.mb-warning .mb-icon-wrap{background:#f59e0b1f}.mb-warning .mb-icon{color:#f59e0b}.mb-danger .mb-icon-wrap{background:#ef44441f}.mb-danger .mb-icon{color:#ef4444}.mb-question .mb-icon-wrap{background:#8b5cf61f}.mb-question .mb-icon{color:#8b5cf6}.mb-title{font-size:15px;font-weight:700;color:var(--text-color)}.mb-message{font-size:13px;color:var(--text-color-secondary);line-height:1.55;max-width:340px;white-space:pre-line}.mb-dont-ask-again{display:flex;align-items:center;gap:8px;margin-block-start:12px;font-size:12px;color:var(--text-color-secondary);cursor:pointer;align-self:flex-start;-webkit-user-select:none;user-select:none}.mb-dont-ask-again input[type=checkbox]{margin:0;cursor:pointer}.mb-dont-ask-again span{line-height:1.3}.mb-actions{display:flex;gap:10px;margin-top:16px;width:100%;justify-content:center;flex-wrap:wrap}.mb-btn{min-width:90px}.mb-btn--primary{background:var(--primary-color, #E8732A);color:#fff}.mb-btn--primary:hover{filter:brightness(1.1)}.mb-btn--danger{background:#ef4444;color:#fff}.mb-btn--danger:hover{background:#dc2626}\n"] }]
780
995
  }], propDecorators: { onEscape: [{
781
996
  type: HostListener,
782
997
  args: ['document:keydown.escape']
@@ -804,6 +1019,17 @@ class SharePanelComponent {
804
1019
  loadOuTree;
805
1020
  /** Chart selector options; first entry should be the default chart (`id: null`). */
806
1021
  loadChartOptions;
1022
+ /**
1023
+ * Initial chart selection. Resolution order:
1024
+ * <list type="number">
1025
+ * <item>{@link defaultChartId} when explicitly provided (string or null).</item>
1026
+ * <item>First option matching {@link defaultChartSystemKey} (e.g. <c>'apps'</c>).</item>
1027
+ * <item>Host order — <c>options[0]?.id ?? null</c>.</item>
1028
+ * </list>
1029
+ */
1030
+ defaultChartSystemKey;
1031
+ /** Hard-coded chart id to pre-select. Takes priority over {@link defaultChartSystemKey}. */
1032
+ defaultChartId;
807
1033
  /**
808
1034
  * Labels from the default (official) OU tree for stable display names on OU grants
809
1035
  * when the picker shows an alternative chart.
@@ -909,8 +1135,7 @@ class SharePanelComponent {
909
1135
  .subscribe({
910
1136
  next: (opts) => {
911
1137
  this.chartOptions.set(opts);
912
- const first = opts[0];
913
- const initial = first ? first.id : null;
1138
+ const initial = this.resolveInitialChartId(opts);
914
1139
  this.selectedOrgChartId.set(initial);
915
1140
  this.selectedChartId$.next(initial);
916
1141
  },
@@ -921,6 +1146,24 @@ class SharePanelComponent {
921
1146
  },
922
1147
  });
923
1148
  }
1149
+ /**
1150
+ * Picks the chart id to seed the OU picker with. Honours explicit
1151
+ * {@link defaultChartId} first (null is a valid id), then {@link defaultChartSystemKey},
1152
+ * then falls back to host order.
1153
+ */
1154
+ resolveInitialChartId(opts) {
1155
+ if (this.defaultChartId !== undefined) {
1156
+ const exists = opts.some((o) => o.id === this.defaultChartId);
1157
+ return exists ? this.defaultChartId : (opts[0]?.id ?? null);
1158
+ }
1159
+ const key = this.defaultChartSystemKey;
1160
+ if (key) {
1161
+ const match = opts.find((o) => o.systemKey === key);
1162
+ if (match)
1163
+ return match.id;
1164
+ }
1165
+ return opts[0]?.id ?? null;
1166
+ }
924
1167
  onOrgChartSelectChange(chartId) {
925
1168
  this.selectedOrgChartId.set(chartId);
926
1169
  this.selectedChartId$.next(chartId);
@@ -1080,7 +1323,7 @@ class SharePanelComponent {
1080
1323
  }
1081
1324
  }
1082
1325
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1083
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: SharePanelComponent, isStandalone: true, selector: "fly-share-panel", inputs: { targetName: "targetName", titleKey: "titleKey", loadPermissions: "loadPermissions", searchUsers: "searchUsers", loadOuTree: "loadOuTree", loadChartOptions: "loadChartOptions", loadOuLabelMap: "loadOuLabelMap", grantToUser: "grantToUser", grantToOu: "grantToOu", updatePermission: "updatePermission", revokePermission: "revokePermission", showApplyToChildren: "showApplyToChildren", supportsDeny: "supportsDeny", permissionLevels: "permissionLevels", everyoneLabelKey: "everyoneLabelKey", loadRoleOus: "loadRoleOus" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\r\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"fac-header\">\r\n <h3>{{ titleKey | translate }}</h3>\r\n <span class=\"fac-target-name\">{{ targetName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fac-close\"\r\n (click)=\"close.emit()\"\r\n [attr.aria-label]=\"'files.share.close' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n\r\n <div class=\"fac-add-section\">\r\n <div class=\"fac-search-row\">\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"searchQuery\"\r\n (input)=\"onSearchInput()\"\r\n [placeholder]=\"'files.share.search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_users' | translate\"\r\n />\r\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n @if (searchResults().length > 0) {\r\n <div class=\"fac-search-results\">\r\n @for (result of searchResults(); track result.id) {\r\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ result.firstName }} {{ result.lastName }}</span>\r\n <span class=\"fac-email\">{{ result.email }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (chartOptions().length > 0) {\r\n <div class=\"fac-chart-row\">\r\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\r\n 'files.share.org_chart' | translate\r\n }}</label>\r\n <select\r\n id=\"fac-org-chart-select\"\r\n class=\"fac-select\"\r\n [ngModel]=\"selectedOrgChartId()\"\r\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\r\n @for (opt of chartOptions(); track $index) {\r\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\r\n }\r\n </select>\r\n </div>\r\n }\r\n\r\n @if (showOuPicker()) {\r\n <div class=\"fac-ou-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\r\n @for (ou of ouTree(); track ou.id) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-ou-item\"\r\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\r\n (click)=\"onGrantToOu(ou)\"\r\n [attr.aria-label]=\"('files.share.share_with_ou_named' | translate) + ' ' + ou.displayName\"\r\n >\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ ou.displayName }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <div class=\"fac-role-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_role' | translate }}</div>\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"roleSearchQuery\"\r\n [placeholder]=\"'files.share.role_search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_roles' | translate\"\r\n [hidden]=\"!showRolePicker()\" />\r\n @if (showRolePicker()) {\r\n <div class=\"fac-role-list\">\r\n @for (role of filteredRoles(); track role.ouId) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-role-item\"\r\n (click)=\"onGrantToRole(role)\"\r\n [attr.aria-label]=\"('files.share.share_with_role_named' | translate) + ' ' + role.displayName\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ role.displayName }}</span>\r\n <span class=\"fac-role-app\">{{ role.appId }}</span>\r\n </button>\r\n }\r\n @if (filteredRoles().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_roles_match' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <div class=\"fac-toggle-row\">\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\r\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\r\n </button>\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showRolePicker.set(!showRolePicker())\">\r\n {{ showRolePicker() ? ('files.share.hide_roles' | translate) : ('files.share.show_roles' | translate) }}\r\n </button>\r\n }\r\n @if (showApplyToChildren) {\r\n <label class=\"fac-checkbox-label\">\r\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\r\n {{ 'files.share.apply_children' | translate }}\r\n </label>\r\n }\r\n @if (supportsDeny) {\r\n <label class=\"fac-checkbox-label fac-deny-toggle\"\r\n [title]=\"'files.share.deny_help' | translate\">\r\n <input type=\"checkbox\" [(ngModel)]=\"addAsDeny\" />\r\n <i class=\"pi pi-ban\" aria-hidden=\"true\"></i>\r\n {{ 'files.share.add_as_deny' | translate }}\r\n </label>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fac-perms-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\r\n @if (loading()) {\r\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\r\n } @else if (permissions().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\r\n } @else {\r\n @for (perm of permissions(); track perm.id) {\r\n <div class=\"fac-perm-row\" [class.fac-perm-row-deny]=\"perm.isDeny\">\r\n <div class=\"fac-perm-info\">\r\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\r\n <span class=\"fac-perm-name\" dir=\"auto\">{{ getPermLabel(perm) }}</span>\r\n @if (perm.isDeny) {\r\n <span class=\"fac-badge deny\">{{ 'files.share.denied' | translate }}</span>\r\n }\r\n @if (perm.isInherited) {\r\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\r\n }\r\n </div>\r\n <div class=\"fac-perm-actions\">\r\n @if (!perm.isDeny) {\r\n <select\r\n class=\"fac-select sm\"\r\n [ngModel]=\"perm.level\"\r\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n >\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"fac-icon-btn danger\"\r\n (click)=\"onRevokePermission(perm)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n [title]=\"'files.share.revoke' | translate\"\r\n >\r\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".fac-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:2000;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.fac-panel{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:12px;width:600px;max-width:min(600px,96vw);max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 16px 48px #0006}.fac-header{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-header h3{margin:0;font-size:16px;font-weight:600}.fac-target-name{flex:1;font-size:13px;color:var(--text-color-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:16px;padding:4px}.fac-close:hover{color:var(--text-color)}.fac-add-section{padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-search-row{display:flex;gap:8px}.fac-chart-row{display:flex;flex-direction:column;gap:6px;margin-top:12px}.fac-chart-row .fac-select{width:100%}.fac-input{flex:1;background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fac-input:focus{border-color:var(--primary-color, #e8732a)}.fac-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 10px;color:inherit;font-size:13px;outline:none}.fac-select.sm{padding:4px 8px;font-size:12px}.fac-search-results{margin-top:8px;max-height:150px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-search-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-search-item:hover{background:var(--surface-hover)}.fac-search-item i{color:var(--text-color-secondary)}.fac-email{color:var(--text-color-secondary);font-size:12px;margin-inline-start:auto}.fac-ou-section{margin-top:10px;max-height:220px;overflow-y:auto}.fac-ou-item{display:flex;align-items:center;gap:8px;padding:6px 12px;padding-inline-start:12px;cursor:pointer;font-size:13px;border-radius:6px}.fac-ou-item:hover{background:var(--surface-hover)}.fac-ou-item i{color:var(--text-color-secondary)}.fac-section-label{font-size:11px;font-weight:700;text-transform:uppercase;color:var(--text-color-secondary);letter-spacing:.5px;margin-bottom:8px}.fac-toggle-row{display:flex;align-items:center;justify-content:space-between;margin-top:10px}.fac-link{background:none;border:none;color:var(--primary-color, #e8732a);cursor:pointer;font-size:12px;padding:0}.fac-link:hover{text-decoration:underline}.fac-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fac-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fac-perms-section{flex:1;overflow-y:auto;padding:16px 20px}.fac-loading,.fac-empty{text-align:center;padding:20px;color:var(--text-color-secondary);font-size:13px}.fac-perm-row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.05);gap:8px}.fac-perm-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.fac-perm-info i{color:var(--text-color-secondary);flex-shrink:0}.fac-perm-name{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-badge{font-size:10px;padding:2px 6px;border-radius:8px;flex-shrink:0}.fac-badge.inherited{background:#ffffff1a;color:var(--text-color-secondary)}.fac-badge.deny{background:#ff3b302e;color:#ff3b30;text-transform:uppercase;font-weight:600;letter-spacing:.5px}.fac-perm-row-deny{background:#ff3b300f;border-inline-start:3px solid #ff3b30;padding-inline-start:8px;border-radius:6px}.fac-perm-row-deny .fac-perm-info i{color:#ff3b30}.fac-deny-toggle{color:#ff6b6b}.fac-deny-toggle i{color:#ff6b6b;font-size:11px;margin-inline-end:2px}.fac-deny-toggle input{accent-color:#ff3b30}.fac-role-section{margin-top:10px;display:flex;flex-direction:column;gap:6px}.fac-role-list{margin-top:6px;max-height:220px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-role-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-role-item:hover{background:var(--surface-hover)}.fac-role-item i{color:var(--text-color-secondary)}.fac-role-app{margin-inline-start:auto;font-size:11px;color:var(--text-color-secondary);text-transform:uppercase;letter-spacing:.5px}.fac-perm-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.fac-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:12px}.fac-icon-btn:hover{background:#ffffff24}.fac-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fac-icon-btn:disabled{opacity:.4;cursor:default}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { 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 });
1326
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: SharePanelComponent, isStandalone: true, selector: "fly-share-panel", inputs: { targetName: "targetName", titleKey: "titleKey", loadPermissions: "loadPermissions", searchUsers: "searchUsers", loadOuTree: "loadOuTree", loadChartOptions: "loadChartOptions", defaultChartSystemKey: "defaultChartSystemKey", defaultChartId: "defaultChartId", loadOuLabelMap: "loadOuLabelMap", grantToUser: "grantToUser", grantToOu: "grantToOu", updatePermission: "updatePermission", revokePermission: "revokePermission", showApplyToChildren: "showApplyToChildren", supportsDeny: "supportsDeny", permissionLevels: "permissionLevels", everyoneLabelKey: "everyoneLabelKey", loadRoleOus: "loadRoleOus" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\r\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"fac-header\">\r\n <h3>{{ titleKey | translate }}</h3>\r\n <span class=\"fac-target-name\">{{ targetName }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"fac-close\"\r\n (click)=\"close.emit()\"\r\n [attr.aria-label]=\"'files.share.close' | translate\">\r\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n\r\n <div class=\"fac-add-section\">\r\n <div class=\"fac-search-row\">\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"searchQuery\"\r\n (input)=\"onSearchInput()\"\r\n [placeholder]=\"'files.share.search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_users' | translate\"\r\n />\r\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n </div>\r\n\r\n @if (searchResults().length > 0) {\r\n <div class=\"fac-search-results\">\r\n @for (result of searchResults(); track result.id) {\r\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\r\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\r\n <span>{{ result.firstName }} {{ result.lastName }}</span>\r\n <span class=\"fac-email\">{{ result.email }}</span>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n @if (chartOptions().length > 0) {\r\n <div class=\"fac-chart-row\">\r\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\r\n 'files.share.org_chart' | translate\r\n }}</label>\r\n <select\r\n id=\"fac-org-chart-select\"\r\n class=\"fac-select\"\r\n [ngModel]=\"selectedOrgChartId()\"\r\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\r\n @for (opt of chartOptions(); track $index) {\r\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\r\n }\r\n </select>\r\n </div>\r\n }\r\n\r\n @if (showOuPicker()) {\r\n <div class=\"fac-ou-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\r\n @for (ou of ouTree(); track ou.id) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-ou-item\"\r\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\r\n (click)=\"onGrantToOu(ou)\"\r\n [attr.aria-label]=\"('files.share.share_with_ou_named' | translate) + ' ' + ou.displayName\"\r\n >\r\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ ou.displayName }}</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <div class=\"fac-role-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.share_with_role' | translate }}</div>\r\n <input\r\n type=\"text\"\r\n class=\"fac-input\"\r\n [(ngModel)]=\"roleSearchQuery\"\r\n [placeholder]=\"'files.share.role_search_placeholder' | translate\"\r\n [attr.aria-label]=\"'files.share.search_roles' | translate\"\r\n [hidden]=\"!showRolePicker()\" />\r\n @if (showRolePicker()) {\r\n <div class=\"fac-role-list\">\r\n @for (role of filteredRoles(); track role.ouId) {\r\n <button\r\n type=\"button\"\r\n class=\"fac-role-item\"\r\n (click)=\"onGrantToRole(role)\"\r\n [attr.aria-label]=\"('files.share.share_with_role_named' | translate) + ' ' + role.displayName\">\r\n <i class=\"pi pi-id-card\" aria-hidden=\"true\"></i>\r\n <span dir=\"auto\">{{ role.displayName }}</span>\r\n <span class=\"fac-role-app\">{{ role.appId }}</span>\r\n </button>\r\n }\r\n @if (filteredRoles().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_roles_match' | translate }}</div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <div class=\"fac-toggle-row\">\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\r\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\r\n </button>\r\n @if (loadRoleOus && roleOus().length > 0) {\r\n <button type=\"button\" class=\"fac-link\" (click)=\"showRolePicker.set(!showRolePicker())\">\r\n {{ showRolePicker() ? ('files.share.hide_roles' | translate) : ('files.share.show_roles' | translate) }}\r\n </button>\r\n }\r\n @if (showApplyToChildren) {\r\n <label class=\"fac-checkbox-label\">\r\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\r\n {{ 'files.share.apply_children' | translate }}\r\n </label>\r\n }\r\n @if (supportsDeny) {\r\n <label class=\"fac-checkbox-label fac-deny-toggle\"\r\n [title]=\"'files.share.deny_help' | translate\">\r\n <input type=\"checkbox\" [(ngModel)]=\"addAsDeny\" />\r\n <i class=\"pi pi-ban\" aria-hidden=\"true\"></i>\r\n {{ 'files.share.add_as_deny' | translate }}\r\n </label>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"fac-perms-section\">\r\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\r\n @if (loading()) {\r\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\r\n } @else if (permissions().length === 0) {\r\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\r\n } @else {\r\n @for (perm of permissions(); track perm.id) {\r\n <div class=\"fac-perm-row\" [class.fac-perm-row-deny]=\"perm.isDeny\">\r\n <div class=\"fac-perm-info\">\r\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\r\n <span class=\"fac-perm-name\" dir=\"auto\">{{ getPermLabel(perm) }}</span>\r\n @if (perm.isDeny) {\r\n <span class=\"fac-badge deny\">{{ 'files.share.denied' | translate }}</span>\r\n }\r\n @if (perm.isInherited) {\r\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\r\n }\r\n </div>\r\n <div class=\"fac-perm-actions\">\r\n @if (!perm.isDeny) {\r\n <select\r\n class=\"fac-select sm\"\r\n [ngModel]=\"perm.level\"\r\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n >\r\n @for (opt of levelOptions; track opt.value) {\r\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\r\n }\r\n </select>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"fac-icon-btn danger\"\r\n (click)=\"onRevokePermission(perm)\"\r\n [disabled]=\"perm.isInherited === true\"\r\n [title]=\"'files.share.revoke' | translate\"\r\n >\r\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".fac-overlay{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:2000;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.fac-panel{background:var(--surface-card, #1e1e1e);border:1px solid var(--surface-border);border-radius:12px;width:600px;max-width:min(600px,96vw);max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 16px 48px #0006}.fac-header{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-header h3{margin:0;font-size:16px;font-weight:600}.fac-target-name{flex:1;font-size:13px;color:var(--text-color-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-close{background:none;border:none;color:var(--text-color-secondary);cursor:pointer;font-size:16px;padding:4px}.fac-close:hover{color:var(--text-color)}.fac-add-section{padding:16px 20px;border-bottom:1px solid var(--surface-border)}.fac-search-row{display:flex;gap:8px}.fac-chart-row{display:flex;flex-direction:column;gap:6px;margin-top:12px}.fac-chart-row .fac-select{width:100%}.fac-input{flex:1;background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 12px;color:inherit;font-size:13px;outline:none}.fac-input:focus{border-color:var(--primary-color, #e8732a)}.fac-select{background:#ffffff14;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:8px 10px;color:inherit;font-size:13px;outline:none}.fac-select.sm{padding:4px 8px;font-size:12px}.fac-search-results{margin-top:8px;max-height:150px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-search-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-search-item:hover{background:var(--surface-hover)}.fac-search-item i{color:var(--text-color-secondary)}.fac-email{color:var(--text-color-secondary);font-size:12px;margin-inline-start:auto}.fac-ou-section{margin-top:10px;max-height:220px;overflow-y:auto}.fac-ou-item{display:flex;align-items:center;gap:8px;padding:6px 12px;padding-inline-start:12px;cursor:pointer;font-size:13px;border-radius:6px}.fac-ou-item:hover{background:var(--surface-hover)}.fac-ou-item i{color:var(--text-color-secondary)}.fac-section-label{font-size:11px;font-weight:700;text-transform:uppercase;color:var(--text-color-secondary);letter-spacing:.5px;margin-bottom:8px}.fac-toggle-row{display:flex;align-items:center;justify-content:space-between;margin-top:10px}.fac-link{background:none;border:none;color:var(--primary-color, #e8732a);cursor:pointer;font-size:12px;padding:0}.fac-link:hover{text-decoration:underline}.fac-checkbox-label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-color-secondary);cursor:pointer}.fac-checkbox-label input{accent-color:var(--primary-color, #e8732a)}.fac-perms-section{flex:1;overflow-y:auto;padding:16px 20px}.fac-loading,.fac-empty{text-align:center;padding:20px;color:var(--text-color-secondary);font-size:13px}.fac-perm-row{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.05);gap:8px}.fac-perm-info{display:flex;align-items:center;gap:8px;flex:1;min-width:0}.fac-perm-info i{color:var(--text-color-secondary);flex-shrink:0}.fac-perm-name{font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fac-badge{font-size:10px;padding:2px 6px;border-radius:8px;flex-shrink:0}.fac-badge.inherited{background:#ffffff1a;color:var(--text-color-secondary)}.fac-badge.deny{background:#ff3b302e;color:#ff3b30;text-transform:uppercase;font-weight:600;letter-spacing:.5px}.fac-perm-row-deny{background:#ff3b300f;border-inline-start:3px solid #ff3b30;padding-inline-start:8px;border-radius:6px}.fac-perm-row-deny .fac-perm-info i{color:#ff3b30}.fac-deny-toggle{color:#ff6b6b}.fac-deny-toggle i{color:#ff6b6b;font-size:11px;margin-inline-end:2px}.fac-deny-toggle input{accent-color:#ff3b30}.fac-role-section{margin-top:10px;display:flex;flex-direction:column;gap:6px}.fac-role-list{margin-top:6px;max-height:220px;overflow-y:auto;border:1px solid var(--surface-border);border-radius:8px}.fac-role-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:13px}.fac-role-item:hover{background:var(--surface-hover)}.fac-role-item i{color:var(--text-color-secondary)}.fac-role-app{margin-inline-start:auto;font-size:11px;color:var(--text-color-secondary);text-transform:uppercase;letter-spacing:.5px}.fac-perm-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}.fac-icon-btn{background:#ffffff0f;border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;font-size:12px}.fac-icon-btn:hover{background:#ffffff24}.fac-icon-btn.danger:hover{background:#ff3b3033;color:#ff3b30}.fac-icon-btn:disabled{opacity:.4;cursor:default}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { 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 });
1084
1327
  }
1085
1328
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, decorators: [{
1086
1329
  type: Component,
@@ -1101,6 +1344,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1101
1344
  }], loadChartOptions: [{
1102
1345
  type: Input,
1103
1346
  args: [{ required: true }]
1347
+ }], defaultChartSystemKey: [{
1348
+ type: Input
1349
+ }], defaultChartId: [{
1350
+ type: Input
1104
1351
  }], loadOuLabelMap: [{
1105
1352
  type: Input,
1106
1353
  args: [{ required: true }]
@@ -1175,6 +1422,25 @@ class AudienceBuilderComponent {
1175
1422
  searchUsers;
1176
1423
  loadOuTree;
1177
1424
  loadChartOptions;
1425
+ /**
1426
+ * Initial chart selection for the OU picker. Resolution order:
1427
+ * <list type="number">
1428
+ * <item>If <see cref="defaultChartId"/> is explicitly provided (string or null), use it.</item>
1429
+ * <item>Else if <see cref="defaultChartSystemKey"/> is set, pick the first option whose
1430
+ * <c>systemKey</c> matches (e.g. <c>'apps'</c> → the platform Apps chart).</item>
1431
+ * <item>Else fall back to <c>options[0]?.id ?? null</c> (host-defined default order).</item>
1432
+ * </list>
1433
+ * Hosts that already know the tenant-scoped chart Guid use {@link defaultChartId};
1434
+ * hosts that only know the semantic kind (typically <c>'apps'</c>) use
1435
+ * {@link defaultChartSystemKey}.
1436
+ */
1437
+ defaultChartSystemKey;
1438
+ /**
1439
+ * Hard-coded chart id to pre-select. Takes priority over {@link defaultChartSystemKey}.
1440
+ * Use <c>null</c> to force the default/official chart (id null) even when other charts
1441
+ * are listed first.
1442
+ */
1443
+ defaultChartId;
1178
1444
  /**
1179
1445
  * Optional apps-chart role-OU index. When supplied, the picker exposes a "Roles" tab
1180
1446
  * surfacing role chips; without it, that tab is hidden even if `'roles'` is in
@@ -1280,7 +1546,7 @@ class AudienceBuilderComponent {
1280
1546
  .subscribe({
1281
1547
  next: (opts) => {
1282
1548
  this.chartOptions.set(opts);
1283
- const initial = opts[0]?.id ?? null;
1549
+ const initial = this.resolveInitialChartId(opts);
1284
1550
  this.selectedChartId.set(initial);
1285
1551
  this.selectedChartId$.next(initial);
1286
1552
  },
@@ -1379,6 +1645,26 @@ class AudienceBuilderComponent {
1379
1645
  const d = ou.depth ?? 0;
1380
1646
  return 12 + d * 18;
1381
1647
  }
1648
+ /**
1649
+ * Picks the chart id to seed the OU picker with. Honours explicit {@link defaultChartId}
1650
+ * first (even when null, since null is a valid id meaning "default chart"), then
1651
+ * {@link defaultChartSystemKey}, then falls back to host order. Lookup uses
1652
+ * <see cref="hasOwnProperty"/> on the input keys to distinguish "host did not set this
1653
+ * input" from "host set it to null" — Angular reflects an unset input as <c>undefined</c>.
1654
+ */
1655
+ resolveInitialChartId(opts) {
1656
+ if (this.defaultChartId !== undefined) {
1657
+ const exists = opts.some((o) => o.id === this.defaultChartId);
1658
+ return exists ? this.defaultChartId : (opts[0]?.id ?? null);
1659
+ }
1660
+ const key = this.defaultChartSystemKey;
1661
+ if (key) {
1662
+ const match = opts.find((o) => o.systemKey === key);
1663
+ if (match)
1664
+ return match.id;
1665
+ }
1666
+ return opts[0]?.id ?? null;
1667
+ }
1382
1668
  // ── App-everyone picker ──────────────────────────────────────────────────────
1383
1669
  pickAppEveryone(appId) {
1384
1670
  this.addToActiveBucket((existing) => this.upsertAppEveryoneTerm(existing, appId));
@@ -1676,7 +1962,7 @@ class AudienceBuilderComponent {
1676
1962
  return this.validateFilter(candidate) === null;
1677
1963
  }
1678
1964
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AudienceBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1679
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: AudienceBuilderComponent, isStandalone: true, selector: "fly-audience-builder", inputs: { value: "value", showExcludes: "showExcludes", supportedKinds: "supportedKinds", selfUserId: "selfUserId", appEveryoneOptions: "appEveryoneOptions", searchUsers: "searchUsers", loadOuTree: "loadOuTree", loadChartOptions: "loadChartOptions", loadRoleOus: "loadRoleOus" }, outputs: { audienceChange: "audienceChange", validityChange: "validityChange" }, providers: [
1965
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: AudienceBuilderComponent, isStandalone: true, selector: "fly-audience-builder", inputs: { value: "value", showExcludes: "showExcludes", supportedKinds: "supportedKinds", selfUserId: "selfUserId", appEveryoneOptions: "appEveryoneOptions", searchUsers: "searchUsers", loadOuTree: "loadOuTree", loadChartOptions: "loadChartOptions", defaultChartSystemKey: "defaultChartSystemKey", defaultChartId: "defaultChartId", loadRoleOus: "loadRoleOus" }, outputs: { audienceChange: "audienceChange", validityChange: "validityChange" }, providers: [
1680
1966
  {
1681
1967
  provide: NG_VALUE_ACCESSOR,
1682
1968
  useExisting: forwardRef(() => AudienceBuilderComponent),
@@ -1722,6 +2008,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1722
2008
  }], loadChartOptions: [{
1723
2009
  type: Input,
1724
2010
  args: [{ required: true }]
2011
+ }], defaultChartSystemKey: [{
2012
+ type: Input
2013
+ }], defaultChartId: [{
2014
+ type: Input
1725
2015
  }], loadRoleOus: [{
1726
2016
  type: Input
1727
2017
  }], audienceChange: [{
@@ -1741,11 +2031,11 @@ class FlyBlockUiComponent {
1741
2031
  return k && k.length > 0 ? k : 'common.loading';
1742
2032
  }, ...(ngDevMode ? [{ debugName: "resolvedMessageKey" }] : /* istanbul ignore next */ []));
1743
2033
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyBlockUiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1744
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", 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);backdrop-filter:blur(6px) saturate(120%);-webkit-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 });
2034
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", 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 });
1745
2035
  }
1746
2036
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyBlockUiComponent, decorators: [{
1747
2037
  type: Component,
1748
- 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);backdrop-filter:blur(6px) saturate(120%);-webkit-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"] }]
2038
+ 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"] }]
1749
2039
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: true }] }], messageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageKey", required: false }] }] } });
1750
2040
 
1751
2041
  /**
@@ -1926,110 +2216,110 @@ class FlyImageUploadComponent {
1926
2216
  });
1927
2217
  }
1928
2218
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1929
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyImageUploadComponent, isStandalone: true, selector: "fly-image-upload", inputs: { aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, maxSizeBytes: { classPropertyName: "maxSizeBytes", publicName: "maxSizeBytes", isSignal: true, isRequired: false, transformFunction: null }, currentImageId: { classPropertyName: "currentImageId", publicName: "currentImageId", isSignal: true, isRequired: false, transformFunction: null }, sourceApp: { classPropertyName: "sourceApp", publicName: "sourceApp", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityType: { classPropertyName: "sourceEntityType", publicName: "sourceEntityType", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityId: { classPropertyName: "sourceEntityId", publicName: "sourceEntityId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { uploaded: "uploaded", removed: "removed" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "cropImage", first: true, predicate: ["cropImage"], descendants: true, isSignal: true }], ngImport: i0, template: `
1930
- <div class="fly-image-upload">
1931
- @if (previewUrl()) {
1932
- <div class="fly-image-upload__preview">
1933
- <img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
1934
- <div class="fly-image-upload__overlay">
1935
- <button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
1936
- <span class="pi pi-pencil" aria-hidden="true"></span>
1937
- </button>
1938
- <button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
1939
- <span class="pi pi-trash" aria-hidden="true"></span>
1940
- </button>
1941
- </div>
1942
- </div>
1943
- } @else {
1944
- <button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
1945
- @if (uploading()) {
1946
- <span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
1947
- <span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
1948
- } @else {
1949
- <span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
1950
- <span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
1951
- <span class="fly-image-upload__hint">{{ sizeHint() }}</span>
1952
- }
1953
- </button>
1954
- }
1955
-
1956
- @if (error()) {
1957
- <div class="fly-image-upload__error">{{ error() }}</div>
1958
- }
1959
-
1960
- <input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
1961
-
1962
- @if (showCropper()) {
1963
- <div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
1964
- <div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
1965
- <div class="fly-image-upload__crop-header">
1966
- <h3>{{ 'files.imageUpload.crop' | translate }}</h3>
1967
- </div>
1968
- <div class="fly-image-upload__crop-body">
1969
- <img #cropImage [src]="rawImageUrl()" alt="" />
1970
- </div>
1971
- <div class="fly-image-upload__crop-footer">
1972
- <button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
1973
- <button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
1974
- </div>
1975
- </div>
1976
- </div>
1977
- }
1978
- </div>
2219
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyImageUploadComponent, isStandalone: true, selector: "fly-image-upload", inputs: { aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, maxSizeBytes: { classPropertyName: "maxSizeBytes", publicName: "maxSizeBytes", isSignal: true, isRequired: false, transformFunction: null }, currentImageId: { classPropertyName: "currentImageId", publicName: "currentImageId", isSignal: true, isRequired: false, transformFunction: null }, sourceApp: { classPropertyName: "sourceApp", publicName: "sourceApp", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityType: { classPropertyName: "sourceEntityType", publicName: "sourceEntityType", isSignal: true, isRequired: false, transformFunction: null }, sourceEntityId: { classPropertyName: "sourceEntityId", publicName: "sourceEntityId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { uploaded: "uploaded", removed: "removed" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "cropImage", first: true, predicate: ["cropImage"], descendants: true, isSignal: true }], ngImport: i0, template: `
2220
+ <div class="fly-image-upload">
2221
+ @if (previewUrl()) {
2222
+ <div class="fly-image-upload__preview">
2223
+ <img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
2224
+ <div class="fly-image-upload__overlay">
2225
+ <button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
2226
+ <span class="pi pi-pencil" aria-hidden="true"></span>
2227
+ </button>
2228
+ <button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
2229
+ <span class="pi pi-trash" aria-hidden="true"></span>
2230
+ </button>
2231
+ </div>
2232
+ </div>
2233
+ } @else {
2234
+ <button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
2235
+ @if (uploading()) {
2236
+ <span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
2237
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
2238
+ } @else {
2239
+ <span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
2240
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
2241
+ <span class="fly-image-upload__hint">{{ sizeHint() }}</span>
2242
+ }
2243
+ </button>
2244
+ }
2245
+
2246
+ @if (error()) {
2247
+ <div class="fly-image-upload__error">{{ error() }}</div>
2248
+ }
2249
+
2250
+ <input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
2251
+
2252
+ @if (showCropper()) {
2253
+ <div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
2254
+ <div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
2255
+ <div class="fly-image-upload__crop-header">
2256
+ <h3>{{ 'files.imageUpload.crop' | translate }}</h3>
2257
+ </div>
2258
+ <div class="fly-image-upload__crop-body">
2259
+ <img #cropImage [src]="rawImageUrl()" alt="" />
2260
+ </div>
2261
+ <div class="fly-image-upload__crop-footer">
2262
+ <button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
2263
+ <button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
2264
+ </div>
2265
+ </div>
2266
+ </div>
2267
+ }
2268
+ </div>
1979
2269
  `, isInline: true, styles: [".cropper-container{-webkit-touch-callout:none;direction:ltr;font-size:0;line-height:0;position:relative;touch-action:none;-webkit-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{inset:0;position:absolute}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:\" \";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}\n", ".fly-image-upload{display:flex;flex-direction:column;gap:6px}.fly-image-upload__hidden{display:none}.fly-image-upload__dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:24px;border:2px dashed var(--separator-primary, #e5e7eb);border-radius:12px;background:var(--fill-quaternary, #f9fafb);cursor:pointer;transition:border-color .15s,background .15s;min-height:120px}.fly-image-upload__dropzone:hover{border-color:var(--accent-primary, #0071e3);background:var(--fill-tertiary, #f3f4f6)}.fly-image-upload__icon{font-size:28px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__label{font-size:13px;color:var(--label-secondary, #6b7280)}.fly-image-upload__hint{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__preview{position:relative;border-radius:12px;overflow:hidden}.fly-image-upload__img{display:block;width:100%;max-height:280px;object-fit:cover;border-radius:12px}.fly-image-upload__overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:12px;background:#00000059;opacity:0;transition:opacity .15s}.fly-image-upload__preview:hover .fly-image-upload__overlay{opacity:1}.fly-image-upload__action-btn{width:36px;height:36px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;background:#ffffffe6;color:var(--label-primary, #1f2937);font-size:14px;transition:background .15s}.fly-image-upload__action-btn:hover{background:#fff}.fly-image-upload__action-btn--danger:hover{background:#fee2e2;color:#dc2626}.fly-image-upload__error{font-size:12px;color:var(--system-red, #ef4444)}.fly-image-upload__crop-backdrop{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:#0009;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.fly-image-upload__crop-modal{background:var(--bg-primary, #fff);color:var(--label-primary, #1f2937);border-radius:16px;overflow:hidden;max-width:700px;width:90vw;box-shadow:0 20px 60px #0006;border:1px solid var(--separator-primary, rgba(0,0,0,.1))}.fly-image-upload__crop-header{padding:16px 20px;border-bottom:1px solid var(--separator-primary, #e5e7eb);background:var(--fill-quaternary, transparent)}.fly-image-upload__crop-header h3{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1f2937)}.fly-image-upload__crop-body{max-height:60vh;overflow:hidden;background:var(--fill-tertiary, #f3f4f6)}.fly-image-upload__crop-body img{display:block;max-width:100%}.fly-image-upload__crop-footer{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--separator-primary, #e5e7eb);background:var(--fill-quaternary, transparent)}html.spatial-theme .fly-image-upload__crop-modal{background:#ffffffa6;backdrop-filter:blur(40px) saturate(1.8);-webkit-backdrop-filter:blur(40px) saturate(1.8);border:1px solid rgba(255,255,255,.4)}html.spatial-theme .fly-image-upload__crop-header,html.spatial-theme .fly-image-upload__crop-footer{background:transparent}html.spatial-theme .fly-image-upload__crop-body{background:#0000000d}html.dark-theme .fly-image-upload__crop-modal{background:var(--bg-primary, #1c1c1e);border:1px solid var(--separator-primary, rgba(255,255,255,.1))}html.dark-theme .fly-image-upload__crop-body{background:var(--fill-tertiary, #2c2c2e)}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1980
2270
  }
1981
2271
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, decorators: [{
1982
2272
  type: Component,
1983
- args: [{ selector: 'fly-image-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
1984
- <div class="fly-image-upload">
1985
- @if (previewUrl()) {
1986
- <div class="fly-image-upload__preview">
1987
- <img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
1988
- <div class="fly-image-upload__overlay">
1989
- <button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
1990
- <span class="pi pi-pencil" aria-hidden="true"></span>
1991
- </button>
1992
- <button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
1993
- <span class="pi pi-trash" aria-hidden="true"></span>
1994
- </button>
1995
- </div>
1996
- </div>
1997
- } @else {
1998
- <button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
1999
- @if (uploading()) {
2000
- <span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
2001
- <span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
2002
- } @else {
2003
- <span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
2004
- <span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
2005
- <span class="fly-image-upload__hint">{{ sizeHint() }}</span>
2006
- }
2007
- </button>
2008
- }
2009
-
2010
- @if (error()) {
2011
- <div class="fly-image-upload__error">{{ error() }}</div>
2012
- }
2013
-
2014
- <input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
2015
-
2016
- @if (showCropper()) {
2017
- <div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
2018
- <div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
2019
- <div class="fly-image-upload__crop-header">
2020
- <h3>{{ 'files.imageUpload.crop' | translate }}</h3>
2021
- </div>
2022
- <div class="fly-image-upload__crop-body">
2023
- <img #cropImage [src]="rawImageUrl()" alt="" />
2024
- </div>
2025
- <div class="fly-image-upload__crop-footer">
2026
- <button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
2027
- <button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
2028
- </div>
2029
- </div>
2030
- </div>
2031
- }
2032
- </div>
2273
+ args: [{ selector: 'fly-image-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
2274
+ <div class="fly-image-upload">
2275
+ @if (previewUrl()) {
2276
+ <div class="fly-image-upload__preview">
2277
+ <img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
2278
+ <div class="fly-image-upload__overlay">
2279
+ <button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
2280
+ <span class="pi pi-pencil" aria-hidden="true"></span>
2281
+ </button>
2282
+ <button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
2283
+ <span class="pi pi-trash" aria-hidden="true"></span>
2284
+ </button>
2285
+ </div>
2286
+ </div>
2287
+ } @else {
2288
+ <button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
2289
+ @if (uploading()) {
2290
+ <span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
2291
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
2292
+ } @else {
2293
+ <span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
2294
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
2295
+ <span class="fly-image-upload__hint">{{ sizeHint() }}</span>
2296
+ }
2297
+ </button>
2298
+ }
2299
+
2300
+ @if (error()) {
2301
+ <div class="fly-image-upload__error">{{ error() }}</div>
2302
+ }
2303
+
2304
+ <input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
2305
+
2306
+ @if (showCropper()) {
2307
+ <div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
2308
+ <div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
2309
+ <div class="fly-image-upload__crop-header">
2310
+ <h3>{{ 'files.imageUpload.crop' | translate }}</h3>
2311
+ </div>
2312
+ <div class="fly-image-upload__crop-body">
2313
+ <img #cropImage [src]="rawImageUrl()" alt="" />
2314
+ </div>
2315
+ <div class="fly-image-upload__crop-footer">
2316
+ <button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
2317
+ <button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
2318
+ </div>
2319
+ </div>
2320
+ </div>
2321
+ }
2322
+ </div>
2033
2323
  `, styles: [".cropper-container{-webkit-touch-callout:none;direction:ltr;font-size:0;line-height:0;position:relative;touch-action:none;-webkit-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{inset:0;position:absolute}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:\" \";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}\n", ".fly-image-upload{display:flex;flex-direction:column;gap:6px}.fly-image-upload__hidden{display:none}.fly-image-upload__dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:24px;border:2px dashed var(--separator-primary, #e5e7eb);border-radius:12px;background:var(--fill-quaternary, #f9fafb);cursor:pointer;transition:border-color .15s,background .15s;min-height:120px}.fly-image-upload__dropzone:hover{border-color:var(--accent-primary, #0071e3);background:var(--fill-tertiary, #f3f4f6)}.fly-image-upload__icon{font-size:28px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__label{font-size:13px;color:var(--label-secondary, #6b7280)}.fly-image-upload__hint{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-image-upload__preview{position:relative;border-radius:12px;overflow:hidden}.fly-image-upload__img{display:block;width:100%;max-height:280px;object-fit:cover;border-radius:12px}.fly-image-upload__overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:12px;background:#00000059;opacity:0;transition:opacity .15s}.fly-image-upload__preview:hover .fly-image-upload__overlay{opacity:1}.fly-image-upload__action-btn{width:36px;height:36px;border-radius:50%;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;background:#ffffffe6;color:var(--label-primary, #1f2937);font-size:14px;transition:background .15s}.fly-image-upload__action-btn:hover{background:#fff}.fly-image-upload__action-btn--danger:hover{background:#fee2e2;color:#dc2626}.fly-image-upload__error{font-size:12px;color:var(--system-red, #ef4444)}.fly-image-upload__crop-backdrop{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;background:#0009;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}.fly-image-upload__crop-modal{background:var(--bg-primary, #fff);color:var(--label-primary, #1f2937);border-radius:16px;overflow:hidden;max-width:700px;width:90vw;box-shadow:0 20px 60px #0006;border:1px solid var(--separator-primary, rgba(0,0,0,.1))}.fly-image-upload__crop-header{padding:16px 20px;border-bottom:1px solid var(--separator-primary, #e5e7eb);background:var(--fill-quaternary, transparent)}.fly-image-upload__crop-header h3{margin:0;font-size:16px;font-weight:600;color:var(--label-primary, #1f2937)}.fly-image-upload__crop-body{max-height:60vh;overflow:hidden;background:var(--fill-tertiary, #f3f4f6)}.fly-image-upload__crop-body img{display:block;max-width:100%}.fly-image-upload__crop-footer{display:flex;justify-content:flex-end;gap:10px;padding:14px 20px;border-top:1px solid var(--separator-primary, #e5e7eb);background:var(--fill-quaternary, transparent)}html.spatial-theme .fly-image-upload__crop-modal{background:#ffffffa6;backdrop-filter:blur(40px) saturate(1.8);-webkit-backdrop-filter:blur(40px) saturate(1.8);border:1px solid rgba(255,255,255,.4)}html.spatial-theme .fly-image-upload__crop-header,html.spatial-theme .fly-image-upload__crop-footer{background:transparent}html.spatial-theme .fly-image-upload__crop-body{background:#0000000d}html.dark-theme .fly-image-upload__crop-modal{background:var(--bg-primary, #1c1c1e);border:1px solid var(--separator-primary, rgba(255,255,255,.1))}html.dark-theme .fly-image-upload__crop-body{background:var(--fill-tertiary, #2c2c2e)}\n"] }]
2034
2324
  }], ctorParameters: () => [], propDecorators: { aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], maxSizeBytes: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSizeBytes", required: false }] }], currentImageId: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentImageId", required: false }] }], sourceApp: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceApp", required: false }] }], sourceEntityType: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityType", required: false }] }], sourceEntityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityId", required: false }] }], uploaded: [{ type: i0.Output, args: ["uploaded"] }], removed: [{ type: i0.Output, args: ["removed"] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], cropImage: [{ type: i0.ViewChild, args: ['cropImage', { isSignal: true }] }] } });
2035
2325
 
@@ -2365,6 +2655,1113 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2365
2655
  `, styles: [".fly-file-upload{display:flex;flex-direction:column;gap:8px}.fly-file-upload__hidden{display:none}.fly-file-upload__dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:20px;border:2px dashed var(--separator-primary, #e5e7eb);border-radius:10px;background:var(--fill-quaternary, #f9fafb);cursor:pointer;transition:border-color .15s,background .15s}.fly-file-upload__dropzone:hover,.fly-file-upload__dropzone--drag{border-color:var(--accent-primary, #0071e3);background:var(--fill-tertiary, #f3f4f6)}.fly-file-upload__icon{font-size:24px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__label{font-size:13px;color:var(--label-secondary, #6b7280)}.fly-file-upload__hint{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__error{font-size:12px;color:var(--system-red, #ef4444);padding:4px 0}.fly-file-upload__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}.fly-file-upload__item{display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--separator-primary, #e5e7eb);border-radius:8px;background:var(--fill-quaternary, #f9fafb);font-size:13px}.fly-file-upload__file-icon{font-size:18px;color:var(--label-tertiary, #9ca3af);flex-shrink:0}.fly-file-upload__file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.fly-file-upload__file-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--label-primary, #1f2937)}.fly-file-upload__file-size{font-size:11px;color:var(--label-tertiary, #9ca3af)}.fly-file-upload__progress-track{width:60px;height:4px;border-radius:2px;background:var(--fill-tertiary, #e5e7eb);overflow:hidden}.fly-file-upload__progress-fill{height:100%;background:var(--accent-primary, #0071e3);border-radius:2px;transition:width .2s}.fly-file-upload__file-ok{color:var(--system-green, #22c55e);font-size:14px}.fly-file-upload__file-error{color:var(--system-red, #ef4444);font-size:14px}.fly-file-upload__remove-btn{background:none;border:none;cursor:pointer;color:var(--label-tertiary, #9ca3af);width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:color .15s,background .15s;flex-shrink:0}.fly-file-upload__remove-btn:hover{color:var(--system-red, #ef4444);background:var(--fill-tertiary, #f3f4f6)}\n"] }]
2366
2656
  }], ctorParameters: () => [], propDecorators: { maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], maxFileSizeBytes: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSizeBytes", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], sourceApp: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceApp", required: false }] }], sourceEntityType: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityType", required: false }] }], sourceEntityId: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceEntityId", required: false }] }], existingFileIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "existingFileIds", required: false }] }], files: [{ type: i0.Input, args: [{ isSignal: true, alias: "files", required: false }] }, { type: i0.Output, args: ["filesChange"] }], filesChanged: [{ type: i0.Output, args: ["filesChanged"] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
2367
2657
 
2658
+ /**
2659
+ * Singleton registry of slash commands offered by the agent input palette.
2660
+ *
2661
+ * Lives in the DS so it crosses the federation boundary as a single instance via
2662
+ * `sharedMappings: ['@mohamedatia/fly-design-system']`. Federated remotes register
2663
+ * their commands at boot (and dispose on window close) without forking the shell.
2664
+ *
2665
+ * Storage is a signal store. `register` is O(1) for the unique-id case and O(n) when
2666
+ * replacing an existing id (filter then push). The append-then-replace strategy is
2667
+ * deliberate: registrations are rare (each one corresponds to a remote's app-init
2668
+ * effect), and the tradeoff buys us a stable id-collision contract — the *latest*
2669
+ * registration wins, and the previous handle's `dispose()` becomes a no-op rather
2670
+ * than removing the new entry.
2671
+ */
2672
+ class AgentCommandRegistry {
2673
+ _commands = signal([], ...(ngDevMode ? [{ debugName: "_commands" }] : /* istanbul ignore next */ []));
2674
+ /** All currently-registered commands, in insertion order. */
2675
+ all = this._commands.asReadonly();
2676
+ /**
2677
+ * Returns a signal of commands whose scope is `'global'` OR whose `scope.appId` is in
2678
+ * the live app set. The signal recomputes when either the registry or `liveAppIds`
2679
+ * changes — pass a `Signal<ReadonlySet<string>>` from the host's app-registry service
2680
+ * for reactive filtering.
2681
+ */
2682
+ visible(liveAppIds) {
2683
+ const liveSignal = isSignal(liveAppIds) ? liveAppIds : signal(liveAppIds).asReadonly();
2684
+ return computed(() => {
2685
+ const live = liveSignal();
2686
+ return this._commands().filter((cmd) => {
2687
+ if (cmd.scope === 'global')
2688
+ return true;
2689
+ return live.has(cmd.scope.appId);
2690
+ });
2691
+ });
2692
+ }
2693
+ /**
2694
+ * Register a single command. Returns a handle whose `dispose()` removes the row by
2695
+ * id. If the same id is later re-registered, the original handle's `dispose()`
2696
+ * becomes a no-op (the newer registration owns the row). Idempotent disposal.
2697
+ */
2698
+ register(cmd) {
2699
+ const generation = ++this._generation;
2700
+ this._commands.update((rows) => [...rows.filter((r) => r.id !== cmd.id), cmd]);
2701
+ this._owners.set(cmd.id, generation);
2702
+ return {
2703
+ dispose: () => {
2704
+ if (this._owners.get(cmd.id) === generation) {
2705
+ this._owners.delete(cmd.id);
2706
+ this._commands.update((rows) => rows.filter((r) => r.id !== cmd.id));
2707
+ }
2708
+ },
2709
+ };
2710
+ }
2711
+ /**
2712
+ * Bulk register. Rolls back on duplicate id within the input batch (throws before any
2713
+ * row lands). Cross-batch duplicates against existing rows follow the standard
2714
+ * "latest wins" rule and do NOT trigger rollback.
2715
+ *
2716
+ * Returns a handle whose `dispose()` tears down every row registered by this call.
2717
+ */
2718
+ registerAll(cmds) {
2719
+ const seen = new Set();
2720
+ for (const c of cmds) {
2721
+ if (seen.has(c.id)) {
2722
+ throw new Error(`AgentCommandRegistry.registerAll: duplicate id "${c.id}" in batch`);
2723
+ }
2724
+ seen.add(c.id);
2725
+ }
2726
+ const handles = cmds.map((c) => this.register(c));
2727
+ let disposed = false;
2728
+ return {
2729
+ dispose: () => {
2730
+ if (disposed)
2731
+ return;
2732
+ disposed = true;
2733
+ for (const h of handles)
2734
+ h.dispose();
2735
+ },
2736
+ };
2737
+ }
2738
+ /** Tear down by id. Idempotent. */
2739
+ unregister(id) {
2740
+ if (this._owners.delete(id)) {
2741
+ this._commands.update((rows) => rows.filter((r) => r.id !== id));
2742
+ }
2743
+ }
2744
+ /** Monotonic counter; identifies which registration call currently owns each id. */
2745
+ _generation = 0;
2746
+ /** id → generation. Used so a stale handle's `dispose()` is a no-op after replacement. */
2747
+ _owners = new Map();
2748
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentCommandRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2749
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentCommandRegistry, providedIn: 'root' });
2750
+ }
2751
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentCommandRegistry, decorators: [{
2752
+ type: Injectable,
2753
+ args: [{ providedIn: 'root' }]
2754
+ }] });
2755
+ /** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
2756
+ function isSignal(v) {
2757
+ return typeof v === 'function';
2758
+ }
2759
+
2760
+ /**
2761
+ * Singleton registry of chip-renderer components and keyboard-alternative draggable
2762
+ * items, keyed by `kind` and `appId`.
2763
+ *
2764
+ * Like {@link AgentCommandRegistry}, this lives in the DS so it crosses the federation
2765
+ * boundary as a single instance. Hosts (the shell `<fly-agent-input>`) lookup
2766
+ * renderers; remotes register them.
2767
+ *
2768
+ * Renderer lookup is `O(n)` over the registered list — the registry is small (one or
2769
+ * two entries per app) and lookups happen on drop, not per frame.
2770
+ *
2771
+ * Draggable storage is per-`appId` writable signal cached in a Map, so each `appId`
2772
+ * gets a stable {@link Signal} reference across reads (callers can `===`-compare).
2773
+ */
2774
+ class AgentDropRegistry {
2775
+ _renderers = signal([], ...(ngDevMode ? [{ debugName: "_renderers" }] : /* istanbul ignore next */ []));
2776
+ /** Per-appId writable store for draggables. Read-only mirror returned to callers. */
2777
+ _draggablesByApp = new Map();
2778
+ /**
2779
+ * Look up the chip-renderer component class for a `kind`. The newest registration for
2780
+ * a given `kind` wins, regardless of `appId` — we scan the list in reverse so a later
2781
+ * `register` call shadows an earlier one for the same `kind`.
2782
+ *
2783
+ * Returns `null` when no renderer is registered; the host falls back to a generic
2784
+ * `plainTextFallback` chip.
2785
+ */
2786
+ rendererFor(kind) {
2787
+ const list = this._renderers();
2788
+ for (let i = list.length - 1; i >= 0; i--) {
2789
+ if (list[i].kind === kind)
2790
+ return list[i].component;
2791
+ }
2792
+ return null;
2793
+ }
2794
+ /**
2795
+ * Register a renderer for a `(kind, appId)` pair. Re-registering the same pair
2796
+ * replaces the prior entry; the disposal handle for the prior registration becomes
2797
+ * a no-op.
2798
+ *
2799
+ * Returns a {@link AgentCommandHandle} (re-used to keep the disposable shape uniform
2800
+ * across registries) whose `dispose()` removes this exact registration.
2801
+ */
2802
+ register(reg) {
2803
+ const generation = ++this._generation;
2804
+ const key = compositeKey(reg.kind, reg.appId);
2805
+ this._renderers.update((rows) => [
2806
+ ...rows.filter((r) => compositeKey(r.kind, r.appId) !== key),
2807
+ reg,
2808
+ ]);
2809
+ this._owners.set(key, generation);
2810
+ return {
2811
+ dispose: () => {
2812
+ if (this._owners.get(key) === generation) {
2813
+ this._owners.delete(key);
2814
+ this._renderers.update((rows) => rows.filter((r) => compositeKey(r.kind, r.appId) !== key));
2815
+ }
2816
+ },
2817
+ };
2818
+ }
2819
+ /**
2820
+ * Apps publish their live "draggable from focused window" set so the keyboard
2821
+ * "Attach from app…" menu can offer them. Hosts call this each time the user-visible
2822
+ * draggable list changes; passing an empty array clears the entry for this `appId`.
2823
+ */
2824
+ publishDraggables(appId, items) {
2825
+ if (items.length === 0) {
2826
+ const existing = this._draggablesByApp.get(appId);
2827
+ if (existing)
2828
+ existing.set([]);
2829
+ return;
2830
+ }
2831
+ this.bucketFor(appId).set(items);
2832
+ }
2833
+ /**
2834
+ * Reactive read of the draggable set published for `appId`. Empty when none. The
2835
+ * returned signal is stable across calls (cached by `appId`), so consumers can use
2836
+ * it as a stable input to `computed()`.
2837
+ */
2838
+ draggablesFor(appId) {
2839
+ return this.bucketFor(appId).asReadonly();
2840
+ }
2841
+ /** Lazy-init the per-appId writable bucket. Returns the writable handle for internal use. */
2842
+ bucketFor(appId) {
2843
+ let bucket = this._draggablesByApp.get(appId);
2844
+ if (!bucket) {
2845
+ bucket = signal([]);
2846
+ this._draggablesByApp.set(appId, bucket);
2847
+ }
2848
+ return bucket;
2849
+ }
2850
+ _generation = 0;
2851
+ _owners = new Map();
2852
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentDropRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2853
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentDropRegistry, providedIn: 'root' });
2854
+ }
2855
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentDropRegistry, decorators: [{
2856
+ type: Injectable,
2857
+ args: [{ providedIn: 'root' }]
2858
+ }] });
2859
+ function compositeKey(kind, appId) {
2860
+ return `${kind}::${appId}`;
2861
+ }
2862
+
2863
+ /**
2864
+ * Agent input contracts.
2865
+ *
2866
+ * These types define the wire and DI surface for the new `<fly-agent-input>` orchestrator
2867
+ * (Phase 2+). Phase 1 ships only the contracts: payload envelope, command/drop registry
2868
+ * shapes, and chip-host inputs. The concrete components consume them later.
2869
+ *
2870
+ * Design rules:
2871
+ * - All fields are `readonly` — the registries store these in signal stores and we never
2872
+ * want a host mutating after registration. Hosts construct fresh objects to update.
2873
+ * - The discriminated kinds (`scope`, `mode`, `kind`) carry the entire variance — no
2874
+ * boolean flags. New variants extend the union without breaking existing consumers.
2875
+ * - The `Type<AgentChipHostInputs<T>>` shape is intentional: chip components declare
2876
+ * `payload`, `mode`, optional `onRemove` as Angular `input()` signals; the registry
2877
+ * wires them via `NgComponentOutlet` inputs in the consumer. The component reference
2878
+ * itself never leaks across the federation boundary by value — only by class identity.
2879
+ */
2880
+ /** Frozen MIME used by `flyAgentDraggable` and the drop-zone reader. Never change without a DS major. */
2881
+ const AGENT_DRAG_MIME = 'application/x-fly-agent-payload+json';
2882
+ /** Frozen payload envelope version. Bump only with a published DS major. */
2883
+ const AGENT_PAYLOAD_VERSION = 1;
2884
+ const DEFAULT_AGENT_PAYLOAD_LIMITS = Object.freeze({
2885
+ maxJsonBytes: 32 * 1024,
2886
+ maxPlainTextFallbackBytes: 8 * 1024,
2887
+ maxStringFieldBytes: 4 * 1024,
2888
+ });
2889
+
2890
+ /**
2891
+ * Pure utility helpers for agent drag payload size guarding.
2892
+ *
2893
+ * Used by `flyAgentDraggable` at `dragstart` (Phase 2) and by app pre-flight code that
2894
+ * wants a clean payload before constructing the directive input. No Angular DI, no
2895
+ * runtime side effects — safe to import from anywhere.
2896
+ *
2897
+ * Performance notes:
2898
+ * - The `TextEncoder` is lazily allocated and cached. The constructor runs once per
2899
+ * module instance even under federation (singleton DS).
2900
+ * - String trimming uses `Intl.Segmenter` when available (all evergreen browsers
2901
+ * including Safari 14.1+) so we cut on grapheme boundaries — emoji ZWJ sequences,
2902
+ * combining marks, regional indicator pairs all stay intact. Fallback is
2903
+ * UTF-16 code-unit slice, which is a graceful degradation: it can split a
2904
+ * surrogate pair, but the resulting JSON is still syntactically valid and
2905
+ * downstream renderers handle the replacement char.
2906
+ */
2907
+ /**
2908
+ * Thrown by {@link trimAgentPayload} when even a fully string-trimmed envelope still
2909
+ * exceeds `maxJsonBytes` (e.g. an envelope with thousands of nested empty objects /
2910
+ * array entries / property keys). Subclasses `RangeError` so callers using the standard
2911
+ * `instanceof RangeError` check still match, while exposing the structured fields
2912
+ * (`actualBytes`, `limitBytes`, `reason: 'json_too_large'`) needed for telemetry and
2913
+ * symmetric handling with {@link AgentPayloadValidationResult} failures.
2914
+ */
2915
+ class AgentPayloadOversizeError extends RangeError {
2916
+ actualBytes;
2917
+ limitBytes;
2918
+ reason = 'json_too_large';
2919
+ constructor(message, actualBytes, limitBytes) {
2920
+ super(message);
2921
+ this.actualBytes = actualBytes;
2922
+ this.limitBytes = limitBytes;
2923
+ this.name = 'AgentPayloadOversizeError';
2924
+ }
2925
+ }
2926
+ /** Cap on the dotted `fieldPath` string so a 10 MB property-key payload can't lock dev tools when `console.warn`d. */
2927
+ const FIELD_PATH_MAX_CHARS = 256;
2928
+ /** Append a path segment, capping the result at {@link FIELD_PATH_MAX_CHARS} chars (with `…` marker). */
2929
+ function appendPath(base, segment) {
2930
+ const next = base + segment;
2931
+ if (next.length <= FIELD_PATH_MAX_CHARS)
2932
+ return next;
2933
+ return next.slice(0, FIELD_PATH_MAX_CHARS - 1) + '…';
2934
+ }
2935
+ let _cachedEncoder = null;
2936
+ function getEncoder() {
2937
+ return (_cachedEncoder ??= new TextEncoder());
2938
+ }
2939
+ /** UTF-8 byte length of `s`. Cheap; cached encoder. */
2940
+ function utf8ByteLength(s) {
2941
+ return getEncoder().encode(s).length;
2942
+ }
2943
+ /**
2944
+ * Trim `input` so the UTF-8 byte length of the result is `<= maxBytes`. When trimming
2945
+ * occurs, append `ellipsis` (default `…`, U+2026) — its bytes are included in the cap.
2946
+ *
2947
+ * Idempotent: `trimAgentString(trimAgentString(s, n), n)` equals `trimAgentString(s, n)`.
2948
+ *
2949
+ * Edge cases:
2950
+ * - `maxBytes` shorter than the ellipsis itself returns an empty string.
2951
+ * - `Intl.Segmenter` (when present) ensures we never split a grapheme cluster.
2952
+ * - When the encoded length is already within the cap, returns `input` unchanged.
2953
+ */
2954
+ function trimAgentString(input, maxBytes, ellipsis = '…') {
2955
+ if (maxBytes <= 0)
2956
+ return '';
2957
+ const enc = getEncoder();
2958
+ const inputLen = enc.encode(input).length;
2959
+ if (inputLen <= maxBytes)
2960
+ return input;
2961
+ const ellipsisLen = enc.encode(ellipsis).length;
2962
+ if (maxBytes < ellipsisLen)
2963
+ return '';
2964
+ const budget = maxBytes - ellipsisLen;
2965
+ // Segmenter path: walk graphemes, accumulate until next would overflow.
2966
+ const segmenterCtor = typeof Intl !== 'undefined' && Intl.Segmenter;
2967
+ if (segmenterCtor) {
2968
+ const segmenter = new segmenterCtor(undefined, { granularity: 'grapheme' });
2969
+ let used = 0;
2970
+ let acc = '';
2971
+ for (const seg of segmenter.segment(input)) {
2972
+ const segLen = enc.encode(seg.segment).length;
2973
+ if (used + segLen > budget)
2974
+ break;
2975
+ acc += seg.segment;
2976
+ used += segLen;
2977
+ }
2978
+ return acc + ellipsis;
2979
+ }
2980
+ // Fallback: code-unit slice; verify byte length and shrink until under budget.
2981
+ let cut = input.length;
2982
+ while (cut > 0 && enc.encode(input.slice(0, cut)).length > budget) {
2983
+ cut--;
2984
+ }
2985
+ return input.slice(0, cut) + ellipsis;
2986
+ }
2987
+ /**
2988
+ * Validate an {@link AgentDragPayload} against the supplied limits (or
2989
+ * {@link DEFAULT_AGENT_PAYLOAD_LIMITS} when omitted). Returns `{ ok: true }` or a
2990
+ * structured failure with the offending field path and byte counts.
2991
+ *
2992
+ * Order of checks (cheap → expensive):
2993
+ * 1. `version` matches the frozen {@link AGENT_PAYLOAD_VERSION}.
2994
+ * 2. `kind` is a non-empty string.
2995
+ * 3. `plainTextFallback` fits its cap.
2996
+ * 4. Every string in `payload` (recursively) fits the per-field cap.
2997
+ * 5. The full `JSON.stringify(envelope)` fits the JSON cap.
2998
+ *
2999
+ * The walker stops on the first oversize string field — the intent is to surface
3000
+ * actionable feedback, not enumerate every offender.
3001
+ */
3002
+ function validateAgentPayload(payload, limits) {
3003
+ const eff = { ...DEFAULT_AGENT_PAYLOAD_LIMITS, ...(limits ?? {}) };
3004
+ if (payload.version !== AGENT_PAYLOAD_VERSION) {
3005
+ return { ok: false, reason: 'invalid_version' };
3006
+ }
3007
+ if (typeof payload.kind !== 'string' || payload.kind.length === 0) {
3008
+ return { ok: false, reason: 'invalid_kind' };
3009
+ }
3010
+ const fbBytes = utf8ByteLength(payload.plainTextFallback ?? '');
3011
+ if (fbBytes > eff.maxPlainTextFallbackBytes) {
3012
+ return {
3013
+ ok: false,
3014
+ reason: 'fallback_too_large',
3015
+ fieldPath: 'plainTextFallback',
3016
+ actualBytes: fbBytes,
3017
+ limitBytes: eff.maxPlainTextFallbackBytes,
3018
+ };
3019
+ }
3020
+ const fieldFailure = walkStringsForOverage(payload.payload, 'payload', eff.maxStringFieldBytes);
3021
+ if (fieldFailure)
3022
+ return fieldFailure;
3023
+ const json = safeStringify(payload);
3024
+ const jsonBytes = utf8ByteLength(json);
3025
+ if (jsonBytes > eff.maxJsonBytes) {
3026
+ return {
3027
+ ok: false,
3028
+ reason: 'json_too_large',
3029
+ actualBytes: jsonBytes,
3030
+ limitBytes: eff.maxJsonBytes,
3031
+ };
3032
+ }
3033
+ return { ok: true };
3034
+ }
3035
+ /**
3036
+ * Returns a NEW payload with all string fields trimmed to fit the per-field cap, the
3037
+ * `plainTextFallback` trimmed to its cap, and the full envelope re-checked against the
3038
+ * JSON cap. Idempotent — a second call against the trimmed result yields equal output.
3039
+ *
3040
+ * Throws {@link AgentPayloadOversizeError} only if even an empty trim couldn't shrink it
3041
+ * under the cap (e.g. an envelope with 1000+ nested empty objects). The error subclasses
3042
+ * `RangeError`, so legacy `instanceof RangeError` checks still match. Apps that hit this
3043
+ * should re-shape `payload` rather than call again.
3044
+ */
3045
+ function trimAgentPayload(payload, limits) {
3046
+ const eff = { ...DEFAULT_AGENT_PAYLOAD_LIMITS, ...(limits ?? {}) };
3047
+ const trimmedFallback = trimAgentString(payload.plainTextFallback ?? '', eff.maxPlainTextFallbackBytes);
3048
+ const trimmedPayload = mapStrings(payload.payload, (s) => trimAgentString(s, eff.maxStringFieldBytes));
3049
+ const next = {
3050
+ ...payload,
3051
+ plainTextFallback: trimmedFallback,
3052
+ payload: trimmedPayload,
3053
+ };
3054
+ const jsonBytes = utf8ByteLength(safeStringify(next));
3055
+ if (jsonBytes > eff.maxJsonBytes) {
3056
+ throw new AgentPayloadOversizeError(`AgentDragPayload exceeds maxJsonBytes (${jsonBytes} > ${eff.maxJsonBytes}) after string trimming. Re-shape the payload.`, jsonBytes, eff.maxJsonBytes);
3057
+ }
3058
+ return next;
3059
+ }
3060
+ // ─── Internal helpers ───────────────────────────────────────────────────────
3061
+ /**
3062
+ * Recursively walks `value`, returning the first string field that exceeds `maxBytes`
3063
+ * (as a structured failure). Returns `null` when every string fits.
3064
+ *
3065
+ * Path uses dotted property-access notation: `payload.body`, `payload.tags[2]`. Cycles
3066
+ * are detected via a WeakSet so attacker-crafted self-references don't loop forever.
3067
+ * Path strings are capped at {@link FIELD_PATH_MAX_CHARS} characters via {@link appendPath}
3068
+ * to prevent a malicious payload with multi-MB property keys from producing a multi-MB
3069
+ * `fieldPath` that locks up dev tools when logged.
3070
+ */
3071
+ function walkStringsForOverage(value, path, maxBytes, seen = new WeakSet()) {
3072
+ if (typeof value === 'string') {
3073
+ const bytes = utf8ByteLength(value);
3074
+ if (bytes > maxBytes) {
3075
+ return {
3076
+ ok: false,
3077
+ reason: 'field_too_large',
3078
+ fieldPath: path,
3079
+ actualBytes: bytes,
3080
+ limitBytes: maxBytes,
3081
+ };
3082
+ }
3083
+ return null;
3084
+ }
3085
+ if (value === null || typeof value !== 'object')
3086
+ return null;
3087
+ if (seen.has(value))
3088
+ return null;
3089
+ seen.add(value);
3090
+ if (Array.isArray(value)) {
3091
+ for (let i = 0; i < value.length; i++) {
3092
+ const r = walkStringsForOverage(value[i], appendPath(path, `[${i}]`), maxBytes, seen);
3093
+ if (r)
3094
+ return r;
3095
+ }
3096
+ return null;
3097
+ }
3098
+ for (const [k, v] of Object.entries(value)) {
3099
+ const r = walkStringsForOverage(v, appendPath(path, `.${k}`), maxBytes, seen);
3100
+ if (r)
3101
+ return r;
3102
+ }
3103
+ return null;
3104
+ }
3105
+ /**
3106
+ * Recursively maps every string leaf via `fn`, preserving object/array structure.
3107
+ * Cycles are short-circuited to `null` rather than throwing — the caller already
3108
+ * has a JSON.stringify cap that will catch any pathological input.
3109
+ */
3110
+ function mapStrings(value, fn, seen = new WeakSet()) {
3111
+ if (typeof value === 'string')
3112
+ return fn(value);
3113
+ if (value === null || typeof value !== 'object')
3114
+ return value;
3115
+ if (seen.has(value))
3116
+ return null;
3117
+ seen.add(value);
3118
+ if (Array.isArray(value)) {
3119
+ return value.map((v) => mapStrings(v, fn, seen));
3120
+ }
3121
+ const out = {};
3122
+ for (const [k, v] of Object.entries(value)) {
3123
+ out[k] = mapStrings(v, fn, seen);
3124
+ }
3125
+ return out;
3126
+ }
3127
+ /** `JSON.stringify` with a cycle-safe replacer — defends against attacker-crafted graphs. */
3128
+ function safeStringify(value) {
3129
+ const seen = new WeakSet();
3130
+ return JSON.stringify(value, (_k, v) => {
3131
+ if (v !== null && typeof v === 'object') {
3132
+ if (seen.has(v))
3133
+ return '[Circular]';
3134
+ seen.add(v);
3135
+ }
3136
+ return v;
3137
+ });
3138
+ }
3139
+
3140
+ /**
3141
+ * Strict whitelist for the ghost element id. Letter-led ASCII, allows word/hyphen/colon/dot,
3142
+ * capped at 128 chars. Anything else is rejected before reaching `getElementById` so a
3143
+ * future maintainer can't swap to `querySelector` and quietly reintroduce a CSS-injection
3144
+ * vector. (`getElementById` itself is string-equality-safe; the whitelist is defence-in-depth.)
3145
+ */
3146
+ const GHOST_ID_PATTERN = /^[A-Za-z][\w\-:.]{0,127}$/;
3147
+ /**
3148
+ * Single canonical way to declare a drag source for the agent input.
3149
+ *
3150
+ * Apply on any element a remote wants to make draggable into the `<fly-agent-input>`:
3151
+ *
3152
+ * ```html
3153
+ * <article [flyAgentDraggable]="() => buildPayload(trend)" flyAgentDraggableGhostId="trend-ghost">…</article>
3154
+ * ```
3155
+ *
3156
+ * On `dragstart` the directive:
3157
+ * 1. Resolves the payload (calls the builder fn if supplied; otherwise reads the
3158
+ * static value).
3159
+ * 2. Validates against {@link DEFAULT_AGENT_PAYLOAD_LIMITS} via
3160
+ * {@link validateAgentPayload}. On failure it `console.warn`s with the structured
3161
+ * reason and writes ONLY the `text/plain` fallback — the receiver still sees a
3162
+ * legible drop, but the typed envelope is skipped so a misconfigured app can't
3163
+ * corrupt downstream consumers with an oversize payload.
3164
+ * 3. Writes the typed MIME (`application/x-fly-agent-payload+json`) plus the
3165
+ * `text/plain` fallback so non-Fly drop targets still get useful text.
3166
+ * 4. Sets `effectAllowed = 'copy'` (we never *move* domain objects across windows).
3167
+ * 5. When `flyAgentDraggableGhostId` resolves to an element in the DOM, calls
3168
+ * `setDragImage` to use that element as the drag preview; otherwise the browser
3169
+ * default applies.
3170
+ *
3171
+ * Notes:
3172
+ * - Builder form is preferred over static — it captures fresh state at `dragstart`,
3173
+ * not at directive init. A static `AgentDragPayload` is fine for items whose
3174
+ * domain object never changes after the row mounts.
3175
+ * - The directive sets `[attr.draggable]="true"` so callers don't have to.
3176
+ */
3177
+ class FlyAgentDraggableDirective {
3178
+ /**
3179
+ * Either a static payload or a builder evaluated at `dragstart` (preferred — captures
3180
+ * fresh state). `input.required` so misuse fails at template compile time.
3181
+ */
3182
+ flyAgentDraggable = input.required(...(ngDevMode ? [{ debugName: "flyAgentDraggable" }] : /* istanbul ignore next */ []));
3183
+ /**
3184
+ * Optional ghost element id (an existing element in the DOM whose snapshot becomes
3185
+ * the drag image). Falls back to the default browser drag image when absent or the
3186
+ * element can't be resolved.
3187
+ *
3188
+ * Must be a valid HTML id (ASCII, letter-led). `getElementById` is intentional —
3189
+ * do NOT swap to `querySelector` without re-introducing this whitelist.
3190
+ */
3191
+ flyAgentDraggableGhostId = input(undefined, ...(ngDevMode ? [{ debugName: "flyAgentDraggableGhostId" }] : /* istanbul ignore next */ []));
3192
+ doc = inject(DOCUMENT$1);
3193
+ onDragStart(ev) {
3194
+ const dt = ev.dataTransfer;
3195
+ if (!dt)
3196
+ return;
3197
+ const payload = this.resolvePayload();
3198
+ const validation = validateAgentPayload(payload);
3199
+ if (!validation.ok) {
3200
+ // eslint-disable-next-line no-console
3201
+ console.warn('[flyAgentDraggable] payload validation failed; degrading to text/plain', validation);
3202
+ dt.setData('text/plain', payload.plainTextFallback ?? '');
3203
+ dt.effectAllowed = 'copy';
3204
+ return;
3205
+ }
3206
+ dt.setData(AGENT_DRAG_MIME, JSON.stringify(payload));
3207
+ dt.setData('text/plain', payload.plainTextFallback ?? '');
3208
+ dt.effectAllowed = 'copy';
3209
+ const ghostId = this.flyAgentDraggableGhostId();
3210
+ if (ghostId) {
3211
+ if (!GHOST_ID_PATTERN.test(ghostId)) {
3212
+ // Don't echo the rejected id (caller-controlled, could be huge).
3213
+ // eslint-disable-next-line no-console
3214
+ console.warn('[flyAgentDraggable] ignoring ghost id', { reason: 'invalid_ghost_id' });
3215
+ }
3216
+ else {
3217
+ const ghost = this.doc.getElementById(ghostId);
3218
+ if (ghost) {
3219
+ // Center the drag image; ResizeObserver-aware sizing is left to the host element.
3220
+ dt.setDragImage(ghost, ghost.clientWidth / 2, ghost.clientHeight / 2);
3221
+ }
3222
+ }
3223
+ }
3224
+ }
3225
+ resolvePayload() {
3226
+ const raw = this.flyAgentDraggable();
3227
+ return typeof raw === 'function' ? raw() : raw;
3228
+ }
3229
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyAgentDraggableDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3230
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.5", type: FlyAgentDraggableDirective, isStandalone: true, selector: "[flyAgentDraggable]", inputs: { flyAgentDraggable: { classPropertyName: "flyAgentDraggable", publicName: "flyAgentDraggable", isSignal: true, isRequired: true, transformFunction: null }, flyAgentDraggableGhostId: { classPropertyName: "flyAgentDraggableGhostId", publicName: "flyAgentDraggableGhostId", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "dragstart": "onDragStart($event)" }, properties: { "attr.draggable": "\"true\"" } }, ngImport: i0 });
3231
+ }
3232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyAgentDraggableDirective, decorators: [{
3233
+ type: Directive,
3234
+ args: [{
3235
+ selector: '[flyAgentDraggable]',
3236
+ standalone: true,
3237
+ host: {
3238
+ '[attr.draggable]': '"true"',
3239
+ },
3240
+ }]
3241
+ }], propDecorators: { flyAgentDraggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "flyAgentDraggable", required: true }] }], flyAgentDraggableGhostId: [{ type: i0.Input, args: [{ isSignal: true, alias: "flyAgentDraggableGhostId", required: false }] }], onDragStart: [{
3242
+ type: HostListener,
3243
+ args: ['dragstart', ['$event']]
3244
+ }] } });
3245
+
3246
+ /**
3247
+ * Verbatim TypeScript port of the C# `LanguageDTO[] _AllLanguages` array supplied by
3248
+ * product. **31 entries**, in the canonical order. Three RTL flagged: `he-IL`, `ur-IN`,
3249
+ * `ar-AE`. Includes `cmn-Hant-TW` for Chinese and `ka-GE` for Georgian.
3250
+ *
3251
+ * The order matches the C# source byte-for-byte — do not re-sort. Downstream tests
3252
+ * (`fly-locale-catalog.spec.ts`) pin the count, the unique-key constraints, and the
3253
+ * RTL set; if you reorder or add an entry, those tests will guide the fix.
3254
+ *
3255
+ * The strings carry Cyrillic, Hebrew, Vietnamese, Thai, Indonesian, Bengali, Devanagari,
3256
+ * Urdu, Catalan, Polish, Korean, Russian, Japanese, Han, Turkish, Romanian, Norwegian,
3257
+ * Hungarian, Greek, Finnish, Danish, Dutch, Swedish, Portuguese, Italian, German,
3258
+ * Spanish, French, English, Arabic, and Georgian. The file is saved as UTF-8 without
3259
+ * BOM (matches every other locale file in the repo). The `…` (U+2026) ellipsis is
3260
+ * preserved verbatim.
3261
+ */
3262
+ const FLY_LOCALE_CATALOG = Object.freeze([
3263
+ {
3264
+ prefix: 'UK',
3265
+ dialect: 'uk-UA',
3266
+ englishName: 'Ukrainian',
3267
+ arabicName: 'الأوكرانية',
3268
+ nativeName: 'Українська',
3269
+ detected: 'Виявлена мова',
3270
+ searching: 'Шукаючи',
3271
+ elevating: 'Підняття до',
3272
+ translating: 'Переклад на',
3273
+ generatingAnswersForYou: 'Створення відповідей для вас...',
3274
+ learnMore: 'Дізнайтеся більше',
3275
+ waitingForResponseMsg: 'Будь ласка, наберіться терпіння, ми ще думаємо',
3276
+ isRtl: false,
3277
+ },
3278
+ {
3279
+ prefix: 'HE',
3280
+ dialect: 'he-IL',
3281
+ englishName: 'Hebrew',
3282
+ arabicName: 'العبرية',
3283
+ nativeName: 'עברית',
3284
+ detected: 'שפה מזוהה',
3285
+ searching: 'מחפש אחר',
3286
+ elevating: 'מרומם ל',
3287
+ translating: 'תרגום ל',
3288
+ generatingAnswersForYou: 'מייצר עבורך תשובות...',
3289
+ learnMore: 'למידע נוסף',
3290
+ waitingForResponseMsg: 'אנא התאזרו בסבלנות, אנחנו עדיין חושבים',
3291
+ isRtl: true,
3292
+ },
3293
+ {
3294
+ prefix: 'VI',
3295
+ dialect: 'vi-VN',
3296
+ englishName: 'Vietnamese',
3297
+ arabicName: 'الفيتنامية',
3298
+ nativeName: 'Tiếng Việt',
3299
+ detected: 'Ngôn ngữ được phát hiện',
3300
+ searching: 'Tìm kiếm',
3301
+ elevating: 'Nâng lên',
3302
+ translating: 'Dịch sang',
3303
+ generatingAnswersForYou: 'Đang tạo câu trả lời cho bạn...',
3304
+ learnMore: 'Tìm hiểu thêm',
3305
+ waitingForResponseMsg: 'Vui lòng kiên nhẫn, chúng tôi vẫn đang suy nghĩ',
3306
+ isRtl: false,
3307
+ },
3308
+ {
3309
+ prefix: 'TH',
3310
+ dialect: 'th-TH',
3311
+ englishName: 'Thai',
3312
+ arabicName: 'التايلاندية',
3313
+ nativeName: 'ไทย',
3314
+ detected: 'ภาษาที่ตรวจพบ',
3315
+ searching: 'ค้นหา',
3316
+ elevating: 'ยกระดับเป็น',
3317
+ translating: 'การแปล',
3318
+ generatingAnswersForYou: 'กำลังสร้างคำตอบให้กับคุณ...',
3319
+ learnMore: 'เรียนรู้เพิ่มเติม',
3320
+ waitingForResponseMsg: 'โปรดอดใจรอ, พวกเรายังคงกำลังคิดอยู่',
3321
+ isRtl: false,
3322
+ },
3323
+ {
3324
+ prefix: 'ID',
3325
+ dialect: 'id-ID',
3326
+ englishName: 'Indonesian',
3327
+ arabicName: 'الإندونيسية',
3328
+ nativeName: 'Bahasa Indonesia',
3329
+ detected: 'Bahasa yang terdeteksi',
3330
+ searching: 'Mencari',
3331
+ elevating: 'Meninggalkan ke',
3332
+ translating: 'Menerjemahkan ke',
3333
+ generatingAnswersForYou: 'Menghasilkan jawaban untuk Anda...',
3334
+ learnMore: 'Cari tahu lebih lanjut',
3335
+ waitingForResponseMsg: 'Harap bersabar, kami masih sedang berpikir',
3336
+ isRtl: false,
3337
+ },
3338
+ {
3339
+ prefix: 'BN',
3340
+ dialect: 'bn-BD',
3341
+ englishName: 'Bengali',
3342
+ arabicName: 'البنغالية',
3343
+ nativeName: 'বাংলা',
3344
+ detected: 'সনাক্ত করা ভাষা',
3345
+ searching: 'খুঁজছি',
3346
+ elevating: 'উন্নত',
3347
+ translating: 'অনুবাদ',
3348
+ generatingAnswersForYou: 'আপনার জন্য উত্তর তৈরি করা হচ্ছে...',
3349
+ learnMore: 'আরো জানুন',
3350
+ waitingForResponseMsg: 'অনুগ্রহ করে ধৈর্য ধরুন, আমরা এখনও চিন্তা করছি',
3351
+ isRtl: false,
3352
+ },
3353
+ {
3354
+ prefix: 'HI',
3355
+ dialect: 'hi-IN',
3356
+ englishName: 'Hindi',
3357
+ arabicName: 'الهندية',
3358
+ nativeName: 'हिन्दी',
3359
+ detected: 'पता चला भाषा',
3360
+ searching: 'के लिए खोज रहे हैं',
3361
+ elevating: 'के लिए ऊंचा',
3362
+ translating: 'अनुवाद करना',
3363
+ generatingAnswersForYou: 'आपके लिए उत्तर तैयार किया जा रहा है...',
3364
+ learnMore: 'और जानें',
3365
+ waitingForResponseMsg: 'कृपया धैर्य रखें, हम अभी सोच रहे हैं',
3366
+ isRtl: false,
3367
+ },
3368
+ {
3369
+ prefix: 'UR',
3370
+ dialect: 'ur-IN',
3371
+ englishName: 'Urdu',
3372
+ arabicName: 'الأردية',
3373
+ nativeName: 'اردو',
3374
+ detected: 'پتہ چلا زبان',
3375
+ searching: 'کے لیے تلاش',
3376
+ elevating: 'بلند کرنا',
3377
+ translating: 'ترجمہ کرنا',
3378
+ generatingAnswersForYou: 'آپ کے لیے جوابات تیار کیے جا رہے ہیں...',
3379
+ learnMore: 'مزید جانیں',
3380
+ waitingForResponseMsg: 'براہ کرم صبر کریں، ہم ابھی سوچ رہے ہیں۔',
3381
+ isRtl: true,
3382
+ },
3383
+ {
3384
+ prefix: 'CA',
3385
+ dialect: 'ca-ES',
3386
+ englishName: 'Catalan',
3387
+ arabicName: 'الكتالانية',
3388
+ nativeName: 'Català',
3389
+ detected: 'Idioma detectat',
3390
+ searching: 'Buscant',
3391
+ elevating: 'Elevant a',
3392
+ translating: 'Traduir a',
3393
+ generatingAnswersForYou: 'Generant respostes per a tu...',
3394
+ learnMore: 'Aprèn més',
3395
+ waitingForResponseMsg: 'Si us plau, tingueu paciència, encara estem pensant',
3396
+ isRtl: false,
3397
+ },
3398
+ {
3399
+ prefix: 'PL',
3400
+ dialect: 'pl-PL',
3401
+ englishName: 'Polish',
3402
+ arabicName: 'البولندية',
3403
+ nativeName: 'Polski',
3404
+ detected: 'Wykryty język',
3405
+ searching: 'Szukać',
3406
+ elevating: 'Podniesienie do',
3407
+ translating: 'Tłumaczenie na',
3408
+ generatingAnswersForYou: 'Generowanie odpowiedzi dla Ciebie...',
3409
+ learnMore: 'Dowiedz się więcej',
3410
+ waitingForResponseMsg: 'Proszę o cierpliwość, nadal myślimy',
3411
+ isRtl: false,
3412
+ },
3413
+ {
3414
+ prefix: 'KO',
3415
+ dialect: 'ko-KR',
3416
+ englishName: 'Korean',
3417
+ arabicName: 'الكورية',
3418
+ nativeName: '한국어',
3419
+ detected: '탐지 언어',
3420
+ searching: '검색',
3421
+ elevating: '상승',
3422
+ translating: '번역',
3423
+ generatingAnswersForYou: '답변을 생성하는 중...',
3424
+ learnMore: '더 알아보기',
3425
+ waitingForResponseMsg: '잠시만 기다려 주세요, 저희는 아직 생각 중입니다',
3426
+ isRtl: false,
3427
+ },
3428
+ {
3429
+ prefix: 'RU',
3430
+ dialect: 'ru-RU',
3431
+ englishName: 'Russian',
3432
+ arabicName: 'الروسية',
3433
+ nativeName: 'Русский',
3434
+ detected: 'Обнаруженный язык',
3435
+ searching: 'В поисках',
3436
+ elevating: 'Поднимаясь до',
3437
+ translating: 'Перевод',
3438
+ generatingAnswersForYou: 'Генерация ответов для вас...',
3439
+ learnMore: 'Узнать больше',
3440
+ waitingForResponseMsg: 'Пожалуйста, подождите, мы всё ещё думаем',
3441
+ isRtl: false,
3442
+ },
3443
+ {
3444
+ prefix: 'JA',
3445
+ dialect: 'ja-JP',
3446
+ englishName: 'Japanese',
3447
+ arabicName: 'اليابانية',
3448
+ nativeName: '日本語',
3449
+ detected: '検出言語',
3450
+ searching: '探している',
3451
+ elevating: '昇格',
3452
+ translating: '翻訳',
3453
+ generatingAnswersForYou: 'あなたのための答えを生成中...',
3454
+ learnMore: 'もっと詳しく',
3455
+ waitingForResponseMsg: '少々お待ちください、ただいま考え中です',
3456
+ isRtl: false,
3457
+ },
3458
+ {
3459
+ prefix: 'ZH',
3460
+ dialect: 'cmn-Hant-TW',
3461
+ englishName: 'Chinese',
3462
+ arabicName: 'الصينية',
3463
+ nativeName: '中文',
3464
+ detected: '检测语言',
3465
+ searching: '搜索',
3466
+ elevating: '提升到',
3467
+ translating: '翻译成',
3468
+ generatingAnswersForYou: '为您生成答案...',
3469
+ learnMore: '了解更多',
3470
+ waitingForResponseMsg: '请耐心等待,我们还在思考',
3471
+ isRtl: false,
3472
+ },
3473
+ {
3474
+ prefix: 'TR',
3475
+ dialect: 'tr-TR',
3476
+ englishName: 'Turkish',
3477
+ arabicName: 'التركية',
3478
+ nativeName: 'Türkçe',
3479
+ detected: 'Tespit edilen dil',
3480
+ searching: 'Arama',
3481
+ elevating: 'Yükseltme',
3482
+ translating: 'Çevirme',
3483
+ generatingAnswersForYou: 'Sizin için yanıtlar üretiliyor...',
3484
+ learnMore: 'Daha fazla bilgi edin',
3485
+ waitingForResponseMsg: 'Lütfen sabırlı olun, hâlâ düşünüyoruz',
3486
+ isRtl: false,
3487
+ },
3488
+ {
3489
+ prefix: 'RO',
3490
+ dialect: 'ro-RO',
3491
+ englishName: 'Romanian',
3492
+ arabicName: 'الرومانية',
3493
+ nativeName: 'Română',
3494
+ detected: 'Limba detectată',
3495
+ searching: 'Căutare de',
3496
+ elevating: 'Elevator la',
3497
+ translating: 'Traducerea în',
3498
+ generatingAnswersForYou: 'Generarea de răspunsuri pentru tine...',
3499
+ learnMore: 'Află mai multe',
3500
+ waitingForResponseMsg: 'Vă rugăm să aveți răbdare, încă ne gândim',
3501
+ isRtl: false,
3502
+ },
3503
+ {
3504
+ prefix: 'NO',
3505
+ dialect: 'nb-NO',
3506
+ englishName: 'Norwegian',
3507
+ arabicName: 'النرويجية',
3508
+ nativeName: 'Norsk',
3509
+ detected: 'Oppdaget språk',
3510
+ searching: 'Leter etter',
3511
+ elevating: 'Løfter til',
3512
+ translating: 'Oversette til',
3513
+ generatingAnswersForYou: 'Genererer svar for deg...',
3514
+ learnMore: 'Lær mer',
3515
+ waitingForResponseMsg: 'Vennligst vær tålmodig, vi tenker fortsatt',
3516
+ isRtl: false,
3517
+ },
3518
+ {
3519
+ prefix: 'HU',
3520
+ dialect: 'hu-HU',
3521
+ englishName: 'Hungarian',
3522
+ arabicName: 'الهنغارية',
3523
+ nativeName: 'Magyar',
3524
+ detected: 'Detektált nyelv',
3525
+ searching: 'Keresés-ra,-re',
3526
+ elevating: 'Felemelve',
3527
+ translating: 'Fordítás',
3528
+ generatingAnswersForYou: 'Válaszok generálása az Ön számára...',
3529
+ learnMore: 'Tudj meg többet',
3530
+ waitingForResponseMsg: 'Kérjük, legyen türelemmel, még gondolkodunk',
3531
+ isRtl: false,
3532
+ },
3533
+ {
3534
+ prefix: 'EL',
3535
+ dialect: 'el-GR',
3536
+ englishName: 'Greek',
3537
+ arabicName: 'اليونانية',
3538
+ nativeName: 'Ελληνικά',
3539
+ detected: 'Εντοπισμένη γλώσσα',
3540
+ searching: 'Ψάχνοντας για',
3541
+ elevating: 'Ανυψώ',
3542
+ translating: 'Μεταφράζοντας σε',
3543
+ generatingAnswersForYou: 'Δημιουργία απαντήσεων για εσάς...',
3544
+ learnMore: 'Μάθε περισσότερα',
3545
+ waitingForResponseMsg: 'Παρακαλώ περιμένετε με υπομονή, σκεφτόμαστε ακόμα',
3546
+ isRtl: false,
3547
+ },
3548
+ {
3549
+ prefix: 'FI',
3550
+ dialect: 'fi-FI',
3551
+ englishName: 'Finnish',
3552
+ arabicName: 'الفنلندية',
3553
+ nativeName: 'Suomi',
3554
+ detected: 'Havaittu kieli',
3555
+ searching: 'Etsimässä',
3556
+ elevating: 'Nousee jhk',
3557
+ translating: 'Kääntäen',
3558
+ generatingAnswersForYou: 'Luodaan vastauksia sinulle...',
3559
+ learnMore: 'Lue lisää',
3560
+ waitingForResponseMsg: 'Ole hyvä ja ole kärsivällinen, ajattelemme yhä',
3561
+ isRtl: false,
3562
+ },
3563
+ {
3564
+ prefix: 'DA',
3565
+ dialect: 'da-DK',
3566
+ englishName: 'Danish',
3567
+ arabicName: 'الدانماركية',
3568
+ nativeName: 'Dansk',
3569
+ detected: 'Opdaget sprog',
3570
+ searching: 'Leder efter',
3571
+ elevating: 'Hæve til',
3572
+ translating: 'Oversættelse til',
3573
+ generatingAnswersForYou: 'Generer svar til dig...',
3574
+ learnMore: 'Lær mere',
3575
+ waitingForResponseMsg: 'Vent venligst tålmodigt, vi tænker stadig',
3576
+ isRtl: false,
3577
+ },
3578
+ {
3579
+ prefix: 'NL',
3580
+ dialect: 'nl-NL',
3581
+ englishName: 'Dutch',
3582
+ arabicName: 'الهولندية',
3583
+ nativeName: 'Nederlands',
3584
+ detected: 'Gedetecteerde taal',
3585
+ searching: 'Op zoek naar',
3586
+ elevating: 'Verheffen tot',
3587
+ translating: 'Vertalen naar',
3588
+ generatingAnswersForYou: 'Antwoorden voor u genereren...',
3589
+ learnMore: 'Lees meer',
3590
+ waitingForResponseMsg: 'Gelieve geduld te hebben, we zijn nog aan het nadenken',
3591
+ isRtl: false,
3592
+ },
3593
+ {
3594
+ prefix: 'SV',
3595
+ dialect: 'sv-SE',
3596
+ englishName: 'Swedish',
3597
+ arabicName: 'السويدية',
3598
+ nativeName: 'Svenska',
3599
+ detected: 'Upptäckt språk',
3600
+ searching: 'Söker efter',
3601
+ elevating: 'Upphöjande till',
3602
+ translating: 'Översätta till',
3603
+ generatingAnswersForYou: 'Genererar svar åt dig...',
3604
+ learnMore: 'Läs mer',
3605
+ waitingForResponseMsg: 'Vänligen ha tålamod, vi tänker fortfarande',
3606
+ isRtl: false,
3607
+ },
3608
+ {
3609
+ prefix: 'PT',
3610
+ dialect: 'pt-PT',
3611
+ englishName: 'Portuguese',
3612
+ arabicName: 'البرتغالية',
3613
+ nativeName: 'Português',
3614
+ detected: 'Idioma detectado',
3615
+ searching: 'Procurando por',
3616
+ elevating: 'Elevando para',
3617
+ translating: 'Traduzindo para',
3618
+ generatingAnswersForYou: 'Gerando respostas para você...',
3619
+ learnMore: 'Saiba mais',
3620
+ waitingForResponseMsg: 'Por favor, tenha paciência, ainda estamos a pensar',
3621
+ isRtl: false,
3622
+ },
3623
+ {
3624
+ prefix: 'IT',
3625
+ dialect: 'it-IT',
3626
+ englishName: 'Italian',
3627
+ arabicName: 'الإيطالية',
3628
+ nativeName: 'Italiano',
3629
+ detected: 'Lingua rilevata',
3630
+ searching: 'Alla ricerca di',
3631
+ elevating: 'Elevare a',
3632
+ translating: 'Tradurre in',
3633
+ generatingAnswersForYou: 'Generazione di risposte per te...',
3634
+ learnMore: 'Scopri di più',
3635
+ waitingForResponseMsg: 'Per favore, abbi pazienza, stiamo ancora pensando',
3636
+ isRtl: false,
3637
+ },
3638
+ {
3639
+ prefix: 'DE',
3640
+ dialect: 'de-DE',
3641
+ englishName: 'German',
3642
+ arabicName: 'الألمانية',
3643
+ nativeName: 'Deutsch',
3644
+ detected: 'Erkannte Sprache',
3645
+ searching: 'Auf der Suche nach',
3646
+ elevating: 'Erhöhung zu',
3647
+ translating: 'Übersetzung auf',
3648
+ generatingAnswersForYou: 'Wir generieren Antworten für Sie...',
3649
+ learnMore: 'Erfahre mehr',
3650
+ waitingForResponseMsg: 'Bitte haben Sie Geduld, wir denken noch nach',
3651
+ isRtl: false,
3652
+ },
3653
+ {
3654
+ prefix: 'ES',
3655
+ dialect: 'es-AR',
3656
+ englishName: 'Spanish',
3657
+ arabicName: 'الأسبانية',
3658
+ nativeName: 'Español',
3659
+ detected: 'Idioma detectado',
3660
+ searching: 'Buscando',
3661
+ elevating: 'Elevando a',
3662
+ translating: 'Traduciendo a',
3663
+ generatingAnswersForYou: 'Generando respuestas para ti...',
3664
+ learnMore: 'Aprende más',
3665
+ waitingForResponseMsg: 'Por favor, ten paciencia, todavía estamos pensando',
3666
+ isRtl: false,
3667
+ },
3668
+ {
3669
+ prefix: 'FR',
3670
+ dialect: 'fr-FR',
3671
+ englishName: 'French',
3672
+ arabicName: 'الفرنسية',
3673
+ nativeName: 'Français',
3674
+ detected: 'Langue détectée',
3675
+ searching: 'À la recherche de',
3676
+ elevating: 'Élever à',
3677
+ translating: 'Traduire par',
3678
+ generatingAnswersForYou: 'Générer des réponses pour vous...',
3679
+ learnMore: 'En savoir plus',
3680
+ waitingForResponseMsg: 'Veuillez patienter, nous réfléchissons encore',
3681
+ isRtl: false,
3682
+ },
3683
+ {
3684
+ prefix: 'EN',
3685
+ dialect: 'en-US',
3686
+ englishName: 'English',
3687
+ arabicName: 'الإنجليزية',
3688
+ nativeName: 'English',
3689
+ detected: 'Detected language',
3690
+ searching: 'Searching for',
3691
+ elevating: 'Elevating to',
3692
+ translating: 'Translating to',
3693
+ generatingAnswersForYou: 'Generating answers for you...',
3694
+ learnMore: 'Learn more',
3695
+ waitingForResponseMsg: 'Please bear with us, we are still thinking',
3696
+ isRtl: false,
3697
+ },
3698
+ {
3699
+ prefix: 'AR',
3700
+ dialect: 'ar-AE',
3701
+ englishName: 'Arabic',
3702
+ arabicName: 'العربية',
3703
+ nativeName: 'العربية',
3704
+ detected: 'اللغة المكتشفة',
3705
+ searching: 'البحث عن',
3706
+ elevating: 'رفع إلى',
3707
+ translating: 'ترجمة إلى',
3708
+ generatingAnswersForYou: 'جارٍ إنشاء إجابات لك...',
3709
+ learnMore: 'تعرف على المزيد',
3710
+ waitingForResponseMsg: 'نرجو منك التحلّي بالصبر، نحن ما زلنا نفكر',
3711
+ isRtl: true,
3712
+ },
3713
+ {
3714
+ prefix: 'KA',
3715
+ dialect: 'ka-GE',
3716
+ englishName: 'Georgian',
3717
+ arabicName: 'الجورجية',
3718
+ nativeName: 'ქართული',
3719
+ detected: 'გამოვლენილი ენა',
3720
+ searching: 'ძიება',
3721
+ elevating: 'ამაღლება',
3722
+ translating: 'თარგმნა',
3723
+ generatingAnswersForYou: 'პასუხების გენერირება თქვენთვის...',
3724
+ learnMore: 'გაიგე მეტი',
3725
+ waitingForResponseMsg: 'გთხოვთ მოითმინოთ, ჯერ კიდევ ვფიქრობთ',
3726
+ isRtl: false,
3727
+ },
3728
+ ]);
3729
+ /**
3730
+ * Lookup by BCP-47 dialect (e.g. `'ar-AE'`). Case-insensitive per BCP-47 §2.1.1
3731
+ * (subtags are case-insensitive on the wire) — accepts `'en-US'`, `'en-us'`,
3732
+ * `'EN-US'`, etc. all as the canonical EN entry. The catalog itself keeps its
3733
+ * canonical casing for display.
3734
+ *
3735
+ * Returns `undefined` when not found; callers decide whether to fall back to `'en-US'`.
3736
+ */
3737
+ function findLocaleByDialect(dialect) {
3738
+ if (typeof dialect !== 'string' || dialect.length === 0)
3739
+ return undefined;
3740
+ const lower = dialect.toLowerCase();
3741
+ return FLY_LOCALE_CATALOG.find((e) => e.dialect.toLowerCase() === lower);
3742
+ }
3743
+ /**
3744
+ * Lookup by short prefix (e.g. `'AR'`). Prefixes are unique in the catalog (asserted by tests).
3745
+ * Case-insensitive — accepts `'AR'`, `'ar'`, `'Ar'`. Catalog stores prefixes uppercase
3746
+ * for display.
3747
+ */
3748
+ function findLocaleByPrefix(prefix) {
3749
+ if (typeof prefix !== 'string' || prefix.length === 0)
3750
+ return undefined;
3751
+ const lower = prefix.toLowerCase();
3752
+ return FLY_LOCALE_CATALOG.find((e) => e.prefix.toLowerCase() === lower);
3753
+ }
3754
+ /**
3755
+ * Type-narrowing helper for entries flagged RTL.
3756
+ *
3757
+ * Named with the `Entry` suffix to avoid colliding with the existing
3758
+ * `isRtlLocale(lang: string)` from `i18n.service.ts`, which answers a different
3759
+ * question (is the *UI* locale code RTL?). Both helpers coexist in `public-api`.
3760
+ */
3761
+ function isRtlLocaleEntry(entry) {
3762
+ return entry.isRtl === true;
3763
+ }
3764
+
2368
3765
  /*
2369
3766
  * @mohamedatia/fly-design-system — Public API
2370
3767
  * https://www.npmjs.com/package/@mohamedatia/fly-design-system
@@ -2429,6 +3826,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
2429
3826
  * own preset pickers. `OuTerm.chartId` and `OuTerm.includeDescendants` are now
2430
3827
  * REQUIRED (no longer optional) so wire round-trips are deterministic — emit explicit
2431
3828
  * `null` / `boolean` rather than `undefined`. New `AudienceErrorCodes` mirror added.
3829
+ * v2.4.0: SharePanelComponent + AudienceBuilderComponent — `defaultChartSystemKey` and
3830
+ * `defaultChartId` inputs let hosts pre-select a specific chart in the OU picker
3831
+ * without knowing the tenant-scoped Guid. `ShareOrgChartOption` gains an optional
3832
+ * `systemKey` field so platform-managed charts (`'apps'`, `'default-company'`) are
3833
+ * identifiable across tenants. New `SHARE_ORG_CHART_SYSTEM_KEY_APPS` /
3834
+ * `SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT` constants exported.
3835
+ * v2.5.0: Agent input contracts (Phase 1 of the `<fly-agent-input>` plan) — additive only.
3836
+ * `AgentCommandRegistry` + `AgentDropRegistry` (singletons via `providedIn: 'root'`,
3837
+ * shared across federation), `flyAgentDraggable` directive, payload-guard helpers
3838
+ * (`validateAgentPayload`, `trimAgentPayload`, `trimAgentString`, `utf8ByteLength`),
3839
+ * and the cross-cutting `FLY_LOCALE_CATALOG` (31 entries, frozen — verbatim port of
3840
+ * `Fly.Shared.Core.LanguageDTO[]`). Three RTL flagged: `he-IL`, `ur-IN`, `ar-AE`.
3841
+ * The catalog's RTL helper is exported as `isRtlLocaleEntry` to avoid colliding with
3842
+ * the existing `isRtlLocale(lang: string)` from `i18n.service.ts`.
3843
+ * No UI consumers yet — Phase 2 wires the registries into the shell input component.
3844
+ * v2.5.1: New `MessageBoxService.showAcknowledged()` entry point returning
3845
+ * `DialogResultWithAcknowledgement` for "Don't ask again"-style flows.
3846
+ * The `dontAskAgain` config (`MessageBoxDontAskAgainConfig`) lives ONLY on
3847
+ * `MessageBoxOptionsWithAcknowledgement` — the base `MessageBoxOptions`
3848
+ * no longer carries it, so the type system enforces "checkbox ⇒ caller
3849
+ * MUST use `showAcknowledged()`" and eliminates the silent-discard path
3850
+ * on `show()`. Existing `show()` callers are unaffected. Case-insensitive
3851
+ * `findLocaleByDialect` / `findLocaleByPrefix` (BCP-47 §2.1.1). See
3852
+ * `.workflow/plans/agent-input-component.md` Phase 2b-1.
2432
3853
  * See docs/ExternalAppsGuide/03-frontend-app.md.
2433
3854
  */
2434
3855
  /**
@@ -2447,5 +3868,5 @@ const AUDIENCE_ERROR_CODES = {
2447
3868
  * Generated bundle index. Do not edit.
2448
3869
  */
2449
3870
 
2450
- export { AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_FLY_THEME_MODE, DialogResult, FLY_THEME_MODE_IDS, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyThemeService, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, isRtlLocale, normalizeFlyTheme };
3871
+ export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentCommandRegistry, AgentDropRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DialogResult, FLY_LOCALE_CATALOG, FLY_THEME_MODE_IDS, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyRemoteRouter, FlyThemeService, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, normalizeFlyTheme, trimAgentPayload, trimAgentString, utf8ByteLength, validateAgentPayload };
2451
3872
  //# sourceMappingURL=mohamedatia-fly-design-system.mjs.map