shirkasoft-ui-components 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,154 @@
1
1
  import * as i1 from '@angular/common';
2
2
  import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { inject, ChangeDetectorRef, input, output, viewChild, signal, computed, DestroyRef, effect, forwardRef, ChangeDetectionStrategy, Component, Injectable, model, ElementRef, HostListener, ApplicationRef, EnvironmentInjector, createComponent, ViewContainerRef, untracked, PLATFORM_ID, Inject, Directive, contentChildren } from '@angular/core';
4
+ import { inject, ChangeDetectorRef, input, output, viewChild, signal, computed, DestroyRef, effect, forwardRef, ChangeDetectionStrategy, Component, Injectable, model, ElementRef, HostListener, ApplicationRef, EnvironmentInjector, createComponent, ViewContainerRef, untracked, linkedSignal, PLATFORM_ID, Directive, contentChildren } from '@angular/core';
5
5
  import * as i2 from '@angular/forms';
6
6
  import { ReactiveFormsModule, NG_VALUE_ACCESSOR, FormsModule, FormControl, FormGroup, FormBuilder } from '@angular/forms';
7
7
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
8
8
  import * as i1$1 from '@jsverse/transloco';
9
9
  import { TranslocoService, TranslocoModule } from '@jsverse/transloco';
10
- import { LucideEyeOff, LucideEye, LucideSearch, LucideDynamicIcon, LucideX, LucideTriangleAlert, LucideInfo, LucideCircleX, LucideCheck, LucideLoader, LucideTrash2, LucideLoaderCircle, LucideMinimize2, LucideMaximize2, LucideChevronRight, LucideChevronLeft, LucideChevronDown, LucideFile, LucideUpload, LucideArrowDown, LucideArrowUp, LucideArrowLeft, LucideArrowRight, LucideFilter, LucideCalculator, LucideMoreHorizontal, LucideMenu, LucideCircleCheck, LucideGlobe, LucideWallet, LucidePercent, LucideDollarSign, LucideCreditCard, LucideTag, LucideTruck, LucidePackage, LucideShoppingCart, LucideFolderOpen, LucideFileText, LucideHome, LucideMapPin, LucidePhone, LucideMail, LucideLock, LucideChevronUp, LucideSparkles, LucideCopy, LucideRocket, LucideRefreshCw, LucideDownload, LucideAlertCircle, LucideAlertTriangle, LucideSettings, LucideClock, LucideCalendarCheck, LucideCalendar, LucideStar, LucideUsers, LucideUser, LucideSave, LucidePencil, LucidePlus, LucideCircleHelp, LucideList, LucideWarehouse, LucideUserCog, LucideShieldCheck, LucideLineChart, LucideBot, LucideReceipt, LucideTags, LucideBox, LucideLandmark, LucideBuilding, LucideUserPlus, LucideUserRoundX, LucideUserCheck, LucideLogOut, LucideVideo, LucidePlay, LucideShare2, LucideArchive, LucidePresentation, LucideFileSpreadsheet, LucideCirclePlus, LucideToggleLeft, LucideToggleRight, LucideUserPen, LucideKey, LucideFunnel, LucideInbox, LucideArrowUpDown, LucideChevronsRight, LucideChevronsLeft, LucideBarChart3, LucideBuilding2, LucideLayoutDashboard, LucideHouse, LucideBanknote, LucidePanelRight, LucideSliders, LucideBriefcase, LucideIdCard, LucideChartLine, LucideChartPie, LucidePuzzle, LucideLayers, LucideShield, LucideUserRound, LucideUtensils, LucideShip, LucideBus, LucideMountain, LucideCar, LucideTicket, LucideHotel, LucideCalendarDays, LucideCircleAlert, LucideStickyNote, LucideCoins, LucideMap, LucideClipboardCheck, LucideScrollText } from '@lucide/angular';
10
+ import { LucideX, LucideUpload, LucideTriangleAlert, LucideTrash2, LucideSearch, LucideRefreshCw, LucidePlus, LucidePanelRight, LucideMinimize2, LucideMenu, LucideMaximize2, LucideLogOut, LucideLoaderCircle, LucideLoader, LucideInfo, LucideInbox, LucideGlobe, LucideFunnel, LucideFile, LucideEyeOff, LucideEye, LucideCirclePlus, LucideCircleQuestionMark, LucideChevronsRight, LucideChevronsLeft, LucideChevronRight, LucideChevronLeft, LucideChevronDown, LucideCalendarDays, LucideCheck, LucideArrowUpDown, LucideArrowLeft, LucideDynamicIcon, LucideTag, LucidePackage, LucideUserRound, LucideMapPin, LucideUtensils, LucideShield, LucideShip, LucideBus, LucideMountain, LucideCar, LucideTicket, LucideHotel } from '@lucide/angular';
11
11
  import { Router, RouterLink } from '@angular/router';
12
12
  import { Subject, debounceTime, BehaviorSubject } from 'rxjs';
13
13
  import { Chart, registerables } from 'chart.js';
14
14
 
15
- const icons$9 = {
16
- search: LucideSearch,
15
+ /**
16
+ * Shirkasoft UI Components.
17
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
18
+ * Belongs to Shirkasoft.
19
+ */
20
+ /** Maps the icon names used across the library to their Lucide component. */
21
+ const ICONS = {
22
+ 'arrow-left': LucideArrowLeft,
23
+ 'arrow-up-down': LucideArrowUpDown,
24
+ check: LucideCheck,
25
+ 'calendar-days': LucideCalendarDays,
26
+ 'chevron-down': LucideChevronDown,
27
+ 'chevron-left': LucideChevronLeft,
28
+ 'chevron-right': LucideChevronRight,
29
+ 'chevrons-left': LucideChevronsLeft,
30
+ 'chevrons-right': LucideChevronsRight,
31
+ 'circle-help': LucideCircleQuestionMark,
32
+ 'circle-plus': LucideCirclePlus,
17
33
  eye: LucideEye,
18
34
  'eye-off': LucideEyeOff,
35
+ file: LucideFile,
36
+ filter: LucideFunnel,
37
+ globe: LucideGlobe,
38
+ inbox: LucideInbox,
39
+ info: LucideInfo,
40
+ loader: LucideLoader,
41
+ 'loader-circle': LucideLoaderCircle,
42
+ 'log-out': LucideLogOut,
43
+ 'maximize-2': LucideMaximize2,
44
+ menu: LucideMenu,
45
+ 'minimize-2': LucideMinimize2,
46
+ 'panel-right': LucidePanelRight,
47
+ plus: LucidePlus,
48
+ refresh: LucideRefreshCw,
49
+ search: LucideSearch,
50
+ 'trash-2': LucideTrash2,
51
+ 'triangle-alert': LucideTriangleAlert,
52
+ upload: LucideUpload,
53
+ x: LucideX,
19
54
  };
55
+ /**
56
+ * Resolves the icon passed to a component into a lucide component.
57
+ * Accepts a registered string name, an existing lucide component or a LucideIconData object.
58
+ */
59
+ function getIcon(icon, fallback = LucideCircleQuestionMark) {
60
+ if (!icon)
61
+ return null;
62
+ if (typeof icon === 'string') {
63
+ return ICONS[icon] ?? fallback;
64
+ }
65
+ if (typeof icon === 'function') {
66
+ return icon;
67
+ }
68
+ return fallback;
69
+ }
70
+
71
+ /**
72
+ * Shirkasoft UI Components.
73
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
74
+ * Belongs to Shirkasoft.
75
+ */
76
+ /**
77
+ * Text input (or textarea) that implements ControlValueAccessor with built-in
78
+ * validation messages, card/number formatting, password reveal and search mode.
79
+ */
20
80
  class TextFieldComponent {
21
81
  constructor() {
22
82
  this.cdr = inject(ChangeDetectorRef);
83
+ /** id attribute applied to the native input. */
23
84
  this.id = input('', ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
85
+ /** Formats the value as a credit card number (XXXX-XXXX-XXXX-XXXX). */
24
86
  this.formatCard = input(false, ...(ngDevMode ? [{ debugName: "formatCard" }] : /* istanbul ignore next */ []));
87
+ /** Blocks the minus sign from being typed. */
25
88
  this.preventNegative = input(false, ...(ngDevMode ? [{ debugName: "preventNegative" }] : /* istanbul ignore next */ []));
89
+ /** Label rendered above the field. */
26
90
  this.label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
91
+ /** Native input type ('text', 'password', 'number', etc.). */
27
92
  this.type = input('text', ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
93
+ /** Allows digits only, stripping any other character. */
28
94
  this.numbersOnly = input(false, ...(ngDevMode ? [{ debugName: "numbersOnly" }] : /* istanbul ignore next */ []));
95
+ /** When true, allows decimals and normalizes ',' to '.'. */
29
96
  this.allowDecimals = input(false, ...(ngDevMode ? [{ debugName: "allowDecimals" }] : /* istanbul ignore next */ []));
97
+ /** Maximum number of characters the user can type. */
30
98
  this.maxLength = input(undefined, ...(ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []));
99
+ /** Placeholder text of the native input. */
31
100
  this.placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
101
+ /** Custom messages keyed by validation error name. */
32
102
  this.errorMessages = input({}, ...(ngDevMode ? [{ debugName: "errorMessages" }] : /* istanbul ignore next */ []));
103
+ /** Parent reactive form used to resolve the control by label. */
33
104
  this.formGroup = input(undefined, ...(ngDevMode ? [{ debugName: "formGroup" }] : /* istanbul ignore next */ []));
105
+ /** Reactive control bound directly to this field. */
34
106
  this.control = input(undefined, ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
107
+ /** Disables the field. */
35
108
  this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
109
+ /** Custom error message shown regardless of validation errors. */
36
110
  this.errorMessage = input('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
111
+ /** Renders a textarea instead of an input. */
37
112
  this.isTextArea = input(false, ...(ngDevMode ? [{ debugName: "isTextArea" }] : /* istanbul ignore next */ []));
113
+ /** Height classes applied when `isTextArea` is true. */
38
114
  this.textAreaHeight = input('h-28', ...(ngDevMode ? [{ debugName: "textAreaHeight" }] : /* istanbul ignore next */ []));
115
+ /** Renders the field as a read-only search trigger. */
39
116
  this.searchMode = input(false, ...(ngDevMode ? [{ debugName: "searchMode" }] : /* istanbul ignore next */ []));
117
+ /** Value shown when `searchMode` is enabled. */
40
118
  this.displayValue = input('', ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
119
+ /** Native autocomplete attribute. */
41
120
  this.autocomplete = input('', ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
121
+ /** Emitted when the search button is clicked (search mode). */
42
122
  this.searchButtonClick = output();
43
- this.getIcon = (name) => icons$9[name] ?? LucideEye;
123
+ this.getIcon = (name) => getIcon(name, LucideEye);
124
+ /** Reference to the native input element. */
44
125
  this.inputElement = viewChild('input', ...(ngDevMode ? [{ debugName: "inputElement" }] : /* istanbul ignore next */ []));
126
+ /** Reference to the native textarea element. */
45
127
  this.textareaElement = viewChild('textarea', ...(ngDevMode ? [{ debugName: "textareaElement" }] : /* istanbul ignore next */ []));
128
+ /** Internal raw value of the field. */
46
129
  this._value = signal('', ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
130
+ /** Reactive disabled state from the form control. */
47
131
  this._disabledState = signal(false, ...(ngDevMode ? [{ debugName: "_disabledState" }] : /* istanbul ignore next */ []));
132
+ /** Whether the password is currently revealed. */
48
133
  this.showPassword = signal(false, ...(ngDevMode ? [{ debugName: "showPassword" }] : /* istanbul ignore next */ []));
134
+ /** Tracks whether the control was in error at some point (for "was fixed" UX). */
49
135
  this.hadError = signal(false, ...(ngDevMode ? [{ debugName: "hadError" }] : /* istanbul ignore next */ []));
136
+ /** Counter bumped to trigger recomputation of derived error signals. */
50
137
  this.errorTracker = signal(0, ...(ngDevMode ? [{ debugName: "errorTracker" }] : /* istanbul ignore next */ []));
138
+ /** Text displayed in search mode when `displayValue` is set. */
51
139
  this.displayLabel = computed(() => {
52
140
  if (this.searchMode() && this.displayValue()) {
53
141
  return this.displayValue();
54
142
  }
55
143
  return '';
56
144
  }, ...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
145
+ /** Prevents re-subscribing to the same form control. */
57
146
  this.subscribed = signal(false, ...(ngDevMode ? [{ debugName: "subscribed" }] : /* istanbul ignore next */ []));
147
+ /** Effective disabled state combining the input and the form binding. */
58
148
  this.isDisabled = computed(() => this.disabled() || this._disabledState(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
149
+ /** Current field value. */
59
150
  this.value = computed(() => this._value(), ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
151
+ /** Resolves the reactive control: explicit `control` or `formGroup.get(label)`. */
60
152
  this.actualControl = computed(() => {
61
153
  const ctrl = this.control();
62
154
  if (ctrl)
@@ -68,16 +160,19 @@ class TextFieldComponent {
68
160
  }
69
161
  return undefined;
70
162
  }, ...(ngDevMode ? [{ debugName: "actualControl" }] : /* istanbul ignore next */ []));
163
+ /** True when the resolved control is invalid and dirty or touched. */
71
164
  this.shouldShowError = computed(() => {
72
165
  this.errorTracker();
73
166
  const ctrl = this.actualControl();
74
167
  return ctrl?.invalid && (ctrl?.dirty || ctrl?.touched);
75
168
  }, ...(ngDevMode ? [{ debugName: "shouldShowError" }] : /* istanbul ignore next */ []));
169
+ /** True when the control had an error before and is now valid (success state). */
76
170
  this.wasFixedError = computed(() => {
77
171
  this.errorTracker();
78
172
  const ctrl = this.actualControl();
79
173
  return this.hadError() && ctrl?.valid && ctrl?.touched;
80
174
  }, ...(ngDevMode ? [{ debugName: "wasFixedError" }] : /* istanbul ignore next */ []));
175
+ /** Resolved list of error messages for the current control state. */
81
176
  this.getErrorMessages = computed(() => {
82
177
  this.errorTracker();
83
178
  const customError = this.errorMessage();
@@ -112,6 +207,7 @@ class TextFieldComponent {
112
207
  }
113
208
  });
114
209
  }, ...(ngDevMode ? [{ debugName: "getErrorMessages" }] : /* istanbul ignore next */ []));
210
+ /** Effective input type (handles textarea and password reveal). */
115
211
  this.effectiveType = computed(() => {
116
212
  if (this.isTextArea())
117
213
  return 'text';
@@ -120,7 +216,9 @@ class TextFieldComponent {
120
216
  return this.type();
121
217
  }, ...(ngDevMode ? [{ debugName: "effectiveType" }] : /* istanbul ignore next */ []));
122
218
  this.transloco = inject(TranslocoService);
219
+ /** Callback notified with the new value (ControlValueAccessor). */
123
220
  this.onChange = () => { };
221
+ /** Callback notified when the field is touched (ControlValueAccessor). */
124
222
  this.onTouched = () => { };
125
223
  this.destroyRef = inject(DestroyRef);
126
224
  effect(() => {
@@ -160,6 +258,7 @@ class TextFieldComponent {
160
258
  this.errorTracker.update((v) => v + 1);
161
259
  });
162
260
  }
261
+ /** Prevents disallowed characters based on the `numbersOnly`/`allowDecimals` config. */
163
262
  numberValidation(event) {
164
263
  const allowedKeys = [
165
264
  'Backspace',
@@ -198,6 +297,7 @@ class TextFieldComponent {
198
297
  }
199
298
  event.preventDefault();
200
299
  }
300
+ /** Marks the field as touched and refreshes the error state. */
201
301
  onBlur() {
202
302
  this.onTouched();
203
303
  this.errorTracker.update((v) => v + 1);
@@ -206,6 +306,7 @@ class TextFieldComponent {
206
306
  this.hadError.set(true);
207
307
  }
208
308
  }
309
+ /** Formats the raw input as a credit card number and notifies the control. */
209
310
  onCardInput(event) {
210
311
  const input = event.target;
211
312
  let value = input.value.replace(/[^0-9]/g, '');
@@ -222,6 +323,7 @@ class TextFieldComponent {
222
323
  input.value = formatted;
223
324
  this.onChange(formatted);
224
325
  }
326
+ /** Handles raw input: applies number/card/max-length formatting and notifies the control. */
225
327
  onInput(event) {
226
328
  const input = event.target;
227
329
  let newValue = input.value;
@@ -255,9 +357,11 @@ class TextFieldComponent {
255
357
  this.onChange(newValue);
256
358
  }
257
359
  }
360
+ /** Toggles the visibility of a password field. */
258
361
  togglePasswordVisibility() {
259
362
  this.showPassword.update((v) => !v);
260
363
  }
364
+ /** Sets the value from the form control applying the same formatting rules. */
261
365
  writeValue(value) {
262
366
  let processedValue = value;
263
367
  if (this.allowDecimals() && value) {
@@ -284,12 +388,15 @@ class TextFieldComponent {
284
388
  this._value.set(processedValue);
285
389
  }
286
390
  }
391
+ /** Registers the callback invoked on value changes (ControlValueAccessor). */
287
392
  registerOnChange(fn) {
288
393
  this.onChange = fn;
289
394
  }
395
+ /** Registers the callback invoked when the field is touched. */
290
396
  registerOnTouched(fn) {
291
397
  this.onTouched = fn;
292
398
  }
399
+ /** Reflects the disabled state of the form control. */
293
400
  setDisabledState(isDisabled) {
294
401
  this._disabledState.set(isDisabled);
295
402
  this.cdr.markForCheck();
@@ -314,12 +421,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
314
421
  ], template: "<div class=\"relative mb-4\">\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n </div>\n }\n\n <div class=\"relative\">\n @if (!isTextArea()) {\n <div class=\"flex\">\n @if (searchMode()) {\n @let isDisabledSearch = actualControl()?.disabled || isDisabled();\n <input\n #input\n [id]=\"id()\"\n type=\"text\"\n [value]=\"displayValue() || ''\"\n [placeholder]=\"placeholder() || label()\"\n [readOnly]=\"true\"\n [disabled]=\"isDisabledSearch\"\n (click)=\"!isDisabledSearch && searchButtonClick.emit()\"\n (keydown.enter)=\"!isDisabledSearch && searchButtonClick.emit()\"\n (blur)=\"onBlur()\"\n autocomplete=\"off\"\n [class.cursor-pointer]=\"!isDisabledSearch\"\n [class.cursor-not-allowed]=\"isDisabledSearch\"\n [class._shk-bg-surface-50]=\"isDisabledSearch\"\n [class.dark:[background-color:var(--shk-surface-100)]]=\"isDisabledSearch\"\n [class._shk-text-surface-400]=\"isDisabledSearch\"\n [class.dark:[color:var(--shk-surface-400)]]=\"isDisabledSearch\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-l-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500': shouldShowError() && !isDisabledSearch,\n '_shk-border': !shouldShowError() || isDisabledSearch,\n 'cursor-pointer': !isDisabledSearch,\n 'cursor-not-allowed _shk-text-surface-400 dark:[color:var(--shk-surface-400)]': isDisabledSearch\n }\"\n />\n <button\n type=\"button\"\n (click)=\"searchButtonClick.emit()\"\n tabindex=\"-1\"\n [disabled]=\"isDisabledSearch\"\n class=\"px-3 flex items-center justify-center shrink-0 _shk-bg-primary hover:[background-color:var(--shk-primary-700)] text-white border _shk-border-primary rounded-r-lg transition-colors duration-200 disabled:[background-color:var(--shk-surface-300)] dark:disabled:[background-color:var(--shk-surface-200)] disabled:[border-color:var(--shk-surface-300)] dark:disabled:[border-color:var(--shk-surface-200)] disabled:cursor-not-allowed\"\n >\n <svg [lucideIcon]=\"getIcon('search')\" class=\"w-4 h-4\"></svg>\n </button>\n }\n\n @if (!searchMode()) {\n <input\n #input\n [id]=\"id()\"\n [maxlength]=\"maxLength() || (formatCard() ? 19 : null)\"\n [min]=\"type() === 'number' && preventNegative() ? '0' : null\"\n (keypress)=\"type() === 'number' || numbersOnly() ? numberValidation($event) : null\"\n [type]=\"effectiveType()\"\n [formControl]=\"$any(actualControl())\"\n [placeholder]=\"placeholder() || label()\"\n (input)=\"onInput($event)\"\n [autocomplete]=\"autocomplete()\"\n (blur)=\"onBlur()\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:outline-none focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] focus:ring-1 disabled:[background-color:var(--shk-surface-50)] dark:disabled:[background-color:var(--shk-surface-100)] disabled:cursor-not-allowed sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': shouldShowError(),\n 'border-green-500 focus:border-green-500 focus:ring-green-500': wasFixedError(),\n '_shk-border focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !shouldShowError() && !wasFixedError()\n }\"\n [class.pr-8]=\"type() === 'password' && actualControl()?.value?.length > 0\"\n [class.pr-3]=\"type() !== 'password' || !actualControl()?.value?.length\"\n />\n\n @if (type() === 'password' && actualControl()?.value?.length > 0) {\n <button\n type=\"button\"\n (click)=\"togglePasswordVisibility()\"\n [disabled]=\"actualControl()?.disabled\"\n class=\"absolute right-3 top-1/2 -translate-y-1/2 _shk-text-surface-400 dark:[color:var(--shk-surface-400)] hover:[color:var(--shk-surface-600)] dark:hover:[color:var(--shk-surface-600)] focus:outline-none\"\n >\n @if (showPassword()) {\n <svg [lucideIcon]=\"getIcon('eye-off')\" class=\"w-4 h-4\"></svg>\n } @else {\n <svg [lucideIcon]=\"getIcon('eye')\" class=\"w-4 h-4\"></svg>\n }\n </button>\n }\n }\n </div>\n }\n\n @if (isTextArea()) {\n <textarea\n #textarea\n [id]=\"id()\"\n [formControl]=\"$any(actualControl())\"\n [placeholder]=\"placeholder() || label()\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n [class]=\"isTextArea() ? textAreaHeight() : 'h-28'\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:outline-none focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] focus:ring-1 resize-none disabled:[background-color:var(--shk-surface-50)] dark:disabled:[background-color:var(--shk-surface-100)] disabled:cursor-not-allowed disabled:[color:var(--shk-surface-400)] dark:disabled:[color:var(--shk-surface-400)] sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': shouldShowError(),\n 'border-green-500 focus:border-green-500 focus:ring-green-500': wasFixedError(),\n '_shk-border focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !shouldShowError() && !wasFixedError()\n }\"\n ></textarea>\n }\n </div>\n\n @if (shouldShowError()) {\n <div class=\"mt-1 text-xs text-red-500 max-h-20 overflow-y-auto _shk-scrollbar\" tabindex=\"-1\">\n <ul class=\"list-disc pl-4 space-y-1\">\n @for (error of getErrorMessages(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n }\n</div>\n" }]
315
422
  }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], formatCard: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatCard", required: false }] }], preventNegative: [{ type: i0.Input, args: [{ isSignal: true, alias: "preventNegative", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], numbersOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "numbersOnly", required: false }] }], allowDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDecimals", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], isTextArea: [{ type: i0.Input, args: [{ isSignal: true, alias: "isTextArea", required: false }] }], textAreaHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "textAreaHeight", required: false }] }], searchMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchMode", required: false }] }], displayValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayValue", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], searchButtonClick: [{ type: i0.Output, args: ["searchButtonClick"] }], inputElement: [{ type: i0.ViewChild, args: ['input', { isSignal: true }] }], textareaElement: [{ type: i0.ViewChild, args: ['textarea', { isSignal: true }] }] } });
316
423
 
424
+ /**
425
+ * Central notification store. Components push/update/remove notifications
426
+ * reactively through signals for the notification component to render.
427
+ */
317
428
  class NotificationService {
318
429
  constructor() {
430
+ /** Reactive list of visible notifications. */
319
431
  this.notificationsSignal = signal([], ...(ngDevMode ? [{ debugName: "notificationsSignal" }] : /* istanbul ignore next */ []));
432
+ /** Monotonic id counter for new notifications. */
320
433
  this.currentId = 0;
434
+ /** Readonly reactive list of visible notifications. */
321
435
  this.notifications = this.notificationsSignal.asReadonly();
322
436
  }
437
+ /**
438
+ * Adds a notification and returns its id. When `showProgress` is false and
439
+ * `duration` is positive, it auto-removes after that many ms.
440
+ */
323
441
  addNotification(message, type = 'info', showProgress = false, duration = 5000) {
324
442
  const id = (++this.currentId).toString();
325
443
  const notification = {
@@ -338,6 +456,7 @@ class NotificationService {
338
456
  }
339
457
  return id;
340
458
  }
459
+ /** Updates a progress notification; auto-removes when progress hits 100. */
341
460
  updateProgress(notificationId, progress) {
342
461
  const currentNotifications = this.notificationsSignal();
343
462
  const index = currentNotifications.findIndex((n) => n.id === notificationId);
@@ -349,12 +468,15 @@ class NotificationService {
349
468
  }
350
469
  }
351
470
  }
471
+ /** Removes the given notification. */
352
472
  removeNotification(notification) {
353
473
  this.removeNotificationById(notification.id);
354
474
  }
475
+ /** Removes the notification with the given id. */
355
476
  removeNotificationById(notificationId) {
356
477
  this.notificationsSignal.update((notifications) => notifications.filter((n) => n.id !== notificationId));
357
478
  }
479
+ /** Removes every visible notification. */
358
480
  clearAll() {
359
481
  this.notificationsSignal.set([]);
360
482
  }
@@ -368,27 +490,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
368
490
  }]
369
491
  }] });
370
492
 
371
- const icons$8 = {
372
- check: LucideCheck,
373
- 'circle-x': LucideCircleX,
374
- info: LucideInfo,
375
- 'triangle-alert': LucideTriangleAlert,
376
- x: LucideX,
377
- };
493
+ /**
494
+ * Shirkasoft UI Components.
495
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
496
+ * Belongs to Shirkasoft.
497
+ */
498
+ /**
499
+ * Toast notification host that renders the notifications pushed through the
500
+ * NotificationService in the configured position.
501
+ */
378
502
  class NotificationComponent {
379
503
  constructor() {
504
+ /** Position of the notification stack: center, right or left top. */
380
505
  this.position = input('center-top', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
381
506
  this.notificationService = inject(NotificationService);
382
- this.getIcon = (name) => icons$8[name] ?? LucideInfo;
507
+ this.getIcon = (name) => getIcon(name, LucideInfo);
508
+ /** Maps each notification type to its internal icon name. */
383
509
  this.iconMap = {
384
510
  success: 'check',
385
511
  error: 'circle-x',
386
512
  info: 'info',
387
513
  warning: 'triangle-alert',
388
514
  };
515
+ /** Live list of active notifications. */
389
516
  this.notifications = this.notificationService.notifications;
517
+ /** Notifications sorted with the newest first. */
390
518
  this.sortedNotifications = computed(() => [...this.notifications()].reverse(), ...(ngDevMode ? [{ debugName: "sortedNotifications" }] : /* istanbul ignore next */ []));
519
+ /** Whether there are notifications to show. */
391
520
  this.hasNotifications = computed(() => this.notifications().length > 0, ...(ngDevMode ? [{ debugName: "hasNotifications" }] : /* istanbul ignore next */ []));
521
+ /** CSS classes positioning the stack according to `position`. */
392
522
  this.positionClass = computed(() => {
393
523
  const base = 'fixed top-4 z-[9999] space-y-4 sm:space-y-6 sm:top-6 lg:top-8';
394
524
  switch (this.position()) {
@@ -401,12 +531,15 @@ class NotificationComponent {
401
531
  }
402
532
  }, ...(ngDevMode ? [{ debugName: "positionClass" }] : /* istanbul ignore next */ []));
403
533
  }
534
+ /** Resolves the icon name for a notification type. */
404
535
  getNotificationIcon(type) {
405
536
  return this.iconMap[type] || 'info';
406
537
  }
538
+ /** Removes a notification through the service. */
407
539
  removeNotification(notification) {
408
540
  this.notificationService.removeNotification(notification);
409
541
  }
542
+ /** Rounds the progress value of a notification. */
410
543
  roundProgress(progress) {
411
544
  return Math.round(progress || 0);
412
545
  }
@@ -418,15 +551,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
418
551
  args: [{ selector: 'shk-notification', standalone: true, imports: [CommonModule, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div [class]=\"positionClass()\">\n @for (notification of sortedNotifications(); track notification.id) {\n <div\n [ngClass]=\"{\n 'bg-green-50/80 dark:bg-green-900/30 border-green-400 dark:border-green-600 text-green-800 dark:text-green-200':\n notification.type === 'success',\n 'bg-red-50/80 dark:bg-red-900/30 border-red-400 dark:border-red-600 text-red-800 dark:text-red-200':\n notification.type === 'error',\n 'bg-blue-50/80 dark:bg-blue-900/30 border-blue-400 dark:border-blue-600 text-blue-800 dark:text-blue-200':\n notification.type === 'info',\n 'bg-yellow-50/80 dark:bg-yellow-900/30 border-yellow-400 dark:border-yellow-500 text-yellow-800 dark:text-yellow-200':\n notification.type === 'warning',\n }\"\n class=\"flex items-center w-full min-w-[320px] max-w-xs sm:max-w-sm lg:max-w-md p-4 mb-4 border-l-4 rounded-lg shadow-lg backdrop-blur-sm transition-all duration-300 transform hover:scale-105 hover:shadow-xl\"\n role=\"alert\"\n >\n <div\n [ngClass]=\"{\n 'text-green-600': notification.type === 'success',\n 'text-red-600': notification.type === 'error',\n 'text-blue-600': notification.type === 'info',\n 'text-yellow-600': notification.type === 'warning',\n }\"\n class=\"inline-flex items-center justify-center flex-shrink-0 w-10 h-10 rounded-lg bg-opacity-10\"\n >\n <svg [lucideIcon]=\"getIcon(getNotificationIcon(notification.type))\" class=\"w-6 h-6\"></svg>\n </div>\n <div class=\"ms-4 flex-1 text-sm font-medium break-words\">\n {{ notification.message }}\n </div>\n <button\n type=\"button\"\n (click)=\"removeNotification(notification)\"\n aria-label=\"Close\"\n [ngClass]=\"{\n 'text-green-600 hover:text-green-800':\n notification.type === 'success',\n 'text-red-600 hover:text-red-800': notification.type === 'error',\n 'text-blue-600 hover:text-blue-800': notification.type === 'info',\n 'text-yellow-600 hover:text-yellow-800':\n notification.type === 'warning',\n }\"\n class=\"mx-2 -my-1.5 bg-transparent focus:outline-none\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n }\n</div>\n" }]
419
552
  }], propDecorators: { position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
420
553
 
554
+ /**
555
+ * Shirkasoft UI Components.
556
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
557
+ * Belongs to Shirkasoft.
558
+ */
559
+ /**
560
+ * Switch / checkbox component that implements ControlValueAccessor so it can
561
+ * be used with reactive and template-driven forms.
562
+ */
421
563
  class ToggleComponent {
422
564
  constructor() {
565
+ /** Text displayed next to the control. */
423
566
  this.label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
567
+ /** Visual style: 'toggle' (switch) or 'checkbox'. Defaults to 'toggle'. */
424
568
  this.mode = input('toggle', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
569
+ /** Manually controlled checked state (supports one-way binding). */
425
570
  this.checked = input(undefined, ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
571
+ /** Emitted with the new value when the user toggles the control. */
426
572
  this.checkedChange = output();
573
+ /** Internal controlled/reactive value. */
427
574
  this._value = signal(false, ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
575
+ /** Reactive disabled state. */
428
576
  this._disabled = signal(false, ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
577
+ /** Callback notified with the new value on change (ControlValueAccessor). */
429
578
  this.onChange = () => { };
579
+ /** Callback notified when the control loses focus or is touched. */
430
580
  this.onTouched = () => { };
431
581
  effect(() => {
432
582
  const manual = this.checked();
@@ -435,18 +585,26 @@ class ToggleComponent {
435
585
  }
436
586
  });
437
587
  }
588
+ /** Sets the value from the form control. */
438
589
  writeValue(value) {
439
590
  this._value.set(!!value);
440
591
  }
592
+ /** Registers the callback invoked on value changes (ControlValueAccessor). */
441
593
  registerOnChange(fn) {
442
594
  this.onChange = fn;
443
595
  }
596
+ /** Registers the callback invoked when the control is touched. */
444
597
  registerOnTouched(fn) {
445
598
  this.onTouched = fn;
446
599
  }
600
+ /** Reflects the disabled state of the form control. */
447
601
  setDisabledState(isDisabled) {
448
602
  this._disabled.set(isDisabled);
449
603
  }
604
+ /**
605
+ * Inverts the current value (unless disabled), notifies the form control,
606
+ * marks the control as touched and emits `checkedChange`.
607
+ */
450
608
  toggle() {
451
609
  if (this._disabled())
452
610
  return;
@@ -568,24 +726,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
568
726
  }]
569
727
  }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }], checkedChange: [{ type: i0.Output, args: ["checkedChange"] }] } });
570
728
 
729
+ /**
730
+ * Shirkasoft UI Components.
731
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
732
+ * Belongs to Shirkasoft.
733
+ */
734
+ /**
735
+ * Numeric input tailored for currency values with a optional percentage mode.
736
+ * Uses two-way bindable models: `value` and `isPercentage`.
737
+ */
571
738
  class PriceInputComponent {
572
739
  constructor() {
740
+ /** Numeric value, bindable with [(value)]. */
573
741
  this.value = model.required(...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
742
+ /** Whether the input works as a percentage, bindable with [(isPercentage)]. */
574
743
  this.isPercentage = model(false, ...(ngDevMode ? [{ debugName: "isPercentage" }] : /* istanbul ignore next */ []));
744
+ /** Label displayed above the field. */
575
745
  this.label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
746
+ /** id attribute applied to the native input. */
576
747
  this.inputId = input('', ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
748
+ /** Marks the field as required. */
577
749
  this.required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
750
+ /** Disables the input. */
578
751
  this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
752
+ /** Minimum allowed value. */
579
753
  this.min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
754
+ /** Maximum allowed value. */
580
755
  this.max = input(Infinity, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
756
+ /** Shows the error styling on the field. */
581
757
  this.showError = input(false, ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
758
+ /** Message rendered when the field is in error state. */
582
759
  this.errorMessage = input('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
583
760
  }
761
+ /** Toggles between amount and percentage modes (unless disabled). */
584
762
  toggleMode() {
585
763
  if (this.disabled())
586
764
  return;
587
765
  this.isPercentage.update(v => !v);
588
766
  }
767
+ /** Parses the user input and updates the bound `value`. */
589
768
  onInput(event) {
590
769
  const input = event.target;
591
770
  this.value.set(parseFloat(input.value) || 0);
@@ -598,38 +777,93 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
598
777
  args: [{ selector: 'shk-price-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div>\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n @if (required()) {\n <span class=\"text-red-400\">*</span>\n }\n </div>\n }\n <div class=\"flex\">\n <input\n [id]=\"inputId()\"\n type=\"number\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"value.set($event ?? 0)\"\n (input)=\"onInput($event)\"\n [disabled]=\"disabled()\"\n [min]=\"min()\"\n [max]=\"max()\"\n step=\"0.01\"\n class=\"block w-full px-3 py-2 _shk-text-surface-700 dark:[color:var(--shk-surface-700)] _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-l-lg transition-colors placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-400)] focus:outline-none focus:[background-color:var(--shk-surface-50)] dark:focus:[background-color:var(--shk-surface-100)] focus:ring-1 disabled:[background-color:var(--shk-surface-50)] dark:disabled:[background-color:var(--shk-surface-100)] disabled:cursor-not-allowed sm:text-sm\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': showError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !showError()\n }\"\n />\n <button\n type=\"button\"\n (click)=\"toggleMode()\"\n [disabled]=\"disabled()\"\n class=\"px-3 py-2 text-sm font-medium border rounded-r-lg transition-colors shrink-0\"\n [ngClass]=\"{\n '_shk-bg-primary-soft _shk-border-primary-200 _shk-text-primary-700 dark:[background-color:var(--shk-primary-900)] dark:[border-color:var(--shk-primary-700)] dark:[color:var(--shk-primary-300)]': isPercentage(),\n '_shk-bg-white _shk-border _shk-text-surface-500 dark:[background-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-200)] dark:[color:var(--shk-surface-400)]': !isPercentage(),\n 'cursor-not-allowed opacity-50': disabled()\n }\"\n >\n {{ isPercentage() ? '%' : '$' }}\n </button>\n </div>\n @if (showError()) {\n <div class=\"text-xs _shk-text-red-500 mt-1\">\n {{ errorMessage() }}\n </div>\n }\n</div>\n" }]
599
778
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], isPercentage: [{ type: i0.Input, args: [{ isSignal: true, alias: "isPercentage", required: false }] }, { type: i0.Output, args: ["isPercentageChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], showError: [{ type: i0.Input, args: [{ isSignal: true, alias: "showError", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }] } });
600
779
 
780
+ /**
781
+ * Shirkasoft UI Components.
782
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
783
+ * Belongs to Shirkasoft.
784
+ */
785
+ /**
786
+ * Date/time picker implemented as a ControlValueAccessor. Supports choosing a
787
+ * day, month or year, optional time selection (with seconds/12-hour format),
788
+ * min/max constraints and locale-aware labels.
789
+ */
601
790
  class DatePickerComponent {
602
791
  constructor() {
603
792
  this.elementRef = inject(ElementRef);
604
793
  this.cdr = inject(ChangeDetectorRef);
605
794
  this.destroyRef = inject(DestroyRef);
606
795
  this.transloco = inject(TranslocoService);
796
+ /** HTML id for the input (for label/for pairing). */
607
797
  this.id = input('', ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
798
+ /** Granularity of the picker ('date' | 'month' | 'year'). */
608
799
  this.view = input('date', ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
800
+ /** Label shown above the input. */
609
801
  this.label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
802
+ /** Placeholder of the input. */
610
803
  this.placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
804
+ /** FormGroup used to resolve the control from `label`. */
611
805
  this.formGroup = input(undefined, ...(ngDevMode ? [{ debugName: "formGroup" }] : /* istanbul ignore next */ []));
806
+ /** Directly bound FormControl. */
612
807
  this.control = input(undefined, ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
808
+ /** Disables the picker. */
613
809
  this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
810
+ /** Custom error message displayed (overrides translated ones). */
614
811
  this.errorMessage = input('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
812
+ /** Per-validation-key custom error messages. */
615
813
  this.errorMessages = input({}, ...(ngDevMode ? [{ debugName: "errorMessages" }] : /* istanbul ignore next */ []));
814
+ /** Minimum allowed date (YYYY-MM-DD) or year/month string. */
616
815
  this.minValue = input('', ...(ngDevMode ? [{ debugName: "minValue" }] : /* istanbul ignore next */ []));
816
+ /** Maximum allowed date (YYYY-MM-DD) or year/month string. */
617
817
  this.maxValue = input('', ...(ngDevMode ? [{ debugName: "maxValue" }] : /* istanbul ignore next */ []));
818
+ /** Locale used for month/day names and formatted labels. */
618
819
  this.locale = input('es', ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
820
+ /** Edits only the time portion of the value. */
821
+ this.timeOnly = input(false, ...(ngDevMode ? [{ debugName: "timeOnly" }] : /* istanbul ignore next */ []));
822
+ /** Shows the time selectors alongside the date. */
823
+ this.showTime = input(false, ...(ngDevMode ? [{ debugName: "showTime" }] : /* istanbul ignore next */ []));
824
+ /** Hour format: '12' or '24'. */
825
+ this.hourFormat = input('24', ...(ngDevMode ? [{ debugName: "hourFormat" }] : /* istanbul ignore next */ []));
826
+ /** Whether the seconds selector is shown. */
827
+ this.showSeconds = input(false, ...(ngDevMode ? [{ debugName: "showSeconds" }] : /* istanbul ignore next */ []));
828
+ /** Step (in minutes) for the minute selector. */
829
+ this.minuteStep = input(1, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
830
+ /** Whether the dropdown calendar is open. */
619
831
  this.isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
832
+ /** Year currently displayed in the calendar. */
620
833
  this.viewYear = signal(new Date().getFullYear(), ...(ngDevMode ? [{ debugName: "viewYear" }] : /* istanbul ignore next */ []));
834
+ /** Month currently displayed in the calendar (0-based). */
621
835
  this.viewMonth = signal(new Date().getMonth(), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
836
+ /** Starting year of the decade shown in year view. */
622
837
  this.viewDecade = signal(Math.floor(new Date().getFullYear() / 10) * 10, ...(ngDevMode ? [{ debugName: "viewDecade" }] : /* istanbul ignore next */ []));
838
+ /** Hour selected in the time selectors. */
839
+ this.viewHour = signal(0, ...(ngDevMode ? [{ debugName: "viewHour" }] : /* istanbul ignore next */ []));
840
+ /** Minute selected in the time selectors. */
841
+ this.viewMinute = signal(0, ...(ngDevMode ? [{ debugName: "viewMinute" }] : /* istanbul ignore next */ []));
842
+ /** Second selected in the time selectors. */
843
+ this.viewSecond = signal(0, ...(ngDevMode ? [{ debugName: "viewSecond" }] : /* istanbul ignore next */ []));
844
+ /** Meridiem selected in 12-hour format. */
845
+ this.viewMeridiem = signal('AM', ...(ngDevMode ? [{ debugName: "viewMeridiem" }] : /* istanbul ignore next */ []));
846
+ /** Date part selected but not yet committed until the time is confirmed. */
847
+ this.pendingDate = signal(null, ...(ngDevMode ? [{ debugName: "pendingDate" }] : /* istanbul ignore next */ []));
848
+ /** Underlying value (mirrors the reactive control). */
623
849
  this._value = signal(null, ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
850
+ /** Disabled state coming from the reactive form infrastructure. */
624
851
  this._disabledState = signal(false, ...(ngDevMode ? [{ debugName: "_disabledState" }] : /* istanbul ignore next */ []));
852
+ /** Whether status subscriptions have been attached to the control. */
625
853
  this.subscribed = signal(false, ...(ngDevMode ? [{ debugName: "subscribed" }] : /* istanbul ignore next */ []));
626
854
  this.valueSubscribed = signal(false, ...(ngDevMode ? [{ debugName: "valueSubscribed" }] : /* istanbul ignore next */ []));
855
+ /** Counter used to recompute the error helpers reactively. */
627
856
  this.errorTracker = signal(0, ...(ngDevMode ? [{ debugName: "errorTracker" }] : /* istanbul ignore next */ []));
857
+ /** Whether an error was displayed at some point. */
628
858
  this.hadError = signal(false, ...(ngDevMode ? [{ debugName: "hadError" }] : /* istanbul ignore next */ []));
859
+ /** ControlValueAccessor callbacks. */
629
860
  this.onChange = () => { };
630
861
  this.onTouched = () => { };
862
+ /** Current value of the picker. */
631
863
  this.value = computed(() => this._value(), ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
864
+ /** Whether the picker is disabled (input or reactive form). */
632
865
  this.isDisabled = computed(() => this.disabled() || this._disabledState(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
866
+ /** Resolves the control from `control` input or `formGroup`+`label`. */
633
867
  this.actualControl = computed(() => {
634
868
  const ctrl = this.control();
635
869
  if (ctrl)
@@ -640,16 +874,19 @@ class DatePickerComponent {
640
874
  return fg.get(lbl);
641
875
  return undefined;
642
876
  }, ...(ngDevMode ? [{ debugName: "actualControl" }] : /* istanbul ignore next */ []));
877
+ /** Whether the bound control should show its error message. */
643
878
  this.shouldShowError = computed(() => {
644
879
  this.errorTracker();
645
880
  const ctrl = this.actualControl();
646
881
  return ctrl?.invalid && (ctrl?.dirty || ctrl?.touched);
647
882
  }, ...(ngDevMode ? [{ debugName: "shouldShowError" }] : /* istanbul ignore next */ []));
883
+ /** Whether an error was fixed (invalid before, now valid and touched). */
648
884
  this.wasFixedError = computed(() => {
649
885
  this.errorTracker();
650
886
  const ctrl = this.actualControl();
651
887
  return this.hadError() && ctrl?.valid && ctrl?.touched;
652
888
  }, ...(ngDevMode ? [{ debugName: "wasFixedError" }] : /* istanbul ignore next */ []));
889
+ /** Validation error messages to display, custom or translated. */
653
890
  this.getErrorMessages = computed(() => {
654
891
  this.errorTracker();
655
892
  const customError = this.errorMessage();
@@ -671,28 +908,54 @@ class DatePickerComponent {
671
908
  }
672
909
  });
673
910
  }, ...(ngDevMode ? [{ debugName: "getErrorMessages" }] : /* istanbul ignore next */ []));
911
+ /** Date part displayed in the calendar (pending date or committed value). */
912
+ this.displayedDate = computed(() => {
913
+ if (this.timeOnly())
914
+ return null;
915
+ const value = this.pendingDate();
916
+ if (value)
917
+ return value;
918
+ const current = this._value();
919
+ if (!current)
920
+ return null;
921
+ return this.datePartOf(current);
922
+ }, ...(ngDevMode ? [{ debugName: "displayedDate" }] : /* istanbul ignore next */ []));
923
+ /** Human-readable label for the selected value in the input. */
674
924
  this.displayLabel = computed(() => {
675
925
  const value = this._value();
676
926
  if (!value)
677
927
  return '';
928
+ if (this.timeOnly()) {
929
+ return this.formatTimeFromValue(value);
930
+ }
931
+ const datePart = this.datePartOf(value);
932
+ const timePart = value.includes('T') ? value.split('T')[1] : null;
678
933
  const v = this.view();
934
+ let label;
679
935
  if (v === 'year') {
680
- return value;
936
+ label = datePart;
681
937
  }
682
- if (v === 'month') {
683
- const [y, m] = value.split('-').map(Number);
938
+ else if (v === 'month') {
939
+ const [y, m] = datePart.split('-').map(Number);
684
940
  const fmt = new Intl.DateTimeFormat(this.locale(), {
685
941
  month: 'long',
686
942
  year: 'numeric',
687
943
  });
688
- const label = fmt.format(new Date(y, m - 1, 1));
689
- return label.charAt(0).toUpperCase() + label.slice(1);
944
+ label = fmt.format(new Date(y, m - 1, 1));
945
+ label = label.charAt(0).toUpperCase() + label.slice(1);
690
946
  }
691
- const fmt = new Intl.DateTimeFormat(this.locale(), {
692
- dateStyle: 'long',
693
- });
694
- return fmt.format(new Date(value + 'T12:00:00'));
947
+ else {
948
+ const fmt = new Intl.DateTimeFormat(this.locale(), {
949
+ dateStyle: 'long',
950
+ });
951
+ label = fmt.format(new Date(datePart + 'T12:00:00'));
952
+ }
953
+ if (this.showTime() && timePart) {
954
+ label += ' ' + this.formatTimeFromValue(timePart);
955
+ }
956
+ return label;
695
957
  }, ...(ngDevMode ? [{ debugName: "displayLabel" }] : /* istanbul ignore next */ []));
958
+ /** Localized short month names for the year view. */
696
959
  this.monthNames = computed(() => {
697
960
  const fmt = new Intl.DateTimeFormat(this.locale(), { month: 'short' });
698
961
  return Array.from({ length: 12 }, (_, i) => {
@@ -700,6 +963,7 @@ class DatePickerComponent {
700
963
  return name.charAt(0).toUpperCase() + name.slice(1);
701
964
  });
702
965
  }, ...(ngDevMode ? [{ debugName: "monthNames" }] : /* istanbul ignore next */ []));
966
+ /** Localized short weekday names for the calendar header. */
703
967
  this.dayNames = computed(() => {
704
968
  const fmt = new Intl.DateTimeFormat(this.locale(), { weekday: 'short' });
705
969
  return Array.from({ length: 7 }, (_, i) => {
@@ -707,12 +971,15 @@ class DatePickerComponent {
707
971
  return name.charAt(0).toUpperCase() + name.slice(1, 3);
708
972
  });
709
973
  }, ...(ngDevMode ? [{ debugName: "dayNames" }] : /* istanbul ignore next */ []));
974
+ /** Number of days in the currently viewed month. */
710
975
  this.daysInMonth = computed(() => {
711
976
  return new Date(this.viewYear(), this.viewMonth() + 1, 0).getDate();
712
977
  }, ...(ngDevMode ? [{ debugName: "daysInMonth" }] : /* istanbul ignore next */ []));
978
+ /** Weekday index (0=Sunday) of the first day of the viewed month. */
713
979
  this.firstDayOfMonth = computed(() => {
714
980
  return new Date(this.viewYear(), this.viewMonth(), 1).getDay();
715
981
  }, ...(ngDevMode ? [{ debugName: "firstDayOfMonth" }] : /* istanbul ignore next */ []));
982
+ /** Calendar grid: nulls for leading blanks then the days of the month. */
716
983
  this.calendarDays = computed(() => {
717
984
  const totalDays = this.daysInMonth();
718
985
  const firstDay = this.firstDayOfMonth();
@@ -723,6 +990,7 @@ class DatePickerComponent {
723
990
  days.push(i);
724
991
  return days;
725
992
  }, ...(ngDevMode ? [{ debugName: "calendarDays" }] : /* istanbul ignore next */ []));
993
+ /** Whether the prev navigation button is allowed by `minValue`. */
726
994
  this.canGoPrev = computed(() => {
727
995
  const v = this.view();
728
996
  const min = this.minValue();
@@ -739,6 +1007,7 @@ class DatePickerComponent {
739
1007
  const minY = min ? Number(min) : undefined;
740
1008
  return minY === undefined || this.viewYear() > minY;
741
1009
  }, ...(ngDevMode ? [{ debugName: "canGoPrev" }] : /* istanbul ignore next */ []));
1010
+ /** Whether the next navigation button is allowed by `maxValue`. */
742
1011
  this.canGoNext = computed(() => {
743
1012
  const v = this.view();
744
1013
  const max = this.maxValue();
@@ -755,23 +1024,29 @@ class DatePickerComponent {
755
1024
  const maxY = max ? Number(max) : undefined;
756
1025
  return maxY === undefined || this.viewYear() < maxY;
757
1026
  }, ...(ngDevMode ? [{ debugName: "canGoNext" }] : /* istanbul ignore next */ []));
1027
+ /** Year of the displayed date, if any. */
758
1028
  this.selectedYear = computed(() => {
759
- const value = this._value();
1029
+ const value = this.displayedDate();
760
1030
  return value ? Number(value.split('-')[0]) : null;
761
1031
  }, ...(ngDevMode ? [{ debugName: "selectedYear" }] : /* istanbul ignore next */ []));
1032
+ /** Month index (0-based) of the displayed date, if any. */
762
1033
  this.selectedMonth = computed(() => {
763
- const value = this._value();
1034
+ const value = this.displayedDate();
764
1035
  return value ? Number(value.split('-')[1]) - 1 : null;
765
1036
  }, ...(ngDevMode ? [{ debugName: "selectedMonth" }] : /* istanbul ignore next */ []));
1037
+ /** Day of the displayed date, if any (date view only). */
766
1038
  this.selectedDay = computed(() => {
767
- const value = this._value();
1039
+ const value = this.displayedDate();
768
1040
  return value && this.view() === 'date' ? Number(value.split('-')[2]) : null;
769
1041
  }, ...(ngDevMode ? [{ debugName: "selectedDay" }] : /* istanbul ignore next */ []));
1042
+ /** 12 years of the viewed decade for the year view. */
770
1043
  this.decadeYears = computed(() => {
771
1044
  const start = this.viewDecade();
772
1045
  return Array.from({ length: 12 }, (_, i) => start + i);
773
1046
  }, ...(ngDevMode ? [{ debugName: "decadeYears" }] : /* istanbul ignore next */ []));
1047
+ /** (Unused legacy placeholder) plain year list. */
774
1048
  this.years = [];
1049
+ // Attach error-state subscriptions to the validated control once found.
775
1050
  effect(() => {
776
1051
  const ctrl = this.actualControl();
777
1052
  if (!ctrl) {
@@ -791,6 +1066,7 @@ class DatePickerComponent {
791
1066
  });
792
1067
  this.subscribed.set(true);
793
1068
  });
1069
+ // One-way sync from the reactive control to the internal value signal.
794
1070
  effect(() => {
795
1071
  const ctrl = this.actualControl();
796
1072
  if (!ctrl)
@@ -806,6 +1082,7 @@ class DatePickerComponent {
806
1082
  }
807
1083
  });
808
1084
  }
1085
+ /** Closes the dropdown when clicking outside the component. */
809
1086
  onDocumentClick(event) {
810
1087
  if (!this.isOpen())
811
1088
  return;
@@ -813,9 +1090,11 @@ class DatePickerComponent {
813
1090
  this.close();
814
1091
  }
815
1092
  }
1093
+ /** Closes the dropdown on Escape. */
816
1094
  onEscape() {
817
1095
  this.close();
818
1096
  }
1097
+ /** Opens/closes the dropdown, syncing the calendar to the current value. */
819
1098
  toggle() {
820
1099
  if (this.isDisabled())
821
1100
  return;
@@ -827,6 +1106,7 @@ class DatePickerComponent {
827
1106
  this.isOpen.set(true);
828
1107
  }
829
1108
  }
1109
+ /** Aligns the calendar view signals with the current value (or today). */
830
1110
  syncViewToValue() {
831
1111
  const val = this._value();
832
1112
  const v = this.view();
@@ -835,20 +1115,38 @@ class DatePickerComponent {
835
1115
  this.viewYear.set(now.getFullYear());
836
1116
  this.viewMonth.set(now.getMonth());
837
1117
  this.viewDecade.set(Math.floor(now.getFullYear() / 10) * 10);
1118
+ this.viewHour.set(now.getHours());
1119
+ this.viewMinute.set(now.getMinutes());
1120
+ this.viewSecond.set(now.getSeconds());
1121
+ this.pendingDate.set(this.toISODate(now));
838
1122
  return;
839
1123
  }
1124
+ const datePart = this.datePartOf(val);
1125
+ const timePart = val.includes('T') ? val.split('T')[1] : undefined;
840
1126
  if (v === 'year') {
841
- this.viewYear.set(Number(val));
1127
+ this.viewYear.set(Number(datePart));
842
1128
  }
843
1129
  else if (v === 'month') {
844
- this.viewYear.set(Number(val.split('-')[0]));
1130
+ this.viewYear.set(Number(datePart.split('-')[0]));
845
1131
  }
846
1132
  else {
847
- const d = new Date(val + 'T12:00:00');
1133
+ const d = new Date(datePart + 'T12:00:00');
848
1134
  this.viewYear.set(d.getFullYear());
849
1135
  this.viewMonth.set(d.getMonth());
850
1136
  }
1137
+ const time = timePart ?? (this.timeOnly() ? val : undefined);
1138
+ if (time) {
1139
+ const [h, m, ss] = time.split(':').map(Number);
1140
+ this.viewHour.set(h || 0);
1141
+ this.viewMinute.set(m || 0);
1142
+ this.viewSecond.set(ss || 0);
1143
+ if (this.hourFormat() === '12') {
1144
+ this.viewMeridiem.set(h >= 12 ? 'PM' : 'AM');
1145
+ }
1146
+ }
1147
+ this.pendingDate.set(datePart);
851
1148
  }
1149
+ /** Closes the dropdown, marks the control as touched and refreshes errors. */
852
1150
  close() {
853
1151
  if (!this.isOpen())
854
1152
  return;
@@ -861,6 +1159,7 @@ class DatePickerComponent {
861
1159
  if (ctrl?.invalid && ctrl?.touched)
862
1160
  this.hadError.set(true);
863
1161
  }
1162
+ /** Navigates the calendar to the previous year/month/decade. */
864
1163
  prev() {
865
1164
  const v = this.view();
866
1165
  if (v === 'year') {
@@ -880,6 +1179,7 @@ class DatePickerComponent {
880
1179
  });
881
1180
  }
882
1181
  }
1182
+ /** Navigates the calendar to the next year/month/decade. */
883
1183
  next() {
884
1184
  const v = this.view();
885
1185
  if (v === 'year') {
@@ -899,22 +1199,38 @@ class DatePickerComponent {
899
1199
  });
900
1200
  }
901
1201
  }
1202
+ /** Selects a day of the month (pending if time is shown). */
902
1203
  selectDay(day) {
903
1204
  const y = this.viewYear();
904
1205
  const m = String(this.viewMonth() + 1).padStart(2, '0');
905
1206
  const d = String(day).padStart(2, '0');
906
1207
  const value = `${y}-${m}-${d}`;
1208
+ if (this.showTime()) {
1209
+ this.pendingDate.set(value);
1210
+ return;
1211
+ }
907
1212
  this.setValue(value);
908
1213
  }
1214
+ /** Selects a month of the year (pending if time is shown). */
909
1215
  selectMonth(monthIndex) {
910
1216
  const value = `${this.viewYear()}-${String(monthIndex + 1).padStart(2, '0')}`;
1217
+ if (this.showTime()) {
1218
+ this.pendingDate.set(value);
1219
+ return;
1220
+ }
911
1221
  this.setValue(value);
912
1222
  }
1223
+ /** Selects a year (pending if time is shown). */
913
1224
  selectYear(year) {
914
1225
  const value = String(year);
1226
+ if (this.showTime()) {
1227
+ this.pendingDate.set(value);
1228
+ return;
1229
+ }
915
1230
  this.setValue(value);
916
1231
  }
917
- setValue(value) {
1232
+ /** Commits a value, updates the component, the control and the outputs. */
1233
+ setValue(value, close = true) {
918
1234
  this._value.set(value);
919
1235
  this.onChange(value);
920
1236
  const ctrl = this.actualControl();
@@ -922,19 +1238,115 @@ class DatePickerComponent {
922
1238
  ctrl.setValue(value);
923
1239
  ctrl.markAsDirty();
924
1240
  }
925
- this.close();
1241
+ if (close)
1242
+ this.close();
926
1243
  }
1244
+ /** Confirms the selected time and commits the combined date+time value. */
1245
+ confirmTime() {
1246
+ const h = this.viewHour();
1247
+ const m = this.viewMinute();
1248
+ const s = this.viewSecond();
1249
+ let time = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
1250
+ if (this.showSeconds())
1251
+ time += `:${String(s).padStart(2, '0')}`;
1252
+ if (this.timeOnly()) {
1253
+ this.setValue(time);
1254
+ return;
1255
+ }
1256
+ const datePart = this.pendingDate() ??
1257
+ `${this.viewYear()}-${String(this.viewMonth() + 1).padStart(2, '0')}-01`;
1258
+ this.setValue(`${datePart}T${time}`);
1259
+ }
1260
+ /** Reads the hour selected in the time selector. */
1261
+ onHourChange(event) {
1262
+ this.viewHour.set(Number(event.target.value));
1263
+ }
1264
+ /** Reads the minute selected in the time selector. */
1265
+ onMinuteChange(event) {
1266
+ this.viewMinute.set(Number(event.target.value));
1267
+ }
1268
+ /** Reads the second selected in the time selector. */
1269
+ onSecondChange(event) {
1270
+ this.viewSecond.set(Number(event.target.value));
1271
+ }
1272
+ /** Current meridiem ("AM"/"PM") for the 12-hour format. */
1273
+ meridiem() {
1274
+ return this.viewMeridiem();
1275
+ }
1276
+ /** Reads the meridiem selected in the time selector. */
1277
+ onMeridiemChange(event) {
1278
+ this.viewMeridiem.set(event.target.value);
1279
+ }
1280
+ /** Splits the date part from a full ISO "date" or "dateTtime" value. */
1281
+ datePartOf(value) {
1282
+ const i = value.indexOf('T');
1283
+ return i >= 0 ? value.slice(0, i) : value;
1284
+ }
1285
+ /** Formats a raw time string ("HH:mm[:ss]") using the configured format. */
1286
+ formatTimeFromValue(time) {
1287
+ const [h, m, ss] = time.split(':').map(Number);
1288
+ return this.formatTime(h || 0, m || 0, ss || 0);
1289
+ }
1290
+ /** Formats hour/minute/second according to hour format and seconds option. */
1291
+ formatTime(h, m, s) {
1292
+ if (this.hourFormat() === '12') {
1293
+ const h12 = h % 12 || 12;
1294
+ const suffix = h < 12 ? ' AM' : ' PM';
1295
+ return `${String(h12).padStart(2, '0')}:${String(m).padStart(2, '0')}${suffix}`;
1296
+ }
1297
+ let label = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
1298
+ if (this.showSeconds())
1299
+ label += `:${String(s).padStart(2, '0')}`;
1300
+ return label;
1301
+ }
1302
+ /** Formats a Date as a YYYY-MM-DD ISO string. */
1303
+ toISODate(date) {
1304
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
1305
+ }
1306
+ /** Whether the given day matches the currently selected date. */
927
1307
  isSelectedDay(day) {
928
1308
  return (this.selectedYear() === this.viewYear() &&
929
1309
  this.selectedMonth() === this.viewMonth() &&
930
1310
  this.selectedDay() === day);
931
1311
  }
1312
+ /** Hours available in the time selector ('12' maps to 0..11 or 12..23). */
1313
+ hoursRange() {
1314
+ if (this.hourFormat() === '12') {
1315
+ const oneToTwelve = Array.from({ length: 12 }, (_, i) => i + 1);
1316
+ return this.viewMeridiem() === 'PM'
1317
+ ? oneToTwelve.map((h) => (h === 12 ? 12 : h + 12))
1318
+ : oneToTwelve.map((h) => (h === 12 ? 0 : h));
1319
+ }
1320
+ return Array.from({ length: 24 }, (_, i) => i);
1321
+ }
1322
+ /** Minutes available in the time selector, stepped by `minuteStep`. */
1323
+ minutesRange() {
1324
+ const step = this.minuteStep();
1325
+ return Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step);
1326
+ }
1327
+ /** Seconds available in the time selector (0..59). */
1328
+ secondsRange() {
1329
+ return Array.from({ length: 60 }, (_, i) => i);
1330
+ }
1331
+ /** Formats an hour for display in the selector (12-hour aware). */
1332
+ hourDisplay(hour) {
1333
+ if (this.hourFormat() === '12') {
1334
+ return String(hour % 12 || 12).padStart(2, '0');
1335
+ }
1336
+ return String(hour).padStart(2, '0');
1337
+ }
1338
+ /** Pads a minute value with a leading zero. */
1339
+ minutesLabel(value) {
1340
+ return String(value).padStart(2, '0');
1341
+ }
1342
+ /** Whether the given day is today's date. */
932
1343
  isToday(day) {
933
1344
  const now = new Date();
934
1345
  return (this.viewYear() === now.getFullYear() &&
935
1346
  this.viewMonth() === now.getMonth() &&
936
1347
  day === now.getDate());
937
1348
  }
1349
+ /** Whether the given month matches the selected month and year. */
938
1350
  isSelectedMonth(monthIndex) {
939
1351
  return (this.selectedYear() === this.viewYear() &&
940
1352
  this.selectedMonth() === monthIndex);
@@ -963,13 +1375,13 @@ class DatePickerComponent {
963
1375
  this.cdr.markForCheck();
964
1376
  }
965
1377
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DatePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
966
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: DatePickerComponent, isStandalone: true, selector: "shk-date-picker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null }, minValue: { classPropertyName: "minValue", publicName: "minValue", isSignal: true, isRequired: false, transformFunction: null }, maxValue: { classPropertyName: "maxValue", publicName: "maxValue", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, providers: [
1378
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: DatePickerComponent, isStandalone: true, selector: "shk-date-picker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, formGroup: { classPropertyName: "formGroup", publicName: "formGroup", isSignal: true, isRequired: false, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null }, minValue: { classPropertyName: "minValue", publicName: "minValue", isSignal: true, isRequired: false, transformFunction: null }, maxValue: { classPropertyName: "maxValue", publicName: "maxValue", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, timeOnly: { classPropertyName: "timeOnly", publicName: "timeOnly", isSignal: true, isRequired: false, transformFunction: null }, showTime: { classPropertyName: "showTime", publicName: "showTime", isSignal: true, isRequired: false, transformFunction: null }, hourFormat: { classPropertyName: "hourFormat", publicName: "hourFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, providers: [
967
1379
  {
968
1380
  provide: NG_VALUE_ACCESSOR,
969
1381
  useExisting: forwardRef(() => DatePickerComponent),
970
1382
  multi: true,
971
1383
  },
972
- ], ngImport: i0, template: "<div class=\"relative mb-4\">\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n </div>\n }\n\n <div class=\"relative\">\n <button\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n class=\"flex items-center justify-between w-full px-3 py-2 text-left _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-lg transition-colors sm:text-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:[color:var(--shk-surface-400)] dark:disabled:[color:var(--shk-surface-500)]\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': shouldShowError(),\n 'border-green-500 focus:border-green-500 focus:ring-green-500': wasFixedError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !shouldShowError() && !wasFixedError(),\n }\"\n >\n <span\n [ngClass]=\"value() ? '_shk-text-surface-700 dark:[color:var(--shk-surface-700)]' : '_shk-text-surface-400 dark:[color:var(--shk-surface-400)]'\"\n >\n {{ displayLabel() || placeholder() || label() }}\n </span>\n <svg\n class=\"w-4 h-4 _shk-text-surface-400 dark:[color:var(--shk-surface-400)] shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"></rect>\n <path d=\"M16 2v4M8 2v4M3 10h18\"></path>\n </svg>\n </button>\n\n @if (isOpen()) {\n <div class=\"fixed inset-0 z-40 bg-black/30\" (click)=\"close()\"></div>\n <div\n class=\"fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-72 p-4 bg-white dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-lg shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (view() === 'date') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ monthNames()[viewMonth()] }} {{ viewYear() }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-7 gap-0 mb-1\">\n @for (name of dayNames(); track $index) {\n <div class=\"text-center text-xs font-medium _shk-text-surface-400 dark:[color:var(--shk-surface-400)] py-1\">\n {{ name }}\n </div>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-0\">\n @for (day of calendarDays(); track $index) {\n @if (day !== null) {\n <button\n type=\"button\"\n (click)=\"selectDay(day)\"\n class=\"py-1.5 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedDay(day),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isToday(day) && !isSelectedDay(day),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedDay(day) && !isToday(day),\n }\"\n >\n {{ day }}\n </button>\n } @else {\n <div></div>\n }\n }\n </div>\n }\n\n @if (view() === 'month') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">{{ viewYear() }}</span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (name of monthNames(); track $index) {\n <button\n type=\"button\"\n (click)=\"selectMonth($index)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedMonth($index),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentMonth($index) && !isSelectedMonth($index),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedMonth($index) && !isCurrentMonth($index),\n }\"\n >\n {{ name }}\n </button>\n }\n </div>\n }\n\n @if (view() === 'year') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ viewDecade() }} - {{ viewDecade() + 11 }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (year of decadeYears(); track year) {\n <button\n type=\"button\"\n (click)=\"selectYear(year)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedYear(year),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentYear(year) && !isSelectedYear(year),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedYear(year) && !isCurrentYear(year),\n }\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (shouldShowError()) {\n <div class=\"mt-1 text-xs text-red-500 max-h-20 overflow-y-auto _shk-scrollbar\">\n <ul class=\"list-disc pl-4 space-y-1\">\n @for (error of getErrorMessages(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1384
+ ], ngImport: i0, template: "<div class=\"relative mb-4\">\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n </div>\n }\n\n <div class=\"relative\">\n <button\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n class=\"flex items-center justify-between w-full px-3 py-2 text-left _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-lg transition-colors sm:text-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:[color:var(--shk-surface-400)] dark:disabled:[color:var(--shk-surface-500)]\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': shouldShowError(),\n 'border-green-500 focus:border-green-500 focus:ring-green-500': wasFixedError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !shouldShowError() && !wasFixedError(),\n }\"\n >\n <span\n [ngClass]=\"value() ? '_shk-text-surface-700 dark:[color:var(--shk-surface-700)]' : '_shk-text-surface-400 dark:[color:var(--shk-surface-400)]'\"\n >\n {{ displayLabel() || placeholder() || label() }}\n </span>\n <svg\n class=\"w-4 h-4 _shk-text-surface-400 dark:[color:var(--shk-surface-400)] shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"></rect>\n <path d=\"M16 2v4M8 2v4M3 10h18\"></path>\n </svg>\n </button>\n\n @if (isOpen()) {\n <div class=\"fixed inset-0 z-40 bg-black/30\" (click)=\"close()\"></div>\n <div\n class=\"fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-72 p-4 bg-white dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-lg shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (!timeOnly() && view() === 'date') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ monthNames()[viewMonth()] }} {{ viewYear() }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-7 gap-0 mb-1\">\n @for (name of dayNames(); track $index) {\n <div class=\"text-center text-xs font-medium _shk-text-surface-400 dark:[color:var(--shk-surface-400)] py-1\">\n {{ name }}\n </div>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-0\">\n @for (day of calendarDays(); track $index) {\n @if (day !== null) {\n <button\n type=\"button\"\n (click)=\"selectDay(day)\"\n class=\"py-1.5 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedDay(day),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isToday(day) && !isSelectedDay(day),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedDay(day) && !isToday(day),\n }\"\n >\n {{ day }}\n </button>\n } @else {\n <div></div>\n }\n }\n </div>\n }\n\n @if (!timeOnly() && view() === 'month') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">{{ viewYear() }}</span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (name of monthNames(); track $index) {\n <button\n type=\"button\"\n (click)=\"selectMonth($index)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedMonth($index),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentMonth($index) && !isSelectedMonth($index),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedMonth($index) && !isCurrentMonth($index),\n }\"\n >\n {{ name }}\n </button>\n }\n </div>\n }\n\n @if (!timeOnly() && view() === 'year') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ viewDecade() }} - {{ viewDecade() + 11 }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (year of decadeYears(); track year) {\n <button\n type=\"button\"\n (click)=\"selectYear(year)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedYear(year),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentYear(year) && !isSelectedYear(year),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedYear(year) && !isCurrentYear(year),\n }\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n\n @if (timeOnly() || showTime()) {\n <div class=\"mt-3 pt-3 _shk-border dark:[border-color:var(--shk-surface-200)]\">\n <div class=\"flex items-center justify-center gap-1\">\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"viewHour()\"\n (change)=\"onHourChange($event)\"\n aria-label=\"Hora\"\n >\n @for (h of hoursRange(); track h) {\n <option [value]=\"h\">{{ hourDisplay(h) }}</option>\n }\n </select>\n <span class=\"_shk-text-surface-400\">:</span>\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"viewMinute()\"\n (change)=\"onMinuteChange($event)\"\n aria-label=\"Minutos\"\n >\n @for (minutes of minutesRange(); track minutes) {\n <option [value]=\"minutes\">{{ minutesLabel(minutes) }}</option>\n }\n </select>\n @if (showSeconds()) {\n <span class=\"text-sm _shk-text-surface-400\">:</span>\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"viewSecond()\"\n (change)=\"onSecondChange($event)\"\n aria-label=\"Segundos\"\n >\n @for (seconds of secondsRange(); track seconds) {\n <option [value]=\"seconds\">{{ minutesLabel(seconds) }}</option>\n }\n </select>\n }\n @if (hourFormat() === '12') {\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"meridiem()\"\n (change)=\"onMeridiemChange($event)\"\n aria-label=\"AM/PM\"\n >\n <option value=\"AM\">AM</option>\n <option value=\"PM\">PM</option>\n </select>\n }\n </div>\n <button\n type=\"button\"\n (click)=\"confirmTime()\"\n class=\"mt-3 w-full py-2 text-sm _shk-bg-primary _shk-text-white rounded-lg hover:[background-color:var(--shk-primary-700)] transition-colors\"\n >\n {{ timeOnly() ? 'Confirmar' : 'Introducir hora' }}\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n @if (shouldShowError()) {\n <div class=\"mt-1 text-xs text-red-500 max-h-20 overflow-y-auto _shk-scrollbar\">\n <ul class=\"list-disc pl-4 space-y-1\">\n @for (error of getErrorMessages(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
973
1385
  }
974
1386
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: DatePickerComponent, decorators: [{
975
1387
  type: Component,
@@ -979,8 +1391,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
979
1391
  useExisting: forwardRef(() => DatePickerComponent),
980
1392
  multi: true,
981
1393
  },
982
- ], template: "<div class=\"relative mb-4\">\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n </div>\n }\n\n <div class=\"relative\">\n <button\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n class=\"flex items-center justify-between w-full px-3 py-2 text-left _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-lg transition-colors sm:text-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:[color:var(--shk-surface-400)] dark:disabled:[color:var(--shk-surface-500)]\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': shouldShowError(),\n 'border-green-500 focus:border-green-500 focus:ring-green-500': wasFixedError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !shouldShowError() && !wasFixedError(),\n }\"\n >\n <span\n [ngClass]=\"value() ? '_shk-text-surface-700 dark:[color:var(--shk-surface-700)]' : '_shk-text-surface-400 dark:[color:var(--shk-surface-400)]'\"\n >\n {{ displayLabel() || placeholder() || label() }}\n </span>\n <svg\n class=\"w-4 h-4 _shk-text-surface-400 dark:[color:var(--shk-surface-400)] shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"></rect>\n <path d=\"M16 2v4M8 2v4M3 10h18\"></path>\n </svg>\n </button>\n\n @if (isOpen()) {\n <div class=\"fixed inset-0 z-40 bg-black/30\" (click)=\"close()\"></div>\n <div\n class=\"fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-72 p-4 bg-white dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-lg shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (view() === 'date') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ monthNames()[viewMonth()] }} {{ viewYear() }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-7 gap-0 mb-1\">\n @for (name of dayNames(); track $index) {\n <div class=\"text-center text-xs font-medium _shk-text-surface-400 dark:[color:var(--shk-surface-400)] py-1\">\n {{ name }}\n </div>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-0\">\n @for (day of calendarDays(); track $index) {\n @if (day !== null) {\n <button\n type=\"button\"\n (click)=\"selectDay(day)\"\n class=\"py-1.5 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedDay(day),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isToday(day) && !isSelectedDay(day),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedDay(day) && !isToday(day),\n }\"\n >\n {{ day }}\n </button>\n } @else {\n <div></div>\n }\n }\n </div>\n }\n\n @if (view() === 'month') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">{{ viewYear() }}</span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (name of monthNames(); track $index) {\n <button\n type=\"button\"\n (click)=\"selectMonth($index)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedMonth($index),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentMonth($index) && !isSelectedMonth($index),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedMonth($index) && !isCurrentMonth($index),\n }\"\n >\n {{ name }}\n </button>\n }\n </div>\n }\n\n @if (view() === 'year') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ viewDecade() }} - {{ viewDecade() + 11 }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (year of decadeYears(); track year) {\n <button\n type=\"button\"\n (click)=\"selectYear(year)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedYear(year),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentYear(year) && !isSelectedYear(year),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedYear(year) && !isCurrentYear(year),\n }\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n </div>\n }\n </div>\n\n @if (shouldShowError()) {\n <div class=\"mt-1 text-xs text-red-500 max-h-20 overflow-y-auto _shk-scrollbar\">\n <ul class=\"list-disc pl-4 space-y-1\">\n @for (error of getErrorMessages(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n }\n</div>\n" }]
983
- }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], minValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "minValue", required: false }] }], maxValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxValue", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], onDocumentClick: [{
1394
+ ], template: "<div class=\"relative mb-4\">\n @if (label()) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)] mb-1\">\n {{ label() }}\n </div>\n }\n\n <div class=\"relative\">\n <button\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"isDisabled()\"\n (click)=\"toggle()\"\n class=\"flex items-center justify-between w-full px-3 py-2 text-left _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border rounded-lg transition-colors sm:text-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:[color:var(--shk-surface-400)] dark:disabled:[color:var(--shk-surface-500)]\"\n [ngClass]=\"{\n 'border-red-500 focus:border-red-500 focus:ring-red-500': shouldShowError(),\n 'border-green-500 focus:border-green-500 focus:ring-green-500': wasFixedError(),\n '_shk-border dark:[border-color:var(--shk-surface-200)] focus:[border-color:var(--shk-primary-500)] focus:[--tw-ring-color:var(--shk-primary-500)]': !shouldShowError() && !wasFixedError(),\n }\"\n >\n <span\n [ngClass]=\"value() ? '_shk-text-surface-700 dark:[color:var(--shk-surface-700)]' : '_shk-text-surface-400 dark:[color:var(--shk-surface-400)]'\"\n >\n {{ displayLabel() || placeholder() || label() }}\n </span>\n <svg\n class=\"w-4 h-4 _shk-text-surface-400 dark:[color:var(--shk-surface-400)] shrink-0\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\"></rect>\n <path d=\"M16 2v4M8 2v4M3 10h18\"></path>\n </svg>\n </button>\n\n @if (isOpen()) {\n <div class=\"fixed inset-0 z-40 bg-black/30\" (click)=\"close()\"></div>\n <div\n class=\"fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-72 p-4 bg-white dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-lg shadow-xl\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (!timeOnly() && view() === 'date') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ monthNames()[viewMonth()] }} {{ viewYear() }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-7 gap-0 mb-1\">\n @for (name of dayNames(); track $index) {\n <div class=\"text-center text-xs font-medium _shk-text-surface-400 dark:[color:var(--shk-surface-400)] py-1\">\n {{ name }}\n </div>\n }\n </div>\n\n <div class=\"grid grid-cols-7 gap-0\">\n @for (day of calendarDays(); track $index) {\n @if (day !== null) {\n <button\n type=\"button\"\n (click)=\"selectDay(day)\"\n class=\"py-1.5 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedDay(day),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isToday(day) && !isSelectedDay(day),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedDay(day) && !isToday(day),\n }\"\n >\n {{ day }}\n </button>\n } @else {\n <div></div>\n }\n }\n </div>\n }\n\n @if (!timeOnly() && view() === 'month') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">{{ viewYear() }}</span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (name of monthNames(); track $index) {\n <button\n type=\"button\"\n (click)=\"selectMonth($index)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedMonth($index),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentMonth($index) && !isSelectedMonth($index),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedMonth($index) && !isCurrentMonth($index),\n }\"\n >\n {{ name }}\n </button>\n }\n </div>\n }\n\n @if (!timeOnly() && view() === 'year') {\n <div class=\"flex items-center justify-between mb-3\">\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"!canGoPrev()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M15 18l-6-6 6-6\"></path>\n </svg>\n </button>\n <span class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-600)]\">\n {{ viewDecade() }} - {{ viewDecade() + 11 }}\n </span>\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"!canGoNext()\"\n class=\"p-1 rounded hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed\"\n >\n <svg class=\"w-4 h-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M9 18l6-6-6-6\"></path>\n </svg>\n </button>\n </div>\n\n <div class=\"grid grid-cols-3 gap-1\">\n @for (year of decadeYears(); track year) {\n <button\n type=\"button\"\n (click)=\"selectYear(year)\"\n class=\"py-2 text-sm rounded transition-colors\"\n [ngClass]=\"{\n '_shk-bg-primary _shk-text-white hover:[background-color:var(--shk-primary-700)]': isSelectedYear(year),\n 'ring-1 [--tw-ring-color:var(--shk-primary-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)]': isCurrentYear(year) && !isSelectedYear(year),\n '_shk-text-surface-700 dark:[color:var(--shk-surface-700)] hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)]': !isSelectedYear(year) && !isCurrentYear(year),\n }\"\n >\n {{ year }}\n </button>\n }\n </div>\n }\n\n @if (timeOnly() || showTime()) {\n <div class=\"mt-3 pt-3 _shk-border dark:[border-color:var(--shk-surface-200)]\">\n <div class=\"flex items-center justify-center gap-1\">\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"viewHour()\"\n (change)=\"onHourChange($event)\"\n aria-label=\"Hora\"\n >\n @for (h of hoursRange(); track h) {\n <option [value]=\"h\">{{ hourDisplay(h) }}</option>\n }\n </select>\n <span class=\"_shk-text-surface-400\">:</span>\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"viewMinute()\"\n (change)=\"onMinuteChange($event)\"\n aria-label=\"Minutos\"\n >\n @for (minutes of minutesRange(); track minutes) {\n <option [value]=\"minutes\">{{ minutesLabel(minutes) }}</option>\n }\n </select>\n @if (showSeconds()) {\n <span class=\"text-sm _shk-text-surface-400\">:</span>\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"viewSecond()\"\n (change)=\"onSecondChange($event)\"\n aria-label=\"Segundos\"\n >\n @for (seconds of secondsRange(); track seconds) {\n <option [value]=\"seconds\">{{ minutesLabel(seconds) }}</option>\n }\n </select>\n }\n @if (hourFormat() === '12') {\n <select\n class=\"px-2 py-1.5 text-sm _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md\"\n [value]=\"meridiem()\"\n (change)=\"onMeridiemChange($event)\"\n aria-label=\"AM/PM\"\n >\n <option value=\"AM\">AM</option>\n <option value=\"PM\">PM</option>\n </select>\n }\n </div>\n <button\n type=\"button\"\n (click)=\"confirmTime()\"\n class=\"mt-3 w-full py-2 text-sm _shk-bg-primary _shk-text-white rounded-lg hover:[background-color:var(--shk-primary-700)] transition-colors\"\n >\n {{ timeOnly() ? 'Confirmar' : 'Introducir hora' }}\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n @if (shouldShowError()) {\n <div class=\"mt-1 text-xs text-red-500 max-h-20 overflow-y-auto _shk-scrollbar\">\n <ul class=\"list-disc pl-4 space-y-1\">\n @for (error of getErrorMessages(); track error) {\n <li>{{ error }}</li>\n }\n </ul>\n </div>\n }\n</div>\n" }]
1395
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], formGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "formGroup", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], minValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "minValue", required: false }] }], maxValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxValue", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], timeOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeOnly", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], hourFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "hourFormat", required: false }] }], showSeconds: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeconds", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], onDocumentClick: [{
984
1396
  type: HostListener,
985
1397
  args: ['document:click', ['$event']]
986
1398
  }], onEscape: [{
@@ -988,34 +1400,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
988
1400
  args: ['document:keydown.escape']
989
1401
  }] } });
990
1402
 
991
- const icons$7 = {
992
- info: LucideInfo,
993
- 'triangle-alert': LucideTriangleAlert,
994
- x: LucideX,
995
- 'trash-2': LucideTrash2,
996
- loader: LucideLoader,
997
- };
1403
+ /**
1404
+ * Shirkasoft UI Components.
1405
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
1406
+ * Belongs to Shirkasoft.
1407
+ */
1408
+ /**
1409
+ * Confirmation dialog opened via `show()` which resolves a Promise<boolean>
1410
+ * with the user's choice.
1411
+ */
998
1412
  class ConfirmDialogComponent {
999
1413
  constructor() {
1000
- this.getIcon = (name) => icons$7[name] ?? LucideTriangleAlert;
1414
+ this.getIcon = (name) => getIcon(name, LucideTriangleAlert);
1415
+ /** Title shown in the dialog header. */
1001
1416
  this.title = input('Confirmar acción', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
1417
+ /** Message body of the dialog. */
1002
1418
  this.message = input('¿Está seguro de realizar esta acción?', ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
1419
+ /** Label of the confirm button. */
1003
1420
  this.confirmLabel = input('Confirmar', ...(ngDevMode ? [{ debugName: "confirmLabel" }] : /* istanbul ignore next */ []));
1421
+ /** Label of the cancel button. */
1004
1422
  this.cancelLabel = input('Cancelar', ...(ngDevMode ? [{ debugName: "cancelLabel" }] : /* istanbul ignore next */ []));
1423
+ /** Text shown on the confirm button while `loading` is true. */
1005
1424
  this.loadingText = input('Procesando…', ...(ngDevMode ? [{ debugName: "loadingText" }] : /* istanbul ignore next */ []));
1425
+ /** Visual style: 'danger', 'info' or 'warning'. */
1006
1426
  this.type = input('danger', ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
1427
+ /** Shows a spinner and disables the confirm button. */
1007
1428
  this.loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1429
+ /** Whether the cancel button is shown. */
1008
1430
  this.showCancel = input(true, ...(ngDevMode ? [{ debugName: "showCancel" }] : /* istanbul ignore next */ []));
1431
+ /** Controls dialog visibility. */
1009
1432
  this.visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
1433
+ /** Resolver of the currently pending confirmation promise. */
1010
1434
  this.resolveRef = signal(null, ...(ngDevMode ? [{ debugName: "resolveRef" }] : /* istanbul ignore next */ []));
1435
+ /** Emitted whenever the dialog is closed (confirm or cancel). */
1011
1436
  this.closed = output();
1012
1437
  }
1438
+ /** Opens the dialog and returns a promise resolving with the user choice. */
1013
1439
  show() {
1014
1440
  this.visible.set(true);
1015
1441
  return new Promise((resolve) => {
1016
1442
  this.resolveRef.set(resolve);
1017
1443
  });
1018
1444
  }
1445
+ /** Resolves with `true` when the user confirms. */
1019
1446
  onConfirm() {
1020
1447
  this.visible.set(false);
1021
1448
  const resolve = this.resolveRef();
@@ -1025,6 +1452,7 @@ class ConfirmDialogComponent {
1025
1452
  }
1026
1453
  this.closed.emit();
1027
1454
  }
1455
+ /** Resolves with `false` when the user cancels. */
1028
1456
  onCancel() {
1029
1457
  this.visible.set(false);
1030
1458
  const resolve = this.resolveRef();
@@ -1034,6 +1462,7 @@ class ConfirmDialogComponent {
1034
1462
  }
1035
1463
  this.closed.emit();
1036
1464
  }
1465
+ /** Clears the pending resolver on destroy. */
1037
1466
  ngOnDestroy() {
1038
1467
  this.resolveRef.set(null);
1039
1468
  }
@@ -1045,14 +1474,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
1045
1474
  args: [{ selector: 'shk-confirm-dialog', standalone: true, imports: [CommonModule, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible()) {\n <div\n class=\"fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-[9999] p-4\"\n (click)=\"onCancel()\"\n >\n <div\n class=\"bg-white dark:[background-color:var(--shk-surface-100)] rounded-2xl shadow-2xl max-w-sm w-full animate-in fade-in zoom-in duration-200 overflow-hidden\"\n (click)=\"$event.stopPropagation()\"\n >\n <div\n class=\"h-1.5 w-full\"\n [class.bg-gradient-to-r]=\"type() !== 'warning'\"\n [class.from-red-400]=\"type() === 'danger'\"\n [class.to-red-600]=\"type() === 'danger'\"\n [class.from-blue-400]=\"type() === 'info'\"\n [class.to-blue-600]=\"type() === 'info'\"\n [class.bg-amber-400]=\"type() === 'warning'\"\n ></div>\n <div class=\"p-6\">\n <div class=\"flex items-start gap-4 mb-4\">\n <div\n class=\"w-12 h-12 rounded-full flex items-center justify-center shrink-0\"\n [ngClass]=\"{\n 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400': type() === 'danger',\n 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400': type() === 'info',\n 'bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400': type() === 'warning',\n }\"\n >\n @if (type() === 'info') {\n <svg [lucideIcon]=\"getIcon('info')\" class=\"w-6 h-6\"></svg>\n } @else {\n <svg [lucideIcon]=\"getIcon('triangle-alert')\" class=\"w-6 h-6\"></svg>\n }\n </div>\n <div>\n <h3 class=\"text-lg font-bold _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">{{ title() }}</h3>\n <p class=\"_shk-text-surface-500 dark:[color:var(--shk-surface-400)] text-sm leading-relaxed mb-6\">{{ message() }}</p>\n </div>\n </div>\n <div class=\"flex gap-3\">\n @if (showCancel()) {\n <button\n type=\"button\"\n (click)=\"onCancel()\"\n [disabled]=\"loading()\"\n class=\"flex-1 px-4 py-2 text-sm font-semibold _shk-text-surface-600 dark:[color:var(--shk-surface-600)] _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] hover:[background-color:var(--shk-surface-200)] dark:hover:[background-color:var(--shk-surface-300)] rounded-lg transition-colors flex items-center justify-center gap-2\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-4 h-4\"></svg>\n {{ cancelLabel() }}\n </button>\n }\n <button\n type=\"button\"\n (click)=\"onConfirm()\"\n [disabled]=\"loading()\"\n class=\"flex-1 px-4 py-2 text-sm font-semibold text-white rounded-lg shadow-md transition-all flex items-center justify-center gap-2\"\n [class.bg-red-600]=\"type() === 'danger'\"\n [class.hover:bg-red-700]=\"type() === 'danger'\"\n [class.shadow-red-500/20]=\"type() === 'danger'\"\n [class.bg-primary-600]=\"type() === 'info'\"\n [class.hover:bg-primary-700]=\"type() === 'info'\"\n [class.shadow-primary-500/20]=\"type() === 'info'\"\n [class.bg-amber-600]=\"type() === 'warning'\"\n [class.hover:bg-amber-700]=\"type() === 'warning'\"\n [class.shadow-amber-500/20]=\"type() === 'warning'\"\n >\n @if (loading()) {\n <svg [lucideIcon]=\"getIcon('loader')\" class=\"w-4 h-4 animate-spin\"></svg>\n } @else if (type() === 'danger') {\n <svg [lucideIcon]=\"getIcon('trash-2')\" class=\"w-4 h-4\"></svg>\n }\n {{ loading() ? loadingText() : confirmLabel() }}\n </button>\n </div>\n </div>\n </div>\n </div>\n}\n" }]
1046
1475
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], confirmLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "confirmLabel", required: false }] }], cancelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelLabel", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], showCancel: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCancel", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
1047
1476
 
1477
+ /**
1478
+ * Programmatically opens the confirmation dialog and resolves a Promise
1479
+ * with the user's decision (true = confirm, false = cancel).
1480
+ */
1048
1481
  class ConfirmDialogService {
1049
1482
  constructor() {
1483
+ /** Reference to the currently open dialog, if any. */
1050
1484
  this.dialogRef = null;
1051
1485
  this.destroyRef = inject(DestroyRef);
1052
1486
  this.appRef = inject(ApplicationRef);
1053
1487
  this.injector = inject(EnvironmentInjector);
1054
1488
  this.destroyRef.onDestroy(() => this.cleanupDialog());
1055
1489
  }
1490
+ /**
1491
+ * Opens a confirmation dialog with the given configuration and returns a
1492
+ * promise resolving with the user's choice.
1493
+ */
1056
1494
  async confirm(config) {
1057
1495
  if (this.dialogRef)
1058
1496
  this.cleanupDialog();
@@ -1076,6 +1514,7 @@ class ConfirmDialogService {
1076
1514
  this.cleanupDialog();
1077
1515
  }
1078
1516
  }
1517
+ /** Detaches and destroys the mounted dialog if present. */
1079
1518
  cleanupDialog() {
1080
1519
  if (this.dialogRef) {
1081
1520
  this.appRef.detachView(this.dialogRef.hostView);
@@ -1091,29 +1530,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
1091
1530
  args: [{ providedIn: 'root' }]
1092
1531
  }], ctorParameters: () => [] });
1093
1532
 
1533
+ /**
1534
+ * Global modal manager. Opens modals on a stack, exposing reactive access to
1535
+ * the current modal and the top-most opened config.
1536
+ */
1094
1537
  class ModalService {
1095
1538
  constructor() {
1539
+ /** Stack of open modals (top-most renders on top). */
1096
1540
  this.stackSignal = signal([], ...(ngDevMode ? [{ debugName: "stackSignal" }] : /* istanbul ignore next */ []));
1541
+ /** Counter emitted on each close, used to trigger close animations. */
1097
1542
  this.closeRequestSignal = signal(0, ...(ngDevMode ? [{ debugName: "closeRequestSignal" }] : /* istanbul ignore next */ []));
1098
1543
  this.router = inject(Router);
1099
1544
  this.destroyRef = inject(DestroyRef);
1545
+ /** Reactive list of open modals. */
1100
1546
  this.modalStack = computed(() => this.stackSignal(), ...(ngDevMode ? [{ debugName: "modalStack" }] : /* istanbul ignore next */ []));
1547
+ /** Top-most modal on the stack, or null. */
1101
1548
  this.currentModal = computed(() => {
1102
1549
  const stack = this.stackSignal();
1103
1550
  return stack.length > 0 ? stack[stack.length - 1] : null;
1104
1551
  }, ...(ngDevMode ? [{ debugName: "currentModal" }] : /* istanbul ignore next */ []));
1552
+ /** Reactive close-request counter. */
1105
1553
  this.closeRequest = computed(() => this.closeRequestSignal(), ...(ngDevMode ? [{ debugName: "closeRequest" }] : /* istanbul ignore next */ []));
1554
+ // Close all modals on navigation and on service destruction.
1106
1555
  this.router.events.subscribe(() => {
1107
1556
  this.stackSignal.set([]);
1108
1557
  });
1109
1558
  this.destroyRef.onDestroy(() => this.stackSignal.set([]));
1110
1559
  }
1560
+ /** Opens a new modal on top of the stack. */
1111
1561
  open(config) {
1112
1562
  this.stackSignal.update((stack) => [...stack, config]);
1113
1563
  }
1564
+ /** Alias of `close()` used by the accept button. */
1114
1565
  accept() {
1115
1566
  this.close();
1116
1567
  }
1568
+ /** Closes the top-most modal, invoking its onClose callback. */
1117
1569
  close() {
1118
1570
  const stack = this.stackSignal();
1119
1571
  if (stack.length > 0) {
@@ -1122,6 +1574,7 @@ class ModalService {
1122
1574
  this.closeRequestSignal.update((v) => v + 1);
1123
1575
  }
1124
1576
  }
1577
+ /** Closes all open modals at once. */
1125
1578
  clear() {
1126
1579
  this.stackSignal.set([]);
1127
1580
  }
@@ -1133,37 +1586,55 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
1133
1586
  args: [{ providedIn: 'root' }]
1134
1587
  }], ctorParameters: () => [] });
1135
1588
 
1136
- const icons$6 = {
1137
- x: LucideX,
1138
- check: LucideCheck,
1139
- 'maximize-2': LucideMaximize2,
1140
- 'minimize-2': LucideMinimize2,
1141
- 'loader-circle': LucideLoaderCircle,
1142
- };
1589
+ /**
1590
+ * Shirkasoft UI Components.
1591
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
1592
+ * Belongs to Shirkasoft.
1593
+ */
1594
+ /**
1595
+ * Modal dialog that renders a dynamically loaded component, tracks its
1596
+ * form validity and submit outputs, and delegates accept/cancel to the
1597
+ * shared ModalService.
1598
+ */
1143
1599
  class ModalComponent {
1144
1600
  constructor() {
1145
- this.getIcon = (name) => icons$6[name] ?? LucideX;
1601
+ this.getIcon = (name) => getIcon(name, LucideX);
1146
1602
  this.notificationSrv = inject(NotificationService);
1147
1603
  this.destroyRef = inject(DestroyRef);
1148
1604
  this.modalSrv = inject(ModalService);
1149
1605
  this.transloco = inject(TranslocoService);
1150
1606
  this.cdr = inject(ChangeDetectorRef);
1607
+ /** Reference where the dynamic content is rendered. */
1151
1608
  this.container = viewChild('dynamicContent', { ...(ngDevMode ? { debugName: "container" } : /* istanbul ignore next */ {}), read: ViewContainerRef });
1609
+ /** Configuration of the modal currently open (from the service). */
1152
1610
  this.currentConfig = signal(null, ...(ngDevMode ? [{ debugName: "currentConfig" }] : /* istanbul ignore next */ []));
1611
+ /** Modal title. */
1153
1612
  this.title = signal('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
1613
+ /** Whether the modal is visible. */
1154
1614
  this.visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
1615
+ /** Generic loading flag for the footer actions. */
1155
1616
  this.loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1617
+ /** Whether the accept action is being processed. */
1156
1618
  this.isProcessing = signal(false, ...(ngDevMode ? [{ debugName: "isProcessing" }] : /* istanbul ignore next */ []));
1619
+ /** Whether the modal is currently expanded to fullscreen. */
1157
1620
  this.isExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isExpanded" }] : /* istanbul ignore next */ []));
1621
+ /** True when the dynamic component reports a valid form. */
1158
1622
  this.isFormValid = signal(false, ...(ngDevMode ? [{ debugName: "isFormValid" }] : /* istanbul ignore next */ []));
1623
+ /** Reference to the dynamically created component. */
1159
1624
  this.componentRef = null;
1625
+ /** Subscriptions to the dynamic component outputs. */
1160
1626
  this.subscriptions = [];
1627
+ /** Width of the modal (fullscreen when expanded). */
1161
1628
  this.computedModalWidth = computed(() => {
1162
1629
  return this.isExpanded() ? '100vw' : (this.currentConfig()?.width ?? '560px');
1163
1630
  }, ...(ngDevMode ? [{ debugName: "computedModalWidth" }] : /* istanbul ignore next */ []));
1631
+ /** Whether the footer buttons should be rendered. */
1164
1632
  this.modalButtonsVisible = computed(() => this.currentConfig()?.showButtons ?? true, ...(ngDevMode ? [{ debugName: "modalButtonsVisible" }] : /* istanbul ignore next */ []));
1633
+ /** Whether the expand/fullscreen button is shown. */
1165
1634
  this.showExpandButton = computed(() => this.currentConfig()?.showExpandButton ?? false, ...(ngDevMode ? [{ debugName: "showExpandButton" }] : /* istanbul ignore next */ []));
1635
+ /** Resolved cancel button label. */
1166
1636
  this.cancelLabel = computed(() => this.currentConfig()?.cancelLabel || 'Cancelar', ...(ngDevMode ? [{ debugName: "cancelLabel" }] : /* istanbul ignore next */ []));
1637
+ /** Resolved accept button label. */
1167
1638
  this.acceptLabel = computed(() => this.currentConfig()?.acceptLabel || 'Aceptar', ...(ngDevMode ? [{ debugName: "acceptLabel" }] : /* istanbul ignore next */ []));
1168
1639
  effect(() => {
1169
1640
  const config = this.modalSrv.currentModal();
@@ -1187,6 +1658,7 @@ class ModalComponent {
1187
1658
  });
1188
1659
  this.destroyRef.onDestroy(() => this.cleanup());
1189
1660
  }
1661
+ /** Updates visibility when the backdrop/close controls change. */
1190
1662
  onVisibleChange(show) {
1191
1663
  this.visible.set(show);
1192
1664
  if (!show) {
@@ -1194,6 +1666,7 @@ class ModalComponent {
1194
1666
  }
1195
1667
  this.cdr.markForCheck();
1196
1668
  }
1669
+ /** Creates the dynamic component and wires its form/submit outputs. */
1197
1670
  loadComponent(config) {
1198
1671
  const containerRef = this.container();
1199
1672
  if (!containerRef)
@@ -1216,11 +1689,13 @@ class ModalComponent {
1216
1689
  this.componentRef.changeDetectorRef.markForCheck();
1217
1690
  this.cdr.markForCheck();
1218
1691
  }
1692
+ /** Subscribes to a dynamic component output if it exists. */
1219
1693
  subscribeToOutput(output, callback) {
1220
1694
  if (output?.subscribe) {
1221
1695
  this.subscriptions.push(output.subscribe(callback));
1222
1696
  }
1223
1697
  }
1698
+ /** Finalizes the submit flow, accepting the modal on success. */
1224
1699
  handleSubmit(success) {
1225
1700
  this.isProcessing.set(false);
1226
1701
  this.loading.set(false);
@@ -1229,12 +1704,14 @@ class ModalComponent {
1229
1704
  untracked(() => this.modalSrv.accept());
1230
1705
  }
1231
1706
  }
1707
+ /** Closes the modal, calling the dynamic component's `handleCancel` if any. */
1232
1708
  closeModal() {
1233
1709
  if (this.componentRef?.instance?.handleCancel) {
1234
1710
  this.componentRef.instance.handleCancel();
1235
1711
  }
1236
1712
  this.doClose();
1237
1713
  }
1714
+ /** Performs the actual close sequence and cleanup. */
1238
1715
  doClose() {
1239
1716
  this.visible.set(false);
1240
1717
  this.isProcessing.set(false);
@@ -1248,10 +1725,12 @@ class ModalComponent {
1248
1725
  this.modalSrv.close();
1249
1726
  }, 100);
1250
1727
  }
1728
+ /** Toggles between normal width and fullscreen. */
1251
1729
  toggleExpand() {
1252
1730
  this.isExpanded.update((v) => !v);
1253
1731
  this.cdr.markForCheck();
1254
1732
  }
1733
+ /** Validates the dynamic component form and triggers its `onSubmit`. */
1255
1734
  onAccept() {
1256
1735
  if (!this.componentRef || !this.isDynamicComponent(this.componentRef.instance))
1257
1736
  return;
@@ -1273,6 +1752,7 @@ class ModalComponent {
1273
1752
  this.cdr.markForCheck();
1274
1753
  instance.onSubmit();
1275
1754
  }
1755
+ /** Resolves the dynamic component's form (property or getter function). */
1276
1756
  getForm(instance) {
1277
1757
  if (!instance['form'])
1278
1758
  return null;
@@ -1280,6 +1760,7 @@ class ModalComponent {
1280
1760
  ? instance['form']()
1281
1761
  : instance['form'];
1282
1762
  }
1763
+ /** Recursively checks for real validation errors (ignoring 'warning'). */
1283
1764
  hasRealErrors(control) {
1284
1765
  if (control instanceof FormControl) {
1285
1766
  const errors = control.errors;
@@ -1294,6 +1775,7 @@ class ModalComponent {
1294
1775
  }
1295
1776
  return Object.values(control.controls).some((child) => this.hasRealErrors(child));
1296
1777
  }
1778
+ /** Marks a form (and all its descendants) as touched. */
1297
1779
  markAllAsTouched(control) {
1298
1780
  control.markAsTouched();
1299
1781
  control.updateValueAndValidity({ onlySelf: true, emitEvent: true });
@@ -1301,9 +1783,11 @@ class ModalComponent {
1301
1783
  Object.values(control.controls).forEach((child) => this.markAllAsTouched(child));
1302
1784
  }
1303
1785
  }
1786
+ /** True when the component exposes both `onSubmit` and a `form`. */
1304
1787
  isDynamicComponent(instance) {
1305
1788
  return typeof instance?.onSubmit === 'function' && instance?.form !== undefined;
1306
1789
  }
1790
+ /** Unsubscribes outputs and destroys the dynamic component. */
1307
1791
  cleanup() {
1308
1792
  this.subscriptions.forEach((sub) => sub.unsubscribe());
1309
1793
  this.subscriptions = [];
@@ -1320,56 +1804,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
1320
1804
  args: [{ selector: 'shk-modal', standalone: true, imports: [CommonModule, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (visible()) {\n <div\n class=\"fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-40 p-4 pt-4 overflow-y-auto _shk-scrollbar\"\n (click)=\"closeModal()\"\n >\n <div\n class=\"bg-white dark:[background-color:var(--shk-surface-100)] rounded-xl shadow-2xl overflow-hidden flex flex-col animate-in fade-in zoom-in duration-200\"\n [class.w-[95vw]]=\"isExpanded()\"\n [class.h-[90vh]]=\"isExpanded()\"\n [class.w-full]=\"!isExpanded()\"\n [style.max-width]=\"isExpanded() ? undefined : computedModalWidth()\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"h-1.5 w-full _shk-gradient-emerald shrink-0\"></div>\n\n <div class=\"flex items-center justify-between px-6 pt-4 pb-2 shrink-0\">\n <span class=\"font-semibold text-lg _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">{{ title() }}</span>\n <div class=\"flex items-center gap-2\">\n @if (showExpandButton()) {\n <button\n type=\"button\"\n (click)=\"toggleExpand()\"\n class=\"p-1.5 rounded-lg hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 dark:[color:var(--shk-surface-500)] transition-colors\"\n >\n @if (isExpanded()) {\n <svg [lucideIcon]=\"getIcon('minimize-2')\" class=\"w-4 h-4\"></svg>\n } @else {\n <svg [lucideIcon]=\"getIcon('maximize-2')\" class=\"w-4 h-4\"></svg>\n }\n </button>\n }\n <button\n type=\"button\"\n (click)=\"closeModal()\"\n class=\"p-1.5 rounded-lg hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] _shk-text-surface-500 dark:[color:var(--shk-surface-500)] transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n </div>\n\n <div class=\"overflow-y-auto px-6 py-4 flex-1 max-h-[70vh] _shk-scrollbar\">\n <ng-container #dynamicContent></ng-container>\n </div>\n\n @if (modalButtonsVisible()) {\n <hr class=\"border-t _shk-border dark:[border-color:var(--shk-surface-200)] shrink-0\" />\n <div class=\"flex justify-end gap-3 px-6 py-4 shrink-0\">\n <button\n type=\"button\"\n (click)=\"closeModal()\"\n [disabled]=\"loading()\"\n class=\"px-4 py-2 text-sm font-semibold _shk-text-surface-600 dark:[color:var(--shk-surface-600)] _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] hover:[background-color:var(--shk-surface-200)] dark:hover:[background-color:var(--shk-surface-300)] rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-4 h-4\"></svg>\n {{ cancelLabel() }}\n </button>\n <button\n type=\"button\"\n (click)=\"onAccept()\"\n [disabled]=\"loading()\"\n class=\"px-4 py-2 text-sm font-semibold _shk-text-white _shk-bg-emerald hover:[background-color:var(--shk-emerald-700)] rounded-lg shadow-md transition-all flex items-center gap-2 disabled:opacity-50\"\n >\n @if (loading()) {\n <svg [lucideIcon]=\"getIcon('loader-circle')\" class=\"w-4 h-4 animate-spin\"></svg>\n } @else {\n <svg [lucideIcon]=\"getIcon('check')\" class=\"w-4 h-4\"></svg>\n }\n {{ acceptLabel() }}\n </button>\n </div>\n }\n </div>\n </div>\n}\n" }]
1321
1805
  }], ctorParameters: () => [], propDecorators: { container: [{ type: i0.ViewChild, args: ['dynamicContent', { ...{ read: ViewContainerRef }, isSignal: true }] }] } });
1322
1806
 
1323
- const icons$5 = {
1324
- 'chevron-down': LucideChevronDown,
1325
- 'chevron-left': LucideChevronLeft,
1326
- 'chevron-right': LucideChevronRight,
1327
- };
1807
+ /**
1808
+ * Shirkasoft UI Components.
1809
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
1810
+ * Belongs to Shirkasoft.
1811
+ */
1812
+ /**
1813
+ * Custom select that works as a ControlValueAccessor: single/multiple
1814
+ * selection, searchable, keyboard navigation, pagination, custom entries and
1815
+ * reactive-form integration.
1816
+ */
1328
1817
  class SelectComponent {
1329
1818
  constructor(elementRef) {
1330
1819
  this.elementRef = elementRef;
1331
- this.getIcon = (name) => icons$5[name] ?? LucideChevronDown;
1820
+ /** Resolves icon names to lucide components, falling back to a chevron. */
1821
+ this.getIcon = (name) => getIcon(name, LucideChevronDown);
1822
+ /** Available options. */
1332
1823
  this.options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1824
+ /** Enables multi-select mode. */
1333
1825
  this.multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
1826
+ /** Restricts multi-selection to the given value combinations. */
1334
1827
  this.validCombinations = input(undefined, ...(ngDevMode ? [{ debugName: "validCombinations" }] : /* istanbul ignore next */ []));
1828
+ /** Placeholder shown when there is no selection. */
1335
1829
  this.placeholder = input('Seleccionar...', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1830
+ /** Label used to bind the control inside a `formGroup`. */
1336
1831
  this.label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1832
+ /** Shows a loading state while options are being fetched. */
1337
1833
  this.isLoading = input(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
1834
+ /** Marks that all options have been loaded (used with pagination + search). */
1338
1835
  this.isAllDataLoaded = input(false, ...(ngDevMode ? [{ debugName: "isAllDataLoaded" }] : /* istanbul ignore next */ []));
1836
+ /** Allows creating new entries from the typed search term. */
1339
1837
  this.allowCustomEntries = input(false, ...(ngDevMode ? [{ debugName: "allowCustomEntries" }] : /* istanbul ignore next */ []));
1838
+ /** Parent form group to resolve the control from `label`. */
1340
1839
  this.formGroup = input(undefined, ...(ngDevMode ? [{ debugName: "formGroup" }] : /* istanbul ignore next */ []));
1840
+ /** Enables the search input inside the dropdown. */
1341
1841
  this.isSearchable = input(false, ...(ngDevMode ? [{ debugName: "isSearchable" }] : /* istanbul ignore next */ []));
1842
+ /** Number of options loaded per page when pagination is enabled. */
1342
1843
  this.itemsPerPage = input(10, ...(ngDevMode ? [{ debugName: "itemsPerPage" }] : /* istanbul ignore next */ []));
1844
+ /** FormControl bound externally (used instead of `formGroup`/`label`). */
1343
1845
  this.control = input(undefined, ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
1846
+ /** Paginates the option list instead of rendering it all at once. */
1344
1847
  this.usePagination = input(false, ...(ngDevMode ? [{ debugName: "usePagination" }] : /* istanbul ignore next */ []));
1848
+ /** Disables the whole select. */
1345
1849
  this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1850
+ /** Custom per-validation-error messages shown under the select. */
1346
1851
  this.errorMessages = input({
1347
1852
  required: 'Este campo es requerido',
1348
1853
  }, ...(ngDevMode ? [{ debugName: "errorMessages" }] : /* istanbul ignore next */ []));
1854
+ /** Keeps the search term when options reload. */
1349
1855
  this.preserveSearchOnLoad = input(false, ...(ngDevMode ? [{ debugName: "preserveSearchOnLoad" }] : /* istanbul ignore next */ []));
1856
+ /** Translation key of the "no records" empty message. */
1350
1857
  this.emptyMessageKey = input('common.no_records', ...(ngDevMode ? [{ debugName: "emptyMessageKey" }] : /* istanbul ignore next */ []));
1858
+ /** Opens the dropdown above the trigger instead of below. */
1351
1859
  this.dropdownUpward = input(false, ...(ngDevMode ? [{ debugName: "dropdownUpward" }] : /* istanbul ignore next */ []));
1860
+ /** Shows the "no selection" option in single-select mode. */
1352
1861
  this.showEmptyOption = input(true, ...(ngDevMode ? [{ debugName: "showEmptyOption" }] : /* istanbul ignore next */ []));
1862
+ /** Emits when the selection changes (new value or array in multiple mode). */
1353
1863
  this.selectionChange = output();
1864
+ /** Emits the debounced search term while typing. */
1354
1865
  this.search = output();
1866
+ /** Reference to the filter input inside the dropdown (when searchable). */
1355
1867
  this.filterInput = viewChild('filterInput', ...(ngDevMode ? [{ debugName: "filterInput" }] : /* istanbul ignore next */ []));
1868
+ /** Reference to the select container element. */
1356
1869
  this.selectContainer = viewChild('selectContainer', ...(ngDevMode ? [{ debugName: "selectContainer" }] : /* istanbul ignore next */ []));
1870
+ /** Reference to the trigger button. */
1357
1871
  this.selectTrigger = viewChild('selectTrigger', ...(ngDevMode ? [{ debugName: "selectTrigger" }] : /* istanbul ignore next */ []));
1872
+ /** Reference to the dropdown list container. */
1358
1873
  this.dropdownContainer = viewChild('dropdownContainer', ...(ngDevMode ? [{ debugName: "dropdownContainer" }] : /* istanbul ignore next */ []));
1874
+ /** Underlying selection value (mirrors the reactive control). */
1359
1875
  this._value = signal(null, ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
1876
+ /** Whether the dropdown is open. */
1360
1877
  this.isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
1878
+ /** Index of the currently focused option for keyboard navigation. */
1361
1879
  this.focusedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : /* istanbul ignore next */ []));
1880
+ /** Current search term (searchable mode). */
1362
1881
  this.searchTerm = signal('', ...(ngDevMode ? [{ debugName: "searchTerm" }] : /* istanbul ignore next */ []));
1882
+ /** Current page of the paginated dropdown. */
1363
1883
  this.currentPage = signal(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
1884
+ /** Tracks whether the user typed in the search input. */
1364
1885
  this.searchTermChanged = signal(false, ...(ngDevMode ? [{ debugName: "searchTermChanged" }] : /* istanbul ignore next */ []));
1886
+ /** Internal disabled state from the reactive form (setDisabledState). */
1365
1887
  this._isDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_isDisabled" }] : /* istanbul ignore next */ []));
1888
+ /** Debounce timer handle for the search output. */
1366
1889
  this.debounceTimer = null;
1890
+ /** Error-state counter used to recompute error helpers reactively. */
1367
1891
  this.errorTracker = signal(0, ...(ngDevMode ? [{ debugName: "errorTracker" }] : /* istanbul ignore next */ []));
1892
+ /** Whether an error has been shown at some point (for "fixed" messaging). */
1368
1893
  this.hadError = signal(false, ...(ngDevMode ? [{ debugName: "hadError" }] : /* istanbul ignore next */ []));
1894
+ /** Whether control status/value subscriptions have been attached. */
1369
1895
  this.subscribed = signal(false, ...(ngDevMode ? [{ debugName: "subscribed" }] : /* istanbul ignore next */ []));
1370
1896
  this.valueSubscribed = signal(false, ...(ngDevMode ? [{ debugName: "valueSubscribed" }] : /* istanbul ignore next */ []));
1897
+ /** Current selection value. */
1371
1898
  this.value = computed(() => this._value(), ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1899
+ /** True when the select is disabled via input or by the reactive form. */
1372
1900
  this.isDisabled = computed(() => this.disabled() || this._isDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
1901
+ /** Resolves the form control from `control` or from `formGroup`+`label`. */
1373
1902
  this.actualControl = computed(() => {
1374
1903
  const ctrl = this.control();
1375
1904
  if (ctrl)
@@ -1381,16 +1910,19 @@ class SelectComponent {
1381
1910
  }
1382
1911
  return undefined;
1383
1912
  }, ...(ngDevMode ? [{ debugName: "actualControl" }] : /* istanbul ignore next */ []));
1913
+ /** Whether the bound control should display its error (invalid + touched/dirty). */
1384
1914
  this.shouldShowError = computed(() => {
1385
1915
  this.errorTracker();
1386
1916
  const ctrl = this.actualControl();
1387
1917
  return ctrl?.invalid && (ctrl?.dirty || ctrl?.touched);
1388
1918
  }, ...(ngDevMode ? [{ debugName: "shouldShowError" }] : /* istanbul ignore next */ []));
1919
+ /** Whether an error was shown before and the control is now valid + touched. */
1389
1920
  this.wasFixedError = computed(() => {
1390
1921
  this.errorTracker();
1391
1922
  const ctrl = this.actualControl();
1392
1923
  return this.hadError() && ctrl?.valid && ctrl?.touched;
1393
1924
  }, ...(ngDevMode ? [{ debugName: "wasFixedError" }] : /* istanbul ignore next */ []));
1925
+ /** Text shown in the trigger: the selected label(s) or the placeholder. */
1394
1926
  this.displayValue = computed(() => {
1395
1927
  const val = this._value();
1396
1928
  const opts = this.options();
@@ -1412,6 +1944,7 @@ class SelectComponent {
1412
1944
  const selectedOption = opts.find((opt) => opt.value === val);
1413
1945
  return selectedOption ? selectedOption.label : val;
1414
1946
  }, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
1947
+ /** List of validation error messages for the bound control. */
1415
1948
  this.getErrorMessages = computed(() => {
1416
1949
  this.errorTracker();
1417
1950
  const ctrl = this.actualControl();
@@ -1442,6 +1975,7 @@ class SelectComponent {
1442
1975
  }
1443
1976
  });
1444
1977
  }, ...(ngDevMode ? [{ debugName: "getErrorMessages" }] : /* istanbul ignore next */ []));
1978
+ /** Options filtered by the search term (matches first, then contains). */
1445
1979
  this.filteredOptions = computed(() => {
1446
1980
  const opts = this.options();
1447
1981
  const term = this.searchTerm();
@@ -1461,6 +1995,7 @@ class SelectComponent {
1461
1995
  return 0;
1462
1996
  });
1463
1997
  }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
1998
+ /** Options for the current page, with a synthetic custom entry when allowed. */
1464
1999
  this.paginatedOptions = computed(() => {
1465
2000
  let filtered = this.filteredOptions();
1466
2001
  const term = this.searchTerm();
@@ -1484,6 +2019,7 @@ class SelectComponent {
1484
2019
  const endIndex = startIndex + perPage;
1485
2020
  return filtered.slice(startIndex, endIndex);
1486
2021
  }, ...(ngDevMode ? [{ debugName: "paginatedOptions" }] : /* istanbul ignore next */ []));
2022
+ /** Non-custom options, including the empty option in single-select mode. */
1487
2023
  this.regularOptions = computed(() => {
1488
2024
  const opts = this.paginatedOptions().filter((option) => !option.custom);
1489
2025
  if (!this.multiple() && this.showEmptyOption()) {
@@ -1491,6 +2027,7 @@ class SelectComponent {
1491
2027
  }
1492
2028
  return opts;
1493
2029
  }, ...(ngDevMode ? [{ debugName: "regularOptions" }] : /* istanbul ignore next */ []));
2030
+ /** Total pages of the filtered option list (1 when pagination is off). */
1494
2031
  this.totalPages = computed(() => {
1495
2032
  if (!this.usePagination()) {
1496
2033
  return 1;
@@ -1499,6 +2036,7 @@ class SelectComponent {
1499
2036
  const perPage = this.itemsPerPage();
1500
2037
  return Math.ceil(filtered.length / perPage);
1501
2038
  }, ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
2039
+ /** Whether there is at least one selected value. */
1502
2040
  this.hasSelectedValues = computed(() => {
1503
2041
  const val = this._value();
1504
2042
  if (this.multiple()) {
@@ -1506,6 +2044,7 @@ class SelectComponent {
1506
2044
  }
1507
2045
  return !!val;
1508
2046
  }, ...(ngDevMode ? [{ debugName: "hasSelectedValues" }] : /* istanbul ignore next */ []));
2047
+ /** Whether the custom optional entry should be offered for the current term. */
1509
2048
  this.showCustomOption = computed(() => {
1510
2049
  const term = this.searchTerm();
1511
2050
  const opts = this.options();
@@ -1514,9 +2053,12 @@ class SelectComponent {
1514
2053
  !opts.some((o) => o.value === term));
1515
2054
  }, ...(ngDevMode ? [{ debugName: "showCustomOption" }] : /* istanbul ignore next */ []));
1516
2055
  this.transloco = inject(TranslocoService);
2056
+ /** ControlValueAccessor onChange callback. */
1517
2057
  this.onChange = (_) => { };
2058
+ /** ControlValueAccessor onTouched callback. */
1518
2059
  this.onTouched = () => { };
1519
2060
  this.destroyRef = inject(DestroyRef);
2061
+ // Debounced search emission whenever the term changes (searchable mode).
1520
2062
  effect(() => {
1521
2063
  const term = this.searchTerm();
1522
2064
  if (term !== '' && this.isSearchable()) {
@@ -1528,12 +2070,15 @@ class SelectComponent {
1528
2070
  }, 300);
1529
2071
  }
1530
2072
  });
2073
+ // Normalizes the value to arrays in multiple mode when empty.
1531
2074
  effect(() => {
1532
2075
  const isMultiple = this.multiple();
1533
2076
  if (this._value() === null || this._value() === undefined) {
1534
2077
  this._value.set(isMultiple ? [] : null);
1535
2078
  }
1536
2079
  });
2080
+ // Resets to the first page when options reload, clearing the search
2081
+ // term unless it must be preserved.
1537
2082
  effect(() => {
1538
2083
  this.options();
1539
2084
  this.currentPage.set(1);
@@ -1541,6 +2086,7 @@ class SelectComponent {
1541
2086
  this.searchTerm.set('');
1542
2087
  }
1543
2088
  });
2089
+ // Subscribes to the bound control status/value changes to track errors.
1544
2090
  effect(() => {
1545
2091
  const ctrl = this.actualControl();
1546
2092
  if (!ctrl) {
@@ -1567,11 +2113,13 @@ class SelectComponent {
1567
2113
  });
1568
2114
  this.subscribed.set(true);
1569
2115
  });
2116
+ // Forces error recomputation when the active translation changes.
1570
2117
  this.transloco.langChanges$
1571
2118
  .pipe(takeUntilDestroyed(this.destroyRef))
1572
2119
  .subscribe(() => {
1573
2120
  this.errorTracker.update((v) => v + 1);
1574
2121
  });
2122
+ // One-way sync from the reactive control to the internal value signal.
1575
2123
  effect(() => {
1576
2124
  const ctrl = this.actualControl();
1577
2125
  if (!ctrl)
@@ -1585,9 +2133,11 @@ class SelectComponent {
1585
2133
  }
1586
2134
  });
1587
2135
  }
2136
+ /** Sets the disabled state coming from the reactive form infrastructure. */
1588
2137
  setDisabledState(isDisabled) {
1589
2138
  this._isDisabled.set(isDisabled);
1590
2139
  }
2140
+ /** Marks the control as touched and updates validity and error state. */
1591
2141
  markAsTouchedAndUpdate() {
1592
2142
  const ctrl = this.actualControl();
1593
2143
  if (ctrl) {
@@ -1600,12 +2150,14 @@ class SelectComponent {
1600
2150
  this.hadError.set(true);
1601
2151
  }
1602
2152
  }
2153
+ /** Strips accents and lowercases text for accent-insensitive matching. */
1603
2154
  normalizeText(text) {
1604
2155
  return text
1605
2156
  .normalize('NFD')
1606
2157
  .replace(/[\u0300-\u036f]/g, '')
1607
2158
  .toLowerCase();
1608
2159
  }
2160
+ /** Keyboard handler for the trigger element (open/close/navigate/select). */
1609
2161
  onKeydown(event) {
1610
2162
  if (this.isDisabled())
1611
2163
  return;
@@ -1658,6 +2210,7 @@ class SelectComponent {
1658
2210
  break;
1659
2211
  }
1660
2212
  }
2213
+ /** Keyboard handler for the search input inside a searchable dropdown. */
1661
2214
  handleKeyDown(event) {
1662
2215
  switch (event.key) {
1663
2216
  case 'Enter':
@@ -1697,6 +2250,7 @@ class SelectComponent {
1697
2250
  break;
1698
2251
  }
1699
2252
  }
2253
+ /** Moves the focused option index by `direction`, skipping disabled ones. */
1700
2254
  moveFocus(direction) {
1701
2255
  const options = this.regularOptions();
1702
2256
  if (!options.length)
@@ -1727,6 +2281,7 @@ class SelectComponent {
1727
2281
  }
1728
2282
  });
1729
2283
  }
2284
+ /** Index of the currently selected option, or -1. */
1730
2285
  getSelectedIndex() {
1731
2286
  const currentVal = this._value();
1732
2287
  const options = this.regularOptions();
@@ -1735,6 +2290,7 @@ class SelectComponent {
1735
2290
  const index = options.findIndex((opt) => opt.value === currentVal);
1736
2291
  return index >= 0 ? index : -1;
1737
2292
  }
2293
+ /** Whether an option is disabled based on the valid combinations rule. */
1738
2294
  isOptionDisabled(option) {
1739
2295
  if (option.value === null)
1740
2296
  return false;
@@ -1756,6 +2312,7 @@ class SelectComponent {
1756
2312
  return allSelected && withinLimit;
1757
2313
  });
1758
2314
  }
2315
+ /** Whether an option is currently selected. */
1759
2316
  isSelected(option) {
1760
2317
  const val = this._value();
1761
2318
  if (this.multiple()) {
@@ -1763,6 +2320,7 @@ class SelectComponent {
1763
2320
  }
1764
2321
  return val === option.value;
1765
2322
  }
2323
+ /** Goes to the next options page when possible. */
1766
2324
  nextPage() {
1767
2325
  const total = this.totalPages();
1768
2326
  const current = this.currentPage();
@@ -1770,12 +2328,14 @@ class SelectComponent {
1770
2328
  this.currentPage.set(current + 1);
1771
2329
  }
1772
2330
  }
2331
+ /** Goes to the previous options page when possible. */
1773
2332
  previousPage() {
1774
2333
  const current = this.currentPage();
1775
2334
  if (current > 1) {
1776
2335
  this.currentPage.set(current - 1);
1777
2336
  }
1778
2337
  }
2338
+ /** Closes the dropdown when a click happens outside the component. */
1779
2339
  onClickOutside(event) {
1780
2340
  const clickedInside = this.elementRef.nativeElement.contains(event.target);
1781
2341
  if (!clickedInside && this.isOpen()) {
@@ -1785,6 +2345,7 @@ class SelectComponent {
1785
2345
  this.focusedIndex.set(-1);
1786
2346
  }
1787
2347
  }
2348
+ /** Selects a typed free-text value as a new custom entry. */
1788
2349
  onCustomOptionClick() {
1789
2350
  const term = this.searchTerm();
1790
2351
  const customOption = {
@@ -1802,6 +2363,7 @@ class SelectComponent {
1802
2363
  }
1803
2364
  this.markAsTouchedAndUpdate();
1804
2365
  }
2366
+ /** Handles typing in the search input (updates term and emits search). */
1805
2367
  onInputChange(event) {
1806
2368
  const previousSearch = this.searchTerm();
1807
2369
  const target = event.target;
@@ -1817,6 +2379,7 @@ class SelectComponent {
1817
2379
  }
1818
2380
  this.currentPage.set(1);
1819
2381
  }
2382
+ /** Selects or toggles the clicked option (custom entries included). */
1820
2383
  onOptionClick(option) {
1821
2384
  if (this.isDisabled())
1822
2385
  return;
@@ -1844,6 +2407,7 @@ class SelectComponent {
1844
2407
  this.selectOption(option);
1845
2408
  }
1846
2409
  }
2410
+ /** Selects a single option and updates the control/outputs. */
1847
2411
  selectOption(option) {
1848
2412
  if (this.multiple()) {
1849
2413
  this.toggleOption(option);
@@ -1862,6 +2426,7 @@ class SelectComponent {
1862
2426
  this.focusedIndex.set(-1);
1863
2427
  this.selectTrigger()?.nativeElement.focus();
1864
2428
  }
2429
+ /** Toggles an option in multiple mode, honoring the valid combinations. */
1865
2430
  toggleOption(option) {
1866
2431
  if (this.isOptionDisabled(option))
1867
2432
  return;
@@ -1885,6 +2450,7 @@ class SelectComponent {
1885
2450
  this.selectionChange.emit(newValue);
1886
2451
  this.markAsTouchedAndUpdate();
1887
2452
  }
2453
+ /** Opens/closes the dropdown and manages focus/selection state. */
1888
2454
  toggleDropdown() {
1889
2455
  if (this.isDisabled())
1890
2456
  return;
@@ -1906,9 +2472,11 @@ class SelectComponent {
1906
2472
  }
1907
2473
  this.onTouched();
1908
2474
  }
2475
+ /** trackBy for rendered options so Angular reuses DOM nodes by value. */
1909
2476
  trackByFn(index, option) {
1910
2477
  return option.value;
1911
2478
  }
2479
+ /** Writes an external value into the internal signal (ControlValueAccessor). */
1912
2480
  writeValue(value) {
1913
2481
  if (value === null ||
1914
2482
  value === undefined ||
@@ -1920,9 +2488,11 @@ class SelectComponent {
1920
2488
  this._value.set(value);
1921
2489
  }
1922
2490
  }
2491
+ /** Stores the onChange callback (ControlValueAccessor). */
1923
2492
  registerOnChange(fn) {
1924
2493
  this.onChange = fn;
1925
2494
  }
2495
+ /** Stores the onTouched callback (ControlValueAccessor). */
1926
2496
  registerOnTouched(fn) {
1927
2497
  this.onTouched = fn;
1928
2498
  }
@@ -1955,31 +2525,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
1955
2525
  args: ['document:mousedown', ['$event']]
1956
2526
  }] } });
1957
2527
 
1958
- const icons$4 = {
1959
- upload: LucideUpload,
1960
- file: LucideFile,
1961
- x: LucideX,
1962
- };
2528
+ /**
2529
+ * Shirkasoft UI Components.
2530
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
2531
+ * Belongs to Shirkasoft.
2532
+ */
2533
+ /**
2534
+ * Drag & drop file uploader with file type/size validation, image previews,
2535
+ * multiple files support and localized error messages.
2536
+ */
1963
2537
  class FileUploadComponent {
1964
2538
  constructor() {
1965
- this.getIcon = (name) => icons$4[name] ?? LucideUpload;
2539
+ this.getIcon = (name) => getIcon(name, LucideUpload);
1966
2540
  this.transloco = inject(TranslocoService);
2541
+ /** Main label of the uploader. */
1967
2542
  this.label = input('Cargar archivo', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2543
+ /** Accepted MIME types/extensions (e.g. 'image/*', '.pdf'). */
1968
2544
  this.accept = input('image/*', ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
2545
+ /** Maximum allowed size per file (bytes). */
1969
2546
  this.maxFileSize = input(2 * 1024 * 1024, ...(ngDevMode ? [{ debugName: "maxFileSize" }] : /* istanbul ignore next */ []));
2547
+ /** Maximum number of files (0 = unlimited). */
1970
2548
  this.maxFiles = input(0, ...(ngDevMode ? [{ debugName: "maxFiles" }] : /* istanbul ignore next */ []));
2549
+ /** Text of the file picker button. */
1971
2550
  this.fileUploadText = input('Seleccionar archivo', ...(ngDevMode ? [{ debugName: "fileUploadText" }] : /* istanbul ignore next */ []));
2551
+ /** Helper text shown next to the label. */
1972
2552
  this.fileRecommendation = input('', ...(ngDevMode ? [{ debugName: "fileRecommendation" }] : /* istanbul ignore next */ []));
2553
+ /** Text shown when files are already loaded. */
1973
2554
  this.changeFilesText = input('Cambiar archivos', ...(ngDevMode ? [{ debugName: "changeFilesText" }] : /* istanbul ignore next */ []));
2555
+ /** Enables selecting more than one file at once. */
1974
2556
  this.multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
2557
+ /** Emitted with the currently selected files. */
1975
2558
  this.fileSelected = output();
2559
+ /** Emitted when the last selected file is removed. */
1976
2560
  this.fileRemoved = output();
2561
+ /** Emitted when a file fails validation (type/size). */
1977
2562
  this.fileError = output();
2563
+ /** Emitted when a file is near the size limit. */
1978
2564
  this.fileWarning = output();
2565
+ /** Currently selected files. */
1979
2566
  this.selectedFiles = signal([], ...(ngDevMode ? [{ debugName: "selectedFiles" }] : /* istanbul ignore next */ []));
2567
+ /** Localized error message displayed to the user. */
1980
2568
  this.errorMessage = signal('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
2569
+ /** Whether the user is dragging files over the zone. */
1981
2570
  this.isDragOver = signal(false, ...(ngDevMode ? [{ debugName: "isDragOver" }] : /* istanbul ignore next */ []));
2571
+ /** Object URLs used for image previews. */
1982
2572
  this.previewUrls = new Map();
2573
+ /** Whether another selection is allowed. */
1983
2574
  this.canAddMore = computed(() => {
1984
2575
  if (!this.multiple())
1985
2576
  return this.selectedFiles().length === 0;
@@ -1988,6 +2579,7 @@ class FileUploadComponent {
1988
2579
  return true;
1989
2580
  return this.selectedFiles().length < max;
1990
2581
  }, ...(ngDevMode ? [{ debugName: "canAddMore" }] : /* istanbul ignore next */ []));
2582
+ /** True when the maximum number of files has been reached. */
1991
2583
  this.maxReached = computed(() => {
1992
2584
  const max = this.maxFiles();
1993
2585
  if (max <= 0)
@@ -1995,25 +2587,30 @@ class FileUploadComponent {
1995
2587
  return this.selectedFiles().length >= max;
1996
2588
  }, ...(ngDevMode ? [{ debugName: "maxReached" }] : /* istanbul ignore next */ []));
1997
2589
  }
2590
+ /** Whether the file is an image (used for previews). */
1998
2591
  isImageFile(file) {
1999
2592
  return file.type.startsWith('image/');
2000
2593
  }
2594
+ /** Returns (creating on demand) the object URL for a file preview. */
2001
2595
  previewUrl(file) {
2002
2596
  if (!this.previewUrls.has(file)) {
2003
2597
  this.previewUrls.set(file, URL.createObjectURL(file));
2004
2598
  }
2005
2599
  return this.previewUrls.get(file) ?? '';
2006
2600
  }
2601
+ /** Marks the drop zone as active while dragging over it. */
2007
2602
  onDragOver(event) {
2008
2603
  event.preventDefault();
2009
2604
  event.stopPropagation();
2010
2605
  this.isDragOver.set(true);
2011
2606
  }
2607
+ /** Clears the drag-over state when the drag leaves the zone. */
2012
2608
  onDragLeave(event) {
2013
2609
  event.preventDefault();
2014
2610
  event.stopPropagation();
2015
2611
  this.isDragOver.set(false);
2016
2612
  }
2613
+ /** Processes the files dropped on the zone. */
2017
2614
  onDrop(event) {
2018
2615
  event.preventDefault();
2019
2616
  event.stopPropagation();
@@ -2023,6 +2620,7 @@ class FileUploadComponent {
2023
2620
  this.processFiles(Array.from(files));
2024
2621
  }
2025
2622
  }
2623
+ /** Processes the files picked from the native file input. */
2026
2624
  onFileChange(event) {
2027
2625
  const input = event.target;
2028
2626
  const files = input.files;
@@ -2031,6 +2629,7 @@ class FileUploadComponent {
2031
2629
  }
2032
2630
  input.value = '';
2033
2631
  }
2632
+ /** Validates and appends the received files, emitting errors/warnings. */
2034
2633
  processFiles(files) {
2035
2634
  this.errorMessage.set('');
2036
2635
  const validFiles = [];
@@ -2066,6 +2665,7 @@ class FileUploadComponent {
2066
2665
  }
2067
2666
  this.fileSelected.emit(this.selectedFiles());
2068
2667
  }
2668
+ /** Validates a file against `accept` and `maxFileSize`. */
2069
2669
  validateFile(file) {
2070
2670
  const acceptStr = this.accept();
2071
2671
  if (acceptStr && acceptStr !== '*') {
@@ -2101,6 +2701,7 @@ class FileUploadComponent {
2101
2701
  }
2102
2702
  return null;
2103
2703
  }
2704
+ /** Emits a warning when the file is close to the size limit (>90%). */
2104
2705
  checkSizeWarning(file) {
2105
2706
  if (this.maxFileSize() > 0 && file.size > this.maxFileSize() * 0.9) {
2106
2707
  return {
@@ -2111,6 +2712,7 @@ class FileUploadComponent {
2111
2712
  }
2112
2713
  return null;
2113
2714
  }
2715
+ /** Removes a file, revoking its preview URL. */
2114
2716
  removeFile(file) {
2115
2717
  const url = this.previewUrls.get(file);
2116
2718
  if (url) {
@@ -2123,17 +2725,20 @@ class FileUploadComponent {
2123
2725
  this.fileRemoved.emit();
2124
2726
  }
2125
2727
  }
2728
+ /** Clears all selected files and previews. */
2126
2729
  changeFiles() {
2127
2730
  this.clearPreviews();
2128
2731
  this.selectedFiles.set([]);
2129
2732
  this.errorMessage.set('');
2130
2733
  }
2734
+ /** Revokes every stored preview object URL. */
2131
2735
  clearPreviews() {
2132
2736
  for (const [file, url] of this.previewUrls) {
2133
2737
  URL.revokeObjectURL(url);
2134
2738
  }
2135
2739
  this.previewUrls.clear();
2136
2740
  }
2741
+ /** Human readable size (B/KB/MB). */
2137
2742
  formatFileSize(bytes) {
2138
2743
  if (bytes < 1024)
2139
2744
  return bytes + ' B';
@@ -2149,75 +2754,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
2149
2754
  args: [{ selector: 'shk-file-upload', standalone: true, imports: [CommonModule, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-2\">\n @if (label()) {\n <label class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ label() }}\n </label>\n }\n\n @if (canAddMore()) {\n <div\n class=\"relative border-2 border-dashed _shk-border dark:[border-color:var(--shk-surface-700)] rounded-lg p-4 transition-colors hover:[border-color:var(--shk-primary-400)] dark:hover:[border-color:var(--shk-primary-500)]\"\n [class.border-red-400]=\"errorMessage()\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n >\n <input\n #fileInput\n type=\"file\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n (change)=\"onFileChange($event)\"\n class=\"hidden\"\n />\n\n <div\n class=\"flex flex-col items-center gap-2 cursor-pointer\"\n (click)=\"fileInput.click()\"\n [class.drag-over]=\"isDragOver()\"\n >\n <svg [lucideIcon]=\"getIcon('upload')\" class=\"w-8 h-8 _shk-icon-subtle dark:[color:var(--shk-surface-600)]\"></svg>\n <span class=\"text-sm _shk-text-surface-700 dark:[color:var(--shk-surface-300)]\">\n {{ fileUploadText() }}\n </span>\n @if (fileRecommendation()) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n {{ fileRecommendation() }}\n </span>\n }\n </div>\n </div>\n }\n\n @if (maxReached()) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] text-center py-2\">\n M\u00E1ximo {{ maxFiles() }} archivos alcanzado\n </div>\n }\n\n @if (selectedFiles().length > 0) {\n <div class=\"flex flex-wrap gap-3\">\n @for (file of selectedFiles(); track file.name + file.size + file.lastModified) {\n <div class=\"relative group\">\n @if (isImageFile(file)) {\n <img\n [src]=\"previewUrl(file)\"\n [alt]=\"file.name\"\n class=\"w-20 h-20 object-cover rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)]\"\n />\n } @else {\n <div class=\"w-20 h-20 rounded-lg _shk-border dark:[border-color:var(--shk-surface-700)] flex flex-col items-center justify-center _shk-bg-surface-50 dark:[background-color:var(--shk-surface-800)] gap-1\">\n <svg [lucideIcon]=\"getIcon('file')\" class=\"w-6 h-6 _shk-text-surface-400\"></svg>\n <span class=\"text-[10px] _shk-text-surface-400 truncate max-w-[70px] px-1 text-center\">{{ file.name }}</span>\n </div>\n }\n <button\n type=\"button\"\n (click)=\"removeFile(file)\"\n class=\"absolute -top-1.5 -right-1.5 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3 h-3\"></svg>\n </button>\n </div>\n }\n </div>\n @if (!multiple()) {\n <button\n type=\"button\"\n (click)=\"changeFiles()\"\n class=\"text-xs _shk-text-primary hover:[color:var(--shk-primary-700)] dark:[color:var(--shk-primary-400)] dark:hover:[color:var(--shk-primary-300)] underline cursor-pointer bg-transparent border-0\"\n >\n {{ changeFilesText() }}\n </button>\n }\n }\n\n @if (errorMessage()) {\n <p class=\"text-xs _shk-text-red-500\">{{ errorMessage() }}</p>\n }\n</div>" }]
2150
2755
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], fileUploadText: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileUploadText", required: false }] }], fileRecommendation: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileRecommendation", required: false }] }], changeFilesText: [{ type: i0.Input, args: [{ isSignal: true, alias: "changeFilesText", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], fileSelected: [{ type: i0.Output, args: ["fileSelected"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileError: [{ type: i0.Output, args: ["fileError"] }], fileWarning: [{ type: i0.Output, args: ["fileWarning"] }] } });
2151
2756
 
2152
- const ICONS$2 = {
2153
- plus: LucidePlus,
2154
- 'circle-plus': LucidePlus,
2155
- pencil: LucidePencil,
2156
- edit: LucidePencil,
2157
- 'trash-2': LucideTrash2,
2158
- delete: LucideTrash2,
2159
- save: LucideSave,
2160
- x: LucideX,
2161
- close: LucideX,
2162
- check: LucideCheck,
2163
- search: LucideSearch,
2164
- user: LucideUser,
2165
- users: LucideUsers,
2166
- star: LucideStar,
2167
- calendar: LucideCalendar,
2168
- 'calendar-check': LucideCalendarCheck,
2169
- clock: LucideClock,
2170
- settings: LucideSettings,
2171
- 'alert-triangle': LucideAlertTriangle,
2172
- warning: LucideAlertTriangle,
2173
- 'alert-circle': LucideAlertCircle,
2174
- info: LucideInfo,
2175
- download: LucideDownload,
2176
- upload: LucideUpload,
2177
- refresh: LucideRefreshCw,
2178
- rocket: LucideRocket,
2179
- copy: LucideCopy,
2180
- sparkles: LucideSparkles,
2181
- 'chevron-down': LucideChevronDown,
2182
- 'chevron-up': LucideChevronUp,
2183
- 'chevron-left': LucideChevronLeft,
2184
- 'chevron-right': LucideChevronRight,
2185
- eye: LucideEye,
2186
- 'eye-off': LucideEyeOff,
2187
- lock: LucideLock,
2188
- mail: LucideMail,
2189
- phone: LucidePhone,
2190
- 'map-pin': LucideMapPin,
2191
- home: LucideHome,
2192
- 'file-text': LucideFileText,
2193
- 'folder-open': LucideFolderOpen,
2194
- 'shopping-cart': LucideShoppingCart,
2195
- package: LucidePackage,
2196
- truck: LucideTruck,
2197
- tag: LucideTag,
2198
- 'credit-card': LucideCreditCard,
2199
- 'dollar-sign': LucideDollarSign,
2200
- percent: LucidePercent,
2201
- wallet: LucideWallet,
2202
- globe: LucideGlobe,
2203
- 'circle-check': LucideCircleCheck,
2204
- 'circle-x': LucideCircleX,
2205
- menu: LucideMenu,
2206
- 'more-horizontal': LucideMoreHorizontal,
2207
- calculator: LucideCalculator,
2208
- filter: LucideFilter,
2209
- 'arrow-right': LucideArrowRight,
2210
- 'arrow-left': LucideArrowLeft,
2211
- 'arrow-up': LucideArrowUp,
2212
- 'arrow-down': LucideArrowDown,
2213
- };
2214
- /** Resolves a lucide icon name to its component. Falls back to a help circle. */
2215
- function getIcon$6(name) {
2216
- if (!name)
2217
- return null;
2218
- return ICONS$2[name] ?? LucideCircleHelp;
2219
- }
2220
-
2221
2757
  const SEVERITY_CLASS = {
2222
2758
  secondary: '_shk-tag-secondary',
2223
2759
  success: '_shk-tag-success',
@@ -2235,13 +2771,13 @@ class TagComponent {
2235
2771
  this.severity = input('secondary', ...(ngDevMode ? [{ debugName: "severity" }] : /* istanbul ignore next */ []));
2236
2772
  /** Renders the tag with fully rounded corners (pill). */
2237
2773
  this.rounded = input(false, ...(ngDevMode ? [{ debugName: "rounded" }] : /* istanbul ignore next */ []));
2238
- /** Optional leading lucide icon name. */
2774
+ /** Optional leading lucide icon name or component. */
2239
2775
  this.icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
2240
2776
  /** Inline styles applied to the tag. */
2241
2777
  this.style = input({}, ...(ngDevMode ? [{ debugName: "style" }] : /* istanbul ignore next */ []));
2242
2778
  /** Extra CSS classes. */
2243
2779
  this.styleClass = input('', ...(ngDevMode ? [{ debugName: "styleClass" }] : /* istanbul ignore next */ []));
2244
- this.getIcon = getIcon$6;
2780
+ this.getIcon = getIcon;
2245
2781
  this.tagClass = computed(() => {
2246
2782
  const base = 'inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium';
2247
2783
  const severity = SEVERITY_CLASS[this.severity()] ?? SEVERITY_CLASS.secondary;
@@ -2279,22 +2815,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
2279
2815
 
2280
2816
  const TOOLTIP_Z_INDEX = '2147483647';
2281
2817
  /**
2282
- * Tooltip que se renderiza en `document.body` con `position: fixed`, de modo
2283
- * que siempre flota por encima de cualquier contenedor (tablas con
2284
- * `overflow: hidden`, modales, scroll wrappers, etc.) y nunca queda
2285
- * recortado ni pasa por debajo de su elemento ancla.
2818
+ * Shirkasoft UI Components.
2819
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
2820
+ * Belongs to Shirkasoft.
2821
+ */
2822
+ /**
2823
+ * Tooltip rendered in `document.body` with `position: fixed`, so it always
2824
+ * floats above any container (tables with `overflow: hidden`, modals, scroll
2825
+ * wrappers, etc.) and never gets clipped or hidden behind its anchor element.
2286
2826
  */
2287
2827
  class TooltipComponent {
2288
2828
  constructor() {
2829
+ /** Content of the tooltip. */
2289
2830
  this.text = input('', ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
2831
+ /** Preferred placement relative to the trigger. */
2290
2832
  this.position = input('bottom', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
2833
+ /** 'hover' shows on mouse enter; 'fixed' stays pinned to the content. */
2291
2834
  this.mode = input('hover', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
2835
+ /** Distance (px) between the tooltip and the trigger. */
2292
2836
  this.offset = input(8, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
2293
2837
  this.host = inject((ElementRef));
2294
2838
  this.document = inject(DOCUMENT);
2839
+ /** The dynamically created tooltip element. */
2295
2840
  this.tooltipEl = null;
2841
+ /** Unsubscribe callbacks for the scroll/resize listeners. */
2296
2842
  this.cleanup = null;
2297
2843
  }
2844
+ /** Shows the tooltip over the host trigger on mouseenter. */
2298
2845
  onMouseEnter() {
2299
2846
  if (!this.text())
2300
2847
  return;
@@ -2303,9 +2850,11 @@ class TooltipComponent {
2303
2850
  return;
2304
2851
  this.show(trigger);
2305
2852
  }
2853
+ /** Hides the tooltip on mouseleave. */
2306
2854
  onMouseLeave() {
2307
2855
  this.hide();
2308
2856
  }
2857
+ /** Creates and positions the tooltip element on `document.body`. */
2309
2858
  show(trigger) {
2310
2859
  this.removeTooltip();
2311
2860
  const el = this.document.createElement('span');
@@ -2351,6 +2900,7 @@ class TooltipComponent {
2351
2900
  window.removeEventListener('resize', onScrollOrResize);
2352
2901
  };
2353
2902
  }
2903
+ /** Computes the tooltip position, clamped to the viewport. */
2354
2904
  computePosition(rect, width, height) {
2355
2905
  const off = this.offset();
2356
2906
  const viewportW = this.document.defaultView?.innerWidth ?? 0;
@@ -2374,6 +2924,7 @@ class TooltipComponent {
2374
2924
  const top = Math.max(margin, Math.min(y, Math.max(margin, viewportH - height - margin)));
2375
2925
  return { left, top };
2376
2926
  }
2927
+ /** Fades the tooltip out and removes it from the DOM. */
2377
2928
  hide() {
2378
2929
  if (!this.tooltipEl)
2379
2930
  return;
@@ -2384,12 +2935,14 @@ class TooltipComponent {
2384
2935
  el.remove();
2385
2936
  }, 150);
2386
2937
  }
2938
+ /** Removes the tooltip and its listeners. */
2387
2939
  removeTooltip() {
2388
2940
  this.cleanup?.();
2389
2941
  this.cleanup = null;
2390
2942
  this.tooltipEl?.remove();
2391
2943
  this.tooltipEl = null;
2392
2944
  }
2945
+ /** Cleans up the tooltip on component destroy. */
2393
2946
  ngOnDestroy() {
2394
2947
  this.removeTooltip();
2395
2948
  }
@@ -2419,87 +2972,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
2419
2972
  args: ['mouseleave']
2420
2973
  }] } });
2421
2974
 
2422
- const icons$3 = {
2423
- 'chevrons-left': LucideChevronsLeft,
2424
- 'chevrons-right': LucideChevronsRight,
2425
- 'chevron-left': LucideChevronLeft,
2426
- 'chevron-right': LucideChevronRight,
2427
- 'arrow-up': LucideArrowUp,
2428
- 'arrow-down': LucideArrowDown,
2429
- 'arrow-up-down': LucideArrowUpDown,
2430
- search: LucideSearch,
2431
- x: LucideX,
2432
- loader: LucideLoader,
2433
- inbox: LucideInbox,
2434
- filter: LucideFunnel,
2435
- funnel: LucideFunnel,
2436
- pencil: LucidePencil,
2437
- edit: LucidePencil,
2438
- 'trash-2': LucideTrash2,
2439
- delete: LucideTrash2,
2440
- eye: LucideEye,
2441
- view: LucideEye,
2442
- key: LucideKey,
2443
- 'change-password': LucideKey,
2444
- 'user-pen': LucideUserPen,
2445
- 'edit-user': LucideUserPen,
2446
- 'toggle-right': LucideToggleRight,
2447
- activate: LucideToggleRight,
2448
- 'toggle-left': LucideToggleLeft,
2449
- deactivate: LucideToggleLeft,
2450
- download: LucideDownload,
2451
- export: LucideDownload,
2452
- upload: LucideUpload,
2453
- import: LucideUpload,
2454
- plus: LucidePlus,
2455
- add: LucidePlus,
2456
- 'circle-plus': LucideCirclePlus,
2457
- 'eye-off': LucideEyeOff,
2458
- 'refresh-cw': LucideRefreshCw,
2459
- 'file-spreadsheet': LucideFileSpreadsheet,
2460
- presentation: LucidePresentation,
2461
- file: LucideFile,
2462
- 'file-text': LucideFileText,
2463
- archive: LucideArchive,
2464
- 'circle-check': LucideCircleCheck,
2465
- 'circle-x': LucideCircleX,
2466
- check: LucideCheck,
2467
- copy: LucideCopy,
2468
- 'share-2': LucideShare2,
2469
- play: LucidePlay,
2470
- video: LucideVideo,
2471
- star: LucideStar,
2472
- clock: LucideClock,
2473
- settings: LucideSettings,
2474
- 'log-out': LucideLogOut,
2475
- logout: LucideLogOut,
2476
- user: LucideUser,
2477
- 'user-check': LucideUserCheck,
2478
- 'user-round-x': LucideUserRoundX,
2479
- 'user-plus': LucideUserPlus,
2480
- building: LucideBuilding,
2481
- landmark: LucideLandmark,
2482
- package: LucidePackage,
2483
- box: LucideBox,
2484
- tags: LucideTags,
2485
- tag: LucideTag,
2486
- truck: LucideTruck,
2487
- users: LucideUsers,
2488
- receipt: LucideReceipt,
2489
- bot: LucideBot,
2490
- 'line-chart': LucideLineChart,
2491
- 'dollar-sign': LucideDollarSign,
2492
- percent: LucidePercent,
2493
- calculator: LucideCalculator,
2494
- 'shield-check': LucideShieldCheck,
2495
- 'user-cog': LucideUserCog,
2496
- warehouse: LucideWarehouse,
2497
- list: LucideList,
2498
- };
2499
- function getIcon$5(name) {
2500
- return icons$3[name] ?? LucidePlus;
2501
- }
2975
+ /**
2976
+ * Shirkasoft UI Components.
2977
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
2978
+ * Belongs to Shirkasoft.
2979
+ */
2980
+ /**
2981
+ * Data table with sorting, column filters, global search, pagination,
2982
+ * row/header actions, tag cells and optional server-side mode.
2983
+ */
2502
2984
  class TableComponent {
2985
+ /** Returns the effective CSS width for a column ('actions' included). */
2503
2986
  getColumnWidth(col) {
2504
2987
  return col.width || this.DEFAULT_COLUMN_WIDTH;
2505
2988
  }
@@ -2508,69 +2991,109 @@ class TableComponent {
2508
2991
  this.cdr = inject(ChangeDetectorRef);
2509
2992
  this.transloco = inject(TranslocoService);
2510
2993
  this.destroyRef = inject(DestroyRef);
2511
- this.getIcon = getIcon$5;
2994
+ /** Maps icon names to lucide components, falling back to a plus icon. */
2995
+ this.getIcon = (name) => getIcon(name, LucidePlus);
2512
2996
  /**
2513
- * FIX responsive: ancho mínimo que toma cada columna (excepto 'actions',
2514
- * que ya tiene su propio ancho calculado) SOLO en pantallas angostas
2515
- * (ver el media query en `styles`). En pantallas anchas esta variable no
2516
- * se usa: las columnas se estiran normalmente al 100% del contenedor,
2517
- * igual que antes. Ajustable por columna vía `col.width` si alguna
2518
- * necesita más o menos espacio en mobile.
2997
+ * Minimum width applied to each column (except `actions`, which keeps its
2998
+ * own computed width) only on narrow screens. On wide screens columns
2999
+ * stretch normally to fill the container width. Can be overridden per
3000
+ * column via `col.width`.
2519
3001
  */
2520
3002
  this.DEFAULT_COLUMN_WIDTH = '140px';
3003
+ /** Signal with the translated "Actions" header label. */
2521
3004
  this.actionsHeaderLabel = toSignal(this.transloco.selectTranslate('common.actions'), {
2522
3005
  initialValue: 'Acciones',
2523
3006
  });
2524
- // FIX #14: idioma como señal para que los textos traducidos de forma
2525
- // imperativa (transloco.translate) se recalculen al cambiar de idioma.
3007
+ /** Signal reacting to language changes so imperative translations are recomputed. */
2526
3008
  this.currentLang = toSignal(this.transloco.langChanges$, {
2527
3009
  initialValue: this.transloco.getActiveLang(),
2528
3010
  });
3011
+ /** Reactive signal of loaded translations so pageReport is not stuck with raw keys. */
3012
+ this.translations = toSignal(this.transloco.selectTranslation());
3013
+ /** Full dataset. In server-side mode only the current page rows. */
2529
3014
  this.data = input([], ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
3015
+ /** Column definitions (field, header, filterable, tag cell options, etc.). */
2530
3016
  this.columns = input([], ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
3017
+ /** Number of rows displayed per page. */
2531
3018
  this.rowsPerPage = input(10, ...(ngDevMode ? [{ debugName: "rowsPerPage" }] : /* istanbul ignore next */ []));
3019
+ /** Options shown in the rows-per-page selector. */
2532
3020
  this.rowsPerPageOptions = input([10, 25, 50], ...(ngDevMode ? [{ debugName: "rowsPerPageOptions" }] : /* istanbul ignore next */ []));
3021
+ /** Whether the table is in loading state (shows the overlay/skeleton). */
2533
3022
  this.loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
3023
+ /** Whether to render the row actions column. */
2534
3024
  this.showActionRow = input(true, ...(ngDevMode ? [{ debugName: "showActionRow" }] : /* istanbul ignore next */ []));
3025
+ /** Custom cell/header templates keyed by column field. */
2535
3026
  this.customTemplates = input({}, ...(ngDevMode ? [{ debugName: "customTemplates" }] : /* istanbul ignore next */ []));
3027
+ /** Actions rendered in the table header. */
2536
3028
  this.headerActions = input([], ...(ngDevMode ? [{ debugName: "headerActions" }] : /* istanbul ignore next */ []));
3029
+ /** Actions rendered per row (icon, label, class, visibility/disabled). */
2537
3030
  this.rowActions = input([], ...(ngDevMode ? [{ debugName: "rowActions" }] : /* istanbul ignore next */ []));
3031
+ /** Whether the table container renders a drop shadow. */
2538
3032
  this.hasShadow = input(true, ...(ngDevMode ? [{ debugName: "hasShadow" }] : /* istanbul ignore next */ []));
3033
+ /** Field used as the default initial sort. */
2539
3034
  this.defaultSortField = input('', ...(ngDevMode ? [{ debugName: "defaultSortField" }] : /* istanbul ignore next */ []));
3035
+ /** Order used by the default initial sort (1 asc, -1 desc). */
2540
3036
  this.defaultSortOrder = input(1, ...(ngDevMode ? [{ debugName: "defaultSortOrder" }] : /* istanbul ignore next */ []));
3037
+ /** Whether the global search input is shown. */
2541
3038
  this.showSearch = input(true, ...(ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []));
3039
+ /** Placeholder for the global search input. */
2542
3040
  this.searchPlaceholder = input('', ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
3041
+ /** Message shown when there are no rows. */
2543
3042
  this.emptyMessage = input('', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
3043
+ /** Enables server-side pagination/filtering/sorting mode. */
2544
3044
  this.serverSide = input(false, ...(ngDevMode ? [{ debugName: "serverSide" }] : /* istanbul ignore next */ []));
3045
+ /** Total number of records in server-side mode (for pagination math). */
2545
3046
  this.totalRecords = input(0, ...(ngDevMode ? [{ debugName: "totalRecords" }] : /* istanbul ignore next */ []));
3047
+ /** Optional prebuilt filter chips (label + value) shown above the table. */
2546
3048
  this.filters = input([], ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
3049
+ /** Currently active filter chip value. */
2547
3050
  this.activeFilter = input('', ...(ngDevMode ? [{ debugName: "activeFilter" }] : /* istanbul ignore next */ []));
3051
+ /** Emits when the user clicks the refresh button. */
2548
3052
  this.refresh = output();
3053
+ /** Emits page changes (server-side mode) with first/rows/page info. */
2549
3054
  this.pageChange = output();
3055
+ /** Emits the active column filters. */
2550
3056
  this.filterChange = output();
3057
+ /** Emits the debounced global search term. */
2551
3058
  this.searchChange = output();
3059
+ /** Emits when a filter chip is clicked. */
2552
3060
  this.filterClick = output();
3061
+ /** Internal copy of the input data used as the base for search/filter. */
2553
3062
  this.originalData = signal([], ...(ngDevMode ? [{ debugName: "originalData" }] : /* istanbul ignore next */ []));
3063
+ /** Rows after the global search is applied. */
2554
3064
  this.searchedData = signal([], ...(ngDevMode ? [{ debugName: "searchedData" }] : /* istanbul ignore next */ []));
3065
+ /** Rows after column filters are applied. */
2555
3066
  this.filteredData = signal([], ...(ngDevMode ? [{ debugName: "filteredData" }] : /* istanbul ignore next */ []));
3067
+ /** Rows belonging to the current page (after slicing). */
2556
3068
  this.displayedData = signal([], ...(ngDevMode ? [{ debugName: "displayedData" }] : /* istanbul ignore next */ []));
3069
+ /** Local total records after filtering (client mode). */
2557
3070
  this.localTotalRecords = signal(0, ...(ngDevMode ? [{ debugName: "localTotalRecords" }] : /* istanbul ignore next */ []));
3071
+ /** Index of the first row of the current page. */
2558
3072
  this.first = signal(0, ...(ngDevMode ? [{ debugName: "first" }] : /* istanbul ignore next */ []));
3073
+ /** Visibility map for each column filter input. */
2559
3074
  this.showFilterInput = signal({}, ...(ngDevMode ? [{ debugName: "showFilterInput" }] : /* istanbul ignore next */ []));
3075
+ /** Current global search term. */
2560
3076
  this.searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
3077
+ /** Currently sorted field. */
2561
3078
  this.sortField = signal('', ...(ngDevMode ? [{ debugName: "sortField" }] : /* istanbul ignore next */ []));
3079
+ /** Current sort direction (1 asc, -1 desc). */
2562
3080
  this.sortOrder = signal(1, ...(ngDevMode ? [{ debugName: "sortOrder" }] : /* istanbul ignore next */ []));
2563
- this.rowsPerPageLocal = signal(10, ...(ngDevMode ? [{ debugName: "rowsPerPageLocal" }] : /* istanbul ignore next */ []));
3081
+ /** Local rows per page, kept in sync with the `rowsPerPage` input. */
3082
+ this.rowsPerPageLocal = linkedSignal(() => this.rowsPerPage(), ...(ngDevMode ? [{ debugName: "rowsPerPageLocal" }] : /* istanbul ignore next */ []));
3083
+ /** Total number of pages based on the effective record count. */
2564
3084
  this.totalPages = computed(() => Math.max(1, Math.ceil(this.effectiveTotalRecords() / this.rowsPerPageLocal())), ...(ngDevMode ? [{ debugName: "totalPages" }] : /* istanbul ignore next */ []));
3085
+ /** Current page number (1-based). */
2565
3086
  this.currentPage = computed(() => Math.floor(this.first() / this.rowsPerPageLocal()) + 1, ...(ngDevMode ? [{ debugName: "currentPage" }] : /* istanbul ignore next */ []));
2566
- // FIX #4 (medio): debounce también para el buscador global en modo servidor.
3087
+ /** Subjects used to debounce column filters (server mode) and search. */
2567
3088
  this.filterSubject = new Subject();
2568
3089
  this.searchSubject = new Subject();
2569
- // FIX #3/#crítico: para evitar reconstruir el FormGroup (y perder lo escrito
2570
- // por el usuario) cuando `columns()` cambia de referencia pero el set de
2571
- // campos filtrables es el mismo.
3090
+ /**
3091
+ * Signature of the last built set of filterable fields (+filter type) to
3092
+ * avoid rebuilding the form (and losing user input) on reference changes.
3093
+ */
2572
3094
  this.lastFilterFieldsSignature = '';
2573
3095
  this.columnFilterSubscriptions = [];
3096
+ /** Columns array with the leading `actions` column when row actions exist. */
2574
3097
  this.columnsWithActions = computed(() => {
2575
3098
  const columnsArray = this.columns();
2576
3099
  const showAction = this.showActionRow();
@@ -2590,16 +3113,24 @@ class TableComponent {
2590
3113
  }
2591
3114
  return [...columnsArray];
2592
3115
  }, ...(ngDevMode ? [{ debugName: "columnsWithActions" }] : /* istanbul ignore next */ []));
3116
+ /** Total records: server-provided in server mode, locally computed otherwise. */
2593
3117
  this.effectiveTotalRecords = computed(() => {
2594
3118
  return this.serverSide() ? this.totalRecords() : this.localTotalRecords();
2595
3119
  }, ...(ngDevMode ? [{ debugName: "effectiveTotalRecords" }] : /* istanbul ignore next */ []));
3120
+ /**
3121
+ * Rows rendered by the template: the input data in server-side mode or the
3122
+ * locally sliced/page current rows otherwise.
3123
+ */
2596
3124
  this.effectiveDisplayedData = computed(() => {
2597
3125
  return this.serverSide() ? this.data() : this.displayedData();
2598
3126
  }, ...(ngDevMode ? [{ debugName: "effectiveDisplayedData" }] : /* istanbul ignore next */ []));
2599
- // FIX #14: pageReport ahora es un computed reactivo (incluye idioma como
2600
- // dependencia) en vez de un método imperativo llamado desde el template.
3127
+ /**
3128
+ * Reactive "X to Y of Z" report. Depends on loaded translations and the
3129
+ * active language so it always reflects the translated template.
3130
+ */
2601
3131
  this.pageReport = computed(() => {
2602
- this.currentLang(); // fuerza recálculo al cambiar de idioma
3132
+ this.translations(); // forces recomputation when translations finish loading
3133
+ this.currentLang(); // forces recomputation when the language changes
2603
3134
  const total = this.effectiveTotalRecords();
2604
3135
  if (total === 0) {
2605
3136
  return this.transloco.translate('table.pageReport', {
@@ -2613,6 +3144,7 @@ class TableComponent {
2613
3144
  return this.transloco.translate('table.pageReport', { from, to, total });
2614
3145
  }, ...(ngDevMode ? [{ debugName: "pageReport" }] : /* istanbul ignore next */ []));
2615
3146
  this.columnFiltersForm = this.fb.group({});
3147
+ // Debounced column-filter changes (server-side mode only).
2616
3148
  this.filterSubject
2617
3149
  .pipe(debounceTime(300), takeUntilDestroyed(this.destroyRef))
2618
3150
  .subscribe(() => {
@@ -2620,11 +3152,14 @@ class TableComponent {
2620
3152
  this.emitFilterChange();
2621
3153
  }
2622
3154
  });
3155
+ // Debounced global search changes (server-side mode only).
2623
3156
  this.searchSubject
2624
3157
  .pipe(debounceTime(300), takeUntilDestroyed(this.destroyRef))
2625
3158
  .subscribe((value) => {
2626
3159
  this.searchChange.emit(value);
2627
3160
  });
3161
+ // When input data changes, refresh the internal copy and re-apply filters
3162
+ // unless the server already handles the filtering.
2628
3163
  effect(() => {
2629
3164
  const newData = this.data();
2630
3165
  untracked(() => {
@@ -2634,12 +3169,14 @@ class TableComponent {
2634
3169
  }
2635
3170
  });
2636
3171
  });
3172
+ // Rebuild the column filter form whenever the columns change.
2637
3173
  effect(() => {
2638
3174
  const cols = this.columns();
2639
3175
  untracked(() => {
2640
3176
  this.setupColumnFilters(cols);
2641
3177
  });
2642
3178
  });
3179
+ // Apply the default sort once when the default fields are provided.
2643
3180
  effect(() => {
2644
3181
  const field = this.defaultSortField();
2645
3182
  const order = this.defaultSortOrder();
@@ -2654,6 +3191,7 @@ class TableComponent {
2654
3191
  });
2655
3192
  }
2656
3193
  });
3194
+ // Apply the global search locally whenever the term changes.
2657
3195
  effect(() => {
2658
3196
  const query = this.searchQuery();
2659
3197
  untracked(() => {
@@ -2662,18 +3200,12 @@ class TableComponent {
2662
3200
  }
2663
3201
  });
2664
3202
  });
2665
- effect(() => {
2666
- this.rowsPerPageLocal.set(this.rowsPerPage());
2667
- });
2668
3203
  }
2669
3204
  /**
2670
- * FIX crítico: antes se reconstruía el FormGroup completo (perdiendo el
2671
- * texto escrito por el usuario en los filtros y el estado de visibilidad
2672
- * de cada filtro) cada vez que `columns()` cambiaba de referencia, aunque
2673
- * el conjunto de campos filtrables fuera idéntico. Ahora comparamos una
2674
- * "firma" del set de campos+tipo de filtro y solo reconstruimos si
2675
- * realmente cambió. Además, las suscripciones previas se limpian antes de
2676
- * crear las nuevas para no acumular fugas de memoria.
3205
+ * Builds (or keeps) the reactive form with one control per filterable
3206
+ * column. The form is only rebuilt when the set of filterable
3207
+ * fields/filter types changes, preserving the values already typed by the
3208
+ * user otherwise. Previous subscriptions are unsubscribed to avoid leaks.
2677
3209
  */
2678
3210
  setupColumnFilters(cols) {
2679
3211
  const filterableCols = cols.filter((c) => c.filter);
@@ -2682,7 +3214,7 @@ class TableComponent {
2682
3214
  return;
2683
3215
  }
2684
3216
  this.lastFilterFieldsSignature = signature;
2685
- // Limpiar suscripciones anteriores antes de reconstruir.
3217
+ // Clean up previous subscriptions before rebuilding.
2686
3218
  this.columnFilterSubscriptions.forEach((sub) => sub.unsubscribe());
2687
3219
  this.columnFilterSubscriptions = [];
2688
3220
  const previousValues = {};
@@ -2724,6 +3256,7 @@ class TableComponent {
2724
3256
  }
2725
3257
  });
2726
3258
  }
3259
+ /** Collects the non-empty column filter values and emits them. */
2727
3260
  emitFilterChange() {
2728
3261
  const filters = {};
2729
3262
  Object.keys(this.columnFiltersForm.controls).forEach((field) => {
@@ -2734,6 +3267,7 @@ class TableComponent {
2734
3267
  });
2735
3268
  this.filterChange.emit({ filters });
2736
3269
  }
3270
+ /** Applies the global search over the original data (client mode). */
2737
3271
  applySearch() {
2738
3272
  const original = this.originalData();
2739
3273
  const query = this.searchQuery().toLowerCase();
@@ -2752,9 +3286,14 @@ class TableComponent {
2752
3286
  }
2753
3287
  this.applyColumnFilters();
2754
3288
  }
3289
+ /** Entry point that applies search (which cascades into column filters). */
2755
3290
  applyFilters() {
2756
3291
  this.applySearch();
2757
3292
  }
3293
+ /**
3294
+ * Applies every column filter on top of the searched data using each
3295
+ * column's `filterType` ('select', 'exact' or text contains).
3296
+ */
2758
3297
  applyColumnFilters() {
2759
3298
  const data = this.searchedData();
2760
3299
  let filtered = [...data];
@@ -2781,6 +3320,7 @@ class TableComponent {
2781
3320
  this.applySort();
2782
3321
  this.updateDisplayedData();
2783
3322
  }
3323
+ /** Slices the filtered rows for the current page. */
2784
3324
  updateDisplayedData() {
2785
3325
  const filtered = this.filteredData();
2786
3326
  const firstIndex = this.first();
@@ -2788,9 +3328,11 @@ class TableComponent {
2788
3328
  const displayed = filtered.slice(firstIndex, firstIndex + rows);
2789
3329
  this.displayedData.set(displayed);
2790
3330
  }
3331
+ /** Emits the filter chip clicked in the template. */
2791
3332
  onFilterClick(value) {
2792
3333
  this.filterClick.emit(value);
2793
3334
  }
3335
+ /** Toggles the sort direction of a column (first click sorts ascending). */
2794
3336
  toggleSort(field) {
2795
3337
  if (this.sortField() === field) {
2796
3338
  this.sortOrder.set(this.sortOrder() * -1);
@@ -2806,11 +3348,8 @@ class TableComponent {
2806
3348
  }
2807
3349
  }
2808
3350
  /**
2809
- * FIX: se documenta explícitamente el criterio de valores null/undefined.
2810
- * Se mantienen SIEMPRE al final de la lista, sin importar la dirección del
2811
- * orden (comportamiento común e intencional en tablas de datos). Si en tu
2812
- * caso de uso prefieres que "sigan" la dirección del sort, multiplica
2813
- * ambos returns por `order`.
3351
+ * Sorts the filtered rows by the current field. Rows with null/undefined
3352
+ * values are always kept at the end regardless of sort direction.
2814
3353
  */
2815
3354
  applySort() {
2816
3355
  const field = this.sortField();
@@ -2824,7 +3363,7 @@ class TableComponent {
2824
3363
  if (va == null && vb == null)
2825
3364
  return 0;
2826
3365
  if (va == null)
2827
- return 1; // nulls siempre al final
3366
+ return 1; // nulls always at the end
2828
3367
  if (vb == null)
2829
3368
  return -1;
2830
3369
  if (va < vb)
@@ -2835,6 +3374,7 @@ class TableComponent {
2835
3374
  });
2836
3375
  this.filteredData.set(data);
2837
3376
  }
3377
+ /** Handles the global search input (updates query, resets page, debounces server mode). */
2838
3378
  onSearchInput(value) {
2839
3379
  this.searchQuery.set(value);
2840
3380
  this.first.set(0);
@@ -2842,6 +3382,7 @@ class TableComponent {
2842
3382
  this.searchSubject.next(value);
2843
3383
  }
2844
3384
  }
3385
+ /** Clears a column filter value and hides its input. */
2845
3386
  clearFilter(field) {
2846
3387
  const column = this.columns().find((col) => col.field === field);
2847
3388
  const value = column?.filterType === 'select' ? null : '';
@@ -2850,24 +3391,25 @@ class TableComponent {
2850
3391
  this.showFilterInput.set({ ...visibility, [field]: false });
2851
3392
  this.cdr.markForCheck();
2852
3393
  }
3394
+ /** Toggles the visibility of a column filter input. */
2853
3395
  toggleFilter(field) {
2854
3396
  const visibility = this.showFilterInput();
2855
3397
  this.showFilterInput.set({ ...visibility, [field]: !visibility[field] });
2856
3398
  this.cdr.markForCheck();
2857
3399
  }
2858
3400
  /**
2859
- * FIX #5 (falsy zero): antes el template usaba `columnFiltersForm.get(field)?.value`
2860
- * directamente en un `@if`/`[class]`, lo cual falla cuando el valor es `0`
2861
- * (número), ya que `0` es falsy en JS. Este helper compara explícitamente
2862
- * contra null/undefined/''.
3401
+ * Whether a column filter is non-empty. Avoids treating the number 0 as
3402
+ * "no value".
2863
3403
  */
2864
3404
  hasFilterValue(field) {
2865
3405
  const value = this.columnFiltersForm.get(field)?.value;
2866
3406
  return value !== null && value !== undefined && value !== '';
2867
3407
  }
3408
+ /** Returns the reactive page report string (template helper). */
2868
3409
  getPageReport() {
2869
3410
  return this.pageReport();
2870
3411
  }
3412
+ /** Goes to the first page (emits pageChange in server-side mode). */
2871
3413
  goToFirst() {
2872
3414
  const rows = this.rowsPerPageLocal();
2873
3415
  this.first.set(0);
@@ -2883,6 +3425,7 @@ class TableComponent {
2883
3425
  this.updateDisplayedData();
2884
3426
  }
2885
3427
  }
3428
+ /** Goes to the last page (emits pageChange in server-side mode). */
2886
3429
  goToLast() {
2887
3430
  const total = this.serverSide() ? this.totalRecords() : this.filteredData().length;
2888
3431
  const rows = this.rowsPerPageLocal();
@@ -2901,6 +3444,7 @@ class TableComponent {
2901
3444
  this.updateDisplayedData();
2902
3445
  }
2903
3446
  }
3447
+ /** Updates rows-per-page and resets to the first page. */
2904
3448
  onRowsPerPageChange(rows) {
2905
3449
  rows = Number(rows);
2906
3450
  this.rowsPerPageLocal.set(rows);
@@ -2917,16 +3461,20 @@ class TableComponent {
2917
3461
  this.updateDisplayedData();
2918
3462
  }
2919
3463
  }
3464
+ /** Emits the refresh output. */
2920
3465
  refreshData() {
2921
3466
  this.refresh.emit();
2922
3467
  }
3468
+ /** Whether the current page is the first one. */
2923
3469
  isFirstPage() {
2924
3470
  return this.first() === 0;
2925
3471
  }
3472
+ /** Whether the current page is the last one. */
2926
3473
  isLastPage() {
2927
3474
  const total = this.effectiveTotalRecords();
2928
3475
  return this.first() + this.rowsPerPageLocal() >= total;
2929
3476
  }
3477
+ /** Moves to the next page (emits pageChange in server-side mode). */
2930
3478
  next() {
2931
3479
  const total = this.serverSide() ? this.totalRecords() : this.filteredData().length;
2932
3480
  const rows = this.rowsPerPageLocal();
@@ -2948,6 +3496,7 @@ class TableComponent {
2948
3496
  this.updateDisplayedData();
2949
3497
  }
2950
3498
  }
3499
+ /** Moves to the previous page (emits pageChange in server mode). */
2951
3500
  prev() {
2952
3501
  const rows = this.rowsPerPageLocal();
2953
3502
  let first = this.first() - rows;
@@ -2969,68 +3518,97 @@ class TableComponent {
2969
3518
  this.updateDisplayedData();
2970
3519
  }
2971
3520
  }
3521
+ /** Resets the table to the first page. */
2972
3522
  reset() {
2973
3523
  this.goToFirst();
2974
3524
  }
3525
+ /** Resolves the row action label, static or per-row function. */
2975
3526
  getRowActionLabel(action, rowData) {
2976
3527
  return typeof action.label === 'function' ? action.label(rowData) : action.label;
2977
3528
  }
3529
+ /**
3530
+ * Resolves the row action icon: a lucide component, a function returning
3531
+ * the icon name, or a raw string resolved by the central icon map.
3532
+ */
2978
3533
  getRowActionIconName(action, rowData) {
2979
- return typeof action.icon === 'function' ? action.icon(rowData) : action.icon;
3534
+ const icon = action.icon;
3535
+ if (typeof icon === 'function') {
3536
+ if (icon.prototype === undefined) {
3537
+ return icon(rowData);
3538
+ }
3539
+ return icon;
3540
+ }
3541
+ return icon;
2980
3542
  }
3543
+ /** Resolves the CSS class of a row action, static or per-row function. */
2981
3544
  getRowActionClass(action, rowData) {
2982
3545
  if (!action.class)
2983
3546
  return '';
2984
3547
  return typeof action.class === 'function' ? action.class(rowData) : action.class;
2985
3548
  }
3549
+ /** Whether a header action is disabled. */
2986
3550
  isHeaderActionDisabled(action) {
2987
3551
  return action.isDisabled ? action.isDisabled() : false;
2988
3552
  }
3553
+ /** Whether a header action is visible. */
2989
3554
  isHeaderActionVisible(action) {
2990
3555
  return action.isVisible ? action.isVisible() : true;
2991
3556
  }
3557
+ /** Whether a row action is disabled for the given row. */
2992
3558
  isRowActionDisabled(action, rowData) {
2993
3559
  return action.isDisabled ? action.isDisabled(rowData) : false;
2994
3560
  }
3561
+ /** Whether a row action is visible for the given row. */
2995
3562
  isRowActionVisible(action, rowData) {
2996
3563
  return action.isVisible ? action.isVisible(rowData) : true;
2997
3564
  }
3565
+ /** Truncates a value with an ellipsis past the given character limit. */
2998
3566
  truncate(value, limit = 30) {
2999
3567
  if (value === null || value === undefined)
3000
3568
  return '';
3001
3569
  const str = String(value);
3002
3570
  return str.length > limit ? str.slice(0, limit) + '\u2026' : str;
3003
3571
  }
3572
+ /** Resolves the value displayed inside a tag cell, static or per-row function. */
3004
3573
  getTagValue(col, rowData) {
3005
3574
  return col.tagValue ? col.tagValue(rowData) : String(rowData[col.field] ?? '');
3006
3575
  }
3576
+ /** Resolves and validates the tag severity, falling back to 'secondary'. */
3007
3577
  getTagSeverity(col, rowData) {
3008
3578
  const raw = col.tagSeverity ? col.tagSeverity(rowData) : 'secondary';
3009
- const allowed = ['secondary', 'success', 'info', 'warning', 'danger', 'contrast'];
3579
+ const allowed = [
3580
+ 'secondary',
3581
+ 'success',
3582
+ 'info',
3583
+ 'warning',
3584
+ 'danger',
3585
+ 'contrast',
3586
+ ];
3010
3587
  return allowed.includes(raw) ? raw : 'secondary';
3011
3588
  }
3589
+ /** Resolves whether the tag in a cell is rounded, static or per-row function. */
3012
3590
  getTagRounded(col, rowData) {
3013
3591
  if (typeof col.tagRounded === 'function')
3014
3592
  return col.tagRounded(rowData);
3015
3593
  return col.tagRounded ?? false;
3016
3594
  }
3595
+ /** Resolves the icon shown inside a tag cell, static or per-row function. */
3017
3596
  getTagIcon(col, rowData) {
3018
3597
  if (typeof col.tagIcon === 'function')
3019
3598
  return col.tagIcon(rowData);
3020
3599
  return col.tagIcon ?? '';
3021
3600
  }
3601
+ /** Resolves the CSS styles of a tag cell, static or per-row function. */
3022
3602
  getTagStyle(col, rowData) {
3023
3603
  return col.tagStyle ? col.tagStyle(rowData) || {} : {};
3024
3604
  }
3025
3605
  /**
3026
- * FIX #7 (medio): trackBy real por identidad de fila en vez de `$index`.
3027
- * Si tu dato no tiene `id`/`_id`, cae de vuelta al índice (comportamiento
3028
- * anterior), pero se recomienda pasar filas con un identificador único.
3606
+ * trackBy for row identity: prefers `id`/`_id` and falls back to the index.
3029
3607
  */
3030
3608
  trackByRow(row, index) {
3031
3609
  return row?.id ?? row?._id ?? index;
3032
3610
  }
3033
- /** FIX accesibilidad: soporte de teclado (Enter/Espacio) para ordenar columnas. */
3611
+ /** Keyboard support (Enter/Space) for sorting a column header. */
3034
3612
  onHeaderKeydown(event, col) {
3035
3613
  if (col.field === 'actions' || !col.sortable)
3036
3614
  return;
@@ -3039,7 +3617,7 @@ class TableComponent {
3039
3617
  this.toggleSort(col.field);
3040
3618
  }
3041
3619
  }
3042
- /** FIX accesibilidad: valor aria-sort para el <th> correspondiente. */
3620
+ /** Accessible aria-sort value for a given column header. */
3043
3621
  getAriaSort(col) {
3044
3622
  if (col.field === 'actions' || !col.sortable)
3045
3623
  return 'none';
@@ -3062,92 +3640,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
3062
3640
  ], template: "<div class=\"space-y-2 min-[1440px]:space-y-3 mt-2 min-[1440px]:mt-3 animate-fade-in pb-8\">\n @if (showSearch() || filters().length > 0 || headerActions().length > 0) {\n <div class=\"card p-2 min-[1440px]:p-2.5 flex flex-row items-center justify-between gap-2\">\n <div class=\"flex items-center gap-2 min-[1440px]:gap-3 flex-1 min-w-0\">\n @if (showSearch()) {\n <div class=\"relative w-full md:w-52 min-[1440px]:w-56\">\n <label [for]=\"'shk-table-search'\" class=\"sr-only\">{{\n searchPlaceholder() || ('common.search_placeholder' | transloco)\n }}</label>\n <div class=\"absolute inset-y-0 left-0 pl-2.5 flex items-center pointer-events-none\">\n <svg\n [lucideIcon]=\"getIcon('search')\"\n class=\"w-3.5 h-3.5 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n </div>\n <input\n id=\"shk-table-search\"\n type=\"text\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($any($event.target).value)\"\n [placeholder]=\"searchPlaceholder() || ('common.search_placeholder' | transloco)\"\n class=\"block w-full pl-8 pr-3 py-1.5 text-xs border _shk-border dark:[border-color:var(--shk-surface-200)] rounded-md leading-5 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-500)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:outline-none focus:bg-white dark:focus:[background-color:var(--shk-surface-800)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)]\"\n />\n </div>\n }\n @if (filters().length > 0) {\n <div\n class=\"flex _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] p-0.5 rounded-md ml-auto\"\n role=\"group\"\n >\n @for (f of filters(); track f.value) {\n <button\n type=\"button\"\n (click)=\"onFilterClick(f.value)\"\n [attr.aria-pressed]=\"f.value === activeFilter()\"\n class=\"px-2.5 min-[1440px]:px-3 py-1 text-xs font-medium rounded _shk-text-surface-700 dark:[color:var(--shk-surface-700)] transition-all\"\n [class]=\"\n f.value === activeFilter()\n ? 'bg-white shadow-sm dark:[background-color:var(--shk-surface-700)]'\n : ''\n \"\n >\n {{ f.label }}\n </button>\n }\n </div>\n }\n </div>\n @if (headerActions().length > 0) {\n <div class=\"flex items-center gap-1.5 shrink-0\">\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <button\n type=\"button\"\n [class]=\"\n 'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed ' +\n (action.class || '')\n \"\n [disabled]=\"isHeaderActionDisabled(action)\"\n [attr.aria-label]=\"action.label\"\n (click)=\"action.onClick()\"\n >\n @if (action.icon) {\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n }\n {{ action.label }}\n </button>\n }\n }\n </div>\n }\n </div>\n }\n <div class=\"card p-0 overflow-hidden\" [class.shadow-lg]=\"hasShadow()\">\n <div class=\"table-scroll-wrapper _shk-scrollbar\">\n <table>\n <thead>\n <tr>\n @for (col of columnsWithActions(); track col.field) {\n <th\n scope=\"col\"\n (click)=\"col.field !== 'actions' && col.sortable && toggleSort(col.field)\"\n (keydown)=\"onHeaderKeydown($event, col)\"\n [tabindex]=\"col.field !== 'actions' && col.sortable ? 0 : -1\"\n [attr.role]=\"col.field !== 'actions' && col.sortable ? 'button' : null\"\n [attr.aria-sort]=\"getAriaSort(col)\"\n [attr.data-shk-actions]=\"col.field === 'actions' ? '' : null\"\n [style.cursor]=\"col.field !== 'actions' && col.sortable ? 'pointer' : 'default'\"\n [style.width]=\"col.field === 'actions' ? col.width : null\"\n [style.min-width]=\"col.field === 'actions' ? col.width : null\"\n [style.--shk-col-width]=\"col.field !== 'actions' ? getColumnWidth(col) : null\"\n class=\"px-2 min-[1440px]:px-3 py-2 min-[1440px]:py-2.5 text-left text-[10px] min-[1440px]:text-xs font-bold _shk-text-surface-500 dark:[color:var(--shk-surface-400)] uppercase tracking-wider _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border-b _shk-border dark:[border-color:var(--shk-surface-200)] focus:outline-none focus-visible:ring-1 focus-visible:[--tw-ring-color:var(--shk-primary-500)]\"\n >\n @if (col.field !== 'actions') {\n <div class=\"flex items-center justify-between gap-2\">\n <span>{{ col.header }}</span>\n <div class=\"flex items-center gap-1\">\n @if (col.sortable) {\n <shk-tooltip [text]=\"'table.tooltips.sort' | transloco\">\n @if (sortField() === col.field) {\n <svg\n [lucideIcon]=\"getIcon(sortOrder() === 1 ? 'arrow-up' : 'arrow-down')\"\n class=\"w-3.5 h-3.5 _shk-text-primary\"\n ></svg>\n } @else {\n <svg\n [lucideIcon]=\"getIcon('arrow-up-down')\"\n class=\"w-3.5 h-3.5 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n }\n </shk-tooltip>\n }\n @if (col.filter) {\n <shk-tooltip [text]=\"'table.tooltips.filter' | transloco\">\n <button\n type=\"button\"\n (click)=\"$event.stopPropagation(); toggleFilter(col.field)\"\n [attr.aria-label]=\"'table.tooltips.filter' | transloco\"\n [attr.aria-expanded]=\"showFilterInput()[col.field] || false\"\n class=\"_shk-text-surface-400 dark:[color:var(--shk-surface-500)] hover:[color:var(--shk-surface-600)] dark:hover:[color:var(--shk-surface-300)] transition-colors\"\n >\n <svg\n [lucideIcon]=\"getIcon('filter')\"\n class=\"w-3.5 h-3.5\"\n [class._shk-text-primary]=\"hasFilterValue(col.field)\"\n ></svg>\n </button>\n </shk-tooltip>\n }\n </div>\n </div>\n @if (col.filter && showFilterInput()[col.field]) {\n <div\n class=\"mt-2\"\n [formGroup]=\"columnFiltersForm\"\n (click)=\"$event.stopPropagation()\"\n >\n <div class=\"relative\">\n @if (col.filterType !== 'select') {\n <label [for]=\"'shk-filter-' + col.field\" class=\"sr-only\">{{\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n }}</label>\n <input\n [id]=\"'shk-filter-' + col.field\"\n type=\"text\"\n [formControlName]=\"col.field\"\n class=\"w-full px-3 py-1.5 text-sm rounded-md border _shk-border dark:[border-color:var(--shk-surface-200)] bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] placeholder:[color:var(--shk-surface-400)] dark:placeholder:[color:var(--shk-surface-500)]\"\n [placeholder]=\"\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n \"\n />\n }\n @if (col.filterType === 'select') {\n <label [for]=\"'shk-filter-' + col.field\" class=\"sr-only\">{{\n col.filterPlaceholder || ('common.search_placeholder' | transloco)\n }}</label>\n <select\n [id]=\"'shk-filter-' + col.field\"\n [formControlName]=\"col.field\"\n class=\"w-full px-2 py-1 text-xs rounded-md border _shk-border dark:[border-color:var(--shk-surface-200)] bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:[border-color:var(--shk-primary-500)] focus:ring-1 focus:[--tw-ring-color:var(--shk-primary-500)] appearance-none\"\n >\n <option [ngValue]=\"null\">{{ 'table.filterAll' | transloco }}</option>\n @for (opt of col.filterOptions || []; track opt.value) {\n <option [ngValue]=\"opt.value\">{{ opt.label }}</option>\n }\n </select>\n }\n @if (hasFilterValue(col.field) && col.filterType !== 'select') {\n <button\n type=\"button\"\n class=\"absolute right-2 top-1/2 -translate-y-1/2 _shk-text-surface-400 dark:[color:var(--shk-surface-500)] hover:[color:var(--shk-surface-600)] dark:hover:[color:var(--shk-surface-300)]\"\n (click)=\"clearFilter(col.field)\"\n >\n <svg [lucideIcon]=\"getIcon('x')\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n }\n </div>\n </div>\n }\n } @else {\n <div class=\"flex items-center justify-center\">\n <span>{{ col.header }}</span>\n </div>\n }\n </th>\n }\n </tr>\n </thead>\n @if (loading()) {\n <tbody class=\"border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <tr>\n <td [attr.colspan]=\"columnsWithActions().length\" class=\"text-center py-12\">\n <div\n class=\"flex items-center justify-center\"\n role=\"status\"\n [attr.aria-label]=\"'common.loading' | transloco\"\n >\n <svg\n [lucideIcon]=\"getIcon('loader')\"\n class=\"w-6 h-6 animate-spin _shk-text-primary\"\n ></svg>\n </div>\n </td>\n </tr>\n </tbody>\n } @else if (effectiveDisplayedData().length === 0) {\n <tbody class=\"border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <tr>\n <td [attr.colspan]=\"columnsWithActions().length\" class=\"text-center py-12\">\n <div class=\"flex flex-col items-center\">\n <div\n class=\"w-16 h-16 _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] rounded-full flex items-center justify-center mb-4\"\n >\n <svg\n [lucideIcon]=\"getIcon('inbox')\"\n class=\"w-8 h-8 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"\n ></svg>\n </div>\n <p\n class=\"text-lg font-semibold _shk-text-surface-600 dark:[color:var(--shk-surface-300)]\"\n >\n {{ emptyMessage() || ('common.no_records' | transloco) }}\n </p>\n </div>\n </td>\n </tr>\n </tbody>\n } @else {\n <tbody>\n @for (rowData of effectiveDisplayedData(); track trackByRow(rowData, $index)) {\n <tr\n class=\"hover:[background-color:color-mix(in srgb,var(--shk-primary-50)30%,transparent)] dark:hover:[background-color:color-mix(in srgb,var(--shk-primary-900)10%,transparent)] transition-colors duration-200 border-b _shk-border dark:[border-color:var(--shk-surface-100)]\"\n >\n @for (col of columnsWithActions(); track col.field) {\n <td\n [attr.data-shk-actions]=\"col.field === 'actions' ? '' : null\"\n class=\"px-2 min-[1440px]:px-3 py-2 min-[1440px]:py-2.5 text-xs font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)] whitespace-nowrap\"\n [style.overflow]=\"col.field === 'actions' ? 'visible' : 'hidden'\"\n [style.text-overflow]=\"col.field === 'actions' ? 'clip' : 'ellipsis'\"\n [style.--shk-col-width]=\"col.field !== 'actions' ? getColumnWidth(col) : null\"\n >\n @if (col.field !== 'actions') {\n @if (col.template === 'tag') {\n <shk-tag\n [value]=\"getTagValue(col, rowData)\"\n [severity]=\"getTagSeverity(col, rowData)\"\n [rounded]=\"getTagRounded(col, rowData)\"\n [icon]=\"getTagIcon(col, rowData)\"\n [style]=\"getTagStyle(col, rowData)\"\n />\n } @else if (col.format) {\n <span>{{ col.format(rowData) }}</span>\n } @else {\n <ng-container\n *ngTemplateOutlet=\"\n customTemplates()[col.field] || defaultTemplate;\n context: { $implicit: rowData, field: col.field }\n \"\n ></ng-container>\n }\n } @else {\n <div class=\"flex items-center justify-start gap-px\">\n @for (action of rowActions(); track $index) {\n @if (isRowActionVisible(action, rowData)) {\n <div class=\"relative group\">\n <shk-tooltip\n [text]=\"getRowActionLabel(action, rowData)\"\n position=\"top\"\n >\n <button\n type=\"button\"\n (click)=\"action.onClick(rowData)\"\n [class]=\"\n 'table-action-btn hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] rounded-md ' +\n getRowActionClass(action, rowData)\n \"\n [disabled]=\"isRowActionDisabled(action, rowData) || false\"\n [attr.aria-label]=\"getRowActionLabel(action, rowData)\"\n >\n <svg\n [lucideIcon]=\"getIcon(getRowActionIconName(action, rowData))\"\n class=\"w-4 h-4\"\n ></svg>\n </button>\n </shk-tooltip>\n </div>\n }\n }\n </div>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n }\n </table>\n <ng-template #defaultTemplate let-rowData let-field=\"field\">\n {{ truncate(rowData[field], 30) }}\n </ng-template>\n </div>\n <div\n class=\"flex items-center justify-between gap-4 px-2 min-[1440px]:px-3 py-2 border-t _shk-border dark:[border-color:var(--shk-surface-100)]\"\n >\n <div class=\"text-xs _shk-text-surface-600 dark:[color:var(--shk-surface-400)]\">\n {{ pageReport() }}\n </div>\n <div\n class=\"flex items-center gap-0.5\"\n role=\"navigation\"\n >\n <button\n type=\"button\"\n (click)=\"goToFirst()\"\n [disabled]=\"isFirstPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevrons-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"prev()\"\n [disabled]=\"isFirstPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-left')\" class=\"w-4 h-4\"></svg>\n </button>\n <span\n class=\"px-2 text-xs font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n >{{ currentPage() }} / {{ totalPages() }}</span\n >\n <button\n type=\"button\"\n (click)=\"next()\"\n [disabled]=\"isLastPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevron-right')\" class=\"w-4 h-4\"></svg>\n </button>\n <button\n type=\"button\"\n (click)=\"goToLast()\"\n [disabled]=\"isLastPage()\"\n class=\"p-1 _shk-text-surface-500 hover:[color:var(--shk-surface-700)] dark:[color:var(--shk-surface-400)] dark:hover:[color:var(--shk-surface-200)] disabled:opacity-30 disabled:cursor-not-allowed rounded transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('chevrons-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n <div\n class=\"flex items-center gap-1.5 text-xs _shk-text-surface-600 dark:[color:var(--shk-surface-400)]\"\n >\n <label [for]=\"'shk-rows-per-page'\">{{ 'table.rows' | transloco }}</label>\n <select\n id=\"shk-rows-per-page\"\n [value]=\"rowsPerPageLocal()\"\n (change)=\"onRowsPerPageChange($any($event.target).value)\"\n class=\"border _shk-border dark:[border-color:var(--shk-surface-200)] rounded px-2 py-1 text-xs bg-white dark:[background-color:var(--shk-surface-100)] _shk-text-surface-700 dark:[color:var(--shk-surface-700)] focus:outline-none focus:[border-color:var(--shk-primary-500)]\"\n >\n @for (opt of rowsPerPageOptions(); track opt) {\n <option [value]=\"opt\">{{ opt }}</option>\n }\n </select>\n </div>\n </div>\n </div>\n</div>\n", styles: ["input:focus{outline:none}input[type=text]{font-weight:400!important}table th,table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}td button svg{width:1.125rem!important;height:1.125rem!important}td button:disabled{opacity:.4;cursor:not-allowed}.table-action-btn svg{width:1rem!important;height:1rem!important;min-width:1rem!important;min-height:1rem!important}.table-action-btn{padding:.5rem!important;min-width:2rem!important;min-height:2rem!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.table-scroll-wrapper{width:100%;overflow-x:auto;overflow-y:hidden;border-radius:inherit;-webkit-overflow-scrolling:touch;container-type:inline-size}.table-scroll-wrapper table{table-layout:fixed;width:100%}@container (max-width: 768px){.table-scroll-wrapper{scroll-snap-type:x mandatory}.table-scroll-wrapper table{table-layout:auto;width:100%}.table-scroll-wrapper th:not([data-shk-actions]),.table-scroll-wrapper td:not([data-shk-actions]){min-width:var(--shk-col-width, 140px);max-width:var(--shk-col-width, 140px)}.table-scroll-wrapper th,.table-scroll-wrapper td{scroll-snap-align:start}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
3063
3641
  }], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rowsPerPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPage", required: false }] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], showActionRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActionRow", required: false }] }], customTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "customTemplates", required: false }] }], headerActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerActions", required: false }] }], rowActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActions", required: false }] }], hasShadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasShadow", required: false }] }], defaultSortField: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultSortField", required: false }] }], defaultSortOrder: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultSortOrder", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], serverSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverSide", required: false }] }], totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }], activeFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeFilter", required: false }] }], refresh: [{ type: i0.Output, args: ["refresh"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], filterChange: [{ type: i0.Output, args: ["filterChange"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], filterClick: [{ type: i0.Output, args: ["filterClick"] }] } });
3064
3642
 
3065
- const ICONS$1 = {
3066
- house: LucideHouse,
3067
- home: LucideHome,
3068
- dashboard: LucideLayoutDashboard,
3069
- warehouse: LucideWarehouse,
3070
- building: LucideBuilding,
3071
- 'building-2': LucideBuilding2,
3072
- truck: LucideTruck,
3073
- users: LucideUsers,
3074
- user: LucideUser,
3075
- clients: LucideUsers,
3076
- box: LucideBox,
3077
- product: LucidePackage,
3078
- package: LucidePackage,
3079
- tags: LucideTags,
3080
- tag: LucideTag,
3081
- receipt: LucideReceipt,
3082
- orders: LucideReceipt,
3083
- bot: LucideBot,
3084
- settings: LucideSettings,
3085
- config: LucideSettings,
3086
- menu: LucideMenu,
3087
- 'line-chart': LucideLineChart,
3088
- 'bar-chart-3': LucideBarChart3,
3089
- star: LucideStar,
3090
- 'dollar-sign': LucideDollarSign,
3091
- percent: LucidePercent,
3092
- calculator: LucideCalculator,
3093
- search: LucideSearch,
3094
- 'shield-check': LucideShieldCheck,
3095
- 'user-cog': LucideUserCog,
3096
- 'chevrons-left': LucideChevronsLeft,
3097
- 'chevrons-right': LucideChevronsRight,
3098
- 'chevron-down': LucideChevronDown,
3099
- 'chevron-left': LucideChevronLeft,
3100
- 'chevron-right': LucideChevronRight,
3101
- 'log-out': LucideLogOut,
3102
- logout: LucideLogOut,
3103
- x: LucideX,
3104
- };
3105
- function getIcon$4(name) {
3106
- if (!name)
3107
- return null;
3108
- return ICONS$1[name] ?? LucideMenu;
3109
- }
3643
+ /**
3644
+ * Shirkasoft UI Components.
3645
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
3646
+ * Belongs to Shirkasoft.
3647
+ */
3648
+ /**
3649
+ * Collapsible vertical navigation sidebar with groups, icons, badges,
3650
+ * role-based filtering, responsive mobile drawer and tooltips.
3651
+ */
3110
3652
  class SidebarComponent {
3111
- constructor(platformId) {
3112
- this.platformId = platformId;
3653
+ constructor() {
3654
+ this.platformId = inject(PLATFORM_ID);
3655
+ /** Flat list of navigation items (used when no groups are provided). */
3113
3656
  this.items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
3657
+ /** Grouped navigation items; takes precedence over `items`. */
3114
3658
  this.groups = input([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
3659
+ /** Brand title shown at the top of the sidebar. */
3115
3660
  this.title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3661
+ /** Subtitle under the title. */
3116
3662
  this.subtitle = input('', ...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
3663
+ /** Brand logo image/url shown at the top. */
3117
3664
  this.logo = input('', ...(ngDevMode ? [{ debugName: "logo" }] : /* istanbul ignore next */ []));
3665
+ /** id of the item currently highlighted as active. */
3118
3666
  this.activeItemId = input('', ...(ngDevMode ? [{ debugName: "activeItemId" }] : /* istanbul ignore next */ []));
3667
+ /** Role used to filter items that declare `roles`. */
3119
3668
  this.userRole = input('', ...(ngDevMode ? [{ debugName: "userRole" }] : /* istanbul ignore next */ []));
3669
+ /** Whether the sidebar can collapse into a rail. */
3120
3670
  this.collapsible = input(true, ...(ngDevMode ? [{ debugName: "collapsible" }] : /* istanbul ignore next */ []));
3671
+ /** Shows the expand/collapse toggle button. */
3121
3672
  this.showToggle = input(true, ...(ngDevMode ? [{ debugName: "showToggle" }] : /* istanbul ignore next */ []));
3673
+ /** Enables the mobile drawer behaviour below lg breakpoint. */
3122
3674
  this.responsive = input(true, ...(ngDevMode ? [{ debugName: "responsive" }] : /* istanbul ignore next */ []));
3675
+ /** Keeps the sidebar fixed while the page scrolls. */
3123
3676
  this.fixed = input(false, ...(ngDevMode ? [{ debugName: "fixed" }] : /* istanbul ignore next */ []));
3677
+ /** Adds a drop shadow to the sidebar. */
3124
3678
  this.hasShadow = input(true, ...(ngDevMode ? [{ debugName: "hasShadow" }] : /* istanbul ignore next */ []));
3679
+ /** Width in expanded state. */
3125
3680
  this.expandedWidth = input('16rem', ...(ngDevMode ? [{ debugName: "expandedWidth" }] : /* istanbul ignore next */ []));
3681
+ /** Width when collapsed. */
3126
3682
  this.collapsedWidth = input('5rem', ...(ngDevMode ? [{ debugName: "collapsedWidth" }] : /* istanbul ignore next */ []));
3683
+ /** Group ids opened by default on load. */
3127
3684
  this.defaultExpanded = input([], ...(ngDevMode ? [{ debugName: "defaultExpanded" }] : /* istanbul ignore next */ []));
3685
+ /** Optional transform applied to item labels (e.g. i18n). */
3128
3686
  this.labelPipe = input((label) => label, ...(ngDevMode ? [{ debugName: "labelPipe" }] : /* istanbul ignore next */ []));
3687
+ /** Tooltip for the expand action. */
3129
3688
  this.expandTooltip = input('Expandir', ...(ngDevMode ? [{ debugName: "expandTooltip" }] : /* istanbul ignore next */ []));
3689
+ /** Tooltip for the collapse action. */
3130
3690
  this.collapseTooltip = input('Colapsar', ...(ngDevMode ? [{ debugName: "collapseTooltip" }] : /* istanbul ignore next */ []));
3691
+ /** Collapsed state, bindable with [(collapsed)]. */
3131
3692
  this.collapsed = model(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
3693
+ /** Mobile drawer open state, bindable with [(mobileOpen)]. */
3132
3694
  this.mobileOpen = model(false, ...(ngDevMode ? [{ debugName: "mobileOpen" }] : /* istanbul ignore next */ []));
3695
+ /** Emitted when a navigation item is clicked. */
3133
3696
  this.itemClick = output();
3697
+ /** Emitted with the new collapsed state after toggling. */
3134
3698
  this.toggle = output();
3135
- this.getIcon = getIcon$4;
3699
+ this.getIcon = (name) => getIcon(name, LucideMenu);
3700
+ /** Currently selected item id. */
3136
3701
  this.selectedId = signal('', ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
3702
+ /** Set of group ids currently expanded. */
3137
3703
  this.expandedGroups = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedGroups" }] : /* istanbul ignore next */ []));
3138
- this.isBrowser = isPlatformBrowser(this.platformId);
3704
+ this.isBrowser = false;
3705
+ /** Current sidebar width depending on the collapsed state. */
3139
3706
  this.width = computed(() => this.collapsed() ? this.collapsedWidth() : this.expandedWidth(), ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
3707
+ /** Navigation items after group flattening and role filtering. */
3140
3708
  this.navItems = computed(() => {
3141
3709
  const role = this.userRole();
3142
- const base = this.groups().length
3143
- ? this.groups().flatMap((g) => g.items)
3144
- : this.items();
3710
+ const base = this.groups().length ? this.groups().flatMap((g) => g.items) : this.items();
3145
3711
  return base.filter((item) => {
3146
3712
  if (!item.roles || item.roles.length === 0)
3147
3713
  return true;
3148
3714
  return role ? item.roles.includes(role) : false;
3149
3715
  });
3150
3716
  }, ...(ngDevMode ? [{ debugName: "navItems" }] : /* istanbul ignore next */ []));
3717
+ if (isPlatformBrowser(this.platformId)) {
3718
+ this.isBrowser = isPlatformBrowser(this.platformId);
3719
+ }
3151
3720
  effect(() => {
3152
3721
  this.selectedId.set(this.activeItemId());
3153
3722
  });
@@ -3177,23 +3746,29 @@ class SidebarComponent {
3177
3746
  });
3178
3747
  });
3179
3748
  }
3749
+ /** Closes the mobile drawer when the window grows below lg. */
3180
3750
  onResize(_event) {
3181
3751
  if (this.responsive() && this.isBrowser && window.innerWidth < 1024) {
3182
3752
  this.mobileOpen.set(false);
3183
3753
  }
3184
3754
  }
3755
+ /** Applies the `labelPipe` transform to an item label. */
3185
3756
  label(item) {
3186
3757
  return this.labelPipe()(item.label);
3187
3758
  }
3759
+ /** Whether the item matches the selected id. */
3188
3760
  isItemActive(item) {
3189
3761
  return !!item.id && item.id === this.selectedId();
3190
3762
  }
3763
+ /** Whether one of the item's children is currently active. */
3191
3764
  isGroupActive(item) {
3192
3765
  return item.children?.some((c) => c.id === this.selectedId()) ?? false;
3193
3766
  }
3767
+ /** Whether a group is expanded. */
3194
3768
  isGroupExpanded(item) {
3195
3769
  return this.expandedGroups().has(item.id);
3196
3770
  }
3771
+ /** Toggles the expanded state of a group. */
3197
3772
  toggleGroup(item) {
3198
3773
  this.expandedGroups.update((set) => {
3199
3774
  const next = new Set(set);
@@ -3206,6 +3781,7 @@ class SidebarComponent {
3206
3781
  return next;
3207
3782
  });
3208
3783
  }
3784
+ /** Selects an item, emits `itemClick` and closes the mobile drawer when needed. */
3209
3785
  onItemClick(item) {
3210
3786
  if (item.disabled)
3211
3787
  return;
@@ -3215,6 +3791,7 @@ class SidebarComponent {
3215
3791
  this.mobileOpen.set(false);
3216
3792
  }
3217
3793
  }
3794
+ /** Toggles collapse (desktop) or the mobile drawer (below lg). */
3218
3795
  onToggle() {
3219
3796
  if (this.responsive() && this.isBrowser && window.innerWidth < 1024) {
3220
3797
  this.mobileOpen.update((v) => !v);
@@ -3224,93 +3801,80 @@ class SidebarComponent {
3224
3801
  this.toggle.emit(this.collapsed());
3225
3802
  }
3226
3803
  }
3227
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: SidebarComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); }
3804
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: SidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3228
3805
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: SidebarComponent, isStandalone: true, selector: "shk-sidebar", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: true, isRequired: false, transformFunction: null }, activeItemId: { classPropertyName: "activeItemId", publicName: "activeItemId", isSignal: true, isRequired: false, transformFunction: null }, userRole: { classPropertyName: "userRole", publicName: "userRole", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, showToggle: { classPropertyName: "showToggle", publicName: "showToggle", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, fixed: { classPropertyName: "fixed", publicName: "fixed", isSignal: true, isRequired: false, transformFunction: null }, hasShadow: { classPropertyName: "hasShadow", publicName: "hasShadow", isSignal: true, isRequired: false, transformFunction: null }, expandedWidth: { classPropertyName: "expandedWidth", publicName: "expandedWidth", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, defaultExpanded: { classPropertyName: "defaultExpanded", publicName: "defaultExpanded", isSignal: true, isRequired: false, transformFunction: null }, labelPipe: { classPropertyName: "labelPipe", publicName: "labelPipe", isSignal: true, isRequired: false, transformFunction: null }, expandTooltip: { classPropertyName: "expandTooltip", publicName: "expandTooltip", isSignal: true, isRequired: false, transformFunction: null }, collapseTooltip: { classPropertyName: "collapseTooltip", publicName: "collapseTooltip", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, mobileOpen: { classPropertyName: "mobileOpen", publicName: "mobileOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", mobileOpen: "mobileOpenChange", itemClick: "itemClick", toggle: "toggle" }, host: { listeners: { "window:resize": "onResize($event)" } }, ngImport: i0, template: "@if (responsive() && mobileOpen()) {\n <!-- Mobile overlay backdrop (sibling of aside: must NOT be inside the\n transformed aside, otherwise it becomes its containing block and\n only covers the sidebar, also blocking nav clicks on mobile) -->\n <div\n class=\"fixed inset-0 bg-black/50 z-30 lg:hidden\"\n (click)=\"mobileOpen.set(false)\"\n ></div>\n}\n\n<aside\n [class.fixed]=\"fixed()\"\n [class.relative]=\"!fixed()\"\n [class.h-screen]=\"fixed()\"\n [class.h-full]=\"!fixed()\"\n class=\"top-0 left-0 flex flex-col shrink-0 sidebar-transition sidebar-mobile-transform z-40 _shk-bg-white dark:[background-color:var(--shk-surface-50)] border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [class.shadow-sm]=\"hasShadow()\"\n [style.width]=\"width()\"\n [style.transform]=\"responsive() ? (mobileOpen() ? 'translateX(0)' : 'translateX(-100%)') : null\"\n>\n <!-- Logo / Brand -->\n @if (logo() || title() || subtitle()) {\n <div\n class=\"flex items-center h-20 px-4 border-b _shk-border dark:[border-color:var(--shk-surface-100)] shrink-0\"\n >\n <div class=\"flex items-center gap-3 overflow-hidden\">\n @if (logo()) {\n <img [src]=\"logo()\" alt=\"Logo\" class=\"h-16 w-16 shrink-0 object-contain\" />\n }\n @if (!collapsed() && (title() || subtitle())) {\n <div class=\"flex flex-col leading-tight whitespace-nowrap\">\n @if (title()) {\n <span class=\"text-lg font-bold _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">\n {{ title() }}\n </span>\n }\n @if (subtitle()) {\n <span class=\"text-sm _shk-text-surface-500 dark:[color:var(--shk-surface-500)]\">\n {{ subtitle() }}\n </span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Toggle button -->\n @if (showToggle() && collapsible()) {\n <button\n type=\"button\"\n (click)=\"onToggle()\"\n class=\"group absolute -right-3 top-20 z-50 w-6 h-6 rounded-full bg-white dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] shadow-md max-lg:hidden flex items-center justify-center _shk-text-surface-600 dark:[color:var(--shk-surface-600)] hover:[background-color:var(--shk-primary-50)] dark:hover:[background-color:var(--shk-surface-200)] hover:[color:var(--shk-primary-600)] dark:hover:[color:var(--shk-primary-400)] transition-colors\"\n >\n @if (collapsed()) {\n <svg [lucideIcon]=\"getIcon('chevron-right')\" class=\"w-3 h-3\"></svg>\n } @else {\n <svg [lucideIcon]=\"getIcon('chevron-left')\" class=\"w-3 h-3\"></svg>\n }\n <span\n class=\"absolute left-full ml-1 top-1/2 -translate-y-1/2 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\"\n >\n {{ collapsed() ? expandTooltip() : collapseTooltip() }}\n </span>\n </button>\n }\n\n <ng-content select=\"[sidebarHeader]\"></ng-content>\n\n <!-- Custom content between header and nav -->\n <ng-content select=\"[sidebarContent]\"></ng-content>\n\n <!-- Navigation -->\n <nav class=\"flex-1 sidebar-scroll _shk-scrollbar py-4 px-3\">\n <ul class=\"space-y-1\">\n @for (item of navItems(); track item.id) {\n @if (item.separator) {\n <li class=\"my-2 border-t _shk-border dark:[border-color:var(--shk-surface-200)]\"></li>\n }\n\n @if (item.children?.length) {\n <!-- Group -->\n <li>\n @if (collapsed()) {\n <shk-tooltip [text]=\"label(item)\" position=\"right\" mode=\"fixed\">\n <div\n class=\"flex items-center justify-center px-3 py-2.5 rounded-lg transition-all duration-200\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class._shk-text-surface-500]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n </div>\n </shk-tooltip>\n } @else {\n <button\n type=\"button\"\n (click)=\"toggleGroup(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group relative\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class.shadow-md]=\"isGroupActive(item)\"\n [class._shk-text-surface-700]=\"!isGroupActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isGroupActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isGroupActive(item)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item) }}</span>\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 transition-transform duration-200 shrink-0\"\n [class.rotate-180]=\"isGroupExpanded(item)\"\n ></svg>\n </button>\n\n @if (isGroupExpanded(item)) {\n <ul class=\"ml-3 mt-1 space-y-1 border-l-2 _shk-border dark:[border-color:var(--shk-surface-200)] pl-2\">\n @for (child of item.children; track child.id) {\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(child)\"\n class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary-soft]=\"isItemActive(child)\"\n [class._shk-bg-primary-soft-dark]=\"isItemActive(child)\"\n [class._shk-text-primary-700]=\"isItemActive(child)\"\n [class.dark:[color:var(--shk-primary-300)]]=\"isItemActive(child)\"\n [class._shk-text-surface-600]=\"!isItemActive(child)\"\n [class.dark:[color:var(--shk-surface-400)]]=\"!isItemActive(child)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(child)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(child)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(child)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(child)\"\n >\n @if (child.icon) {\n <svg [lucideIcon]=\"getIcon(child.icon)\" class=\"w-4 h-4 shrink-0\"></svg>\n }\n <span class=\"whitespace-nowrap overflow-hidden\">{{ label(child) }}</span>\n @if (isItemActive(child)) {\n <span class=\"w-1.5 h-1.5 rounded-full _shk-bg-primary shrink-0 ml-auto\"></span>\n }\n </button>\n </li>\n }\n </ul>\n }\n }\n </li>\n } @else {\n <!-- Standalone item -->\n <li>\n @if (collapsed()) {\n <shk-tooltip [text]=\"label(item)\" position=\"right\" mode=\"fixed\">\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center justify-center px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(item)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n </button>\n </shk-tooltip>\n } @else {\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(item)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item) }}</span>\n @if (item.badge !== undefined) {\n <span\n class=\"text-xs font-bold px-2 py-0.5 rounded-full _shk-bg-primary _shk-text-white shrink-0\"\n [class.hidden]=\"collapsed()\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isItemActive(item)) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-white shrink-0\"></span>\n }\n </button>\n }\n </li>\n }\n }\n </ul>\n </nav>\n\n <!-- Footer -->\n <div class=\"shrink-0 p-2 [&:empty]:hidden\">\n <ng-content select=\"[sidebarFooter]\"></ng-content>\n </div>\n</aside>\n", styles: [":host{display:block}.sidebar-transition{transition:width .3s cubic-bezier(.4,0,.2,1),transform .3s cubic-bezier(.4,0,.2,1)}.sidebar-scroll{overflow-y:auto;overflow-x:visible}@media(min-width:1024px){.sidebar-mobile-transform{transform:none!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: TooltipComponent, selector: "shk-tooltip", inputs: ["text", "position", "mode", "offset"] }] }); }
3229
3806
  }
3230
3807
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: SidebarComponent, decorators: [{
3231
3808
  type: Component,
3232
3809
  args: [{ selector: 'shk-sidebar', standalone: true, imports: [CommonModule, LucideDynamicIcon, TooltipComponent], template: "@if (responsive() && mobileOpen()) {\n <!-- Mobile overlay backdrop (sibling of aside: must NOT be inside the\n transformed aside, otherwise it becomes its containing block and\n only covers the sidebar, also blocking nav clicks on mobile) -->\n <div\n class=\"fixed inset-0 bg-black/50 z-30 lg:hidden\"\n (click)=\"mobileOpen.set(false)\"\n ></div>\n}\n\n<aside\n [class.fixed]=\"fixed()\"\n [class.relative]=\"!fixed()\"\n [class.h-screen]=\"fixed()\"\n [class.h-full]=\"!fixed()\"\n class=\"top-0 left-0 flex flex-col shrink-0 sidebar-transition sidebar-mobile-transform z-40 _shk-bg-white dark:[background-color:var(--shk-surface-50)] border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [class.shadow-sm]=\"hasShadow()\"\n [style.width]=\"width()\"\n [style.transform]=\"responsive() ? (mobileOpen() ? 'translateX(0)' : 'translateX(-100%)') : null\"\n>\n <!-- Logo / Brand -->\n @if (logo() || title() || subtitle()) {\n <div\n class=\"flex items-center h-20 px-4 border-b _shk-border dark:[border-color:var(--shk-surface-100)] shrink-0\"\n >\n <div class=\"flex items-center gap-3 overflow-hidden\">\n @if (logo()) {\n <img [src]=\"logo()\" alt=\"Logo\" class=\"h-16 w-16 shrink-0 object-contain\" />\n }\n @if (!collapsed() && (title() || subtitle())) {\n <div class=\"flex flex-col leading-tight whitespace-nowrap\">\n @if (title()) {\n <span class=\"text-lg font-bold _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">\n {{ title() }}\n </span>\n }\n @if (subtitle()) {\n <span class=\"text-sm _shk-text-surface-500 dark:[color:var(--shk-surface-500)]\">\n {{ subtitle() }}\n </span>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- Toggle button -->\n @if (showToggle() && collapsible()) {\n <button\n type=\"button\"\n (click)=\"onToggle()\"\n class=\"group absolute -right-3 top-20 z-50 w-6 h-6 rounded-full bg-white dark:[background-color:var(--shk-surface-100)] border _shk-border dark:[border-color:var(--shk-surface-200)] shadow-md max-lg:hidden flex items-center justify-center _shk-text-surface-600 dark:[color:var(--shk-surface-600)] hover:[background-color:var(--shk-primary-50)] dark:hover:[background-color:var(--shk-surface-200)] hover:[color:var(--shk-primary-600)] dark:hover:[color:var(--shk-primary-400)] transition-colors\"\n >\n @if (collapsed()) {\n <svg [lucideIcon]=\"getIcon('chevron-right')\" class=\"w-3 h-3\"></svg>\n } @else {\n <svg [lucideIcon]=\"getIcon('chevron-left')\" class=\"w-3 h-3\"></svg>\n }\n <span\n class=\"absolute left-full ml-1 top-1/2 -translate-y-1/2 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\"\n >\n {{ collapsed() ? expandTooltip() : collapseTooltip() }}\n </span>\n </button>\n }\n\n <ng-content select=\"[sidebarHeader]\"></ng-content>\n\n <!-- Custom content between header and nav -->\n <ng-content select=\"[sidebarContent]\"></ng-content>\n\n <!-- Navigation -->\n <nav class=\"flex-1 sidebar-scroll _shk-scrollbar py-4 px-3\">\n <ul class=\"space-y-1\">\n @for (item of navItems(); track item.id) {\n @if (item.separator) {\n <li class=\"my-2 border-t _shk-border dark:[border-color:var(--shk-surface-200)]\"></li>\n }\n\n @if (item.children?.length) {\n <!-- Group -->\n <li>\n @if (collapsed()) {\n <shk-tooltip [text]=\"label(item)\" position=\"right\" mode=\"fixed\">\n <div\n class=\"flex items-center justify-center px-3 py-2.5 rounded-lg transition-all duration-200\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class._shk-text-surface-500]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n </div>\n </shk-tooltip>\n } @else {\n <button\n type=\"button\"\n (click)=\"toggleGroup(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group relative\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class.shadow-md]=\"isGroupActive(item)\"\n [class._shk-text-surface-700]=\"!isGroupActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isGroupActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isGroupActive(item)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item) }}</span>\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 transition-transform duration-200 shrink-0\"\n [class.rotate-180]=\"isGroupExpanded(item)\"\n ></svg>\n </button>\n\n @if (isGroupExpanded(item)) {\n <ul class=\"ml-3 mt-1 space-y-1 border-l-2 _shk-border dark:[border-color:var(--shk-surface-200)] pl-2\">\n @for (child of item.children; track child.id) {\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(child)\"\n class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary-soft]=\"isItemActive(child)\"\n [class._shk-bg-primary-soft-dark]=\"isItemActive(child)\"\n [class._shk-text-primary-700]=\"isItemActive(child)\"\n [class.dark:[color:var(--shk-primary-300)]]=\"isItemActive(child)\"\n [class._shk-text-surface-600]=\"!isItemActive(child)\"\n [class.dark:[color:var(--shk-surface-400)]]=\"!isItemActive(child)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(child)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(child)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(child)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(child)\"\n >\n @if (child.icon) {\n <svg [lucideIcon]=\"getIcon(child.icon)\" class=\"w-4 h-4 shrink-0\"></svg>\n }\n <span class=\"whitespace-nowrap overflow-hidden\">{{ label(child) }}</span>\n @if (isItemActive(child)) {\n <span class=\"w-1.5 h-1.5 rounded-full _shk-bg-primary shrink-0 ml-auto\"></span>\n }\n </button>\n </li>\n }\n </ul>\n }\n }\n </li>\n } @else {\n <!-- Standalone item -->\n <li>\n @if (collapsed()) {\n <shk-tooltip [text]=\"label(item)\" position=\"right\" mode=\"fixed\">\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center justify-center px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(item)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n </button>\n </shk-tooltip>\n } @else {\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(item)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item) }}</span>\n @if (item.badge !== undefined) {\n <span\n class=\"text-xs font-bold px-2 py-0.5 rounded-full _shk-bg-primary _shk-text-white shrink-0\"\n [class.hidden]=\"collapsed()\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isItemActive(item)) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-white shrink-0\"></span>\n }\n </button>\n }\n </li>\n }\n }\n </ul>\n </nav>\n\n <!-- Footer -->\n <div class=\"shrink-0 p-2 [&:empty]:hidden\">\n <ng-content select=\"[sidebarFooter]\"></ng-content>\n </div>\n</aside>\n", styles: [":host{display:block}.sidebar-transition{transition:width .3s cubic-bezier(.4,0,.2,1),transform .3s cubic-bezier(.4,0,.2,1)}.sidebar-scroll{overflow-y:auto;overflow-x:visible}@media(min-width:1024px){.sidebar-mobile-transform{transform:none!important}}\n"] }]
3233
- }], ctorParameters: () => [{ type: Object, decorators: [{
3234
- type: Inject,
3235
- args: [PLATFORM_ID]
3236
- }] }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], logo: [{ type: i0.Input, args: [{ isSignal: true, alias: "logo", required: false }] }], activeItemId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeItemId", required: false }] }], userRole: [{ type: i0.Input, args: [{ isSignal: true, alias: "userRole", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], showToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToggle", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], fixed: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixed", required: false }] }], hasShadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasShadow", required: false }] }], expandedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedWidth", required: false }] }], collapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedWidth", required: false }] }], defaultExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultExpanded", required: false }] }], labelPipe: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPipe", required: false }] }], expandTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandTooltip", required: false }] }], collapseTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseTooltip", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], mobileOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileOpen", required: false }] }, { type: i0.Output, args: ["mobileOpenChange"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], toggle: [{ type: i0.Output, args: ["toggle"] }], onResize: [{
3810
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], logo: [{ type: i0.Input, args: [{ isSignal: true, alias: "logo", required: false }] }], activeItemId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeItemId", required: false }] }], userRole: [{ type: i0.Input, args: [{ isSignal: true, alias: "userRole", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], showToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToggle", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], fixed: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixed", required: false }] }], hasShadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasShadow", required: false }] }], expandedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedWidth", required: false }] }], collapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedWidth", required: false }] }], defaultExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultExpanded", required: false }] }], labelPipe: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPipe", required: false }] }], expandTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandTooltip", required: false }] }], collapseTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseTooltip", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], mobileOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileOpen", required: false }] }, { type: i0.Output, args: ["mobileOpenChange"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], toggle: [{ type: i0.Output, args: ["toggle"] }], onResize: [{
3237
3811
  type: HostListener,
3238
3812
  args: ['window:resize', ['$event']]
3239
3813
  }] } });
3240
3814
 
3241
- const ICONS = {
3242
- shield: LucideShield,
3243
- building: LucideBuilding,
3244
- 'building-2': LucideBuilding2,
3245
- 'layer-group': LucideLayers,
3246
- 'puzzle-piece': LucidePuzzle,
3247
- 'chart-pie': LucideChartPie,
3248
- 'chart-line': LucideChartLine,
3249
- 'credit-card': LucideCreditCard,
3250
- 'id-card': LucideIdCard,
3251
- briefcase: LucideBriefcase,
3252
- truck: LucideTruck,
3253
- tags: LucideTags,
3254
- tag: LucideTag,
3255
- package: LucidePackage,
3256
- box: LucideBox,
3257
- 'dollar-sign': LucideDollarSign,
3258
- wallet: LucideWallet,
3259
- percent: LucidePercent,
3260
- sliders: LucideSliders,
3261
- search: LucideSearch,
3262
- calendar: LucideCalendar,
3263
- 'calendar-check': LucideCalendarCheck,
3264
- users: LucideUsers,
3265
- 'user-cog': LucideUserCog,
3266
- globe: LucideGlobe,
3267
- 'log-out': LucideLogOut,
3268
- logout: LucideLogOut,
3269
- 'panel-right': LucidePanelRight,
3270
- home: LucideHome,
3271
- receipt: LucideReceipt,
3272
- bot: LucideBot,
3273
- star: LucideStar,
3274
- 'file-invoice-dollar': LucideBanknote,
3275
- 'file-contract': LucideFileText,
3276
- user: LucideUser,
3277
- menu: LucideMenu,
3278
- 'chevron-down': LucideChevronDown,
3279
- x: LucideX,
3280
- };
3281
- function getIcon$3(name) {
3282
- if (!name)
3283
- return null;
3284
- return ICONS[name] ?? LucideMenu;
3285
- }
3815
+ /**
3816
+ * Shirkasoft UI Components.
3817
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
3818
+ * Belongs to Shirkasoft.
3819
+ */
3820
+ /**
3821
+ * Narrow "rail" sidebar that expands into sections. Supports sections, roles,
3822
+ * language switching, collapse/expand and responsive tooltips.
3823
+ */
3286
3824
  class RailSidebarComponent {
3287
3825
  constructor() {
3826
+ /** Sections presented as expandable areas of the rail. */
3288
3827
  this.sections = input([], ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
3828
+ /** Current location url used to detect the active section/item. */
3289
3829
  this.activeUrl = input('', ...(ngDevMode ? [{ debugName: "activeUrl" }] : /* istanbul ignore next */ []));
3830
+ /** Key of the active section, bindable with [(activeSectionKey)]. */
3290
3831
  this.activeSectionKey = model('', ...(ngDevMode ? [{ debugName: "activeSectionKey" }] : /* istanbul ignore next */ []));
3832
+ /** Collapsed (rail only) state, bindable with [(collapsed)]. */
3291
3833
  this.collapsed = model(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
3834
+ /** Active language code. */
3292
3835
  this.activeLang = input('es', ...(ngDevMode ? [{ debugName: "activeLang" }] : /* istanbul ignore next */ []));
3836
+ /** Languages available in the language switcher. */
3293
3837
  this.langs = input(['es', 'en'], ...(ngDevMode ? [{ debugName: "langs" }] : /* istanbul ignore next */ []));
3838
+ /** Brand logo image/url. */
3294
3839
  this.logo = input('', ...(ngDevMode ? [{ debugName: "logo" }] : /* istanbul ignore next */ []));
3840
+ /** Application name shown when expanded. */
3295
3841
  this.appName = input('', ...(ngDevMode ? [{ debugName: "appName" }] : /* istanbul ignore next */ []));
3842
+ /** Role used to filter items that declare `roles`. */
3296
3843
  this.userRole = input('', ...(ngDevMode ? [{ debugName: "userRole" }] : /* istanbul ignore next */ []));
3844
+ /** Optional transform applied to item labels (e.g. i18n). */
3297
3845
  this.labelPipe = input((label) => label, ...(ngDevMode ? [{ debugName: "labelPipe" }] : /* istanbul ignore next */ []));
3846
+ /** Width of the collapsed rail. */
3298
3847
  this.railWidth = input('4.5rem', ...(ngDevMode ? [{ debugName: "railWidth" }] : /* istanbul ignore next */ []));
3848
+ /** Width when a section is expanded. */
3299
3849
  this.expandedWidth = input('16.5rem', ...(ngDevMode ? [{ debugName: "expandedWidth" }] : /* istanbul ignore next */ []));
3850
+ /** Tooltip for the workspace/home action. */
3300
3851
  this.workspaceTooltip = input('Inicio', ...(ngDevMode ? [{ debugName: "workspaceTooltip" }] : /* istanbul ignore next */ []));
3852
+ /** Tooltip for the logout action. */
3301
3853
  this.logoutTooltip = input('Cerrar sesión', ...(ngDevMode ? [{ debugName: "logoutTooltip" }] : /* istanbul ignore next */ []));
3854
+ /** Tooltip for collapsing the sidebar. */
3302
3855
  this.collapseTooltip = input('Colapsar', ...(ngDevMode ? [{ debugName: "collapseTooltip" }] : /* istanbul ignore next */ []));
3856
+ /** Tooltip for expanding the sidebar. */
3303
3857
  this.expandTooltip = input('Expandir', ...(ngDevMode ? [{ debugName: "expandTooltip" }] : /* istanbul ignore next */ []));
3858
+ /** Emitted when a section is selected, with its key and optional route. */
3304
3859
  this.sectionSelected = output();
3860
+ /** Emitted when a navigation item is clicked. */
3305
3861
  this.itemClick = output();
3862
+ /** Emitted with the next language code when the user switches language. */
3306
3863
  this.setLang = output();
3864
+ /** Emitted when the user requests to log out. */
3307
3865
  this.logoutRequested = output();
3866
+ /** Emitted with the new collapsed state after toggling. */
3308
3867
  this.toggleSidebar = output();
3868
+ /** Emitted when the workspace/home entry is clicked. */
3309
3869
  this.workspaceClick = output();
3310
- this.getIcon = getIcon$3;
3870
+ this.getIcon = (name) => getIcon(name, LucideMenu);
3871
+ /** Set of group ids currently expanded. */
3311
3872
  this.expandedGroups = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedGroups" }] : /* istanbul ignore next */ []));
3873
+ /** Current sidebar width depending on the collapsed state. */
3312
3874
  this.width = computed(() => this.collapsed() ? this.railWidth() : this.expandedWidth(), ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
3875
+ /** The section currently selected. */
3313
3876
  this.activeSection = computed(() => this.sections().find((s) => s.key === this.activeSectionKey()) ?? null, ...(ngDevMode ? [{ debugName: "activeSection" }] : /* istanbul ignore next */ []));
3877
+ /** Items of the active section, filtered by role. */
3314
3878
  this.navItems = computed(() => {
3315
3879
  const role = this.userRole();
3316
3880
  const items = this.activeSection()?.items ?? [];
@@ -3335,23 +3899,29 @@ class RailSidebarComponent {
3335
3899
  }
3336
3900
  });
3337
3901
  }
3902
+ /** Applies the `labelPipe` transform to a label. */
3338
3903
  label(label) {
3339
3904
  return this.labelPipe()(label);
3340
3905
  }
3906
+ /** Whether the section matches the active key. */
3341
3907
  isSectionActive(section) {
3342
3908
  return section.key === this.activeSectionKey();
3343
3909
  }
3910
+ /** Whether the item is the best route match for the active url. */
3344
3911
  isItemActive(item) {
3345
3912
  if (!item.routerLink)
3346
3913
  return false;
3347
3914
  return this.bestMatchItem() === item;
3348
3915
  }
3916
+ /** Whether one of the item's children matches the active url. */
3349
3917
  isGroupActive(item) {
3350
3918
  return (item.children?.some((c) => c.routerLink && this.matchesUrl(c.routerLink, this.activeUrl())) ?? false);
3351
3919
  }
3920
+ /** Exact or prefix match between a router link and the active url. */
3352
3921
  matchesUrl(routerLink, url) {
3353
3922
  return url === routerLink || url.startsWith(routerLink + '/');
3354
3923
  }
3924
+ /** Longest (most specific) item whose route matches the active url. */
3355
3925
  bestMatchItem() {
3356
3926
  const url = this.activeUrl();
3357
3927
  let best = null;
@@ -3371,9 +3941,11 @@ class RailSidebarComponent {
3371
3941
  }
3372
3942
  return best;
3373
3943
  }
3944
+ /** Whether a group is expanded. */
3374
3945
  isGroupExpanded(item) {
3375
3946
  return this.expandedGroups().has(item.id);
3376
3947
  }
3948
+ /** Toggles the expanded state of a group. */
3377
3949
  toggleGroup(item) {
3378
3950
  this.expandedGroups.update((set) => {
3379
3951
  const next = new Set(set);
@@ -3386,6 +3958,7 @@ class RailSidebarComponent {
3386
3958
  return next;
3387
3959
  });
3388
3960
  }
3961
+ /** Selects a section, emits `sectionSelected` and expands the rail when collapsed. */
3389
3962
  onSectionClick(section) {
3390
3963
  this.activeSectionKey.set(section.key);
3391
3964
  this.sectionSelected.emit({ key: section.key, routerLink: section.routerLink });
@@ -3394,11 +3967,13 @@ class RailSidebarComponent {
3394
3967
  this.toggleSidebar.emit(false);
3395
3968
  }
3396
3969
  }
3970
+ /** Emits `itemClick` for a navigation item (skipping disabled items). */
3397
3971
  onItemClick(item) {
3398
3972
  if (item.disabled)
3399
3973
  return;
3400
3974
  this.itemClick.emit(item);
3401
3975
  }
3976
+ /** Emits `workspaceClick` and expands the sidebar if it was collapsed. */
3402
3977
  onWorkspace() {
3403
3978
  this.workspaceClick.emit();
3404
3979
  if (this.collapsed()) {
@@ -3406,6 +3981,7 @@ class RailSidebarComponent {
3406
3981
  this.toggleSidebar.emit(false);
3407
3982
  }
3408
3983
  }
3984
+ /** Cycles to the next language and emits `setLang`. */
3409
3985
  onToggleLang() {
3410
3986
  const langs = this.langs();
3411
3987
  if (langs.length === 0)
@@ -3414,26 +3990,43 @@ class RailSidebarComponent {
3414
3990
  const next = langs[(idx + 1) % langs.length] ?? langs[0];
3415
3991
  this.setLang.emit(next);
3416
3992
  }
3993
+ /** Toggles collapse and emits `toggleSidebar`. */
3417
3994
  onToggleSidebar() {
3418
3995
  this.collapsed.update((v) => !v);
3419
3996
  this.toggleSidebar.emit(this.collapsed());
3420
3997
  }
3421
3998
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: RailSidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3422
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: RailSidebarComponent, isStandalone: true, selector: "shk-rail-sidebar", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, activeUrl: { classPropertyName: "activeUrl", publicName: "activeUrl", isSignal: true, isRequired: false, transformFunction: null }, activeSectionKey: { classPropertyName: "activeSectionKey", publicName: "activeSectionKey", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeLang: { classPropertyName: "activeLang", publicName: "activeLang", isSignal: true, isRequired: false, transformFunction: null }, langs: { classPropertyName: "langs", publicName: "langs", isSignal: true, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: true, isRequired: false, transformFunction: null }, appName: { classPropertyName: "appName", publicName: "appName", isSignal: true, isRequired: false, transformFunction: null }, userRole: { classPropertyName: "userRole", publicName: "userRole", isSignal: true, isRequired: false, transformFunction: null }, labelPipe: { classPropertyName: "labelPipe", publicName: "labelPipe", isSignal: true, isRequired: false, transformFunction: null }, railWidth: { classPropertyName: "railWidth", publicName: "railWidth", isSignal: true, isRequired: false, transformFunction: null }, expandedWidth: { classPropertyName: "expandedWidth", publicName: "expandedWidth", isSignal: true, isRequired: false, transformFunction: null }, workspaceTooltip: { classPropertyName: "workspaceTooltip", publicName: "workspaceTooltip", isSignal: true, isRequired: false, transformFunction: null }, logoutTooltip: { classPropertyName: "logoutTooltip", publicName: "logoutTooltip", isSignal: true, isRequired: false, transformFunction: null }, collapseTooltip: { classPropertyName: "collapseTooltip", publicName: "collapseTooltip", isSignal: true, isRequired: false, transformFunction: null }, expandTooltip: { classPropertyName: "expandTooltip", publicName: "expandTooltip", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeSectionKey: "activeSectionKeyChange", collapsed: "collapsedChange", sectionSelected: "sectionSelected", itemClick: "itemClick", setLang: "setLang", logoutRequested: "logoutRequested", toggleSidebar: "toggleSidebar", workspaceClick: "workspaceClick" }, ngImport: i0, template: "<aside\n class=\"h-full flex overflow-hidden shrink-0 _shk-bg-white dark:[background-color:var(--shk-surface-50)] border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"width()\"\n>\n <!-- Rail: vertical icon column -->\n <div\n class=\"h-full flex flex-col items-center shrink-0 px-[0.3rem] py-2 gap-1 border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"railWidth()\"\n >\n <!-- Workspace / logo -->\n <div class=\"flex items-center justify-center w-full\">\n <button\n type=\"button\"\n (click)=\"onWorkspace()\"\n [class.is-collapsed]=\"collapsed()\"\n class=\"rail-workspace w-[3.2rem] h-[3.2rem] rounded-xl _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] border-none grid place-items-center p-0 relative transition-all\"\n [title]=\"workspaceTooltip()\"\n >\n @if (logo()) {\n <img [src]=\"logo()\" alt=\"Logo\" class=\"rail-app-icon w-8 h-8 object-contain\" />\n } @else {\n <svg [lucideIcon]=\"getIcon('menu')\" class=\"rail-app-icon w-6 h-6 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n @if (collapsed()) {\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"rail-toggle-icon w-5 h-5 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n </button>\n </div>\n\n <!-- Section actions -->\n <div class=\"rail-actions flex-1 flex flex-col items-center gap-1 pt-1\">\n @for (section of sections(); track section.key) {\n <shk-tooltip [text]=\"label(section.label)\" position=\"right\" mode=\"fixed\">\n <button\n type=\"button\"\n (click)=\"onSectionClick(section)\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [class.is-active]=\"isSectionActive(section)\"\n [attr.aria-label]=\"label(section.label)\"\n >\n <svg [lucideIcon]=\"getIcon(section.icon)\" class=\"w-5 h-5\"></svg>\n </button>\n </shk-tooltip>\n }\n </div>\n\n <!-- Bottom: language + logout -->\n <div class=\"rail-bottom mt-auto flex flex-col items-center gap-1\">\n <button\n type=\"button\"\n (click)=\"onToggleLang()\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"activeLang()\"\n >\n <div class=\"flex flex-col items-center gap-[0.15rem]\">\n <svg [lucideIcon]=\"getIcon('globe')\" class=\"w-5 h-5\"></svg>\n <span class=\"rail-lang-code text-[0.5rem] font-bold tracking-[0.05em] leading-none\">{{ activeLang() | uppercase }}</span>\n </div>\n </button>\n\n <button\n type=\"button\"\n (click)=\"logoutRequested.emit()\"\n class=\"rail-icon rail-logout w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"logoutTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('log-out')\" class=\"w-5 h-5\"></svg>\n </button>\n </div>\n </div>\n\n <!-- Nav: section panel (hidden when collapsed) -->\n @if (!collapsed()) {\n <div class=\"flex-1 min-w-0 flex flex-col _shk-bg-white dark:[background-color:var(--shk-surface-50)]\">\n <!-- Brand header -->\n <div class=\"flex items-center justify-between gap-2 px-3 py-4 border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <span class=\"nav-app text-base font-bold whitespace-nowrap overflow-hidden text-ellipsis _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">\n {{ appName() }}\n </span>\n <button\n type=\"button\"\n (click)=\"onToggleSidebar()\"\n class=\"nav-toggle w-[1.8rem] h-[1.8rem] rounded-lg border-none bg-transparent grid place-items-center cursor-pointer shrink-0 transition-colors _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n [title]=\"collapseTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n\n <!-- Menu -->\n <nav class=\"flex-1 overflow-y-auto _shk-scrollbar px-2 py-3\">\n @if (activeSection()) {\n <div class=\"px-2 pb-3\">\n <span class=\"nav-section-label text-xs font-bold tracking-[0.08em] uppercase _shk-text-surface-400 dark:[color:var(--shk-surface-500)] block whitespace-nowrap overflow-hidden text-ellipsis\">\n {{ label(activeSection()!.label) }}\n </span>\n </div>\n }\n\n <ul class=\"space-y-1\">\n @for (item of navItems(); track item.id) {\n @if (item.separator) {\n <li class=\"my-2 border-t _shk-border dark:[border-color:var(--shk-surface-200)]\"></li>\n }\n\n @if (item.children?.length) {\n <!-- Expandable group -->\n <li>\n <button\n type=\"button\"\n (click)=\"toggleGroup(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class.shadow-md]=\"isGroupActive(item)\"\n [class._shk-text-surface-700]=\"!isGroupActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isGroupActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 transition-transform duration-200 shrink-0\"\n [class.rotate-180]=\"isGroupExpanded(item)\"\n ></svg>\n </button>\n\n @if (isGroupExpanded(item)) {\n <ul class=\"ml-3 mt-1 space-y-1 border-l-2 _shk-border dark:[border-color:var(--shk-surface-200)] pl-2\">\n @for (child of item.children; track child.id) {\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(child)\"\n class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary-soft]=\"isItemActive(child)\"\n [class._shk-bg-primary-soft-dark]=\"isItemActive(child)\"\n [class._shk-text-primary-700]=\"isItemActive(child)\"\n [class.dark:[color:var(--shk-primary-300)]]=\"isItemActive(child)\"\n [class._shk-text-surface-600]=\"!isItemActive(child)\"\n [class.dark:[color:var(--shk-surface-400)]]=\"!isItemActive(child)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(child)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(child)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(child)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(child)\"\n >\n @if (child.icon) {\n <svg [lucideIcon]=\"getIcon(child.icon)\" class=\"w-4 h-4 shrink-0\"></svg>\n }\n <span class=\"whitespace-nowrap overflow-hidden\">{{ label(child.label) }}</span>\n @if (isItemActive(child)) {\n <span class=\"w-1.5 h-1.5 rounded-full _shk-bg-primary shrink-0 ml-auto\"></span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </li>\n } @else {\n <!-- Standalone item -->\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n @if (item.badge !== undefined) {\n <span class=\"text-xs font-bold px-2 py-0.5 rounded-full _shk-bg-primary _shk-text-white shrink-0\">\n {{ item.badge }}\n </span>\n }\n </button>\n </li>\n }\n }\n </ul>\n </nav>\n </div>\n }\n</aside>\n", styles: [":host{display:block}.rail-workspace{cursor:pointer;transition:opacity .2s ease,box-shadow .2s ease}.rail-workspace.is-collapsed:hover{cursor:col-resize}.rail-workspace .rail-app-icon,.rail-workspace .rail-toggle-icon{grid-area:1 / 1 / 2 / 2;transition:opacity .2s ease}.rail-toggle-icon,.rail-workspace.is-collapsed:hover .rail-app-icon{opacity:0}.rail-workspace.is-collapsed:hover .rail-toggle-icon{opacity:1}.rail-icon{background:transparent;color:var(--shk-surface-700);border:1px solid transparent;transition:transform .18s ease,color .18s ease,background-color .18s ease,box-shadow .18s ease}.rail-icon svg{stroke-width:1.8}.rail-icon:hover{background:var(--shk-surface-100);color:var(--shk-primary-600)}.dark .rail-icon:hover{background:var(--shk-surface-200);color:var(--shk-primary-400)}.rail-icon.is-active{background:var(--shk-primary-600);color:#fff;box-shadow:0 1px 2px #0003}.dark .rail-icon.is-active{background:var(--shk-primary-500);color:#fff}.rail-logout:hover{color:var(--shk-error-500)}.dark .rail-logout:hover{color:var(--shk-error-400)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: TooltipComponent, selector: "shk-tooltip", inputs: ["text", "position", "mode", "offset"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] }); }
3999
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: RailSidebarComponent, isStandalone: true, selector: "shk-rail-sidebar", inputs: { sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, activeUrl: { classPropertyName: "activeUrl", publicName: "activeUrl", isSignal: true, isRequired: false, transformFunction: null }, activeSectionKey: { classPropertyName: "activeSectionKey", publicName: "activeSectionKey", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeLang: { classPropertyName: "activeLang", publicName: "activeLang", isSignal: true, isRequired: false, transformFunction: null }, langs: { classPropertyName: "langs", publicName: "langs", isSignal: true, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: true, isRequired: false, transformFunction: null }, appName: { classPropertyName: "appName", publicName: "appName", isSignal: true, isRequired: false, transformFunction: null }, userRole: { classPropertyName: "userRole", publicName: "userRole", isSignal: true, isRequired: false, transformFunction: null }, labelPipe: { classPropertyName: "labelPipe", publicName: "labelPipe", isSignal: true, isRequired: false, transformFunction: null }, railWidth: { classPropertyName: "railWidth", publicName: "railWidth", isSignal: true, isRequired: false, transformFunction: null }, expandedWidth: { classPropertyName: "expandedWidth", publicName: "expandedWidth", isSignal: true, isRequired: false, transformFunction: null }, workspaceTooltip: { classPropertyName: "workspaceTooltip", publicName: "workspaceTooltip", isSignal: true, isRequired: false, transformFunction: null }, logoutTooltip: { classPropertyName: "logoutTooltip", publicName: "logoutTooltip", isSignal: true, isRequired: false, transformFunction: null }, collapseTooltip: { classPropertyName: "collapseTooltip", publicName: "collapseTooltip", isSignal: true, isRequired: false, transformFunction: null }, expandTooltip: { classPropertyName: "expandTooltip", publicName: "expandTooltip", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeSectionKey: "activeSectionKeyChange", collapsed: "collapsedChange", sectionSelected: "sectionSelected", itemClick: "itemClick", setLang: "setLang", logoutRequested: "logoutRequested", toggleSidebar: "toggleSidebar", workspaceClick: "workspaceClick" }, ngImport: i0, template: "<aside\n class=\"h-full flex overflow-hidden shrink-0 _shk-bg-white dark:[background-color:var(--shk-surface-50)] border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"width()\"\n>\n <!-- Rail: vertical icon column -->\n <div\n class=\"h-full flex flex-col items-center shrink-0 px-[0.3rem] py-2 gap-1 border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"railWidth()\"\n >\n <!-- Workspace / logo -->\n <div class=\"flex items-center justify-center w-full\">\n <button\n type=\"button\"\n (click)=\"onWorkspace()\"\n [class.is-collapsed]=\"collapsed()\"\n class=\"rail-workspace w-[3.2rem] h-[3.2rem] rounded-xl _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] border-none grid place-items-center p-0 relative transition-all\"\n [title]=\"workspaceTooltip()\"\n >\n @if (logo()) {\n <img [src]=\"logo()\" alt=\"Logo\" class=\"rail-app-icon w-8 h-8 object-contain\" />\n } @else {\n <svg [lucideIcon]=\"getIcon('menu')\" class=\"rail-app-icon w-6 h-6 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n @if (collapsed()) {\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"rail-toggle-icon w-5 h-5 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n </button>\n </div>\n\n <!-- Section actions -->\n <div class=\"rail-actions flex-1 flex flex-col items-center gap-1 pt-1\">\n @for (section of sections(); track section.key) {\n <shk-tooltip [text]=\"label(section.label)\" position=\"right\" mode=\"fixed\">\n <button\n type=\"button\"\n (click)=\"onSectionClick(section)\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [class.is-active]=\"isSectionActive(section)\"\n [attr.aria-label]=\"label(section.label)\"\n >\n <svg [lucideIcon]=\"getIcon(section.icon)\" class=\"w-5 h-5\"></svg>\n </button>\n </shk-tooltip>\n }\n </div>\n\n <!-- Bottom: language + logout -->\n <div class=\"rail-bottom mt-auto flex flex-col items-center gap-1\">\n <button\n type=\"button\"\n (click)=\"onToggleLang()\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"activeLang()\"\n >\n <div class=\"flex flex-col items-center gap-[0.15rem]\">\n <svg [lucideIcon]=\"getIcon('globe')\" class=\"w-5 h-5\"></svg>\n <span class=\"rail-lang-code text-[0.5rem] font-bold tracking-[0.05em] leading-none\">{{ activeLang() | uppercase }}</span>\n </div>\n </button>\n\n <button\n type=\"button\"\n (click)=\"logoutRequested.emit()\"\n class=\"rail-icon rail-logout w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"logoutTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('log-out')\" class=\"w-5 h-5\"></svg>\n </button>\n </div>\n </div>\n\n <!-- Nav: section panel (hidden when collapsed) -->\n @if (!collapsed()) {\n <div class=\"flex-1 min-w-0 flex flex-col _shk-bg-white dark:[background-color:var(--shk-surface-50)]\">\n <!-- Brand header -->\n <div class=\"flex items-center justify-between gap-2 px-3 py-4 border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <span class=\"nav-app text-base font-bold whitespace-nowrap overflow-hidden text-ellipsis _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">\n {{ appName() }}\n </span>\n <button\n type=\"button\"\n (click)=\"onToggleSidebar()\"\n class=\"nav-toggle w-[1.8rem] h-[1.8rem] rounded-lg border-none bg-transparent grid place-items-center cursor-pointer shrink-0 transition-colors _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n [title]=\"collapseTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n\n <!-- Menu -->\n <nav class=\"flex-1 overflow-y-auto _shk-scrollbar px-2 py-3\">\n @if (activeSection()) {\n <div class=\"px-2 pb-3\">\n <span class=\"nav-section-label text-xs font-bold tracking-[0.08em] uppercase _shk-text-surface-400 dark:[color:var(--shk-surface-500)] block whitespace-nowrap overflow-hidden text-ellipsis\">\n {{ label(activeSection()!.label) }}\n </span>\n </div>\n }\n\n <ul class=\"space-y-1\">\n @for (item of navItems(); track item.id) {\n @if (item.separator) {\n <li class=\"my-2 border-t _shk-border dark:[border-color:var(--shk-surface-200)]\"></li>\n }\n\n @if (item.children?.length) {\n <!-- Expandable group -->\n <li>\n <button\n type=\"button\"\n (click)=\"toggleGroup(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class.shadow-md]=\"isGroupActive(item)\"\n [class._shk-text-surface-700]=\"!isGroupActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isGroupActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 transition-transform duration-200 shrink-0\"\n [class.rotate-180]=\"isGroupExpanded(item)\"\n ></svg>\n </button>\n\n @if (isGroupExpanded(item)) {\n <ul class=\"ml-3 mt-1 space-y-1 border-l-2 _shk-border dark:[border-color:var(--shk-surface-200)] pl-2\">\n @for (child of item.children; track child.id) {\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(child)\"\n class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary-soft]=\"isItemActive(child)\"\n [class._shk-bg-primary-soft-dark]=\"isItemActive(child)\"\n [class._shk-text-primary-700]=\"isItemActive(child)\"\n [class.dark:[color:var(--shk-primary-300)]]=\"isItemActive(child)\"\n [class._shk-text-surface-600]=\"!isItemActive(child)\"\n [class.dark:[color:var(--shk-surface-400)]]=\"!isItemActive(child)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(child)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(child)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(child)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(child)\"\n >\n @if (child.icon) {\n <svg [lucideIcon]=\"getIcon(child.icon)\" class=\"w-4 h-4 shrink-0\"></svg>\n }\n <span class=\"whitespace-nowrap overflow-hidden\">{{ label(child.label) }}</span>\n @if (isItemActive(child)) {\n <span class=\"w-1.5 h-1.5 rounded-full _shk-bg-primary shrink-0 ml-auto\"></span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </li>\n } @else {\n <!-- Standalone item -->\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n @if (item.badge !== undefined) {\n <span class=\"text-xs font-bold px-2 py-0.5 rounded-full _shk-bg-primary _shk-text-white shrink-0\">\n {{ item.badge }}\n </span>\n }\n </button>\n </li>\n }\n }\n </ul>\n </nav>\n </div>\n }\n</aside>\n", styles: [":host{display:block;height:100%}.rail-workspace{cursor:pointer;transition:opacity .2s ease,box-shadow .2s ease}.rail-workspace.is-collapsed:hover{cursor:col-resize}.rail-workspace .rail-app-icon,.rail-workspace .rail-toggle-icon{grid-area:1 / 1 / 2 / 2;transition:opacity .2s ease}.rail-toggle-icon,.rail-workspace.is-collapsed:hover .rail-app-icon{opacity:0}.rail-workspace.is-collapsed:hover .rail-toggle-icon{opacity:1}.rail-icon{background:transparent;color:var(--shk-surface-700);border:1px solid transparent;transition:transform .18s ease,color .18s ease,background-color .18s ease,box-shadow .18s ease}.rail-icon svg{stroke-width:1.8}.rail-icon:hover{background:var(--shk-surface-100);color:var(--shk-primary-600)}.dark .rail-icon:hover{background:var(--shk-surface-200);color:var(--shk-primary-400)}.rail-icon.is-active{background:var(--shk-primary-600);color:#fff;box-shadow:0 1px 2px #0003}.dark .rail-icon.is-active{background:var(--shk-primary-500);color:#fff}.rail-logout:hover{color:var(--shk-error-500)}.dark .rail-logout:hover{color:var(--shk-error-400)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: TooltipComponent, selector: "shk-tooltip", inputs: ["text", "position", "mode", "offset"] }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }] }); }
3423
4000
  }
3424
4001
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: RailSidebarComponent, decorators: [{
3425
4002
  type: Component,
3426
- args: [{ selector: 'shk-rail-sidebar', standalone: true, imports: [CommonModule, LucideDynamicIcon, TooltipComponent], template: "<aside\n class=\"h-full flex overflow-hidden shrink-0 _shk-bg-white dark:[background-color:var(--shk-surface-50)] border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"width()\"\n>\n <!-- Rail: vertical icon column -->\n <div\n class=\"h-full flex flex-col items-center shrink-0 px-[0.3rem] py-2 gap-1 border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"railWidth()\"\n >\n <!-- Workspace / logo -->\n <div class=\"flex items-center justify-center w-full\">\n <button\n type=\"button\"\n (click)=\"onWorkspace()\"\n [class.is-collapsed]=\"collapsed()\"\n class=\"rail-workspace w-[3.2rem] h-[3.2rem] rounded-xl _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] border-none grid place-items-center p-0 relative transition-all\"\n [title]=\"workspaceTooltip()\"\n >\n @if (logo()) {\n <img [src]=\"logo()\" alt=\"Logo\" class=\"rail-app-icon w-8 h-8 object-contain\" />\n } @else {\n <svg [lucideIcon]=\"getIcon('menu')\" class=\"rail-app-icon w-6 h-6 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n @if (collapsed()) {\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"rail-toggle-icon w-5 h-5 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n </button>\n </div>\n\n <!-- Section actions -->\n <div class=\"rail-actions flex-1 flex flex-col items-center gap-1 pt-1\">\n @for (section of sections(); track section.key) {\n <shk-tooltip [text]=\"label(section.label)\" position=\"right\" mode=\"fixed\">\n <button\n type=\"button\"\n (click)=\"onSectionClick(section)\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [class.is-active]=\"isSectionActive(section)\"\n [attr.aria-label]=\"label(section.label)\"\n >\n <svg [lucideIcon]=\"getIcon(section.icon)\" class=\"w-5 h-5\"></svg>\n </button>\n </shk-tooltip>\n }\n </div>\n\n <!-- Bottom: language + logout -->\n <div class=\"rail-bottom mt-auto flex flex-col items-center gap-1\">\n <button\n type=\"button\"\n (click)=\"onToggleLang()\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"activeLang()\"\n >\n <div class=\"flex flex-col items-center gap-[0.15rem]\">\n <svg [lucideIcon]=\"getIcon('globe')\" class=\"w-5 h-5\"></svg>\n <span class=\"rail-lang-code text-[0.5rem] font-bold tracking-[0.05em] leading-none\">{{ activeLang() | uppercase }}</span>\n </div>\n </button>\n\n <button\n type=\"button\"\n (click)=\"logoutRequested.emit()\"\n class=\"rail-icon rail-logout w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"logoutTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('log-out')\" class=\"w-5 h-5\"></svg>\n </button>\n </div>\n </div>\n\n <!-- Nav: section panel (hidden when collapsed) -->\n @if (!collapsed()) {\n <div class=\"flex-1 min-w-0 flex flex-col _shk-bg-white dark:[background-color:var(--shk-surface-50)]\">\n <!-- Brand header -->\n <div class=\"flex items-center justify-between gap-2 px-3 py-4 border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <span class=\"nav-app text-base font-bold whitespace-nowrap overflow-hidden text-ellipsis _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">\n {{ appName() }}\n </span>\n <button\n type=\"button\"\n (click)=\"onToggleSidebar()\"\n class=\"nav-toggle w-[1.8rem] h-[1.8rem] rounded-lg border-none bg-transparent grid place-items-center cursor-pointer shrink-0 transition-colors _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n [title]=\"collapseTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n\n <!-- Menu -->\n <nav class=\"flex-1 overflow-y-auto _shk-scrollbar px-2 py-3\">\n @if (activeSection()) {\n <div class=\"px-2 pb-3\">\n <span class=\"nav-section-label text-xs font-bold tracking-[0.08em] uppercase _shk-text-surface-400 dark:[color:var(--shk-surface-500)] block whitespace-nowrap overflow-hidden text-ellipsis\">\n {{ label(activeSection()!.label) }}\n </span>\n </div>\n }\n\n <ul class=\"space-y-1\">\n @for (item of navItems(); track item.id) {\n @if (item.separator) {\n <li class=\"my-2 border-t _shk-border dark:[border-color:var(--shk-surface-200)]\"></li>\n }\n\n @if (item.children?.length) {\n <!-- Expandable group -->\n <li>\n <button\n type=\"button\"\n (click)=\"toggleGroup(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class.shadow-md]=\"isGroupActive(item)\"\n [class._shk-text-surface-700]=\"!isGroupActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isGroupActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 transition-transform duration-200 shrink-0\"\n [class.rotate-180]=\"isGroupExpanded(item)\"\n ></svg>\n </button>\n\n @if (isGroupExpanded(item)) {\n <ul class=\"ml-3 mt-1 space-y-1 border-l-2 _shk-border dark:[border-color:var(--shk-surface-200)] pl-2\">\n @for (child of item.children; track child.id) {\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(child)\"\n class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary-soft]=\"isItemActive(child)\"\n [class._shk-bg-primary-soft-dark]=\"isItemActive(child)\"\n [class._shk-text-primary-700]=\"isItemActive(child)\"\n [class.dark:[color:var(--shk-primary-300)]]=\"isItemActive(child)\"\n [class._shk-text-surface-600]=\"!isItemActive(child)\"\n [class.dark:[color:var(--shk-surface-400)]]=\"!isItemActive(child)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(child)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(child)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(child)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(child)\"\n >\n @if (child.icon) {\n <svg [lucideIcon]=\"getIcon(child.icon)\" class=\"w-4 h-4 shrink-0\"></svg>\n }\n <span class=\"whitespace-nowrap overflow-hidden\">{{ label(child.label) }}</span>\n @if (isItemActive(child)) {\n <span class=\"w-1.5 h-1.5 rounded-full _shk-bg-primary shrink-0 ml-auto\"></span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </li>\n } @else {\n <!-- Standalone item -->\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n @if (item.badge !== undefined) {\n <span class=\"text-xs font-bold px-2 py-0.5 rounded-full _shk-bg-primary _shk-text-white shrink-0\">\n {{ item.badge }}\n </span>\n }\n </button>\n </li>\n }\n }\n </ul>\n </nav>\n </div>\n }\n</aside>\n", styles: [":host{display:block}.rail-workspace{cursor:pointer;transition:opacity .2s ease,box-shadow .2s ease}.rail-workspace.is-collapsed:hover{cursor:col-resize}.rail-workspace .rail-app-icon,.rail-workspace .rail-toggle-icon{grid-area:1 / 1 / 2 / 2;transition:opacity .2s ease}.rail-toggle-icon,.rail-workspace.is-collapsed:hover .rail-app-icon{opacity:0}.rail-workspace.is-collapsed:hover .rail-toggle-icon{opacity:1}.rail-icon{background:transparent;color:var(--shk-surface-700);border:1px solid transparent;transition:transform .18s ease,color .18s ease,background-color .18s ease,box-shadow .18s ease}.rail-icon svg{stroke-width:1.8}.rail-icon:hover{background:var(--shk-surface-100);color:var(--shk-primary-600)}.dark .rail-icon:hover{background:var(--shk-surface-200);color:var(--shk-primary-400)}.rail-icon.is-active{background:var(--shk-primary-600);color:#fff;box-shadow:0 1px 2px #0003}.dark .rail-icon.is-active{background:var(--shk-primary-500);color:#fff}.rail-logout:hover{color:var(--shk-error-500)}.dark .rail-logout:hover{color:var(--shk-error-400)}\n"] }]
4003
+ args: [{ selector: 'shk-rail-sidebar', standalone: true, imports: [CommonModule, LucideDynamicIcon, TooltipComponent], template: "<aside\n class=\"h-full flex overflow-hidden shrink-0 _shk-bg-white dark:[background-color:var(--shk-surface-50)] border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"width()\"\n>\n <!-- Rail: vertical icon column -->\n <div\n class=\"h-full flex flex-col items-center shrink-0 px-[0.3rem] py-2 gap-1 border-r _shk-border dark:[border-color:var(--shk-surface-100)]\"\n [style.width]=\"railWidth()\"\n >\n <!-- Workspace / logo -->\n <div class=\"flex items-center justify-center w-full\">\n <button\n type=\"button\"\n (click)=\"onWorkspace()\"\n [class.is-collapsed]=\"collapsed()\"\n class=\"rail-workspace w-[3.2rem] h-[3.2rem] rounded-xl _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] border-none grid place-items-center p-0 relative transition-all\"\n [title]=\"workspaceTooltip()\"\n >\n @if (logo()) {\n <img [src]=\"logo()\" alt=\"Logo\" class=\"rail-app-icon w-8 h-8 object-contain\" />\n } @else {\n <svg [lucideIcon]=\"getIcon('menu')\" class=\"rail-app-icon w-6 h-6 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n @if (collapsed()) {\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"rail-toggle-icon w-5 h-5 _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"></svg>\n }\n </button>\n </div>\n\n <!-- Section actions -->\n <div class=\"rail-actions flex-1 flex flex-col items-center gap-1 pt-1\">\n @for (section of sections(); track section.key) {\n <shk-tooltip [text]=\"label(section.label)\" position=\"right\" mode=\"fixed\">\n <button\n type=\"button\"\n (click)=\"onSectionClick(section)\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [class.is-active]=\"isSectionActive(section)\"\n [attr.aria-label]=\"label(section.label)\"\n >\n <svg [lucideIcon]=\"getIcon(section.icon)\" class=\"w-5 h-5\"></svg>\n </button>\n </shk-tooltip>\n }\n </div>\n\n <!-- Bottom: language + logout -->\n <div class=\"rail-bottom mt-auto flex flex-col items-center gap-1\">\n <button\n type=\"button\"\n (click)=\"onToggleLang()\"\n class=\"rail-icon w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"activeLang()\"\n >\n <div class=\"flex flex-col items-center gap-[0.15rem]\">\n <svg [lucideIcon]=\"getIcon('globe')\" class=\"w-5 h-5\"></svg>\n <span class=\"rail-lang-code text-[0.5rem] font-bold tracking-[0.05em] leading-none\">{{ activeLang() | uppercase }}</span>\n </div>\n </button>\n\n <button\n type=\"button\"\n (click)=\"logoutRequested.emit()\"\n class=\"rail-icon rail-logout w-[3.4rem] h-[3.4rem] rounded-[0.85rem] grid place-items-center transition-all cursor-pointer\"\n [title]=\"logoutTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('log-out')\" class=\"w-5 h-5\"></svg>\n </button>\n </div>\n </div>\n\n <!-- Nav: section panel (hidden when collapsed) -->\n @if (!collapsed()) {\n <div class=\"flex-1 min-w-0 flex flex-col _shk-bg-white dark:[background-color:var(--shk-surface-50)]\">\n <!-- Brand header -->\n <div class=\"flex items-center justify-between gap-2 px-3 py-4 border-b _shk-border dark:[border-color:var(--shk-surface-100)]\">\n <span class=\"nav-app text-base font-bold whitespace-nowrap overflow-hidden text-ellipsis _shk-text-surface-800 dark:[color:var(--shk-surface-800)]\">\n {{ appName() }}\n </span>\n <button\n type=\"button\"\n (click)=\"onToggleSidebar()\"\n class=\"nav-toggle w-[1.8rem] h-[1.8rem] rounded-lg border-none bg-transparent grid place-items-center cursor-pointer shrink-0 transition-colors _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\"\n [title]=\"collapseTooltip()\"\n >\n <svg [lucideIcon]=\"getIcon('panel-right')\" class=\"w-4 h-4\"></svg>\n </button>\n </div>\n\n <!-- Menu -->\n <nav class=\"flex-1 overflow-y-auto _shk-scrollbar px-2 py-3\">\n @if (activeSection()) {\n <div class=\"px-2 pb-3\">\n <span class=\"nav-section-label text-xs font-bold tracking-[0.08em] uppercase _shk-text-surface-400 dark:[color:var(--shk-surface-500)] block whitespace-nowrap overflow-hidden text-ellipsis\">\n {{ label(activeSection()!.label) }}\n </span>\n </div>\n }\n\n <ul class=\"space-y-1\">\n @for (item of navItems(); track item.id) {\n @if (item.separator) {\n <li class=\"my-2 border-t _shk-border dark:[border-color:var(--shk-surface-200)]\"></li>\n }\n\n @if (item.children?.length) {\n <!-- Expandable group -->\n <li>\n <button\n type=\"button\"\n (click)=\"toggleGroup(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isGroupActive(item)\"\n [class._shk-text-white]=\"isGroupActive(item)\"\n [class.shadow-md]=\"isGroupActive(item)\"\n [class._shk-text-surface-700]=\"!isGroupActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isGroupActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isGroupActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isGroupActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n <svg\n [lucideIcon]=\"getIcon('chevron-down')\"\n class=\"w-4 h-4 transition-transform duration-200 shrink-0\"\n [class.rotate-180]=\"isGroupExpanded(item)\"\n ></svg>\n </button>\n\n @if (isGroupExpanded(item)) {\n <ul class=\"ml-3 mt-1 space-y-1 border-l-2 _shk-border dark:[border-color:var(--shk-surface-200)] pl-2\">\n @for (child of item.children; track child.id) {\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(child)\"\n class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary-soft]=\"isItemActive(child)\"\n [class._shk-bg-primary-soft-dark]=\"isItemActive(child)\"\n [class._shk-text-primary-700]=\"isItemActive(child)\"\n [class.dark:[color:var(--shk-primary-300)]]=\"isItemActive(child)\"\n [class._shk-text-surface-600]=\"!isItemActive(child)\"\n [class.dark:[color:var(--shk-surface-400)]]=\"!isItemActive(child)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(child)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(child)\"\n [class.hover:[color:var(--shk-surface-900)]]=\"!isItemActive(child)\"\n [class.dark:hover:[color:var(--shk-surface-700)]]=\"!isItemActive(child)\"\n >\n @if (child.icon) {\n <svg [lucideIcon]=\"getIcon(child.icon)\" class=\"w-4 h-4 shrink-0\"></svg>\n }\n <span class=\"whitespace-nowrap overflow-hidden\">{{ label(child.label) }}</span>\n @if (isItemActive(child)) {\n <span class=\"w-1.5 h-1.5 rounded-full _shk-bg-primary shrink-0 ml-auto\"></span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </li>\n } @else {\n <!-- Standalone item -->\n <li>\n <button\n type=\"button\"\n (click)=\"onItemClick(item)\"\n class=\"w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200\"\n [class._shk-bg-primary]=\"isItemActive(item)\"\n [class._shk-text-white]=\"isItemActive(item)\"\n [class.shadow-md]=\"isItemActive(item)\"\n [class._shk-text-surface-700]=\"!isItemActive(item)\"\n [class.dark:[color:var(--shk-surface-600)]]=\"!isItemActive(item)\"\n [class.hover:[background-color:var(--shk-surface-100)]]=\"!isItemActive(item)\"\n [class.dark:hover:[background-color:var(--shk-surface-200)]]=\"!isItemActive(item)\"\n >\n @if (item.icon) {\n <svg [lucideIcon]=\"getIcon(item.icon)\" class=\"w-5 h-5 shrink-0\"></svg>\n }\n <span class=\"flex-1 text-left whitespace-nowrap overflow-hidden\">{{ label(item.label) }}</span>\n @if (item.badge !== undefined) {\n <span class=\"text-xs font-bold px-2 py-0.5 rounded-full _shk-bg-primary _shk-text-white shrink-0\">\n {{ item.badge }}\n </span>\n }\n </button>\n </li>\n }\n }\n </ul>\n </nav>\n </div>\n }\n</aside>\n", styles: [":host{display:block;height:100%}.rail-workspace{cursor:pointer;transition:opacity .2s ease,box-shadow .2s ease}.rail-workspace.is-collapsed:hover{cursor:col-resize}.rail-workspace .rail-app-icon,.rail-workspace .rail-toggle-icon{grid-area:1 / 1 / 2 / 2;transition:opacity .2s ease}.rail-toggle-icon,.rail-workspace.is-collapsed:hover .rail-app-icon{opacity:0}.rail-workspace.is-collapsed:hover .rail-toggle-icon{opacity:1}.rail-icon{background:transparent;color:var(--shk-surface-700);border:1px solid transparent;transition:transform .18s ease,color .18s ease,background-color .18s ease,box-shadow .18s ease}.rail-icon svg{stroke-width:1.8}.rail-icon:hover{background:var(--shk-surface-100);color:var(--shk-primary-600)}.dark .rail-icon:hover{background:var(--shk-surface-200);color:var(--shk-primary-400)}.rail-icon.is-active{background:var(--shk-primary-600);color:#fff;box-shadow:0 1px 2px #0003}.dark .rail-icon.is-active{background:var(--shk-primary-500);color:#fff}.rail-logout:hover{color:var(--shk-error-500)}.dark .rail-logout:hover{color:var(--shk-error-400)}\n"] }]
3427
4004
  }], ctorParameters: () => [], propDecorators: { sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }], activeUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeUrl", required: false }] }], activeSectionKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeSectionKey", required: false }] }, { type: i0.Output, args: ["activeSectionKeyChange"] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], activeLang: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeLang", required: false }] }], langs: [{ type: i0.Input, args: [{ isSignal: true, alias: "langs", required: false }] }], logo: [{ type: i0.Input, args: [{ isSignal: true, alias: "logo", required: false }] }], appName: [{ type: i0.Input, args: [{ isSignal: true, alias: "appName", required: false }] }], userRole: [{ type: i0.Input, args: [{ isSignal: true, alias: "userRole", required: false }] }], labelPipe: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPipe", required: false }] }], railWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "railWidth", required: false }] }], expandedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedWidth", required: false }] }], workspaceTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "workspaceTooltip", required: false }] }], logoutTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoutTooltip", required: false }] }], collapseTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseTooltip", required: false }] }], expandTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandTooltip", required: false }] }], sectionSelected: [{ type: i0.Output, args: ["sectionSelected"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], setLang: [{ type: i0.Output, args: ["setLang"] }], logoutRequested: [{ type: i0.Output, args: ["logoutRequested"] }], toggleSidebar: [{ type: i0.Output, args: ["toggleSidebar"] }], workspaceClick: [{ type: i0.Output, args: ["workspaceClick"] }] } });
3428
4005
 
4006
+ /**
4007
+ * Shirkasoft UI Components.
4008
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
4009
+ * Belongs to Shirkasoft.
4010
+ */
4011
+ /**
4012
+ * Chart.js wrapper that renders line, bar, pie, doughnut or polarArea charts
4013
+ * and updates them reactively as the inputs change.
4014
+ */
3429
4015
  class ChartComponent {
4016
+ /** Guarantees Chart.js plugins are only registered once. */
3430
4017
  static { this.registered = false; }
3431
4018
  constructor() {
4019
+ /** Type of chart to render. */
3432
4020
  this.type = input('bar', ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
4021
+ /** Chart.js datasets payload. */
3433
4022
  this.data = input({ datasets: [] }, ...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
4023
+ /** Chart.js options object. */
3434
4024
  this.options = input({}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4025
+ /** Reference to the inner canvas element. */
3435
4026
  this.canvasRef = viewChild.required('canvas');
4027
+ /** Live Chart.js instance. */
3436
4028
  this.chartInstance = null;
4029
+ /** Whether the view has been initialized yet. */
3437
4030
  this.initialized = false;
3438
4031
  if (!ChartComponent.registered) {
3439
4032
  Chart.register(...registerables);
@@ -3450,17 +4043,21 @@ class ChartComponent {
3450
4043
  });
3451
4044
  });
3452
4045
  }
4046
+ /** Creates the initial chart once the canvas is available. */
3453
4047
  ngAfterViewInit() {
3454
4048
  this.initialized = true;
3455
4049
  this.initChart();
3456
4050
  }
4051
+ /** Destroys the Chart.js instance. */
3457
4052
  ngOnDestroy() {
3458
4053
  this.chartInstance?.destroy();
3459
4054
  }
4055
+ /** Clears the canvas by reassigning its width. */
3460
4056
  resetCanvas() {
3461
4057
  const canvas = this.canvasRef().nativeElement;
3462
4058
  canvas.width = canvas.width;
3463
4059
  }
4060
+ /** Creates a new Chart.js instance on the canvas. */
3464
4061
  initChart() {
3465
4062
  this.resetCanvas();
3466
4063
  const canvas = this.canvasRef().nativeElement;
@@ -3474,6 +4071,7 @@ class ChartComponent {
3474
4071
  });
3475
4072
  this.chartInstance.resize();
3476
4073
  }
4074
+ /** Deep clones a value to detach it from Angular's reactive objects. */
3477
4075
  deepClone(value) {
3478
4076
  try {
3479
4077
  if (typeof structuredClone === 'function')
@@ -3482,6 +4080,7 @@ class ChartComponent {
3482
4080
  catch { }
3483
4081
  return JSON.parse(JSON.stringify(value));
3484
4082
  }
4083
+ /** Applies the new data/options, rebuilding the chart when the type changes. */
3485
4084
  updateChart(type, data, options) {
3486
4085
  if (!this.chartInstance) {
3487
4086
  this.initChart();
@@ -3520,6 +4119,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
3520
4119
  }]
3521
4120
  }], ctorParameters: () => [], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], canvasRef: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }] } });
3522
4121
 
4122
+ /**
4123
+ * Shirkasoft UI Components.
4124
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
4125
+ * Belongs to Shirkasoft.
4126
+ */
4127
+ /** Default dataset colors used by the ChartComponent when no palette is provided. */
3523
4128
  const COLOR_PALETTE = [
3524
4129
  { bg: 'rgba(59, 130, 246, 0.2)', border: 'rgb(59, 130, 246)' },
3525
4130
  { bg: 'rgba(34, 197, 94, 0.2)', border: 'rgb(34, 197, 94)' },
@@ -3528,6 +4133,10 @@ const COLOR_PALETTE = [
3528
4133
  { bg: 'rgba(236, 72, 153, 0.2)', border: 'rgb(236, 72, 153)' },
3529
4134
  { bg: 'rgba(6, 182, 212, 0.2)', border: 'rgb(6, 182, 212)' },
3530
4135
  ];
4136
+ /**
4137
+ * Reads the current `--shk-*` CSS variables from the document and returns
4138
+ * the text, muted and border colors used by charts to follow the active theme.
4139
+ */
3531
4140
  function readThemeColors() {
3532
4141
  const docStyle = getComputedStyle(document.documentElement);
3533
4142
  return {
@@ -3537,34 +4146,6 @@ function readThemeColors() {
3537
4146
  };
3538
4147
  }
3539
4148
 
3540
- const icons$2 = {
3541
- plus: LucidePlus,
3542
- pencil: LucidePencil,
3543
- 'trash-2': LucideTrash2,
3544
- delete: LucideTrash2,
3545
- 'circle-plus': LucideCirclePlus,
3546
- 'circle-help': LucideCircleHelp,
3547
- calendar: LucideCalendar,
3548
- 'calendar-days': LucideCalendarDays,
3549
- clock: LucideClock,
3550
- tag: LucideTag,
3551
- package: LucidePackage,
3552
- info: LucideInfo,
3553
- inbox: LucideInbox,
3554
- hotel: LucideHotel,
3555
- ticket: LucideTicket,
3556
- car: LucideCar,
3557
- mountain: LucideMountain,
3558
- bus: LucideBus,
3559
- ship: LucideShip,
3560
- shield: LucideShield,
3561
- utensils: LucideUtensils,
3562
- 'map-pin': LucideMapPin,
3563
- 'user-round': LucideUserRound,
3564
- };
3565
- function getIcon$2(name) {
3566
- return icons$2[name] ?? LucideCircleHelp;
3567
- }
3568
4149
  const DEFAULT_MARKER_GRADIENTS = [
3569
4150
  'bg-gradient-to-br from-blue-500 to-blue-600',
3570
4151
  'bg-gradient-to-br from-emerald-500 to-emerald-600',
@@ -3592,24 +4173,24 @@ const DEFAULT_SUPPLIER_ICON_COLORS = {
3592
4173
  package: { bg: '#84cc16', text: '#ffffff' },
3593
4174
  other: { bg: '#e0fbfc', text: '#293241' },
3594
4175
  };
3595
- /** Maps a sub-item `type` to a lucide icon name. */
4176
+ /** Maps a sub-item `type` to a lucide icon component. */
3596
4177
  const DEFAULT_SUPPLIER_TYPE_ICONS = {
3597
- accommodation: 'hotel',
3598
- air_tickets: 'ticket',
3599
- rent_a_car: 'car',
3600
- excursion: 'mountain',
3601
- transfer: 'bus',
3602
- cruise: 'ship',
3603
- insurance: 'shield',
3604
- restaurant: 'utensils',
3605
- visit: 'map-pin',
3606
- guide: 'user-round',
3607
- package: 'package',
3608
- other: 'tag',
4178
+ accommodation: LucideHotel,
4179
+ air_tickets: LucideTicket,
4180
+ rent_a_car: LucideCar,
4181
+ excursion: LucideMountain,
4182
+ transfer: LucideBus,
4183
+ cruise: LucideShip,
4184
+ insurance: LucideShield,
4185
+ restaurant: LucideUtensils,
4186
+ visit: LucideMapPin,
4187
+ guide: LucideUserRound,
4188
+ package: LucidePackage,
4189
+ other: LucideTag,
3609
4190
  };
3610
4191
  class TimelineComponent {
3611
4192
  constructor() {
3612
- this.getIcon = getIcon$2;
4193
+ this.getIcon = (name) => getIcon(name, LucideCircleQuestionMark);
3613
4194
  /** Timeline data. */
3614
4195
  this.items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
3615
4196
  // ---------- Layout ----------
@@ -3649,7 +4230,7 @@ class TimelineComponent {
3649
4230
  this.itemActions = input([], ...(ngDevMode ? [{ debugName: "itemActions" }] : /* istanbul ignore next */ []));
3650
4231
  this.subItemActions = input([], ...(ngDevMode ? [{ debugName: "subItemActions" }] : /* istanbul ignore next */ []));
3651
4232
  // ---------- Sub-item icon mapping ----------
3652
- /** Maps a sub-item `type` to a lucide icon name. */
4233
+ /** Maps a sub-item `type` to a lucide icon (name or component). */
3653
4234
  this.iconMap = input({}, ...(ngDevMode ? [{ debugName: "iconMap" }] : /* istanbul ignore next */ []));
3654
4235
  /** Maps a sub-item `type` to its icon-box colors. */
3655
4236
  this.iconColors = input({}, ...(ngDevMode ? [{ debugName: "iconColors" }] : /* istanbul ignore next */ []));
@@ -3726,7 +4307,14 @@ class TimelineComponent {
3726
4307
  return typeof action.label === 'function' ? action.label(item) : action.label;
3727
4308
  }
3728
4309
  getItemActionIconName(action, item) {
3729
- return typeof action.icon === 'function' ? action.icon(item) : action.icon;
4310
+ const icon = action.icon;
4311
+ if (typeof icon === 'function') {
4312
+ if (icon.prototype === undefined) {
4313
+ return icon(item);
4314
+ }
4315
+ return icon;
4316
+ }
4317
+ return icon;
3730
4318
  }
3731
4319
  getItemActionClass(action, item) {
3732
4320
  if (!action.class)
@@ -3744,7 +4332,14 @@ class TimelineComponent {
3744
4332
  return typeof action.label === 'function' ? action.label(item, sub) : action.label;
3745
4333
  }
3746
4334
  getSubItemActionIconName(action, item, sub) {
3747
- return typeof action.icon === 'function' ? action.icon(item, sub) : action.icon;
4335
+ const icon = action.icon;
4336
+ if (typeof icon === 'function') {
4337
+ if (icon.prototype === undefined) {
4338
+ return icon(item, sub);
4339
+ }
4340
+ return icon;
4341
+ }
4342
+ return icon;
3748
4343
  }
3749
4344
  isSubItemActionVisible(action, item, sub) {
3750
4345
  return action.isVisible ? action.isVisible(item, sub) : true;
@@ -3753,28 +4348,26 @@ class TimelineComponent {
3753
4348
  return action.isDisabled ? action.isDisabled(item, sub) : false;
3754
4349
  }
3755
4350
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: TimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3756
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: TimelineComponent, isStandalone: true, selector: "shk-timeline", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, showLine: { classPropertyName: "showLine", publicName: "showLine", isSignal: true, isRequired: false, transformFunction: null }, markerSize: { classPropertyName: "markerSize", publicName: "markerSize", isSignal: true, isRequired: false, transformFunction: null }, markerGradients: { classPropertyName: "markerGradients", publicName: "markerGradients", isSignal: true, isRequired: false, transformFunction: null }, showOpposite: { classPropertyName: "showOpposite", publicName: "showOpposite", isSignal: true, isRequired: false, transformFunction: null }, showDescription: { classPropertyName: "showDescription", publicName: "showDescription", isSignal: true, isRequired: false, transformFunction: null }, showSubItemMeta: { classPropertyName: "showSubItemMeta", publicName: "showSubItemMeta", isSignal: true, isRequired: false, transformFunction: null }, showSubItemPrice: { classPropertyName: "showSubItemPrice", publicName: "showSubItemPrice", isSignal: true, isRequired: false, transformFunction: null }, showSubItemCount: { classPropertyName: "showSubItemCount", publicName: "showSubItemCount", isSignal: true, isRequired: false, transformFunction: null }, subItemCountLabel: { classPropertyName: "subItemCountLabel", publicName: "subItemCountLabel", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, headerTitle: { classPropertyName: "headerTitle", publicName: "headerTitle", isSignal: true, isRequired: false, transformFunction: null }, showAddItem: { classPropertyName: "showAddItem", publicName: "showAddItem", isSignal: true, isRequired: false, transformFunction: null }, addItemLabel: { classPropertyName: "addItemLabel", publicName: "addItemLabel", isSignal: true, isRequired: false, transformFunction: null }, addItemIcon: { classPropertyName: "addItemIcon", publicName: "addItemIcon", isSignal: true, isRequired: false, transformFunction: null }, addItemTooltip: { classPropertyName: "addItemTooltip", publicName: "addItemTooltip", isSignal: true, isRequired: false, transformFunction: null }, headerActions: { classPropertyName: "headerActions", publicName: "headerActions", isSignal: true, isRequired: false, transformFunction: null }, showAddSubItem: { classPropertyName: "showAddSubItem", publicName: "showAddSubItem", isSignal: true, isRequired: false, transformFunction: null }, addSubItemLabel: { classPropertyName: "addSubItemLabel", publicName: "addSubItemLabel", isSignal: true, isRequired: false, transformFunction: null }, addSubItemTooltip: { classPropertyName: "addSubItemTooltip", publicName: "addSubItemTooltip", isSignal: true, isRequired: false, transformFunction: null }, itemActions: { classPropertyName: "itemActions", publicName: "itemActions", isSignal: true, isRequired: false, transformFunction: null }, subItemActions: { classPropertyName: "subItemActions", publicName: "subItemActions", isSignal: true, isRequired: false, transformFunction: null }, iconMap: { classPropertyName: "iconMap", publicName: "iconMap", isSignal: true, isRequired: false, transformFunction: null }, iconColors: { classPropertyName: "iconColors", publicName: "iconColors", isSignal: true, isRequired: false, transformFunction: null }, fallbackIcon: { classPropertyName: "fallbackIcon", publicName: "fallbackIcon", isSignal: true, isRequired: false, transformFunction: null }, showEmptyState: { classPropertyName: "showEmptyState", publicName: "showEmptyState", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, editTooltip: { classPropertyName: "editTooltip", publicName: "editTooltip", isSignal: true, isRequired: false, transformFunction: null }, deleteTooltip: { classPropertyName: "deleteTooltip", publicName: "deleteTooltip", isSignal: true, isRequired: false, transformFunction: null }, optionalLabel: { classPropertyName: "optionalLabel", publicName: "optionalLabel", isSignal: true, isRequired: false, transformFunction: null }, noSubItemsMessage: { classPropertyName: "noSubItemsMessage", publicName: "noSubItemsMessage", isSignal: true, isRequired: false, transformFunction: null }, customTemplates: { classPropertyName: "customTemplates", publicName: "customTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addItem: "addItem", addSubItem: "addSubItem", itemClick: "itemClick", subItemClick: "subItemClick" }, ngImport: i0, template: "<div class=\"space-y-4\">\n\n <!-- ============ Header ============ -->\n @if (showHeader() && (headerTitle() || showAddItem() || headerActions().length > 0)) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n @if (headerTitle()) {\n <div class=\"text-sm font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n {{ headerTitle() }}\n </div>\n }\n <div class=\"flex items-center gap-1.5 ml-auto\">\n @if (showAddItem()) {\n <button\n type=\"button\"\n (click)=\"addItem.emit()\"\n [attr.aria-label]=\"addItemTooltip()\"\n class=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon(addItemIcon())\" class=\"w-3.5 h-3.5\"></svg>\n {{ addItemLabel() }}\n </button>\n }\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <button\n type=\"button\"\n [class]=\"action.class || 'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed'\"\n [disabled]=\"isHeaderActionDisabled(action)\"\n (click)=\"action.onClick()\"\n >\n @if (action.icon) {\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n }\n {{ action.label }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- ============ Timeline ============ -->\n @if (items().length > 0) {\n <div class=\"relative\">\n @if (showLine()) {\n <div [class]=\"lineClass()\" aria-hidden=\"true\"></div>\n }\n\n @for (item of items(); track item.id; let i = $index) {\n @if (alignment() === 'alternate') {\n <div class=\"relative flex items-start gap-4 lg:grid lg:grid-cols-2 lg:gap-0\">\n <!-- Marker (centered on desktop) -->\n <div [class]=\"alternateMarkerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n\n <!-- Content card -->\n <div\n class=\"flex-1 min-w-0\"\n [class.lg:order-1]=\"$even\"\n [class.lg:order-2]=\"!$even\"\n [class.lg:pr-12]=\"$even\"\n [class.lg:pl-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n\n <!-- Opposite info -->\n @if (showOpposite()) {\n <div\n class=\"hidden lg:flex lg:items-center\"\n [class.lg:order-2]=\"$even\"\n [class.lg:order-1]=\"!$even\"\n [class.lg:justify-end]=\"$even\"\n [class.lg:pl-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['opposite'] || itemInfo; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n }\n </div>\n } @else {\n <!-- Left alignment -->\n <div class=\"relative flex items-start gap-4\">\n <div [class]=\"markerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n </div>\n }\n }\n </div>\n } @else if (showEmptyState()) {\n <!-- ============ Empty state ============ -->\n <div class=\"text-center py-12\">\n @if (customTemplates()['empty']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['empty']\"></ng-container>\n } @else {\n <div class=\"w-16 h-16 mx-auto mb-4 _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] rounded-full flex items-center justify-center\">\n <svg [lucideIcon]=\"getIcon(emptyIcon())\" class=\"w-8 h-8 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"></svg>\n </div>\n <p class=\"text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">{{ emptyMessage() }}</p>\n }\n </div>\n }\n\n <!-- ============ Templates ============ -->\n\n <ng-template #defaultMarker let-item let-index=\"index\">\n <span class=\"font-bold\">{{ markerText(item, index) }}</span>\n </ng-template>\n\n <ng-template #itemInfo let-item>\n <div>\n @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {\n <div class=\"mb-2\">\n <span class=\"inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\">\n @if (item.badgeIcon) {\n <svg [lucideIcon]=\"getIcon(item.badgeIcon)\" class=\"w-3 h-3\"></svg>\n }\n {{ item.badge }}\n </span>\n </div>\n }\n @if (item.title) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">{{ item.title }}</div>\n }\n @let subCount = (item.subItems?.length || 0);\n @if (item.subtitle || (showSubItemCount() && subCount > 0)) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] mt-1\">\n {{ item.subtitle || (subCount + ' ' + subItemCountLabel()) }}\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #card let-item>\n @let subItems = item.subItems || [];\n <div class=\"shk-timeline-card mb-4 bg-white dark:[background-color:var(--shk-surface-50)] rounded-xl border _shk-border dark:[border-color:var(--shk-surface-200)] shadow-sm overflow-hidden\">\n <!-- Header (mobile in alternate, always in left) -->\n <div class=\"flex items-center gap-2 px-4 pt-4 pb-2\" [class.lg:hidden]=\"alignment() === 'alternate'\">\n <ng-container *ngTemplateOutlet=\"itemInfo; context: { $implicit: item }\"></ng-container>\n </div>\n\n <!-- Description -->\n @if (showDescription() && item.description) {\n <div class=\"px-4 pt-3\">\n @if (customTemplates()['description']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['description']; context: { $implicit: item }\"></ng-container>\n } @else {\n <p class=\"text-sm _shk-text-surface-600 dark:[color:var(--shk-surface-600)] leading-relaxed\">{{ item.description }}</p>\n }\n </div>\n }\n\n <!-- Sub items -->\n @if (subItems.length > 0) {\n <div class=\"mt-2\">\n @for (sub of subItems; track sub.id) {\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['subItem'] || defaultSubItem; context: { $implicit: item, subItem: sub }\"\n ></ng-container>\n }\n </div>\n } @else if (noSubItemsMessage()) {\n <div class=\"px-4 py-6 text-center\">\n <div class=\"inline-flex items-center gap-2 text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n <svg [lucideIcon]=\"getIcon('info')\" class=\"w-4 h-4\"></svg>\n <span class=\"italic\">{{ noSubItemsMessage() }}</span>\n </div>\n </div>\n }\n\n <!-- Footer actions -->\n @if (showAddSubItem() || itemActions().length > 0) {\n <div class=\"flex items-center gap-1 px-4 py-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border-t _shk-border dark:[border-color:var(--shk-surface-100)]\">\n @if (showAddSubItem()) {\n <button\n type=\"button\"\n (click)=\"addSubItem.emit(item)\"\n class=\"relative group inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium _shk-text-primary hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] rounded-lg transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('circle-plus')\" class=\"w-3.5 h-3.5\"></svg>\n {{ addSubItemLabel() }}\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ addSubItemTooltip() }}\n </span>\n </button>\n }\n <div class=\"flex-1\"></div>\n @for (action of itemActions(); track $index) {\n @if (isItemActionVisible(action, item)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item)\"\n [class]=\"'timeline-icon-btn ' + getItemActionClass(action, item)\"\n [disabled]=\"isItemActionDisabled(action, item)\"\n [attr.aria-label]=\"getItemActionLabel(action, item)\"\n >\n <svg [lucideIcon]=\"getIcon(getItemActionIconName(action, item))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getItemActionLabel(action, item) }}\n </span>\n </div>\n }\n }\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #defaultSubItem let-item let-sub=\"subItem\">\n <div class=\"px-4 py-3 border-t [border-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-700)] hover:[background-color:color-mix(in srgb,var(--shk-primary-50)30%,transparent)] dark:hover:[background-color:color-mix(in srgb,var(--shk-primary-900)10%,transparent)] transition-colors duration-200 group\">\n <div class=\"flex items-start gap-3\">\n <span\n [class]=\"sub.iconClass || 'flex items-center justify-center w-9 h-9 rounded-lg shrink-0 mt-0.5'\"\n [style]=\"sub.iconClass ? undefined : subItemStyle(sub)\"\n >\n <svg [lucideIcon]=\"getIcon(subItemIconName(sub))\" class=\"w-4 h-4\"></svg>\n </span>\n\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2 flex-wrap\">\n @if (sub.optional) {\n <span class=\"text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded _shk-bg-amber-soft _shk-text-amber-600 dark:[color:var(--shk-amber-400)]\">\n {{ sub.optionalLabel || optionalLabel() }}\n </span>\n }\n <span class=\"font-medium _shk-text-surface-800 dark:[color:var(--shk-surface-800)] text-sm truncate\">{{ sub.title }}</span>\n @if (sub.subtitle) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] truncate\">\u00B7 {{ sub.subtitle }}</span>\n }\n </div>\n\n @if (showSubItemMeta() && (sub.meta?.length || 0) > 0) {\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-1 mt-1.5 text-xs _shk-text-surface-500 dark:[color:var(--shk-surface-500)]\">\n @for (m of sub.meta!; track $index) {\n <span class=\"flex items-center gap-1\">\n <svg [lucideIcon]=\"getIcon(m.icon)\" class=\"w-3 h-3\"></svg>\n @if (m.label) {\n <span class=\"font-medium\">{{ m.label }}:</span>\n }\n {{ m.value }}\n </span>\n }\n </div>\n }\n </div>\n\n <div class=\"flex items-center gap-0.5 shrink-0\">\n @if (showSubItemPrice() && sub.price !== undefined && sub.price !== null && sub.price !== '') {\n <span class=\"font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)] text-xs mr-1\">{{ sub.price }}</span>\n }\n @for (action of subItemActions(); track $index) {\n @if (isSubItemActionVisible(action, item, sub)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item, sub)\"\n class=\"timeline-icon-btn opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity\"\n [disabled]=\"isSubItemActionDisabled(action, item, sub)\"\n [attr.aria-label]=\"getSubItemActionLabel(action, item, sub)\"\n >\n <svg [lucideIcon]=\"getIcon(getSubItemActionIconName(action, item, sub))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute top-full left-1/2 -translate-x-1/2 mt-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getSubItemActionLabel(action, item, sub) }}\n </span>\n </div>\n }\n }\n </div>\n </div>\n </div>\n </ng-template>\n\n</div>\n", styles: [":host{display:block}.timeline-icon-btn{padding:.375rem;display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;color:var(--shk-primary-600);transition:background-color .15s ease-in-out,color .15s ease-in-out,opacity .15s ease-in-out}.timeline-icon-btn:hover:not(:disabled){background-color:color-mix(in srgb,var(--shk-primary-50) 60%,transparent);color:var(--shk-primary-700)}.timeline-icon-btn:disabled{opacity:.4;cursor:not-allowed}.timeline-icon-btn svg{width:.875rem!important;height:.875rem!important}.shk-timeline-card{animation:shk-timeline-fade-in .3s ease-out}@keyframes shk-timeline-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
4351
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.18", type: TimelineComponent, isStandalone: true, selector: "shk-timeline", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, showLine: { classPropertyName: "showLine", publicName: "showLine", isSignal: true, isRequired: false, transformFunction: null }, markerSize: { classPropertyName: "markerSize", publicName: "markerSize", isSignal: true, isRequired: false, transformFunction: null }, markerGradients: { classPropertyName: "markerGradients", publicName: "markerGradients", isSignal: true, isRequired: false, transformFunction: null }, showOpposite: { classPropertyName: "showOpposite", publicName: "showOpposite", isSignal: true, isRequired: false, transformFunction: null }, showDescription: { classPropertyName: "showDescription", publicName: "showDescription", isSignal: true, isRequired: false, transformFunction: null }, showSubItemMeta: { classPropertyName: "showSubItemMeta", publicName: "showSubItemMeta", isSignal: true, isRequired: false, transformFunction: null }, showSubItemPrice: { classPropertyName: "showSubItemPrice", publicName: "showSubItemPrice", isSignal: true, isRequired: false, transformFunction: null }, showSubItemCount: { classPropertyName: "showSubItemCount", publicName: "showSubItemCount", isSignal: true, isRequired: false, transformFunction: null }, subItemCountLabel: { classPropertyName: "subItemCountLabel", publicName: "subItemCountLabel", isSignal: true, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: true, isRequired: false, transformFunction: null }, headerTitle: { classPropertyName: "headerTitle", publicName: "headerTitle", isSignal: true, isRequired: false, transformFunction: null }, showAddItem: { classPropertyName: "showAddItem", publicName: "showAddItem", isSignal: true, isRequired: false, transformFunction: null }, addItemLabel: { classPropertyName: "addItemLabel", publicName: "addItemLabel", isSignal: true, isRequired: false, transformFunction: null }, addItemIcon: { classPropertyName: "addItemIcon", publicName: "addItemIcon", isSignal: true, isRequired: false, transformFunction: null }, addItemTooltip: { classPropertyName: "addItemTooltip", publicName: "addItemTooltip", isSignal: true, isRequired: false, transformFunction: null }, headerActions: { classPropertyName: "headerActions", publicName: "headerActions", isSignal: true, isRequired: false, transformFunction: null }, showAddSubItem: { classPropertyName: "showAddSubItem", publicName: "showAddSubItem", isSignal: true, isRequired: false, transformFunction: null }, addSubItemLabel: { classPropertyName: "addSubItemLabel", publicName: "addSubItemLabel", isSignal: true, isRequired: false, transformFunction: null }, addSubItemTooltip: { classPropertyName: "addSubItemTooltip", publicName: "addSubItemTooltip", isSignal: true, isRequired: false, transformFunction: null }, itemActions: { classPropertyName: "itemActions", publicName: "itemActions", isSignal: true, isRequired: false, transformFunction: null }, subItemActions: { classPropertyName: "subItemActions", publicName: "subItemActions", isSignal: true, isRequired: false, transformFunction: null }, iconMap: { classPropertyName: "iconMap", publicName: "iconMap", isSignal: true, isRequired: false, transformFunction: null }, iconColors: { classPropertyName: "iconColors", publicName: "iconColors", isSignal: true, isRequired: false, transformFunction: null }, fallbackIcon: { classPropertyName: "fallbackIcon", publicName: "fallbackIcon", isSignal: true, isRequired: false, transformFunction: null }, showEmptyState: { classPropertyName: "showEmptyState", publicName: "showEmptyState", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, editTooltip: { classPropertyName: "editTooltip", publicName: "editTooltip", isSignal: true, isRequired: false, transformFunction: null }, deleteTooltip: { classPropertyName: "deleteTooltip", publicName: "deleteTooltip", isSignal: true, isRequired: false, transformFunction: null }, optionalLabel: { classPropertyName: "optionalLabel", publicName: "optionalLabel", isSignal: true, isRequired: false, transformFunction: null }, noSubItemsMessage: { classPropertyName: "noSubItemsMessage", publicName: "noSubItemsMessage", isSignal: true, isRequired: false, transformFunction: null }, customTemplates: { classPropertyName: "customTemplates", publicName: "customTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addItem: "addItem", addSubItem: "addSubItem", itemClick: "itemClick", subItemClick: "subItemClick" }, ngImport: i0, template: "<div class=\"space-y-4\">\n\n <!-- ============ Header ============ -->\n @if (showHeader() && (headerTitle() || showAddItem() || headerActions().length > 0)) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n @if (headerTitle()) {\n <div class=\"text-sm font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n {{ headerTitle() }}\n </div>\n }\n <div class=\"flex items-center gap-1.5 ml-auto\">\n @if (showAddItem()) {\n <button\n type=\"button\"\n (click)=\"addItem.emit()\"\n [attr.aria-label]=\"addItemTooltip()\"\n class=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon(addItemIcon())\" class=\"w-3.5 h-3.5\"></svg>\n {{ addItemLabel() }}\n </button>\n }\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <button\n type=\"button\"\n [class]=\"action.class || 'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed'\"\n [disabled]=\"isHeaderActionDisabled(action)\"\n (click)=\"action.onClick()\"\n >\n @if (action.icon) {\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n }\n {{ action.label }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- ============ Timeline ============ -->\n @if (items().length > 0) {\n <div class=\"relative\">\n @if (showLine()) {\n <div [class]=\"lineClass()\" aria-hidden=\"true\"></div>\n }\n\n @for (item of items(); track item.id; let i = $index) {\n @if (alignment() === 'alternate') {\n <div class=\"relative flex items-start gap-4 lg:grid lg:grid-cols-2 lg:gap-0\">\n <!-- Marker (centered on desktop) -->\n <div [class]=\"alternateMarkerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n\n <!-- Content card -->\n <div\n class=\"flex-1 min-w-0\"\n [class.lg:order-1]=\"$even\"\n [class.lg:order-2]=\"!$even\"\n [class.lg:pr-12]=\"$even\"\n [class.lg:pl-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n\n <!-- Opposite info -->\n @if (showOpposite()) {\n <div\n class=\"hidden lg:flex lg:items-center\"\n [class.lg:order-2]=\"$even\"\n [class.lg:order-1]=\"!$even\"\n [class.lg:pl-12]=\"$even\"\n [class.lg:justify-end]=\"!$even\"\n [class.lg:pr-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['opposite'] || itemInfo; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n }\n </div>\n } @else {\n <!-- Left alignment -->\n <div class=\"relative flex items-start gap-4\">\n <div [class]=\"markerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n </div>\n }\n }\n </div>\n } @else if (showEmptyState()) {\n <!-- ============ Empty state ============ -->\n <div class=\"text-center py-12\">\n @if (customTemplates()['empty']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['empty']\"></ng-container>\n } @else {\n <div class=\"w-16 h-16 mx-auto mb-4 _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] rounded-full flex items-center justify-center\">\n <svg [lucideIcon]=\"getIcon(emptyIcon())\" class=\"w-8 h-8 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"></svg>\n </div>\n <p class=\"text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">{{ emptyMessage() }}</p>\n }\n </div>\n }\n\n <!-- ============ Templates ============ -->\n\n <ng-template #defaultMarker let-item let-index=\"index\">\n <span class=\"font-bold\">{{ markerText(item, index) }}</span>\n </ng-template>\n\n <ng-template #itemInfo let-item>\n <div>\n @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {\n <div class=\"mb-2\">\n <span class=\"inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\">\n @if (item.badgeIcon) {\n <svg [lucideIcon]=\"getIcon(item.badgeIcon)\" class=\"w-3 h-3\"></svg>\n }\n {{ item.badge }}\n </span>\n </div>\n }\n @if (item.title) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">{{ item.title }}</div>\n }\n @let subCount = (item.subItems?.length || 0);\n @if (item.subtitle || (showSubItemCount() && subCount > 0)) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] mt-1\">\n {{ item.subtitle || (subCount + ' ' + subItemCountLabel()) }}\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #card let-item>\n @let subItems = item.subItems || [];\n <div class=\"shk-timeline-card mb-4 bg-white dark:[background-color:var(--shk-surface-50)] rounded-xl border _shk-border dark:[border-color:var(--shk-surface-200)] shadow-sm overflow-hidden\">\n <!-- Header (mobile in alternate, always in left) -->\n <div class=\"flex items-center gap-2 px-4 pt-4 pb-2\" [class.lg:hidden]=\"alignment() === 'alternate'\">\n <ng-container *ngTemplateOutlet=\"itemInfo; context: { $implicit: item }\"></ng-container>\n </div>\n\n <!-- Description -->\n @if (showDescription() && item.description) {\n <div class=\"px-4 pt-3\">\n @if (customTemplates()['description']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['description']; context: { $implicit: item }\"></ng-container>\n } @else {\n <p class=\"text-sm _shk-text-surface-600 dark:[color:var(--shk-surface-600)] leading-relaxed\">{{ item.description }}</p>\n }\n </div>\n }\n\n <!-- Sub items -->\n @if (subItems.length > 0) {\n <div class=\"mt-2\">\n @for (sub of subItems; track sub.id) {\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['subItem'] || defaultSubItem; context: { $implicit: item, subItem: sub }\"\n ></ng-container>\n }\n </div>\n } @else if (noSubItemsMessage()) {\n <div class=\"px-4 py-6 text-center\">\n <div class=\"inline-flex items-center gap-2 text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n <svg [lucideIcon]=\"getIcon('info')\" class=\"w-4 h-4\"></svg>\n <span class=\"italic\">{{ noSubItemsMessage() }}</span>\n </div>\n </div>\n }\n\n <!-- Footer actions -->\n @if (showAddSubItem() || itemActions().length > 0) {\n <div class=\"flex items-center gap-1 px-4 py-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border-t _shk-border dark:[border-color:var(--shk-surface-100)]\">\n @if (showAddSubItem()) {\n <button\n type=\"button\"\n (click)=\"addSubItem.emit(item)\"\n class=\"relative group inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium _shk-text-primary hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] rounded-lg transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('circle-plus')\" class=\"w-3.5 h-3.5\"></svg>\n {{ addSubItemLabel() }}\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ addSubItemTooltip() }}\n </span>\n </button>\n }\n <div class=\"flex-1\"></div>\n @for (action of itemActions(); track $index) {\n @if (isItemActionVisible(action, item)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item)\"\n [class]=\"'timeline-icon-btn ' + getItemActionClass(action, item)\"\n [disabled]=\"isItemActionDisabled(action, item)\"\n [attr.aria-label]=\"getItemActionLabel(action, item)\"\n >\n <svg [lucideIcon]=\"getIcon(getItemActionIconName(action, item))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getItemActionLabel(action, item) }}\n </span>\n </div>\n }\n }\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #defaultSubItem let-item let-sub=\"subItem\">\n <div class=\"px-4 py-3 border-t [border-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-700)] hover:[background-color:color-mix(in srgb,var(--shk-primary-50)30%,transparent)] dark:hover:[background-color:color-mix(in srgb,var(--shk-primary-900)10%,transparent)] transition-colors duration-200 group\">\n <div class=\"flex items-start gap-3\">\n <span\n [class]=\"sub.iconClass || 'flex items-center justify-center w-9 h-9 rounded-lg shrink-0 mt-0.5'\"\n [style]=\"sub.iconClass ? undefined : subItemStyle(sub)\"\n >\n <svg [lucideIcon]=\"getIcon(subItemIconName(sub))\" class=\"w-4 h-4\"></svg>\n </span>\n\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2 flex-wrap\">\n @if (sub.optional) {\n <span class=\"text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded _shk-bg-amber-soft _shk-text-amber-600 dark:[color:var(--shk-amber-400)]\">\n {{ sub.optionalLabel || optionalLabel() }}\n </span>\n }\n <span class=\"font-medium _shk-text-surface-800 dark:[color:var(--shk-surface-800)] text-sm truncate\">{{ sub.title }}</span>\n @if (sub.subtitle) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] truncate\">\u00B7 {{ sub.subtitle }}</span>\n }\n </div>\n\n @if (showSubItemMeta() && (sub.meta?.length || 0) > 0) {\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-1 mt-1.5 text-xs _shk-text-surface-500 dark:[color:var(--shk-surface-500)]\">\n @for (m of sub.meta!; track $index) {\n <span class=\"flex items-center gap-1\">\n <svg [lucideIcon]=\"getIcon(m.icon)\" class=\"w-3 h-3\"></svg>\n @if (m.label) {\n <span class=\"font-medium\">{{ m.label }}:</span>\n }\n {{ m.value }}\n </span>\n }\n </div>\n }\n </div>\n\n <div class=\"flex items-center gap-0.5 shrink-0\">\n @if (showSubItemPrice() && sub.price !== undefined && sub.price !== null && sub.price !== '') {\n <span class=\"font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)] text-xs mr-1\">{{ sub.price }}</span>\n }\n @for (action of subItemActions(); track $index) {\n @if (isSubItemActionVisible(action, item, sub)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item, sub)\"\n class=\"timeline-icon-btn opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity\"\n [disabled]=\"isSubItemActionDisabled(action, item, sub)\"\n [attr.aria-label]=\"getSubItemActionLabel(action, item, sub)\"\n >\n <svg [lucideIcon]=\"getIcon(getSubItemActionIconName(action, item, sub))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute top-full left-1/2 -translate-x-1/2 mt-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getSubItemActionLabel(action, item, sub) }}\n </span>\n </div>\n }\n }\n </div>\n </div>\n </div>\n </ng-template>\n\n</div>\n", styles: [":host{display:block}.timeline-icon-btn{padding:.375rem;display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;color:var(--shk-primary-600);transition:background-color .15s ease-in-out,color .15s ease-in-out,opacity .15s ease-in-out}.timeline-icon-btn:hover:not(:disabled){background-color:color-mix(in srgb,var(--shk-primary-50) 60%,transparent);color:var(--shk-primary-700)}.timeline-icon-btn:disabled{opacity:.4;cursor:not-allowed}.timeline-icon-btn svg{width:.875rem!important;height:.875rem!important}.shk-timeline-card{animation:shk-timeline-fade-in .3s ease-out}@keyframes shk-timeline-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3757
4352
  }
3758
4353
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: TimelineComponent, decorators: [{
3759
4354
  type: Component,
3760
- args: [{ selector: 'shk-timeline', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, LucideDynamicIcon], template: "<div class=\"space-y-4\">\n\n <!-- ============ Header ============ -->\n @if (showHeader() && (headerTitle() || showAddItem() || headerActions().length > 0)) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n @if (headerTitle()) {\n <div class=\"text-sm font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n {{ headerTitle() }}\n </div>\n }\n <div class=\"flex items-center gap-1.5 ml-auto\">\n @if (showAddItem()) {\n <button\n type=\"button\"\n (click)=\"addItem.emit()\"\n [attr.aria-label]=\"addItemTooltip()\"\n class=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon(addItemIcon())\" class=\"w-3.5 h-3.5\"></svg>\n {{ addItemLabel() }}\n </button>\n }\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <button\n type=\"button\"\n [class]=\"action.class || 'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed'\"\n [disabled]=\"isHeaderActionDisabled(action)\"\n (click)=\"action.onClick()\"\n >\n @if (action.icon) {\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n }\n {{ action.label }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- ============ Timeline ============ -->\n @if (items().length > 0) {\n <div class=\"relative\">\n @if (showLine()) {\n <div [class]=\"lineClass()\" aria-hidden=\"true\"></div>\n }\n\n @for (item of items(); track item.id; let i = $index) {\n @if (alignment() === 'alternate') {\n <div class=\"relative flex items-start gap-4 lg:grid lg:grid-cols-2 lg:gap-0\">\n <!-- Marker (centered on desktop) -->\n <div [class]=\"alternateMarkerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n\n <!-- Content card -->\n <div\n class=\"flex-1 min-w-0\"\n [class.lg:order-1]=\"$even\"\n [class.lg:order-2]=\"!$even\"\n [class.lg:pr-12]=\"$even\"\n [class.lg:pl-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n\n <!-- Opposite info -->\n @if (showOpposite()) {\n <div\n class=\"hidden lg:flex lg:items-center\"\n [class.lg:order-2]=\"$even\"\n [class.lg:order-1]=\"!$even\"\n [class.lg:justify-end]=\"$even\"\n [class.lg:pl-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['opposite'] || itemInfo; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n }\n </div>\n } @else {\n <!-- Left alignment -->\n <div class=\"relative flex items-start gap-4\">\n <div [class]=\"markerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n </div>\n }\n }\n </div>\n } @else if (showEmptyState()) {\n <!-- ============ Empty state ============ -->\n <div class=\"text-center py-12\">\n @if (customTemplates()['empty']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['empty']\"></ng-container>\n } @else {\n <div class=\"w-16 h-16 mx-auto mb-4 _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] rounded-full flex items-center justify-center\">\n <svg [lucideIcon]=\"getIcon(emptyIcon())\" class=\"w-8 h-8 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"></svg>\n </div>\n <p class=\"text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">{{ emptyMessage() }}</p>\n }\n </div>\n }\n\n <!-- ============ Templates ============ -->\n\n <ng-template #defaultMarker let-item let-index=\"index\">\n <span class=\"font-bold\">{{ markerText(item, index) }}</span>\n </ng-template>\n\n <ng-template #itemInfo let-item>\n <div>\n @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {\n <div class=\"mb-2\">\n <span class=\"inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\">\n @if (item.badgeIcon) {\n <svg [lucideIcon]=\"getIcon(item.badgeIcon)\" class=\"w-3 h-3\"></svg>\n }\n {{ item.badge }}\n </span>\n </div>\n }\n @if (item.title) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">{{ item.title }}</div>\n }\n @let subCount = (item.subItems?.length || 0);\n @if (item.subtitle || (showSubItemCount() && subCount > 0)) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] mt-1\">\n {{ item.subtitle || (subCount + ' ' + subItemCountLabel()) }}\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #card let-item>\n @let subItems = item.subItems || [];\n <div class=\"shk-timeline-card mb-4 bg-white dark:[background-color:var(--shk-surface-50)] rounded-xl border _shk-border dark:[border-color:var(--shk-surface-200)] shadow-sm overflow-hidden\">\n <!-- Header (mobile in alternate, always in left) -->\n <div class=\"flex items-center gap-2 px-4 pt-4 pb-2\" [class.lg:hidden]=\"alignment() === 'alternate'\">\n <ng-container *ngTemplateOutlet=\"itemInfo; context: { $implicit: item }\"></ng-container>\n </div>\n\n <!-- Description -->\n @if (showDescription() && item.description) {\n <div class=\"px-4 pt-3\">\n @if (customTemplates()['description']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['description']; context: { $implicit: item }\"></ng-container>\n } @else {\n <p class=\"text-sm _shk-text-surface-600 dark:[color:var(--shk-surface-600)] leading-relaxed\">{{ item.description }}</p>\n }\n </div>\n }\n\n <!-- Sub items -->\n @if (subItems.length > 0) {\n <div class=\"mt-2\">\n @for (sub of subItems; track sub.id) {\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['subItem'] || defaultSubItem; context: { $implicit: item, subItem: sub }\"\n ></ng-container>\n }\n </div>\n } @else if (noSubItemsMessage()) {\n <div class=\"px-4 py-6 text-center\">\n <div class=\"inline-flex items-center gap-2 text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n <svg [lucideIcon]=\"getIcon('info')\" class=\"w-4 h-4\"></svg>\n <span class=\"italic\">{{ noSubItemsMessage() }}</span>\n </div>\n </div>\n }\n\n <!-- Footer actions -->\n @if (showAddSubItem() || itemActions().length > 0) {\n <div class=\"flex items-center gap-1 px-4 py-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border-t _shk-border dark:[border-color:var(--shk-surface-100)]\">\n @if (showAddSubItem()) {\n <button\n type=\"button\"\n (click)=\"addSubItem.emit(item)\"\n class=\"relative group inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium _shk-text-primary hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] rounded-lg transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('circle-plus')\" class=\"w-3.5 h-3.5\"></svg>\n {{ addSubItemLabel() }}\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ addSubItemTooltip() }}\n </span>\n </button>\n }\n <div class=\"flex-1\"></div>\n @for (action of itemActions(); track $index) {\n @if (isItemActionVisible(action, item)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item)\"\n [class]=\"'timeline-icon-btn ' + getItemActionClass(action, item)\"\n [disabled]=\"isItemActionDisabled(action, item)\"\n [attr.aria-label]=\"getItemActionLabel(action, item)\"\n >\n <svg [lucideIcon]=\"getIcon(getItemActionIconName(action, item))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getItemActionLabel(action, item) }}\n </span>\n </div>\n }\n }\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #defaultSubItem let-item let-sub=\"subItem\">\n <div class=\"px-4 py-3 border-t [border-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-700)] hover:[background-color:color-mix(in srgb,var(--shk-primary-50)30%,transparent)] dark:hover:[background-color:color-mix(in srgb,var(--shk-primary-900)10%,transparent)] transition-colors duration-200 group\">\n <div class=\"flex items-start gap-3\">\n <span\n [class]=\"sub.iconClass || 'flex items-center justify-center w-9 h-9 rounded-lg shrink-0 mt-0.5'\"\n [style]=\"sub.iconClass ? undefined : subItemStyle(sub)\"\n >\n <svg [lucideIcon]=\"getIcon(subItemIconName(sub))\" class=\"w-4 h-4\"></svg>\n </span>\n\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2 flex-wrap\">\n @if (sub.optional) {\n <span class=\"text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded _shk-bg-amber-soft _shk-text-amber-600 dark:[color:var(--shk-amber-400)]\">\n {{ sub.optionalLabel || optionalLabel() }}\n </span>\n }\n <span class=\"font-medium _shk-text-surface-800 dark:[color:var(--shk-surface-800)] text-sm truncate\">{{ sub.title }}</span>\n @if (sub.subtitle) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] truncate\">\u00B7 {{ sub.subtitle }}</span>\n }\n </div>\n\n @if (showSubItemMeta() && (sub.meta?.length || 0) > 0) {\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-1 mt-1.5 text-xs _shk-text-surface-500 dark:[color:var(--shk-surface-500)]\">\n @for (m of sub.meta!; track $index) {\n <span class=\"flex items-center gap-1\">\n <svg [lucideIcon]=\"getIcon(m.icon)\" class=\"w-3 h-3\"></svg>\n @if (m.label) {\n <span class=\"font-medium\">{{ m.label }}:</span>\n }\n {{ m.value }}\n </span>\n }\n </div>\n }\n </div>\n\n <div class=\"flex items-center gap-0.5 shrink-0\">\n @if (showSubItemPrice() && sub.price !== undefined && sub.price !== null && sub.price !== '') {\n <span class=\"font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)] text-xs mr-1\">{{ sub.price }}</span>\n }\n @for (action of subItemActions(); track $index) {\n @if (isSubItemActionVisible(action, item, sub)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item, sub)\"\n class=\"timeline-icon-btn opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity\"\n [disabled]=\"isSubItemActionDisabled(action, item, sub)\"\n [attr.aria-label]=\"getSubItemActionLabel(action, item, sub)\"\n >\n <svg [lucideIcon]=\"getIcon(getSubItemActionIconName(action, item, sub))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute top-full left-1/2 -translate-x-1/2 mt-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getSubItemActionLabel(action, item, sub) }}\n </span>\n </div>\n }\n }\n </div>\n </div>\n </div>\n </ng-template>\n\n</div>\n", styles: [":host{display:block}.timeline-icon-btn{padding:.375rem;display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;color:var(--shk-primary-600);transition:background-color .15s ease-in-out,color .15s ease-in-out,opacity .15s ease-in-out}.timeline-icon-btn:hover:not(:disabled){background-color:color-mix(in srgb,var(--shk-primary-50) 60%,transparent);color:var(--shk-primary-700)}.timeline-icon-btn:disabled{opacity:.4;cursor:not-allowed}.timeline-icon-btn svg{width:.875rem!important;height:.875rem!important}.shk-timeline-card{animation:shk-timeline-fade-in .3s ease-out}@keyframes shk-timeline-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n"] }]
4355
+ args: [{ selector: 'shk-timeline', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, LucideDynamicIcon], template: "<div class=\"space-y-4\">\n\n <!-- ============ Header ============ -->\n @if (showHeader() && (headerTitle() || showAddItem() || headerActions().length > 0)) {\n <div class=\"flex flex-wrap items-center justify-between gap-2\">\n @if (headerTitle()) {\n <div class=\"text-sm font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">\n {{ headerTitle() }}\n </div>\n }\n <div class=\"flex items-center gap-1.5 ml-auto\">\n @if (showAddItem()) {\n <button\n type=\"button\"\n (click)=\"addItem.emit()\"\n [attr.aria-label]=\"addItemTooltip()\"\n class=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon(addItemIcon())\" class=\"w-3.5 h-3.5\"></svg>\n {{ addItemLabel() }}\n </button>\n }\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <button\n type=\"button\"\n [class]=\"action.class || 'inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md _shk-bg-primary text-white hover:[background-color:var(--shk-primary-700)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed'\"\n [disabled]=\"isHeaderActionDisabled(action)\"\n (click)=\"action.onClick()\"\n >\n @if (action.icon) {\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n }\n {{ action.label }}\n </button>\n }\n }\n </div>\n </div>\n }\n\n <!-- ============ Timeline ============ -->\n @if (items().length > 0) {\n <div class=\"relative\">\n @if (showLine()) {\n <div [class]=\"lineClass()\" aria-hidden=\"true\"></div>\n }\n\n @for (item of items(); track item.id; let i = $index) {\n @if (alignment() === 'alternate') {\n <div class=\"relative flex items-start gap-4 lg:grid lg:grid-cols-2 lg:gap-0\">\n <!-- Marker (centered on desktop) -->\n <div [class]=\"alternateMarkerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n\n <!-- Content card -->\n <div\n class=\"flex-1 min-w-0\"\n [class.lg:order-1]=\"$even\"\n [class.lg:order-2]=\"!$even\"\n [class.lg:pr-12]=\"$even\"\n [class.lg:pl-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n\n <!-- Opposite info -->\n @if (showOpposite()) {\n <div\n class=\"hidden lg:flex lg:items-center\"\n [class.lg:order-2]=\"$even\"\n [class.lg:order-1]=\"!$even\"\n [class.lg:pl-12]=\"$even\"\n [class.lg:justify-end]=\"!$even\"\n [class.lg:pr-12]=\"!$even\"\n >\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['opposite'] || itemInfo; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n }\n </div>\n } @else {\n <!-- Left alignment -->\n <div class=\"relative flex items-start gap-4\">\n <div [class]=\"markerColumnClass()\">\n <span [class]=\"markerClass(item, i)\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['marker'] || defaultMarker; context: { $implicit: item, index: i }\"\n ></ng-container>\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['content'] || card; context: { $implicit: item, index: i }\"\n ></ng-container>\n </div>\n </div>\n }\n }\n </div>\n } @else if (showEmptyState()) {\n <!-- ============ Empty state ============ -->\n <div class=\"text-center py-12\">\n @if (customTemplates()['empty']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['empty']\"></ng-container>\n } @else {\n <div class=\"w-16 h-16 mx-auto mb-4 _shk-bg-surface-100 dark:[background-color:var(--shk-surface-100)] rounded-full flex items-center justify-center\">\n <svg [lucideIcon]=\"getIcon(emptyIcon())\" class=\"w-8 h-8 _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\"></svg>\n </div>\n <p class=\"text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">{{ emptyMessage() }}</p>\n }\n </div>\n }\n\n <!-- ============ Templates ============ -->\n\n <ng-template #defaultMarker let-item let-index=\"index\">\n <span class=\"font-bold\">{{ markerText(item, index) }}</span>\n </ng-template>\n\n <ng-template #itemInfo let-item>\n <div>\n @if (item.badge !== undefined && item.badge !== null && item.badge !== '') {\n <div class=\"mb-2\">\n <span class=\"inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold _shk-bg-surface-100 dark:[background-color:var(--shk-surface-200)] _shk-text-surface-600 dark:[color:var(--shk-surface-600)]\">\n @if (item.badgeIcon) {\n <svg [lucideIcon]=\"getIcon(item.badgeIcon)\" class=\"w-3 h-3\"></svg>\n }\n {{ item.badge }}\n </span>\n </div>\n }\n @if (item.title) {\n <div class=\"text-sm font-medium _shk-text-surface-700 dark:[color:var(--shk-surface-700)]\">{{ item.title }}</div>\n }\n @let subCount = (item.subItems?.length || 0);\n @if (item.subtitle || (showSubItemCount() && subCount > 0)) {\n <div class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] mt-1\">\n {{ item.subtitle || (subCount + ' ' + subItemCountLabel()) }}\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #card let-item>\n @let subItems = item.subItems || [];\n <div class=\"shk-timeline-card mb-4 bg-white dark:[background-color:var(--shk-surface-50)] rounded-xl border _shk-border dark:[border-color:var(--shk-surface-200)] shadow-sm overflow-hidden\">\n <!-- Header (mobile in alternate, always in left) -->\n <div class=\"flex items-center gap-2 px-4 pt-4 pb-2\" [class.lg:hidden]=\"alignment() === 'alternate'\">\n <ng-container *ngTemplateOutlet=\"itemInfo; context: { $implicit: item }\"></ng-container>\n </div>\n\n <!-- Description -->\n @if (showDescription() && item.description) {\n <div class=\"px-4 pt-3\">\n @if (customTemplates()['description']) {\n <ng-container *ngTemplateOutlet=\"customTemplates()['description']; context: { $implicit: item }\"></ng-container>\n } @else {\n <p class=\"text-sm _shk-text-surface-600 dark:[color:var(--shk-surface-600)] leading-relaxed\">{{ item.description }}</p>\n }\n </div>\n }\n\n <!-- Sub items -->\n @if (subItems.length > 0) {\n <div class=\"mt-2\">\n @for (sub of subItems; track sub.id) {\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['subItem'] || defaultSubItem; context: { $implicit: item, subItem: sub }\"\n ></ng-container>\n }\n </div>\n } @else if (noSubItemsMessage()) {\n <div class=\"px-4 py-6 text-center\">\n <div class=\"inline-flex items-center gap-2 text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-500)]\">\n <svg [lucideIcon]=\"getIcon('info')\" class=\"w-4 h-4\"></svg>\n <span class=\"italic\">{{ noSubItemsMessage() }}</span>\n </div>\n </div>\n }\n\n <!-- Footer actions -->\n @if (showAddSubItem() || itemActions().length > 0) {\n <div class=\"flex items-center gap-1 px-4 py-2 _shk-bg-surface-50 dark:[background-color:var(--shk-surface-100)] border-t _shk-border dark:[border-color:var(--shk-surface-100)]\">\n @if (showAddSubItem()) {\n <button\n type=\"button\"\n (click)=\"addSubItem.emit(item)\"\n class=\"relative group inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium _shk-text-primary hover:[background-color:var(--shk-surface-100)] dark:hover:[background-color:var(--shk-surface-200)] rounded-lg transition-colors\"\n >\n <svg [lucideIcon]=\"getIcon('circle-plus')\" class=\"w-3.5 h-3.5\"></svg>\n {{ addSubItemLabel() }}\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ addSubItemTooltip() }}\n </span>\n </button>\n }\n <div class=\"flex-1\"></div>\n @for (action of itemActions(); track $index) {\n @if (isItemActionVisible(action, item)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item)\"\n [class]=\"'timeline-icon-btn ' + getItemActionClass(action, item)\"\n [disabled]=\"isItemActionDisabled(action, item)\"\n [attr.aria-label]=\"getItemActionLabel(action, item)\"\n >\n <svg [lucideIcon]=\"getIcon(getItemActionIconName(action, item))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getItemActionLabel(action, item) }}\n </span>\n </div>\n }\n }\n </div>\n }\n </div>\n </ng-template>\n\n <ng-template #defaultSubItem let-item let-sub=\"subItem\">\n <div class=\"px-4 py-3 border-t [border-color:var(--shk-surface-100)] dark:[border-color:var(--shk-surface-700)] hover:[background-color:color-mix(in srgb,var(--shk-primary-50)30%,transparent)] dark:hover:[background-color:color-mix(in srgb,var(--shk-primary-900)10%,transparent)] transition-colors duration-200 group\">\n <div class=\"flex items-start gap-3\">\n <span\n [class]=\"sub.iconClass || 'flex items-center justify-center w-9 h-9 rounded-lg shrink-0 mt-0.5'\"\n [style]=\"sub.iconClass ? undefined : subItemStyle(sub)\"\n >\n <svg [lucideIcon]=\"getIcon(subItemIconName(sub))\" class=\"w-4 h-4\"></svg>\n </span>\n\n <div class=\"flex-1 min-w-0\">\n <div class=\"flex items-center gap-2 flex-wrap\">\n @if (sub.optional) {\n <span class=\"text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded _shk-bg-amber-soft _shk-text-amber-600 dark:[color:var(--shk-amber-400)]\">\n {{ sub.optionalLabel || optionalLabel() }}\n </span>\n }\n <span class=\"font-medium _shk-text-surface-800 dark:[color:var(--shk-surface-800)] text-sm truncate\">{{ sub.title }}</span>\n @if (sub.subtitle) {\n <span class=\"text-xs _shk-text-surface-400 dark:[color:var(--shk-surface-500)] truncate\">\u00B7 {{ sub.subtitle }}</span>\n }\n </div>\n\n @if (showSubItemMeta() && (sub.meta?.length || 0) > 0) {\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-1 mt-1.5 text-xs _shk-text-surface-500 dark:[color:var(--shk-surface-500)]\">\n @for (m of sub.meta!; track $index) {\n <span class=\"flex items-center gap-1\">\n <svg [lucideIcon]=\"getIcon(m.icon)\" class=\"w-3 h-3\"></svg>\n @if (m.label) {\n <span class=\"font-medium\">{{ m.label }}:</span>\n }\n {{ m.value }}\n </span>\n }\n </div>\n }\n </div>\n\n <div class=\"flex items-center gap-0.5 shrink-0\">\n @if (showSubItemPrice() && sub.price !== undefined && sub.price !== null && sub.price !== '') {\n <span class=\"font-semibold _shk-text-surface-700 dark:[color:var(--shk-surface-700)] text-xs mr-1\">{{ sub.price }}</span>\n }\n @for (action of subItemActions(); track $index) {\n @if (isSubItemActionVisible(action, item, sub)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick(item, sub)\"\n class=\"timeline-icon-btn opacity-100 lg:opacity-0 lg:group-hover:opacity-100 transition-opacity\"\n [disabled]=\"isSubItemActionDisabled(action, item, sub)\"\n [attr.aria-label]=\"getSubItemActionLabel(action, item, sub)\"\n >\n <svg [lucideIcon]=\"getIcon(getSubItemActionIconName(action, item, sub))\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute top-full left-1/2 -translate-x-1/2 mt-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ getSubItemActionLabel(action, item, sub) }}\n </span>\n </div>\n }\n }\n </div>\n </div>\n </div>\n </ng-template>\n\n</div>\n", styles: [":host{display:block}.timeline-icon-btn{padding:.375rem;display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;color:var(--shk-primary-600);transition:background-color .15s ease-in-out,color .15s ease-in-out,opacity .15s ease-in-out}.timeline-icon-btn:hover:not(:disabled){background-color:color-mix(in srgb,var(--shk-primary-50) 60%,transparent);color:var(--shk-primary-700)}.timeline-icon-btn:disabled{opacity:.4;cursor:not-allowed}.timeline-icon-btn svg{width:.875rem!important;height:.875rem!important}.shk-timeline-card{animation:shk-timeline-fade-in .3s ease-out}@keyframes shk-timeline-fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n"] }]
3761
4356
  }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], showLine: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLine", required: false }] }], markerSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "markerSize", required: false }] }], markerGradients: [{ type: i0.Input, args: [{ isSignal: true, alias: "markerGradients", required: false }] }], showOpposite: [{ type: i0.Input, args: [{ isSignal: true, alias: "showOpposite", required: false }] }], showDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDescription", required: false }] }], showSubItemMeta: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSubItemMeta", required: false }] }], showSubItemPrice: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSubItemPrice", required: false }] }], showSubItemCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSubItemCount", required: false }] }], subItemCountLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "subItemCountLabel", required: false }] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], headerTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerTitle", required: false }] }], showAddItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAddItem", required: false }] }], addItemLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "addItemLabel", required: false }] }], addItemIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addItemIcon", required: false }] }], addItemTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "addItemTooltip", required: false }] }], headerActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerActions", required: false }] }], showAddSubItem: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAddSubItem", required: false }] }], addSubItemLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "addSubItemLabel", required: false }] }], addSubItemTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "addSubItemTooltip", required: false }] }], itemActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemActions", required: false }] }], subItemActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "subItemActions", required: false }] }], iconMap: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconMap", required: false }] }], iconColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconColors", required: false }] }], fallbackIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackIcon", required: false }] }], showEmptyState: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEmptyState", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], editTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "editTooltip", required: false }] }], deleteTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "deleteTooltip", required: false }] }], optionalLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionalLabel", required: false }] }], noSubItemsMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "noSubItemsMessage", required: false }] }], customTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "customTemplates", required: false }] }], addItem: [{ type: i0.Output, args: ["addItem"] }], addSubItem: [{ type: i0.Output, args: ["addSubItem"] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], subItemClick: [{ type: i0.Output, args: ["subItemClick"] }] } });
3762
4357
 
3763
- const icons$1 = {
3764
- 'chevron-down': LucideChevronDown,
3765
- 'chevron-right': LucideChevronRight,
3766
- plus: LucidePlus,
3767
- pencil: LucidePencil,
3768
- 'trash-2': LucideTrash2,
3769
- info: LucideInfo,
3770
- };
3771
- function getIcon$1(name) {
3772
- return icons$1[name] ?? LucideChevronRight;
3773
- }
3774
4358
  let fieldsetCounter = 0;
4359
+ /**
4360
+ * Shirkasoft UI Components.
4361
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
4362
+ * Belongs to Shirkasoft.
4363
+ */
4364
+ /**
4365
+ * Collapsible content group with a legend header, optional leading icon,
4366
+ * badge and action buttons on the right side.
4367
+ */
3775
4368
  class FieldsetComponent {
3776
4369
  constructor() {
3777
- this.getIcon = getIcon$1;
4370
+ this.getIcon = (name) => getIcon(name, LucideChevronRight);
3778
4371
  /** Title shown in the header. */
3779
4372
  this.legend = input('', ...(ngDevMode ? [{ debugName: "legend" }] : /* istanbul ignore next */ []));
3780
4373
  /** Collapsed state (default: collapsed). Two-way bindable with [(collapsed)]. */
@@ -3797,15 +4390,22 @@ class FieldsetComponent {
3797
4390
  this.headerActions = input([], ...(ngDevMode ? [{ debugName: "headerActions" }] : /* istanbul ignore next */ []));
3798
4391
  /** Custom content templates: key 'legend' (context: $implicit collapsed, expanded). */
3799
4392
  this.customTemplates = input({}, ...(ngDevMode ? [{ debugName: "customTemplates" }] : /* istanbul ignore next */ []));
4393
+ /** CSS classes for the extra shadow styling of the panel. */
3800
4394
  this.hasShadow = input(true, ...(ngDevMode ? [{ debugName: "hasShadow" }] : /* istanbul ignore next */ []));
4395
+ /** Accessible label for the expand action. */
3801
4396
  this.expandLabel = input('Expandir', ...(ngDevMode ? [{ debugName: "expandLabel" }] : /* istanbul ignore next */ []));
4397
+ /** Accessible label for the collapse action. */
3802
4398
  this.collapseLabel = input('Colapsar', ...(ngDevMode ? [{ debugName: "collapseLabel" }] : /* istanbul ignore next */ []));
3803
4399
  /** Emits the new expanded state when toggled. */
3804
4400
  this.toggle = output();
4401
+ /** Emitted when the fieldset is expanded. */
3805
4402
  this.expand = output();
4403
+ /** Emitted when the fieldset is collapsed. */
3806
4404
  this.collapse = output();
4405
+ /** Stable id linking the header button and the content region. */
3807
4406
  this.contentId = `shk-fieldset-content-${++fieldsetCounter}`;
3808
4407
  }
4408
+ /** Toggles the collapsed state and emits the new state (unless not toggleable). */
3809
4409
  onToggle() {
3810
4410
  if (!this.toggleable())
3811
4411
  return;
@@ -3820,9 +4420,11 @@ class FieldsetComponent {
3820
4420
  this.collapse.emit();
3821
4421
  }
3822
4422
  }
4423
+ /** Whether a header action can be rendered. */
3823
4424
  isHeaderActionVisible(action) {
3824
4425
  return action.isVisible ? action.isVisible() : true;
3825
4426
  }
4427
+ /** Whether a header action should be disabled. */
3826
4428
  isHeaderActionDisabled(action) {
3827
4429
  return action.isDisabled ? action.isDisabled() : false;
3828
4430
  }
@@ -3834,31 +4436,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
3834
4436
  args: [{ selector: 'shk-fieldset', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, LucideDynamicIcon], template: "<div\n class=\"rounded-xl bg-white dark:[background-color:var(--shk-surface-50)] border _shk-border dark:[border-color:var(--shk-surface-200)] overflow-hidden\"\n [class.shadow-sm]=\"hasShadow()\"\n>\n <div\n class=\"flex items-center gap-2 px-4 py-3\"\n [class.border-b]=\"!collapsed()\"\n [class._shk-border]=\"!collapsed()\"\n [class.dark:[border-color:var(--shk-surface-200)]]=\"!collapsed()\"\n >\n <button\n type=\"button\"\n (click)=\"onToggle()\"\n [disabled]=\"!toggleable()\"\n [attr.aria-expanded]=\"!collapsed()\"\n [attr.aria-controls]=\"contentId\"\n class=\"flex items-center gap-2 flex-1 min-w-0 text-left group disabled:cursor-default\"\n >\n @if (customTemplates()['legend']) {\n <ng-container\n *ngTemplateOutlet=\"customTemplates()['legend']; context: { $implicit: collapsed(), expanded: !collapsed() }\"\n ></ng-container>\n } @else {\n @if (icon()) {\n <svg [lucideIcon]=\"getIcon(icon())\" class=\"w-4 h-4 _shk-text-primary shrink-0\"></svg>\n }\n <span class=\"font-semibold text-sm _shk-text-surface-800 dark:[color:var(--shk-surface-800)] truncate\">\n {{ legend() }}\n </span>\n @if (badge() !== undefined && badge() !== null && badge() !== '') {\n <span\n [class]=\"badgeClass() || 'inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold _shk-bg-primary-soft _shk-text-primary-700 dark:[background-color:var(--shk-primary-900)] dark:[color:var(--shk-primary-300)] shrink-0'\"\n >\n {{ badge() }}\n </span>\n }\n }\n </button>\n\n @if (headerActions().length > 0) {\n <div class=\"flex items-center gap-0.5 shrink-0\">\n @for (action of headerActions(); track $index) {\n @if (isHeaderActionVisible(action)) {\n <div class=\"relative group\">\n <button\n type=\"button\"\n (click)=\"action.onClick()\"\n [class]=\"'fieldset-action-btn ' + (action.class || '')\"\n [disabled]=\"isHeaderActionDisabled(action)\"\n [attr.aria-label]=\"action.label\"\n >\n <svg [lucideIcon]=\"getIcon(action.icon)\" class=\"w-3.5 h-3.5\"></svg>\n </button>\n <span class=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-2 py-1 text-xs _shk-bg-tooltip _shk-text-tooltip rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition pointer-events-none z-[9999] shadow-lg\">\n {{ action.label }}\n </span>\n </div>\n }\n }\n </div>\n }\n\n @if (toggleable() && showToggleIcon()) {\n <button\n type=\"button\"\n (click)=\"onToggle()\"\n [attr.aria-label]=\"collapsed() ? expandLabel() : collapseLabel()\"\n class=\"shk-fieldset-toggle shrink-0\"\n [disabled]=\"!toggleable()\"\n >\n <svg [lucideIcon]=\"getIcon(collapsed() ? collapsedIcon() : expandedIcon())\" class=\"w-4 h-4 transition-transform\"></svg>\n </button>\n }\n </div>\n\n @if (!collapsed()) {\n <div [id]=\"contentId\" class=\"shk-fieldset-content px-4 py-3.5\">\n <ng-content></ng-content>\n </div>\n }\n</div>\n", styles: [":host{display:block}.shk-fieldset-toggle{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:.5rem;color:var(--shk-surface-400);transition:background-color .15s ease-in-out,color .15s ease-in-out}.shk-fieldset-toggle:hover:not(:disabled){background-color:var(--shk-surface-100);color:var(--shk-surface-700)}.shk-fieldset-toggle:disabled{cursor:default}.shk-fieldset-toggle svg{width:1rem!important;height:1rem!important}.fieldset-action-btn{display:inline-flex;align-items:center;justify-content:center;padding:.375rem;border-radius:.5rem;color:var(--shk-primary-600);transition:background-color .15s ease-in-out,color .15s ease-in-out,opacity .15s ease-in-out}.fieldset-action-btn:hover:not(:disabled){background-color:color-mix(in srgb,var(--shk-primary-50) 60%,transparent);color:var(--shk-primary-700)}.fieldset-action-btn:disabled{opacity:.4;cursor:not-allowed}.fieldset-action-btn svg{width:.875rem!important;height:.875rem!important}.shk-fieldset-content{animation:shk-fieldset-fade .25s ease-out}@keyframes shk-fieldset-fade{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n"] }]
3835
4437
  }], propDecorators: { legend: [{ type: i0.Input, args: [{ isSignal: true, alias: "legend", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], toggleable: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleable", required: false }] }], showToggleIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToggleIcon", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeClass", required: false }] }], collapsedIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedIcon", required: false }] }], expandedIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedIcon", required: false }] }], headerActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerActions", required: false }] }], customTemplates: [{ type: i0.Input, args: [{ isSignal: true, alias: "customTemplates", required: false }] }], hasShadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasShadow", required: false }] }], expandLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandLabel", required: false }] }], collapseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseLabel", required: false }] }], toggle: [{ type: i0.Output, args: ["toggle"] }], expand: [{ type: i0.Output, args: ["expand"] }], collapse: [{ type: i0.Output, args: ["collapse"] }] } });
3836
4438
 
3837
- const icons = {
3838
- check: LucideCheck,
3839
- 'chevron-left': LucideChevronLeft,
3840
- 'chevron-right': LucideChevronRight,
3841
- 'arrow-left': LucideArrowLeft,
3842
- x: LucideX,
3843
- 'loader-circle': LucideLoaderCircle,
3844
- 'file-contract': LucideScrollText,
3845
- building: LucideBuilding,
3846
- 'user-round': LucideUserRound,
3847
- info: LucideInfo,
3848
- 'clipboard-check': LucideClipboardCheck,
3849
- package: LucidePackage,
3850
- hotel: LucideHotel,
3851
- map: LucideMap,
3852
- tag: LucideTag,
3853
- utensils: LucideUtensils,
3854
- coins: LucideCoins,
3855
- 'sticky-note': LucideStickyNote,
3856
- 'circle-exclamation': LucideCircleAlert,
3857
- 'circle-alert': LucideCircleAlert,
3858
- };
3859
- function getIcon(name) {
3860
- return icons[name] ?? LucideChevronRight;
3861
- }
3862
4439
  /** Declares a step's content template: <ng-template shkWizardStepContent="stepId">…</ng-template>. */
3863
4440
  class ShkWizardStepContent {
3864
4441
  constructor(templateRef) {
@@ -3876,9 +4453,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
3876
4453
  }]
3877
4454
  }], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { stepId: [{ type: i0.Input, args: [{ isSignal: true, alias: "shkWizardStepContent", required: true }] }] } });
3878
4455
  let wizardCounter = 0;
4456
+ /**
4457
+ * Shirkasoft UI Components.
4458
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
4459
+ * Belongs to Shirkasoft.
4460
+ */
4461
+ /**
4462
+ * Multi-step wizard with a progress stepper, header, footer actions and
4463
+ * content templates declared with `shkWizardStepContent`.
4464
+ */
3879
4465
  class WizardComponent {
3880
4466
  constructor() {
3881
- this.getIcon = getIcon;
4467
+ this.getIcon = (name) => getIcon(name, LucideChevronRight);
3882
4468
  /** Steps definition shown in the stepper and used to resolve step content. */
3883
4469
  this.steps = input([], ...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
3884
4470
  /** Active step id. Two-way bindable with [(currentStep)]. Null resolves to the first step. */
@@ -4004,6 +4590,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4004
4590
  args: [{ selector: 'shk-wizard', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, LucideDynamicIcon], template: "<div class=\"min-h-full bg-transparent\">\n\n <!-- ============ Sticky Header + Stepper ============ -->\n @if (showHeader()) {\n <div\n class=\"sticky top-0 z-20 bg-white dark:[background-color:var(--shk-surface-50)] border-b _shk-border dark:[border-color:var(--shk-surface-200)] shadow-sm\"\n >\n <div class=\"w-full px-4 sm:px-6 lg:px-8 py-4\">\n <div class=\"flex items-center gap-4 mb-4\">\n @if (showBackButton()) {\n <button\n type=\"button\"\n (click)=\"back.emit()\"\n [attr.aria-label]=\"backTooltip()\"\n [attr.title]=\"backTooltip()\"\n class=\"shk-wizard-icon-btn shrink-0\"\n >\n <svg [lucideIcon]=\"getIcon('arrow-left')\" class=\"w-4 h-4\"></svg>\n </button>\n }\n <div class=\"flex-1 min-w-0 flex items-center gap-2.5\">\n @if (headerIcon()) {\n <span class=\"w-9 h-9 rounded-lg _shk-bg-primary-soft _shk-text-primary flex items-center justify-center shrink-0\">\n <svg [lucideIcon]=\"getIcon(headerIcon())\" class=\"w-4 h-4\"></svg>\n </span>\n }\n <div class=\"min-w-0\">\n <h1 class=\"text-xl font-bold _shk-text-surface-800 dark:[color:var(--shk-surface-800)] truncate\">\n {{ headerTitle() }}\n </h1>\n @if (headerSubtitle()) {\n <p class=\"text-xs _shk-text-surface-500 dark:[color:var(--shk-surface-500)] mt-0.5 truncate\">\n {{ headerSubtitle() }}\n </p>\n }\n </div>\n </div>\n <ng-content select=\"[shkWizardHeaderActions]\"></ng-content>\n </div>\n\n <!-- Stepper -->\n @if (showStepper() && steps().length > 1) {\n <div class=\"flex items-center gap-2 w-full max-w-3xl mx-auto\">\n @for (step of steps(); track step.id; let i = $index, last = $last) {\n <div class=\"flex items-center gap-3 shrink-0\">\n <button\n type=\"button\"\n (click)=\"goTo(step.id)\"\n [disabled]=\"!stepperClickable() || currentIndex() < i\"\n [attr.aria-label]=\"step.label\"\n class=\"shk-wizard-stepper-dot\"\n [class._shk-bg-primary]=\"currentIndex() > i || currentIndex() === i\"\n [class.text-white]=\"currentIndex() > i || currentIndex() === i\"\n [class._shk-bg-surface-200]=\"currentIndex() < i\"\n [class._shk-text-surface-500]=\"currentIndex() < i\"\n [class.cursor-pointer]=\"stepperClickable() && currentIndex() >= i\"\n [class.cursor-default]=\"!stepperClickable() || currentIndex() < i\"\n >\n @if (currentIndex() > i) {\n <svg [lucideIcon]=\"getIcon(step.icon || 'check')\" class=\"w-4 h-4\"></svg>\n } @else if (step.icon) {\n <svg [lucideIcon]=\"getIcon(step.icon)\" class=\"w-4 h-4\"></svg>\n } @else {\n {{ i + 1 }}\n }\n </button>\n <span\n class=\"text-sm font-semibold transition-colors whitespace-nowrap hidden sm:inline\"\n [class._shk-text-surface-800]=\"currentIndex() === i\"\n [class._shk-text-surface-500]=\"currentIndex() !== i\"\n [class.dark:[color:var(--shk-surface-800)]]=\"currentIndex() === i\"\n [class.dark:[color:var(--shk-surface-500)]]=\"currentIndex() !== i\"\n >\n {{ step.label }}\n </span>\n </div>\n\n @if (!last) {\n <div class=\"flex-1 h-0.5 _shk-bg-surface-200 dark:[background-color:var(--shk-surface-200)] relative overflow-hidden rounded-full\">\n <div\n class=\"absolute inset-y-0 left-0 _shk-bg-primary transition-all duration-300\"\n [class.w-full]=\"currentIndex() > i\"\n [class.w-0]=\"currentIndex() <= i\"\n ></div>\n </div>\n }\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- ============ Active step content ============ -->\n <div class=\"w-full px-4 sm:px-6 lg:px-8 py-6 pb-28\">\n @if (currentIndex() >= 0) {\n @if (getStepTemplate(steps()[currentIndex()].id); as tpl) {\n <div class=\"shk-wizard-fade\" [style.view-transition-name]=\"'shk-wizard-step'\">\n <ng-container\n *ngTemplateOutlet=\"tpl; context: { step: currentStepObj(), index: currentIndex() }\"\n ></ng-container>\n </div>\n } @else {\n <p class=\"text-sm _shk-text-surface-400 dark:[color:var(--shk-surface-400)]\">\n No hay contenido definido para el paso \u00AB{{ steps()[currentIndex()].label }}\u00BB.\n </p>\n }\n }\n </div>\n\n <!-- ============ Sticky Footer ============ -->\n @if (showFooter() && steps().length > 0) {\n <div\n class=\"sticky bottom-0 z-20 bg-white dark:[background-color:var(--shk-surface-50)] border-t _shk-border dark:[border-color:var(--shk-surface-200)] shadow-[0_-4px_12px_rgba(0,0,0,0.04)] mt-8\"\n >\n <div class=\"w-full px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-end gap-3\">\n @if (showStepCounter()) {\n <span class=\"text-sm _shk-text-surface-500 dark:[color:var(--shk-surface-500)] mr-auto\">\n {{ stepCounterPrefix() }} {{ currentIndex() + 1 }} / {{ steps().length }}\n </span>\n }\n\n @if (isFirstStep()) {\n @if (showCancel()) {\n <button type=\"button\" (click)=\"cancel.emit()\" class=\"shk-wizard-btn-secondary\">\n @if (cancelIcon()) {\n <svg [lucideIcon]=\"getIcon(cancelIcon())\" class=\"w-4 h-4\"></svg>\n }\n {{ cancelLabel() }}\n </button>\n }\n } @else {\n <button type=\"button\" (click)=\"onPrev()\" class=\"shk-wizard-btn-secondary\">\n <svg [lucideIcon]=\"getIcon(prevIcon())\" class=\"w-4 h-4\"></svg>\n {{ prevLabel() }}\n </button>\n }\n\n <button\n type=\"button\"\n (click)=\"onPrimary()\"\n [disabled]=\"primaryDisabled() || primaryLoading()\"\n class=\"shk-wizard-btn-primary\"\n >\n @if (primaryLoading()) {\n <svg [lucideIcon]=\"getIcon('loader-circle')\" class=\"w-4 h-4 shk-wizard-spin\"></svg>\n } @else {\n @if (primaryActionIcon()) {\n <svg [lucideIcon]=\"getIcon(primaryActionIcon())\" class=\"w-4 h-4\"></svg>\n }\n }\n {{ primaryActionLabel() }}\n </button>\n </div>\n </div>\n }\n</div>\n", styles: [":host{display:block}.shk-wizard-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:9999px;color:var(--shk-surface-600);transition:background-color .15s ease-in-out,color .15s ease-in-out}.shk-wizard-icon-btn:hover:not(:disabled){background-color:var(--shk-surface-100);color:var(--shk-surface-800)}.shk-wizard-icon-btn:disabled{cursor:default}.shk-wizard-icon-btn svg{width:1rem!important;height:1rem!important}.shk-wizard-stepper-dot{display:flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:9999px;font-size:.875rem;font-weight:700;flex-shrink:0;transition:background-color .15s ease-in-out,color .15s ease-in-out}.shk-wizard-stepper-dot svg{width:1rem!important;height:1rem!important}.shk-wizard-btn-secondary{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:.5rem;font-size:.875rem;font-weight:500;color:var(--shk-surface-700);background-color:transparent;border:1px solid var(--shk-surface-200);transition:background-color .15s ease-in-out,border-color .15s ease-in-out,color .15s ease-in-out}.shk-wizard-btn-secondary:hover:not(:disabled){background-color:var(--shk-surface-100);border-color:var(--shk-surface-300);color:var(--shk-surface-900)}.shk-wizard-btn-secondary:disabled{opacity:.5;cursor:not-allowed}.shk-wizard-btn-secondary svg{width:1rem!important;height:1rem!important}.shk-wizard-btn-primary{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:.5rem;font-size:.875rem;font-weight:500;color:#fff;background-color:var(--shk-primary-600);box-shadow:0 6px 20px -6px color-mix(in srgb,var(--shk-primary-600) 55%,transparent);transition:background-color .15s ease-in-out,opacity .15s ease-in-out}.shk-wizard-btn-primary:hover:not(:disabled){background-color:var(--shk-primary-700)}.shk-wizard-btn-primary:disabled{opacity:.5;cursor:not-allowed;box-shadow:none}.shk-wizard-btn-primary svg{width:1rem!important;height:1rem!important}.shk-wizard-spin{animation:shk-wizard-spin .8s linear infinite}@keyframes shk-wizard-spin{to{transform:rotate(360deg)}}.shk-wizard-fade{animation:shk-wizard-fade .25s ease-out}@keyframes shk-wizard-fade{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n"] }]
4005
4591
  }], propDecorators: { steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: false }] }], currentStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentStep", required: false }] }, { type: i0.Output, args: ["currentStepChange"] }], showHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHeader", required: false }] }], headerTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerTitle", required: false }] }], headerSubtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerSubtitle", required: false }] }], showBackButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBackButton", required: false }] }], backTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "backTooltip", required: false }] }], headerIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerIcon", required: false }] }], showStepper: [{ type: i0.Input, args: [{ isSignal: true, alias: "showStepper", required: false }] }], stepperClickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "stepperClickable", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], showStepCounter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showStepCounter", required: false }] }], stepCounterPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "stepCounterPrefix", required: false }] }], showCancel: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCancel", required: false }] }], advanceOnNext: [{ type: i0.Input, args: [{ isSignal: true, alias: "advanceOnNext", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], prevIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevIcon", required: false }] }], cancelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelLabel", required: false }] }], cancelIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelIcon", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], nextIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextIcon", required: false }] }], finishLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "finishLabel", required: false }] }], finishIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "finishIcon", required: false }] }], primaryLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryLabel", required: false }] }], primaryIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryIcon", required: false }] }], primaryLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryLoading", required: false }] }], primaryDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryDisabled", required: false }] }], back: [{ type: i0.Output, args: ["back"] }], cancel: [{ type: i0.Output, args: ["cancel"] }], prev: [{ type: i0.Output, args: ["prev"] }], next: [{ type: i0.Output, args: ["next"] }], finish: [{ type: i0.Output, args: ["finish"] }], stepChange: [{ type: i0.Output, args: ["stepChange"] }], stepContents: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ShkWizardStepContent), { isSignal: true }] }] } });
4006
4592
 
4593
+ /**
4594
+ * Shirkasoft UI Components.
4595
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
4596
+ * Belongs to Shirkasoft.
4597
+ */
4007
4598
  /** Placeholder block shown while content is loading. */
4008
4599
  class SkeletonComponent {
4009
4600
  constructor() {
@@ -4064,12 +4655,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4064
4655
  `, styles: [":host{display:block}.shk-skeleton{position:relative;overflow:hidden;background-color:var(--shk-skeleton-bg)}.shk-skeleton-circle{border-radius:9999px}.shk-skeleton-animate-wave:after{content:\"\";position:absolute;inset:0;background-image:linear-gradient(90deg,transparent,var(--shk-skeleton-highlight) 50%,transparent);animation:shk-skeleton-wave 1.6s ease-in-out infinite}.shk-skeleton-animate-pulse{animation:shk-skeleton-pulse 2s ease-in-out infinite}@keyframes shk-skeleton-wave{0%{transform:translate(-100%)}to{transform:translate(100%)}}@keyframes shk-skeleton-pulse{0%,to{opacity:1}50%{opacity:.55}}\n"] }]
4065
4656
  }], propDecorators: { shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], borderRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderRadius", required: false }] }], animation: [{ type: i0.Input, args: [{ isSignal: true, alias: "animation", required: false }] }], style: [{ type: i0.Input, args: [{ isSignal: true, alias: "style", required: false }] }], styleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "styleClass", required: false }] }] } });
4066
4657
 
4658
+ /**
4659
+ * Shirkasoft UI Components.
4660
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
4661
+ * Belongs to Shirkasoft.
4662
+ */
4067
4663
  /** Generic button with icon, severity variants, loading state and sizes. */
4068
4664
  class ButtonComponent {
4069
4665
  constructor() {
4070
4666
  /** Text shown inside the button. */
4071
4667
  this.label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4072
- /** Optional leading lucide icon name. */
4668
+ /** Optional leading lucide icon name or component. */
4073
4669
  this.icon = input('', ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
4074
4670
  /** Position of the icon relative to the label. */
4075
4671
  this.iconPos = input('left', ...(ngDevMode ? [{ debugName: "iconPos" }] : /* istanbul ignore next */ []));
@@ -4105,7 +4701,7 @@ class ButtonComponent {
4105
4701
  this.styleClass = input('', ...(ngDevMode ? [{ debugName: "styleClass" }] : /* istanbul ignore next */ []));
4106
4702
  /** Emitted on click with the original MouseEvent. */
4107
4703
  this.onClick = output();
4108
- this.getIcon = getIcon$6;
4704
+ this.getIcon = getIcon;
4109
4705
  this.iconOnly = computed(() => !this.label() && !!this.icon(), ...(ngDevMode ? [{ debugName: "iconOnly" }] : /* istanbul ignore next */ []));
4110
4706
  }
4111
4707
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -4196,6 +4792,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4196
4792
  `, styles: [":host{display:inline-block}.shk-button{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.5rem 1rem;border-radius:.5rem;border:1px solid transparent;font-size:.875rem;font-weight:500;line-height:1.25rem;cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,color .15s ease-in-out,box-shadow .15s ease-in-out,opacity .15s ease-in-out}.shk-button:focus-visible{outline:2px solid var(--shk-primary-400);outline-offset:2px}.shk-button:disabled{opacity:.55;cursor:not-allowed}.shk-button-icon{width:1rem;height:1rem;flex-shrink:0}.shk-button-primary{--shk-btn-bg: var(--shk-primary-600);--shk-btn-color: #ffffff;--shk-btn-border: var(--shk-primary-600)}.shk-button-secondary{--shk-btn-bg: var(--shk-surface-100);--shk-btn-color: var(--shk-surface-700);--shk-btn-border: var(--shk-surface-200)}.shk-button-success{--shk-btn-bg: var(--shk-success-600);--shk-btn-color: #ffffff;--shk-btn-border: var(--shk-success-600)}.shk-button-info{--shk-btn-bg: var(--shk-info-600);--shk-btn-color: #ffffff;--shk-btn-border: var(--shk-info-600)}.shk-button-warning{--shk-btn-bg: var(--shk-warning-600);--shk-btn-color: #ffffff;--shk-btn-border: var(--shk-warning-600)}.shk-button-danger{--shk-btn-bg: var(--shk-error-600);--shk-btn-color: #ffffff;--shk-btn-border: var(--shk-error-600)}.shk-button-help{--shk-btn-bg: var(--shk-primary-500);--shk-btn-color: #ffffff;--shk-btn-border: var(--shk-primary-500)}.shk-button-contrast{--shk-btn-bg: var(--shk-surface-900);--shk-btn-color: var(--shk-surface-50);--shk-btn-border: var(--shk-surface-900)}.shk-button{background-color:var(--shk-btn-bg);color:var(--shk-btn-color);border-color:var(--shk-btn-border)}.shk-button:hover:not(:disabled){background-color:color-mix(in srgb,var(--shk-btn-bg) 88%,#000000)}.shk-button-outlined,.shk-button-text{background-color:transparent}.shk-button-outlined{border-color:var(--shk-btn-color)}.shk-button-text{border-color:transparent}.shk-button-text:hover:not(:disabled),.shk-button-outlined:hover:not(:disabled){background-color:color-mix(in srgb,var(--shk-btn-color) 12%,transparent)}.shk-button-raised{box-shadow:0 1px 2px #0000000f,0 4px 8px #0000001a}.shk-button-rounded{border-radius:9999px}.shk-button-sm{padding:.375rem .75rem;font-size:.8125rem}.shk-button-sm .shk-button-icon{width:.875rem;height:.875rem}.shk-button-lg{padding:.75rem 1.5rem;font-size:1rem}.shk-button-lg .shk-button-icon{width:1.25rem;height:1.25rem}.shk-button-icon-only{padding:.5rem}.shk-button-sm.shk-button-icon-only{padding:.375rem}.shk-button-lg.shk-button-icon-only{padding:.75rem}.shk-button-icon-right{flex-direction:row-reverse}.shk-button-icon-top{flex-direction:column}.shk-button-icon-bottom{flex-direction:column-reverse}.shk-button-loading .shk-button-spin{animation:shk-button-spin 1s linear infinite}@keyframes shk-button-spin{to{transform:rotate(360deg)}}\n"] }]
4197
4793
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconPos: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPos", required: false }] }], severity: [{ type: i0.Input, args: [{ isSignal: true, alias: "severity", required: false }] }], outlined: [{ type: i0.Input, args: [{ isSignal: true, alias: "outlined", required: false }] }], text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], raised: [{ type: i0.Input, args: [{ isSignal: true, alias: "raised", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingIcon", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], style: [{ type: i0.Input, args: [{ isSignal: true, alias: "style", required: false }] }], styleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "styleClass", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }] } });
4198
4794
 
4795
+ /**
4796
+ * Shirkasoft UI Components.
4797
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
4798
+ * Belongs to Shirkasoft.
4799
+ */
4199
4800
  /** SVG radial knob with drag and keyboard support. Implements ControlValueAccessor. */
4200
4801
  class KnobComponent {
4201
4802
  constructor() {
@@ -4450,6 +5051,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4450
5051
  ], styles: [".shk-knob{position:relative;display:inline-block}.shk-knob-svg{display:block;touch-action:none}.shk-knob-svg-focusable{cursor:pointer;outline:none}.shk-knob-svg-focusable:focus-visible{border-radius:9999px;outline:2px solid var(--shk-primary-400);outline-offset:2px}.shk-knob-disabled{opacity:.55}.shk-knob-disabled .shk-knob-svg{cursor:not-allowed}.shk-knob-label{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1.125rem;font-weight:600;color:var(--shk-surface-800);pointer-events:none;-webkit-user-select:none;user-select:none}\n"] }]
4451
5052
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }], valueColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueColor", required: false }] }], rangeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeColor", required: false }] }], showValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValue", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], end: [{ type: i0.Output, args: ["end"] }], knobSvg: [{ type: i0.ViewChild, args: ['knobSvg', { isSignal: true }] }] } });
4452
5053
 
5054
+ /**
5055
+ * Shirkasoft UI Components.
5056
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5057
+ * Belongs to Shirkasoft.
5058
+ */
4453
5059
  /** Circular indeterminate spinner. */
4454
5060
  class LoadingSpinnerComponent {
4455
5061
  constructor() {
@@ -4512,6 +5118,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4512
5118
  `, styles: [":host{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem}.shk-loading-spinner{display:inline-block}.shk-loading-spinner-circle{width:100%;height:100%;border-radius:9999px;border-style:solid;border-color:color-mix(in srgb,var(--shk-primary-600) 20%,transparent);border-top-color:var(--shk-primary-600);animation:shk-loading-spin .8s linear infinite}.shk-loading-spinner-label{font-size:.8125rem;color:var(--shk-surface-500)}@keyframes shk-loading-spin{to{transform:rotate(360deg)}}\n"] }]
4513
5119
  }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], thickness: [{ type: i0.Input, args: [{ isSignal: true, alias: "thickness", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
4514
5120
 
5121
+ /**
5122
+ * Shirkasoft UI Components.
5123
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5124
+ * Belongs to Shirkasoft.
5125
+ */
4515
5126
  /** Progress bar with an indeterminate sweep or a determinate percentage. */
4516
5127
  class LoadingBarComponent {
4517
5128
  constructor() {
@@ -4525,6 +5136,7 @@ class LoadingBarComponent {
4525
5136
  this.color = input('', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
4526
5137
  /** Renders fully rounded ends. */
4527
5138
  this.rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded" }] : /* istanbul ignore next */ []));
5139
+ /** Fill width/style for the determinate mode, empty for indeterminate. */
4528
5140
  this.fillStyle = computed(() => {
4529
5141
  if (this.indeterminate())
4530
5142
  return {};
@@ -4571,6 +5183,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4571
5183
  `, styles: [".shk-loading-bar{width:100%;position:relative;overflow:hidden;background-color:var(--shk-surface-200)}.shk-loading-bar-fill{height:100%;background-color:var(--shk-primary-600);transition:width .3s ease-in-out}.shk-loading-bar-indeterminate .shk-loading-bar-fill{width:40%;animation:shk-loading-bar-sweep 1.2s ease-in-out infinite}@keyframes shk-loading-bar-sweep{0%{transform:translate(-100%)}to{transform:translate(350%)}}\n"] }]
4572
5184
  }], propDecorators: { height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }], progress: [{ type: i0.Input, args: [{ isSignal: true, alias: "progress", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }] } });
4573
5185
 
5186
+ /**
5187
+ * Shirkasoft UI Components.
5188
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5189
+ * Belongs to Shirkasoft.
5190
+ */
4574
5191
  /** Wraps content and shows a centered spinner over it while `visible` is true. */
4575
5192
  class LoadingOverlayComponent {
4576
5193
  constructor() {
@@ -4619,6 +5236,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4619
5236
  `, styles: [":host{display:block}.shk-loading-overlay{position:relative;min-height:1.5rem}.shk-loading-overlay-layer{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;z-index:10}.shk-loading-overlay-light .shk-loading-overlay-layer{background-color:#ffffffa6}.dark .shk-loading-overlay-light .shk-loading-overlay-layer{background-color:#0000008c}.shk-loading-overlay-dark .shk-loading-overlay-layer{background-color:#0006}\n"] }]
4620
5237
  }], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], spinnerSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "spinnerSize", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }] } });
4621
5238
 
5239
+ /**
5240
+ * Shirkasoft UI Components.
5241
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5242
+ * Belongs to Shirkasoft.
5243
+ */
4622
5244
  /** Bouncing dots loading indicator. */
4623
5245
  class LoadingDotsComponent {
4624
5246
  constructor() {
@@ -4630,6 +5252,7 @@ class LoadingDotsComponent {
4630
5252
  this.count = input(3, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
4631
5253
  /** Optional text shown next to the dots. */
4632
5254
  this.label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
5255
+ /** Array of dot indices to render based on the `count` input. */
4633
5256
  this.dots = computed(() => Array.from({ length: Math.max(1, this.count()) }), ...(ngDevMode ? [{ debugName: "dots" }] : /* istanbul ignore next */ []));
4634
5257
  }
4635
5258
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: LoadingDotsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -4674,11 +5297,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4674
5297
  `, styles: [":host{display:inline-flex;align-items:center;gap:.5rem}.shk-loading-dots{display:inline-flex;align-items:center;gap:.25rem}.shk-loading-dot{border-radius:9999px;background-color:var(--shk-primary-600);animation:shk-loading-dots-bounce 1.4s ease-in-out infinite}.shk-loading-dots-label{font-size:.8125rem;color:var(--shk-surface-500)}@keyframes shk-loading-dots-bounce{0%,80%,to{transform:scale(.6);opacity:.5}40%{transform:scale(1);opacity:1}}\n"] }]
4675
5298
  }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
4676
5299
 
4677
- /** Shared state between shk-tabs and its projected tab/tabpanel children. */
5300
+ /**
5301
+ * Shirkasoft UI Components.
5302
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5303
+ * Belongs to Shirkasoft.
5304
+ */
5305
+ /** Shared state between shk-abs and its tab/tabpanel children. */
4678
5306
  class TabsService {
4679
5307
  constructor() {
5308
+ /** Currently active tab value. */
4680
5309
  this.active = signal(undefined, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
4681
5310
  }
5311
+ /** Sets the active tab to the given value. */
4682
5312
  select(value) {
4683
5313
  this.active.set(value);
4684
5314
  }
@@ -4689,6 +5319,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4689
5319
  type: Injectable
4690
5320
  }] });
4691
5321
 
5322
+ /**
5323
+ * Shirkasoft UI Components.
5324
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5325
+ * Belongs to Shirkasoft.
5326
+ */
4692
5327
  /** Container for tab panels. Renders its projected shk-tablist/shh-tab content. */
4693
5328
  class TabsComponent {
4694
5329
  constructor() {
@@ -4696,10 +5331,13 @@ class TabsComponent {
4696
5331
  this.value = model(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4697
5332
  /** Allows the tab header to scroll horizontally when it overflows. */
4698
5333
  this.scrollable = input(false, ...(ngDevMode ? [{ debugName: "scrollable" }] : /* istanbul ignore next */ []));
5334
+ /** Shared service used to sync the active tab with the children. */
4699
5335
  this.tabsSvc = inject(TabsService);
5336
+ // Push external value changes into the shared service.
4700
5337
  effect(() => {
4701
5338
  this.tabsSvc.active.set(this.value());
4702
5339
  });
5340
+ // Reflect child-side selections back into the two-way value.
4703
5341
  effect(() => {
4704
5342
  const active = this.tabsSvc.active();
4705
5343
  if (active !== this.value()) {
@@ -4729,6 +5367,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4729
5367
  }]
4730
5368
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], scrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollable", required: false }] }] } });
4731
5369
 
5370
+ /**
5371
+ * Shirkasoft UI Components.
5372
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5373
+ * Belongs to Shirkasoft.
5374
+ */
4732
5375
  /** Horizontal bar that contains the shk-tab headers. */
4733
5376
  class TablistComponent {
4734
5377
  constructor() {
@@ -4765,6 +5408,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4765
5408
  `, styles: [".shk-tablist{display:flex;align-items:center;gap:.25rem;flex-wrap:wrap;border-bottom:1px solid var(--shk-surface-200)}.dark .shk-tablist{border-bottom-color:var(--shk-surface-200)}.shk-tablist-scrollable{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:thin}.shk-tablist-scrollable::-webkit-scrollbar{height:6px}.shk-tablist-scrollable::-webkit-scrollbar-thumb{background:var(--shk-surface-300);border-radius:9999px}\n"] }]
4766
5409
  }], propDecorators: { scrollable: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollable", required: false }] }], styleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "styleClass", required: false }] }] } });
4767
5410
 
5411
+ /**
5412
+ * Shirkasoft UI Components.
5413
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5414
+ * Belongs to Shirkasoft.
5415
+ */
4768
5416
  /** A single tab header. Clicking it activates the matching panel. */
4769
5417
  class TabComponent {
4770
5418
  constructor() {
@@ -4774,9 +5422,12 @@ class TabComponent {
4774
5422
  this.disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4775
5423
  /** Extra CSS classes. */
4776
5424
  this.styleClass = input('', ...(ngDevMode ? [{ debugName: "styleClass" }] : /* istanbul ignore next */ []));
5425
+ /** Shared service used to read the active tab value. */
4777
5426
  this.tabsSvc = inject(TabsService);
5427
+ /** Whether this tab is the active one. */
4778
5428
  this.active = computed(() => this.tabsSvc.active() === this.value(), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
4779
5429
  }
5430
+ /** Activates this tab through the shared service. */
4780
5431
  select() {
4781
5432
  if (this.disabled())
4782
5433
  return;
@@ -4818,6 +5469,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4818
5469
  `, styles: [".shk-tab{display:inline-flex;align-items:center;gap:.375rem;padding:.625rem 1rem;font-size:.875rem;font-weight:500;color:var(--shk-surface-500);background:transparent;border:none;border-bottom:2px solid transparent;margin-bottom:-1px;cursor:pointer;white-space:nowrap;transition:color .15s ease-in-out,border-color .15s ease-in-out,background-color .15s ease-in-out}.shk-tab:hover:not(:disabled):not(.shk-tab-active){color:var(--shk-surface-800)}.shk-tab-active{color:var(--shk-primary-600);border-bottom-color:var(--shk-primary-600);font-weight:600}.dark .shk-tab-active{color:var(--shk-primary-400);border-bottom-color:var(--shk-primary-400)}.shk-tab:disabled{opacity:.5;cursor:not-allowed}\n"] }]
4819
5470
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], styleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "styleClass", required: false }] }] } });
4820
5471
 
5472
+ /**
5473
+ * Shirkasoft UI Components.
5474
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5475
+ * Belongs to Shirkasoft.
5476
+ */
4821
5477
  /** Container that groups the shk-tabpanel content sections. */
4822
5478
  class TabpanelsComponent {
4823
5479
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: TabpanelsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -4841,12 +5497,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4841
5497
  }]
4842
5498
  }] });
4843
5499
 
5500
+ /**
5501
+ * Shirkasoft UI Components.
5502
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5503
+ * Belongs to Shirkasoft.
5504
+ */
4844
5505
  /** Content section shown only while its value is the active tab. */
4845
5506
  class TabPanelComponent {
4846
5507
  constructor() {
4847
5508
  /** Value that links this panel to its tab. */
4848
5509
  this.value = input(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
5510
+ /** Shared service used to read the active tab value. */
4849
5511
  this.tabsSvc = inject(TabsService);
5512
+ /** Whether this panel is the active one. */
4850
5513
  this.active = computed(() => this.tabsSvc.active() === this.value(), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
4851
5514
  }
4852
5515
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: TabPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -4874,17 +5537,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4874
5537
  }]
4875
5538
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }] } });
4876
5539
 
5540
+ /**
5541
+ * Global breadcrumb state. Components and pages write the current trail via
5542
+ * `set()` and observatories read it through `breadcrumbs$`.
5543
+ */
4877
5544
  class BreadcrumbService {
4878
5545
  constructor() {
5546
+ /** Internal buffer holding the current breadcrumb trail. */
4879
5547
  this.breadcrumbsSubject = new BehaviorSubject([]);
5548
+ /** Observable emitted with the latest breadcrumb trail. */
4880
5549
  this.breadcrumbs$ = this.breadcrumbsSubject.asObservable();
4881
5550
  }
5551
+ /** Replaces the whole breadcrumb trail. */
4882
5552
  set(items) {
4883
5553
  this.breadcrumbsSubject.next(items);
4884
5554
  }
5555
+ /** Empties the breadcrumb trail. */
4885
5556
  clear() {
4886
5557
  this.breadcrumbsSubject.next([]);
4887
5558
  }
5559
+ /** Returns the breadcrumb trail currently held (snapshot). */
4888
5560
  getCurrent() {
4889
5561
  return this.breadcrumbsSubject.getValue();
4890
5562
  }
@@ -4898,13 +5570,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImpo
4898
5570
  }]
4899
5571
  }] });
4900
5572
 
5573
+ /**
5574
+ * Shirkasoft UI Components.
5575
+ * Authors: Roberto Valladares Martin (Ansem13), Julio César García García (Sanjuli14).
5576
+ * Belongs to Shirkasoft.
5577
+ */
5578
+ /**
5579
+ * Navigation breadcrumb showing the current page hierarchy. Can be fed with
5580
+ * static items or driven by the shared BreadcrumbService.
5581
+ */
4901
5582
  class BreadcrumbComponent {
4902
5583
  constructor() {
4903
5584
  this.breadcrumbService = inject(BreadcrumbService);
5585
+ /** Static list of breadcrumb items. Takes precedence over the service items. */
4904
5586
  this.items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
5587
+ /** Visual separator between items: 'chevron', 'slash' or 'arrow'. */
4905
5588
  this.separator = input('chevron', ...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
5589
+ /** When true, subscribes to the BreadcrumbService to resolve items. */
4906
5590
  this.useService = input(true, ...(ngDevMode ? [{ debugName: "useService" }] : /* istanbul ignore next */ []));
5591
+ /** Items received from the BreadcrumbService. */
4907
5592
  this.serviceItems = signal([], ...(ngDevMode ? [{ debugName: "serviceItems" }] : /* istanbul ignore next */ []));
5593
+ /** Final items: static `items` if provided, otherwise the service items. */
4908
5594
  this.resolvedItems = computed(() => {
4909
5595
  const staticItems = this.items();
4910
5596
  if (staticItems.length > 0)
@@ -4913,11 +5599,13 @@ class BreadcrumbComponent {
4913
5599
  return this.serviceItems();
4914
5600
  return [];
4915
5601
  }, ...(ngDevMode ? [{ debugName: "resolvedItems" }] : /* istanbul ignore next */ []));
5602
+ /** Active subscription to the breadcrumb stream. */
4916
5603
  this.serviceSub = null;
4917
5604
  effect(() => {
4918
5605
  this.resolvedItems();
4919
5606
  });
4920
5607
  }
5608
+ /** Subscribes to the BreadcrumbService when `useService` is enabled. */
4921
5609
  ngOnInit() {
4922
5610
  if (this.useService()) {
4923
5611
  this.serviceSub = this.breadcrumbService.breadcrumbs$.subscribe((items) => {
@@ -4925,6 +5613,7 @@ class BreadcrumbComponent {
4925
5613
  });
4926
5614
  }
4927
5615
  }
5616
+ /** Unsubscribes from the breadcrumb stream to avoid leaks. */
4928
5617
  ngOnDestroy() {
4929
5618
  this.serviceSub?.unsubscribe();
4930
5619
  }