@mohamedatia/fly-design-system 1.5.0 → 1.9.1

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,11 +1,14 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, ChangeDetectionStrategy, Component, EventEmitter, DestroyRef, Output, Input } 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, viewChild, ViewEncapsulation, model } from '@angular/core';
3
+ import { isPlatformBrowser, CommonModule } from '@angular/common';
3
4
  import { Router } from '@angular/router';
4
- import { of } from 'rxjs';
5
- import { CommonModule } from '@angular/common';
5
+ import { of, ReplaySubject } from 'rxjs';
6
6
  import * as i1 from '@angular/forms';
7
7
  import { FormsModule } from '@angular/forms';
8
8
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
+ import { switchMap } from 'rxjs/operators';
10
+ import { HttpClient, HttpEventType } from '@angular/common/http';
11
+ import Cropper from 'cropperjs';
9
12
 
10
13
  const WINDOW_DATA = new InjectionToken('WINDOW_DATA');
11
14
 
@@ -229,21 +232,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
229
232
  args: [{ providedIn: 'root' }]
230
233
  }] });
231
234
 
235
+ /** Single source of truth for persisted / API theme strings. */
236
+ const FLY_THEME_MODE_IDS = ['light', 'spatial', 'dark'];
237
+ /** Factory default: `dark` (opaque dark / `html.dark-theme`); shell i18n `settings.theme.dark`. Keep in sync with `UserSettings` defaults. */
238
+ const DEFAULT_FLY_THEME_MODE = 'dark';
239
+ /** Coerce unknown values (localStorage, API) to a valid theme mode. */
240
+ function normalizeFlyTheme(value) {
241
+ if (typeof value === 'string' && FLY_THEME_MODE_IDS.includes(value)) {
242
+ return value;
243
+ }
244
+ return DEFAULT_FLY_THEME_MODE;
245
+ }
232
246
  /**
233
- * Applies `html.light-theme` / `html.dark-theme` / `html.transparent-theme` for DS SCSS.
247
+ * Applies `html.light-theme` / `html.spatial-theme` / `html.dark-theme` for DS SCSS.
234
248
  * Shell and standalone Business Apps use the same service (federation singleton when shared).
235
249
  */
236
250
  class FlyThemeService {
237
- theme = signal('light', ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
251
+ platformId = inject(PLATFORM_ID);
252
+ theme = signal(DEFAULT_FLY_THEME_MODE, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
238
253
  applyTheme(mode) {
239
254
  this.theme.set(mode);
255
+ if (!isPlatformBrowser(this.platformId)) {
256
+ return;
257
+ }
240
258
  const html = document.documentElement;
241
- html.classList.remove('dark-theme', 'light-theme', 'transparent-theme');
242
- if (mode === 'dark') {
243
- html.classList.add('dark-theme');
259
+ html.classList.remove('light-theme', 'spatial-theme', 'dark-theme');
260
+ if (mode === 'spatial') {
261
+ html.classList.add('spatial-theme');
244
262
  }
245
- else if (mode === 'transparent') {
246
- html.classList.add('transparent-theme');
263
+ else if (mode === 'dark') {
264
+ html.classList.add('dark-theme');
247
265
  }
248
266
  else {
249
267
  html.classList.add('light-theme');
@@ -264,6 +282,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
264
282
  * on the shell's internal implementation.
265
283
  */
266
284
  class WindowManagerService {
285
+ /**
286
+ * Shell: increments per-window depth and shows content block UI.
287
+ * Standalone / no host: no-op.
288
+ */
289
+ beginContentUiBlock(_windowId, _messageKey) { }
290
+ /** Shell: decrements depth; clears block when depth reaches 0. Standalone: no-op. */
291
+ endContentUiBlock(_windowId) { }
267
292
  }
268
293
  /**
269
294
  * No-op fallback implementation used when running a Business App in
@@ -352,6 +377,13 @@ class MockAuthService {
352
377
  hasPendingTwoFactor;
353
378
  /** Always false in mock mode — auth is pre-populated synchronously. */
354
379
  initializing;
380
+ /**
381
+ * Shell and guards may read this flag; real auth uses `localStorage` `fly_had_session`.
382
+ * Default false — override in the app’s `auth.service.mock.ts` if needed.
383
+ */
384
+ get hadPriorSession() {
385
+ return false;
386
+ }
355
387
  constructor() {
356
388
  // All signal/computed calls are inside the constructor body so Angular's
357
389
  // injection context and ngDevMode are fully set up before they execute.
@@ -718,7 +750,18 @@ class SharePanelComponent {
718
750
  /** Load current shares / permissions (host resolves display names when possible). */
719
751
  loadPermissions;
720
752
  searchUsers;
753
+ /**
754
+ * Load OU hierarchy for sharing. `chartId` is null for the default/official tree;
755
+ * non-null for an alternative chart.
756
+ */
721
757
  loadOuTree;
758
+ /** Chart selector options; first entry should be the default chart (`id: null`). */
759
+ loadChartOptions;
760
+ /**
761
+ * Labels from the default (official) OU tree for stable display names on OU grants
762
+ * when the picker shows an alternative chart.
763
+ */
764
+ loadOuLabelMap;
722
765
  grantToUser;
723
766
  grantToOu;
724
767
  updatePermission;
@@ -740,17 +783,49 @@ class SharePanelComponent {
740
783
  searchResults = signal([], ...(ngDevMode ? [{ debugName: "searchResults" }] : /* istanbul ignore next */ []));
741
784
  ouTree = signal([], ...(ngDevMode ? [{ debugName: "ouTree" }] : /* istanbul ignore next */ []));
742
785
  showOuPicker = signal(false, ...(ngDevMode ? [{ debugName: "showOuPicker" }] : /* istanbul ignore next */ []));
786
+ chartOptions = signal([], ...(ngDevMode ? [{ debugName: "chartOptions" }] : /* istanbul ignore next */ []));
787
+ selectedOrgChartId = signal(null, ...(ngDevMode ? [{ debugName: "selectedOrgChartId" }] : /* istanbul ignore next */ []));
788
+ /** Default-tree id → display name for permission rows (OU grants). */
789
+ ouLabelById = signal({}, ...(ngDevMode ? [{ debugName: "ouLabelById" }] : /* istanbul ignore next */ []));
743
790
  searchTimeout = null;
791
+ /** Emits after chart options load; further emissions on user chart changes (no initial null). */
792
+ selectedChartId$ = new ReplaySubject(1);
744
793
  ngOnInit() {
745
794
  const levels = this.levelOptions;
746
795
  this.newLevel = levels[0]?.value ?? 'View';
747
796
  this.refreshPermissions();
748
- this.loadOuTree()
797
+ this.loadOuLabelMap()
749
798
  .pipe(takeUntilDestroyed(this.destroyRef))
750
799
  .subscribe({
800
+ next: (m) => this.ouLabelById.set(m),
801
+ error: () => this.ouLabelById.set({}),
802
+ });
803
+ this.selectedChartId$
804
+ .pipe(switchMap((id) => this.loadOuTree(id)), takeUntilDestroyed(this.destroyRef))
805
+ .subscribe({
751
806
  next: (tree) => this.ouTree.set(tree),
752
807
  error: () => this.ouTree.set([]),
753
808
  });
809
+ this.loadChartOptions()
810
+ .pipe(takeUntilDestroyed(this.destroyRef))
811
+ .subscribe({
812
+ next: (opts) => {
813
+ this.chartOptions.set(opts);
814
+ const first = opts[0];
815
+ const initial = first ? first.id : null;
816
+ this.selectedOrgChartId.set(initial);
817
+ this.selectedChartId$.next(initial);
818
+ },
819
+ error: () => {
820
+ this.chartOptions.set([]);
821
+ this.selectedOrgChartId.set(null);
822
+ this.selectedChartId$.next(null);
823
+ },
824
+ });
825
+ }
826
+ onOrgChartSelectChange(chartId) {
827
+ this.selectedOrgChartId.set(chartId);
828
+ this.selectedChartId$.next(chartId);
754
829
  }
755
830
  get levelOptions() {
756
831
  const p = this.permissionLevels;
@@ -819,6 +894,11 @@ class SharePanelComponent {
819
894
  return 'pi pi-globe';
820
895
  return 'pi pi-question';
821
896
  }
897
+ /** Logical start padding for OU hierarchy (roots align with section padding). */
898
+ ouIndentStartPx(ou) {
899
+ const d = ou.depth ?? 0;
900
+ return 12 + d * 18;
901
+ }
822
902
  getPermLabel(perm) {
823
903
  if (perm.displayName?.trim())
824
904
  return perm.displayName.trim();
@@ -826,21 +906,25 @@ class SharePanelComponent {
826
906
  return `${perm.grantedToUserId.substring(0, 8)}…`;
827
907
  }
828
908
  if (perm.grantedToOuId) {
829
- const ou = this.ouTree().find((o) => o.id === perm.grantedToOuId);
909
+ const id = perm.grantedToOuId;
910
+ const fromDefault = this.ouLabelById()[id];
911
+ if (fromDefault)
912
+ return fromDefault;
913
+ const ou = this.ouTree().find((o) => o.id === id);
830
914
  if (ou)
831
915
  return ou.displayName;
832
- return `${perm.grantedToOuId.substring(0, 8)}…`;
916
+ return `${id.substring(0, 8)}…`;
833
917
  }
834
918
  if (perm.grantedToAppId === '*')
835
919
  return this.i18n.t(this.everyoneLabelKey);
836
920
  return perm.grantedToAppId ?? '';
837
921
  }
838
922
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
839
- 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", grantToUser: "grantToUser", grantToOu: "grantToOu", updatePermission: "updatePermission", revokePermission: "revokePermission", showApplyToChildren: "showApplyToChildren", permissionLevels: "permissionLevels", everyoneLabelKey: "everyoneLabelKey" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"fac-header\">\n <h3>{{ titleKey | translate }}</h3>\n <span class=\"fac-target-name\">{{ targetName }}</span>\n <button type=\"button\" class=\"fac-close\" (click)=\"close.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <div class=\"fac-add-section\">\n <div class=\"fac-search-row\">\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n [placeholder]=\"'files.share.search_placeholder' | translate\"\n />\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n </div>\n\n @if (searchResults().length > 0) {\n <div class=\"fac-search-results\">\n @for (result of searchResults(); track result.id) {\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ result.firstName }} {{ result.lastName }}</span>\n <span class=\"fac-email\">{{ result.email }}</span>\n </div>\n }\n </div>\n }\n\n @if (showOuPicker()) {\n <div class=\"fac-ou-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\n @for (ou of ouTree(); track ou.id) {\n <div class=\"fac-ou-item\" (click)=\"onGrantToOu(ou)\">\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"fac-toggle-row\">\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\n </button>\n @if (showApplyToChildren) {\n <label class=\"fac-checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\n {{ 'files.share.apply_children' | translate }}\n </label>\n }\n </div>\n </div>\n\n <div class=\"fac-perms-section\">\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\n @if (loading()) {\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\n } @else if (permissions().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\n } @else {\n @for (perm of permissions(); track perm.id) {\n <div class=\"fac-perm-row\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\">{{ getPermLabel(perm) }}</span>\n @if (perm.isInherited) {\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\n }\n </div>\n <div class=\"fac-perm-actions\">\n <select\n class=\"fac-select sm\"\n [ngModel]=\"perm.level\"\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\n [disabled]=\"perm.isInherited === true\"\n >\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n <button\n type=\"button\"\n class=\"fac-icon-btn danger\"\n (click)=\"onRevokePermission(perm)\"\n [disabled]=\"perm.isInherited === true\"\n [title]=\"'files.share.revoke' | translate\"\n >\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </div>\n</div>\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:480px;max-height:80vh;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-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:120px;overflow-y:auto}.fac-ou-item{display:flex;align-items:center;gap:8px;padding:6px 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-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 });
923
+ 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", permissionLevels: "permissionLevels", everyoneLabelKey: "everyoneLabelKey" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"fac-header\">\n <h3>{{ titleKey | translate }}</h3>\n <span class=\"fac-target-name\">{{ targetName }}</span>\n <button type=\"button\" class=\"fac-close\" (click)=\"close.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <div class=\"fac-add-section\">\n <div class=\"fac-search-row\">\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n [placeholder]=\"'files.share.search_placeholder' | translate\"\n />\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n </div>\n\n @if (searchResults().length > 0) {\n <div class=\"fac-search-results\">\n @for (result of searchResults(); track result.id) {\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ result.firstName }} {{ result.lastName }}</span>\n <span class=\"fac-email\">{{ result.email }}</span>\n </div>\n }\n </div>\n }\n\n @if (chartOptions().length > 0) {\n <div class=\"fac-chart-row\">\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\n 'files.share.org_chart' | translate\n }}</label>\n <select\n id=\"fac-org-chart-select\"\n class=\"fac-select\"\n [ngModel]=\"selectedOrgChartId()\"\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\n @for (opt of chartOptions(); track $index) {\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\n }\n </select>\n </div>\n }\n\n @if (showOuPicker()) {\n <div class=\"fac-ou-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\n @for (ou of ouTree(); track ou.id) {\n <div\n class=\"fac-ou-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"onGrantToOu(ou)\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"fac-toggle-row\">\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\n </button>\n @if (showApplyToChildren) {\n <label class=\"fac-checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\n {{ 'files.share.apply_children' | translate }}\n </label>\n }\n </div>\n </div>\n\n <div class=\"fac-perms-section\">\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\n @if (loading()) {\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\n } @else if (permissions().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\n } @else {\n @for (perm of permissions(); track perm.id) {\n <div class=\"fac-perm-row\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\">{{ getPermLabel(perm) }}</span>\n @if (perm.isInherited) {\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\n }\n </div>\n <div class=\"fac-perm-actions\">\n <select\n class=\"fac-select sm\"\n [ngModel]=\"perm.level\"\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\n [disabled]=\"perm.isInherited === true\"\n >\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n <button\n type=\"button\"\n class=\"fac-icon-btn danger\"\n (click)=\"onRevokePermission(perm)\"\n [disabled]=\"perm.isInherited === true\"\n [title]=\"'files.share.revoke' | translate\"\n >\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </div>\n</div>\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-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 });
840
924
  }
841
925
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: SharePanelComponent, decorators: [{
842
926
  type: Component,
843
- args: [{ selector: 'fly-share-panel', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"fac-header\">\n <h3>{{ titleKey | translate }}</h3>\n <span class=\"fac-target-name\">{{ targetName }}</span>\n <button type=\"button\" class=\"fac-close\" (click)=\"close.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <div class=\"fac-add-section\">\n <div class=\"fac-search-row\">\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n [placeholder]=\"'files.share.search_placeholder' | translate\"\n />\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n </div>\n\n @if (searchResults().length > 0) {\n <div class=\"fac-search-results\">\n @for (result of searchResults(); track result.id) {\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ result.firstName }} {{ result.lastName }}</span>\n <span class=\"fac-email\">{{ result.email }}</span>\n </div>\n }\n </div>\n }\n\n @if (showOuPicker()) {\n <div class=\"fac-ou-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\n @for (ou of ouTree(); track ou.id) {\n <div class=\"fac-ou-item\" (click)=\"onGrantToOu(ou)\">\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"fac-toggle-row\">\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\n </button>\n @if (showApplyToChildren) {\n <label class=\"fac-checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\n {{ 'files.share.apply_children' | translate }}\n </label>\n }\n </div>\n </div>\n\n <div class=\"fac-perms-section\">\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\n @if (loading()) {\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\n } @else if (permissions().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\n } @else {\n @for (perm of permissions(); track perm.id) {\n <div class=\"fac-perm-row\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\">{{ getPermLabel(perm) }}</span>\n @if (perm.isInherited) {\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\n }\n </div>\n <div class=\"fac-perm-actions\">\n <select\n class=\"fac-select sm\"\n [ngModel]=\"perm.level\"\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\n [disabled]=\"perm.isInherited === true\"\n >\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n <button\n type=\"button\"\n class=\"fac-icon-btn danger\"\n (click)=\"onRevokePermission(perm)\"\n [disabled]=\"perm.isInherited === true\"\n [title]=\"'files.share.revoke' | translate\"\n >\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </div>\n</div>\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:480px;max-height:80vh;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-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:120px;overflow-y:auto}.fac-ou-item{display:flex;align-items:center;gap:8px;padding:6px 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-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"] }]
927
+ args: [{ selector: 'fly-share-panel', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"fac-overlay\" (click)=\"close.emit()\">\n <div class=\"fac-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"fac-header\">\n <h3>{{ titleKey | translate }}</h3>\n <span class=\"fac-target-name\">{{ targetName }}</span>\n <button type=\"button\" class=\"fac-close\" (click)=\"close.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n <div class=\"fac-add-section\">\n <div class=\"fac-search-row\">\n <input\n type=\"text\"\n class=\"fac-input\"\n [(ngModel)]=\"searchQuery\"\n (input)=\"onSearchInput()\"\n [placeholder]=\"'files.share.search_placeholder' | translate\"\n />\n <select class=\"fac-select\" [(ngModel)]=\"newLevel\">\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n </div>\n\n @if (searchResults().length > 0) {\n <div class=\"fac-search-results\">\n @for (result of searchResults(); track result.id) {\n <div class=\"fac-search-item\" (click)=\"onGrantToUser(result)\">\n <i class=\"pi pi-user\" aria-hidden=\"true\"></i>\n <span>{{ result.firstName }} {{ result.lastName }}</span>\n <span class=\"fac-email\">{{ result.email }}</span>\n </div>\n }\n </div>\n }\n\n @if (chartOptions().length > 0) {\n <div class=\"fac-chart-row\">\n <label for=\"fac-org-chart-select\" class=\"fac-section-label\">{{\n 'files.share.org_chart' | translate\n }}</label>\n <select\n id=\"fac-org-chart-select\"\n class=\"fac-select\"\n [ngModel]=\"selectedOrgChartId()\"\n (ngModelChange)=\"onOrgChartSelectChange($event)\">\n @for (opt of chartOptions(); track $index) {\n <option [ngValue]=\"opt.id\">{{ opt.name }}</option>\n }\n </select>\n </div>\n }\n\n @if (showOuPicker()) {\n <div class=\"fac-ou-section\">\n <div class=\"fac-section-label\">{{ 'files.share.share_with_ou' | translate }}</div>\n @for (ou of ouTree(); track ou.id) {\n <div\n class=\"fac-ou-item\"\n [style.padding-inline-start.px]=\"ouIndentStartPx(ou)\"\n (click)=\"onGrantToOu(ou)\"\n >\n <i class=\"pi pi-sitemap\" aria-hidden=\"true\"></i>\n <span>{{ ou.displayName }}</span>\n </div>\n }\n </div>\n }\n\n <div class=\"fac-toggle-row\">\n <button type=\"button\" class=\"fac-link\" (click)=\"showOuPicker.set(!showOuPicker())\">\n {{ showOuPicker() ? ('files.share.hide_ous' | translate) : ('files.share.show_ous' | translate) }}\n </button>\n @if (showApplyToChildren) {\n <label class=\"fac-checkbox-label\">\n <input type=\"checkbox\" [(ngModel)]=\"applyToChildren\" />\n {{ 'files.share.apply_children' | translate }}\n </label>\n }\n </div>\n </div>\n\n <div class=\"fac-perms-section\">\n <div class=\"fac-section-label\">{{ 'files.share.current_permissions' | translate }}</div>\n @if (loading()) {\n <div class=\"fac-loading\"><i class=\"pi pi-spin pi-spinner\" aria-hidden=\"true\"></i></div>\n } @else if (permissions().length === 0) {\n <div class=\"fac-empty\">{{ 'files.share.no_permissions' | translate }}</div>\n } @else {\n @for (perm of permissions(); track perm.id) {\n <div class=\"fac-perm-row\">\n <div class=\"fac-perm-info\">\n <i [class]=\"getPermIcon(perm)\" aria-hidden=\"true\"></i>\n <span class=\"fac-perm-name\">{{ getPermLabel(perm) }}</span>\n @if (perm.isInherited) {\n <span class=\"fac-badge inherited\">{{ 'files.share.inherited' | translate }}</span>\n }\n </div>\n <div class=\"fac-perm-actions\">\n <select\n class=\"fac-select sm\"\n [ngModel]=\"perm.level\"\n (ngModelChange)=\"onUpdateLevel(perm, $event)\"\n [disabled]=\"perm.isInherited === true\"\n >\n @for (opt of levelOptions; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.labelKey | translate }}</option>\n }\n </select>\n <button\n type=\"button\"\n class=\"fac-icon-btn danger\"\n (click)=\"onRevokePermission(perm)\"\n [disabled]=\"perm.isInherited === true\"\n [title]=\"'files.share.revoke' | translate\"\n >\n <i class=\"pi pi-trash\" aria-hidden=\"true\"></i>\n </button>\n </div>\n </div>\n }\n }\n </div>\n </div>\n</div>\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-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"] }]
844
928
  }], propDecorators: { targetName: [{
845
929
  type: Input
846
930
  }], titleKey: [{
@@ -854,6 +938,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
854
938
  }], loadOuTree: [{
855
939
  type: Input,
856
940
  args: [{ required: true }]
941
+ }], loadChartOptions: [{
942
+ type: Input,
943
+ args: [{ required: true }]
944
+ }], loadOuLabelMap: [{
945
+ type: Input,
946
+ args: [{ required: true }]
857
947
  }], grantToUser: [{
858
948
  type: Input,
859
949
  args: [{ required: true }]
@@ -876,6 +966,599 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
876
966
  type: Output
877
967
  }] } });
878
968
 
969
+ /** Full-bleed loading overlay for window content (shell) or embedded hosts. */
970
+ class FlyBlockUiComponent {
971
+ /** When false, the overlay is not rendered (host may use @if instead). */
972
+ active = input.required(...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
973
+ /** i18n key for status text; empty uses `common.loading`. */
974
+ messageKey = input('', ...(ngDevMode ? [{ debugName: "messageKey" }] : /* istanbul ignore next */ []));
975
+ resolvedMessageKey = computed(() => {
976
+ const k = this.messageKey()?.trim();
977
+ return k && k.length > 0 ? k : 'common.loading';
978
+ }, ...(ngDevMode ? [{ debugName: "resolvedMessageKey" }] : /* istanbul ignore next */ []));
979
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyBlockUiComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
980
+ 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 });
981
+ }
982
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyBlockUiComponent, decorators: [{
983
+ type: Component,
984
+ 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"] }]
985
+ }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: true }] }], messageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "messageKey", required: false }] }] } });
986
+
987
+ /**
988
+ * Image upload component with built-in cropperjs cropping modal.
989
+ *
990
+ * Usage:
991
+ * ```html
992
+ * <fly-image-upload
993
+ * [aspectRatio]="16/9"
994
+ * [currentImageId]="trend.coverImageId"
995
+ * sourceApp="circles"
996
+ * sourceEntityType="trend"
997
+ * (uploaded)="onImageUploaded($event)"
998
+ * (removed)="onImageRemoved()"
999
+ * />
1000
+ * ```
1001
+ */
1002
+ class FlyImageUploadComponent {
1003
+ http = inject(HttpClient);
1004
+ // ── Inputs ──
1005
+ aspectRatio = input(16 / 9, ...(ngDevMode ? [{ debugName: "aspectRatio" }] : /* istanbul ignore next */ []));
1006
+ maxSizeBytes = input(5 * 1024 * 1024, ...(ngDevMode ? [{ debugName: "maxSizeBytes" }] : /* istanbul ignore next */ []));
1007
+ currentImageId = input(null, ...(ngDevMode ? [{ debugName: "currentImageId" }] : /* istanbul ignore next */ []));
1008
+ sourceApp = input('unknown', ...(ngDevMode ? [{ debugName: "sourceApp" }] : /* istanbul ignore next */ []));
1009
+ sourceEntityType = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityType" }] : /* istanbul ignore next */ []));
1010
+ sourceEntityId = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityId" }] : /* istanbul ignore next */ []));
1011
+ // ── Outputs ──
1012
+ uploaded = output();
1013
+ removed = output();
1014
+ // ── State ──
1015
+ uploading = signal(false, ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
1016
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
1017
+ showCropper = signal(false, ...(ngDevMode ? [{ debugName: "showCropper" }] : /* istanbul ignore next */ []));
1018
+ rawImageUrl = signal('', ...(ngDevMode ? [{ debugName: "rawImageUrl" }] : /* istanbul ignore next */ []));
1019
+ localBlob = signal(null, ...(ngDevMode ? [{ debugName: "localBlob" }] : /* istanbul ignore next */ []));
1020
+ fileInput = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
1021
+ cropImage = viewChild('cropImage', ...(ngDevMode ? [{ debugName: "cropImage" }] : /* istanbul ignore next */ []));
1022
+ cropper = null;
1023
+ selectedFile = null;
1024
+ previewUrl = computed(() => {
1025
+ if (this.localBlob())
1026
+ return this.localBlob();
1027
+ const id = this.currentImageId();
1028
+ return id ? `/api/files/${id}/download` : null;
1029
+ }, ...(ngDevMode ? [{ debugName: "previewUrl" }] : /* istanbul ignore next */ []));
1030
+ sizeHint = computed(() => {
1031
+ const mb = this.maxSizeBytes() / (1024 * 1024);
1032
+ return `Max ${mb}MB`;
1033
+ }, ...(ngDevMode ? [{ debugName: "sizeHint" }] : /* istanbul ignore next */ []));
1034
+ triggerFileInput() {
1035
+ this.fileInput()?.nativeElement.click();
1036
+ }
1037
+ onDragOver(e) {
1038
+ e.preventDefault();
1039
+ e.stopPropagation();
1040
+ }
1041
+ onDrop(e) {
1042
+ e.preventDefault();
1043
+ e.stopPropagation();
1044
+ const file = e.dataTransfer?.files?.[0];
1045
+ if (file)
1046
+ this.processFile(file);
1047
+ }
1048
+ onFileSelected(e) {
1049
+ const input = e.target;
1050
+ const file = input.files?.[0];
1051
+ if (file)
1052
+ this.processFile(file);
1053
+ input.value = '';
1054
+ }
1055
+ removeImage() {
1056
+ this.localBlob.set(null);
1057
+ this.error.set(null);
1058
+ this.removed.emit();
1059
+ }
1060
+ cancelCrop() {
1061
+ this.showCropper.set(false);
1062
+ this.destroyCropper();
1063
+ if (this.rawImageUrl())
1064
+ URL.revokeObjectURL(this.rawImageUrl());
1065
+ this.rawImageUrl.set('');
1066
+ }
1067
+ applyCrop() {
1068
+ if (!this.cropper)
1069
+ return;
1070
+ const canvas = this.cropper.getCroppedCanvas({ maxWidth: 1920, maxHeight: 1080 });
1071
+ this.showCropper.set(false);
1072
+ this.destroyCropper();
1073
+ if (this.rawImageUrl())
1074
+ URL.revokeObjectURL(this.rawImageUrl());
1075
+ this.rawImageUrl.set('');
1076
+ canvas.toBlob((blob) => {
1077
+ if (!blob)
1078
+ return;
1079
+ const fileName = this.selectedFile?.name ?? 'cropped-image.jpg';
1080
+ const file = new File([blob], fileName, { type: blob.type || 'image/jpeg' });
1081
+ this.uploadFile(file);
1082
+ }, 'image/jpeg', 0.9);
1083
+ }
1084
+ processFile(file) {
1085
+ this.error.set(null);
1086
+ if (!file.type.startsWith('image/')) {
1087
+ this.error.set('Only image files are allowed');
1088
+ return;
1089
+ }
1090
+ if (file.size > this.maxSizeBytes()) {
1091
+ const mb = this.maxSizeBytes() / (1024 * 1024);
1092
+ this.error.set(`File exceeds ${mb}MB limit`);
1093
+ return;
1094
+ }
1095
+ this.selectedFile = file;
1096
+ const url = URL.createObjectURL(file);
1097
+ this.rawImageUrl.set(url);
1098
+ this.showCropper.set(true);
1099
+ // Wait for next tick so cropImage element is in DOM
1100
+ setTimeout(() => this.initCropper(), 0);
1101
+ }
1102
+ initCropper() {
1103
+ this.destroyCropper();
1104
+ const imgEl = this.cropImage()?.nativeElement;
1105
+ if (!imgEl)
1106
+ return;
1107
+ this.cropper = new Cropper(imgEl, {
1108
+ aspectRatio: this.aspectRatio(),
1109
+ viewMode: 1,
1110
+ autoCropArea: 0.9,
1111
+ responsive: true,
1112
+ background: false,
1113
+ });
1114
+ }
1115
+ destroyCropper() {
1116
+ this.cropper?.destroy();
1117
+ this.cropper = null;
1118
+ }
1119
+ uploadFile(file) {
1120
+ this.uploading.set(true);
1121
+ this.error.set(null);
1122
+ const formData = new FormData();
1123
+ formData.append('file', file, file.name);
1124
+ formData.append('sourceApp', this.sourceApp());
1125
+ if (this.sourceEntityType())
1126
+ formData.append('sourceEntityType', this.sourceEntityType());
1127
+ if (this.sourceEntityId())
1128
+ formData.append('sourceEntityId', this.sourceEntityId());
1129
+ this.http.post('/api/files/upload', formData).subscribe({
1130
+ next: (res) => {
1131
+ this.uploading.set(false);
1132
+ const info = res.data ?? res;
1133
+ this.localBlob.set(URL.createObjectURL(file));
1134
+ this.uploaded.emit(info);
1135
+ },
1136
+ error: (err) => {
1137
+ this.uploading.set(false);
1138
+ this.error.set(err?.error?.message ?? 'Upload failed');
1139
+ },
1140
+ });
1141
+ }
1142
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1143
+ 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: `
1144
+ <div class="fly-image-upload">
1145
+ @if (previewUrl()) {
1146
+ <div class="fly-image-upload__preview">
1147
+ <img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
1148
+ <div class="fly-image-upload__overlay">
1149
+ <button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
1150
+ <span class="pi pi-pencil" aria-hidden="true"></span>
1151
+ </button>
1152
+ <button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
1153
+ <span class="pi pi-trash" aria-hidden="true"></span>
1154
+ </button>
1155
+ </div>
1156
+ </div>
1157
+ } @else {
1158
+ <button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
1159
+ @if (uploading()) {
1160
+ <span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
1161
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
1162
+ } @else {
1163
+ <span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
1164
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
1165
+ <span class="fly-image-upload__hint">{{ sizeHint() }}</span>
1166
+ }
1167
+ </button>
1168
+ }
1169
+
1170
+ @if (error()) {
1171
+ <div class="fly-image-upload__error">{{ error() }}</div>
1172
+ }
1173
+
1174
+ <input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
1175
+
1176
+ @if (showCropper()) {
1177
+ <div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
1178
+ <div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
1179
+ <div class="fly-image-upload__crop-header">
1180
+ <h3>{{ 'files.imageUpload.crop' | translate }}</h3>
1181
+ </div>
1182
+ <div class="fly-image-upload__crop-body">
1183
+ <img #cropImage [src]="rawImageUrl()" alt="" />
1184
+ </div>
1185
+ <div class="fly-image-upload__crop-footer">
1186
+ <button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
1187
+ <button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
1188
+ </div>
1189
+ </div>
1190
+ </div>
1191
+ }
1192
+ </div>
1193
+ `, 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 });
1194
+ }
1195
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyImageUploadComponent, decorators: [{
1196
+ type: Component,
1197
+ args: [{ selector: 'fly-image-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: `
1198
+ <div class="fly-image-upload">
1199
+ @if (previewUrl()) {
1200
+ <div class="fly-image-upload__preview">
1201
+ <img [src]="previewUrl()" alt="" class="fly-image-upload__img" />
1202
+ <div class="fly-image-upload__overlay">
1203
+ <button type="button" class="fly-image-upload__action-btn" (click)="triggerFileInput()" [title]="'files.imageUpload.change' | translate">
1204
+ <span class="pi pi-pencil" aria-hidden="true"></span>
1205
+ </button>
1206
+ <button type="button" class="fly-image-upload__action-btn fly-image-upload__action-btn--danger" (click)="removeImage()" [title]="'files.imageUpload.remove' | translate">
1207
+ <span class="pi pi-trash" aria-hidden="true"></span>
1208
+ </button>
1209
+ </div>
1210
+ </div>
1211
+ } @else {
1212
+ <button type="button" class="fly-image-upload__dropzone" (click)="triggerFileInput()" (dragover)="onDragOver($event)" (drop)="onDrop($event)">
1213
+ @if (uploading()) {
1214
+ <span class="pi pi-spin pi-spinner fly-image-upload__icon" aria-hidden="true"></span>
1215
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.uploading' | translate }}</span>
1216
+ } @else {
1217
+ <span class="pi pi-image fly-image-upload__icon" aria-hidden="true"></span>
1218
+ <span class="fly-image-upload__label">{{ 'files.imageUpload.placeholder' | translate }}</span>
1219
+ <span class="fly-image-upload__hint">{{ sizeHint() }}</span>
1220
+ }
1221
+ </button>
1222
+ }
1223
+
1224
+ @if (error()) {
1225
+ <div class="fly-image-upload__error">{{ error() }}</div>
1226
+ }
1227
+
1228
+ <input #fileInput type="file" accept="image/*" class="fly-image-upload__hidden" (change)="onFileSelected($event)" />
1229
+
1230
+ @if (showCropper()) {
1231
+ <div class="fly-image-upload__crop-backdrop" (click)="cancelCrop()">
1232
+ <div class="fly-image-upload__crop-modal" (click)="$event.stopPropagation()">
1233
+ <div class="fly-image-upload__crop-header">
1234
+ <h3>{{ 'files.imageUpload.crop' | translate }}</h3>
1235
+ </div>
1236
+ <div class="fly-image-upload__crop-body">
1237
+ <img #cropImage [src]="rawImageUrl()" alt="" />
1238
+ </div>
1239
+ <div class="fly-image-upload__crop-footer">
1240
+ <button type="button" class="vos-btn sm platter" (click)="cancelCrop()">{{ 'common.cancel' | translate }}</button>
1241
+ <button type="button" class="vos-btn sm primary" (click)="applyCrop()">{{ 'files.imageUpload.apply' | translate }}</button>
1242
+ </div>
1243
+ </div>
1244
+ </div>
1245
+ }
1246
+ </div>
1247
+ `, 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"] }]
1248
+ }], 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 }] }] } });
1249
+
1250
+ const FILE_ICONS = {
1251
+ 'application/pdf': 'pi-file-pdf',
1252
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'pi-file-word',
1253
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'pi-file-excel',
1254
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pi-file',
1255
+ 'image/': 'pi-image',
1256
+ 'video/': 'pi-video',
1257
+ 'audio/': 'pi-volume-up',
1258
+ };
1259
+ function iconForType(contentType) {
1260
+ for (const [prefix, icon] of Object.entries(FILE_ICONS)) {
1261
+ if (contentType.startsWith(prefix))
1262
+ return icon;
1263
+ }
1264
+ return 'pi-file';
1265
+ }
1266
+ function formatSize(bytes) {
1267
+ if (bytes < 1024)
1268
+ return `${bytes} B`;
1269
+ if (bytes < 1024 * 1024)
1270
+ return `${(bytes / 1024).toFixed(1)} KB`;
1271
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1272
+ }
1273
+ /**
1274
+ * Multi-file upload with drag-drop, progress, and validation.
1275
+ *
1276
+ * Usage:
1277
+ * ```html
1278
+ * <fly-file-upload
1279
+ * [maxFiles]="5"
1280
+ * [maxFileSizeBytes]="5242880"
1281
+ * accept=".pdf,.docx,.xlsx"
1282
+ * [(files)]="trend.attachments"
1283
+ * sourceApp="circles"
1284
+ * sourceEntityType="trend"
1285
+ * (filesChanged)="onAttachmentsChanged($event)"
1286
+ * />
1287
+ * ```
1288
+ */
1289
+ class FlyFileUploadComponent {
1290
+ http = inject(HttpClient);
1291
+ // ── Inputs ──
1292
+ maxFiles = input(5, ...(ngDevMode ? [{ debugName: "maxFiles" }] : /* istanbul ignore next */ []));
1293
+ maxFileSizeBytes = input(5 * 1024 * 1024, ...(ngDevMode ? [{ debugName: "maxFileSizeBytes" }] : /* istanbul ignore next */ []));
1294
+ accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
1295
+ sourceApp = input('unknown', ...(ngDevMode ? [{ debugName: "sourceApp" }] : /* istanbul ignore next */ []));
1296
+ sourceEntityType = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityType" }] : /* istanbul ignore next */ []));
1297
+ sourceEntityId = input(null, ...(ngDevMode ? [{ debugName: "sourceEntityId" }] : /* istanbul ignore next */ []));
1298
+ /** Two-way model for completed file metadata. */
1299
+ files = model([], ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
1300
+ // ── Outputs ──
1301
+ filesChanged = output();
1302
+ // ── State ──
1303
+ error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
1304
+ dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
1305
+ slots = signal([], ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
1306
+ /** Pre-existing files loaded from the entity (before any new uploads this session). */
1307
+ existingFiles = signal([], ...(ngDevMode ? [{ debugName: "existingFiles" }] : /* istanbul ignore next */ []));
1308
+ fileInput = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
1309
+ allSlots = computed(() => this.slots(), ...(ngDevMode ? [{ debugName: "allSlots" }] : /* istanbul ignore next */ []));
1310
+ canAddMore = computed(() => {
1311
+ const current = this.files().length + this.slots().filter(s => s.status === 'uploading').length;
1312
+ return current < this.maxFiles();
1313
+ }, ...(ngDevMode ? [{ debugName: "canAddMore" }] : /* istanbul ignore next */ []));
1314
+ limitHint = computed(() => {
1315
+ const mb = this.maxFileSizeBytes() / (1024 * 1024);
1316
+ return `Max ${this.maxFiles()} files, ${mb}MB each`;
1317
+ }, ...(ngDevMode ? [{ debugName: "limitHint" }] : /* istanbul ignore next */ []));
1318
+ triggerFileInput() {
1319
+ this.fileInput()?.nativeElement.click();
1320
+ }
1321
+ onDragOver(e) {
1322
+ e.preventDefault();
1323
+ e.stopPropagation();
1324
+ this.dragging.set(true);
1325
+ }
1326
+ onDragLeave(e) {
1327
+ e.preventDefault();
1328
+ this.dragging.set(false);
1329
+ }
1330
+ onDrop(e) {
1331
+ e.preventDefault();
1332
+ e.stopPropagation();
1333
+ this.dragging.set(false);
1334
+ const fileList = e.dataTransfer?.files;
1335
+ if (fileList)
1336
+ this.processFiles(Array.from(fileList));
1337
+ }
1338
+ onFilesSelected(e) {
1339
+ const input = e.target;
1340
+ if (input.files)
1341
+ this.processFiles(Array.from(input.files));
1342
+ input.value = '';
1343
+ }
1344
+ removeSlot(slot) {
1345
+ this.slots.update(s => s.filter(x => x !== slot));
1346
+ if (slot.info) {
1347
+ this.files.update(f => f.filter(x => x.id !== slot.info.id));
1348
+ this.filesChanged.emit(this.files());
1349
+ }
1350
+ }
1351
+ removeExisting(f) {
1352
+ this.existingFiles.update(list => list.filter(x => x.id !== f.id));
1353
+ this.files.update(list => list.filter(x => x.id !== f.id));
1354
+ this.filesChanged.emit(this.files());
1355
+ }
1356
+ iconFor(slot) { return iconForType(slot.file.type); }
1357
+ iconForInfo(f) { return iconForType(f.contentType); }
1358
+ formatFileSize(bytes) { return formatSize(bytes); }
1359
+ processFiles(newFiles) {
1360
+ this.error.set(null);
1361
+ const currentCount = this.files().length + this.slots().filter(s => s.status === 'uploading').length;
1362
+ const remaining = this.maxFiles() - currentCount;
1363
+ if (remaining <= 0) {
1364
+ this.error.set(`Maximum ${this.maxFiles()} files allowed`);
1365
+ return;
1366
+ }
1367
+ const toUpload = newFiles.slice(0, remaining);
1368
+ if (newFiles.length > remaining) {
1369
+ this.error.set(`Only ${remaining} more file(s) can be added`);
1370
+ }
1371
+ for (const file of toUpload) {
1372
+ if (file.size > this.maxFileSizeBytes()) {
1373
+ const mb = this.maxFileSizeBytes() / (1024 * 1024);
1374
+ this.error.set(`"${file.name}" exceeds ${mb}MB limit`);
1375
+ continue;
1376
+ }
1377
+ this.uploadFile(file);
1378
+ }
1379
+ }
1380
+ uploadFile(file) {
1381
+ const slot = { file, progress: 0, status: 'uploading' };
1382
+ this.slots.update(s => [...s, slot]);
1383
+ const formData = new FormData();
1384
+ formData.append('file', file, file.name);
1385
+ formData.append('sourceApp', this.sourceApp());
1386
+ if (this.sourceEntityType())
1387
+ formData.append('sourceEntityType', this.sourceEntityType());
1388
+ if (this.sourceEntityId())
1389
+ formData.append('sourceEntityId', this.sourceEntityId());
1390
+ this.http.post('/api/files/upload', formData, {
1391
+ reportProgress: true,
1392
+ observe: 'events',
1393
+ }).subscribe({
1394
+ next: (event) => {
1395
+ if (event.type === HttpEventType.UploadProgress && event.total) {
1396
+ slot.progress = Math.round((event.loaded / event.total) * 100);
1397
+ this.slots.update(s => [...s]); // trigger signal change
1398
+ }
1399
+ else if (event.type === HttpEventType.Response) {
1400
+ const info = event.body?.data ?? event.body;
1401
+ slot.status = 'done';
1402
+ slot.progress = 100;
1403
+ slot.info = info;
1404
+ this.slots.update(s => [...s]);
1405
+ this.files.update(f => [...f, info]);
1406
+ this.filesChanged.emit(this.files());
1407
+ }
1408
+ },
1409
+ error: (err) => {
1410
+ slot.status = 'error';
1411
+ slot.error = err?.error?.message ?? 'Upload failed';
1412
+ this.slots.update(s => [...s]);
1413
+ },
1414
+ });
1415
+ }
1416
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyFileUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1417
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyFileUploadComponent, isStandalone: true, selector: "fly-file-upload", inputs: { maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxFileSizeBytes: { classPropertyName: "maxFileSizeBytes", publicName: "maxFileSizeBytes", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", 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 }, files: { classPropertyName: "files", publicName: "files", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { files: "filesChange", filesChanged: "filesChanged" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
1418
+ <div class="fly-file-upload">
1419
+ @if (canAddMore()) {
1420
+ <div class="fly-file-upload__dropzone"
1421
+ [class.fly-file-upload__dropzone--drag]="dragging()"
1422
+ (click)="triggerFileInput()"
1423
+ (dragover)="onDragOver($event)"
1424
+ (dragleave)="onDragLeave($event)"
1425
+ (drop)="onDrop($event)">
1426
+ <span class="pi pi-cloud-upload fly-file-upload__icon" aria-hidden="true"></span>
1427
+ <span class="fly-file-upload__label">{{ 'files.fileUpload.dropOrClick' | translate }}</span>
1428
+ <span class="fly-file-upload__hint">{{ limitHint() }}</span>
1429
+ </div>
1430
+ }
1431
+
1432
+ @if (error()) {
1433
+ <div class="fly-file-upload__error">{{ error() }}</div>
1434
+ }
1435
+
1436
+ @if (allSlots().length) {
1437
+ <ul class="fly-file-upload__list">
1438
+ @for (slot of allSlots(); track slot.file.name + slot.file.size) {
1439
+ <li class="fly-file-upload__item">
1440
+ <span class="pi {{ iconFor(slot) }} fly-file-upload__file-icon" aria-hidden="true"></span>
1441
+ <div class="fly-file-upload__file-info">
1442
+ <span class="fly-file-upload__file-name">{{ slot.file.name }}</span>
1443
+ <span class="fly-file-upload__file-size">{{ formatFileSize(slot.file.size) }}</span>
1444
+ </div>
1445
+ @if (slot.status === 'uploading') {
1446
+ <div class="fly-file-upload__progress-track">
1447
+ <div class="fly-file-upload__progress-fill" [style.width.%]="slot.progress"></div>
1448
+ </div>
1449
+ }
1450
+ @if (slot.status === 'error') {
1451
+ <span class="fly-file-upload__file-error" [title]="slot.error ?? ''">
1452
+ <span class="pi pi-exclamation-triangle" aria-hidden="true"></span>
1453
+ </span>
1454
+ }
1455
+ @if (slot.status === 'done') {
1456
+ <span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
1457
+ }
1458
+ <button type="button" class="fly-file-upload__remove-btn" (click)="removeSlot(slot)" [title]="'common.remove' | translate">
1459
+ <span class="pi pi-times" aria-hidden="true"></span>
1460
+ </button>
1461
+ </li>
1462
+ }
1463
+ </ul>
1464
+ }
1465
+
1466
+ @if (existingFiles().length && !allSlots().length) {
1467
+ <ul class="fly-file-upload__list">
1468
+ @for (f of existingFiles(); track f.id) {
1469
+ <li class="fly-file-upload__item">
1470
+ <span class="pi {{ iconForInfo(f) }} fly-file-upload__file-icon" aria-hidden="true"></span>
1471
+ <div class="fly-file-upload__file-info">
1472
+ <span class="fly-file-upload__file-name">{{ f.fileName }}</span>
1473
+ <span class="fly-file-upload__file-size">{{ formatFileSize(f.sizeBytes) }}</span>
1474
+ </div>
1475
+ <span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
1476
+ <button type="button" class="fly-file-upload__remove-btn" (click)="removeExisting(f)" [title]="'common.remove' | translate">
1477
+ <span class="pi pi-times" aria-hidden="true"></span>
1478
+ </button>
1479
+ </li>
1480
+ }
1481
+ </ul>
1482
+ }
1483
+
1484
+ <input #fileInput type="file" [accept]="accept()" multiple class="fly-file-upload__hidden" (change)="onFilesSelected($event)" />
1485
+ </div>
1486
+ `, isInline: true, 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"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1487
+ }
1488
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyFileUploadComponent, decorators: [{
1489
+ type: Component,
1490
+ args: [{ selector: 'fly-file-upload', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1491
+ <div class="fly-file-upload">
1492
+ @if (canAddMore()) {
1493
+ <div class="fly-file-upload__dropzone"
1494
+ [class.fly-file-upload__dropzone--drag]="dragging()"
1495
+ (click)="triggerFileInput()"
1496
+ (dragover)="onDragOver($event)"
1497
+ (dragleave)="onDragLeave($event)"
1498
+ (drop)="onDrop($event)">
1499
+ <span class="pi pi-cloud-upload fly-file-upload__icon" aria-hidden="true"></span>
1500
+ <span class="fly-file-upload__label">{{ 'files.fileUpload.dropOrClick' | translate }}</span>
1501
+ <span class="fly-file-upload__hint">{{ limitHint() }}</span>
1502
+ </div>
1503
+ }
1504
+
1505
+ @if (error()) {
1506
+ <div class="fly-file-upload__error">{{ error() }}</div>
1507
+ }
1508
+
1509
+ @if (allSlots().length) {
1510
+ <ul class="fly-file-upload__list">
1511
+ @for (slot of allSlots(); track slot.file.name + slot.file.size) {
1512
+ <li class="fly-file-upload__item">
1513
+ <span class="pi {{ iconFor(slot) }} fly-file-upload__file-icon" aria-hidden="true"></span>
1514
+ <div class="fly-file-upload__file-info">
1515
+ <span class="fly-file-upload__file-name">{{ slot.file.name }}</span>
1516
+ <span class="fly-file-upload__file-size">{{ formatFileSize(slot.file.size) }}</span>
1517
+ </div>
1518
+ @if (slot.status === 'uploading') {
1519
+ <div class="fly-file-upload__progress-track">
1520
+ <div class="fly-file-upload__progress-fill" [style.width.%]="slot.progress"></div>
1521
+ </div>
1522
+ }
1523
+ @if (slot.status === 'error') {
1524
+ <span class="fly-file-upload__file-error" [title]="slot.error ?? ''">
1525
+ <span class="pi pi-exclamation-triangle" aria-hidden="true"></span>
1526
+ </span>
1527
+ }
1528
+ @if (slot.status === 'done') {
1529
+ <span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
1530
+ }
1531
+ <button type="button" class="fly-file-upload__remove-btn" (click)="removeSlot(slot)" [title]="'common.remove' | translate">
1532
+ <span class="pi pi-times" aria-hidden="true"></span>
1533
+ </button>
1534
+ </li>
1535
+ }
1536
+ </ul>
1537
+ }
1538
+
1539
+ @if (existingFiles().length && !allSlots().length) {
1540
+ <ul class="fly-file-upload__list">
1541
+ @for (f of existingFiles(); track f.id) {
1542
+ <li class="fly-file-upload__item">
1543
+ <span class="pi {{ iconForInfo(f) }} fly-file-upload__file-icon" aria-hidden="true"></span>
1544
+ <div class="fly-file-upload__file-info">
1545
+ <span class="fly-file-upload__file-name">{{ f.fileName }}</span>
1546
+ <span class="fly-file-upload__file-size">{{ formatFileSize(f.sizeBytes) }}</span>
1547
+ </div>
1548
+ <span class="pi pi-check-circle fly-file-upload__file-ok" aria-hidden="true"></span>
1549
+ <button type="button" class="fly-file-upload__remove-btn" (click)="removeExisting(f)" [title]="'common.remove' | translate">
1550
+ <span class="pi pi-times" aria-hidden="true"></span>
1551
+ </button>
1552
+ </li>
1553
+ }
1554
+ </ul>
1555
+ }
1556
+
1557
+ <input #fileInput type="file" [accept]="accept()" multiple class="fly-file-upload__hidden" (change)="onFilesSelected($event)" />
1558
+ </div>
1559
+ `, 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"] }]
1560
+ }], 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 }] }], 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 }] }] } });
1561
+
879
1562
  /*
880
1563
  * @mohamedatia/fly-design-system — Public API
881
1564
  * https://www.npmjs.com/package/@mohamedatia/fly-design-system
@@ -901,6 +1584,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
901
1584
  * MessageBoxButtons, MessageBoxIcon, DialogResult, MessageBoxOptions, MessageBoxButton exported.
902
1585
  * v1.5.0: SharePanelComponent — generic share/ACL overlay; host supplies API callbacks and optional
903
1586
  * permission level options (file ACL vs note-style View/Edit).
1587
+ * v1.6.0: SharePanelComponent — `loadOuTree(chartId: string | null)`; optional `loadChartOptions` for
1588
+ * default vs alternative org charts (OU grants still use real OU ids). `ShareOrgChartOption` exported.
1589
+ * v1.7.0: SharePanelComponent — `loadChartOptions` and `loadOuLabelMap` are required; chart tree loads after
1590
+ * options resolve (no spurious null fetch). `loadOuLabelMap` supplies default-tree labels for OU rows.
1591
+ * v1.8.0: `WindowInstance` optional `contentUiBlocked` / `contentUiBlockMessageKey`; `WindowManagerService`
1592
+ * `beginContentUiBlock` / `endContentUiBlock` default no-ops; `FlyBlockUiComponent` for shell window overlay.
904
1593
  * See docs/ExternalAppsGuide/03-frontend-app.md.
905
1594
  */
906
1595
 
@@ -908,5 +1597,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
908
1597
  * Generated bundle index. Do not edit.
909
1598
  */
910
1599
 
911
- export { AuthService, ContextMenuComponent, DialogResult, FlyThemeService, I18nService, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, isRtlLocale };
1600
+ export { AuthService, ContextMenuComponent, DEFAULT_FLY_THEME_MODE, DialogResult, FLY_THEME_MODE_IDS, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyThemeService, I18nService, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, isRtlLocale, normalizeFlyTheme };
912
1601
  //# sourceMappingURL=mohamedatia-fly-design-system.mjs.map