@dso-design-system/ui 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/esm2022/lib/alert/alert.component.mjs +54 -0
  2. package/esm2022/lib/badge/badge.component.mjs +22 -0
  3. package/esm2022/lib/breadcrumb/breadcrumb.component.mjs +29 -0
  4. package/esm2022/lib/checkbox/checkbox.component.mjs +82 -0
  5. package/esm2022/lib/datepicker/datepicker.component.mjs +161 -0
  6. package/esm2022/lib/dialog/dialog.component.mjs +25 -0
  7. package/esm2022/lib/directives/truncate.directive.mjs +71 -0
  8. package/esm2022/lib/dropdown-list/dropdown-list.component.mjs +89 -0
  9. package/esm2022/lib/file-upload-items/file-upload-items.component.mjs +65 -0
  10. package/esm2022/lib/file-upload-multiple/file-upload-multiple.component.mjs +232 -0
  11. package/esm2022/lib/file-upload-multiple/upload-item.model.mjs +2 -0
  12. package/esm2022/lib/file-upload-multiple/upload-simulator.service.mjs +76 -0
  13. package/esm2022/lib/file-upload-single/file-upload-single.component.mjs +100 -0
  14. package/esm2022/lib/input-text/input-text.component.mjs +93 -0
  15. package/esm2022/lib/pagination/pagination.component.mjs +115 -0
  16. package/esm2022/lib/progress-bar/progress-bar.component.mjs +25 -0
  17. package/esm2022/lib/radio/radio.component.mjs +41 -0
  18. package/esm2022/lib/select-dropdown/select-dropdown.component.mjs +228 -0
  19. package/esm2022/lib/service/toast.service.mjs +20 -0
  20. package/esm2022/lib/side-navigation-bar/side-navigation-bar.component.mjs +113 -0
  21. package/esm2022/lib/spinner/spinner.component.mjs +60 -0
  22. package/esm2022/lib/table/table.component.mjs +136 -0
  23. package/esm2022/lib/tabs/tab.component.mjs +20 -0
  24. package/esm2022/lib/tabs/tabs.component.mjs +40 -0
  25. package/esm2022/lib/tag/tag.component.mjs +27 -0
  26. package/esm2022/lib/text-area/text-area.component.mjs +74 -0
  27. package/esm2022/lib/toast/toast.component.mjs +36 -0
  28. package/esm2022/lib/tooltip/tooltip.component.mjs +38 -0
  29. package/esm2022/lib/tooltip/tooltip.directive.mjs +105 -0
  30. package/esm2022/lib/top-navigation-bar/top-navigation-bar.component.mjs +24 -0
  31. package/esm2022/public-api.mjs +27 -2
  32. package/fesm2022/dso-design-system-ui.mjs +2056 -3
  33. package/fesm2022/dso-design-system-ui.mjs.map +1 -1
  34. package/lib/alert/alert.component.d.ts +20 -0
  35. package/lib/badge/badge.component.d.ts +8 -0
  36. package/lib/breadcrumb/breadcrumb.component.d.ts +15 -0
  37. package/lib/checkbox/checkbox.component.d.ts +42 -0
  38. package/lib/datepicker/datepicker.component.d.ts +48 -0
  39. package/lib/dialog/dialog.component.d.ts +10 -0
  40. package/lib/directives/truncate.directive.d.ts +23 -0
  41. package/lib/dropdown-list/dropdown-list.component.d.ts +33 -0
  42. package/lib/file-upload-items/file-upload-items.component.d.ts +27 -0
  43. package/lib/file-upload-multiple/file-upload-multiple.component.d.ts +44 -0
  44. package/lib/file-upload-multiple/upload-item.model.d.ts +7 -0
  45. package/lib/file-upload-multiple/upload-simulator.service.d.ts +34 -0
  46. package/lib/file-upload-single/file-upload-single.component.d.ts +28 -0
  47. package/lib/input-text/input-text.component.d.ts +24 -0
  48. package/lib/pagination/pagination.component.d.ts +31 -0
  49. package/lib/progress-bar/progress-bar.component.d.ts +11 -0
  50. package/lib/radio/radio.component.d.ts +14 -0
  51. package/lib/select-dropdown/select-dropdown.component.d.ts +78 -0
  52. package/lib/service/toast.service.d.ts +16 -0
  53. package/lib/side-navigation-bar/side-navigation-bar.component.d.ts +74 -0
  54. package/lib/spinner/spinner.component.d.ts +23 -0
  55. package/lib/table/table.component.d.ts +43 -0
  56. package/lib/tabs/tab.component.d.ts +9 -0
  57. package/lib/tabs/tabs.component.d.ts +15 -0
  58. package/lib/tag/tag.component.d.ts +10 -0
  59. package/lib/text-area/text-area.component.d.ts +21 -0
  60. package/lib/toast/toast.component.d.ts +13 -0
  61. package/lib/tooltip/tooltip.component.d.ts +15 -0
  62. package/lib/tooltip/tooltip.directive.d.ts +19 -0
  63. package/lib/top-navigation-bar/top-navigation-bar.component.d.ts +16 -0
  64. package/package.json +13 -7
  65. package/public-api.d.ts +25 -0
@@ -1,7 +1,13 @@
1
1
  import * as i1 from '@angular/common';
2
- import { CommonModule } from '@angular/common';
2
+ import { CommonModule, DatePipe } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { Component, Input, EventEmitter, Output } from '@angular/core';
4
+ import { Component, Input, EventEmitter, Output, ViewChild, HostListener, Directive, Injectable, ContentChildren } from '@angular/core';
5
+ import * as i2$1 from '@angular/router';
6
+ import { RouterLink, RouterModule } from '@angular/router';
7
+ import * as i2 from '@angular/forms';
8
+ import { FormsModule } from '@angular/forms';
9
+ import lottie from 'lottie-web';
10
+ import { ReplaySubject } from 'rxjs';
5
11
 
6
12
  class IconComponent {
7
13
  /** Name of the icon to display (matches sprite ID) */
@@ -122,9 +128,2056 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
122
128
  type: Output
123
129
  }] } });
124
130
 
131
+ class AlertComponent {
132
+ type = 'info';
133
+ message = '';
134
+ showActions = false;
135
+ isDismissable = true;
136
+ actions = [];
137
+ isVisible = true;
138
+ isHiding = false;
139
+ close() {
140
+ this.isHiding = true;
141
+ // Wait for fadeOut animation to finish before removing the alert
142
+ setTimeout(() => {
143
+ this.isVisible = false;
144
+ }, 300); // match duration in CSS
145
+ }
146
+ // Pick the icon name based on alert type
147
+ get iconName() {
148
+ switch (this.type) {
149
+ case 'success': return 'icon-slanted-check';
150
+ case 'error': return 'icon-left-align';
151
+ case 'warning': return 'icon-van';
152
+ default: return 'icon-notification';
153
+ }
154
+ }
155
+ /** Handle when a button inside the alert is clicked */
156
+ handleAction(action) {
157
+ if (action.onClick) {
158
+ action.onClick();
159
+ }
160
+ }
161
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AlertComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
162
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: AlertComponent, isStandalone: true, selector: "dso-alert", inputs: { type: "type", message: "message", showActions: "showActions", isDismissable: "isDismissable", actions: "actions" }, ngImport: i0, template: "\r\n<div\r\n*ngIf=\"isVisible\"\r\nclass=\"alert\"\r\n[ngClass]=\"[type, isHiding ? 'hide' : '']\"> \r\n <div class=\"positioning\">\r\n <dso-icon [iconName]=\"iconName\" [size]=\"'medium'\"></dso-icon>\r\n </div>\r\n <span [innerHTML]=\"message\"></span>\r\n\r\n <div class=\"positioning\" *ngIf=\"actions && actions.length > 0 || isDismissable\">\r\n <div class=\"button-wrapper\">\r\n <dso-button\r\n *ngFor=\"let action of actions\"\r\n [btnLabel]=\"action.btnLabel\"\r\n [btnType]=\"action.btnType || 'contrast'\" \r\n [btnSize]=\"action.btnSize || 'mdBtn'\"\r\n [btnIconName]=\"action.btnIconName ?? null\"\r\n [isDisabled]=\"action.isDisabled || false\"\r\n (click)=\"handleAction(action)\">\r\n </dso-button>\r\n </div>\r\n <button (click)=\"close()\" *ngIf=\"isDismissable\" class=\"close-btn\">&times;</button>\r\n </div>\r\n \r\n</div>", styles: ["@charset \"UTF-8\";@keyframes fadeIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.alert{padding:12px 16px;border-radius:5px;gap:16px;display:flex;align-items:stretch;font-size:16px;animation:fadeIn .4s ease-in}.alert.hide{animation:fadeOut .4s ease-out forwards}.success{background-color:#77c98a;color:#fff}.error{background-color:#dc3545;color:#fff}.info{background-color:#17a2b8;color:#fff}.warning{background-color:#ffc107;color:#000}.close-btn{width:24px;height:24px;padding:0;margin:0;display:flex;align-items:center;justify-content:center;background:none;border:none;font-size:24px;line-height:1;color:inherit;cursor:pointer}.close-btn:hover{opacity:.7}.content-wrapper{display:flex;align-items:center;flex:1;gap:12px}.positioning{display:flex;align-items:flex-start;align-self:stretch}span{flex-grow:1;line-height:1.4;max-height:7em;overflow-y:auto}.button-wrapper{display:flex;flex-direction:column;gap:8px;padding-right:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
163
+ }
164
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AlertComponent, decorators: [{
165
+ type: Component,
166
+ args: [{ selector: 'dso-alert', standalone: true, imports: [CommonModule, ButtonComponent, IconComponent], template: "\r\n<div\r\n*ngIf=\"isVisible\"\r\nclass=\"alert\"\r\n[ngClass]=\"[type, isHiding ? 'hide' : '']\"> \r\n <div class=\"positioning\">\r\n <dso-icon [iconName]=\"iconName\" [size]=\"'medium'\"></dso-icon>\r\n </div>\r\n <span [innerHTML]=\"message\"></span>\r\n\r\n <div class=\"positioning\" *ngIf=\"actions && actions.length > 0 || isDismissable\">\r\n <div class=\"button-wrapper\">\r\n <dso-button\r\n *ngFor=\"let action of actions\"\r\n [btnLabel]=\"action.btnLabel\"\r\n [btnType]=\"action.btnType || 'contrast'\" \r\n [btnSize]=\"action.btnSize || 'mdBtn'\"\r\n [btnIconName]=\"action.btnIconName ?? null\"\r\n [isDisabled]=\"action.isDisabled || false\"\r\n (click)=\"handleAction(action)\">\r\n </dso-button>\r\n </div>\r\n <button (click)=\"close()\" *ngIf=\"isDismissable\" class=\"close-btn\">&times;</button>\r\n </div>\r\n \r\n</div>", styles: ["@charset \"UTF-8\";@keyframes fadeIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.alert{padding:12px 16px;border-radius:5px;gap:16px;display:flex;align-items:stretch;font-size:16px;animation:fadeIn .4s ease-in}.alert.hide{animation:fadeOut .4s ease-out forwards}.success{background-color:#77c98a;color:#fff}.error{background-color:#dc3545;color:#fff}.info{background-color:#17a2b8;color:#fff}.warning{background-color:#ffc107;color:#000}.close-btn{width:24px;height:24px;padding:0;margin:0;display:flex;align-items:center;justify-content:center;background:none;border:none;font-size:24px;line-height:1;color:inherit;cursor:pointer}.close-btn:hover{opacity:.7}.content-wrapper{display:flex;align-items:center;flex:1;gap:12px}.positioning{display:flex;align-items:flex-start;align-self:stretch}span{flex-grow:1;line-height:1.4;max-height:7em;overflow-y:auto}.button-wrapper{display:flex;flex-direction:column;gap:8px;padding-right:8px}\n"] }]
167
+ }], propDecorators: { type: [{
168
+ type: Input
169
+ }], message: [{
170
+ type: Input
171
+ }], showActions: [{
172
+ type: Input
173
+ }], isDismissable: [{
174
+ type: Input
175
+ }], actions: [{
176
+ type: Input
177
+ }] } });
178
+
179
+ class BadgeComponent {
180
+ label = null;
181
+ color = 'primary';
182
+ get isDot() {
183
+ return this.label === null || this.label === '' || this.label === undefined;
184
+ }
185
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
186
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: BadgeComponent, isStandalone: true, selector: "dso-badge", inputs: { label: "label", color: "color" }, ngImport: i0, template: "<span class=\"badge\" [ngClass]=\"[color, isDot ? 'dot' : '']\">\r\n <ng-container *ngIf=\"!isDot\">{{ label }}</ng-container>\r\n</span>", styles: [".badge{display:inline-flex;align-items:center;justify-content:center;padding:.25rem .5rem;border-radius:1rem;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#fff;line-height:1;min-width:1.25rem}.primary{background-color:#007bff}.success{background-color:#28a745}.warning{background-color:#ffc107;color:#000}.danger{background-color:#dc3545}.badge.dot{padding:0;width:.5rem;height:.5rem;min-width:0;border-radius:50%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
187
+ }
188
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BadgeComponent, decorators: [{
189
+ type: Component,
190
+ args: [{ selector: 'dso-badge', standalone: true, imports: [CommonModule], template: "<span class=\"badge\" [ngClass]=\"[color, isDot ? 'dot' : '']\">\r\n <ng-container *ngIf=\"!isDot\">{{ label }}</ng-container>\r\n</span>", styles: [".badge{display:inline-flex;align-items:center;justify-content:center;padding:.25rem .5rem;border-radius:1rem;font-size:.75rem;font-weight:600;text-transform:uppercase;color:#fff;line-height:1;min-width:1.25rem}.primary{background-color:#007bff}.success{background-color:#28a745}.warning{background-color:#ffc107;color:#000}.danger{background-color:#dc3545}.badge.dot{padding:0;width:.5rem;height:.5rem;min-width:0;border-radius:50%}\n"] }]
191
+ }], propDecorators: { label: [{
192
+ type: Input
193
+ }], color: [{
194
+ type: Input
195
+ }] } });
196
+
197
+ class BreadcrumbComponent {
198
+ items = [];
199
+ itemSelected = new EventEmitter();
200
+ // Default separator → you can allow this to be set via @Input() later
201
+ separator = '/';
202
+ onItemClicked(item) {
203
+ this.itemSelected.emit(item);
204
+ console.log('breadcrumb emitted -> ', item);
205
+ }
206
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BreadcrumbComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
207
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: BreadcrumbComponent, isStandalone: true, selector: "dso-breadcrumb", inputs: { items: "items", separator: "separator" }, outputs: { itemSelected: "itemSelected" }, ngImport: i0, template: "<nav aria-label=\"Breadcrumb\" class=\"breadcrumb\">\r\n <ol>\r\n <li *ngFor=\"let item of items; let last = last\" class=\"crumb\">\r\n\r\n <!-- Normal breadcrumb link -->\r\n <ng-container *ngIf=\"!last && item.url; else lastItem\">\r\n\r\n <dso-button\r\n btnType=\"text\"\r\n btnSize=\"smBtn\"\r\n [btnLabel]=\"item.label\"\r\n [isDisabled]=\"false\"\r\n (onClick)=\"onItemClicked(item)\"\r\n [routerLink]=\"item.url\">\r\n </dso-button>\r\n\r\n </ng-container>\r\n\r\n <!-- Last breadcrumb item (non-clickable) -->\r\n <ng-template #lastItem>\r\n <span class=\"current\" aria-current=\"page\">\r\n {{ item.label }}\r\n </span>\r\n </ng-template>\r\n\r\n <!-- Separator -->\r\n <span class=\"separator\" *ngIf=\"!last\">{{ separator }}</span>\r\n\r\n </li>\r\n </ol>\r\n <!-- <dso-button btnType=\"text\" btnLabel=\"Go Home\" [routerLink]=\"['/page1']\"></dso-button> -->\r\n\r\n</nav>\r\n\r\n\r\n", styles: [".breadcrumb{font-size:14px;color:var(--ui-text-muted, #666)}.breadcrumb ol{list-style:none;padding:0;margin:0;display:flex;align-items:center;flex-wrap:wrap}.breadcrumb .crumb{display:flex;align-items:center}.breadcrumb .crumb a{color:var(--ui-primary, #4a5cff);text-decoration:none;font-weight:500}.breadcrumb .crumb a:hover{text-decoration:underline}.breadcrumb .crumb .current{color:var(--ui-text, #222);font-weight:600;padding:0 8px}.breadcrumb .crumb .separator{margin:0 8px;color:var(--ui-text-muted, #999);font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }] });
208
+ }
209
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: BreadcrumbComponent, decorators: [{
210
+ type: Component,
211
+ args: [{ selector: 'dso-breadcrumb', standalone: true, imports: [CommonModule, RouterLink, ButtonComponent], template: "<nav aria-label=\"Breadcrumb\" class=\"breadcrumb\">\r\n <ol>\r\n <li *ngFor=\"let item of items; let last = last\" class=\"crumb\">\r\n\r\n <!-- Normal breadcrumb link -->\r\n <ng-container *ngIf=\"!last && item.url; else lastItem\">\r\n\r\n <dso-button\r\n btnType=\"text\"\r\n btnSize=\"smBtn\"\r\n [btnLabel]=\"item.label\"\r\n [isDisabled]=\"false\"\r\n (onClick)=\"onItemClicked(item)\"\r\n [routerLink]=\"item.url\">\r\n </dso-button>\r\n\r\n </ng-container>\r\n\r\n <!-- Last breadcrumb item (non-clickable) -->\r\n <ng-template #lastItem>\r\n <span class=\"current\" aria-current=\"page\">\r\n {{ item.label }}\r\n </span>\r\n </ng-template>\r\n\r\n <!-- Separator -->\r\n <span class=\"separator\" *ngIf=\"!last\">{{ separator }}</span>\r\n\r\n </li>\r\n </ol>\r\n <!-- <dso-button btnType=\"text\" btnLabel=\"Go Home\" [routerLink]=\"['/page1']\"></dso-button> -->\r\n\r\n</nav>\r\n\r\n\r\n", styles: [".breadcrumb{font-size:14px;color:var(--ui-text-muted, #666)}.breadcrumb ol{list-style:none;padding:0;margin:0;display:flex;align-items:center;flex-wrap:wrap}.breadcrumb .crumb{display:flex;align-items:center}.breadcrumb .crumb a{color:var(--ui-primary, #4a5cff);text-decoration:none;font-weight:500}.breadcrumb .crumb a:hover{text-decoration:underline}.breadcrumb .crumb .current{color:var(--ui-text, #222);font-weight:600;padding:0 8px}.breadcrumb .crumb .separator{margin:0 8px;color:var(--ui-text-muted, #999);font-size:12px}\n"] }]
212
+ }], propDecorators: { items: [{
213
+ type: Input
214
+ }], itemSelected: [{
215
+ type: Output
216
+ }], separator: [{
217
+ type: Input
218
+ }] } });
219
+
220
+ /**
221
+ * Reusable Checkbox Component
222
+ *
223
+ * Features:
224
+ * - Optional label
225
+ * - Configurable size, error state, and disabled state
226
+ * - Emits changes to parent component
227
+ * - Supports custom icons
228
+ */
229
+ class CheckboxComponent {
230
+ /** Text label displayed next to the checkbox */
231
+ label;
232
+ /** Whether the checkbox is checked */
233
+ isChecked = true;
234
+ /** Whether the checkbox is disabled */
235
+ disabled = false;
236
+ /** Size of the checkbox: small, medium, or large */
237
+ size = 'medium';
238
+ /** Error state for styling purposes */
239
+ error = false;
240
+ /** Whether the checkbox is required */
241
+ required = false;
242
+ /** Error message to display when error is true */
243
+ errorMessage = '';
244
+ /** Icon name for the checkbox (default is a checkmark) */
245
+ iconName = 'icon-slanted-check';
246
+ /** Emits the updated checked state whenever the checkbox is toggled */
247
+ change = new EventEmitter();
248
+ /**
249
+ * Toggles the checked state of the checkbox
250
+ * and emits the new value to the parent component
251
+ */
252
+ toggleCheckbox() {
253
+ if (!this.disabled) {
254
+ this.isChecked = !this.isChecked;
255
+ this.change.emit(this.isChecked);
256
+ }
257
+ }
258
+ /**
259
+ * Maps the checkbox size to icon size
260
+ */
261
+ getIconSize() {
262
+ switch (this.size) {
263
+ case 'large': return 'large';
264
+ case 'medium': return 'medium';
265
+ case 'small': return 'small';
266
+ default: return 'medium'; // fallback to medium
267
+ }
268
+ }
269
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CheckboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
270
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: CheckboxComponent, isStandalone: true, selector: "dso-checkbox", inputs: { label: "label", isChecked: "isChecked", disabled: "disabled", size: "size", error: "error", required: "required", errorMessage: "errorMessage", iconName: "iconName" }, outputs: { change: "change" }, ngImport: i0, template: "<!--\r\n Checkbox Component Template\r\n --------------------------\r\n Features:\r\n - Clickable wrapper to toggle checkbox (disabled-safe)\r\n - Custom checkbox with icon\r\n - Label support\r\n - Error state styling\r\n - Size-based styling: small, medium, large\r\n-->\r\n\r\n<div \r\n class=\"checkbox-wrapper\"\r\n (click)=\"!disabled && toggleCheckbox()\"\r\n [ngClass]=\"{\r\n 'small': size === 'small', \r\n 'medium': size === 'medium', \r\n 'large': size === 'large',\r\n 'disabled': disabled\r\n }\">\r\n\r\n <!-- Hidden native checkbox for accessibility and form integration -->\r\n <input\r\n class=\"checkbox-input\"\r\n type=\"checkbox\"\r\n [ngModel]=\"isChecked\"\r\n [disabled]=\"disabled\"/>\r\n\r\n <!-- Custom checkbox UI -->\r\n <span \r\n class=\"custom-checkbox\" \r\n [class.checked]=\"isChecked\" \r\n [class.error]=\"error\">\r\n \r\n <!-- Display check icon only when checked -->\r\n <dso-icon \r\n *ngIf=\"isChecked\" \r\n [iconName]=\"iconName\" \r\n [size]=\"getIconSize()\">\r\n </dso-icon>\r\n\r\n </span> \r\n\r\n <!-- Optional label -->\r\n <span class=\"checkbox-label\">\r\n {{ label }}\r\n </span>\r\n\r\n</div>\r\n", styles: [".checkbox-wrapper{display:inline-flex;align-items:center;gap:8px;cursor:pointer;vertical-align:middle}.checkbox-wrapper.error .custom-checkbox{border-color:red}.checkbox-wrapper.disabled{cursor:not-allowed}.checkbox-wrapper.disabled .custom-checkbox{pointer-events:none;background-color:#f0f0f0;border-color:#d0d0d0}.checkbox-wrapper.disabled .custom-checkbox.checked{background-color:#00f;border-color:#00f;opacity:.3}.checkbox-input{width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-checkbox{display:inline-flex;justify-content:center;border:1px solid #ccc;border-radius:4px;cursor:pointer;width:24px;height:24px}.custom-checkbox.checked{border-color:#00f;background-color:#00f;color:#fff}.checkbox-wrapper:not(.disabled):hover .custom-checkbox{background-color:#eeeff9;border-color:#0000ffc0;cursor:pointer}.checkbox-wrapper:not(.disabled):hover .custom-checkbox.checked{background-color:#0000ffa9;border-color:#0000ffc0}.custom-checkbox.small .checkbox-custom{width:12px;height:12px}.custom-checkbox.medium .checkbox-custom{width:16px;height:16px}.custom-checkbox.large .checkbox-custom{width:24px;height:24px}.checkbox-label{font-size:14px;font-weight:400}.error-message{color:red;font-size:12px;margin-top:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
271
+ }
272
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: CheckboxComponent, decorators: [{
273
+ type: Component,
274
+ args: [{ selector: 'dso-checkbox', standalone: true, imports: [CommonModule, IconComponent, FormsModule], template: "<!--\r\n Checkbox Component Template\r\n --------------------------\r\n Features:\r\n - Clickable wrapper to toggle checkbox (disabled-safe)\r\n - Custom checkbox with icon\r\n - Label support\r\n - Error state styling\r\n - Size-based styling: small, medium, large\r\n-->\r\n\r\n<div \r\n class=\"checkbox-wrapper\"\r\n (click)=\"!disabled && toggleCheckbox()\"\r\n [ngClass]=\"{\r\n 'small': size === 'small', \r\n 'medium': size === 'medium', \r\n 'large': size === 'large',\r\n 'disabled': disabled\r\n }\">\r\n\r\n <!-- Hidden native checkbox for accessibility and form integration -->\r\n <input\r\n class=\"checkbox-input\"\r\n type=\"checkbox\"\r\n [ngModel]=\"isChecked\"\r\n [disabled]=\"disabled\"/>\r\n\r\n <!-- Custom checkbox UI -->\r\n <span \r\n class=\"custom-checkbox\" \r\n [class.checked]=\"isChecked\" \r\n [class.error]=\"error\">\r\n \r\n <!-- Display check icon only when checked -->\r\n <dso-icon \r\n *ngIf=\"isChecked\" \r\n [iconName]=\"iconName\" \r\n [size]=\"getIconSize()\">\r\n </dso-icon>\r\n\r\n </span> \r\n\r\n <!-- Optional label -->\r\n <span class=\"checkbox-label\">\r\n {{ label }}\r\n </span>\r\n\r\n</div>\r\n", styles: [".checkbox-wrapper{display:inline-flex;align-items:center;gap:8px;cursor:pointer;vertical-align:middle}.checkbox-wrapper.error .custom-checkbox{border-color:red}.checkbox-wrapper.disabled{cursor:not-allowed}.checkbox-wrapper.disabled .custom-checkbox{pointer-events:none;background-color:#f0f0f0;border-color:#d0d0d0}.checkbox-wrapper.disabled .custom-checkbox.checked{background-color:#00f;border-color:#00f;opacity:.3}.checkbox-input{width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-checkbox{display:inline-flex;justify-content:center;border:1px solid #ccc;border-radius:4px;cursor:pointer;width:24px;height:24px}.custom-checkbox.checked{border-color:#00f;background-color:#00f;color:#fff}.checkbox-wrapper:not(.disabled):hover .custom-checkbox{background-color:#eeeff9;border-color:#0000ffc0;cursor:pointer}.checkbox-wrapper:not(.disabled):hover .custom-checkbox.checked{background-color:#0000ffa9;border-color:#0000ffc0}.custom-checkbox.small .checkbox-custom{width:12px;height:12px}.custom-checkbox.medium .checkbox-custom{width:16px;height:16px}.custom-checkbox.large .checkbox-custom{width:24px;height:24px}.checkbox-label{font-size:14px;font-weight:400}.error-message{color:red;font-size:12px;margin-top:4px}\n"] }]
275
+ }], propDecorators: { label: [{
276
+ type: Input
277
+ }], isChecked: [{
278
+ type: Input
279
+ }], disabled: [{
280
+ type: Input
281
+ }], size: [{
282
+ type: Input
283
+ }], error: [{
284
+ type: Input
285
+ }], required: [{
286
+ type: Input
287
+ }], errorMessage: [{
288
+ type: Input
289
+ }], iconName: [{
290
+ type: Input
291
+ }], change: [{
292
+ type: Output
293
+ }] } });
294
+
295
+ class DatepickerComponent {
296
+ /** Label displayed above the input */
297
+ inputTextLabel = 'Datepicker Title';
298
+ /** Disable the datepicker input */
299
+ isDisabled = false;
300
+ /** Currently selected date */
301
+ selectedDate = null;
302
+ /** Whether the calendar popup is visible */
303
+ calendarVisible = false;
304
+ /** Current month displayed in the calendar */
305
+ currentMonth = new Date();
306
+ /** View mode: day, month, or year */
307
+ viewMode = 'day';
308
+ /** Minimum and maximum year range */
309
+ minYear = 1990;
310
+ maxYear = 2099;
311
+ /** Month names for display */
312
+ months = [
313
+ 'January', 'February', 'March', 'April', 'May', 'June',
314
+ 'July', 'August', 'September', 'October', 'November', 'December'
315
+ ];
316
+ yearListRef;
317
+ calendarWrapper;
318
+ // --------------------------
319
+ // Calendar Visibility & Toggle
320
+ // --------------------------
321
+ /** Toggle the calendar popup */
322
+ toggleCalendar() {
323
+ this.calendarVisible = !this.calendarVisible;
324
+ this.viewMode = 'day';
325
+ }
326
+ /** Close calendar if clicking outside */
327
+ handleClickOutside(event) {
328
+ if (!this.calendarWrapper)
329
+ return;
330
+ const target = event.target;
331
+ if (this.calendarVisible && !this.calendarWrapper.nativeElement.contains(target)) {
332
+ this.calendarVisible = false;
333
+ }
334
+ }
335
+ // --------------------------
336
+ // View Switching
337
+ // --------------------------
338
+ switchToMonthView() {
339
+ this.viewMode = 'month';
340
+ }
341
+ switchToYearView() {
342
+ this.viewMode = 'year';
343
+ setTimeout(() => this.scrollToCurrentYear(), 50);
344
+ }
345
+ // --------------------------
346
+ // Navigation (Month / Year)
347
+ // --------------------------
348
+ changeMonth(direction) {
349
+ this.currentMonth = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth() + direction, 1);
350
+ }
351
+ changeYear(direction) {
352
+ const newYear = this.currentMonth.getFullYear() + direction;
353
+ if (newYear >= this.minYear && newYear <= this.maxYear) {
354
+ this.currentMonth = new Date(newYear, this.currentMonth.getMonth(), 1);
355
+ setTimeout(() => this.scrollToCurrentYear(), 0);
356
+ }
357
+ }
358
+ // --------------------------
359
+ // Helpers
360
+ // --------------------------
361
+ /** Returns array of days in the current month, with leading nulls for empty grid cells */
362
+ getDaysInMonth() {
363
+ const month = this.currentMonth.getMonth();
364
+ const year = this.currentMonth.getFullYear();
365
+ const firstDay = new Date(year, month, 1).getDay();
366
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
367
+ const daysArray = Array.from({ length: daysInMonth }, (_, i) => i + 1);
368
+ return Array(firstDay).fill(null).concat(daysArray);
369
+ }
370
+ /** Returns array of years in the defined range */
371
+ getYearsRange() {
372
+ const years = [];
373
+ for (let y = this.minYear; y <= this.maxYear; y++)
374
+ years.push(y);
375
+ return years;
376
+ }
377
+ /** Scroll the year list to show the current year */
378
+ scrollToCurrentYear() {
379
+ if (!this.yearListRef)
380
+ return;
381
+ const container = this.yearListRef.nativeElement;
382
+ const currentYear = this.currentMonth.getFullYear();
383
+ const yearIndex = currentYear - this.minYear;
384
+ const columns = 4; // 4 columns grid layout
385
+ const rowIndex = Math.floor(yearIndex / columns);
386
+ const yearCell = container.querySelector('.year-cell');
387
+ if (!yearCell)
388
+ return;
389
+ const scrollTop = rowIndex * yearCell.offsetHeight;
390
+ container.scrollTop = scrollTop;
391
+ }
392
+ // --------------------------
393
+ // Selection Handlers
394
+ // --------------------------
395
+ selectDate(day) {
396
+ if (day === null)
397
+ return;
398
+ this.selectedDate = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth(), day);
399
+ this.calendarVisible = false;
400
+ }
401
+ selectMonth(monthIndex) {
402
+ this.currentMonth = new Date(this.currentMonth.getFullYear(), monthIndex, 1);
403
+ this.viewMode = 'day';
404
+ }
405
+ selectYear(year) {
406
+ this.currentMonth = new Date(year, this.currentMonth.getMonth(), 1);
407
+ this.viewMode = 'month';
408
+ }
409
+ /** Check if a day is currently selected */
410
+ isSelected(day) {
411
+ if (!this.selectedDate || day === null)
412
+ return false;
413
+ return (this.selectedDate.getDate() === day &&
414
+ this.selectedDate.getMonth() === this.currentMonth.getMonth() &&
415
+ this.selectedDate.getFullYear() === this.currentMonth.getFullYear());
416
+ }
417
+ // --------------------------
418
+ // Footer Actions
419
+ // --------------------------
420
+ /** Set today’s date */
421
+ setToday() {
422
+ const today = new Date();
423
+ this.selectedDate = today;
424
+ this.currentMonth = new Date(today.getFullYear(), today.getMonth(), 1);
425
+ this.calendarVisible = false;
426
+ }
427
+ /** Clear the selected date */
428
+ clearDate() {
429
+ this.selectedDate = null;
430
+ }
431
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatepickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
432
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DatepickerComponent, isStandalone: true, selector: "dso-datepicker", inputs: { inputTextLabel: "inputTextLabel", isDisabled: "isDisabled" }, host: { listeners: { "document:click": "handleClickOutside($event)" } }, viewQueries: [{ propertyName: "yearListRef", first: true, predicate: ["yearList"], descendants: true }, { propertyName: "calendarWrapper", first: true, predicate: ["calendarWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"datepicker-wrapper\" #calendarWrapper>\r\n \r\n <!-- Label above the input -->\r\n <div *ngIf=\"inputTextLabel\" class=\"datepicker-label\">{{ inputTextLabel }}</div>\r\n\r\n <!-- Input field (readonly) -->\r\n <input\r\n type=\"text\"\r\n class=\"datepicker-input\"\r\n [value]=\"selectedDate ? (selectedDate | date:'MMM d, y') : ''\"\r\n placeholder=\"Select a date\"\r\n readonly\r\n (click)=\"toggleCalendar()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n />\r\n\r\n <!-- Calendar popup -->\r\n <div class=\"calendar\" *ngIf=\"calendarVisible\">\r\n\r\n <!-- Header -->\r\n <div class=\"calendar-header\">\r\n <div class=\"header-row\">\r\n\r\n <!-- Year & Month Navigation Buttons -->\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(-1)\" class=\"nav-btn\">\u00AB</button>\r\n <button *ngIf=\"viewMode === 'day'\" \r\n (click)=\"changeMonth(-1)\" class=\"nav-btn\">\u2039</button>\r\n\r\n <!-- Calendar Title (Month / Year display) -->\r\n <span class=\"calendar-title\">\r\n\r\n <!-- Day view: show Month + Year -->\r\n <ng-container *ngIf=\"viewMode === 'day'\">\r\n <span class=\"month-label\" (click)=\"switchToMonthView()\">\r\n {{ currentMonth | date:'MMMM' }}\r\n </span>\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Month view: show Year -->\r\n <ng-container *ngIf=\"viewMode === 'month'\">\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Year view: show Year only -->\r\n <ng-container *ngIf=\"viewMode === 'year'\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </ng-container>\r\n\r\n </span>\r\n\r\n <!-- Forward navigation buttons -->\r\n <button *ngIf=\"viewMode === 'day'\" (click)=\"changeMonth(1)\" class=\"nav-btn\">\u203A</button>\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(1)\" class=\"nav-btn\">\u00BB</button>\r\n\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n DAY VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'day'\">\r\n\r\n <!-- Weekday Labels -->\r\n <div class=\"calendar-weekdays\">\r\n <div *ngFor=\"let day of ['S','M','T','W','T','F','S']\">{{ day }}</div>\r\n </div>\r\n\r\n <!-- Days Grid -->\r\n <div class=\"calendar-grid days-grid\">\r\n <div\r\n *ngFor=\"let day of getDaysInMonth()\"\r\n [class.empty]=\"day === null\"\r\n [class.selected]=\"isSelected(day)\"\r\n (click)=\"selectDate(day)\"\r\n >\r\n {{ day }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n MONTH VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'month'\" class=\"calendar-grid months-grid\">\r\n <div *ngFor=\"let m of months; let i = index\" \r\n (click)=\"selectMonth(i)\" \r\n class=\"month-cell\">\r\n {{ m }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n YEAR VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'year'\" \r\n class=\"calendar-grid years-grid scrollable\" \r\n #yearList>\r\n <div\r\n *ngFor=\"let y of getYearsRange()\"\r\n (click)=\"selectYear(y)\"\r\n class=\"year-cell\"\r\n [class.current-year]=\"y === currentMonth.getFullYear()\"\r\n >\r\n {{ y }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n Footer Buttons\r\n ---------------------- -->\r\n <div class=\"calendar-footer\">\r\n <button class=\"footer-btn clear-btn\" (click)=\"clearDate()\">Clear</button>\r\n <button class=\"footer-btn today-btn\" (click)=\"setToday()\">Today</button>\r\n </div>\r\n\r\n </div>\r\n</div>\r\n", styles: [".datepicker-wrapper{position:relative;display:flex;flex-direction:column;gap:8px}.datepicker-input{padding:10px;font-size:15px;width:190px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background-color:#fff}.datepicker-input:hover{border:1px solid #071d35}.datepicker-input.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.datepicker-input.disabled .selected-option{pointer-events:none;color:#aaa}.calendar{position:absolute;top:0;left:100px;width:280px;background:#fff;border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 10px #0000001a;padding:10px;z-index:100;animation:fadeIn .2s ease-in-out}.calendar-header{margin-bottom:8px}.header-row{display:flex;justify-content:center;align-items:center;gap:4px}.nav-btn{background:none;border:none;font-size:16px;cursor:pointer;padding:4px 6px;color:#333;border-radius:4px;transition:background-color .2s}.nav-btn:hover{background-color:#f0f0f0}.calendar-title{font-weight:600;font-size:16px;min-width:90px;text-align:center;display:flex;flex:1;gap:4px;align-items:center;justify-content:center}.month-label,.year-label{cursor:pointer;transition:color .2s}.month-label:hover,.year-label:hover{color:#007bff}.calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;margin-bottom:4px;color:#666}.calendar-grid{display:grid;gap:4px;text-align:center;min-height:200px}.days-grid{grid-template-columns:repeat(7,1fr)}.months-grid{grid-template-columns:repeat(3,1fr)}.years-grid{grid-template-columns:repeat(4,1fr)}.scrollable{max-height:160px;overflow-y:auto;padding-right:5px}.scrollable::-webkit-scrollbar{width:6px}.scrollable::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px}.scrollable::-webkit-scrollbar-thumb:hover{background-color:#999}.calendar-grid div{padding:8px 0;border-radius:4px;cursor:pointer;transition:background-color .2s}.calendar-grid div:hover{background-color:#f0f0f0}.selected{background-color:#007bff;color:#fff!important}.current-year{background-color:#007bff1a;font-weight:700}.empty{visibility:hidden}.calendar-footer{display:flex;justify-content:space-between;margin-top:8px;border-top:1px solid #eee;padding-top:8px}.footer-btn{flex:1;padding:6px 0;font-size:14px;border-radius:4px;cursor:pointer;transition:background .2s;border:none}.clear-btn{background-color:#f8f9fa;color:#333;margin-right:5px}.clear-btn:hover{background-color:#e9ecef}.today-btn{background-color:#007bff;color:#fff}.today-btn:hover{background-color:#0069d9}@keyframes fadeIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1.DatePipe, name: "date" }] });
433
+ }
434
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DatepickerComponent, decorators: [{
435
+ type: Component,
436
+ args: [{ selector: 'dso-datepicker', standalone: true, imports: [CommonModule, DatePipe], template: "<div class=\"datepicker-wrapper\" #calendarWrapper>\r\n \r\n <!-- Label above the input -->\r\n <div *ngIf=\"inputTextLabel\" class=\"datepicker-label\">{{ inputTextLabel }}</div>\r\n\r\n <!-- Input field (readonly) -->\r\n <input\r\n type=\"text\"\r\n class=\"datepicker-input\"\r\n [value]=\"selectedDate ? (selectedDate | date:'MMM d, y') : ''\"\r\n placeholder=\"Select a date\"\r\n readonly\r\n (click)=\"toggleCalendar()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n />\r\n\r\n <!-- Calendar popup -->\r\n <div class=\"calendar\" *ngIf=\"calendarVisible\">\r\n\r\n <!-- Header -->\r\n <div class=\"calendar-header\">\r\n <div class=\"header-row\">\r\n\r\n <!-- Year & Month Navigation Buttons -->\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(-1)\" class=\"nav-btn\">\u00AB</button>\r\n <button *ngIf=\"viewMode === 'day'\" \r\n (click)=\"changeMonth(-1)\" class=\"nav-btn\">\u2039</button>\r\n\r\n <!-- Calendar Title (Month / Year display) -->\r\n <span class=\"calendar-title\">\r\n\r\n <!-- Day view: show Month + Year -->\r\n <ng-container *ngIf=\"viewMode === 'day'\">\r\n <span class=\"month-label\" (click)=\"switchToMonthView()\">\r\n {{ currentMonth | date:'MMMM' }}\r\n </span>\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Month view: show Year -->\r\n <ng-container *ngIf=\"viewMode === 'month'\">\r\n <span class=\"year-label\" (click)=\"switchToYearView()\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </span>\r\n </ng-container>\r\n\r\n <!-- Year view: show Year only -->\r\n <ng-container *ngIf=\"viewMode === 'year'\">\r\n {{ currentMonth | date:'yyyy' }}\r\n </ng-container>\r\n\r\n </span>\r\n\r\n <!-- Forward navigation buttons -->\r\n <button *ngIf=\"viewMode === 'day'\" (click)=\"changeMonth(1)\" class=\"nav-btn\">\u203A</button>\r\n <button *ngIf=\"viewMode === 'day' || viewMode === 'month'\" \r\n (click)=\"changeYear(1)\" class=\"nav-btn\">\u00BB</button>\r\n\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n DAY VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'day'\">\r\n\r\n <!-- Weekday Labels -->\r\n <div class=\"calendar-weekdays\">\r\n <div *ngFor=\"let day of ['S','M','T','W','T','F','S']\">{{ day }}</div>\r\n </div>\r\n\r\n <!-- Days Grid -->\r\n <div class=\"calendar-grid days-grid\">\r\n <div\r\n *ngFor=\"let day of getDaysInMonth()\"\r\n [class.empty]=\"day === null\"\r\n [class.selected]=\"isSelected(day)\"\r\n (click)=\"selectDate(day)\"\r\n >\r\n {{ day }}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n MONTH VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'month'\" class=\"calendar-grid months-grid\">\r\n <div *ngFor=\"let m of months; let i = index\" \r\n (click)=\"selectMonth(i)\" \r\n class=\"month-cell\">\r\n {{ m }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n YEAR VIEW\r\n ---------------------- -->\r\n <div *ngIf=\"viewMode === 'year'\" \r\n class=\"calendar-grid years-grid scrollable\" \r\n #yearList>\r\n <div\r\n *ngFor=\"let y of getYearsRange()\"\r\n (click)=\"selectYear(y)\"\r\n class=\"year-cell\"\r\n [class.current-year]=\"y === currentMonth.getFullYear()\"\r\n >\r\n {{ y }}\r\n </div>\r\n </div>\r\n\r\n <!-- ----------------------\r\n Footer Buttons\r\n ---------------------- -->\r\n <div class=\"calendar-footer\">\r\n <button class=\"footer-btn clear-btn\" (click)=\"clearDate()\">Clear</button>\r\n <button class=\"footer-btn today-btn\" (click)=\"setToday()\">Today</button>\r\n </div>\r\n\r\n </div>\r\n</div>\r\n", styles: [".datepicker-wrapper{position:relative;display:flex;flex-direction:column;gap:8px}.datepicker-input{padding:10px;font-size:15px;width:190px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background-color:#fff}.datepicker-input:hover{border:1px solid #071d35}.datepicker-input.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.datepicker-input.disabled .selected-option{pointer-events:none;color:#aaa}.calendar{position:absolute;top:0;left:100px;width:280px;background:#fff;border:1px solid #ddd;border-radius:8px;box-shadow:0 4px 10px #0000001a;padding:10px;z-index:100;animation:fadeIn .2s ease-in-out}.calendar-header{margin-bottom:8px}.header-row{display:flex;justify-content:center;align-items:center;gap:4px}.nav-btn{background:none;border:none;font-size:16px;cursor:pointer;padding:4px 6px;color:#333;border-radius:4px;transition:background-color .2s}.nav-btn:hover{background-color:#f0f0f0}.calendar-title{font-weight:600;font-size:16px;min-width:90px;text-align:center;display:flex;flex:1;gap:4px;align-items:center;justify-content:center}.month-label,.year-label{cursor:pointer;transition:color .2s}.month-label:hover,.year-label:hover{color:#007bff}.calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;margin-bottom:4px;color:#666}.calendar-grid{display:grid;gap:4px;text-align:center;min-height:200px}.days-grid{grid-template-columns:repeat(7,1fr)}.months-grid{grid-template-columns:repeat(3,1fr)}.years-grid{grid-template-columns:repeat(4,1fr)}.scrollable{max-height:160px;overflow-y:auto;padding-right:5px}.scrollable::-webkit-scrollbar{width:6px}.scrollable::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px}.scrollable::-webkit-scrollbar-thumb:hover{background-color:#999}.calendar-grid div{padding:8px 0;border-radius:4px;cursor:pointer;transition:background-color .2s}.calendar-grid div:hover{background-color:#f0f0f0}.selected{background-color:#007bff;color:#fff!important}.current-year{background-color:#007bff1a;font-weight:700}.empty{visibility:hidden}.calendar-footer{display:flex;justify-content:space-between;margin-top:8px;border-top:1px solid #eee;padding-top:8px}.footer-btn{flex:1;padding:6px 0;font-size:14px;border-radius:4px;cursor:pointer;transition:background .2s;border:none}.clear-btn{background-color:#f8f9fa;color:#333;margin-right:5px}.clear-btn:hover{background-color:#e9ecef}.today-btn{background-color:#007bff;color:#fff}.today-btn:hover{background-color:#0069d9}@keyframes fadeIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}\n"] }]
437
+ }], propDecorators: { inputTextLabel: [{
438
+ type: Input
439
+ }], isDisabled: [{
440
+ type: Input
441
+ }], yearListRef: [{
442
+ type: ViewChild,
443
+ args: ['yearList']
444
+ }], calendarWrapper: [{
445
+ type: ViewChild,
446
+ args: ['calendarWrapper', { static: false }]
447
+ }], handleClickOutside: [{
448
+ type: HostListener,
449
+ args: ['document:click', ['$event']]
450
+ }] } });
451
+
452
+ class DialogComponent {
453
+ visible = false;
454
+ title = 'Dialog Title';
455
+ close = new EventEmitter();
456
+ onClose() {
457
+ this.close.emit();
458
+ }
459
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
460
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DialogComponent, isStandalone: true, selector: "dso-dialog", inputs: { visible: "visible", title: "title" }, outputs: { close: "close" }, ngImport: i0, template: "<div class=\"dialog-overlay\" *ngIf=\"visible\" (click)=\"onClose()\">\r\n <div class=\"dialog\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"dialog-header\">{{ title }}</div>\r\n <div class=\"dialog-body\">\r\n <ng-content></ng-content> <!-- Projected content -->\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".dialog-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;padding:24px;border-radius:8px;width:400px;max-width:90%;box-shadow:0 2px 8px #00000040;position:relative;display:flex;flex-direction:column;gap:12px}.dialog-header{font-size:20px;font-weight:700}.dialog-close{background:none;border:none;font-size:1.5rem;line-height:1;cursor:pointer}.dialog-body{display:flex;flex-direction:column;gap:16px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
461
+ }
462
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DialogComponent, decorators: [{
463
+ type: Component,
464
+ args: [{ selector: 'dso-dialog', standalone: true, imports: [CommonModule], template: "<div class=\"dialog-overlay\" *ngIf=\"visible\" (click)=\"onClose()\">\r\n <div class=\"dialog\" (click)=\"$event.stopPropagation()\">\r\n <div class=\"dialog-header\">{{ title }}</div>\r\n <div class=\"dialog-body\">\r\n <ng-content></ng-content> <!-- Projected content -->\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".dialog-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;display:flex;align-items:center;justify-content:center;z-index:1000}.dialog{background:#fff;padding:24px;border-radius:8px;width:400px;max-width:90%;box-shadow:0 2px 8px #00000040;position:relative;display:flex;flex-direction:column;gap:12px}.dialog-header{font-size:20px;font-weight:700}.dialog-close{background:none;border:none;font-size:1.5rem;line-height:1;cursor:pointer}.dialog-body{display:flex;flex-direction:column;gap:16px}\n"] }]
465
+ }], propDecorators: { visible: [{
466
+ type: Input
467
+ }], title: [{
468
+ type: Input
469
+ }], close: [{
470
+ type: Output
471
+ }] } });
472
+
473
+ class DsoTruncateDirective {
474
+ el;
475
+ /**
476
+ * Enable or disable truncation.
477
+ * Can be used as `<div dsoTruncate>` (enabled by default) or `[dsoTruncate]="false"`.
478
+ */
479
+ enableTruncate = true;
480
+ /** Optional: override tooltip text when text is truncated */
481
+ truncateTooltipText;
482
+ constructor(el) {
483
+ this.el = el;
484
+ }
485
+ /** Initialize truncation after the view is loaded */
486
+ ngAfterViewInit() {
487
+ if (this.enableTruncate) {
488
+ this.applyTruncationStyles();
489
+ this.updateTooltip();
490
+ }
491
+ }
492
+ /** Re-check overflow and update tooltip on window resize */
493
+ onResize() {
494
+ if (this.enableTruncate) {
495
+ this.updateTooltip();
496
+ }
497
+ }
498
+ // -------------------------
499
+ // PRIVATE METHODS
500
+ // -------------------------
501
+ /** Apply CSS styles necessary for truncation */
502
+ applyTruncationStyles() {
503
+ const element = this.el.nativeElement;
504
+ element.style.whiteSpace = 'nowrap';
505
+ element.style.overflow = 'hidden';
506
+ element.style.textOverflow = 'ellipsis';
507
+ element.style.display = 'inline-block';
508
+ element.style.maxWidth = '100%';
509
+ }
510
+ /** Update tooltip based on overflow state */
511
+ updateTooltip() {
512
+ const el = this.el.nativeElement;
513
+ const isOverflowing = el.scrollWidth > el.clientWidth;
514
+ if (isOverflowing) {
515
+ // Add or update tooltip with either the provided text or element content
516
+ el.setAttribute('dsoDirectiveTooltip', this.truncateTooltipText || el.textContent || '');
517
+ }
518
+ else {
519
+ // Remove tooltip if content fits
520
+ el.removeAttribute('dsoDirectiveTooltip');
521
+ }
522
+ }
523
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DsoTruncateDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
524
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: DsoTruncateDirective, isStandalone: true, selector: "[dsoTruncate]", inputs: { enableTruncate: ["dsoTruncate", "enableTruncate"], truncateTooltipText: "truncateTooltipText" }, host: { listeners: { "window:resize": "onResize()" } }, ngImport: i0 });
525
+ }
526
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DsoTruncateDirective, decorators: [{
527
+ type: Directive,
528
+ args: [{
529
+ selector: '[dsoTruncate]',
530
+ standalone: true
531
+ }]
532
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { enableTruncate: [{
533
+ type: Input,
534
+ args: ['dsoTruncate']
535
+ }], truncateTooltipText: [{
536
+ type: Input
537
+ }], onResize: [{
538
+ type: HostListener,
539
+ args: ['window:resize']
540
+ }] } });
541
+
542
+ class DropdownListComponent {
543
+ // -------------------------------
544
+ // ELEMENT REFERENCE
545
+ // -------------------------------
546
+ /** Reference to the root <div> to detect clicks inside/outside dropdown */
547
+ root;
548
+ // -------------------------------
549
+ // INPUT PROPERTIES
550
+ // -------------------------------
551
+ /** Options to display in the dropdown. Each option has a label and a value */
552
+ options = [];
553
+ /** Placeholder text when no option is selected */
554
+ placeholder = 'Select';
555
+ /** Currently selected value */
556
+ value = null;
557
+ /** Disable dropdown interaction */
558
+ disabled = false;
559
+ // -------------------------------
560
+ // OUTPUT EVENTS
561
+ // -------------------------------
562
+ /** Emits the selected value when user clicks an option */
563
+ selectionChange = new EventEmitter();
564
+ // -------------------------------
565
+ // INTERNAL STATE
566
+ // -------------------------------
567
+ /** Tracks whether the dropdown list is open or closed */
568
+ isOpen = false;
569
+ // -------------------------------
570
+ // METHODS
571
+ // -------------------------------
572
+ /** Toggle dropdown open/close when trigger is clicked */
573
+ toggle() {
574
+ if (!this.disabled) {
575
+ this.isOpen = !this.isOpen;
576
+ }
577
+ }
578
+ /** Handle option selection */
579
+ select(option) {
580
+ this.value = option.value; // Update internal state
581
+ this.selectionChange.emit(this.value); // Notify parent component
582
+ this.isOpen = false; // Close dropdown
583
+ }
584
+ // -------------------------------
585
+ // HANDLE CLICKS OUTSIDE DROPDOWN
586
+ // -------------------------------
587
+ handleOutsideClick(event) {
588
+ if (!this.isOpen)
589
+ return;
590
+ const target = event.target;
591
+ if (!this.root.nativeElement.contains(target)) {
592
+ this.isOpen = false;
593
+ }
594
+ }
595
+ // -------------------------------
596
+ // HELPER GETTER
597
+ // -------------------------------
598
+ /** Returns label of selected value or placeholder if none selected */
599
+ get selectedLabel() {
600
+ const found = this.options.find(o => o.value === this.value);
601
+ return found ? found.label : this.placeholder;
602
+ }
603
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DropdownListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
604
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: DropdownListComponent, isStandalone: true, selector: "dso-dropdown-list", inputs: { options: "options", placeholder: "placeholder", value: "value", disabled: "disabled" }, outputs: { selectionChange: "selectionChange" }, host: { listeners: { "document:click": "handleOutsideClick($event)" } }, viewQueries: [{ propertyName: "root", first: true, predicate: ["root"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"dropdown\" #root [class.disabled]=\"disabled\">\r\n <!--\r\n #root: Linked to ViewChild in TS for detecting outside clicks\r\n [class.disabled]: Adds CSS class when dropdown is disabled\r\n -->\r\n\r\n <!-- Trigger area: clicking toggles dropdown open/close -->\r\n <div class=\"dropdown-trigger\" (click)=\"toggle()\">\r\n <!-- Display selected label or placeholder -->\r\n <span>{{ selectedLabel }}</span>\r\n <!-- Arrow icon that rotates when dropdown is open -->\r\n <span class=\"icon\" [class.open]=\"isOpen\">\u25BE</span>\r\n </div>\r\n\r\n <!-- Dropdown menu: visible only when isOpen is true -->\r\n <ul class=\"dropdown-menu\" *ngIf=\"isOpen\">\r\n <!-- Render each option -->\r\n <li \r\n *ngFor=\"let option of options\"\r\n (click)=\"select(option)\"\r\n [class.selected]=\"option.value === value\"> <!-- Highlight selected option -->\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n</div>\r\n", styles: [".dropdown{position:relative;display:inline-flex;font-family:inherit}.dropdown.disabled{opacity:.5;pointer-events:none}.dropdown-trigger{border:1px solid #ccc;padding:8px 12px;background:#fff;cursor:pointer;display:inline-flex;gap:8px;align-items:center;border-radius:4px}.dropdown-trigger:hover{border-color:#888}.dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;background:#fff;border:1px solid #ccc;border-radius:4px;list-style:none;padding:4px 0;margin:0;max-height:200px;overflow-y:auto;z-index:100}.dropdown-menu li{padding:8px 12px;cursor:pointer}.dropdown-menu li:hover{background:#f2f2f2}.dropdown-menu li.selected{background:#e6e6e6;font-weight:500}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
605
+ }
606
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DropdownListComponent, decorators: [{
607
+ type: Component,
608
+ args: [{ selector: 'dso-dropdown-list', standalone: true, imports: [CommonModule], template: "<div class=\"dropdown\" #root [class.disabled]=\"disabled\">\r\n <!--\r\n #root: Linked to ViewChild in TS for detecting outside clicks\r\n [class.disabled]: Adds CSS class when dropdown is disabled\r\n -->\r\n\r\n <!-- Trigger area: clicking toggles dropdown open/close -->\r\n <div class=\"dropdown-trigger\" (click)=\"toggle()\">\r\n <!-- Display selected label or placeholder -->\r\n <span>{{ selectedLabel }}</span>\r\n <!-- Arrow icon that rotates when dropdown is open -->\r\n <span class=\"icon\" [class.open]=\"isOpen\">\u25BE</span>\r\n </div>\r\n\r\n <!-- Dropdown menu: visible only when isOpen is true -->\r\n <ul class=\"dropdown-menu\" *ngIf=\"isOpen\">\r\n <!-- Render each option -->\r\n <li \r\n *ngFor=\"let option of options\"\r\n (click)=\"select(option)\"\r\n [class.selected]=\"option.value === value\"> <!-- Highlight selected option -->\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n</div>\r\n", styles: [".dropdown{position:relative;display:inline-flex;font-family:inherit}.dropdown.disabled{opacity:.5;pointer-events:none}.dropdown-trigger{border:1px solid #ccc;padding:8px 12px;background:#fff;cursor:pointer;display:inline-flex;gap:8px;align-items:center;border-radius:4px}.dropdown-trigger:hover{border-color:#888}.dropdown-menu{position:absolute;top:calc(100% + 4px);left:0;right:0;background:#fff;border:1px solid #ccc;border-radius:4px;list-style:none;padding:4px 0;margin:0;max-height:200px;overflow-y:auto;z-index:100}.dropdown-menu li{padding:8px 12px;cursor:pointer}.dropdown-menu li:hover{background:#f2f2f2}.dropdown-menu li.selected{background:#e6e6e6;font-weight:500}\n"] }]
609
+ }], propDecorators: { root: [{
610
+ type: ViewChild,
611
+ args: ['root', { static: true }]
612
+ }], options: [{
613
+ type: Input
614
+ }], placeholder: [{
615
+ type: Input
616
+ }], value: [{
617
+ type: Input
618
+ }], disabled: [{
619
+ type: Input
620
+ }], selectionChange: [{
621
+ type: Output
622
+ }], handleOutsideClick: [{
623
+ type: HostListener,
624
+ args: ['document:click', ['$event']]
625
+ }] } });
626
+
627
+ class ProgressBarComponent {
628
+ /** Progress value (0–100). If null, shows indeterminate animation. */
629
+ value = null;
630
+ /** Color variants */
631
+ color = 'primary';
632
+ /** Optional animation toggle */
633
+ animated = true;
634
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProgressBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
635
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: ProgressBarComponent, isStandalone: true, selector: "dso-progress-bar", inputs: { value: "value", color: "color", animated: "animated" }, ngImport: i0, template: "<div class=\"progress-bar-container\">\r\n <div\r\n class=\"progress-bar-fill\"\r\n [ngClass]=\"[\r\n color,\r\n animated ? 'animated' : '',\r\n value === null ? 'indeterminate' : ''\r\n ]\"\r\n [style.width.%]=\"value !== null ? value : 50\"\r\n ></div>\r\n</div>\r\n", styles: ["@charset \"UTF-8\";.progress-bar-container{width:100%;height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;position:relative;min-width:240px}.progress-bar-fill{position:absolute;height:100%;border-radius:4px;transition:width .4s ease}.progress-bar-fill.primary{background-color:#007bff}.progress-bar-fill.success{background-color:#28a745}.progress-bar-fill.warning{background-color:#ffc107}.progress-bar-fill.danger{background-color:#dc3545}.progress-bar-fill.animated{transition:width .4s ease-in-out}.progress-bar-fill.indeterminate{width:30%;animation:scrollLoop 1.5s linear infinite}@keyframes scrollLoop{0%{transform:translate(-100%)}to{transform:translate(400%)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
636
+ }
637
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ProgressBarComponent, decorators: [{
638
+ type: Component,
639
+ args: [{ selector: 'dso-progress-bar', standalone: true, imports: [CommonModule], template: "<div class=\"progress-bar-container\">\r\n <div\r\n class=\"progress-bar-fill\"\r\n [ngClass]=\"[\r\n color,\r\n animated ? 'animated' : '',\r\n value === null ? 'indeterminate' : ''\r\n ]\"\r\n [style.width.%]=\"value !== null ? value : 50\"\r\n ></div>\r\n</div>\r\n", styles: ["@charset \"UTF-8\";.progress-bar-container{width:100%;height:8px;background-color:#e9ecef;border-radius:4px;overflow:hidden;position:relative;min-width:240px}.progress-bar-fill{position:absolute;height:100%;border-radius:4px;transition:width .4s ease}.progress-bar-fill.primary{background-color:#007bff}.progress-bar-fill.success{background-color:#28a745}.progress-bar-fill.warning{background-color:#ffc107}.progress-bar-fill.danger{background-color:#dc3545}.progress-bar-fill.animated{transition:width .4s ease-in-out}.progress-bar-fill.indeterminate{width:30%;animation:scrollLoop 1.5s linear infinite}@keyframes scrollLoop{0%{transform:translate(-100%)}to{transform:translate(400%)}}\n"] }]
640
+ }], propDecorators: { value: [{
641
+ type: Input
642
+ }], color: [{
643
+ type: Input
644
+ }], animated: [{
645
+ type: Input
646
+ }] } });
647
+
648
+ class FileUploadItemComponent {
649
+ /** The file upload item to display */
650
+ item;
651
+ /** Emitted when the user retries a failed upload */
652
+ retry = new EventEmitter();
653
+ /** Emitted when the user removes the file from the list */
654
+ remove = new EventEmitter();
655
+ /** Emitted when the user cancels an ongoing upload */
656
+ cancel = new EventEmitter();
657
+ /**
658
+ * Format the file size into a human-readable string
659
+ * @param size - size in bytes
660
+ * @returns formatted string like '12.3 KB', '1.5 MB'
661
+ */
662
+ formatSize(size) {
663
+ if (size < 1024)
664
+ return size + ' B';
665
+ if (size < 1024 * 1024)
666
+ return (size / 1024).toFixed(1) + ' KB';
667
+ return (size / (1024 * 1024)).toFixed(1) + ' MB';
668
+ }
669
+ /**
670
+ * Get an icon name for the file based on its type or extension
671
+ * @param item - the UploadItem
672
+ * @returns icon name string
673
+ */
674
+ getFileIcon(item) {
675
+ const file = item.file;
676
+ const type = file.type.toLowerCase();
677
+ const name = file.name.toLowerCase();
678
+ // Image MIME types
679
+ if (type.startsWith('image/')) {
680
+ return 'icon-gallery';
681
+ }
682
+ // Image file extensions fallback
683
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
684
+ if (imageExtensions.some(ext => name.endsWith(ext))) {
685
+ return 'icon-gallery';
686
+ }
687
+ // Default icon for non-image files
688
+ return 'icon-van';
689
+ }
690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
691
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileUploadItemComponent, isStandalone: true, selector: "dso-file-item", inputs: { item: "item" }, outputs: { retry: "retry", remove: "remove", cancel: "cancel" }, ngImport: i0, template: "<div class=\"file-row\">\r\n\r\n <!-- LEFT SECTION: Thumbnail + filename -->\r\n <div class=\"thumbnail-filename-wrapper\">\r\n <div class=\"file-thumb\">\r\n <dso-icon \r\n [iconName]=\"getFileIcon(item)\"\r\n size=\"small\"\r\n class=\"thumb-icon\">\r\n </dso-icon>\r\n </div>\r\n\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ item.file.name }}</div>\r\n <div class=\"file-size\">{{ formatSize(item.file.size) }}</div>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- MIDDLE SECTION: Progress or status -->\r\n <div class=\"filecontent-loader-wrapper\">\r\n \r\n <!-- Progress bar -->\r\n <div class=\"file-progress\" *ngIf=\"!item.hideProgressBar\">\r\n <dso-progress-bar\r\n [value]=\"\r\n item.status === 'uploading' ? item.progress :\r\n item.status === 'completed' ? 100 :\r\n null\r\n \"\r\n [color]=\"item.status === 'completed' ? 'success' : 'primary'\"\r\n ></dso-progress-bar>\r\n </div>\r\n\r\n <!-- Status text -->\r\n <div class=\"file-status\" *ngIf=\"item.hideProgressBar\">\r\n\r\n <!-- Success -->\r\n <div *ngIf=\"item.status === 'completed'\" class=\"status success\">\r\n <dso-icon iconName=\"icon-van\" size=\"small\"></dso-icon>\r\n <span>Uploaded</span>\r\n </div>\r\n\r\n <!-- Failed -->\r\n <div *ngIf=\"item.status === 'failed'\" class=\"status error\">\r\n <dso-icon iconName=\"icon-center-align\" size=\"small\"></dso-icon>\r\n <span>Upload failed</span>\r\n </div>\r\n\r\n </div>\r\n\r\n <!-- RIGHT SECTION: Action buttons -->\r\n <div class=\"file-actions\">\r\n\r\n <!-- Uploading -->\r\n <ng-container *ngIf=\"item.status === 'uploading'\">\r\n <dso-button\r\n btnLabel=\"Cancel\"\r\n btnType=\"text\"\r\n (onClick)=\"cancel.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Failed -->\r\n <ng-container *ngIf=\"item.status === 'failed'\">\r\n <dso-button\r\n btnLabel=\"Retry\"\r\n btnType=\"text\"\r\n (onClick)=\"retry.emit()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Completed -->\r\n <ng-container *ngIf=\"item.status === 'completed'\">\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n</div>\r\n", styles: [".file-row{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:#fafafa;border-radius:4px}.file-info{display:flex;flex-direction:column}.file-info .file-name{font-weight:300}.file-info .file-size{font-size:.85rem;color:#666}.file-progress{padding-right:1rem;flex:1 0 0;display:flex;justify-content:center;align-items:center;min-width:260px;max-width:320px}.file-actions{display:flex;width:160px;gap:.5rem;justify-content:flex-end}.file-status{display:flex;align-items:center;font-size:14px;flex:1 0 0;min-width:260px;max-width:320px}.file-status .status{display:flex;align-items:center;gap:6px;font-weight:500}.file-status .status.success{color:#16a34a}.file-status .status.error{color:#dc2626}.file-status .status .status-icon{width:16px;height:16px}.file-thumb{width:40px;height:40px;flex-shrink:0;border-radius:6px;overflow:hidden;background:#f3f4f6;display:flex;align-items:center;justify-content:center}.file-thumb img{width:100%;height:100%;object-fit:cover}.thumbnail-filename-wrapper{display:flex;gap:24px}.filecontent-loader-wrapper{display:flex;gap:20px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }, { kind: "component", type: ProgressBarComponent, selector: "dso-progress-bar", inputs: ["value", "color", "animated"] }] });
692
+ }
693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadItemComponent, decorators: [{
694
+ type: Component,
695
+ args: [{ selector: 'dso-file-item', standalone: true, imports: [CommonModule, ButtonComponent, IconComponent, ProgressBarComponent], template: "<div class=\"file-row\">\r\n\r\n <!-- LEFT SECTION: Thumbnail + filename -->\r\n <div class=\"thumbnail-filename-wrapper\">\r\n <div class=\"file-thumb\">\r\n <dso-icon \r\n [iconName]=\"getFileIcon(item)\"\r\n size=\"small\"\r\n class=\"thumb-icon\">\r\n </dso-icon>\r\n </div>\r\n\r\n <div class=\"file-info\">\r\n <div class=\"file-name\">{{ item.file.name }}</div>\r\n <div class=\"file-size\">{{ formatSize(item.file.size) }}</div>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- MIDDLE SECTION: Progress or status -->\r\n <div class=\"filecontent-loader-wrapper\">\r\n \r\n <!-- Progress bar -->\r\n <div class=\"file-progress\" *ngIf=\"!item.hideProgressBar\">\r\n <dso-progress-bar\r\n [value]=\"\r\n item.status === 'uploading' ? item.progress :\r\n item.status === 'completed' ? 100 :\r\n null\r\n \"\r\n [color]=\"item.status === 'completed' ? 'success' : 'primary'\"\r\n ></dso-progress-bar>\r\n </div>\r\n\r\n <!-- Status text -->\r\n <div class=\"file-status\" *ngIf=\"item.hideProgressBar\">\r\n\r\n <!-- Success -->\r\n <div *ngIf=\"item.status === 'completed'\" class=\"status success\">\r\n <dso-icon iconName=\"icon-van\" size=\"small\"></dso-icon>\r\n <span>Uploaded</span>\r\n </div>\r\n\r\n <!-- Failed -->\r\n <div *ngIf=\"item.status === 'failed'\" class=\"status error\">\r\n <dso-icon iconName=\"icon-center-align\" size=\"small\"></dso-icon>\r\n <span>Upload failed</span>\r\n </div>\r\n\r\n </div>\r\n\r\n <!-- RIGHT SECTION: Action buttons -->\r\n <div class=\"file-actions\">\r\n\r\n <!-- Uploading -->\r\n <ng-container *ngIf=\"item.status === 'uploading'\">\r\n <dso-button\r\n btnLabel=\"Cancel\"\r\n btnType=\"text\"\r\n (onClick)=\"cancel.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Failed -->\r\n <ng-container *ngIf=\"item.status === 'failed'\">\r\n <dso-button\r\n btnLabel=\"Retry\"\r\n btnType=\"text\"\r\n (onClick)=\"retry.emit()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Completed -->\r\n <ng-container *ngIf=\"item.status === 'completed'\">\r\n <dso-button\r\n btnLabel=\"Remove\"\r\n btnType=\"text\"\r\n (onClick)=\"remove.emit()\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n </div>\r\n\r\n </div>\r\n\r\n</div>\r\n", styles: [".file-row{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;background:#fafafa;border-radius:4px}.file-info{display:flex;flex-direction:column}.file-info .file-name{font-weight:300}.file-info .file-size{font-size:.85rem;color:#666}.file-progress{padding-right:1rem;flex:1 0 0;display:flex;justify-content:center;align-items:center;min-width:260px;max-width:320px}.file-actions{display:flex;width:160px;gap:.5rem;justify-content:flex-end}.file-status{display:flex;align-items:center;font-size:14px;flex:1 0 0;min-width:260px;max-width:320px}.file-status .status{display:flex;align-items:center;gap:6px;font-weight:500}.file-status .status.success{color:#16a34a}.file-status .status.error{color:#dc2626}.file-status .status .status-icon{width:16px;height:16px}.file-thumb{width:40px;height:40px;flex-shrink:0;border-radius:6px;overflow:hidden;background:#f3f4f6;display:flex;align-items:center;justify-content:center}.file-thumb img{width:100%;height:100%;object-fit:cover}.thumbnail-filename-wrapper{display:flex;gap:24px}.filecontent-loader-wrapper{display:flex;gap:20px}\n"] }]
696
+ }], propDecorators: { item: [{
697
+ type: Input
698
+ }], retry: [{
699
+ type: Output
700
+ }], remove: [{
701
+ type: Output
702
+ }], cancel: [{
703
+ type: Output
704
+ }] } });
705
+
706
+ // upload-simulator.service.ts
707
+ /**
708
+ * UploadSimulatorService
709
+ * -----------------------
710
+ * This service simulates an upload process.
711
+ * It is ONLY for demo and UI development. Later, it can
712
+ * be replaced with a real HTTP upload service without
713
+ * changing the UI components.
714
+ *
715
+ * How it works:
716
+ * - Every 300ms, a timer increases the upload progress.
717
+ * - Progress increases by a random amount (to look realistic).
718
+ * - When it reaches 100%, the upload is marked complete.
719
+ * - There is a 10% chance the upload "fails" to simulate errors.
720
+ * - If an upload is cancelled, the interval stops immediately.
721
+ *
722
+ * This pattern mimics:
723
+ * - progress events (like HttpClient upload events)
724
+ * - cancellation (AbortController / unsubscribe)
725
+ */
726
+ class UploadSimulatorService {
727
+ /**
728
+ * Simulates uploading a single file.
729
+ *
730
+ * @param item The UploadItem being uploaded
731
+ * @param onProgress Callback fired whenever progress changes
732
+ * @param onComplete Callback fired when upload reaches 100%
733
+ * @param onError Callback fired when upload "fails"
734
+ */
735
+ simulateUpload(item, onProgress, onComplete, onError) {
736
+ console.log('%c[SIMULATOR] Starting upload:', 'color: green;', item.file.name);
737
+ let progress = 0;
738
+ // This interval simulates a real upload stream
739
+ const interval = setInterval(() => {
740
+ // If cancelled externally, we stop updating
741
+ if (item.status === 'cancelled') {
742
+ console.log('%c[SIMULATOR] Upload CANCELLED:', 'color: orange;', item.file.name);
743
+ clearInterval(interval);
744
+ return;
745
+ }
746
+ // 🔼 Simulate uploading by adding random progress increments
747
+ const increment = Math.random() * 15 + 5;
748
+ progress += increment;
749
+ console.log(`%c[SIMULATOR] ${item.file.name} +${increment.toFixed(1)}% → ${progress.toFixed(1)}%`, 'color: #1e90ff;');
750
+ // 🟦 If progress reaches 100%, finalize upload
751
+ if (progress >= 100) {
752
+ progress = 100;
753
+ onProgress(progress);
754
+ clearInterval(interval);
755
+ console.log('%c[SIMULATOR] Upload reached 100% for ' + item.file.name, 'color: green; font-weight: bold;');
756
+ // 🔥 50% chance to simulate failure
757
+ const randomFail = Math.random() < 0.5;
758
+ if (randomFail) {
759
+ console.log('%c[SIMULATOR] Simulated FAILURE for ' + item.file.name, 'color: red; font-weight: bold;');
760
+ onError();
761
+ }
762
+ else {
763
+ console.log('%c[SIMULATOR] Upload SUCCESS for ' + item.file.name, 'color: limegreen; font-weight: bold;');
764
+ onComplete();
765
+ }
766
+ return;
767
+ }
768
+ // Emit progress update
769
+ onProgress(progress);
770
+ }, 300); // runs every 300ms like a real upload tick
771
+ }
772
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UploadSimulatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
773
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UploadSimulatorService, providedIn: 'root' });
774
+ }
775
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: UploadSimulatorService, decorators: [{
776
+ type: Injectable,
777
+ args: [{ providedIn: 'root' }]
778
+ }] });
779
+
780
+ // file-upload-multi.component.ts
781
+ /**
782
+ * FileUploadMultiComponent
783
+ * -------------------------
784
+ * This component handles:
785
+ * - Drag & drop file uploads
786
+ * - Selecting multiple files through <input type="file">
787
+ * - Upload progress per file
788
+ * - Cancelling an upload
789
+ * - Retrying failed uploads
790
+ * - Removing items from the queue
791
+ *
792
+ * UploadSimulatorService is used to mimic real upload behavior.
793
+ * Later, you can replace it with a real HTTP upload service.
794
+ */
795
+ class FileUploadMultiComponent {
796
+ sim;
797
+ // ngOnInit() {
798
+ // // Fake completed file
799
+ // this.uploadQueue.push({
800
+ // file: new File([""], "example-image.jpg", { type: "image/jpeg" }),
801
+ // progress: 100,
802
+ // status: "completed",
803
+ // hideProgressBar: true
804
+ // });
805
+ // // Fake failed file
806
+ // this.uploadQueue.push({
807
+ // file: new File([""], "broken-file.pdf", { type: "application/pdf" }),
808
+ // progress: null,
809
+ // status: "failed",
810
+ // hideProgressBar: true
811
+ // });
812
+ // // Fake uploading file
813
+ // this.uploadQueue.push({
814
+ // file: new File([""], "uploading.png", { type: "image/png" }),
815
+ // progress: 40,
816
+ // status: "uploading",
817
+ // hideProgressBar: false
818
+ // });
819
+ // }
820
+ /** All files being managed (uploaded, uploading, failed, completed). */
821
+ uploadQueue = [];
822
+ /** True while user drags a file over the drop zone. */
823
+ isDragOver = false;
824
+ constructor(sim) {
825
+ this.sim = sim;
826
+ // console.log('%c[COMPONENT] FileUploadMultiComponent initialized', 'color: purple;');
827
+ }
828
+ /* ===========================================================
829
+ * DRAG & DROP EVENTS
830
+ * =========================================================== */
831
+ onDragOver(event) {
832
+ event.preventDefault();
833
+ this.isDragOver = true;
834
+ // console.log('%c[DRAG] Dragging over drop zone...', 'color: #00aaff;');
835
+ }
836
+ onDragLeave(event) {
837
+ this.isDragOver = false;
838
+ // console.log('%c[DRAG] Drag left drop zone', 'color: #888;');
839
+ }
840
+ onDrop(event) {
841
+ event.preventDefault();
842
+ this.isDragOver = false;
843
+ // console.log('%c[DRAG] Files dropped', 'color: #00cc66;');
844
+ if (event.dataTransfer?.files) {
845
+ // console.log('[DRAG] Dropped files:', event.dataTransfer.files);
846
+ this.handleFiles(event.dataTransfer.files);
847
+ }
848
+ }
849
+ /* ===========================================================
850
+ * FILE INPUT SELECT
851
+ * =========================================================== */
852
+ onFileSelected(event) {
853
+ const input = event.target;
854
+ if (input.files) {
855
+ // console.log('%c[INPUT] Files selected:', 'color: #0099ff;', input.files);
856
+ this.handleFiles(input.files);
857
+ // Clears <input> so user can re-select same file
858
+ input.value = '';
859
+ }
860
+ }
861
+ /* ===========================================================
862
+ * PROCESSING NEWLY ADDED FILES
863
+ * =========================================================== */
864
+ handleFiles(fileList) {
865
+ // console.log('%c[FILES] Processing new files...', 'color: #ffaa00;');
866
+ Array.from(fileList).forEach(file => {
867
+ // console.log(`%c[FILES] Added: ${file.name} (${file.size} bytes)`, 'color: #ffaa00;');
868
+ const item = {
869
+ file,
870
+ progress: 0,
871
+ status: 'queued'
872
+ };
873
+ this.uploadQueue.push(item);
874
+ this.startUpload(item);
875
+ });
876
+ }
877
+ /* ===========================================================
878
+ * UPLOAD LOGIC
879
+ * =========================================================== */
880
+ startUpload(item) {
881
+ // console.log(`%c[UPLOAD] Starting upload for ${item.file.name}`, 'color: #00cc99;');
882
+ item.status = 'uploading';
883
+ item.progress = 0;
884
+ this.sim.simulateUpload(item,
885
+ // ---- onProgress callback ----
886
+ (progress) => {
887
+ item.progress = progress;
888
+ // console.log(`%c[UPLOAD] ${item.file.name}: ${progress.toFixed(1)}%`, 'color: #0099ff;');
889
+ },
890
+ // ---- onComplete callback ----
891
+ () => {
892
+ item.status = 'completed';
893
+ item.progress = 100;
894
+ // Hide progress bar after a very small delay (100–300ms)
895
+ setTimeout(() => item.hideProgressBar = true, 300);
896
+ // console.log(`%c[UPLOAD] COMPLETED: ${item.file.name}`, 'color: green; font-weight: bold;');
897
+ },
898
+ // ---- onError callback ----
899
+ () => {
900
+ item.status = 'failed';
901
+ item.progress = null;
902
+ // Hide progress bar so status text can show
903
+ setTimeout(() => item.hideProgressBar = true, 300);
904
+ // console.log(`%c[UPLOAD] FAILED: ${item.file.name}`, 'color: red; font-weight: bold;');
905
+ });
906
+ }
907
+ onDropZoneClick(fileInput) {
908
+ // Prevent click if user is dragging files over the drop-zone
909
+ if (this.isDragOver)
910
+ return;
911
+ console.log('%c[CLICK] Drop zone clicked → opening file dialog', 'color: #0077ff;');
912
+ fileInput.click();
913
+ }
914
+ onBrowseButtonClick(event, fileInput) {
915
+ event.stopPropagation(); // stops click from bubbling to drop-zone
916
+ // console.log('%c[BUTTON] Browse clicked → opening file dialog', 'color: #8844ff;');
917
+ fileInput.click();
918
+ }
919
+ /* ===========================================================
920
+ * BUTTON ACTIONS (CANCEL / RETRY / REMOVE)
921
+ * =========================================================== */
922
+ cancelUpload(item) {
923
+ // console.log(`%c[CANCEL] Cancelling ${item.file.name}`, 'color: orange; font-weight: bold;');
924
+ // item.status = 'cancelled';
925
+ // item.progress = null;
926
+ this.removeItem(item);
927
+ }
928
+ retryUpload(item) {
929
+ // console.log('[RETRY] Restarting upload for', item.file.name);
930
+ // Reset state
931
+ item.status = 'queued';
932
+ item.progress = 0;
933
+ item.hideProgressBar = false; // <-- IMPORTANT
934
+ // Start upload again
935
+ this.startUpload(item);
936
+ }
937
+ removeItem(item) {
938
+ // console.log(`%c[REMOVE] Removing ${item.file.name}`, 'color: gray;');
939
+ this.uploadQueue = this.uploadQueue.filter(i => i !== item);
940
+ }
941
+ /* ===========================================================
942
+ * UTILITY FUNCTIONS
943
+ * =========================================================== */
944
+ /**
945
+ * Converts bytes → human readable sizes.
946
+ */
947
+ formatSize(size) {
948
+ if (size < 1024)
949
+ return size + ' B';
950
+ if (size < 1024 * 1024)
951
+ return (size / 1024).toFixed(1) + ' KB';
952
+ return (size / (1024 * 1024)).toFixed(1) + ' MB';
953
+ }
954
+ // getThumbnail(item: UploadItem): string | null {
955
+ // const file = item.file;
956
+ // // If actual image → return preview URL
957
+ // if (file.type.startsWith('image/')) {
958
+ // return URL.createObjectURL(file);
959
+ // }
960
+ // // Otherwise return an icon path depending on type
961
+ // if (file.type === 'application/pdf') return 'assets/icons/pdf.svg';
962
+ // if (file.type.includes('word')) return 'assets/icons/doc.svg';
963
+ // if (file.type.includes('spreadsheet')) return 'assets/icons/xlsx.svg';
964
+ // // default icon
965
+ // return 'assets/icons/file.svg';
966
+ // }
967
+ getThumbnail(item) {
968
+ const file = item.file;
969
+ // Image preview for image file types
970
+ if (file.type.startsWith('image/')) {
971
+ return URL.createObjectURL(file);
972
+ }
973
+ // Not an image → no thumbnail
974
+ return null;
975
+ }
976
+ getFileIcon(item) {
977
+ const file = item.file;
978
+ const type = file.type.toLowerCase();
979
+ const name = file.name.toLowerCase();
980
+ console.log('%c[DEBUG] File name:', 'color: blue;', file.name);
981
+ console.log('%c[DEBUG] File type:', 'color: green;', file.type);
982
+ // Check MIME type first
983
+ if (type.startsWith('image/')) {
984
+ console.log('%c[DEBUG] Detected as image by MIME type', 'color: purple;');
985
+ return 'icon-gallery';
986
+ }
987
+ // Fallback: check extension for image types including SVG
988
+ const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
989
+ if (imageExtensions.some(ext => name.endsWith(ext))) {
990
+ console.log('%c[DEBUG] Detected as image by extension', 'color: orange;');
991
+ return 'icon-gallery';
992
+ }
993
+ console.log('%c[DEBUG] Defaulting to document icon', 'color: red;');
994
+ return 'icon-van';
995
+ }
996
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadMultiComponent, deps: [{ token: UploadSimulatorService }], target: i0.ɵɵFactoryTarget.Component });
997
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileUploadMultiComponent, isStandalone: true, selector: "dso-file-upload-multi", ngImport: i0, template: "<div class=\"upload-container\">\r\n\r\n <!-- DRAG & DROP AREA -->\r\n <div\r\n class=\"drop-zone\"\r\n [class.drag-over]=\"isDragOver\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (click)=\"onDropZoneClick(fileInput)\"\r\n >\r\n\r\n <dso-button \r\n style=\"padding: 0; \"\r\n btnLabel=\"Select files\"\r\n btnType=\"link\"\r\n (onClick)=\"onBrowseButtonClick($event, fileInput)\">\r\n </dso-button>\r\n or drag & drop files \r\n <input \r\n type=\"file\" \r\n multiple \r\n hidden \r\n #fileInput \r\n (change)=\"onFileSelected($event)\"\r\n />\r\n </div>\r\n\r\n <!-- FILE LIST -->\r\n <div class=\"file-list\" *ngIf=\"uploadQueue.length > 0\">\r\n\r\n <dso-file-item\r\n *ngFor=\"let item of uploadQueue\"\r\n [item]=\"item\"\r\n (retry)=\"retryUpload(item)\"\r\n (remove)=\"removeItem(item)\"\r\n (cancel)=\"cancelUpload(item)\"\r\n ></dso-file-item>\r\n\r\n\r\n </div>\r\n</div>\r\n", styles: [".upload-container{display:flex;flex-direction:column;gap:8px}.drop-zone{border:2px dashed #ccc;padding:2rem;text-align:center;border-radius:6px;transition:border-color .2s,background-color .2s;cursor:pointer}.drop-zone.drag-over{border-color:#007bff;background-color:#f0f7ff}.file-list{display:flex;flex-direction:column;gap:.75rem;max-height:360px;overflow-y:auto;padding-right:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: FileUploadItemComponent, selector: "dso-file-item", inputs: ["item"], outputs: ["retry", "remove", "cancel"] }] });
998
+ }
999
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadMultiComponent, decorators: [{
1000
+ type: Component,
1001
+ args: [{ selector: 'dso-file-upload-multi', standalone: true, imports: [CommonModule, ButtonComponent, IconComponent, FileUploadItemComponent], template: "<div class=\"upload-container\">\r\n\r\n <!-- DRAG & DROP AREA -->\r\n <div\r\n class=\"drop-zone\"\r\n [class.drag-over]=\"isDragOver\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (click)=\"onDropZoneClick(fileInput)\"\r\n >\r\n\r\n <dso-button \r\n style=\"padding: 0; \"\r\n btnLabel=\"Select files\"\r\n btnType=\"link\"\r\n (onClick)=\"onBrowseButtonClick($event, fileInput)\">\r\n </dso-button>\r\n or drag & drop files \r\n <input \r\n type=\"file\" \r\n multiple \r\n hidden \r\n #fileInput \r\n (change)=\"onFileSelected($event)\"\r\n />\r\n </div>\r\n\r\n <!-- FILE LIST -->\r\n <div class=\"file-list\" *ngIf=\"uploadQueue.length > 0\">\r\n\r\n <dso-file-item\r\n *ngFor=\"let item of uploadQueue\"\r\n [item]=\"item\"\r\n (retry)=\"retryUpload(item)\"\r\n (remove)=\"removeItem(item)\"\r\n (cancel)=\"cancelUpload(item)\"\r\n ></dso-file-item>\r\n\r\n\r\n </div>\r\n</div>\r\n", styles: [".upload-container{display:flex;flex-direction:column;gap:8px}.drop-zone{border:2px dashed #ccc;padding:2rem;text-align:center;border-radius:6px;transition:border-color .2s,background-color .2s;cursor:pointer}.drop-zone.drag-over{border-color:#007bff;background-color:#f0f7ff}.file-list{display:flex;flex-direction:column;gap:.75rem;max-height:360px;overflow-y:auto;padding-right:4px}\n"] }]
1002
+ }], ctorParameters: () => [{ type: UploadSimulatorService }] });
1003
+
1004
+ class FileUploadSingleComponent {
1005
+ sim;
1006
+ /** Button label text */
1007
+ btnLabel = 'Choose File';
1008
+ /** Allowed file types, e.g. ".png,.jpg,.pdf" */
1009
+ accept = '';
1010
+ /** Emit the selected file to parent component */
1011
+ fileSelected = new EventEmitter();
1012
+ /** The currently uploaded file shown in the UI */
1013
+ item = null;
1014
+ constructor(sim) {
1015
+ this.sim = sim;
1016
+ }
1017
+ /** Trigger the hidden file input click */
1018
+ onButtonClick(input) {
1019
+ input.click();
1020
+ }
1021
+ /** Handle file selection from input */
1022
+ onFileChange(event) {
1023
+ const input = event.target;
1024
+ if (!input.files || input.files.length === 0)
1025
+ return;
1026
+ const file = input.files[0];
1027
+ // Wrap file in UploadItem structure (same as multi-upload)
1028
+ this.item = {
1029
+ file,
1030
+ progress: 0,
1031
+ status: 'queued',
1032
+ hideProgressBar: false
1033
+ };
1034
+ // Emit file to parent
1035
+ this.fileSelected.emit(file);
1036
+ // Start simulated upload
1037
+ this.startUpload();
1038
+ // Reset input so user can pick same file again
1039
+ input.value = '';
1040
+ }
1041
+ /** Start upload simulation */
1042
+ startUpload() {
1043
+ if (!this.item)
1044
+ return;
1045
+ const item = this.item;
1046
+ item.status = 'uploading';
1047
+ item.progress = 0;
1048
+ this.sim.simulateUpload(item,
1049
+ // onProgress callback
1050
+ (progress) => {
1051
+ item.progress = progress;
1052
+ },
1053
+ // onComplete callback
1054
+ () => {
1055
+ item.status = 'completed';
1056
+ item.progress = 100;
1057
+ setTimeout(() => item.hideProgressBar = true, 300);
1058
+ },
1059
+ // onError callback
1060
+ () => {
1061
+ item.status = 'failed';
1062
+ item.progress = null;
1063
+ setTimeout(() => item.hideProgressBar = true, 300);
1064
+ });
1065
+ }
1066
+ /** Retry failed upload */
1067
+ retry() {
1068
+ if (!this.item)
1069
+ return;
1070
+ this.item.status = 'queued';
1071
+ this.item.progress = 0;
1072
+ this.item.hideProgressBar = false;
1073
+ this.startUpload();
1074
+ }
1075
+ /** Cancel or remove the file from UI */
1076
+ clearFile() {
1077
+ this.item = null;
1078
+ }
1079
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadSingleComponent, deps: [{ token: UploadSimulatorService }], target: i0.ɵɵFactoryTarget.Component });
1080
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileUploadSingleComponent, isStandalone: true, selector: "dso-file-upload-single", inputs: { btnLabel: "btnLabel", accept: "accept" }, outputs: { fileSelected: "fileSelected" }, ngImport: i0, template: "<div class=\"file-upload-single\">\r\n\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n #fileInput\r\n [accept]=\"accept\"\r\n (change)=\"onFileChange($event)\"\r\n hidden\r\n />\r\n\r\n <!-- Main button -->\r\n <dso-button\r\n [btnLabel]=\"btnLabel\"\r\n btnType=\"filled\"\r\n (onClick)=\"onButtonClick(fileInput)\"\r\n ></dso-button>\r\n\r\n <!-- Render file info using shared reusable component -->\r\n <dso-file-item\r\n *ngIf=\"item\"\r\n [item]=\"item\"\r\n (cancel)=\"clearFile()\"\r\n (remove)=\"clearFile()\"\r\n (retry)=\"retry()\"\r\n ></dso-file-item>\r\n\r\n</div>\r\n", styles: [".file-upload-single{display:flex;flex-direction:column;gap:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: FileUploadItemComponent, selector: "dso-file-item", inputs: ["item"], outputs: ["retry", "remove", "cancel"] }] });
1081
+ }
1082
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileUploadSingleComponent, decorators: [{
1083
+ type: Component,
1084
+ args: [{ selector: 'dso-file-upload-single', standalone: true, imports: [
1085
+ CommonModule,
1086
+ ButtonComponent,
1087
+ FileUploadItemComponent
1088
+ ], template: "<div class=\"file-upload-single\">\r\n\r\n <!-- Hidden file input -->\r\n <input\r\n type=\"file\"\r\n #fileInput\r\n [accept]=\"accept\"\r\n (change)=\"onFileChange($event)\"\r\n hidden\r\n />\r\n\r\n <!-- Main button -->\r\n <dso-button\r\n [btnLabel]=\"btnLabel\"\r\n btnType=\"filled\"\r\n (onClick)=\"onButtonClick(fileInput)\"\r\n ></dso-button>\r\n\r\n <!-- Render file info using shared reusable component -->\r\n <dso-file-item\r\n *ngIf=\"item\"\r\n [item]=\"item\"\r\n (cancel)=\"clearFile()\"\r\n (remove)=\"clearFile()\"\r\n (retry)=\"retry()\"\r\n ></dso-file-item>\r\n\r\n</div>\r\n", styles: [".file-upload-single{display:flex;flex-direction:column;gap:8px}\n"] }]
1089
+ }], ctorParameters: () => [{ type: UploadSimulatorService }], propDecorators: { btnLabel: [{
1090
+ type: Input
1091
+ }], accept: [{
1092
+ type: Input
1093
+ }], fileSelected: [{
1094
+ type: Output
1095
+ }] } });
1096
+
1097
+ class TextInputComponent {
1098
+ // -------------------------------
1099
+ // INPUT PROPERTIES
1100
+ // -------------------------------
1101
+ inputLabel = ''; // Optional label displayed above input
1102
+ value = ''; // Two-way bound input value
1103
+ placeholder = 'Input value'; // Placeholder text
1104
+ type = ''; // Input type (text, email, etc.)
1105
+ isDisabled = false; // Disable the input
1106
+ helperText = ''; // Optional helper text below input
1107
+ errorText = ''; // Optional error message
1108
+ enableValidation = true; // Flag to enable/disable validation
1109
+ maxLength = 80; // Maximum characters allowed
1110
+ // -------------------------------
1111
+ // OUTPUT EVENTS
1112
+ // -------------------------------
1113
+ valueChange = new EventEmitter(); // Emit input changes to parent
1114
+ // -------------------------------
1115
+ // INTERNAL STATE
1116
+ // -------------------------------
1117
+ isTouched = false; // Tracks if user has interacted with input
1118
+ isInvalid = false; // True if validation fails
1119
+ charCount = 0; // Tracks current character count
1120
+ isExceeded = false; // True if charCount exceeds maxLength
1121
+ // -------------------------------
1122
+ // EVENT HANDLERS
1123
+ // -------------------------------
1124
+ onInputChange(event) {
1125
+ this.value = event.target.value; // Update value
1126
+ const inputValue = event.target.value;
1127
+ const charCount = inputValue.length;
1128
+ // Track if max length exceeded
1129
+ this.isExceeded = charCount > this.maxLength;
1130
+ this.isTouched = true; // Mark input as touched
1131
+ this.valueChange.emit(this.value); // Notify parent
1132
+ this.checkValidity(); // Validate input
1133
+ }
1134
+ onBlur() {
1135
+ this.isTouched = true; // Input lost focus → mark as touched
1136
+ this.checkValidity(); // Validate when blurred
1137
+ }
1138
+ // -------------------------------
1139
+ // HELPER METHODS
1140
+ // -------------------------------
1141
+ getCharCountStyle() {
1142
+ const charCount = this.value.length;
1143
+ if (this.isExceeded) {
1144
+ return 'char-count-error'; // Apply error style if limit exceeded
1145
+ }
1146
+ // Optional: style differently if more than 50% of maxLength used
1147
+ if (charCount > this.maxLength / 2) {
1148
+ return '';
1149
+ }
1150
+ return ''; // Default style
1151
+ }
1152
+ checkValidity() {
1153
+ // If touched and value is empty, mark as invalid
1154
+ this.isInvalid = this.isTouched && !this.value.trim();
1155
+ }
1156
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1157
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TextInputComponent, isStandalone: true, selector: "dso-input-text", inputs: { inputLabel: "inputLabel", value: "value", placeholder: "placeholder", type: "type", isDisabled: "isDisabled", helperText: "helperText", errorText: "errorText", enableValidation: "enableValidation", maxLength: "maxLength" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"input-container\">\r\n <!-- Optional label -->\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }} \r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div> \r\n <input\r\n [type]=\"type\" \r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\" \r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\" \r\n />\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n\r\n", styles: [":host{display:inline-block}.input-container{width:500px;display:flex;flex-direction:column;gap:8px}.text-wrapper{display:flex;justify-content:space-between}input{width:100%;height:48px;padding:12px 8px;font-size:16px;border:1px solid #ccc;border-radius:4px}input:hover{border:1px solid #071d35}input:focus{border-color:#007bff;outline:none}input.invalid{border-color:red}.error-message{color:red;font-size:16px}input.disabled{cursor:not-allowed;background-color:#f0f0f0;border:1px solid #ddd;color:#000}.helper-text{font-size:16px;color:#6c757d}.error-message{font-size:16px;color:red}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
1158
+ }
1159
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextInputComponent, decorators: [{
1160
+ type: Component,
1161
+ args: [{ selector: 'dso-input-text', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"input-container\">\r\n <!-- Optional label -->\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }} \r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div> \r\n <input\r\n [type]=\"type\" \r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\" \r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\" \r\n />\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n\r\n", styles: [":host{display:inline-block}.input-container{width:500px;display:flex;flex-direction:column;gap:8px}.text-wrapper{display:flex;justify-content:space-between}input{width:100%;height:48px;padding:12px 8px;font-size:16px;border:1px solid #ccc;border-radius:4px}input:hover{border:1px solid #071d35}input:focus{border-color:#007bff;outline:none}input.invalid{border-color:red}.error-message{color:red;font-size:16px}input.disabled{cursor:not-allowed;background-color:#f0f0f0;border:1px solid #ddd;color:#000}.helper-text{font-size:16px;color:#6c757d}.error-message{font-size:16px;color:red}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}\n"] }]
1162
+ }], propDecorators: { inputLabel: [{
1163
+ type: Input
1164
+ }], value: [{
1165
+ type: Input
1166
+ }], placeholder: [{
1167
+ type: Input
1168
+ }], type: [{
1169
+ type: Input
1170
+ }], isDisabled: [{
1171
+ type: Input
1172
+ }], helperText: [{
1173
+ type: Input
1174
+ }], errorText: [{
1175
+ type: Input
1176
+ }], enableValidation: [{
1177
+ type: Input
1178
+ }], maxLength: [{
1179
+ type: Input
1180
+ }], valueChange: [{
1181
+ type: Output
1182
+ }] } });
1183
+
1184
+ class PaginationComponent {
1185
+ /* =======================
1186
+ Inputs (controlled state)
1187
+ ======================= */
1188
+ page = 1;
1189
+ pageSize = 10;
1190
+ totalItems = 0;
1191
+ pageSizes = [10, 20, 50, 100];
1192
+ /* =======================
1193
+ Outputs (user intent)
1194
+ ======================= */
1195
+ pageChange = new EventEmitter();
1196
+ pageSizeChange = new EventEmitter();
1197
+ /* =======================
1198
+ Internal UI state
1199
+ ======================= */
1200
+ pageNumbers = [];
1201
+ jumpPageNumber = null;
1202
+ pageSizeOptions = [];
1203
+ /* =======================
1204
+ Lifecycle
1205
+ ======================= */
1206
+ ngOnChanges() {
1207
+ this.buildPageSizeOptions();
1208
+ this.generatePageNumbers();
1209
+ }
1210
+ /* =======================
1211
+ Derived values
1212
+ ======================= */
1213
+ get totalPages() {
1214
+ return Math.max(Math.ceil(this.totalItems / this.pageSize), 1);
1215
+ }
1216
+ get startItem() {
1217
+ return this.totalItems === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
1218
+ }
1219
+ get endItem() {
1220
+ return Math.min(this.page * this.pageSize, this.totalItems);
1221
+ }
1222
+ /* =======================
1223
+ UI helpers
1224
+ ======================= */
1225
+ buildPageSizeOptions() {
1226
+ this.pageSizeOptions = this.pageSizes.map(size => ({
1227
+ label: `${size} per page`,
1228
+ value: size
1229
+ }));
1230
+ }
1231
+ generatePageNumbers() {
1232
+ const total = this.totalPages;
1233
+ let start = Math.max(this.page - 2, 1);
1234
+ let end = Math.min(start + 4, total);
1235
+ start = Math.max(end - 4, 1);
1236
+ this.pageNumbers = [];
1237
+ for (let i = start; i <= end; i++) {
1238
+ this.pageNumbers.push(i);
1239
+ }
1240
+ }
1241
+ /* =======================
1242
+ Navigation (emit only)
1243
+ ======================= */
1244
+ goToPage(page) {
1245
+ if (page >= 1 && page <= this.totalPages && page !== this.page) {
1246
+ this.pageChange.emit(page);
1247
+ }
1248
+ }
1249
+ goFirst() {
1250
+ this.goToPage(1);
1251
+ }
1252
+ goPrevious() {
1253
+ this.goToPage(this.page - 1);
1254
+ }
1255
+ goNext() {
1256
+ this.goToPage(this.page + 1);
1257
+ }
1258
+ goLast() {
1259
+ this.goToPage(this.totalPages);
1260
+ }
1261
+ onPageSizeChange(newSize) {
1262
+ this.pageSizeChange.emit(newSize);
1263
+ }
1264
+ jumpToPage() {
1265
+ if (!this.jumpPageNumber)
1266
+ return;
1267
+ const page = Math.max(1, Math.min(this.jumpPageNumber, this.totalPages));
1268
+ this.pageChange.emit(page);
1269
+ this.jumpPageNumber = null;
1270
+ }
1271
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaginationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1272
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: PaginationComponent, isStandalone: true, selector: "dso-pagination", inputs: { page: "page", pageSize: "pageSize", totalItems: "totalItems", pageSizes: "pageSizes" }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"pagination-container\">\r\n\r\n <!-- First / Previous -->\r\n <dso-button\r\n btnLabel=\"\u00AB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goFirst()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u2039\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goPrevious()\">\r\n </dso-button>\r\n\r\n <!-- Page numbers -->\r\n <div class=\"page-buttons\">\r\n <ng-container *ngFor=\"let p of pageNumbers\">\r\n <dso-button\r\n [btnLabel]=\"p.toString()\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"page === p\"\r\n [isActive]=\"page === p\"\r\n (onClick)=\"goToPage(p)\">\r\n </dso-button>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Next / Last -->\r\n <dso-button\r\n btnLabel=\"\u203A\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goNext()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u00BB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goLast()\">\r\n </dso-button>\r\n\r\n <!-- Page size -->\r\n <dso-dropdown-list\r\n [options]=\"pageSizeOptions\"\r\n [value]=\"pageSize\"\r\n (selectionChange)=\"onPageSizeChange($event)\">\r\n </dso-dropdown-list>\r\n\r\n <!-- Range info -->\r\n <span class=\"range-info\">\r\n Showing {{ startItem }}\u2013{{ endItem }} of {{ totalItems }} results\r\n </span>\r\n\r\n <!-- Jump to page -->\r\n <div class=\"jump-to-page\">\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [(ngModel)]=\"jumpPageNumber\"\r\n (keydown.enter)=\"jumpToPage()\" />\r\n\r\n <dso-button\r\n btnLabel=\"Go\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"!jumpPageNumber\"\r\n (onClick)=\"jumpToPage()\">\r\n </dso-button>\r\n </div>\r\n\r\n</div>\r\n", styles: [".pagination-container{display:flex;align-items:center;gap:8px;font-size:14px}.range-info{margin-right:12px}button{padding:6px 10px;border-radius:4px;border:1px solid #ccc;background:#f0f0f0;cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button.page-number.active{background-color:#007bff;color:#fff;font-weight:700}.page-buttons{display:flex;gap:4px}.page-buttons dso-button{flex:0 0 44px;text-align:center}.jump-to-page{display:flex;align-items:center;gap:4px}.jump-to-page input{width:48px;padding:6px 8px;border-radius:4px;border:1px solid #ccc;text-align:center}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: DropdownListComponent, selector: "dso-dropdown-list", inputs: ["options", "placeholder", "value", "disabled"], outputs: ["selectionChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
1273
+ }
1274
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PaginationComponent, decorators: [{
1275
+ type: Component,
1276
+ args: [{ selector: 'dso-pagination', standalone: true, imports: [ButtonComponent, CommonModule, DropdownListComponent, FormsModule], template: "<div class=\"pagination-container\">\r\n\r\n <!-- First / Previous -->\r\n <dso-button\r\n btnLabel=\"\u00AB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goFirst()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u2039\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === 1\"\r\n (onClick)=\"goPrevious()\">\r\n </dso-button>\r\n\r\n <!-- Page numbers -->\r\n <div class=\"page-buttons\">\r\n <ng-container *ngFor=\"let p of pageNumbers\">\r\n <dso-button\r\n [btnLabel]=\"p.toString()\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"page === p\"\r\n [isActive]=\"page === p\"\r\n (onClick)=\"goToPage(p)\">\r\n </dso-button>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Next / Last -->\r\n <dso-button\r\n btnLabel=\"\u203A\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goNext()\">\r\n </dso-button>\r\n\r\n <dso-button\r\n btnLabel=\"\u00BB\"\r\n btnType=\"outlined\"\r\n [isDisabled]=\"page === totalPages\"\r\n (onClick)=\"goLast()\">\r\n </dso-button>\r\n\r\n <!-- Page size -->\r\n <dso-dropdown-list\r\n [options]=\"pageSizeOptions\"\r\n [value]=\"pageSize\"\r\n (selectionChange)=\"onPageSizeChange($event)\">\r\n </dso-dropdown-list>\r\n\r\n <!-- Range info -->\r\n <span class=\"range-info\">\r\n Showing {{ startItem }}\u2013{{ endItem }} of {{ totalItems }} results\r\n </span>\r\n\r\n <!-- Jump to page -->\r\n <div class=\"jump-to-page\">\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [(ngModel)]=\"jumpPageNumber\"\r\n (keydown.enter)=\"jumpToPage()\" />\r\n\r\n <dso-button\r\n btnLabel=\"Go\"\r\n btnType=\"ghost\"\r\n btnSize=\"mdBtn\"\r\n [isDisabled]=\"!jumpPageNumber\"\r\n (onClick)=\"jumpToPage()\">\r\n </dso-button>\r\n </div>\r\n\r\n</div>\r\n", styles: [".pagination-container{display:flex;align-items:center;gap:8px;font-size:14px}.range-info{margin-right:12px}button{padding:6px 10px;border-radius:4px;border:1px solid #ccc;background:#f0f0f0;cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button.page-number.active{background-color:#007bff;color:#fff;font-weight:700}.page-buttons{display:flex;gap:4px}.page-buttons dso-button{flex:0 0 44px;text-align:center}.jump-to-page{display:flex;align-items:center;gap:4px}.jump-to-page input{width:48px;padding:6px 8px;border-radius:4px;border:1px solid #ccc;text-align:center}input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}\n"] }]
1277
+ }], propDecorators: { page: [{
1278
+ type: Input
1279
+ }], pageSize: [{
1280
+ type: Input
1281
+ }], totalItems: [{
1282
+ type: Input
1283
+ }], pageSizes: [{
1284
+ type: Input
1285
+ }], pageChange: [{
1286
+ type: Output
1287
+ }], pageSizeChange: [{
1288
+ type: Output
1289
+ }] } });
1290
+
1291
+ class RadioComponent {
1292
+ label = 'Radio label';
1293
+ value;
1294
+ name; // for grouping radios
1295
+ disabled = false;
1296
+ error = false;
1297
+ isChecked = false;
1298
+ change = new EventEmitter();
1299
+ // Method to handle change in radio state
1300
+ toggleRadio(event) {
1301
+ // console.log(this.value);
1302
+ event.stopPropagation();
1303
+ this.change.emit(this.value); // Emit the updated value to the parent
1304
+ }
1305
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RadioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1306
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: RadioComponent, isStandalone: true, selector: "dso-radio", inputs: { label: "label", value: "value", name: "name", disabled: "disabled", error: "error", isChecked: "isChecked" }, outputs: { change: "change" }, ngImport: i0, template: "<label class=\"radio-wrapper\" \r\n[class.disabled]=\"disabled\" \r\n[class.error]=\"error\">\r\n <input\r\n class=\"radio-input\"\r\n type=\"radio\"\r\n [attr.name]=\"name\"\r\n [value]=\"value\"\r\n [checked]=\"isChecked\"\r\n [disabled]=\"disabled\"\r\n (change)=\"toggleRadio($event)\"\r\n />\r\n <span class=\"custom-radio\"></span>\r\n <span *ngIf=\"label\" class=\"radio-label\">{{ label }}</span>\r\n</label>\r\n", styles: [".radio-wrapper{display:flex;align-items:center;gap:8px;cursor:pointer}.radio-wrapper.disabled{cursor:not-allowed}.radio-wrapper.disabled .custom-radio{background-color:#f0f0f0;border-color:#d0d0d0}.radio-wrapper.disabled .radio-input:checked~.custom-radio{border:1px solid rgba(0,0,255,.5)}.radio-wrapper.disabled .custom-radio:after{opacity:.5}.radio-input{position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-radio{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #666666;border-radius:50%}.radio-input:checked~.custom-radio{border:1px solid blue}input[type=radio]:checked+.custom-radio:after{display:block;content:\"\";width:100%;height:100%;background:#00f;border:5px solid blue;background:transparent;box-sizing:border-box;border-radius:50%}.radio-label{font-size:14px}.error .custom-radio{border-color:red}.radio-wrapper:not(.disabled):hover .custom-radio{background-color:#eef5f9;border-color:#0000ffc0;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
1307
+ }
1308
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: RadioComponent, decorators: [{
1309
+ type: Component,
1310
+ args: [{ selector: 'dso-radio', standalone: true, imports: [FormsModule, CommonModule], template: "<label class=\"radio-wrapper\" \r\n[class.disabled]=\"disabled\" \r\n[class.error]=\"error\">\r\n <input\r\n class=\"radio-input\"\r\n type=\"radio\"\r\n [attr.name]=\"name\"\r\n [value]=\"value\"\r\n [checked]=\"isChecked\"\r\n [disabled]=\"disabled\"\r\n (change)=\"toggleRadio($event)\"\r\n />\r\n <span class=\"custom-radio\"></span>\r\n <span *ngIf=\"label\" class=\"radio-label\">{{ label }}</span>\r\n</label>\r\n", styles: [".radio-wrapper{display:flex;align-items:center;gap:8px;cursor:pointer}.radio-wrapper.disabled{cursor:not-allowed}.radio-wrapper.disabled .custom-radio{background-color:#f0f0f0;border-color:#d0d0d0}.radio-wrapper.disabled .radio-input:checked~.custom-radio{border:1px solid rgba(0,0,255,.5)}.radio-wrapper.disabled .custom-radio:after{opacity:.5}.radio-input{position:absolute;width:1px;height:1px;margin:-1px;border:0;padding:0;clip:rect(0 0 0 0);clip-path:inset(100%);overflow:hidden;white-space:nowrap}.custom-radio{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid #666666;border-radius:50%}.radio-input:checked~.custom-radio{border:1px solid blue}input[type=radio]:checked+.custom-radio:after{display:block;content:\"\";width:100%;height:100%;background:#00f;border:5px solid blue;background:transparent;box-sizing:border-box;border-radius:50%}.radio-label{font-size:14px}.error .custom-radio{border-color:red}.radio-wrapper:not(.disabled):hover .custom-radio{background-color:#eef5f9;border-color:#0000ffc0;cursor:pointer}\n"] }]
1311
+ }], propDecorators: { label: [{
1312
+ type: Input
1313
+ }], value: [{
1314
+ type: Input
1315
+ }], name: [{
1316
+ type: Input
1317
+ }], disabled: [{
1318
+ type: Input
1319
+ }], error: [{
1320
+ type: Input
1321
+ }], isChecked: [{
1322
+ type: Input
1323
+ }], change: [{
1324
+ type: Output
1325
+ }] } });
1326
+
1327
+ class SingleSelectComponent {
1328
+ elementRef;
1329
+ // Reference to the dropdown <ul> for scroll management
1330
+ dropdownListRef;
1331
+ // Inputs for configuration & data binding
1332
+ options = [];
1333
+ dropdownPosition = 'bottom';
1334
+ placeholder = 'Select an option';
1335
+ inputTextLabel = 'Dropdown Title'; // Optional input label
1336
+ isDisabled = false; // Controls disabled state
1337
+ enableValidation = true; // Enables validation checks
1338
+ helperText; // Optional helper text
1339
+ selectIconName = null; // Icon name/class for clear button
1340
+ errorText = 'This field is required'; // Validation error text
1341
+ // Output event emitter to notify selection changes
1342
+ selectionChange = new EventEmitter();
1343
+ // Internal state properties
1344
+ selectedOption = null; // Currently selected option object
1345
+ searchQuery = ''; // Search input value
1346
+ filteredOptions = []; // Options filtered based on search
1347
+ isOpen = false; // Dropdown open/close state
1348
+ isInvalid = false; // Validation invalid flag
1349
+ isExceeded = false; // (Optional) exceeded limit flag, currently unused
1350
+ hasOpened = false;
1351
+ hasInteracted = false; // (Optional) touched flag, currently unused
1352
+ lastScrollTop = 0; // Stores last scroll position of dropdown list
1353
+ constructor(elementRef) {
1354
+ this.elementRef = elementRef;
1355
+ }
1356
+ /**
1357
+ * //Handles dropdown scroll events to persist scroll position
1358
+ */
1359
+ onDropdownScroll() {
1360
+ if (this.dropdownListRef) {
1361
+ this.lastScrollTop = this.dropdownListRef.nativeElement.scrollTop;
1362
+ console.log(`Dropdown scrollTop updated to: ${this.lastScrollTop}`);
1363
+ }
1364
+ else {
1365
+ console.warn('dropdownListRef is undefined on scroll');
1366
+ }
1367
+ }
1368
+ /**
1369
+ * Detects clicks outside of the component to close the dropdown if open
1370
+ */
1371
+ onOutsideClick(event) {
1372
+ const clickedInside = this.elementRef.nativeElement.contains(event.target);
1373
+ if (!clickedInside) {
1374
+ if (this.isOpen) {
1375
+ this.isOpen = false; // Close dropdown if clicked outside
1376
+ console.log('Dropdown closed via outside click');
1377
+ this.hasInteracted = true;
1378
+ // Validate only if nothing is selected
1379
+ if (this.enableValidation && !this.selectedOption) {
1380
+ this.isInvalid = true;
1381
+ console.log('Validation failed: no option selected after outside click');
1382
+ }
1383
+ else {
1384
+ this.isInvalid = false;
1385
+ }
1386
+ }
1387
+ }
1388
+ }
1389
+ /**
1390
+ * Initialize component data on creation
1391
+ */
1392
+ ngOnInit() {
1393
+ // Sample hardcoded options (can be overridden via @Input)
1394
+ this.options = [
1395
+ { value: '1', label: 'Apple' },
1396
+ { value: '2', label: 'Banana' },
1397
+ { value: '3', label: 'Cherry' },
1398
+ { value: '4', label: 'Date' },
1399
+ { value: '5', label: 'Grapes' },
1400
+ { value: '6', label: 'AAAA' },
1401
+ { value: '7', label: 'BBBB' },
1402
+ { value: '8', label: 'CCCC' },
1403
+ { value: '9', label: 'DDDD' },
1404
+ ];
1405
+ // Show all options initially
1406
+ this.filteredOptions = this.options;
1407
+ }
1408
+ ngOnDestroy() {
1409
+ // Placeholder for any cleanup logic if needed
1410
+ }
1411
+ /**
1412
+ * Handles click on the main select box
1413
+ * Prevents opening if disabled
1414
+ */
1415
+ onSelectClick(event) {
1416
+ event.stopPropagation();
1417
+ if (this.isDisabled) {
1418
+ event.stopPropagation();
1419
+ event.preventDefault();
1420
+ return;
1421
+ }
1422
+ this.toggleDropdown(event);
1423
+ }
1424
+ /**
1425
+ * Toggles dropdown open/close state and restores scroll position if opening
1426
+ */
1427
+ toggleDropdown(event) {
1428
+ if (this.isDisabled) {
1429
+ event.stopPropagation();
1430
+ // console.warn('Dropdown is disabled, toggle prevented.');
1431
+ return;
1432
+ }
1433
+ this.isOpen = !this.isOpen;
1434
+ this.hasInteracted = true;
1435
+ // console.log(`Dropdown toggled. isOpen: ${this.isOpen}`);
1436
+ if (this.isOpen) {
1437
+ this.hasOpened = true;
1438
+ setTimeout(() => {
1439
+ if (this.dropdownListRef) {
1440
+ // console.log(`Restoring scrollTop to: ${this.lastScrollTop}`);
1441
+ this.dropdownListRef.nativeElement.scrollTop = this.lastScrollTop;
1442
+ console.log('Scroll Restored');
1443
+ }
1444
+ });
1445
+ }
1446
+ else {
1447
+ // Validate on close
1448
+ this.hasInteracted = true;
1449
+ console.log('User has interacted and closed the dropdown');
1450
+ if (this.enableValidation && !this.selectedOption) {
1451
+ this.isInvalid = true;
1452
+ console.log('No option selected — validation failed');
1453
+ }
1454
+ else {
1455
+ this.isInvalid = false;
1456
+ }
1457
+ }
1458
+ }
1459
+ /**
1460
+ * Filters dropdown options based on current search query
1461
+ */
1462
+ filterOptions() {
1463
+ const query = this.searchQuery.toLowerCase();
1464
+ this.filteredOptions = this.options.filter((option) => option.label.toLowerCase().includes(query));
1465
+ }
1466
+ /**
1467
+ * Handles option selection:
1468
+ * - Updates selected option
1469
+ * - Validates
1470
+ * - Emits selection change event
1471
+ * - Closes dropdown
1472
+ */
1473
+ selectOption(event, option) {
1474
+ event.stopPropagation();
1475
+ this.selectedOption = option;
1476
+ this.isInvalid = false;
1477
+ this.selectionChange.emit(this.selectedOption);
1478
+ this.isOpen = false;
1479
+ }
1480
+ /**
1481
+ * Clears the selected option and resets validation and scroll
1482
+ */
1483
+ clearSelection(event) {
1484
+ // console.log('Clear selection triggered');
1485
+ // this.hasInteracted = false;
1486
+ this.selectedOption = null;
1487
+ this.searchQuery = ''; // Clear the search input field as well
1488
+ this.filteredOptions = this.options; // Show all options again
1489
+ this.isInvalid = false;
1490
+ this.lastScrollTop = 0;
1491
+ this.selectionChange.emit(this.selectedOption);
1492
+ }
1493
+ /**
1494
+ * Checks validity based on current selection if validation is enabled
1495
+ */
1496
+ checkValidity() {
1497
+ if (this.enableValidation) {
1498
+ this.isInvalid = !this.selectedOption;
1499
+ }
1500
+ }
1501
+ /**
1502
+ * Returns the icon path if icon name is provided
1503
+ */
1504
+ getIconPath() {
1505
+ if (this.selectIconName) {
1506
+ return `./assets/icons/${this.selectIconName}.svg`;
1507
+ }
1508
+ return '';
1509
+ }
1510
+ showError() {
1511
+ return this.enableValidation && this.hasInteracted && !this.selectedOption;
1512
+ }
1513
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SingleSelectComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
1514
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SingleSelectComponent, isStandalone: true, selector: "dso-single-select", inputs: { options: "options", dropdownPosition: "dropdownPosition", placeholder: "placeholder", inputTextLabel: "inputTextLabel", isDisabled: "isDisabled", enableValidation: "enableValidation", helperText: "helperText", selectIconName: "selectIconName", errorText: "errorText" }, outputs: { selectionChange: "selectionChange" }, host: { listeners: { "document:click": "onOutsideClick($event)" } }, viewQueries: [{ propertyName: "dropdownListRef", first: true, predicate: ["dropdownList"], descendants: true }], ngImport: i0, template: "<div class=\"select-wrapper\">\r\n<!-- Label (optional) -->\r\n<div *ngIf=\"inputTextLabel\" class=\"select-label\">{{ inputTextLabel }}</div>\r\n \r\n<!-- <div class=\"select-container\" (click)=\"toggleDropdown($event)\" [class.disabled]=\"isDisabled\"> -->\r\n<div class=\"select-container\" [class.disabled]=\"isDisabled\" [class.error]=\"enableValidation && hasInteracted && isInvalid\" [class.focused]=\"isOpen\"\r\n > \r\n <div class=\"selected-option\"\r\n (click)=\"onSelectClick($event)\" \r\n [ngClass]=\"{'placeholder': !selectedOption, \r\n 'selected': selectedOption }\">\r\n <span>{{ selectedOption?.label || placeholder }}</span>\r\n <div class=\"clearbtn-wrapper\"> \r\n <dso-button \r\n btnLabel=\"Clear\" \r\n btnType=\"ghost\" \r\n btnSize='smBtn'\r\n *ngIf=\"selectedOption && !isDisabled\" \r\n (click)=\"clearSelection($event)\"></dso-button>\r\n <dso-icon *ngIf=\"selectIconName\" [iconName]=\"selectIconName\" [size]=\"'small'\"></dso-icon>\r\n </div>\r\n </div>\r\n\r\n <!-- Dropdown -->\r\n <div class=\"dropdown\" *ngIf=\"isOpen\" \r\n (click)=\"$event.stopPropagation()\" \r\n [ngClass]=\"{'dropdown-bottom': dropdownPosition === 'bottom', 'dropdown-top': dropdownPosition === 'top'}\" >\r\n <input type=\"text\" \r\n [(ngModel)]=\"searchQuery\" \r\n (input)=\"filterOptions()\" \r\n placeholder=\"Search...\" />\r\n <ul #dropdownList \r\n (scroll)=\"onDropdownScroll()\" \r\n class=\"dropdown-list\">\r\n <li *ngFor=\"let option of filteredOptions\" (click)=\"selectOption($event, option)\" [ngClass]=\"{'active': option.value === selectedOption?.value}\">\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n </div>\r\n</div>\r\n\r\n\r\n<!-- Helper Text (when validation is disabled) -->\r\n<div *ngIf=\"enableValidation && helperText && !(hasInteracted && isInvalid)\" class=\"helper-text\">\r\n {{ helperText }}\r\n</div>\r\n\r\n<!-- Error Message (when validation is enabled) -->\r\n<div *ngIf=\"enableValidation && hasInteracted && isInvalid\" class=\"error-message\">\r\n {{ errorText }}\r\n</div>\r\n</div>", styles: [":host{display:inline-block;font-family:inherit}.select-label{font-size:16px;color:#333;display:block}.select-wrapper{display:flex;flex-direction:column;gap:8px}.select-container{position:relative;width:300px;display:flex;flex-direction:column;border:1px solid #ccc;border-radius:4px;background-color:#fff;box-sizing:border-box}.select-container:hover{border:1px solid #071d35}.select-container.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.select-container.disabled .selected-option{pointer-events:none;color:#aaa}.select-container.error{border-color:red}.select-container.focused{border-color:#007bff}.dropdown-list{max-height:150px;overflow-y:auto}.selected-option{padding:12px 8px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}.selected-option.placeholder{color:#aaa}.selected-option.selected{color:#000}img.button-icon{width:20px;height:20px}.clearbtn-wrapper{display:flex;gap:4px;justify-content:space-between;align-items:center}.dropdown{width:300px;position:absolute;background-color:#fff;border:1px solid #ccc;max-height:200px;overflow-y:auto;box-shadow:0 4px 6px #0000000d}.dropdown input{width:100%;padding:10px;box-sizing:border-box;border:none;border-bottom:1px solid #eee;outline:none}.dropdown ul{list-style:none;padding:0;margin:0}.dropdown ul li{padding:8px 12px;cursor:pointer;transition:background-color .2s}.dropdown ul li:hover{background-color:#f0f0f0}.dropdown ul li.active{background-color:#3784d6;color:#fff;font-weight:700}.dropdown-bottom{top:100%}.dropdown-top{bottom:100%}.helper-text,.error-message{font-size:16px}.helper-text{color:#888}.error-message{color:red}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
1515
+ }
1516
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SingleSelectComponent, decorators: [{
1517
+ type: Component,
1518
+ args: [{ selector: 'dso-single-select', standalone: true, imports: [FormsModule, CommonModule, ButtonComponent, IconComponent], template: "<div class=\"select-wrapper\">\r\n<!-- Label (optional) -->\r\n<div *ngIf=\"inputTextLabel\" class=\"select-label\">{{ inputTextLabel }}</div>\r\n \r\n<!-- <div class=\"select-container\" (click)=\"toggleDropdown($event)\" [class.disabled]=\"isDisabled\"> -->\r\n<div class=\"select-container\" [class.disabled]=\"isDisabled\" [class.error]=\"enableValidation && hasInteracted && isInvalid\" [class.focused]=\"isOpen\"\r\n > \r\n <div class=\"selected-option\"\r\n (click)=\"onSelectClick($event)\" \r\n [ngClass]=\"{'placeholder': !selectedOption, \r\n 'selected': selectedOption }\">\r\n <span>{{ selectedOption?.label || placeholder }}</span>\r\n <div class=\"clearbtn-wrapper\"> \r\n <dso-button \r\n btnLabel=\"Clear\" \r\n btnType=\"ghost\" \r\n btnSize='smBtn'\r\n *ngIf=\"selectedOption && !isDisabled\" \r\n (click)=\"clearSelection($event)\"></dso-button>\r\n <dso-icon *ngIf=\"selectIconName\" [iconName]=\"selectIconName\" [size]=\"'small'\"></dso-icon>\r\n </div>\r\n </div>\r\n\r\n <!-- Dropdown -->\r\n <div class=\"dropdown\" *ngIf=\"isOpen\" \r\n (click)=\"$event.stopPropagation()\" \r\n [ngClass]=\"{'dropdown-bottom': dropdownPosition === 'bottom', 'dropdown-top': dropdownPosition === 'top'}\" >\r\n <input type=\"text\" \r\n [(ngModel)]=\"searchQuery\" \r\n (input)=\"filterOptions()\" \r\n placeholder=\"Search...\" />\r\n <ul #dropdownList \r\n (scroll)=\"onDropdownScroll()\" \r\n class=\"dropdown-list\">\r\n <li *ngFor=\"let option of filteredOptions\" (click)=\"selectOption($event, option)\" [ngClass]=\"{'active': option.value === selectedOption?.value}\">\r\n {{ option.label }}\r\n </li>\r\n </ul>\r\n </div>\r\n</div>\r\n\r\n\r\n<!-- Helper Text (when validation is disabled) -->\r\n<div *ngIf=\"enableValidation && helperText && !(hasInteracted && isInvalid)\" class=\"helper-text\">\r\n {{ helperText }}\r\n</div>\r\n\r\n<!-- Error Message (when validation is enabled) -->\r\n<div *ngIf=\"enableValidation && hasInteracted && isInvalid\" class=\"error-message\">\r\n {{ errorText }}\r\n</div>\r\n</div>", styles: [":host{display:inline-block;font-family:inherit}.select-label{font-size:16px;color:#333;display:block}.select-wrapper{display:flex;flex-direction:column;gap:8px}.select-container{position:relative;width:300px;display:flex;flex-direction:column;border:1px solid #ccc;border-radius:4px;background-color:#fff;box-sizing:border-box}.select-container:hover{border:1px solid #071d35}.select-container.disabled{background-color:#f5f5f5;border-color:#ccc;cursor:not-allowed}.select-container.disabled .selected-option{pointer-events:none;color:#aaa}.select-container.error{border-color:red}.select-container.focused{border-color:#007bff}.dropdown-list{max-height:150px;overflow-y:auto}.selected-option{padding:12px 8px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}.selected-option.placeholder{color:#aaa}.selected-option.selected{color:#000}img.button-icon{width:20px;height:20px}.clearbtn-wrapper{display:flex;gap:4px;justify-content:space-between;align-items:center}.dropdown{width:300px;position:absolute;background-color:#fff;border:1px solid #ccc;max-height:200px;overflow-y:auto;box-shadow:0 4px 6px #0000000d}.dropdown input{width:100%;padding:10px;box-sizing:border-box;border:none;border-bottom:1px solid #eee;outline:none}.dropdown ul{list-style:none;padding:0;margin:0}.dropdown ul li{padding:8px 12px;cursor:pointer;transition:background-color .2s}.dropdown ul li:hover{background-color:#f0f0f0}.dropdown ul li.active{background-color:#3784d6;color:#fff;font-weight:700}.dropdown-bottom{top:100%}.dropdown-top{bottom:100%}.helper-text,.error-message{font-size:16px}.helper-text{color:#888}.error-message{color:red}\n"] }]
1519
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { dropdownListRef: [{
1520
+ type: ViewChild,
1521
+ args: ['dropdownList']
1522
+ }], options: [{
1523
+ type: Input
1524
+ }], dropdownPosition: [{
1525
+ type: Input
1526
+ }], placeholder: [{
1527
+ type: Input
1528
+ }], inputTextLabel: [{
1529
+ type: Input
1530
+ }], isDisabled: [{
1531
+ type: Input
1532
+ }], enableValidation: [{
1533
+ type: Input
1534
+ }], helperText: [{
1535
+ type: Input
1536
+ }], selectIconName: [{
1537
+ type: Input
1538
+ }], errorText: [{
1539
+ type: Input
1540
+ }], selectionChange: [{
1541
+ type: Output
1542
+ }], onOutsideClick: [{
1543
+ type: HostListener,
1544
+ args: ['document:click', ['$event']]
1545
+ }] } });
1546
+
1547
+ /**
1548
+ * DSO Side Navigation Component
1549
+ *
1550
+ * Features:
1551
+ * - Configurable title/logo area.
1552
+ * - Dynamic menu items with icons and routing.
1553
+ * - Supports default active item by route or index.
1554
+ * - Click-outside detection to close the sidebar.
1555
+ * - Optional overlay for dimming the background.
1556
+ */
1557
+ class SideNavComponent {
1558
+ host;
1559
+ /** Title displayed next to the logo */
1560
+ title = '';
1561
+ /** List of navigation items */
1562
+ items = [];
1563
+ /** Controls whether the sidebar is visible */
1564
+ open = true;
1565
+ /** Enable/disable background overlay when sidebar is open */
1566
+ overlay = true;
1567
+ /** Optional: default active menu item by route */
1568
+ defaultActiveRoute;
1569
+ /** Optional: default active menu item by index */
1570
+ defaultActiveIndex;
1571
+ /** Event emitted when sidebar is closed */
1572
+ closed = new EventEmitter();
1573
+ /**
1574
+ * ElementRef to the component host element.
1575
+ * Used for detecting click-outside events.
1576
+ */
1577
+ constructor(host) {
1578
+ this.host = host;
1579
+ }
1580
+ /**
1581
+ * Lifecycle hook: initialize default active menu item.
1582
+ */
1583
+ ngOnInit() {
1584
+ // Reset all items to inactive first
1585
+ this.items.forEach(item => item.isActive = false);
1586
+ // Set default active item
1587
+ const defaultItem = this.defaultActiveRoute
1588
+ ? this.items.find(item => item.route === this.defaultActiveRoute)
1589
+ : this.items[this.defaultActiveIndex ?? 0]; // fallback to first item if no index
1590
+ if (defaultItem) {
1591
+ defaultItem.isActive = true;
1592
+ }
1593
+ }
1594
+ /**
1595
+ * Close the sidebar and emit the `closed` event.
1596
+ * Can be called manually or via click-outside detection.
1597
+ */
1598
+ closeSidebar() {
1599
+ this.open = false;
1600
+ this.closed.emit();
1601
+ }
1602
+ /**
1603
+ * HostListener to detect clicks anywhere in the document.
1604
+ * If the click happens outside the sidebar while it's open, it triggers closeSidebar().
1605
+ *
1606
+ * @param event MouseEvent triggered on document click
1607
+ */
1608
+ onDocumentClick(event) {
1609
+ if (!this.open)
1610
+ return;
1611
+ // Check if click was inside the sidebar
1612
+ const clickedInside = this.host.nativeElement.contains(event.target);
1613
+ if (!clickedInside) {
1614
+ this.closeSidebar();
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Called when a sidebar menu item is clicked.
1619
+ * - Marks the clicked item as active.
1620
+ * - Optionally, navigation can be handled via `[routerLink]`.
1621
+ *
1622
+ * @param item The menu item that was clicked
1623
+ */
1624
+ onItemClick(item) {
1625
+ this.items.forEach(i => i.isActive = false);
1626
+ item.isActive = true;
1627
+ }
1628
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SideNavComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
1629
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SideNavComponent, isStandalone: true, selector: "dso-side-nav", inputs: { title: "title", items: "items", open: "open", overlay: "overlay", defaultActiveRoute: "defaultActiveRoute", defaultActiveIndex: "defaultActiveIndex" }, outputs: { closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: "<!-- Optional overlay -->\r\n<div \r\n class=\"sidebar-overlay\" \r\n *ngIf=\"open && overlay\"\r\n (click)=\"closeSidebar()\">\r\n</div>\r\n\r\n<nav class=\"dso-side-nav\" [class.open]=\"open\">\r\n\r\n <!-- Logo and Title -->\r\n <div class=\"side-nav-header\">\r\n <ng-content select=\"[dso-side-logo]\"></ng-content>\r\n <span class=\"side-nav-title\">{{ title }}</span>\r\n </div>\r\n\r\n <!-- Navigation list -->\r\n <ul class=\"side-nav-list\">\r\n <li *ngFor=\"let item of items\" [class.active]=\"item.isActive\">\r\n <a\r\n class=\"nav-item\"\r\n [routerLink]=\"item.route\"\r\n routerLinkActive=\"active\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <!-- <i class=\"nav-icon\" [class]=\"item.icon\"></i> -->\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n\r\n <span class=\"nav-label\">{{ item.label }}</span>\r\n </a>\r\n </li>\r\n </ul>\r\n\r\n <!-- Footer slot (optional) -->\r\n <div class=\"side-nav-footer\">\r\n <ng-content select=\"[dso-side-footer]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: [".dso-side-nav{position:fixed;left:0;top:0;width:240px;height:100vh;background:#fff;border-right:1px solid #e5e5e5;display:flex;flex-direction:column;transform:translate(-100%);transition:transform .25s ease-in-out;z-index:1000}.dso-side-nav.open{transform:translate(0)}.dso-side-nav .side-nav-header{display:flex;align-items:center;gap:.75rem;padding:1rem;white-space:nowrap}.dso-side-nav .side-nav-title{font-size:1rem;font-weight:600}.dso-side-nav .side-nav-list{flex:1;list-style:none;padding:0 4px;margin:.5rem 0}.dso-side-nav .side-nav-list li{margin:.25rem 0}.dso-side-nav .side-nav-list li a.nav-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 1rem;border-radius:4px;text-decoration:none;color:#333}.dso-side-nav .side-nav-list li a.nav-item:hover{background:#f4f4f4}.dso-side-nav .side-nav-list li.active a.nav-item,.dso-side-nav .side-nav-list li a.nav-item.active{background:#e6f0ff;border-left:3px solid #007bff}.dso-side-nav .side-nav-list li.active a.nav-item .nav-label,.dso-side-nav .side-nav-list li a.nav-item.active .nav-label{font-weight:600}.dso-side-nav .side-nav-footer{padding:1rem;border-top:1px solid #e5e5e5}.sidebar-overlay{position:fixed;inset:0;background:#00000059;z-index:900}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i2$1.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
1630
+ }
1631
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SideNavComponent, decorators: [{
1632
+ type: Component,
1633
+ args: [{ selector: 'dso-side-nav', standalone: true, imports: [CommonModule, RouterModule, IconComponent], template: "<!-- Optional overlay -->\r\n<div \r\n class=\"sidebar-overlay\" \r\n *ngIf=\"open && overlay\"\r\n (click)=\"closeSidebar()\">\r\n</div>\r\n\r\n<nav class=\"dso-side-nav\" [class.open]=\"open\">\r\n\r\n <!-- Logo and Title -->\r\n <div class=\"side-nav-header\">\r\n <ng-content select=\"[dso-side-logo]\"></ng-content>\r\n <span class=\"side-nav-title\">{{ title }}</span>\r\n </div>\r\n\r\n <!-- Navigation list -->\r\n <ul class=\"side-nav-list\">\r\n <li *ngFor=\"let item of items\" [class.active]=\"item.isActive\">\r\n <a\r\n class=\"nav-item\"\r\n [routerLink]=\"item.route\"\r\n routerLinkActive=\"active\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <!-- <i class=\"nav-icon\" [class]=\"item.icon\"></i> -->\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n\r\n <span class=\"nav-label\">{{ item.label }}</span>\r\n </a>\r\n </li>\r\n </ul>\r\n\r\n <!-- Footer slot (optional) -->\r\n <div class=\"side-nav-footer\">\r\n <ng-content select=\"[dso-side-footer]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: [".dso-side-nav{position:fixed;left:0;top:0;width:240px;height:100vh;background:#fff;border-right:1px solid #e5e5e5;display:flex;flex-direction:column;transform:translate(-100%);transition:transform .25s ease-in-out;z-index:1000}.dso-side-nav.open{transform:translate(0)}.dso-side-nav .side-nav-header{display:flex;align-items:center;gap:.75rem;padding:1rem;white-space:nowrap}.dso-side-nav .side-nav-title{font-size:1rem;font-weight:600}.dso-side-nav .side-nav-list{flex:1;list-style:none;padding:0 4px;margin:.5rem 0}.dso-side-nav .side-nav-list li{margin:.25rem 0}.dso-side-nav .side-nav-list li a.nav-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 1rem;border-radius:4px;text-decoration:none;color:#333}.dso-side-nav .side-nav-list li a.nav-item:hover{background:#f4f4f4}.dso-side-nav .side-nav-list li.active a.nav-item,.dso-side-nav .side-nav-list li a.nav-item.active{background:#e6f0ff;border-left:3px solid #007bff}.dso-side-nav .side-nav-list li.active a.nav-item .nav-label,.dso-side-nav .side-nav-list li a.nav-item.active .nav-label{font-weight:600}.dso-side-nav .side-nav-footer{padding:1rem;border-top:1px solid #e5e5e5}.sidebar-overlay{position:fixed;inset:0;background:#00000059;z-index:900}\n"] }]
1634
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { title: [{
1635
+ type: Input
1636
+ }], items: [{
1637
+ type: Input
1638
+ }], open: [{
1639
+ type: Input
1640
+ }], overlay: [{
1641
+ type: Input
1642
+ }], defaultActiveRoute: [{
1643
+ type: Input
1644
+ }], defaultActiveIndex: [{
1645
+ type: Input
1646
+ }], closed: [{
1647
+ type: Output
1648
+ }], onDocumentClick: [{
1649
+ type: HostListener,
1650
+ args: ['document:click', ['$event']]
1651
+ }] } });
1652
+
1653
+ class SpinnerComponent {
1654
+ el;
1655
+ /** Size presets */
1656
+ size = 'medium';
1657
+ /** Color variants */
1658
+ color = 'primary';
1659
+ /** Overlay background toggle */
1660
+ overlay = false;
1661
+ /** Custom overlay background color */
1662
+ overlayColor = 'rgba(0,0,0,0.4)';
1663
+ /** Optional illustration (inside spinner) */
1664
+ illustrationSrc;
1665
+ /** Optional label text (below spinner) */
1666
+ label;
1667
+ // NEW: optional Lottie animation
1668
+ animationSrc;
1669
+ constructor(el) {
1670
+ this.el = el;
1671
+ }
1672
+ ngAfterViewInit() {
1673
+ if (this.animationSrc) {
1674
+ lottie.loadAnimation({
1675
+ container: this.el.nativeElement.querySelector('.spinner-lottie'),
1676
+ renderer: 'svg',
1677
+ loop: true,
1678
+ autoplay: true,
1679
+ path: this.animationSrc, // path to your JSON animation
1680
+ });
1681
+ }
1682
+ }
1683
+ get isIllustrated() {
1684
+ return !!this.illustrationSrc;
1685
+ }
1686
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpinnerComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
1687
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SpinnerComponent, isStandalone: true, selector: "dso-spinner", inputs: { size: "size", color: "color", overlay: "overlay", overlayColor: "overlayColor", illustrationSrc: "illustrationSrc", label: "label", animationSrc: "animationSrc" }, ngImport: i0, template: "<div *ngIf=\"overlay\" class=\"spinner-overlay\" [ngStyle]=\"{'background-color': overlayColor}\">\r\n <div class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n </div>\r\n</div>\r\n\r\n<div *ngIf=\"!overlay\" class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n</div>\r\n", styles: [".spinner-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;display:flex;justify-content:center;align-items:center;z-index:9999}.spinner-wrapper{display:flex;flex-direction:column;align-items:center;gap:8px}.spinner{position:relative;display:flex;justify-content:center;align-items:center}.spinner:before{content:\"\";position:absolute;inset:0;border-radius:50%;border:3px solid rgba(0,0,0,.1);border-top-color:#007bff;animation:spin .8s linear infinite}.spinner.small{width:32px;height:32px}.spinner.small:before{border-width:2px}.spinner.medium{width:48px;height:48px}.spinner.medium:before{border-width:3px}.spinner.large{width:72px;height:72px}.spinner.large:before{border-width:4px}.spinner.illustrated{width:240px;height:240px}.spinner.illustrated:before{border-width:6px}.spinner.illustrated .spinner-illustration{width:180px;height:180px}.spinner.primary:before{border-top-color:#007bff}.spinner.success:before{border-top-color:#28a745}.spinner.warning:before{border-top-color:#ffc107}.spinner.danger:before{border-top-color:#dc3545}.spinner-illustration{object-fit:contain;z-index:1}.spinner-label{font-size:14px;color:#fff;text-align:center}@keyframes spin{to{transform:rotate(360deg)}}.spinner-lottie{width:80px;height:80px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
1688
+ }
1689
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpinnerComponent, decorators: [{
1690
+ type: Component,
1691
+ args: [{ selector: 'dso-spinner', standalone: true, imports: [CommonModule], template: "<div *ngIf=\"overlay\" class=\"spinner-overlay\" [ngStyle]=\"{'background-color': overlayColor}\">\r\n <div class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n </div>\r\n</div>\r\n\r\n<div *ngIf=\"!overlay\" class=\"spinner-wrapper\">\r\n \r\n <!-- Static spinner / illustration -->\r\n <div \r\n class=\"spinner\" \r\n *ngIf=\"!animationSrc\"\r\n [ngClass]=\"[isIllustrated ? 'illustrated' : size, color]\">\r\n <img *ngIf=\"illustrationSrc\" [src]=\"illustrationSrc\" class=\"spinner-illustration\" alt=\"Loading illustration\" />\r\n </div>\r\n\r\n <!-- Lottie animation -->\r\n <div *ngIf=\"animationSrc\" class=\"spinner-lottie\"></div>\r\n\r\n <!-- Optional label -->\r\n <p *ngIf=\"label\" class=\"spinner-label\">{{ label }}</p>\r\n</div>\r\n", styles: [".spinner-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;display:flex;justify-content:center;align-items:center;z-index:9999}.spinner-wrapper{display:flex;flex-direction:column;align-items:center;gap:8px}.spinner{position:relative;display:flex;justify-content:center;align-items:center}.spinner:before{content:\"\";position:absolute;inset:0;border-radius:50%;border:3px solid rgba(0,0,0,.1);border-top-color:#007bff;animation:spin .8s linear infinite}.spinner.small{width:32px;height:32px}.spinner.small:before{border-width:2px}.spinner.medium{width:48px;height:48px}.spinner.medium:before{border-width:3px}.spinner.large{width:72px;height:72px}.spinner.large:before{border-width:4px}.spinner.illustrated{width:240px;height:240px}.spinner.illustrated:before{border-width:6px}.spinner.illustrated .spinner-illustration{width:180px;height:180px}.spinner.primary:before{border-top-color:#007bff}.spinner.success:before{border-top-color:#28a745}.spinner.warning:before{border-top-color:#ffc107}.spinner.danger:before{border-top-color:#dc3545}.spinner-illustration{object-fit:contain;z-index:1}.spinner-label{font-size:14px;color:#fff;text-align:center}@keyframes spin{to{transform:rotate(360deg)}}.spinner-lottie{width:80px;height:80px}\n"] }]
1692
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { size: [{
1693
+ type: Input
1694
+ }], color: [{
1695
+ type: Input
1696
+ }], overlay: [{
1697
+ type: Input
1698
+ }], overlayColor: [{
1699
+ type: Input
1700
+ }], illustrationSrc: [{
1701
+ type: Input
1702
+ }], label: [{
1703
+ type: Input
1704
+ }], animationSrc: [{
1705
+ type: Input
1706
+ }] } });
1707
+
1708
+ class TooltipDirective {
1709
+ el;
1710
+ renderer;
1711
+ tooltipElement = null;
1712
+ // Make input reactive so it updates dynamically
1713
+ _tooltipText = '';
1714
+ set dsoDirectiveTooltip(value) {
1715
+ this._tooltipText = value;
1716
+ if (this.tooltipElement) {
1717
+ this.tooltipElement.textContent = value;
1718
+ }
1719
+ }
1720
+ get dsoDirectiveTooltip() {
1721
+ return this._tooltipText;
1722
+ }
1723
+ position = 'top';
1724
+ tooltipWidth = 'auto';
1725
+ gap = 4; // distance from the element
1726
+ constructor(el, renderer) {
1727
+ this.el = el;
1728
+ this.renderer = renderer;
1729
+ }
1730
+ onMouseEnter() {
1731
+ if (!this.dsoDirectiveTooltip)
1732
+ return;
1733
+ // Create tooltip element if not exists
1734
+ if (!this.tooltipElement) {
1735
+ this.tooltipElement = this.renderer.createElement('div');
1736
+ this.renderer.addClass(this.tooltipElement, 'tooltip-directive');
1737
+ // Style
1738
+ this.renderer.setStyle(this.tooltipElement, 'position', 'fixed'); // fixed so it won't be clipped
1739
+ this.renderer.setStyle(this.tooltipElement, 'background', '#333');
1740
+ this.renderer.setStyle(this.tooltipElement, 'color', '#fff');
1741
+ this.renderer.setStyle(this.tooltipElement, 'padding', '4px 8px');
1742
+ this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
1743
+ this.renderer.setStyle(this.tooltipElement, 'font-size', '12px');
1744
+ this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
1745
+ this.renderer.setStyle(this.tooltipElement, 'max-width', this.tooltipWidth);
1746
+ this.renderer.setStyle(this.tooltipElement, 'white-space', 'normal'); // allow wrapping
1747
+ this.renderer.setStyle(this.tooltipElement, 'word-break', 'break-word'); // break long words
1748
+ this.renderer.appendChild(document.body, this.tooltipElement);
1749
+ }
1750
+ // Step 3: set tooltip text
1751
+ if (this.tooltipElement) {
1752
+ this.tooltipElement.textContent = this.dsoDirectiveTooltip;
1753
+ const rect = this.el.nativeElement.getBoundingClientRect();
1754
+ const tooltipHeight = this.tooltipElement.offsetHeight;
1755
+ const tooltipWidth = this.tooltipElement.offsetWidth;
1756
+ // Step 4: position tooltip based on input
1757
+ switch (this.position) {
1758
+ case 'top':
1759
+ this.renderer.setStyle(this.tooltipElement, 'top', `${rect.top - tooltipHeight - this.gap}px`);
1760
+ this.renderer.setStyle(this.tooltipElement, 'left', `${rect.left + (rect.width - tooltipWidth) / 2}px`);
1761
+ break;
1762
+ case 'bottom':
1763
+ this.renderer.setStyle(this.tooltipElement, 'top', `${rect.bottom + this.gap}px`);
1764
+ this.renderer.setStyle(this.tooltipElement, 'left', `${rect.left + (rect.width - tooltipWidth) / 2}px`);
1765
+ break;
1766
+ case 'left':
1767
+ this.renderer.setStyle(this.tooltipElement, 'top', `${rect.top + (rect.height - tooltipHeight) / 2}px`);
1768
+ this.renderer.setStyle(this.tooltipElement, 'left', `${rect.left - tooltipWidth - this.gap}px`);
1769
+ break;
1770
+ case 'right':
1771
+ this.renderer.setStyle(this.tooltipElement, 'top', `${rect.top + (rect.height - tooltipHeight) / 2}px`);
1772
+ this.renderer.setStyle(this.tooltipElement, 'left', `${rect.right + this.gap}px`);
1773
+ break;
1774
+ }
1775
+ this.renderer.setStyle(this.tooltipElement, 'visibility', 'visible');
1776
+ }
1777
+ }
1778
+ onMouseLeave() {
1779
+ if (this.tooltipElement) {
1780
+ this.renderer.setStyle(this.tooltipElement, 'visibility', 'hidden');
1781
+ }
1782
+ }
1783
+ ngOnDestroy() {
1784
+ if (this.tooltipElement) {
1785
+ this.renderer.removeChild(document.body, this.tooltipElement);
1786
+ }
1787
+ }
1788
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
1789
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: TooltipDirective, isStandalone: true, selector: "[dsoDirectiveTooltip]", inputs: { dsoDirectiveTooltip: "dsoDirectiveTooltip", position: "position", tooltipWidth: "tooltipWidth" }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()" } }, ngImport: i0 });
1790
+ }
1791
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipDirective, decorators: [{
1792
+ type: Directive,
1793
+ args: [{
1794
+ selector: '[dsoDirectiveTooltip]', // This directive will be used with the attribute dsoTooltip
1795
+ standalone: true
1796
+ }]
1797
+ }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { dsoDirectiveTooltip: [{
1798
+ type: Input
1799
+ }], position: [{
1800
+ type: Input
1801
+ }], tooltipWidth: [{
1802
+ type: Input
1803
+ }], onMouseEnter: [{
1804
+ type: HostListener,
1805
+ args: ['mouseenter']
1806
+ }], onMouseLeave: [{
1807
+ type: HostListener,
1808
+ args: ['mouseleave']
1809
+ }] } });
1810
+
1811
+ class TableComponent {
1812
+ /* =====================
1813
+ Inputs
1814
+ ===================== */
1815
+ columns = [];
1816
+ rows = [];
1817
+ defaultSortColumn;
1818
+ defaultSortDirection = 'asc';
1819
+ stickyHeader = false;
1820
+ maxTableHeight;
1821
+ /* =====================
1822
+ Outputs
1823
+ ===================== */
1824
+ sortChange = new EventEmitter();
1825
+ /* =====================
1826
+ UI state only
1827
+ ===================== */
1828
+ sortColumn = null;
1829
+ sortDirection = 'asc';
1830
+ /* =====================
1831
+ Lifecycle
1832
+ ===================== */
1833
+ ngOnChanges() {
1834
+ // Initialize visual sort state ONLY
1835
+ if (this.defaultSortColumn && !this.sortColumn) {
1836
+ this.sortColumn = this.defaultSortColumn;
1837
+ this.sortDirection = this.defaultSortDirection;
1838
+ // Emit once so parent applies default sort
1839
+ this.sortChange.emit({
1840
+ column: this.sortColumn,
1841
+ direction: this.sortDirection
1842
+ });
1843
+ }
1844
+ }
1845
+ /* =====================
1846
+ Sorting (emit only)
1847
+ ===================== */
1848
+ sort(colKey) {
1849
+ let direction = 'asc';
1850
+ if (this.sortColumn === colKey) {
1851
+ direction = this.sortDirection === 'asc' ? 'desc' : 'asc';
1852
+ }
1853
+ this.sortColumn = colKey;
1854
+ this.sortDirection = direction;
1855
+ this.sortChange.emit({ column: colKey, direction });
1856
+ }
1857
+ /* =====================
1858
+ Column sizing
1859
+ ===================== */
1860
+ getColumnStyle(col) {
1861
+ return {
1862
+ 'min-width': col.minWidth || null,
1863
+ 'max-width': col.maxWidth || null,
1864
+ 'width': !col.minWidth && !col.maxWidth ? 'auto' : null,
1865
+ };
1866
+ }
1867
+ /* =====================
1868
+ Truncation
1869
+ ===================== */
1870
+ shouldTruncateNative(cell) {
1871
+ if (!cell)
1872
+ return false;
1873
+ const content = cell.querySelector('.cell-text');
1874
+ if (!content)
1875
+ return false;
1876
+ return content.scrollWidth > content.clientWidth;
1877
+ }
1878
+ /* =====================
1879
+ Selection (unchanged)
1880
+ ===================== */
1881
+ selectedRows = new Set();
1882
+ toggleRow(row) {
1883
+ if (this.selectedRows.has(row)) {
1884
+ this.selectedRows.delete(row);
1885
+ }
1886
+ else {
1887
+ this.selectedRows.add(row);
1888
+ }
1889
+ }
1890
+ isRowSelected(row) {
1891
+ return this.selectedRows.has(row);
1892
+ }
1893
+ get allSelected() {
1894
+ return this.rows.length > 0 &&
1895
+ this.selectedRows.size === this.rows.length;
1896
+ }
1897
+ toggleSelectAll() {
1898
+ if (this.allSelected) {
1899
+ this.selectedRows.clear();
1900
+ }
1901
+ else {
1902
+ this.rows.forEach(row => this.selectedRows.add(row));
1903
+ }
1904
+ }
1905
+ onEditRow(row) {
1906
+ console.log('Edit clicked for row:', row);
1907
+ }
1908
+ onDeleteRow(row) {
1909
+ console.log('Delete clicked for row:', row);
1910
+ }
1911
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1912
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TableComponent, isStandalone: true, selector: "dso-table", inputs: { columns: "columns", rows: "rows", defaultSortColumn: "defaultSortColumn", defaultSortDirection: "defaultSortDirection", stickyHeader: "stickyHeader", maxTableHeight: "maxTableHeight" }, outputs: { sortChange: "sortChange" }, usesOnChanges: true, ngImport: i0, template: "<div #tableContainer class=\"dso-table-container\" [class.sticky-header]=\"stickyHeader\" [style.max-height]=\"maxTableHeight\">\r\n <table class=\"dso-table\">\r\n<thead>\r\n <tr>\r\n <th *ngFor=\"let col of columns\"\r\n [class.sticky]=\"stickyHeader || col.sticky\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [class.active]=\"sortColumn === col.key\"\r\n class=\"sortable\"\r\n (click)=\"col.sortable !== false && col.type !== 'checkbox' && sort(col.key)\">\r\n\r\n <!-- If this column is a checkbox column -->\r\n <ng-container *ngIf=\"col.type === 'checkbox'; else normalHeader\">\r\n <dso-checkbox\r\n size=\"small\"\r\n [isChecked]=\"allSelected\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleSelectAll()\">\r\n </dso-checkbox>\r\n </ng-container>\r\n\r\n <!-- Your original header stays EXACTLY as-is -->\r\n <ng-template #normalHeader>\r\n <div class=\"header-content\">\r\n {{ col.label }}\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn === col.key\"\r\n [iconName]=\"sortDirection === 'asc'\r\n ? 'icon-van'\r\n : 'icon-chevron_down'\"\r\n size=\"small\"\r\n class=\"sort-icon\">\r\n </dso-icon>\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn !== col.key\"\r\n iconName=\"icon-chevron_up\"\r\n size=\"small\"\r\n class=\"hover-indicator\">\r\n </dso-icon>\r\n </div>\r\n </ng-template>\r\n\r\n </th>\r\n </tr>\r\n</thead>\r\n\r\n\r\n<tbody>\r\n <tr *ngFor=\"let row of rows\">\r\n <td\r\n #cell\r\n *ngFor=\"let col of columns\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [dsoDirectiveTooltip]=\"shouldTruncateNative(cell) ? row[col.key] : ''\">\r\n <div class=\"cell-wrapper\" style=\"display:flex; align-items:center;\">\r\n <dso-checkbox\r\n *ngIf=\"col.type==='checkbox'\"\r\n size=\"small\"\r\n [isChecked]=\"isRowSelected(row)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleRow(row)\">\r\n </dso-checkbox>\r\n\r\n <!-- Action button cell -->\r\n <ng-container *ngIf=\"col.type==='action'\">\r\n <dso-button\r\n btnType=\"filled\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Edit\"\r\n (click)=\"onEditRow(row)\">\r\n </dso-button>\r\n <dso-button\r\n btnType=\"outlined\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Delete\"\r\n (click)=\"onDeleteRow(row)\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Normal text cell -->\r\n <span *ngIf=\"!col.type || (col.type !== 'checkbox' && col.type !== 'action')\" class=\"cell-text\">\r\n {{ row[col.key] }}\r\n </span>\r\n </div>\r\n </td>\r\n </tr>\r\n</tbody>\r\n\r\n </table>\r\n</div>\r\n", styles: [".dso-table-container{overflow:auto;border:1px solid #ddd;position:relative}.dso-table{width:100%;border-collapse:collapse}.dso-table th,.dso-table td{padding:8px 12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dso-table thead th{background:#2c2f36;color:#fff;font-weight:600}.dso-table-container.sticky-header .dso-table th.sticky{position:sticky;top:0}.dso-table th.sticky{top:0;z-index:20;box-shadow:0 2px 2px -1px #00000040}.dso-table thead th.sortable:hover{background:#3a3e46;cursor:pointer}.dso-table thead th.active{background:#464b54!important}.dso-table thead th:last-child{border-right:none}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}td .cell-text{display:inline-block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cell-wrapper{display:inline-flex;align-items:center;margin:0;padding:0;gap:8px}.sticky-left{position:sticky;left:0;z-index:15;background:#fff}.sticky-right{position:sticky;right:0;z-index:15;background:#fff}.dso-table thead th.sticky-left,.dso-table thead th.sticky-right{z-index:25}.sticky-left.scroll-shadow{box-shadow:4px 0 6px -3px #00000040}.sticky-right.scroll-shadow{box-shadow:-4px 0 6px -3px #00000040}.dso-table tbody tr:hover{background:#f5f7fa}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "dso-button", inputs: ["btnLabel", "btnType", "btnSize", "btnIconName", "isDisabled", "isActive"], outputs: ["onClick"] }, { kind: "component", type: CheckboxComponent, selector: "dso-checkbox", inputs: ["label", "isChecked", "disabled", "size", "error", "required", "errorMessage", "iconName"], outputs: ["change"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }, { kind: "directive", type: TooltipDirective, selector: "[dsoDirectiveTooltip]", inputs: ["dsoDirectiveTooltip", "position", "tooltipWidth"] }] });
1913
+ }
1914
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TableComponent, decorators: [{
1915
+ type: Component,
1916
+ args: [{ selector: 'dso-table', standalone: true, imports: [
1917
+ ButtonComponent,
1918
+ CheckboxComponent,
1919
+ CommonModule,
1920
+ IconComponent,
1921
+ TooltipDirective
1922
+ ], template: "<div #tableContainer class=\"dso-table-container\" [class.sticky-header]=\"stickyHeader\" [style.max-height]=\"maxTableHeight\">\r\n <table class=\"dso-table\">\r\n<thead>\r\n <tr>\r\n <th *ngFor=\"let col of columns\"\r\n [class.sticky]=\"stickyHeader || col.sticky\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [class.active]=\"sortColumn === col.key\"\r\n class=\"sortable\"\r\n (click)=\"col.sortable !== false && col.type !== 'checkbox' && sort(col.key)\">\r\n\r\n <!-- If this column is a checkbox column -->\r\n <ng-container *ngIf=\"col.type === 'checkbox'; else normalHeader\">\r\n <dso-checkbox\r\n size=\"small\"\r\n [isChecked]=\"allSelected\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleSelectAll()\">\r\n </dso-checkbox>\r\n </ng-container>\r\n\r\n <!-- Your original header stays EXACTLY as-is -->\r\n <ng-template #normalHeader>\r\n <div class=\"header-content\">\r\n {{ col.label }}\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn === col.key\"\r\n [iconName]=\"sortDirection === 'asc'\r\n ? 'icon-van'\r\n : 'icon-chevron_down'\"\r\n size=\"small\"\r\n class=\"sort-icon\">\r\n </dso-icon>\r\n\r\n <dso-icon\r\n *ngIf=\"col.sortable !== false && sortColumn !== col.key\"\r\n iconName=\"icon-chevron_up\"\r\n size=\"small\"\r\n class=\"hover-indicator\">\r\n </dso-icon>\r\n </div>\r\n </ng-template>\r\n\r\n </th>\r\n </tr>\r\n</thead>\r\n\r\n\r\n<tbody>\r\n <tr *ngFor=\"let row of rows\">\r\n <td\r\n #cell\r\n *ngFor=\"let col of columns\"\r\n [class.sticky-left]=\"stickyHeader && col.type === 'checkbox'\"\r\n [class.sticky-right]=\"stickyHeader && col.type === 'action'\"\r\n [ngStyle]=\"getColumnStyle(col)\"\r\n [dsoDirectiveTooltip]=\"shouldTruncateNative(cell) ? row[col.key] : ''\">\r\n <div class=\"cell-wrapper\" style=\"display:flex; align-items:center;\">\r\n <dso-checkbox\r\n *ngIf=\"col.type==='checkbox'\"\r\n size=\"small\"\r\n [isChecked]=\"isRowSelected(row)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (change)=\"toggleRow(row)\">\r\n </dso-checkbox>\r\n\r\n <!-- Action button cell -->\r\n <ng-container *ngIf=\"col.type==='action'\">\r\n <dso-button\r\n btnType=\"filled\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Edit\"\r\n (click)=\"onEditRow(row)\">\r\n </dso-button>\r\n <dso-button\r\n btnType=\"outlined\"\r\n btnSize=\"smBtn\"\r\n btnLabel=\"Delete\"\r\n (click)=\"onDeleteRow(row)\">\r\n </dso-button>\r\n </ng-container>\r\n\r\n <!-- Normal text cell -->\r\n <span *ngIf=\"!col.type || (col.type !== 'checkbox' && col.type !== 'action')\" class=\"cell-text\">\r\n {{ row[col.key] }}\r\n </span>\r\n </div>\r\n </td>\r\n </tr>\r\n</tbody>\r\n\r\n </table>\r\n</div>\r\n", styles: [".dso-table-container{overflow:auto;border:1px solid #ddd;position:relative}.dso-table{width:100%;border-collapse:collapse}.dso-table th,.dso-table td{padding:8px 12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dso-table thead th{background:#2c2f36;color:#fff;font-weight:600}.dso-table-container.sticky-header .dso-table th.sticky{position:sticky;top:0}.dso-table th.sticky{top:0;z-index:20;box-shadow:0 2px 2px -1px #00000040}.dso-table thead th.sortable:hover{background:#3a3e46;cursor:pointer}.dso-table thead th.active{background:#464b54!important}.dso-table thead th:last-child{border-right:none}.truncate{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}td .cell-text{display:inline-block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.cell-wrapper{display:inline-flex;align-items:center;margin:0;padding:0;gap:8px}.sticky-left{position:sticky;left:0;z-index:15;background:#fff}.sticky-right{position:sticky;right:0;z-index:15;background:#fff}.dso-table thead th.sticky-left,.dso-table thead th.sticky-right{z-index:25}.sticky-left.scroll-shadow{box-shadow:4px 0 6px -3px #00000040}.sticky-right.scroll-shadow{box-shadow:-4px 0 6px -3px #00000040}.dso-table tbody tr:hover{background:#f5f7fa}\n"] }]
1923
+ }], propDecorators: { columns: [{
1924
+ type: Input
1925
+ }], rows: [{
1926
+ type: Input
1927
+ }], defaultSortColumn: [{
1928
+ type: Input
1929
+ }], defaultSortDirection: [{
1930
+ type: Input
1931
+ }], stickyHeader: [{
1932
+ type: Input
1933
+ }], maxTableHeight: [{
1934
+ type: Input
1935
+ }], sortChange: [{
1936
+ type: Output
1937
+ }] } });
1938
+
1939
+ // tab.component.ts
1940
+ class TabComponent {
1941
+ /** Label for the tab shown in the tabs header */
1942
+ label = '';
1943
+ /** Is this tab currently active? Controlled by TabsComponent */
1944
+ active = false;
1945
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1946
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TabComponent, isStandalone: true, selector: "dso-tab", inputs: { label: "label" }, ngImport: i0, template: "<!-- tab.component.html -->\r\n<!-- Only display the tab content if it is active -->\r\n<div *ngIf=\"active\" class=\"tab-content\">\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
1947
+ }
1948
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabComponent, decorators: [{
1949
+ type: Component,
1950
+ args: [{ selector: 'dso-tab', imports: [CommonModule], standalone: true, template: "<!-- tab.component.html -->\r\n<!-- Only display the tab content if it is active -->\r\n<div *ngIf=\"active\" class=\"tab-content\">\r\n <ng-content></ng-content>\r\n</div>\r\n" }]
1951
+ }], propDecorators: { label: [{
1952
+ type: Input
1953
+ }] } });
1954
+
1955
+ // tabs.component.ts
1956
+ class TabsComponent {
1957
+ /** Query all child tabs inside <dso-tabs> */
1958
+ tabs;
1959
+ /** Optional: two-way binding for active tab index */
1960
+ tabIndex = 0;
1961
+ tabIndexChange = new EventEmitter();
1962
+ ngAfterContentInit() {
1963
+ // Activate the default tab
1964
+ this.selectTab(this.tabIndex);
1965
+ }
1966
+ /** When a tab header is clicked */
1967
+ selectTab(index) {
1968
+ if (!this.tabs)
1969
+ return;
1970
+ this.tabs.forEach((tab, i) => tab.active = i === index);
1971
+ // Update internal tabIndex and emit event for two-way binding
1972
+ this.tabIndex = index;
1973
+ this.tabIndexChange.emit(this.tabIndex);
1974
+ }
1975
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1976
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TabsComponent, isStandalone: true, selector: "dso-tabs", inputs: { tabIndex: "tabIndex" }, outputs: { tabIndexChange: "tabIndexChange" }, queries: [{ propertyName: "tabs", predicate: TabComponent }], ngImport: i0, template: "<!-- tabs.component.html -->\r\n<div class=\"tabs-container\">\r\n <!-- Tab headers -->\r\n <div class=\"tab-headers\">\r\n <button\r\n *ngFor=\"let tab of tabs; let i = index\"\r\n [class.active]=\"tab.active\"\r\n (click)=\"selectTab(i)\">\r\n {{ tab.label }}\r\n </button>\r\n </div>\r\n\r\n <!-- Tab content will render via ng-content inside TabComponent -->\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".tabs-container{border:1px solid #ccc;border-radius:4px}.tab-headers{display:flex;border-bottom:1px solid #ccc}.tab-headers button{flex:1;padding:8px 12px;background:none;border:none;cursor:pointer;font-weight:500}.tab-headers button.active{border-bottom:2px solid #007bff;font-weight:700}.tab-headers button:hover{background-color:#f5f5f5}.tab-content{padding:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
1977
+ }
1978
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TabsComponent, decorators: [{
1979
+ type: Component,
1980
+ args: [{ selector: 'dso-tabs', standalone: true, imports: [CommonModule, TabComponent], template: "<!-- tabs.component.html -->\r\n<div class=\"tabs-container\">\r\n <!-- Tab headers -->\r\n <div class=\"tab-headers\">\r\n <button\r\n *ngFor=\"let tab of tabs; let i = index\"\r\n [class.active]=\"tab.active\"\r\n (click)=\"selectTab(i)\">\r\n {{ tab.label }}\r\n </button>\r\n </div>\r\n\r\n <!-- Tab content will render via ng-content inside TabComponent -->\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".tabs-container{border:1px solid #ccc;border-radius:4px}.tab-headers{display:flex;border-bottom:1px solid #ccc}.tab-headers button{flex:1;padding:8px 12px;background:none;border:none;cursor:pointer;font-weight:500}.tab-headers button.active{border-bottom:2px solid #007bff;font-weight:700}.tab-headers button:hover{background-color:#f5f5f5}.tab-content{padding:12px}\n"] }]
1981
+ }], propDecorators: { tabs: [{
1982
+ type: ContentChildren,
1983
+ args: [TabComponent]
1984
+ }], tabIndex: [{
1985
+ type: Input
1986
+ }], tabIndexChange: [{
1987
+ type: Output
1988
+ }] } });
1989
+
1990
+ class TagComponent {
1991
+ variant = 'neutral';
1992
+ removable = false;
1993
+ label = '';
1994
+ removed = false;
1995
+ removeTag() {
1996
+ this.removed = true;
1997
+ }
1998
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TagComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1999
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TagComponent, isStandalone: true, selector: "dso-tag", inputs: { variant: "variant", removable: "removable", label: "label" }, ngImport: i0, template: "<div\r\n *ngIf=\"!removed\"\r\n class=\"tag\"\r\n [ngClass]=\"'tag-' + variant\"\r\n>\r\n <span class=\"tag-label\">{{ label }}</span>\r\n <!-- <dso-button></dso-button> -->\r\n <button \r\n *ngIf=\"removable\" \r\n class=\"tag-remove-btn\" \r\n (click)=\"removeTag()\"\r\n aria-label=\"Remove tag\"\r\n >\r\n &times;\r\n </button>\r\n</div>\r\n\r\n", styles: [".tag{display:inline-flex;align-items:center;padding:4px 12px;font-size:.875rem;border-radius:16px;font-weight:500;white-space:nowrap;background-color:#e0e0e0;color:#333;gap:2px}.tag-success{background-color:#d4edda;color:#155724}.tag-info{background-color:#d1ecf1;color:#0c5460}.tag-warning{background-color:#fff3cd;color:#856404}.tag-danger{background-color:#f8d7da;color:#721c24}.tag-neutral{background-color:#e2e3e5;color:#383d41}.tag-remove-btn{background:transparent;border:none;font-size:1rem;line-height:1;cursor:pointer;color:inherit}.tag-remove-btn:hover{opacity:.6}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
2000
+ }
2001
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TagComponent, decorators: [{
2002
+ type: Component,
2003
+ args: [{ selector: 'dso-tag', standalone: true, imports: [CommonModule, ButtonComponent], template: "<div\r\n *ngIf=\"!removed\"\r\n class=\"tag\"\r\n [ngClass]=\"'tag-' + variant\"\r\n>\r\n <span class=\"tag-label\">{{ label }}</span>\r\n <!-- <dso-button></dso-button> -->\r\n <button \r\n *ngIf=\"removable\" \r\n class=\"tag-remove-btn\" \r\n (click)=\"removeTag()\"\r\n aria-label=\"Remove tag\"\r\n >\r\n &times;\r\n </button>\r\n</div>\r\n\r\n", styles: [".tag{display:inline-flex;align-items:center;padding:4px 12px;font-size:.875rem;border-radius:16px;font-weight:500;white-space:nowrap;background-color:#e0e0e0;color:#333;gap:2px}.tag-success{background-color:#d4edda;color:#155724}.tag-info{background-color:#d1ecf1;color:#0c5460}.tag-warning{background-color:#fff3cd;color:#856404}.tag-danger{background-color:#f8d7da;color:#721c24}.tag-neutral{background-color:#e2e3e5;color:#383d41}.tag-remove-btn{background:transparent;border:none;font-size:1rem;line-height:1;cursor:pointer;color:inherit}.tag-remove-btn:hover{opacity:.6}\n"] }]
2004
+ }], propDecorators: { variant: [{
2005
+ type: Input
2006
+ }], removable: [{
2007
+ type: Input
2008
+ }], label: [{
2009
+ type: Input
2010
+ }] } });
2011
+
2012
+ class TextAreaComponent {
2013
+ inputLabel = ''; // Optional label for the textarea
2014
+ placeholder = '';
2015
+ value = ''; // Two-way binding for value
2016
+ helperText = 'helpering '; // Optional helper text
2017
+ errorText = 'tes test'; // Optional error text
2018
+ isDisabled = false; // Optional disabled state
2019
+ enableValidation = true; // Toggle validation
2020
+ maxLength = 300; // Maximum character length (for example)
2021
+ isTouched = false;
2022
+ isInvalid = false; // Error state
2023
+ charCount = 0; // To track character count
2024
+ isExceeded = false; // To track if the limit is exceeded
2025
+ onInputChange(event) {
2026
+ const inputValue = event.target.value;
2027
+ console.log(inputValue);
2028
+ const charCount = inputValue.length;
2029
+ // Check if the limit has been exceeded
2030
+ this.isExceeded = charCount > this.maxLength;
2031
+ this.isTouched = true;
2032
+ this.checkValidity();
2033
+ }
2034
+ // Method to determine the style of the character count
2035
+ getCharCountStyle() {
2036
+ const charCount = this.value.length;
2037
+ if (this.isExceeded) {
2038
+ // this.isExceeded = true;
2039
+ return 'char-count-error';
2040
+ }
2041
+ // If character count exceeds 50% of maxLength, change color to black
2042
+ if (charCount > this.maxLength / 2) {
2043
+ return '';
2044
+ }
2045
+ // Otherwise, keep it grey
2046
+ return '';
2047
+ }
2048
+ onBlur() {
2049
+ this.isTouched = true;
2050
+ this.checkValidity();
2051
+ }
2052
+ checkValidity() {
2053
+ // Set to invalid if it's required and empty
2054
+ this.isInvalid = this.isTouched && !this.value.trim();
2055
+ }
2056
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextAreaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2057
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TextAreaComponent, isStandalone: true, selector: "dso-text-area", inputs: { inputLabel: "inputLabel", placeholder: "placeholder", value: "value", helperText: "helperText", errorText: "errorText", isDisabled: "isDisabled", enableValidation: "enableValidation", maxLength: "maxLength" }, ngImport: i0, template: "<div class=\"text-area-container\">\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }}\r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div>\r\n\r\n <textarea\r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\"\r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\"\r\n [class.disabled]=\"isDisabled\"\r\n >\r\n </textarea>\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n", styles: [".text-area-container{display:flex;flex-direction:column;width:500px;gap:8px}.text-wrapper{display:flex;justify-content:space-between}textarea{width:100%;height:160px;padding:12px 8px;font-size:1rem;resize:vertical;border:1px solid #ccc;border-radius:4px}textarea:hover{border:1px solid #071d35}textarea:focus{border-color:#007bff;outline:none}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}.helper-text{font-size:16px;color:#555}.error-message{font-size:16px;color:red}span .error-message{color:red;font-size:12px}textarea.invalid{border-color:red}textarea.disabled{background-color:#f0f0f0;cursor:not-allowed;border-color:#ccc}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
2058
+ }
2059
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TextAreaComponent, decorators: [{
2060
+ type: Component,
2061
+ args: [{ selector: 'dso-text-area', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"text-area-container\">\r\n <div class=\"text-wrapper\">\r\n <!-- Optional label -->\r\n <label *ngIf=\"inputLabel\">\r\n {{ inputLabel }}\r\n </label> \r\n <!-- Character count inside the container -->\r\n <div class=\"char-count\" [ngClass]=\"getCharCountStyle()\" *ngIf=\"maxLength\">\r\n <span>\r\n {{ value.length }}\r\n </span>\r\n / {{ maxLength }}\r\n </div>\r\n </div>\r\n\r\n <textarea\r\n [placeholder]=\"placeholder\"\r\n [(ngModel)]=\"value\"\r\n (input)=\"onInputChange($event)\"\r\n (blur)=\"onBlur()\"\r\n [disabled]=\"isDisabled\"\r\n [class.invalid]=\"(isInvalid && isTouched && enableValidation) || isExceeded\"\r\n [class.disabled]=\"isDisabled\"\r\n >\r\n </textarea>\r\n\r\n <!-- Helper Text \r\n This condition checks if the helper text should be displayed:\r\n - If the character limit is **not exceeded** and validation is **not enabled**, show the helper text.\r\n - If validation **is enabled**, show the helper text only if the input is **not invalid** and `helperText` is provided.\r\n - If the character limit is exceeded, the helper text will be replaced with an error message. -->\r\n <div *ngIf=\"!isExceeded && !enableValidation || ((!isInvalid && helperText) && !isExceeded)\" class=\"helper-text\">\r\n {{ helperText }}\r\n </div>\r\n\r\n <!-- Specified Error Text -->\r\n <div *ngIf=\"isInvalid && isTouched && enableValidation\" class=\"error-message\">\r\n {{ errorText }}\r\n </div>\r\n\r\n <!-- Error message if character limit exceeded -->\r\n <div *ngIf=\"isExceeded\" class=\"error-message\">\r\n Character limit exceeded.\r\n </div>\r\n</div>\r\n", styles: [".text-area-container{display:flex;flex-direction:column;width:500px;gap:8px}.text-wrapper{display:flex;justify-content:space-between}textarea{width:100%;height:160px;padding:12px 8px;font-size:1rem;resize:vertical;border:1px solid #ccc;border-radius:4px}textarea:hover{border:1px solid #071d35}textarea:focus{border-color:#007bff;outline:none}.char-count{font-size:12px;color:#666;align-self:flex-end}.char-count span,.char-count-error{font-size:12px;color:#666}.char-count-error span{font-size:12px;color:red}.helper-text{font-size:16px;color:#555}.error-message{font-size:16px;color:red}span .error-message{color:red;font-size:12px}textarea.invalid{border-color:red}textarea.disabled{background-color:#f0f0f0;cursor:not-allowed;border-color:#ccc}\n"] }]
2062
+ }], propDecorators: { inputLabel: [{
2063
+ type: Input
2064
+ }], placeholder: [{
2065
+ type: Input
2066
+ }], value: [{
2067
+ type: Input
2068
+ }], helperText: [{
2069
+ type: Input
2070
+ }], errorText: [{
2071
+ type: Input
2072
+ }], isDisabled: [{
2073
+ type: Input
2074
+ }], enableValidation: [{
2075
+ type: Input
2076
+ }], maxLength: [{
2077
+ type: Input
2078
+ }] } });
2079
+
2080
+ class ToastService {
2081
+ // private toastSubject = new Subject<ToastMessage>();
2082
+ toastSubject = new ReplaySubject; // keep last message
2083
+ toast$ = this.toastSubject.asObservable();
2084
+ show(header, message, variant = 'neutral', duration) {
2085
+ this.toastSubject.next({ header, message, variant, duration });
2086
+ console.log('Toast Service Broadcasting:', { header, message, variant, duration }); // <-- debug
2087
+ }
2088
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2089
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, providedIn: 'root' });
2090
+ }
2091
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastService, decorators: [{
2092
+ type: Injectable,
2093
+ args: [{ providedIn: 'root' }]
2094
+ }] });
2095
+
2096
+ class ToastComponent {
2097
+ toastService;
2098
+ toasts = [];
2099
+ position = 'top-right';
2100
+ toastData; // Stores the current toast being displayed
2101
+ constructor(toastService) {
2102
+ this.toastService = toastService;
2103
+ } // Inject toast service
2104
+ ngOnInit() {
2105
+ // Subscribe to toast messages from the service
2106
+ this.toastService.toast$.subscribe((toast) => {
2107
+ console.log('Toast Child Listening:', toast); // Add this debug log!
2108
+ this.toasts.push(toast); // Add the new toast to the array for display
2109
+ // Auto-hide after specified duration (default 3000ms)
2110
+ setTimeout(() => {
2111
+ // this.visible = false; // Hide toast
2112
+ this.toasts.shift(); // Remove first toast from the array
2113
+ }, toast.duration || 3000);
2114
+ });
2115
+ }
2116
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastComponent, deps: [{ token: ToastService }], target: i0.ɵɵFactoryTarget.Component });
2117
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: ToastComponent, isStandalone: true, selector: "dso-toast", inputs: { position: "position" }, ngImport: i0, template: "<div class=\"toast-wrapper\" \r\n [ngClass]=\"['toast-' + (position || 'top-right')]\">\r\n <div\r\n *ngFor=\"let toast of toasts; let i = index\"\r\n class=\"toast-container\"\r\n [ngClass]=\"['toast-' + (toast.variant || 'neutral')]\"\r\n >\r\n \r\n <div class=\"toast-header\" *ngIf=\"toast.header\">{{ toast.header }}</div>\r\n <div class=\"toast-body\">\r\n <div class=\"toast-message\">{{ toast.message }}</div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- \r\n[ngStyle]=\"{\r\n animation: 'fadeInOut ' + (toast.duration || 3000) + 'ms ease forwards'}\"> -->", styles: [".toast-wrapper{position:fixed;display:flex;flex-direction:column;z-index:1000;gap:16px}.toast-container{padding:12px 16px;border-radius:8px;box-shadow:0 4px 12px #0003;color:#fff;width:280px;font-family:sans-serif;animation:fadeIn .3s ease forwards}.toast-top-right{top:20px;right:20px;align-items:flex-end}.toast-top-left{top:20px;left:20px;align-items:flex-start}.toast-bottom-right{bottom:20px;right:20px;align-items:flex-end}.toast-bottom-left{bottom:20px;left:20px;align-items:flex-start}.toast-top-center{top:20px;left:50%;transform:translate(-50%)}.toast-bottom-center{bottom:20px;left:50%;transform:translate(-50%)}.toast-success{background-color:#28a745}.toast-info{background-color:#17a2b8}.toast-warning{background-color:#ffc107;color:#333}.toast-danger{background-color:#dc3545}.toast-neutral{background-color:#6c757d}.toast-header{font-weight:600;margin-bottom:4px}.toast-subtext{font-size:.85rem;opacity:.8}@keyframes fadeIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-10px)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
2118
+ }
2119
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ToastComponent, decorators: [{
2120
+ type: Component,
2121
+ args: [{ selector: 'dso-toast', standalone: true, imports: [CommonModule] // Import CommonModule for ngIf, ngFor etc.
2122
+ , template: "<div class=\"toast-wrapper\" \r\n [ngClass]=\"['toast-' + (position || 'top-right')]\">\r\n <div\r\n *ngFor=\"let toast of toasts; let i = index\"\r\n class=\"toast-container\"\r\n [ngClass]=\"['toast-' + (toast.variant || 'neutral')]\"\r\n >\r\n \r\n <div class=\"toast-header\" *ngIf=\"toast.header\">{{ toast.header }}</div>\r\n <div class=\"toast-body\">\r\n <div class=\"toast-message\">{{ toast.message }}</div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<!-- \r\n[ngStyle]=\"{\r\n animation: 'fadeInOut ' + (toast.duration || 3000) + 'ms ease forwards'}\"> -->", styles: [".toast-wrapper{position:fixed;display:flex;flex-direction:column;z-index:1000;gap:16px}.toast-container{padding:12px 16px;border-radius:8px;box-shadow:0 4px 12px #0003;color:#fff;width:280px;font-family:sans-serif;animation:fadeIn .3s ease forwards}.toast-top-right{top:20px;right:20px;align-items:flex-end}.toast-top-left{top:20px;left:20px;align-items:flex-start}.toast-bottom-right{bottom:20px;right:20px;align-items:flex-end}.toast-bottom-left{bottom:20px;left:20px;align-items:flex-start}.toast-top-center{top:20px;left:50%;transform:translate(-50%)}.toast-bottom-center{bottom:20px;left:50%;transform:translate(-50%)}.toast-success{background-color:#28a745}.toast-info{background-color:#17a2b8}.toast-warning{background-color:#ffc107;color:#333}.toast-danger{background-color:#dc3545}.toast-neutral{background-color:#6c757d}.toast-header{font-weight:600;margin-bottom:4px}.toast-subtext{font-size:.85rem;opacity:.8}@keyframes fadeIn{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-10px)}}\n"] }]
2123
+ }], ctorParameters: () => [{ type: ToastService }], propDecorators: { position: [{
2124
+ type: Input
2125
+ }] } });
2126
+
2127
+ class TooltipComponent {
2128
+ el;
2129
+ text; // Tooltip text to display
2130
+ position = 'top'; // Position of the tooltip (default is 'top')
2131
+ tooltipWidth = '300px'; // Default width, can be overridden
2132
+ constructor(el) {
2133
+ this.el = el;
2134
+ }
2135
+ showTooltip = false; // This flag controls the visibility of the tooltip
2136
+ isHovered = false; // This flag checks if the mouse is over the tooltip
2137
+ // Triggered when the mouse enters the wrapper area (button + tooltip)
2138
+ onMouseEnter() {
2139
+ // console.log('Mouse entered');
2140
+ this.showTooltip = true; // Show tooltip
2141
+ }
2142
+ // Triggered when the mouse leaves the wrapper area (button + tooltip)
2143
+ onMouseLeave() {
2144
+ // console.log('Mouse left');
2145
+ this.showTooltip = false; // Hide tooltip
2146
+ }
2147
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
2148
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TooltipComponent, isStandalone: true, selector: "dso-tooltip", inputs: { text: "text", position: "position", tooltipWidth: "tooltipWidth" }, ngImport: i0, template: "<!-- Tooltip wrapper: listens for mouse enter/leave -->\r\n<div class=\"tooltip-wrapper\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\r\n <ng-content></ng-content> <!-- This will allow the button or other content to be passed inside -->\r\n\r\n <!-- Tooltip box -->\r\n <div \r\n class=\"tooltip\" \r\n *ngIf=\"showTooltip\" \r\n [ngClass]=\"position\"\r\n [ngStyle]=\"{ 'width': tooltipWidth }\"\r\n >\r\n {{ text }}\r\n </div>\r\n</div>", styles: [".tooltip-wrapper{position:relative}.tooltip{position:absolute;background-color:#000;color:#fff;display:block;white-space:normal;word-break:break-word;border-radius:4px;pointer-events:auto;padding:12px;font-size:12px;z-index:100}.tooltip.top{bottom:calc(100% + 4px);transform:translate(-25%)}.tooltip.bottom{top:calc(100% + 4px);transform:translate(-25%)}.tooltip.left{top:0%;right:calc(100% + 4px);transform:translateY(-25%)}.tooltip.right{bottom:0%;left:calc(100% + 4px);transform:translateY(25%)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
2149
+ }
2150
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TooltipComponent, decorators: [{
2151
+ type: Component,
2152
+ args: [{ selector: 'dso-tooltip', standalone: true, imports: [CommonModule], template: "<!-- Tooltip wrapper: listens for mouse enter/leave -->\r\n<div class=\"tooltip-wrapper\" (mouseenter)=\"onMouseEnter()\" (mouseleave)=\"onMouseLeave()\">\r\n <ng-content></ng-content> <!-- This will allow the button or other content to be passed inside -->\r\n\r\n <!-- Tooltip box -->\r\n <div \r\n class=\"tooltip\" \r\n *ngIf=\"showTooltip\" \r\n [ngClass]=\"position\"\r\n [ngStyle]=\"{ 'width': tooltipWidth }\"\r\n >\r\n {{ text }}\r\n </div>\r\n</div>", styles: [".tooltip-wrapper{position:relative}.tooltip{position:absolute;background-color:#000;color:#fff;display:block;white-space:normal;word-break:break-word;border-radius:4px;pointer-events:auto;padding:12px;font-size:12px;z-index:100}.tooltip.top{bottom:calc(100% + 4px);transform:translate(-25%)}.tooltip.bottom{top:calc(100% + 4px);transform:translate(-25%)}.tooltip.left{top:0%;right:calc(100% + 4px);transform:translateY(-25%)}.tooltip.right{bottom:0%;left:calc(100% + 4px);transform:translateY(25%)}\n"] }]
2153
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { text: [{
2154
+ type: Input
2155
+ }], position: [{
2156
+ type: Input
2157
+ }], tooltipWidth: [{
2158
+ type: Input
2159
+ }] } });
2160
+
2161
+ class TopNavComponent {
2162
+ /** App name displayed next to logo */
2163
+ appName = '';
2164
+ /** Dynamic menu items */
2165
+ menuItems = [];
2166
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TopNavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2167
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: TopNavComponent, isStandalone: true, selector: "dso-top-nav", inputs: { appName: "appName", menuItems: "menuItems" }, ngImport: i0, template: "<nav class=\"dso-top-nav\">\r\n\r\n <!-- Left section: menu+logo -->\r\n <div class=\"nav-left\">\r\n <div class=\"nav-left-group menu-logo\">\r\n <ng-content select=\"[dso-menu-logo]\"></ng-content>\r\n <span class=\"top-nav-app-name\" *ngIf=\"appName\">{{ appName }}</span>\r\n </div>\r\n\r\n <div class=\"nav-left-group menu-buttons\">\r\n <ng-container *ngFor=\"let item of menuItems\">\r\n <!-- Router link item -->\r\n <a \r\n *ngIf=\"item.route; else clickItem\" \r\n [routerLink]=\"item.route\" \r\n class=\"top-nav-item\"\r\n >\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </a>\r\n\r\n <!-- Click handler item -->\r\n <ng-template #clickItem>\r\n <button class=\"top-nav-item\" (click)=\"item.click?.()\">\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </button>\r\n </ng-template>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <!-- Right section -->\r\n <div class=\"nav-right\">\r\n <!-- \u2705 Project each element individually with dso-nav-right -->\r\n <ng-content select=\"[dso-nav-right]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: ["@charset \"UTF-8\";.dso-top-nav{display:flex;align-items:center;justify-content:space-between;height:52px;padding:0 1rem;background-color:#fff;border:1px solid #e5e5e5}.dso-top-nav .nav-left{display:flex;align-items:center;gap:2rem}.dso-top-nav .nav-left .nav-left-group{display:flex;align-items:center}.dso-top-nav .nav-left .menu-logo{gap:.5rem}.dso-top-nav .nav-left .menu-buttons{gap:1rem}.dso-top-nav .nav-right{display:flex;align-items:center;gap:1rem;flex-direction:row;justify-content:flex-end}.dso-top-nav .nav-right dso-button{margin:0}.dso-top-nav .nav-right p{margin:0;font-weight:500}.top-nav-item{display:flex;align-items:center;gap:.5rem;padding:.25rem .5rem;background:none;border:none;cursor:pointer;font:inherit;color:inherit;text-decoration:none}.top-nav-item:hover{background-color:#f2f2f2;border-radius:4px}.top-nav-app-name{font-weight:700;font-size:1rem;margin-left:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: IconComponent, selector: "dso-icon", inputs: ["iconName", "size"] }] });
2168
+ }
2169
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TopNavComponent, decorators: [{
2170
+ type: Component,
2171
+ args: [{ selector: 'dso-top-nav', standalone: true, imports: [CommonModule, RouterModule, IconComponent], template: "<nav class=\"dso-top-nav\">\r\n\r\n <!-- Left section: menu+logo -->\r\n <div class=\"nav-left\">\r\n <div class=\"nav-left-group menu-logo\">\r\n <ng-content select=\"[dso-menu-logo]\"></ng-content>\r\n <span class=\"top-nav-app-name\" *ngIf=\"appName\">{{ appName }}</span>\r\n </div>\r\n\r\n <div class=\"nav-left-group menu-buttons\">\r\n <ng-container *ngFor=\"let item of menuItems\">\r\n <!-- Router link item -->\r\n <a \r\n *ngIf=\"item.route; else clickItem\" \r\n [routerLink]=\"item.route\" \r\n class=\"top-nav-item\"\r\n >\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </a>\r\n\r\n <!-- Click handler item -->\r\n <ng-template #clickItem>\r\n <button class=\"top-nav-item\" (click)=\"item.click?.()\">\r\n <dso-icon *ngIf=\"item.iconName\" [iconName]=\"item.iconName\" size=\"small\"></dso-icon>\r\n <span>{{ item.label }}</span>\r\n </button>\r\n </ng-template>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n <!-- Right section -->\r\n <div class=\"nav-right\">\r\n <!-- \u2705 Project each element individually with dso-nav-right -->\r\n <ng-content select=\"[dso-nav-right]\"></ng-content>\r\n </div>\r\n\r\n</nav>\r\n", styles: ["@charset \"UTF-8\";.dso-top-nav{display:flex;align-items:center;justify-content:space-between;height:52px;padding:0 1rem;background-color:#fff;border:1px solid #e5e5e5}.dso-top-nav .nav-left{display:flex;align-items:center;gap:2rem}.dso-top-nav .nav-left .nav-left-group{display:flex;align-items:center}.dso-top-nav .nav-left .menu-logo{gap:.5rem}.dso-top-nav .nav-left .menu-buttons{gap:1rem}.dso-top-nav .nav-right{display:flex;align-items:center;gap:1rem;flex-direction:row;justify-content:flex-end}.dso-top-nav .nav-right dso-button{margin:0}.dso-top-nav .nav-right p{margin:0;font-weight:500}.top-nav-item{display:flex;align-items:center;gap:.5rem;padding:.25rem .5rem;background:none;border:none;cursor:pointer;font:inherit;color:inherit;text-decoration:none}.top-nav-item:hover{background-color:#f2f2f2;border-radius:4px}.top-nav-app-name{font-weight:700;font-size:1rem;margin-left:.5rem}\n"] }]
2172
+ }], propDecorators: { appName: [{
2173
+ type: Input
2174
+ }], menuItems: [{
2175
+ type: Input
2176
+ }] } });
2177
+
125
2178
  /**
126
2179
  * Generated bundle index. Do not edit.
127
2180
  */
128
2181
 
129
- export { ButtonComponent, IconComponent };
2182
+ export { AlertComponent, BadgeComponent, BreadcrumbComponent, ButtonComponent, CheckboxComponent, DatepickerComponent, DialogComponent, DropdownListComponent, DsoTruncateDirective, FileUploadItemComponent, FileUploadMultiComponent, FileUploadSingleComponent, IconComponent, PaginationComponent, ProgressBarComponent, RadioComponent, SideNavComponent, SingleSelectComponent, SpinnerComponent, TableComponent, TabsComponent, TagComponent, TextAreaComponent, TextInputComponent, ToastComponent, TooltipComponent, TopNavComponent };
130
2183
  //# sourceMappingURL=dso-design-system-ui.mjs.map