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,
|
|
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 {
|
|
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
|
-
|
|
16
|
-
|
|
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) =>
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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) =>
|
|
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
|
-
|
|
936
|
+
label = datePart;
|
|
681
937
|
}
|
|
682
|
-
if (v === 'month') {
|
|
683
|
-
const [y, m] =
|
|
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
|
-
|
|
689
|
-
|
|
944
|
+
label = fmt.format(new Date(y, m - 1, 1));
|
|
945
|
+
label = label.charAt(0).toUpperCase() + label.slice(1);
|
|
690
946
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
1127
|
+
this.viewYear.set(Number(datePart));
|
|
842
1128
|
}
|
|
843
1129
|
else if (v === 'month') {
|
|
844
|
-
this.viewYear.set(Number(
|
|
1130
|
+
this.viewYear.set(Number(datePart.split('-')[0]));
|
|
845
1131
|
}
|
|
846
1132
|
else {
|
|
847
|
-
const d = new Date(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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) =>
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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) =>
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
*
|
|
2283
|
-
*
|
|
2284
|
-
*
|
|
2285
|
-
|
|
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
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
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
|
-
|
|
2994
|
+
/** Maps icon names to lucide components, falling back to a plus icon. */
|
|
2995
|
+
this.getIcon = (name) => getIcon(name, LucidePlus);
|
|
2512
2996
|
/**
|
|
2513
|
-
*
|
|
2514
|
-
*
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3087
|
+
/** Subjects used to debounce column filters (server mode) and search. */
|
|
2567
3088
|
this.filterSubject = new Subject();
|
|
2568
3089
|
this.searchSubject = new Subject();
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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
|
-
|
|
2600
|
-
|
|
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.
|
|
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
|
-
*
|
|
2671
|
-
*
|
|
2672
|
-
*
|
|
2673
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
2810
|
-
*
|
|
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
|
|
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
|
-
*
|
|
2860
|
-
*
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
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(
|
|
3112
|
-
this.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
|
|
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 =
|
|
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: [
|
|
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:
|
|
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
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
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
|
|
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
|
|
4176
|
+
/** Maps a sub-item `type` to a lucide icon component. */
|
|
3596
4177
|
const DEFAULT_SUPPLIER_TYPE_ICONS = {
|
|
3597
|
-
accommodation:
|
|
3598
|
-
air_tickets:
|
|
3599
|
-
rent_a_car:
|
|
3600
|
-
excursion:
|
|
3601
|
-
transfer:
|
|
3602
|
-
cruise:
|
|
3603
|
-
insurance:
|
|
3604
|
-
restaurant:
|
|
3605
|
-
visit:
|
|
3606
|
-
guide:
|
|
3607
|
-
package:
|
|
3608
|
-
other:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
}
|