@flusys/ng-shared 1.0.0-beta → 1.0.0-rc

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,22 +1,24 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output, HostListener, NgModule, Injector, runInInjectionContext, resource, Component, model, untracked, forwardRef, viewChild, ChangeDetectionStrategy, InjectionToken, DestroyRef, afterNextRender, isDevMode } from '@angular/core';
2
+ import { inject, PLATFORM_ID, Injectable, DOCUMENT, REQUEST, signal, computed, ElementRef, input, effect, Directive, TemplateRef, ViewContainerRef, output, NgModule, Injector, runInInjectionContext, resource, model, untracked, forwardRef, ChangeDetectionStrategy, Component, DestroyRef, viewChild, afterNextRender, InjectionToken, isDevMode } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { isPlatformServer, CommonModule, NgOptimizedImage, NgComponentOutlet, DatePipe } from '@angular/common';
5
5
  import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
6
6
  import { APP_CONFIG, getServiceUrl, ApiLoaderService } from '@flusys/ng-core';
7
- import { of, firstValueFrom, skip, debounceTime, distinctUntilChanged, tap as tap$1, map as map$1 } from 'rxjs';
8
- import { tap, catchError, map } from 'rxjs/operators';
7
+ import { of, firstValueFrom, map as map$1 } from 'rxjs';
8
+ import { map, tap, catchError } from 'rxjs/operators';
9
9
  import * as i1$2 from '@angular/forms';
10
10
  import { NgControl, FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
11
- import { RouterOutlet, RouterLink, Router } from '@angular/router';
11
+ import { RouterOutlet, RouterLink, RouterLinkActive, Router, ActivatedRoute } from '@angular/router';
12
12
  import { AutoCompleteModule } from 'primeng/autocomplete';
13
+ import { AvatarModule } from 'primeng/avatar';
13
14
  import * as i1$3 from 'primeng/button';
14
15
  import { ButtonModule } from 'primeng/button';
15
16
  import { CardModule } from 'primeng/card';
16
- import * as i2 from 'primeng/checkbox';
17
+ import * as i1$1 from 'primeng/checkbox';
17
18
  import { CheckboxModule } from 'primeng/checkbox';
19
+ import { ConfirmDialogModule } from 'primeng/confirmdialog';
18
20
  import { DatePickerModule } from 'primeng/datepicker';
19
- import * as i6 from 'primeng/dialog';
21
+ import * as i4 from 'primeng/dialog';
20
22
  import { DialogModule } from 'primeng/dialog';
21
23
  import { DividerModule } from 'primeng/divider';
22
24
  import { FileUploadModule } from 'primeng/fileupload';
@@ -24,7 +26,7 @@ import { IconFieldModule } from 'primeng/iconfield';
24
26
  import { ImageModule } from 'primeng/image';
25
27
  import { InputIconModule } from 'primeng/inputicon';
26
28
  import { InputNumberModule } from 'primeng/inputnumber';
27
- import * as i1$1 from 'primeng/inputtext';
29
+ import * as i2 from 'primeng/inputtext';
28
30
  import { InputTextModule } from 'primeng/inputtext';
29
31
  import { ListboxModule } from 'primeng/listbox';
30
32
  import { MultiSelectModule } from 'primeng/multiselect';
@@ -39,21 +41,143 @@ import { RippleModule } from 'primeng/ripple';
39
41
  import * as i3 from 'primeng/select';
40
42
  import { SelectModule } from 'primeng/select';
41
43
  import { SelectButtonModule } from 'primeng/selectbutton';
44
+ import { SkeletonModule } from 'primeng/skeleton';
42
45
  import { SplitButtonModule } from 'primeng/splitbutton';
43
46
  import { StepsModule } from 'primeng/steps';
44
47
  import { TableModule } from 'primeng/table';
45
48
  import { TabsModule } from 'primeng/tabs';
46
49
  import { TagModule } from 'primeng/tag';
47
50
  import { TextareaModule } from 'primeng/textarea';
51
+ import { ToastModule } from 'primeng/toast';
48
52
  import { ToggleSwitchModule } from 'primeng/toggleswitch';
49
53
  import { TooltipModule } from 'primeng/tooltip';
50
54
  import { TreeTableModule } from 'primeng/treetable';
51
- import { toSignal, toObservable } from '@angular/core/rxjs-interop';
52
- import * as i4 from 'primeng/api';
53
- import { MessageService } from 'primeng/api';
55
+ import { MessageService, ConfirmationService } from 'primeng/api';
56
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
54
57
 
55
- ;
56
- ;
58
+ /**
59
+ * Centralized Permission Codes
60
+ *
61
+ * Single source of truth for all permission codes used across the application.
62
+ * Use these constants instead of hardcoded strings to prevent typos and enable easy refactoring.
63
+ *
64
+ * Naming Convention: <entity>.<action>
65
+ * - entity: The resource being accessed (e.g., user, role, company)
66
+ * - action: The operation being performed (create, read, update, delete, assign)
67
+ */
68
+ // ==================== AUTH MODULE ====================
69
+ const USER_PERMISSIONS = {
70
+ CREATE: 'user.create',
71
+ READ: 'user.read',
72
+ UPDATE: 'user.update',
73
+ DELETE: 'user.delete',
74
+ };
75
+ const COMPANY_PERMISSIONS = {
76
+ CREATE: 'company.create',
77
+ READ: 'company.read',
78
+ UPDATE: 'company.update',
79
+ DELETE: 'company.delete',
80
+ };
81
+ const BRANCH_PERMISSIONS = {
82
+ CREATE: 'branch.create',
83
+ READ: 'branch.read',
84
+ UPDATE: 'branch.update',
85
+ DELETE: 'branch.delete',
86
+ };
87
+ // ==================== IAM MODULE ====================
88
+ const ACTION_PERMISSIONS = {
89
+ CREATE: 'action.create',
90
+ READ: 'action.read',
91
+ UPDATE: 'action.update',
92
+ DELETE: 'action.delete',
93
+ };
94
+ const ROLE_PERMISSIONS = {
95
+ CREATE: 'role.create',
96
+ READ: 'role.read',
97
+ UPDATE: 'role.update',
98
+ DELETE: 'role.delete',
99
+ };
100
+ const ROLE_ACTION_PERMISSIONS = {
101
+ READ: 'role-action.read',
102
+ ASSIGN: 'role-action.assign',
103
+ };
104
+ const USER_ROLE_PERMISSIONS = {
105
+ READ: 'user-role.read',
106
+ ASSIGN: 'user-role.assign',
107
+ };
108
+ const USER_ACTION_PERMISSIONS = {
109
+ READ: 'user-action.read',
110
+ ASSIGN: 'user-action.assign',
111
+ };
112
+ const COMPANY_ACTION_PERMISSIONS = {
113
+ READ: 'company-action.read',
114
+ ASSIGN: 'company-action.assign',
115
+ };
116
+ // ==================== STORAGE MODULE ====================
117
+ const FILE_PERMISSIONS = {
118
+ CREATE: 'file.create',
119
+ READ: 'file.read',
120
+ UPDATE: 'file.update',
121
+ DELETE: 'file.delete',
122
+ };
123
+ const FOLDER_PERMISSIONS = {
124
+ CREATE: 'folder.create',
125
+ READ: 'folder.read',
126
+ UPDATE: 'folder.update',
127
+ DELETE: 'folder.delete',
128
+ };
129
+ const STORAGE_CONFIG_PERMISSIONS = {
130
+ CREATE: 'storage-config.create',
131
+ READ: 'storage-config.read',
132
+ UPDATE: 'storage-config.update',
133
+ DELETE: 'storage-config.delete',
134
+ };
135
+ // ==================== EMAIL MODULE ====================
136
+ const EMAIL_CONFIG_PERMISSIONS = {
137
+ CREATE: 'email-config.create',
138
+ READ: 'email-config.read',
139
+ UPDATE: 'email-config.update',
140
+ DELETE: 'email-config.delete',
141
+ };
142
+ const EMAIL_TEMPLATE_PERMISSIONS = {
143
+ CREATE: 'email-template.create',
144
+ READ: 'email-template.read',
145
+ UPDATE: 'email-template.update',
146
+ DELETE: 'email-template.delete',
147
+ };
148
+ // ==================== FORM BUILDER MODULE ====================
149
+ const FORM_PERMISSIONS = {
150
+ CREATE: 'form.create',
151
+ READ: 'form.read',
152
+ UPDATE: 'form.update',
153
+ DELETE: 'form.delete',
154
+ };
155
+ // ==================== AGGREGATED EXPORTS ====================
156
+ /**
157
+ * All permission codes grouped by module
158
+ */
159
+ const PERMISSIONS = {
160
+ // Auth
161
+ USER: USER_PERMISSIONS,
162
+ COMPANY: COMPANY_PERMISSIONS,
163
+ BRANCH: BRANCH_PERMISSIONS,
164
+ // IAM
165
+ ACTION: ACTION_PERMISSIONS,
166
+ ROLE: ROLE_PERMISSIONS,
167
+ ROLE_ACTION: ROLE_ACTION_PERMISSIONS,
168
+ USER_ROLE: USER_ROLE_PERMISSIONS,
169
+ USER_ACTION: USER_ACTION_PERMISSIONS,
170
+ COMPANY_ACTION: COMPANY_ACTION_PERMISSIONS,
171
+ // Storage
172
+ FILE: FILE_PERMISSIONS,
173
+ FOLDER: FOLDER_PERMISSIONS,
174
+ STORAGE_CONFIG: STORAGE_CONFIG_PERMISSIONS,
175
+ // Email
176
+ EMAIL_CONFIG: EMAIL_CONFIG_PERMISSIONS,
177
+ EMAIL_TEMPLATE: EMAIL_TEMPLATE_PERMISSIONS,
178
+ // Form Builder
179
+ FORM: FORM_PERMISSIONS,
180
+ };
57
181
 
58
182
  /**
59
183
  * Common file type filters
@@ -147,9 +271,7 @@ class PlatformService {
147
271
  }
148
272
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PlatformService, decorators: [{
149
273
  type: Injectable,
150
- args: [{
151
- providedIn: 'root'
152
- }]
274
+ args: [{ providedIn: 'root' }]
153
275
  }] });
154
276
 
155
277
  class CookieService {
@@ -198,25 +320,54 @@ class FileUrlService {
198
320
  }
199
321
  /**
200
322
  * Fetch file URLs from backend and update cache.
201
- * Returns Observable of fetched files.
323
+ * Skips IDs already in cache to prevent duplicate calls.
324
+ * Returns Observable of fetched files (including cached ones).
202
325
  */
203
- fetchFileUrls(fileIds) {
326
+ fetchFileUrls(fileIds, forceRefresh = false) {
204
327
  if (!fileIds.length)
205
328
  return of([]);
206
- const requestDto = fileIds.map((id) => ({ id }));
207
- return this.http.post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto).pipe(tap((files) => {
208
- // Update cache
209
- const cache = new Map(this.urlCache());
210
- files.forEach((file) => cache.set(file.id, file));
211
- this.urlCache.set(cache);
329
+ const cache = this.urlCache();
330
+ // Filter out IDs already in cache (unless force refresh)
331
+ const missingIds = forceRefresh
332
+ ? fileIds
333
+ : fileIds.filter((id) => !cache.has(id));
334
+ // If all files are cached, return from cache
335
+ if (missingIds.length === 0) {
336
+ const cachedFiles = fileIds
337
+ .map((id) => cache.get(id))
338
+ .filter((f) => !!f);
339
+ return of(cachedFiles);
340
+ }
341
+ const requestDto = missingIds.map((id) => ({ id }));
342
+ return this.http
343
+ .post(`${getServiceUrl(this.appConfig, 'storage')}/file-manager/get-files`, requestDto)
344
+ .pipe(map((response) => response.data ?? []), tap((files) => {
345
+ // Update cache with new files
346
+ const newCache = new Map(this.urlCache());
347
+ files.forEach((file) => newCache.set(file.id, file));
348
+ this.urlCache.set(newCache);
349
+ }), map(() => {
350
+ // Return all requested files (cached + newly fetched)
351
+ const allCache = this.urlCache();
352
+ return fileIds
353
+ .map((id) => allCache.get(id))
354
+ .filter((f) => !!f);
212
355
  }), catchError(() => of([])));
213
356
  }
214
357
  /**
215
358
  * Fetch a single file URL.
359
+ * Uses cache if available to prevent duplicate calls.
216
360
  * Returns Observable of file info or null if not found.
217
361
  */
218
- fetchSingleFileUrl(fileId) {
219
- return this.fetchFileUrls([fileId]).pipe(map((files) => files[0] ?? null));
362
+ fetchSingleFileUrl(fileId, forceRefresh = false) {
363
+ // Return from cache immediately if available
364
+ if (!forceRefresh) {
365
+ const cached = this.urlCache().get(fileId);
366
+ if (cached) {
367
+ return of(cached);
368
+ }
369
+ }
370
+ return this.fetchFileUrls([fileId], forceRefresh).pipe(map((files) => files[0] ?? null));
220
371
  }
221
372
  /**
222
373
  * Clear the URL cache.
@@ -261,6 +412,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
261
412
  * // Set permissions (typically from auth/IAM)
262
413
  * this.permissionValidator.setPermissions(['user.view', 'user.create']);
263
414
  *
415
+ * // Or set wildcard for no-IAM mode
416
+ * this.permissionValidator.setPermissions(['*']);
417
+ *
264
418
  * // Check individual permission
265
419
  * if (this.permissionValidator.hasPermission('user.view')) {
266
420
  * // Show UI element
@@ -392,19 +546,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
392
546
  }]
393
547
  }], ctorParameters: () => [], propDecorators: { isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }] } });
394
548
 
549
+ /**
550
+ * Check if user has a specific permission using wildcard matching.
551
+ * Supports:
552
+ * - Exact match: 'user.create' matches 'user.create'
553
+ * - Full wildcard: '*' matches everything
554
+ * - Module wildcard: 'user.*' matches 'user.create', 'user.read', etc.
555
+ */
556
+ function hasPermission(requiredPermission, userPermissions) {
557
+ // Exact match
558
+ if (userPermissions.includes(requiredPermission))
559
+ return true;
560
+ // Wildcard matching
561
+ for (const permission of userPermissions) {
562
+ // Full wildcard - grants all permissions
563
+ if (permission === '*')
564
+ return true;
565
+ // Module wildcard (e.g., 'user.*' matches 'user.create')
566
+ if (permission.endsWith('.*') &&
567
+ requiredPermission.startsWith(permission.slice(0, -1))) {
568
+ return true;
569
+ }
570
+ }
571
+ return false;
572
+ }
395
573
  /** Evaluate permission logic (string or ILogicNode) against user permissions */
396
574
  function evaluatePermission(logic, permissions) {
397
575
  if (!logic)
398
576
  return false;
399
577
  if (typeof logic === 'string')
400
- return permissions.includes(logic);
578
+ return hasPermission(logic, permissions);
401
579
  return evaluateLogicNode(logic, permissions);
402
580
  }
403
581
  /** Recursively evaluate an ILogicNode tree */
404
582
  function evaluateLogicNode(node, permissions) {
405
583
  switch (node.type) {
406
584
  case 'action':
407
- return node.actionId ? permissions.includes(node.actionId) : false;
585
+ return node.actionId ? hasPermission(node.actionId, permissions) : false;
408
586
  case 'group':
409
587
  if (!node.children || node.children.length === 0)
410
588
  return false;
@@ -419,13 +597,13 @@ function evaluateLogicNode(node, permissions) {
419
597
  function hasAnyPermission(permissionCodes, permissions) {
420
598
  if (!permissionCodes?.length)
421
599
  return false;
422
- return permissionCodes.some((code) => permissions.includes(code));
600
+ return permissionCodes.some((code) => hasPermission(code, permissions));
423
601
  }
424
602
  /** Check if user has ALL of the specified permissions (AND logic) */
425
603
  function hasAllPermissions(permissionCodes, permissions) {
426
604
  if (!permissionCodes?.length)
427
605
  return false;
428
- return permissionCodes.every((code) => permissions.includes(code));
606
+ return permissionCodes.every((code) => hasPermission(code, permissions));
429
607
  }
430
608
 
431
609
  /**
@@ -616,17 +794,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
616
794
  args: [{
617
795
  selector: '[appPreventDefault]',
618
796
  standalone: true,
797
+ host: {
798
+ '(click)': 'onClick($event)',
799
+ '(keydown)': 'onKeydown($event)',
800
+ '(keyup)': 'onKeyup($event)',
801
+ },
619
802
  }]
620
- }], propDecorators: { eventType: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventType", required: false }] }], preventKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "preventKey", required: false }] }], action: [{ type: i0.Output, args: ["action"] }], onClick: [{
621
- type: HostListener,
622
- args: ['click', ['$event']]
623
- }], onKeydown: [{
624
- type: HostListener,
625
- args: ['keydown', ['$event']]
626
- }], onKeyup: [{
627
- type: HostListener,
628
- args: ['keyup', ['$event']]
629
- }] } });
803
+ }], propDecorators: { eventType: [{ type: i0.Input, args: [{ isSignal: true, alias: "eventType", required: false }] }], preventKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "preventKey", required: false }] }], action: [{ type: i0.Output, args: ["action"] }] } });
630
804
 
631
805
  class AngularModule {
632
806
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -635,6 +809,7 @@ class AngularModule {
635
809
  ReactiveFormsModule,
636
810
  RouterOutlet,
637
811
  RouterLink,
812
+ RouterLinkActive,
638
813
  IsEmptyImageDirective,
639
814
  NgOptimizedImage,
640
815
  NgComponentOutlet,
@@ -643,6 +818,7 @@ class AngularModule {
643
818
  ReactiveFormsModule,
644
819
  RouterOutlet,
645
820
  RouterLink,
821
+ RouterLinkActive,
646
822
  IsEmptyImageDirective,
647
823
  NgOptimizedImage,
648
824
  NgComponentOutlet,
@@ -662,6 +838,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
662
838
  ReactiveFormsModule,
663
839
  RouterOutlet,
664
840
  RouterLink,
841
+ RouterLinkActive,
665
842
  IsEmptyImageDirective,
666
843
  NgOptimizedImage,
667
844
  NgComponentOutlet,
@@ -674,6 +851,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
674
851
  ReactiveFormsModule,
675
852
  RouterOutlet,
676
853
  RouterLink,
854
+ RouterLinkActive,
677
855
  IsEmptyImageDirective,
678
856
  NgOptimizedImage,
679
857
  NgComponentOutlet,
@@ -684,109 +862,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
684
862
 
685
863
  class PrimeModule {
686
864
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
687
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, exports: [InputTextModule,
688
- TagModule,
689
- SelectButtonModule,
690
- PasswordModule,
865
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, exports: [AutoCompleteModule,
866
+ AvatarModule,
691
867
  ButtonModule,
692
- TooltipModule,
868
+ CardModule,
693
869
  CheckboxModule,
694
- StepsModule,
695
- RippleModule,
696
- PanelModule,
697
- PaginatorModule,
698
- TableModule,
699
- InputNumberModule,
700
- TextareaModule,
701
- ProgressBarModule,
870
+ ConfirmDialogModule,
871
+ DatePickerModule,
872
+ DialogModule,
873
+ DividerModule,
702
874
  FileUploadModule,
703
- CardModule,
704
- SelectModule,
705
- InputIconModule,
706
875
  IconFieldModule,
707
- PopoverModule,
876
+ ImageModule,
877
+ InputIconModule,
878
+ InputNumberModule,
879
+ InputTextModule,
708
880
  ListboxModule,
881
+ MultiSelectModule,
882
+ PaginatorModule,
883
+ PanelModule,
884
+ PasswordModule,
885
+ PopoverModule,
886
+ ProgressBarModule,
709
887
  RadioButtonModule,
710
- ToggleSwitchModule,
711
- ImageModule,
712
- DatePickerModule,
888
+ RippleModule,
889
+ SelectButtonModule,
890
+ SelectModule,
891
+ SkeletonModule,
713
892
  SplitButtonModule,
714
- DividerModule,
715
- MultiSelectModule,
716
- AutoCompleteModule,
893
+ StepsModule,
894
+ TableModule,
717
895
  TabsModule,
718
- DialogModule,
719
- TreeTableModule] });
720
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, imports: [InputTextModule,
721
896
  TagModule,
722
- SelectButtonModule,
723
- PasswordModule,
724
- ButtonModule,
897
+ TextareaModule,
898
+ ToastModule,
899
+ ToggleSwitchModule,
725
900
  TooltipModule,
901
+ TreeTableModule] });
902
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, imports: [AutoCompleteModule,
903
+ AvatarModule,
904
+ ButtonModule,
905
+ CardModule,
726
906
  CheckboxModule,
727
- StepsModule,
728
- RippleModule,
729
- PanelModule,
730
- PaginatorModule,
731
- TableModule,
732
- InputNumberModule,
733
- TextareaModule,
734
- ProgressBarModule,
907
+ ConfirmDialogModule,
908
+ DatePickerModule,
909
+ DialogModule,
910
+ DividerModule,
735
911
  FileUploadModule,
736
- CardModule,
737
- SelectModule,
738
- InputIconModule,
739
912
  IconFieldModule,
740
- PopoverModule,
913
+ ImageModule,
914
+ InputIconModule,
915
+ InputNumberModule,
916
+ InputTextModule,
741
917
  ListboxModule,
918
+ MultiSelectModule,
919
+ PaginatorModule,
920
+ PanelModule,
921
+ PasswordModule,
922
+ PopoverModule,
923
+ ProgressBarModule,
742
924
  RadioButtonModule,
743
- ToggleSwitchModule,
744
- ImageModule,
745
- DatePickerModule,
925
+ RippleModule,
926
+ SelectButtonModule,
927
+ SelectModule,
928
+ SkeletonModule,
746
929
  SplitButtonModule,
747
- DividerModule,
748
- MultiSelectModule,
749
- AutoCompleteModule,
930
+ StepsModule,
931
+ TableModule,
750
932
  TabsModule,
751
- DialogModule,
933
+ TagModule,
934
+ TextareaModule,
935
+ ToastModule,
936
+ ToggleSwitchModule,
937
+ TooltipModule,
752
938
  TreeTableModule] });
753
939
  }
754
940
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: PrimeModule, decorators: [{
755
941
  type: NgModule,
756
942
  args: [{
757
943
  exports: [
758
- InputTextModule,
759
- TagModule,
760
- SelectButtonModule,
761
- PasswordModule,
944
+ AutoCompleteModule,
945
+ AvatarModule,
762
946
  ButtonModule,
763
- TooltipModule,
947
+ CardModule,
764
948
  CheckboxModule,
765
- StepsModule,
766
- RippleModule,
767
- PanelModule,
768
- PaginatorModule,
769
- TableModule,
770
- InputNumberModule,
771
- TextareaModule,
772
- ProgressBarModule,
949
+ ConfirmDialogModule,
950
+ DatePickerModule,
951
+ DialogModule,
952
+ DividerModule,
773
953
  FileUploadModule,
774
- CardModule,
775
- SelectModule,
776
- InputIconModule,
777
954
  IconFieldModule,
778
- PopoverModule,
955
+ ImageModule,
956
+ InputIconModule,
957
+ InputNumberModule,
958
+ InputTextModule,
779
959
  ListboxModule,
960
+ MultiSelectModule,
961
+ PaginatorModule,
962
+ PanelModule,
963
+ PasswordModule,
964
+ PopoverModule,
965
+ ProgressBarModule,
780
966
  RadioButtonModule,
781
- ToggleSwitchModule,
782
- ImageModule,
783
- DatePickerModule,
967
+ RippleModule,
968
+ SelectButtonModule,
969
+ SelectModule,
970
+ SkeletonModule,
784
971
  SplitButtonModule,
785
- DividerModule,
786
- MultiSelectModule,
787
- AutoCompleteModule,
972
+ StepsModule,
973
+ TableModule,
788
974
  TabsModule,
789
- DialogModule,
975
+ TagModule,
976
+ TextareaModule,
977
+ ToastModule,
978
+ ToggleSwitchModule,
979
+ TooltipModule,
790
980
  TreeTableModule,
791
981
  ],
792
982
  }]
@@ -861,6 +1051,12 @@ class ApiResourceService {
861
1051
  _listResource = null;
862
1052
  /** Whether the list resource has been initialized */
863
1053
  _resourceInitialized = false;
1054
+ /**
1055
+ * Signal to track resource initialization for computed signals.
1056
+ * This allows computed signals to re-evaluate when the resource is created.
1057
+ * Without this, computed signals would not detect when _listResource changes from null.
1058
+ */
1059
+ _resourceInitSignal = signal(false, ...(ngDevMode ? [{ debugName: "_resourceInitSignal" }] : []));
864
1060
  /** Get or create the list resource (lazy initialization) */
865
1061
  get listResource() {
866
1062
  if (!this._listResource) {
@@ -887,20 +1083,50 @@ class ApiResourceService {
887
1083
  return this.fetchAllAsync(search, filter);
888
1084
  } });
889
1085
  });
1086
+ // Signal that resource is now initialized - triggers computed re-evaluation
1087
+ this._resourceInitSignal.set(true);
890
1088
  }
891
1089
  // ==========================================================================
892
1090
  // Computed State Accessors
893
1091
  // ==========================================================================
894
- /** Whether data is currently loading */
895
- isLoading = computed(() => this._listResource?.isLoading() ?? false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
896
- /** List data array */
897
- data = computed(() => this._listResource?.value()?.data ?? [], ...(ngDevMode ? [{ debugName: "data" }] : []));
898
- /** Total count of items */
899
- total = computed(() => this._listResource?.value()?.meta?.total ?? 0, ...(ngDevMode ? [{ debugName: "total" }] : []));
900
- /** Pagination metadata */
901
- pageInfo = computed(() => this._listResource?.value()?.meta, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
902
- /** Whether there are more pages */
1092
+ /**
1093
+ * Whether data is currently loading.
1094
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1095
+ */
1096
+ isLoading = computed(() => {
1097
+ this._resourceInitSignal(); // Track initialization
1098
+ return this._listResource?.isLoading() ?? false;
1099
+ }, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
1100
+ /**
1101
+ * List data array.
1102
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1103
+ */
1104
+ data = computed(() => {
1105
+ this._resourceInitSignal(); // Track initialization
1106
+ return this._listResource?.value()?.data ?? [];
1107
+ }, ...(ngDevMode ? [{ debugName: "data" }] : []));
1108
+ /**
1109
+ * Total count of items.
1110
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1111
+ */
1112
+ total = computed(() => {
1113
+ this._resourceInitSignal(); // Track initialization
1114
+ return this._listResource?.value()?.meta?.total ?? 0;
1115
+ }, ...(ngDevMode ? [{ debugName: "total" }] : []));
1116
+ /**
1117
+ * Pagination metadata.
1118
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1119
+ */
1120
+ pageInfo = computed(() => {
1121
+ this._resourceInitSignal(); // Track initialization
1122
+ return this._listResource?.value()?.meta;
1123
+ }, ...(ngDevMode ? [{ debugName: "pageInfo" }] : []));
1124
+ /**
1125
+ * Whether there are more pages.
1126
+ * Tracks _resourceInitSignal to re-evaluate when resource is created.
1127
+ */
903
1128
  hasMore = computed(() => {
1129
+ this._resourceInitSignal(); // Track initialization
904
1130
  const meta = this._listResource?.value()?.meta;
905
1131
  if (!meta)
906
1132
  return false;
@@ -1075,38 +1301,6 @@ class ApiResourceService {
1075
1301
  }
1076
1302
  }
1077
1303
 
1078
- class IconComponent {
1079
- icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
1080
- iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
1081
- IconTypeEnum = IconTypeEnum; // Needed for template reference
1082
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1083
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: IconComponent, isStandalone: true, selector: "lib-icon", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, iconType: { classPropertyName: "iconType", publicName: "iconType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1084
- @if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
1085
- <i [ngClass]="icon()"></i>
1086
- }@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
1087
- <img [alt]="icon()" [src]="icon()" />
1088
- }@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
1089
- {{ icon() }}
1090
- }@else{ I } }
1091
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }] });
1092
- }
1093
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
1094
- type: Component,
1095
- args: [{
1096
- selector: 'lib-icon',
1097
- imports: [AngularModule],
1098
- template: `
1099
- @if(icon()){ @if(iconType()==IconTypeEnum.PRIMENG_ICON){
1100
- <i [ngClass]="icon()"></i>
1101
- }@else if(iconType()==IconTypeEnum.IMAGE_FILE_LINK){
1102
- <img [alt]="icon()" [src]="icon()" />
1103
- }@else if(iconType()==IconTypeEnum.DIRECT_TAG_SVG){
1104
- {{ icon() }}
1105
- }@else{ I } }
1106
- `,
1107
- }]
1108
- }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
1109
-
1110
1304
  /**
1111
1305
  * Base class for form controls that support ALL Angular form patterns:
1112
1306
  *
@@ -1230,6 +1424,44 @@ function provideValueAccessor(component) {
1230
1424
  };
1231
1425
  }
1232
1426
 
1427
+ class IconComponent {
1428
+ icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : []));
1429
+ iconType = input(IconTypeEnum.PRIMENG_ICON, ...(ngDevMode ? [{ debugName: "iconType" }] : []));
1430
+ IconTypeEnum = IconTypeEnum; // Needed for template reference
1431
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1432
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: IconComponent, isStandalone: true, selector: "lib-icon", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, iconType: { classPropertyName: "iconType", publicName: "iconType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
1433
+ @if (icon()) {
1434
+ @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
1435
+ <i [ngClass]="icon()"></i>
1436
+ } @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
1437
+ <img [alt]="icon()" [src]="icon()" />
1438
+ } @else {
1439
+ <i class="pi pi-question"></i>
1440
+ }
1441
+ }
1442
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1443
+ }
1444
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: IconComponent, decorators: [{
1445
+ type: Component,
1446
+ args: [{
1447
+ selector: 'lib-icon',
1448
+ standalone: true,
1449
+ imports: [AngularModule],
1450
+ changeDetection: ChangeDetectionStrategy.OnPush,
1451
+ template: `
1452
+ @if (icon()) {
1453
+ @if (iconType() === IconTypeEnum.PRIMENG_ICON) {
1454
+ <i [ngClass]="icon()"></i>
1455
+ } @else if (iconType() === IconTypeEnum.IMAGE_FILE_LINK) {
1456
+ <img [alt]="icon()" [src]="icon()" />
1457
+ } @else {
1458
+ <i class="pi pi-question"></i>
1459
+ }
1460
+ }
1461
+ `,
1462
+ }]
1463
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], iconType: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconType", required: false }] }] } });
1464
+
1233
1465
  /**
1234
1466
  * Lazy-loading multi-select component with search, pagination, and select-all.
1235
1467
  *
@@ -1239,6 +1471,10 @@ function provideValueAccessor(component) {
1239
1471
  * - Signal forms: `[formField]="formTree.field"`
1240
1472
  */
1241
1473
  class LazyMultiSelectComponent extends BaseFormControl {
1474
+ destroyRef = inject(DestroyRef);
1475
+ onDocumentClickBound = this.handleDocumentClick.bind(this);
1476
+ // View references
1477
+ pSelectRef = viewChild.required('pSelect');
1242
1478
  // Inputs
1243
1479
  placeHolder = input('Select Options', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1244
1480
  isEditMode = input.required(...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
@@ -1246,14 +1482,15 @@ class LazyMultiSelectComponent extends BaseFormControl {
1246
1482
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1247
1483
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1248
1484
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1249
- /** Two-way bound value using model() for signal forms compatibility */
1485
+ // Two-way bound value
1250
1486
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1251
1487
  // Outputs
1252
1488
  onSearch = output();
1253
1489
  onPagination = output();
1254
- // UI signals
1490
+ // UI state
1255
1491
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1256
- /** Computed: Display text for selected values (replaces getSelectedValue method) */
1492
+ openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1493
+ // Computed values
1257
1494
  selectedValueDisplay = computed(() => {
1258
1495
  const selectedValues = this.value() ?? [];
1259
1496
  if (selectedValues.length === 0)
@@ -1262,44 +1499,63 @@ class LazyMultiSelectComponent extends BaseFormControl {
1262
1499
  return `${selectedValues.length} Items Selected`;
1263
1500
  }
1264
1501
  return this.selectDataList()
1265
- .filter(item => selectedValues.includes(item.value))
1266
- .map(item => item.label)
1502
+ .filter((item) => selectedValues.includes(item.value))
1503
+ .map((item) => item.label)
1267
1504
  .join(', ');
1268
1505
  }, ...(ngDevMode ? [{ debugName: "selectedValueDisplay" }] : []));
1269
- /** Computed: Whether all items are selected (replaces isSelectAll signal) */
1270
1506
  isSelectAll = computed(() => {
1271
1507
  const selectedValues = this.value() ?? [];
1272
- const allValues = this.selectDataList().map(item => item.value);
1508
+ const allValues = this.selectDataList().map((item) => item.value);
1273
1509
  if (selectedValues.length === 0 || allValues.length === 0)
1274
1510
  return false;
1275
- return allValues.every(val => selectedValues.includes(val));
1511
+ return allValues.every((val) => selectedValues.includes(val));
1276
1512
  }, ...(ngDevMode ? [{ debugName: "isSelectAll" }] : []));
1277
1513
  constructor() {
1278
1514
  super();
1279
1515
  this.initializeFormControl();
1280
- // Search debounce effect
1281
- runInInjectionContext(this.injector, () => {
1282
- toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1((value) => {
1283
- this.onSearch.emit(value);
1284
- })), { initialValue: this.searchTerm() });
1516
+ // Search debounce using effect
1517
+ let debounceTimeout = null;
1518
+ let previousValue = this.searchTerm();
1519
+ effect((onCleanup) => {
1520
+ const currentValue = this.searchTerm();
1521
+ // Skip unchanged values
1522
+ if (currentValue === previousValue)
1523
+ return;
1524
+ previousValue = currentValue;
1525
+ // Clear existing timeout
1526
+ if (debounceTimeout)
1527
+ clearTimeout(debounceTimeout);
1528
+ // Debounced emit
1529
+ debounceTimeout = setTimeout(() => {
1530
+ this.onSearch.emit(currentValue);
1531
+ }, 500);
1532
+ onCleanup(() => {
1533
+ if (debounceTimeout)
1534
+ clearTimeout(debounceTimeout);
1535
+ });
1536
+ });
1537
+ // Document click listener for closing dropdown
1538
+ afterNextRender(() => {
1539
+ document.addEventListener('click', this.onDocumentClickBound);
1540
+ });
1541
+ this.destroyRef.onDestroy(() => {
1542
+ document.removeEventListener('click', this.onDocumentClickBound);
1285
1543
  });
1286
1544
  }
1287
- onScrollBound = this.onScroll.bind(this);
1288
- multiScrollContainer = viewChild.required('multiScrollContainer');
1289
1545
  onScroll(event) {
1290
1546
  const el = event.target;
1547
+ if (!(el instanceof HTMLElement))
1548
+ return;
1291
1549
  const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
1292
1550
  if (nearBottom && !this.isLoading()) {
1293
- const pagination = this.pagination();
1294
- const nextPage = pagination.currentPage + 1;
1295
- const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
1551
+ const pag = this.pagination();
1552
+ const nextPage = pag.currentPage + 1;
1553
+ const hasMore = nextPage * pag.pageSize < (this.total() ?? 0);
1296
1554
  if (hasMore) {
1297
- this.onPagination.emit({ ...pagination, currentPage: nextPage });
1555
+ this.onPagination.emit({ ...pag, currentPage: nextPage });
1298
1556
  }
1299
1557
  }
1300
1558
  }
1301
- pSelectRef = viewChild.required('pSelect');
1302
- openOptions = signal(false, ...(ngDevMode ? [{ debugName: "openOptions" }] : []));
1303
1559
  onSelectClick(event) {
1304
1560
  if (this.disabled())
1305
1561
  return;
@@ -1318,7 +1574,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1318
1574
  }
1319
1575
  }
1320
1576
  isSelected(data) {
1321
- return this.value()?.includes(data.value);
1577
+ return this.value()?.includes(data.value) ?? false;
1322
1578
  }
1323
1579
  key(option) {
1324
1580
  return option.value;
@@ -1331,8 +1587,7 @@ class LazyMultiSelectComponent extends BaseFormControl {
1331
1587
  }
1332
1588
  }
1333
1589
  else {
1334
- const updated = previousValue.filter((v) => v !== option.value);
1335
- this.value.set(updated);
1590
+ this.value.set(previousValue.filter((v) => v !== option.value));
1336
1591
  }
1337
1592
  }
1338
1593
  changeSelectAll(event) {
@@ -1348,15 +1603,12 @@ class LazyMultiSelectComponent extends BaseFormControl {
1348
1603
  this.value.set([]);
1349
1604
  }
1350
1605
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1351
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: LazyMultiSelectComponent, isStandalone: true, selector: "lib-lazy-multi-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, host: { listeners: { "document:click": "handleDocumentClick($event)" } }, providers: [provideValueAccessor(LazyMultiSelectComponent)], viewQueries: [{ propertyName: "multiScrollContainer", first: true, predicate: ["multiScrollContainer"], descendants: true, isSignal: true }, { propertyName: "pSelectRef", first: true, predicate: ["pSelect"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\"\n [class.p-disabled]=\"disabled()\">\n @if(selectedValueDisplay()){\n <span class=\"p-select-label\">{{selectedValueDisplay()}}</span>\n }@else {\n <span class=\"p-select-label p-placeholder\">{{placeHolder()}}</span>\n }\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\"><i class=\"pi pi-times\"></i></span>\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n data-p-icon=\"chevron-down\" class=\"p-multiselect-dropdown-icon p-icon ng-star-inserted\"\n data-pc-section=\"triggericon\" aria-hidden=\"true\" pc75=\"\">\n <path\n d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\"\n fill=\"currentColor\"></path>\n </svg>\n </span>\n </div>\n @if(openOptions()){\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox binary=\"true\" (onChange)=\"changeSelectAll($event)\" [ngModel]=\"isSelectAll()\" [disabled]=\"disabled()\"/>\n <input type=\"text\" pInputText class=\"w-full\" [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\" [ngModelOptions]=\"{ standalone: true }\"\n placeholder=\"Search...\" />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data); let i = $index) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox binary=\"true\" (onChange)=\"selectValue($event,data)\" [ngModel]=\"isSelected(data)\" [disabled]=\"disabled()\" />\n <span>{{data.label}}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{top:33px;z-index:1004;transform-origin:center top;margin-top:2px}.p-select-option:hover{background:var(--p-select-option-focus-background);color:var(--p-select-option-focus-color)}.p-select-list-container{max-height:200px}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background);color:var(--p-select-option-selected-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background);color:var(--p-select-option-selected-focus-color)}\n"], dependencies: [{ kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i2.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1606
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: LazyMultiSelectComponent, isStandalone: true, selector: "lib-lazy-multi-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, providers: [provideValueAccessor(LazyMultiSelectComponent)], viewQueries: [{ propertyName: "pSelectRef", first: true, predicate: ["pSelect"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\" [class.p-disabled]=\"disabled()\">\n @if (selectedValueDisplay()) {\n <span class=\"p-select-label\">{{ selectedValueDisplay() }}</span>\n } @else {\n <span class=\"p-select-label p-placeholder\">{{ placeHolder() }}</span>\n }\n\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\">\n <i class=\"pi pi-times\"></i>\n </span>\n\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n class=\"p-multiselect-dropdown-icon p-icon\" aria-hidden=\"true\">\n <path d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\" fill=\"currentColor\" />\n </svg>\n </span>\n </div>\n\n @if (openOptions()) {\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelectAll()\"\n [disabled]=\"disabled()\"\n (onChange)=\"changeSelectAll($event)\"\n />\n <input\n type=\"text\"\n pInputText\n class=\"w-full\"\n placeholder=\"Search...\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data)) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelected(data)\"\n [disabled]=\"disabled()\"\n (onChange)=\"selectValue($event, data)\"\n />\n <span>{{ data.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{position:absolute;top:100%;left:0;right:0;z-index:var(--p-overlay-select-zindex, 1004);margin-top:var(--p-select-overlay-offset, 2px);background:var(--p-select-overlay-background, var(--p-surface-0));border:1px solid var(--p-select-overlay-border-color, var(--p-surface-200));border-radius:var(--p-select-overlay-border-radius, var(--p-border-radius));box-shadow:var(--p-select-overlay-shadow, var(--p-overlay-shadow))}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-200);background:var(--p-surface-50)}:host-context(.p-dark) .p-select-header,.dark .p-select-header{border-color:var(--p-surface-700);background:var(--p-surface-800)}.p-select-list-container{max-height:10rem;overflow-y:auto}@media(min-width:640px){.p-select-list-container{max-height:12.5rem}}.p-select-list{margin:0;padding:.25rem 0;list-style:none}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease}.p-select-option:hover{background:var(--p-select-option-focus-background, var(--p-surface-100));color:var(--p-select-option-focus-color, var(--p-text-color))}:host-context(.p-dark) .p-select-option:hover,.dark .p-select-option:hover{background:var(--p-surface-700)}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background, var(--p-primary-50));color:var(--p-select-option-selected-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected,.dark .p-select-option.p-select-option-selected{background:var(--p-primary-900)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background, var(--p-primary-100));color:var(--p-select-option-selected-focus-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected:hover,.dark .p-select-option.p-select-option-selected:hover{background:var(--p-primary-800)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-color-secondary);cursor:pointer;transition:color .2s ease}.p-select-clear-icon:hover{color:var(--p-text-color)}\n"], dependencies: [{ kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i1$1.Checkbox, selector: "p-checkbox, p-checkBox, p-check-box", inputs: ["hostName", "value", "binary", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "inputStyle", "styleClass", "inputClass", "indeterminate", "formControl", "checkboxIcon", "readonly", "autofocus", "trueValue", "falseValue", "variant", "size"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1352
1607
  }
1353
1608
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazyMultiSelectComponent, decorators: [{
1354
1609
  type: Component,
1355
- args: [{ selector: 'lib-lazy-multi-select', imports: [PrimeModule, AngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazyMultiSelectComponent)], template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\"\n [class.p-disabled]=\"disabled()\">\n @if(selectedValueDisplay()){\n <span class=\"p-select-label\">{{selectedValueDisplay()}}</span>\n }@else {\n <span class=\"p-select-label p-placeholder\">{{placeHolder()}}</span>\n }\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\"><i class=\"pi pi-times\"></i></span>\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n data-p-icon=\"chevron-down\" class=\"p-multiselect-dropdown-icon p-icon ng-star-inserted\"\n data-pc-section=\"triggericon\" aria-hidden=\"true\" pc75=\"\">\n <path\n d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\"\n fill=\"currentColor\"></path>\n </svg>\n </span>\n </div>\n @if(openOptions()){\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox binary=\"true\" (onChange)=\"changeSelectAll($event)\" [ngModel]=\"isSelectAll()\" [disabled]=\"disabled()\"/>\n <input type=\"text\" pInputText class=\"w-full\" [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\" [ngModelOptions]=\"{ standalone: true }\"\n placeholder=\"Search...\" />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data); let i = $index) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox binary=\"true\" (onChange)=\"selectValue($event,data)\" [ngModel]=\"isSelected(data)\" [disabled]=\"disabled()\" />\n <span>{{data.label}}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{top:33px;z-index:1004;transform-origin:center top;margin-top:2px}.p-select-option:hover{background:var(--p-select-option-focus-background);color:var(--p-select-option-focus-color)}.p-select-list-container{max-height:200px}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background);color:var(--p-select-option-selected-color)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background);color:var(--p-select-option-selected-focus-color)}\n"] }]
1356
- }], ctorParameters: () => [], propDecorators: { placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }], multiScrollContainer: [{ type: i0.ViewChild, args: ['multiScrollContainer', { isSignal: true }] }], pSelectRef: [{ type: i0.ViewChild, args: ['pSelect', { isSignal: true }] }], handleDocumentClick: [{
1357
- type: HostListener,
1358
- args: ['document:click', ['$event']]
1359
- }] } });
1610
+ args: [{ selector: 'lib-lazy-multi-select', standalone: true, imports: [PrimeModule, AngularModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazyMultiSelectComponent)], template: "<div class=\"p-select w-full\" #pSelect (click)=\"onSelectClick($event)\" [class.p-disabled]=\"disabled()\">\n @if (selectedValueDisplay()) {\n <span class=\"p-select-label\">{{ selectedValueDisplay() }}</span>\n } @else {\n <span class=\"p-select-label p-placeholder\">{{ placeHolder() }}</span>\n }\n\n <span class=\"p-select-clear-icon\" (click)=\"clear($event)\">\n <i class=\"pi pi-times\"></i>\n </span>\n\n <div class=\"p-select-dropdown\">\n <span class=\"p-select-dropdown-icon flex items-center\">\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"\n class=\"p-multiselect-dropdown-icon p-icon\" aria-hidden=\"true\">\n <path d=\"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z\" fill=\"currentColor\" />\n </svg>\n </span>\n </div>\n\n @if (openOptions()) {\n <div class=\"p-select-overlay\" (click)=\"onOverlayClick($event)\">\n <div class=\"p-select-header flex flex-row gap-2 items-center\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelectAll()\"\n [disabled]=\"disabled()\"\n (onChange)=\"changeSelectAll($event)\"\n />\n <input\n type=\"text\"\n pInputText\n class=\"w-full\"\n placeholder=\"Search...\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </div>\n <div class=\"p-select-list-container\" (scroll)=\"onScroll($event)\">\n <ul class=\"p-select-list\">\n @for (data of selectDataList(); track key(data)) {\n <li class=\"p-select-option flex flex-row gap-2 items-center\"\n [ngClass]=\"{ 'p-select-option-selected': isSelected(data) }\">\n <p-checkbox\n binary=\"true\"\n [ngModel]=\"isSelected(data)\"\n [disabled]=\"disabled()\"\n (onChange)=\"selectValue($event, data)\"\n />\n <span>{{ data.label }}</span>\n </li>\n }\n </ul>\n </div>\n </div>\n }\n</div>", styles: [".p-select-overlay{position:absolute;top:100%;left:0;right:0;z-index:var(--p-overlay-select-zindex, 1004);margin-top:var(--p-select-overlay-offset, 2px);background:var(--p-select-overlay-background, var(--p-surface-0));border:1px solid var(--p-select-overlay-border-color, var(--p-surface-200));border-radius:var(--p-select-overlay-border-radius, var(--p-border-radius));box-shadow:var(--p-select-overlay-shadow, var(--p-overlay-shadow))}.p-select-header{padding:.75rem;border-bottom:1px solid var(--p-surface-200);background:var(--p-surface-50)}:host-context(.p-dark) .p-select-header,.dark .p-select-header{border-color:var(--p-surface-700);background:var(--p-surface-800)}.p-select-list-container{max-height:10rem;overflow-y:auto}@media(min-width:640px){.p-select-list-container{max-height:12.5rem}}.p-select-list{margin:0;padding:.25rem 0;list-style:none}.p-select-option{padding:.5rem .75rem;cursor:pointer;transition:background-color .2s ease}.p-select-option:hover{background:var(--p-select-option-focus-background, var(--p-surface-100));color:var(--p-select-option-focus-color, var(--p-text-color))}:host-context(.p-dark) .p-select-option:hover,.dark .p-select-option:hover{background:var(--p-surface-700)}.p-select-option.p-select-option-selected{background:var(--p-select-option-selected-background, var(--p-primary-50));color:var(--p-select-option-selected-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected,.dark .p-select-option.p-select-option-selected{background:var(--p-primary-900)}.p-select-option.p-select-option-selected:hover{background:var(--p-select-option-selected-focus-background, var(--p-primary-100));color:var(--p-select-option-selected-focus-color, var(--p-primary-color))}:host-context(.p-dark) .p-select-option.p-select-option-selected:hover,.dark .p-select-option.p-select-option-selected:hover{background:var(--p-primary-800)}.p-select-clear-icon{display:flex;align-items:center;padding:0 .5rem;color:var(--p-text-color-secondary);cursor:pointer;transition:color .2s ease}.p-select-clear-icon:hover{color:var(--p-text-color)}\n"] }]
1611
+ }], ctorParameters: () => [], propDecorators: { pSelectRef: [{ type: i0.ViewChild, args: ['pSelect', { isSignal: true }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }] } });
1360
1612
 
1361
1613
  /**
1362
1614
  * Lazy-loading single select component with search and pagination.
@@ -1367,6 +1619,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1367
1619
  * - Signal forms: `[formField]="formTree.field"`
1368
1620
  */
1369
1621
  class LazySelectComponent extends BaseFormControl {
1622
+ destroyRef = inject(DestroyRef);
1623
+ onScrollBound = this.onScroll.bind(this);
1624
+ scrollTargetEl = null;
1625
+ // View references
1626
+ scrollContainer = viewChild.required('scrollContainer');
1370
1627
  // Inputs
1371
1628
  placeHolder = input('Select Option', ...(ngDevMode ? [{ debugName: "placeHolder" }] : []));
1372
1629
  optionLabel = input.required(...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
@@ -1376,77 +1633,89 @@ class LazySelectComponent extends BaseFormControl {
1376
1633
  total = input.required(...(ngDevMode ? [{ debugName: "total" }] : []));
1377
1634
  pagination = input.required(...(ngDevMode ? [{ debugName: "pagination" }] : []));
1378
1635
  selectDataList = input.required(...(ngDevMode ? [{ debugName: "selectDataList" }] : []));
1379
- /** Two-way bound value using model() for signal forms compatibility */
1636
+ // Two-way bound value
1380
1637
  value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
1381
1638
  // Outputs
1382
1639
  onSearch = output();
1383
1640
  onPagination = output();
1384
- // UI signals
1641
+ // UI state
1385
1642
  searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : []));
1386
- // Effect hooks
1643
+ isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
1387
1644
  constructor() {
1388
1645
  super();
1389
1646
  this.initializeFormControl();
1390
- runInInjectionContext(this.injector, () => {
1391
- toSignal(toObservable(this.searchTerm).pipe(skip(1), debounceTime(500), distinctUntilChanged(), tap$1(value => {
1392
- this.onSearch.emit(value);
1393
- })), { initialValue: this.searchTerm() });
1647
+ // Search debounce using effect
1648
+ let debounceTimeout = null;
1649
+ let previousValue = this.searchTerm();
1650
+ effect((onCleanup) => {
1651
+ const currentValue = this.searchTerm();
1652
+ // Skip unchanged values
1653
+ if (currentValue === previousValue)
1654
+ return;
1655
+ previousValue = currentValue;
1656
+ // Clear existing timeout
1657
+ if (debounceTimeout)
1658
+ clearTimeout(debounceTimeout);
1659
+ // Debounced emit
1660
+ debounceTimeout = setTimeout(() => {
1661
+ this.onSearch.emit(currentValue);
1662
+ }, 500);
1663
+ onCleanup(() => {
1664
+ if (debounceTimeout)
1665
+ clearTimeout(debounceTimeout);
1666
+ });
1667
+ });
1668
+ // Cleanup scroll listener on destroy
1669
+ this.destroyRef.onDestroy(() => {
1670
+ if (this.scrollTargetEl) {
1671
+ this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1672
+ }
1394
1673
  });
1395
1674
  }
1396
- // Signal to toggle panel
1397
- isPanelShow = signal(false, ...(ngDevMode ? [{ debugName: "isPanelShow" }] : []));
1398
- scrollTargetEl = null;
1399
- onScrollBound = this.onScroll.bind(this);
1400
- scrollContainer = viewChild.required('scrollContainer');
1401
1675
  onScroll(event) {
1402
1676
  const el = event.target;
1677
+ if (!(el instanceof HTMLElement))
1678
+ return;
1403
1679
  const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 50;
1404
1680
  if (nearBottom && !this.isLoading()) {
1405
- const pagination = this.pagination();
1406
- const nextPage = pagination.currentPage + 1;
1407
- const hasMore = nextPage * pagination.pageSize < (this.total() ?? 0);
1681
+ const pag = this.pagination();
1682
+ const nextPage = pag.currentPage + 1;
1683
+ const hasMore = nextPage * pag.pageSize < (this.total() ?? 0);
1408
1684
  if (hasMore) {
1409
- this.onPagination.emit({ ...pagination, currentPage: nextPage });
1685
+ this.onPagination.emit({ ...pag, currentPage: nextPage });
1410
1686
  }
1411
1687
  }
1412
1688
  }
1413
- // Toggle panel and manage scroll event
1414
1689
  showPanel() {
1415
1690
  if (this.disabled())
1416
1691
  return;
1417
- this.isPanelShow.update(prev => !prev);
1692
+ this.isPanelShow.update((prev) => !prev);
1418
1693
  const isNowVisible = this.isPanelShow();
1419
1694
  if (isNowVisible) {
1420
- setTimeout(() => {
1695
+ afterNextRender(() => {
1421
1696
  const containerEl = this.scrollContainer().nativeElement;
1422
1697
  const target = containerEl.querySelector('.p-select-list-container');
1423
1698
  if (target) {
1424
1699
  target.addEventListener('scroll', this.onScrollBound);
1425
1700
  this.scrollTargetEl = target;
1426
1701
  }
1427
- }, 0);
1702
+ }, { injector: this.injector });
1428
1703
  }
1429
- else {
1430
- if (this.scrollTargetEl) {
1431
- this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1432
- this.scrollTargetEl = null;
1433
- }
1704
+ else if (this.scrollTargetEl) {
1705
+ this.scrollTargetEl.removeEventListener('scroll', this.onScrollBound);
1706
+ this.scrollTargetEl = null;
1434
1707
  }
1435
1708
  }
1436
1709
  onBlur() {
1437
1710
  this.markAsTouched();
1438
1711
  }
1439
1712
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1440
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", type: LazySelectComponent, isStandalone: true, selector: "lib-lazy-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: true, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: true, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, providers: [provideValueAccessor(LazySelectComponent)], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n [options]=\"selectDataList()\"\n [(ngModel)]=\"value\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n class=\"w-full\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\">\n <ng-template let-filter #filter>\n <input\n pInputText\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\"\n [ngModelOptions]=\"{standalone:true}\"\n class=\"w-full\" />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template let-item #item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i3.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "directive", type: EditModeElementChangerDirective, selector: "[appEditModeElementChanger]", inputs: ["isEditMode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1713
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", type: LazySelectComponent, isStandalone: true, selector: "lib-lazy-select", inputs: { placeHolder: { classPropertyName: "placeHolder", publicName: "placeHolder", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: true, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: true, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: true, transformFunction: null }, isLoading: { classPropertyName: "isLoading", publicName: "isLoading", isSignal: true, isRequired: true, transformFunction: null }, total: { classPropertyName: "total", publicName: "total", isSignal: true, isRequired: true, transformFunction: null }, pagination: { classPropertyName: "pagination", publicName: "pagination", isSignal: true, isRequired: true, transformFunction: null }, selectDataList: { classPropertyName: "selectDataList", publicName: "selectDataList", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", onSearch: "onSearch", onPagination: "onPagination" }, providers: [provideValueAccessor(LazySelectComponent)], viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n class=\"w-full\"\n [options]=\"selectDataList()\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n [(ngModel)]=\"value\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\"\n >\n <ng-template #filter let-filter>\n <input\n pInputText\n class=\"w-full\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template #item let-item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i3.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "directive", type: EditModeElementChangerDirective, selector: "[appEditModeElementChanger]", inputs: ["isEditMode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1441
1714
  }
1442
1715
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: LazySelectComponent, decorators: [{
1443
1716
  type: Component,
1444
- args: [{ selector: 'lib-lazy-select', imports: [
1445
- AngularModule,
1446
- PrimeModule,
1447
- EditModeElementChangerDirective
1448
- ], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazySelectComponent)], template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n [options]=\"selectDataList()\"\n [(ngModel)]=\"value\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n class=\"w-full\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\">\n <ng-template let-filter #filter>\n <input\n pInputText\n [ngModel]=\"searchTerm()\"\n (ngModelChange)=\"searchTerm.set($event)\"\n [ngModelOptions]=\"{standalone:true}\"\n class=\"w-full\" />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template let-item #item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n" }]
1449
- }], ctorParameters: () => [], propDecorators: { placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: true }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: true }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }], scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }] } });
1717
+ args: [{ selector: 'lib-lazy-select', standalone: true, imports: [AngularModule, PrimeModule, EditModeElementChangerDirective], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideValueAccessor(LazySelectComponent)], template: "<div #scrollContainer class=\"lib-scroll-container\">\n <p-select\n class=\"w-full\"\n [options]=\"selectDataList()\"\n [optionLabel]=\"optionLabel()\"\n [optionValue]=\"optionValue()\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"placeHolder()\"\n [disabled]=\"disabled()\"\n [(ngModel)]=\"value\"\n appEditModeElementChanger\n [isEditMode]=\"isEditMode()\"\n (click)=\"showPanel()\"\n (onBlur)=\"onBlur()\"\n >\n <ng-template #filter let-filter>\n <input\n pInputText\n class=\"w-full\"\n [ngModel]=\"searchTerm()\"\n [ngModelOptions]=\"{ standalone: true }\"\n (ngModelChange)=\"searchTerm.set($event)\"\n />\n </ng-template>\n <ng-template #selectedItem let-selectedOption>\n {{ selectedOption[optionLabel()] }}\n </ng-template>\n <ng-template #item let-item>\n {{ item[optionLabel()] }}\n </ng-template>\n </p-select>\n</div>\n" }]
1718
+ }], ctorParameters: () => [], propDecorators: { scrollContainer: [{ type: i0.ViewChild, args: ['scrollContainer', { isSignal: true }] }], placeHolder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeHolder", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: true }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: true }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: true }] }], isLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLoading", required: true }] }], total: [{ type: i0.Input, args: [{ isSignal: true, alias: "total", required: true }] }], pagination: [{ type: i0.Input, args: [{ isSignal: true, alias: "pagination", required: true }] }], selectDataList: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectDataList", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], onSearch: [{ type: i0.Output, args: ["onSearch"] }], onPagination: [{ type: i0.Output, args: ["onPagination"] }] } });
1450
1719
 
1451
1720
  /**
1452
1721
  * Injection Tokens for Provider Interfaces
@@ -1513,6 +1782,36 @@ const AUTH_STATE_PROVIDER = new InjectionToken('AUTH_STATE_PROVIDER', {
1513
1782
  throw new Error('AUTH_STATE_PROVIDER not configured. Please provide an implementation in app.config.ts');
1514
1783
  },
1515
1784
  });
1785
+ /**
1786
+ * Profile Permission Provider Token
1787
+ *
1788
+ * Provides user permission data for profile display.
1789
+ * Optional - if not configured, profile permissions section is hidden.
1790
+ * Use with `inject(PROFILE_PERMISSION_PROVIDER, { optional: true })`.
1791
+ */
1792
+ const PROFILE_PERMISSION_PROVIDER = new InjectionToken('PROFILE_PERMISSION_PROVIDER');
1793
+ /**
1794
+ * Profile Upload Provider Token
1795
+ *
1796
+ * Provides file upload functionality for profile pictures.
1797
+ * Optional - if not configured or storage not enabled, upload section is hidden.
1798
+ * Use with `inject(PROFILE_UPLOAD_PROVIDER, { optional: true })`.
1799
+ */
1800
+ const PROFILE_UPLOAD_PROVIDER = new InjectionToken('PROFILE_UPLOAD_PROVIDER');
1801
+ /**
1802
+ * User List Provider Token
1803
+ *
1804
+ * Provides extra actions, columns, and data enrichment for user list.
1805
+ * Optional - if not configured, default user list behavior is used.
1806
+ * Use with `inject(USER_LIST_PROVIDER, { optional: true })`.
1807
+ *
1808
+ * @example
1809
+ * // In app.config.ts
1810
+ * providers: [
1811
+ * { provide: USER_LIST_PROVIDER, useClass: MyUserListProvider },
1812
+ * ]
1813
+ */
1814
+ const USER_LIST_PROVIDER = new InjectionToken('USER_LIST_PROVIDER');
1516
1815
 
1517
1816
  const DEFAULT_PAGE_SIZE$2 = 20;
1518
1817
  /**
@@ -1914,41 +2213,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1914
2213
  * File Uploader Component - Drag & drop file upload with type filtering.
1915
2214
  *
1916
2215
  * Pass your own `uploadFile` function - works with any storage API.
1917
- *
1918
- * Features:
1919
- * - Drag & drop support
1920
- * - File type filtering (images, documents, etc.)
1921
- * - Upload progress indication
1922
- * - Multiple file support (optional)
1923
- * - Image compression options
1924
- *
1925
- * @example
1926
- * ```typescript
1927
- * // In component
1928
- * readonly uploadService = inject(UploadService);
1929
- *
1930
- * readonly uploadFile: UploadFileFn = (file, options) =>
1931
- * this.uploadService.uploadSingleFile(file, options);
1932
- * ```
1933
- *
1934
- * ```html
1935
- * <!-- Single image upload -->
1936
- * <lib-file-uploader
1937
- * [uploadFile]="uploadFile"
1938
- * [acceptTypes]="['image/*']"
1939
- * [multiple]="false"
1940
- * (fileUploaded)="onFileUploaded($event)"
1941
- * />
1942
- *
1943
- * <!-- Multiple document upload -->
1944
- * <lib-file-uploader
1945
- * [uploadFile]="uploadFile"
1946
- * [acceptTypes]="FILE_TYPE_FILTERS.DOCUMENTS"
1947
- * [multiple]="true"
1948
- * [maxFiles]="5"
1949
- * (filesUploaded)="onFilesUploaded($event)"
1950
- * />
1951
- * ```
1952
2216
  */
1953
2217
  class FileUploaderComponent {
1954
2218
  messageService = inject(MessageService);
@@ -2094,11 +2358,7 @@ class FileUploaderComponent {
2094
2358
  });
2095
2359
  }
2096
2360
  catch (error) {
2097
- this.messageService.add({
2098
- severity: 'error',
2099
- summary: 'Upload Failed',
2100
- detail: error.message || 'Failed to upload file',
2101
- });
2361
+ // Error toast handled by global interceptor
2102
2362
  this.onError.emit(error);
2103
2363
  }
2104
2364
  finally {
@@ -2120,37 +2380,41 @@ class FileUploaderComponent {
2120
2380
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2121
2381
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: FileUploaderComponent, isStandalone: true, selector: "lib-file-uploader", inputs: { uploadFile: { classPropertyName: "uploadFile", publicName: "uploadFile", isSignal: true, isRequired: true, transformFunction: null }, acceptTypes: { classPropertyName: "acceptTypes", publicName: "acceptTypes", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxSizeMb: { classPropertyName: "maxSizeMb", publicName: "maxSizeMb", isSignal: true, isRequired: false, transformFunction: null }, uploadOptions: { classPropertyName: "uploadOptions", publicName: "uploadOptions", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showPreview: { classPropertyName: "showPreview", publicName: "showPreview", isSignal: true, isRequired: false, transformFunction: null }, autoUpload: { classPropertyName: "autoUpload", publicName: "autoUpload", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileUploaded: "fileUploaded", filesUploaded: "filesUploaded", onError: "onError", fileSelected: "fileSelected" }, ngImport: i0, template: `
2122
2382
  <div
2123
- class="file-uploader"
2124
- [class.drag-over]="isDragOver()"
2125
- [class.disabled]="disabled()"
2383
+ class="w-full"
2384
+ [class.opacity-60]="disabled()"
2126
2385
  (dragover)="onDragOver($event)"
2127
2386
  (dragleave)="onDragLeave($event)"
2128
2387
  (drop)="onDrop($event)"
2129
2388
  >
2130
- <!-- Upload Area -->
2131
- <div class="upload-area" (click)="fileInput.click()">
2389
+ <!-- Upload Area - Responsive padding -->
2390
+ <div
2391
+ class="upload-zone border-2 border-dashed rounded-lg p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-200 text-center"
2392
+ [class.drag-over]="isDragOver()"
2393
+ [class.cursor-not-allowed]="disabled()"
2394
+ (click)="fileInput.click()"
2395
+ >
2132
2396
  @if (isUploading()) {
2133
- <div class="uploading-state">
2134
- <i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
2135
- <p class="mt-2">Uploading {{ uploadingFileName() }}...</p>
2397
+ <div class="flex flex-col items-center">
2398
+ <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2399
+ <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2136
2400
  @if (uploadProgress() > 0) {
2137
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2401
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2138
2402
  }
2139
2403
  </div>
2140
2404
  } @else {
2141
- <div class="idle-state text-center">
2142
- <i class="pi pi-cloud-upload text-4xl text-primary"></i>
2143
- <p class="mt-2 mb-1 font-semibold">
2405
+ <div class="flex flex-col items-center">
2406
+ <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2407
+ <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2144
2408
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2145
2409
  </p>
2146
- <p class="text-sm text-color-secondary">
2410
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2147
2411
  @if (acceptTypesDisplay()) {
2148
2412
  Allowed: {{ acceptTypesDisplay() }}
2149
2413
  } @else {
2150
2414
  All file types allowed
2151
2415
  }
2152
2416
  @if (maxSizeMb()) {
2153
- (Max {{ maxSizeMb() }}MB)
2417
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2154
2418
  }
2155
2419
  </p>
2156
2420
  </div>
@@ -2161,71 +2425,76 @@ class FileUploaderComponent {
2161
2425
  <input
2162
2426
  #fileInput
2163
2427
  type="file"
2428
+ class="hidden"
2164
2429
  [accept]="acceptString()"
2165
2430
  [multiple]="multiple()"
2166
2431
  [disabled]="disabled() || isUploading()"
2167
2432
  (change)="onFileSelected($event)"
2168
- class="hidden"
2169
2433
  />
2170
2434
 
2171
- <!-- Selected Files Preview -->
2435
+ <!-- Selected Files Preview - Responsive layout -->
2172
2436
  @if (selectedFiles().length > 0 && showPreview()) {
2173
- <div class="selected-files mt-3">
2437
+ <div class="mt-3 space-y-2">
2174
2438
  @for (file of selectedFiles(); track file.name) {
2175
- <div class="file-item flex align-items-center gap-2 p-2 border-round surface-border border-1 mb-2">
2176
- <i [class]="getFileIcon(file)"></i>
2177
- <span class="flex-1 text-overflow-ellipsis overflow-hidden">{{ file.name }}</span>
2178
- <span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
2179
- <button
2180
- pButton
2181
- type="button"
2439
+ <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2440
+ <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2441
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2442
+ <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2443
+ <p-button
2182
2444
  icon="pi pi-times"
2183
- class="p-button-text p-button-rounded p-button-sm"
2184
- (click)="removeFile(file)"
2445
+ [text]="true"
2446
+ [rounded]="true"
2447
+ size="small"
2448
+ severity="secondary"
2185
2449
  [disabled]="isUploading()"
2186
- ></button>
2450
+ (onClick)="removeFile(file)"
2451
+ />
2187
2452
  </div>
2188
2453
  }
2189
2454
  </div>
2190
2455
  }
2191
2456
  </div>
2192
- `, isInline: true, styles: [".file-uploader{width:100%}.upload-area{border:2px dashed var(--surface-border);border-radius:var(--border-radius);padding:2rem;cursor:pointer;transition:all .2s;background:var(--surface-ground)}.upload-area:hover{border-color:var(--primary-color);background:var(--surface-hover)}.drag-over .upload-area{border-color:var(--primary-color);background:var(--primary-100)}.disabled .upload-area{opacity:.6;cursor:not-allowed}.hidden{display:none}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$3.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i2$1.ProgressBar, selector: "p-progressBar, p-progressbar, p-progress-bar", inputs: ["value", "showValue", "styleClass", "valueStyleClass", "unit", "mode", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2457
+ `, isInline: true, styles: [".upload-zone{border-color:var(--p-surface-300);background:var(--p-surface-50)}.upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-100)}.upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .upload-zone,.dark .upload-zone{border-color:var(--p-surface-600);background:var(--p-surface-800)}:host-context(.p-dark) .upload-zone:hover:not(.cursor-not-allowed),.dark .upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-700)}:host-context(.p-dark) .upload-zone.drag-over,.dark .upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-900)}.file-preview-item{border:1px solid var(--p-surface-200);background:var(--p-surface-0)}:host-context(.p-dark) .file-preview-item,.dark .file-preview-item{border-color:var(--p-surface-600);background:var(--p-surface-800)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "ngmodule", type: PrimeModule }, { kind: "component", type: i1$3.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: i2$1.ProgressBar, selector: "p-progressBar, p-progressbar, p-progress-bar", inputs: ["value", "showValue", "styleClass", "valueStyleClass", "unit", "mode", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2193
2458
  }
2194
2459
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileUploaderComponent, decorators: [{
2195
2460
  type: Component,
2196
2461
  args: [{ selector: 'lib-file-uploader', standalone: true, imports: [AngularModule, PrimeModule], template: `
2197
2462
  <div
2198
- class="file-uploader"
2199
- [class.drag-over]="isDragOver()"
2200
- [class.disabled]="disabled()"
2463
+ class="w-full"
2464
+ [class.opacity-60]="disabled()"
2201
2465
  (dragover)="onDragOver($event)"
2202
2466
  (dragleave)="onDragLeave($event)"
2203
2467
  (drop)="onDrop($event)"
2204
2468
  >
2205
- <!-- Upload Area -->
2206
- <div class="upload-area" (click)="fileInput.click()">
2469
+ <!-- Upload Area - Responsive padding -->
2470
+ <div
2471
+ class="upload-zone border-2 border-dashed rounded-lg p-4 sm:p-6 md:p-8 cursor-pointer transition-all duration-200 text-center"
2472
+ [class.drag-over]="isDragOver()"
2473
+ [class.cursor-not-allowed]="disabled()"
2474
+ (click)="fileInput.click()"
2475
+ >
2207
2476
  @if (isUploading()) {
2208
- <div class="uploading-state">
2209
- <i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
2210
- <p class="mt-2">Uploading {{ uploadingFileName() }}...</p>
2477
+ <div class="flex flex-col items-center">
2478
+ <i class="pi pi-spin pi-spinner text-3xl sm:text-4xl text-primary"></i>
2479
+ <p class="mt-2 text-sm sm:text-base break-all px-2">Uploading {{ uploadingFileName() }}...</p>
2211
2480
  @if (uploadProgress() > 0) {
2212
- <p-progressBar [value]="uploadProgress()" [showValue]="true" />
2481
+ <p-progressBar [value]="uploadProgress()" [showValue]="true" class="w-full mt-2 max-w-xs" />
2213
2482
  }
2214
2483
  </div>
2215
2484
  } @else {
2216
- <div class="idle-state text-center">
2217
- <i class="pi pi-cloud-upload text-4xl text-primary"></i>
2218
- <p class="mt-2 mb-1 font-semibold">
2485
+ <div class="flex flex-col items-center">
2486
+ <i class="pi pi-cloud-upload text-3xl sm:text-4xl text-primary"></i>
2487
+ <p class="mt-2 mb-1 font-semibold text-sm sm:text-base">
2219
2488
  {{ multiple() ? 'Drop files here or click to upload' : 'Drop file here or click to upload' }}
2220
2489
  </p>
2221
- <p class="text-sm text-color-secondary">
2490
+ <p class="text-xs sm:text-sm text-color-secondary px-2">
2222
2491
  @if (acceptTypesDisplay()) {
2223
2492
  Allowed: {{ acceptTypesDisplay() }}
2224
2493
  } @else {
2225
2494
  All file types allowed
2226
2495
  }
2227
2496
  @if (maxSizeMb()) {
2228
- (Max {{ maxSizeMb() }}MB)
2497
+ <span class="whitespace-nowrap">(Max {{ maxSizeMb() }}MB)</span>
2229
2498
  }
2230
2499
  </p>
2231
2500
  </div>
@@ -2236,35 +2505,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2236
2505
  <input
2237
2506
  #fileInput
2238
2507
  type="file"
2508
+ class="hidden"
2239
2509
  [accept]="acceptString()"
2240
2510
  [multiple]="multiple()"
2241
2511
  [disabled]="disabled() || isUploading()"
2242
2512
  (change)="onFileSelected($event)"
2243
- class="hidden"
2244
2513
  />
2245
2514
 
2246
- <!-- Selected Files Preview -->
2515
+ <!-- Selected Files Preview - Responsive layout -->
2247
2516
  @if (selectedFiles().length > 0 && showPreview()) {
2248
- <div class="selected-files mt-3">
2517
+ <div class="mt-3 space-y-2">
2249
2518
  @for (file of selectedFiles(); track file.name) {
2250
- <div class="file-item flex align-items-center gap-2 p-2 border-round surface-border border-1 mb-2">
2251
- <i [class]="getFileIcon(file)"></i>
2252
- <span class="flex-1 text-overflow-ellipsis overflow-hidden">{{ file.name }}</span>
2253
- <span class="text-sm text-color-secondary">{{ formatSize(file.size) }}</span>
2254
- <button
2255
- pButton
2256
- type="button"
2519
+ <div class="file-preview-item flex items-center gap-2 p-2 sm:p-3 rounded-lg">
2520
+ <i [class]="getFileIcon(file)" class="text-lg sm:text-xl flex-shrink-0"></i>
2521
+ <span class="flex-1 truncate text-sm sm:text-base min-w-0">{{ file.name }}</span>
2522
+ <span class="text-xs sm:text-sm text-color-secondary whitespace-nowrap">{{ formatSize(file.size) }}</span>
2523
+ <p-button
2257
2524
  icon="pi pi-times"
2258
- class="p-button-text p-button-rounded p-button-sm"
2259
- (click)="removeFile(file)"
2525
+ [text]="true"
2526
+ [rounded]="true"
2527
+ size="small"
2528
+ severity="secondary"
2260
2529
  [disabled]="isUploading()"
2261
- ></button>
2530
+ (onClick)="removeFile(file)"
2531
+ />
2262
2532
  </div>
2263
2533
  }
2264
2534
  </div>
2265
2535
  }
2266
2536
  </div>
2267
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-uploader{width:100%}.upload-area{border:2px dashed var(--surface-border);border-radius:var(--border-radius);padding:2rem;cursor:pointer;transition:all .2s;background:var(--surface-ground)}.upload-area:hover{border-color:var(--primary-color);background:var(--surface-hover)}.drag-over .upload-area{border-color:var(--primary-color);background:var(--primary-100)}.disabled .upload-area{opacity:.6;cursor:not-allowed}.hidden{display:none}\n"] }]
2537
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".upload-zone{border-color:var(--p-surface-300);background:var(--p-surface-50)}.upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-100)}.upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .upload-zone,.dark .upload-zone{border-color:var(--p-surface-600);background:var(--p-surface-800)}:host-context(.p-dark) .upload-zone:hover:not(.cursor-not-allowed),.dark .upload-zone:hover:not(.cursor-not-allowed){border-color:var(--p-primary-color);background:var(--p-surface-700)}:host-context(.p-dark) .upload-zone.drag-over,.dark .upload-zone.drag-over{border-color:var(--p-primary-color);background:var(--p-primary-900)}.file-preview-item{border:1px solid var(--p-surface-200);background:var(--p-surface-0)}:host-context(.p-dark) .file-preview-item,.dark .file-preview-item{border-color:var(--p-surface-600);background:var(--p-surface-800)}\n"] }]
2268
2538
  }], propDecorators: { uploadFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadFile", required: true }] }], acceptTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "acceptTypes", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], maxSizeMb: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSizeMb", required: false }] }], uploadOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadOptions", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreview", required: false }] }], autoUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoUpload", required: false }] }], fileUploaded: [{ type: i0.Output, args: ["fileUploaded"] }], filesUploaded: [{ type: i0.Output, args: ["filesUploaded"] }], onError: [{ type: i0.Output, args: ["onError"] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }] } });
2269
2539
 
2270
2540
  const DEFAULT_PAGE_SIZE = 20;
@@ -2499,11 +2769,13 @@ class FileSelectorDialogComponent {
2499
2769
  [closable]="true"
2500
2770
  [draggable]="false"
2501
2771
  [resizable]="false"
2502
- [style]="{ width: '800px', maxHeight: '90vh' }"
2772
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2773
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2774
+ styleClass="file-selector-dialog"
2503
2775
  (onHide)="onDialogHide()"
2504
2776
  >
2505
2777
  <!-- Search Bar -->
2506
- <div class="flex gap-2 mb-3">
2778
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2507
2779
  <span class="p-input-icon-left flex-1">
2508
2780
  <i class="pi pi-search"></i>
2509
2781
  <input
@@ -2516,25 +2788,25 @@ class FileSelectorDialogComponent {
2516
2788
  />
2517
2789
  </span>
2518
2790
  @if (multiple()) {
2519
- <span class="text-sm text-color-secondary align-self-center">
2791
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2520
2792
  {{ selectedFiles().length }} selected
2521
2793
  </span>
2522
2794
  }
2523
2795
  </div>
2524
2796
 
2525
- <!-- File Grid -->
2797
+ <!-- File Grid - Responsive columns -->
2526
2798
  <div
2527
2799
  class="file-grid"
2528
2800
  #scrollContainer
2529
2801
  (scroll)="onScroll($event)"
2530
2802
  >
2531
2803
  @if (isLoading() && files().length === 0) {
2532
- <div class="flex justify-content-center p-4">
2533
- <i class="pi pi-spin pi-spinner text-4xl"></i>
2804
+ <div class="col-span-full flex justify-center p-4">
2805
+ <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2534
2806
  </div>
2535
2807
  } @else if (files().length === 0) {
2536
- <div class="text-center p-4 text-color-secondary">
2537
- <i class="pi pi-inbox text-4xl mb-2"></i>
2808
+ <div class="col-span-full text-center p-4 text-color-secondary">
2809
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
2538
2810
  <p>No files found</p>
2539
2811
  </div>
2540
2812
  } @else {
@@ -2548,50 +2820,55 @@ class FileSelectorDialogComponent {
2548
2820
  <!-- File Preview -->
2549
2821
  <div class="file-preview">
2550
2822
  @if (isImage(file) && file.url) {
2551
- <img [src]="file.url" [alt]="file.name" class="preview-image" />
2823
+ <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2552
2824
  } @else {
2553
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2825
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2554
2826
  }
2555
2827
  @if (isSelected(file)) {
2556
2828
  <div class="selected-overlay">
2557
- <i class="pi pi-check"></i>
2829
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2558
2830
  </div>
2559
2831
  }
2560
2832
  </div>
2561
2833
 
2562
2834
  <!-- File Info -->
2563
- <div class="file-info">
2564
- <span class="file-name" [title]="file.name">{{ file.name }}</span>
2565
- <span class="file-size">{{ formatSize(file.size) }}</span>
2835
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2836
+ <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2837
+ {{ file.name }}
2838
+ </span>
2839
+ <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2566
2840
  </div>
2567
2841
  </div>
2568
2842
  }
2569
2843
 
2570
2844
  @if (isLoading()) {
2571
- <div class="flex justify-content-center p-2 w-full">
2572
- <i class="pi pi-spin pi-spinner"></i>
2845
+ <div class="col-span-full flex justify-center p-2">
2846
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2573
2847
  </div>
2574
2848
  }
2575
2849
  }
2576
2850
  </div>
2577
2851
 
2578
2852
  <!-- Footer -->
2579
- <ng-template pTemplate="footer">
2580
- <button
2581
- pButton
2582
- label="Cancel"
2583
- class="p-button-text"
2584
- (click)="onCancel()"
2585
- ></button>
2586
- <button
2587
- pButton
2588
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2589
- [disabled]="selectedFiles().length === 0"
2590
- (click)="onConfirm()"
2591
- ></button>
2853
+ <ng-template #footer>
2854
+ <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2855
+ <button
2856
+ pButton
2857
+ label="Cancel"
2858
+ class="p-button-text w-full sm:w-auto"
2859
+ (click)="onCancel()"
2860
+ ></button>
2861
+ <button
2862
+ pButton
2863
+ [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2864
+ [disabled]="selectedFiles().length === 0"
2865
+ class="w-full sm:w-auto"
2866
+ (click)="onConfirm()"
2867
+ ></button>
2868
+ </div>
2592
2869
  </ng-template>
2593
2870
  </p-dialog>
2594
- `, isInline: true, styles: [".file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem;max-height:400px;overflow-y:auto;padding:.5rem}.file-card{border:2px solid var(--surface-border);border-radius:var(--border-radius);cursor:pointer;transition:all .2s;overflow:hidden}.file-card:hover:not(.disabled){border-color:var(--primary-color)}.file-card.selected{border-color:var(--primary-color);background:var(--primary-50)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:100px;display:flex;align-items:center;justify-content:center;background:var(--surface-ground)}.preview-image{width:100%;height:100%;object-fit:cover}.preview-icon{font-size:3rem;color:var(--text-color-secondary)}.selected-overlay{position:absolute;inset:0;background:rgba(var(--primary-color-rgb),.3);display:flex;align-items:center;justify-content:center}.selected-overlay i{font-size:2rem;color:var(--primary-color);background:#fff;border-radius:50%;padding:.5rem}.file-info{padding:.5rem;text-align:center}.file-name{display:block;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-size{display:block;font-size:.75rem;color:var(--text-color-secondary)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$1.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "directive", type: i4.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "directive", type: i1$3.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i6.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2871
+ `, isInline: true, styles: [".file-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem;max-height:300px;overflow-y:auto;padding:.5rem}@media(min-width:480px){.file-grid{grid-template-columns:repeat(3,1fr);max-height:350px}}@media(min-width:640px){.file-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:1rem;max-height:400px}}.file-card{border:2px solid var(--p-surface-300);border-radius:var(--p-border-radius);cursor:pointer;transition:all .2s ease;overflow:hidden;background:var(--p-surface-0)}:host-context(.p-dark) .file-card,.dark .file-card{border-color:var(--p-surface-600);background:var(--p-surface-800)}.file-card:hover:not(.disabled){border-color:var(--p-primary-color);transform:translateY(-2px);box-shadow:var(--p-overlay-shadow)}.file-card.selected{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .file-card.selected,.dark .file-card.selected{background:var(--p-primary-900)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:80px;display:flex;align-items:center;justify-content:center;background:var(--p-surface-100)}@media(min-width:640px){.file-preview{height:100px}}:host-context(.p-dark) .file-preview,.dark .file-preview{background:var(--p-surface-700)}.selected-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--p-primary-500-rgb, 59, 130, 246),.3)}.selected-overlay i{color:var(--p-primary-color);background:var(--p-surface-0);border-radius:50%;padding:.5rem}:host-context(.p-dark) .selected-overlay i,.dark .selected-overlay i{background:var(--p-surface-900)}\n"], dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: IsEmptyImageDirective, selector: "img", inputs: ["src"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i1$3.ButtonDirective, selector: "[pButton]", inputs: ["ptButtonDirective", "pButtonPT", "pButtonUnstyled", "hostName", "text", "plain", "raised", "size", "outlined", "rounded", "iconPos", "loadingIcon", "fluid", "label", "icon", "loading", "buttonProps", "severity"] }, { kind: "component", type: i4.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i2.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2595
2872
  }
2596
2873
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: FileSelectorDialogComponent, decorators: [{
2597
2874
  type: Component,
@@ -2603,11 +2880,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2603
2880
  [closable]="true"
2604
2881
  [draggable]="false"
2605
2882
  [resizable]="false"
2606
- [style]="{ width: '800px', maxHeight: '90vh' }"
2883
+ [breakpoints]="{ '960px': '90vw', '640px': '95vw' }"
2884
+ [style]="{ width: '800px', maxWidth: '95vw', maxHeight: '90vh' }"
2885
+ styleClass="file-selector-dialog"
2607
2886
  (onHide)="onDialogHide()"
2608
2887
  >
2609
2888
  <!-- Search Bar -->
2610
- <div class="flex gap-2 mb-3">
2889
+ <div class="flex flex-col sm:flex-row gap-2 mb-3">
2611
2890
  <span class="p-input-icon-left flex-1">
2612
2891
  <i class="pi pi-search"></i>
2613
2892
  <input
@@ -2620,25 +2899,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2620
2899
  />
2621
2900
  </span>
2622
2901
  @if (multiple()) {
2623
- <span class="text-sm text-color-secondary align-self-center">
2902
+ <span class="text-sm text-color-secondary self-center whitespace-nowrap">
2624
2903
  {{ selectedFiles().length }} selected
2625
2904
  </span>
2626
2905
  }
2627
2906
  </div>
2628
2907
 
2629
- <!-- File Grid -->
2908
+ <!-- File Grid - Responsive columns -->
2630
2909
  <div
2631
2910
  class="file-grid"
2632
2911
  #scrollContainer
2633
2912
  (scroll)="onScroll($event)"
2634
2913
  >
2635
2914
  @if (isLoading() && files().length === 0) {
2636
- <div class="flex justify-content-center p-4">
2637
- <i class="pi pi-spin pi-spinner text-4xl"></i>
2915
+ <div class="col-span-full flex justify-center p-4">
2916
+ <i class="pi pi-spin pi-spinner text-4xl text-color-secondary"></i>
2638
2917
  </div>
2639
2918
  } @else if (files().length === 0) {
2640
- <div class="text-center p-4 text-color-secondary">
2641
- <i class="pi pi-inbox text-4xl mb-2"></i>
2919
+ <div class="col-span-full text-center p-4 text-color-secondary">
2920
+ <i class="pi pi-inbox text-4xl mb-2 block"></i>
2642
2921
  <p>No files found</p>
2643
2922
  </div>
2644
2923
  } @else {
@@ -2652,50 +2931,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2652
2931
  <!-- File Preview -->
2653
2932
  <div class="file-preview">
2654
2933
  @if (isImage(file) && file.url) {
2655
- <img [src]="file.url" [alt]="file.name" class="preview-image" />
2934
+ <img [src]="file.url" [alt]="file.name" class="w-full h-full object-cover" />
2656
2935
  } @else {
2657
- <i [class]="getFileIcon(file)" class="preview-icon"></i>
2936
+ <i [class]="getFileIcon(file)" class="text-4xl sm:text-5xl text-color-secondary"></i>
2658
2937
  }
2659
2938
  @if (isSelected(file)) {
2660
2939
  <div class="selected-overlay">
2661
- <i class="pi pi-check"></i>
2940
+ <i class="pi pi-check text-xl sm:text-2xl"></i>
2662
2941
  </div>
2663
2942
  }
2664
2943
  </div>
2665
2944
 
2666
2945
  <!-- File Info -->
2667
- <div class="file-info">
2668
- <span class="file-name" [title]="file.name">{{ file.name }}</span>
2669
- <span class="file-size">{{ formatSize(file.size) }}</span>
2946
+ <div class="p-2 text-center bg-surface-0 dark:bg-surface-900">
2947
+ <span class="block text-xs sm:text-sm whitespace-nowrap overflow-hidden text-ellipsis" [title]="file.name">
2948
+ {{ file.name }}
2949
+ </span>
2950
+ <span class="block text-xs text-color-secondary">{{ formatSize(file.size) }}</span>
2670
2951
  </div>
2671
2952
  </div>
2672
2953
  }
2673
2954
 
2674
2955
  @if (isLoading()) {
2675
- <div class="flex justify-content-center p-2 w-full">
2676
- <i class="pi pi-spin pi-spinner"></i>
2956
+ <div class="col-span-full flex justify-center p-2">
2957
+ <i class="pi pi-spin pi-spinner text-color-secondary"></i>
2677
2958
  </div>
2678
2959
  }
2679
2960
  }
2680
2961
  </div>
2681
2962
 
2682
2963
  <!-- Footer -->
2683
- <ng-template pTemplate="footer">
2684
- <button
2685
- pButton
2686
- label="Cancel"
2687
- class="p-button-text"
2688
- (click)="onCancel()"
2689
- ></button>
2690
- <button
2691
- pButton
2692
- [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2693
- [disabled]="selectedFiles().length === 0"
2694
- (click)="onConfirm()"
2695
- ></button>
2964
+ <ng-template #footer>
2965
+ <div class="flex flex-col-reverse sm:flex-row gap-2 w-full sm:w-auto sm:justify-end">
2966
+ <button
2967
+ pButton
2968
+ label="Cancel"
2969
+ class="p-button-text w-full sm:w-auto"
2970
+ (click)="onCancel()"
2971
+ ></button>
2972
+ <button
2973
+ pButton
2974
+ [label]="multiple() ? 'Select (' + selectedFiles().length + ')' : 'Select'"
2975
+ [disabled]="selectedFiles().length === 0"
2976
+ class="w-full sm:w-auto"
2977
+ (click)="onConfirm()"
2978
+ ></button>
2979
+ </div>
2696
2980
  </ng-template>
2697
2981
  </p-dialog>
2698
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem;max-height:400px;overflow-y:auto;padding:.5rem}.file-card{border:2px solid var(--surface-border);border-radius:var(--border-radius);cursor:pointer;transition:all .2s;overflow:hidden}.file-card:hover:not(.disabled){border-color:var(--primary-color)}.file-card.selected{border-color:var(--primary-color);background:var(--primary-50)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:100px;display:flex;align-items:center;justify-content:center;background:var(--surface-ground)}.preview-image{width:100%;height:100%;object-fit:cover}.preview-icon{font-size:3rem;color:var(--text-color-secondary)}.selected-overlay{position:absolute;inset:0;background:rgba(var(--primary-color-rgb),.3);display:flex;align-items:center;justify-content:center}.selected-overlay i{font-size:2rem;color:var(--primary-color);background:#fff;border-radius:50%;padding:.5rem}.file-info{padding:.5rem;text-align:center}.file-name{display:block;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.file-size{display:block;font-size:.75rem;color:var(--text-color-secondary)}\n"] }]
2982
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".file-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.75rem;max-height:300px;overflow-y:auto;padding:.5rem}@media(min-width:480px){.file-grid{grid-template-columns:repeat(3,1fr);max-height:350px}}@media(min-width:640px){.file-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:1rem;max-height:400px}}.file-card{border:2px solid var(--p-surface-300);border-radius:var(--p-border-radius);cursor:pointer;transition:all .2s ease;overflow:hidden;background:var(--p-surface-0)}:host-context(.p-dark) .file-card,.dark .file-card{border-color:var(--p-surface-600);background:var(--p-surface-800)}.file-card:hover:not(.disabled){border-color:var(--p-primary-color);transform:translateY(-2px);box-shadow:var(--p-overlay-shadow)}.file-card.selected{border-color:var(--p-primary-color);background:var(--p-primary-50)}:host-context(.p-dark) .file-card.selected,.dark .file-card.selected{background:var(--p-primary-900)}.file-card.disabled{opacity:.5;cursor:not-allowed}.file-preview{position:relative;height:80px;display:flex;align-items:center;justify-content:center;background:var(--p-surface-100)}@media(min-width:640px){.file-preview{height:100px}}:host-context(.p-dark) .file-preview,.dark .file-preview{background:var(--p-surface-700)}.selected-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--p-primary-500-rgb, 59, 130, 246),.3)}.selected-overlay i{color:var(--p-primary-color);background:var(--p-surface-0);border-radius:50%;padding:.5rem}:host-context(.p-dark) .selected-overlay i,.dark .selected-overlay i{background:var(--p-surface-900)}\n"] }]
2699
2983
  }], ctorParameters: () => [], propDecorators: { loadFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadFiles", required: true }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], acceptTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "acceptTypes", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelection", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }, { type: i0.Output, args: ["visibleChange"] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], filesSelected: [{ type: i0.Output, args: ["filesSelected"] }], closed: [{ type: i0.Output, args: ["closed"] }], onError: [{ type: i0.Output, args: ["onError"] }] } });
2700
2984
 
2701
2985
  /** Log only in dev mode */
@@ -2823,11 +3107,362 @@ function allPermissionsGuard(permissions, redirectTo = '/') {
2823
3107
  };
2824
3108
  }
2825
3109
 
2826
- // Interfaces
3110
+ /**
3111
+ * Base class for form page components that handle create/edit operations.
3112
+ * Provides common functionality for loading existing items, form submission,
3113
+ * navigation, and toast notifications.
3114
+ *
3115
+ * ## Features
3116
+ * - Automatic route parameter handling (loads item when ID is present)
3117
+ * - Edit mode detection based on existing item
3118
+ * - Unified submit handler for create/update operations
3119
+ * - Cancel navigation
3120
+ * - Toast messages for success/validation errors
3121
+ *
3122
+ * ## Usage
3123
+ *
3124
+ * ```typescript
3125
+ * interface IProductFormModel {
3126
+ * name: string;
3127
+ * price: number;
3128
+ * }
3129
+ *
3130
+ * @Component({
3131
+ * selector: 'app-product-form',
3132
+ * standalone: true,
3133
+ * changeDetection: ChangeDetectionStrategy.OnPush,
3134
+ * template: `...`
3135
+ * })
3136
+ * export class ProductFormComponent extends BaseFormPage<IProduct, IProductFormModel> {
3137
+ * private readonly productService = inject(ProductApiService);
3138
+ *
3139
+ * // Form model signal (private writable, public readonly)
3140
+ * private readonly _formModel = signal<IProductFormModel>({ name: '', price: 0 });
3141
+ * readonly formModel = this._formModel.asReadonly();
3142
+ *
3143
+ * // Required abstract implementations
3144
+ * getFormModel(): Signal<IProductFormModel> {
3145
+ * return this.formModel;
3146
+ * }
3147
+ *
3148
+ * getResourceRoute(): string {
3149
+ * return '/products';
3150
+ * }
3151
+ *
3152
+ * getResourceName(): string {
3153
+ * return 'Product';
3154
+ * }
3155
+ *
3156
+ * isFormValid(): boolean {
3157
+ * const model = this.formModel();
3158
+ * return model.name.trim().length > 0 && model.price > 0;
3159
+ * }
3160
+ *
3161
+ * loadItem(id: string): void {
3162
+ * this.isLoading.set(true);
3163
+ * this.productService.findById(id)
3164
+ * .pipe(takeUntilDestroyed(this.destroyRef))
3165
+ * .subscribe({
3166
+ * next: (response) => {
3167
+ * if (response.success && response.data) {
3168
+ * this.existingItem.set(response.data);
3169
+ * this._formModel.set({
3170
+ * name: response.data.name,
3171
+ * price: response.data.price,
3172
+ * });
3173
+ * }
3174
+ * this.isLoading.set(false);
3175
+ * },
3176
+ * error: () => {
3177
+ * this.router.navigate([this.getResourceRoute()]);
3178
+ * this.isLoading.set(false);
3179
+ * },
3180
+ * });
3181
+ * }
3182
+ *
3183
+ * createItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
3184
+ * return this.productService.insert(model);
3185
+ * }
3186
+ *
3187
+ * updateItem(model: IProductFormModel): Observable<ISingleResponse<IProduct>> {
3188
+ * return this.productService.update({ id: this.existingItem()!.id, ...model });
3189
+ * }
3190
+ * }
3191
+ * ```
3192
+ *
3193
+ * @template T The entity/interface type being edited
3194
+ * @template TFormModel The form model interface
3195
+ */
3196
+ class BaseFormPage {
3197
+ router = inject(Router);
3198
+ route = inject(ActivatedRoute);
3199
+ messageService = inject(MessageService);
3200
+ destroyRef = inject(DestroyRef);
3201
+ routeParams = toSignal(this.route.paramMap);
3202
+ initialized = false;
3203
+ /** Loading state for async operations */
3204
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
3205
+ /** The existing item when in edit mode, null when creating */
3206
+ existingItem = signal(null, ...(ngDevMode ? [{ debugName: "existingItem" }] : []));
3207
+ /** Whether the form is in edit mode (has existing item) */
3208
+ isEditMode = computed(() => !!this.existingItem(), ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
3209
+ constructor() {
3210
+ effect(() => {
3211
+ const params = this.routeParams();
3212
+ if (!params || this.initialized)
3213
+ return;
3214
+ this.initialized = true;
3215
+ const id = params.get('id');
3216
+ if (id && id !== 'new') {
3217
+ this.loadItem(id);
3218
+ }
3219
+ });
3220
+ }
3221
+ /**
3222
+ * Handle form submission.
3223
+ * Validates the form, then calls createItem or updateItem based on mode.
3224
+ * Shows appropriate toast messages and navigates back on success.
3225
+ */
3226
+ onSubmit() {
3227
+ if (!this.isFormValid()) {
3228
+ this.showValidationError();
3229
+ return;
3230
+ }
3231
+ this.isLoading.set(true);
3232
+ const model = this.getFormModel()();
3233
+ const operation$ = this.isEditMode()
3234
+ ? this.updateItem(model)
3235
+ : this.createItem(model);
3236
+ operation$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
3237
+ next: () => {
3238
+ const action = this.isEditMode() ? 'updated' : 'created';
3239
+ this.showSuccess(`${this.getResourceName()} ${action} successfully.`);
3240
+ this.router.navigate([this.getResourceRoute()]);
3241
+ },
3242
+ error: () => {
3243
+ this.isLoading.set(false);
3244
+ },
3245
+ complete: () => {
3246
+ this.isLoading.set(false);
3247
+ },
3248
+ });
3249
+ }
3250
+ /**
3251
+ * Handle cancel action.
3252
+ * Navigates back to the resource list.
3253
+ */
3254
+ onCancel() {
3255
+ this.router.navigate([this.getResourceRoute()]);
3256
+ }
3257
+ /**
3258
+ * Show validation error toast.
3259
+ * Override to customize the validation error message.
3260
+ */
3261
+ showValidationError() {
3262
+ this.messageService.add({
3263
+ severity: 'error',
3264
+ summary: 'Validation Error',
3265
+ detail: 'Please fill in all required fields.',
3266
+ });
3267
+ }
3268
+ /**
3269
+ * Show success toast.
3270
+ * @param detail The success message to display
3271
+ */
3272
+ showSuccess(detail) {
3273
+ this.messageService.add({
3274
+ severity: 'success',
3275
+ summary: 'Success',
3276
+ detail,
3277
+ });
3278
+ }
3279
+ /**
3280
+ * Show error toast.
3281
+ * @param detail The error message to display
3282
+ */
3283
+ showError(detail) {
3284
+ this.messageService.add({
3285
+ severity: 'error',
3286
+ summary: 'Error',
3287
+ detail,
3288
+ });
3289
+ }
3290
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3291
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: BaseFormPage, isStandalone: true, ngImport: i0 });
3292
+ }
3293
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseFormPage, decorators: [{
3294
+ type: Directive
3295
+ }], ctorParameters: () => [] });
3296
+
3297
+ /**
3298
+ * Base List Page
3299
+ * Abstract directive providing common signals, computed values, and utilities
3300
+ * for list page components across all feature packages.
3301
+ *
3302
+ * Features:
3303
+ * - Pagination state management (pageSize, currentPage, total)
3304
+ * - Loading state
3305
+ * - CRUD navigation helpers
3306
+ * - Message display utilities
3307
+ * - Delete confirmation with API integration
3308
+ * - Company feature flag support
3309
+ *
3310
+ * Usage:
3311
+ * ```typescript
3312
+ * @Component({ ... })
3313
+ * export class UserListComponent extends BaseListPage<IUser> {
3314
+ * getResourceRoute(): string { return '/users'; }
3315
+ * getDeleteConfirmMessage(user: IUser): string { return `Delete "${user.name}"?`; }
3316
+ * async loadData(): Promise<void> { ... }
3317
+ * }
3318
+ * ```
3319
+ */
3320
+ class BaseListPage {
3321
+ router = inject(Router);
3322
+ messageService = inject(MessageService);
3323
+ appConfig = inject(APP_CONFIG);
3324
+ confirmationService = inject(ConfirmationService);
3325
+ destroyRef = inject(DestroyRef);
3326
+ /** Items list */
3327
+ items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
3328
+ /** Loading state */
3329
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
3330
+ /** Total records for pagination */
3331
+ total = signal(0, ...(ngDevMode ? [{ debugName: "total" }] : []));
3332
+ /** Page size */
3333
+ pageSize = signal(10, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
3334
+ /** First record index (for p-table lazy load) */
3335
+ first = signal(0, ...(ngDevMode ? [{ debugName: "first" }] : []));
3336
+ /** Current page (0-based for API, derived from first/pageSize) */
3337
+ currentPage = computed(() => Math.floor(this.first() / this.pageSize()), ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
3338
+ /** Show company info if company feature enabled */
3339
+ showCompanyInfo = computed(() => this.appConfig.enableCompanyFeature, ...(ngDevMode ? [{ debugName: "showCompanyInfo" }] : []));
3340
+ /**
3341
+ * Navigate to create page
3342
+ */
3343
+ onCreate() {
3344
+ this.router.navigate([this.getResourceRoute(), 'new']);
3345
+ }
3346
+ /**
3347
+ * Navigate to edit page
3348
+ * @param id The ID of the item to edit
3349
+ */
3350
+ onEdit(id) {
3351
+ this.router.navigate([this.getResourceRoute(), id]);
3352
+ }
3353
+ /**
3354
+ * Handle page change from p-table lazy load
3355
+ */
3356
+ onPageChange(event) {
3357
+ this.first.set(event.first ?? 0);
3358
+ this.pageSize.set(event.rows ?? 10);
3359
+ this.loadData();
3360
+ }
3361
+ /**
3362
+ * Get pagination params for API call
3363
+ * Returns 0-based page for backend API
3364
+ */
3365
+ getPaginationParams() {
3366
+ return {
3367
+ currentPage: this.currentPage(),
3368
+ pageSize: this.pageSize(),
3369
+ };
3370
+ }
3371
+ /**
3372
+ * Show success toast message
3373
+ */
3374
+ showSuccess(detail, summary = 'Success') {
3375
+ this.messageService.add({ severity: 'success', summary, detail });
3376
+ }
3377
+ /**
3378
+ * Show error toast message
3379
+ */
3380
+ showError(detail, summary = 'Error') {
3381
+ this.messageService.add({ severity: 'error', summary, detail });
3382
+ }
3383
+ /**
3384
+ * Show info toast message
3385
+ */
3386
+ showInfo(detail, summary = 'Info') {
3387
+ this.messageService.add({ severity: 'info', summary, detail });
3388
+ }
3389
+ /**
3390
+ * Show warning toast message
3391
+ */
3392
+ showWarn(detail, summary = 'Warning') {
3393
+ this.messageService.add({ severity: 'warn', summary, detail });
3394
+ }
3395
+ /**
3396
+ * Delete an item with confirmation dialog
3397
+ * @param item The item to delete
3398
+ * @param idGetter Function to extract ID from item
3399
+ * @param deleteApiCall Function that returns Observable for delete API call
3400
+ * @param options Optional configuration
3401
+ */
3402
+ onDelete(item, idGetter, deleteApiCall, options) {
3403
+ this.confirmationService.confirm({
3404
+ message: this.getDeleteConfirmMessage(item),
3405
+ header: options?.header ?? 'Confirm Delete',
3406
+ icon: 'pi pi-exclamation-triangle',
3407
+ acceptButtonStyleClass: 'p-button-danger',
3408
+ accept: () => {
3409
+ deleteApiCall(idGetter(item))
3410
+ .pipe(takeUntilDestroyed(this.destroyRef))
3411
+ .subscribe({
3412
+ next: () => {
3413
+ this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
3414
+ this.loadData();
3415
+ },
3416
+ error: () => {
3417
+ this.showError(options?.errorMessage ?? 'Failed to delete item.');
3418
+ },
3419
+ });
3420
+ },
3421
+ });
3422
+ }
3423
+ /**
3424
+ * Delete an item with confirmation dialog using async/await
3425
+ * @param item The item to delete
3426
+ * @param idGetter Function to extract ID from item
3427
+ * @param deleteApiCall Async function for delete API call
3428
+ * @param options Optional configuration
3429
+ */
3430
+ async onDeleteAsync(item, idGetter, deleteApiCall, options) {
3431
+ this.confirmationService.confirm({
3432
+ message: this.getDeleteConfirmMessage(item),
3433
+ header: options?.header ?? 'Confirm Delete',
3434
+ icon: 'pi pi-exclamation-triangle',
3435
+ acceptButtonStyleClass: 'p-button-danger',
3436
+ accept: async () => {
3437
+ try {
3438
+ await deleteApiCall(idGetter(item));
3439
+ this.showSuccess(options?.successMessage ?? 'Item deleted successfully.');
3440
+ await this.loadData();
3441
+ }
3442
+ catch {
3443
+ this.showError(options?.errorMessage ?? 'Failed to delete item.');
3444
+ }
3445
+ },
3446
+ });
3447
+ }
3448
+ /**
3449
+ * Navigate to a route
3450
+ */
3451
+ navigateTo(path) {
3452
+ this.router.navigate(path);
3453
+ }
3454
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3455
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: BaseListPage, isStandalone: true, ngImport: i0 });
3456
+ }
3457
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BaseListPage, decorators: [{
3458
+ type: Directive
3459
+ }] });
3460
+
3461
+ // Constants
2827
3462
 
2828
3463
  /**
2829
3464
  * Generated bundle index. Do not edit.
2830
3465
  */
2831
3466
 
2832
- export { AUTH_STATE_PROVIDER, AngularModule, ApiResourceService, ApiResourceService as ApiService, COMPANY_API_PROVIDER, ContactTypeEnum, CookieService, EditModeElementChangerDirective, FILE_TYPE_FILTERS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, USER_PERMISSION_PROVIDER, USER_PROVIDER, UserMultiSelectComponent, UserSelectComponent, allPermissionsGuard, anyPermissionGuard, evaluateLogicNode, evaluatePermission, formatFileSize, getAcceptString, getFileIconClass, hasAllPermissions, hasAnyPermission, isFileTypeAllowed, permissionGuard };
3467
+ export { ACTION_PERMISSIONS, AUTH_STATE_PROVIDER, AngularModule, ApiResourceService, ApiResourceService as ApiService, BRANCH_PERMISSIONS, BaseFormControl, BaseFormPage, BaseListPage, COMPANY_ACTION_PERMISSIONS, COMPANY_API_PROVIDER, COMPANY_PERMISSIONS, ContactTypeEnum, CookieService, EMAIL_CONFIG_PERMISSIONS, EMAIL_TEMPLATE_PERMISSIONS, EditModeElementChangerDirective, FILE_PERMISSIONS, FILE_TYPE_FILTERS, FOLDER_PERMISSIONS, FORM_PERMISSIONS, FileSelectorDialogComponent, FileUploaderComponent, FileUrlService, HasPermissionDirective, IconComponent, IconTypeEnum, IsEmptyImageDirective, LazyMultiSelectComponent, LazySelectComponent, PERMISSIONS, PROFILE_PERMISSION_PROVIDER, PROFILE_UPLOAD_PROVIDER, PermissionValidatorService, PlatformService, PreventDefaultDirective, PrimeModule, ROLE_ACTION_PERMISSIONS, ROLE_PERMISSIONS, STORAGE_CONFIG_PERMISSIONS, USER_ACTION_PERMISSIONS, USER_LIST_PROVIDER, USER_PERMISSIONS, USER_PERMISSION_PROVIDER, USER_PROVIDER, USER_ROLE_PERMISSIONS, UserMultiSelectComponent, UserSelectComponent, allPermissionsGuard, anyPermissionGuard, evaluateLogicNode, evaluatePermission, formatFileSize, getAcceptString, getFileIconClass, hasAllPermissions, hasAnyPermission, hasPermission, isFileTypeAllowed, permissionGuard, provideValueAccessor };
2833
3468
  //# sourceMappingURL=flusys-ng-shared.mjs.map