@styloviz/input-field 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/fesm2022/styloviz-input-field.mjs +980 -0
- package/fesm2022/styloviz-input-field.mjs.map +1 -0
- package/package.json +43 -0
- package/types/styloviz-input-field.d.ts +341 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { model, input, output, signal, viewChild, computed, forwardRef, ChangeDetectionStrategy, Component, inject, ElementRef, ApplicationRef, TemplateRef, HostListener, ViewChild, ViewEncapsulation } from '@angular/core';
|
|
3
|
+
import { NgClass } from '@angular/common';
|
|
4
|
+
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
5
|
+
|
|
6
|
+
/** Process-wide counter for generating unique fallback ids. */
|
|
7
|
+
let _svInputUid = 0;
|
|
8
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* InputField — A premium form input for dashboards.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - text, email, password, number, tel, url, date, time, color, …
|
|
14
|
+
* - Textarea mode (multiline) via `multiline` input
|
|
15
|
+
* - Select mode via `options` input
|
|
16
|
+
* - 3 visual variants: default (bordered) · filled · flushed (underline)
|
|
17
|
+
* - 3 sizes: sm · md · lg
|
|
18
|
+
* - 4 validation states: default · error · success · warning
|
|
19
|
+
* - Leading / trailing icon slots (SVG path)
|
|
20
|
+
* - Inline leading / trailing text addons (e.g. "https://", ".com")
|
|
21
|
+
* - Password show/hide toggle built-in
|
|
22
|
+
* - Character counter with optional max-length
|
|
23
|
+
* - Helper text and error message slots
|
|
24
|
+
* - Full ControlValueAccessor — works with `formControl`, `formControlName`, `ngModel`
|
|
25
|
+
* - Accessible: `<label>`, `aria-describedby`, `aria-invalid`, focus-visible ring
|
|
26
|
+
*/
|
|
27
|
+
class SvInputFieldComponent {
|
|
28
|
+
// ── Core ──────────────────────────────────────────────────────────────────
|
|
29
|
+
/** Current value. Use `[(value)]` for signal two-way binding. */
|
|
30
|
+
value = model('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
31
|
+
/** Input type. @default 'text' */
|
|
32
|
+
type = input('text', ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
33
|
+
/** Renders a `<textarea>` instead of `<input>`. @default false */
|
|
34
|
+
multiline = input(false, ...(ngDevMode ? [{ debugName: "multiline" }] : /* istanbul ignore next */ []));
|
|
35
|
+
/**
|
|
36
|
+
* When set, renders a `<select>` element.
|
|
37
|
+
* Use `type` is ignored when options are provided.
|
|
38
|
+
*/
|
|
39
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
40
|
+
// ── Labels & helpers ──────────────────────────────────────────────────────
|
|
41
|
+
/** Label text shown above the input. */
|
|
42
|
+
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
43
|
+
/** Marks the label with a red asterisk. @default false */
|
|
44
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
45
|
+
/** Optional label (replaces asterisk with a soft "(optional)" tag). @default false */
|
|
46
|
+
optionalTag = input(false, ...(ngDevMode ? [{ debugName: "optionalTag" }] : /* istanbul ignore next */ []));
|
|
47
|
+
/** Placeholder text. */
|
|
48
|
+
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
49
|
+
/** Helper text shown below the input (hidden when `errorMessage` is set). */
|
|
50
|
+
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
51
|
+
/** Error message — switches state to `error` automatically when non-empty. */
|
|
52
|
+
errorMessage = input('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
|
|
53
|
+
/** Success message — shown below when state is `success`. */
|
|
54
|
+
successMessage = input('', ...(ngDevMode ? [{ debugName: "successMessage" }] : /* istanbul ignore next */ []));
|
|
55
|
+
// ── Appearance ────────────────────────────────────────────────────────────
|
|
56
|
+
/** Visual style. @default 'default' */
|
|
57
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/** Size. @default 'md' */
|
|
59
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
60
|
+
/**
|
|
61
|
+
* Explicit validation state override.
|
|
62
|
+
* Automatically set to `'error'` when `errorMessage` is non-empty.
|
|
63
|
+
* @default 'default'
|
|
64
|
+
*/
|
|
65
|
+
state = input('default', ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
66
|
+
// ── Icons & addons ────────────────────────────────────────────────────────
|
|
67
|
+
/** SVG `<path d="...">` for the leading (left) icon. */
|
|
68
|
+
leadingIconPath = input('', ...(ngDevMode ? [{ debugName: "leadingIconPath" }] : /* istanbul ignore next */ []));
|
|
69
|
+
/** SVG `<path d="...">` for the trailing (right) icon. */
|
|
70
|
+
trailingIconPath = input('', ...(ngDevMode ? [{ debugName: "trailingIconPath" }] : /* istanbul ignore next */ []));
|
|
71
|
+
/**
|
|
72
|
+
* Short text prepended inside the input (e.g. `'https://'`, `'$'`).
|
|
73
|
+
* Ignored when `leadingIconPath` is set.
|
|
74
|
+
*/
|
|
75
|
+
leadingAddon = input('', ...(ngDevMode ? [{ debugName: "leadingAddon" }] : /* istanbul ignore next */ []));
|
|
76
|
+
/**
|
|
77
|
+
* Short text appended inside the input (e.g. `'.com'`, `'USD'`).
|
|
78
|
+
* Ignored when `trailingIconPath` is set.
|
|
79
|
+
*/
|
|
80
|
+
trailingAddon = input('', ...(ngDevMode ? [{ debugName: "trailingAddon" }] : /* istanbul ignore next */ []));
|
|
81
|
+
// ── Character counter ─────────────────────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Maximum character count shown as a counter below the input.
|
|
84
|
+
* Set to 0 to disable. @default 0
|
|
85
|
+
*/
|
|
86
|
+
maxLength = input(0, ...(ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []));
|
|
87
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
88
|
+
/** Disable the input. @default false */
|
|
89
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
90
|
+
/** Read-only mode. @default false */
|
|
91
|
+
readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
92
|
+
/** Show a loading spinner in the trailing position. @default false */
|
|
93
|
+
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
94
|
+
/** id forwarded to the native input — required for external `<label for>`. */
|
|
95
|
+
inputId = input('', ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
|
|
96
|
+
/** name attribute for the native input. */
|
|
97
|
+
inputName = input('', ...(ngDevMode ? [{ debugName: "inputName" }] : /* istanbul ignore next */ []));
|
|
98
|
+
/** Additional CSS classes on the root wrapper. */
|
|
99
|
+
customClass = input('', ...(ngDevMode ? [{ debugName: "customClass" }] : /* istanbul ignore next */ []));
|
|
100
|
+
// ── Native constraints & hints ──────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* `autocomplete` token (e.g. 'email', 'name', 'current-password', 'one-time-code').
|
|
103
|
+
* Defaults to 'current-password' for password type, otherwise the browser default.
|
|
104
|
+
*/
|
|
105
|
+
autocomplete = input('', ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
|
|
106
|
+
/** Virtual-keyboard hint for mobile (e.g. 'numeric', 'email', 'tel'). */
|
|
107
|
+
inputMode = input('', ...(ngDevMode ? [{ debugName: "inputMode" }] : /* istanbul ignore next */ []));
|
|
108
|
+
/** Native validation regex (without anchors — anchored automatically). */
|
|
109
|
+
pattern = input('', ...(ngDevMode ? [{ debugName: "pattern" }] : /* istanbul ignore next */ []));
|
|
110
|
+
/** Minimum length used by the built-in Validator and native `minlength`. @default 0 */
|
|
111
|
+
minLength = input(0, ...(ngDevMode ? [{ debugName: "minLength" }] : /* istanbul ignore next */ []));
|
|
112
|
+
/** Native `min` for number/date/range types. */
|
|
113
|
+
min = input(null, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
|
|
114
|
+
/** Native `max` for number/date/range types. */
|
|
115
|
+
max = input(null, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
116
|
+
/** Native `step` for number/date/range types. */
|
|
117
|
+
step = input(null, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
|
|
118
|
+
/** Native spellcheck toggle. null = browser default. */
|
|
119
|
+
spellcheck = input(null, ...(ngDevMode ? [{ debugName: "spellcheck" }] : /* istanbul ignore next */ []));
|
|
120
|
+
/** Autofocus the control on render. @default false */
|
|
121
|
+
autofocus = input(false, ...(ngDevMode ? [{ debugName: "autofocus" }] : /* istanbul ignore next */ []));
|
|
122
|
+
// ── Outputs ───────────────────────────────────────────────────────────────
|
|
123
|
+
// Note: `value = model<string>('')` already auto-emits `valueChange` when
|
|
124
|
+
// value.set() is called — no separate output needed.
|
|
125
|
+
/** Emits when the input gains focus. */
|
|
126
|
+
focused = output();
|
|
127
|
+
/** Emits when the input loses focus. */
|
|
128
|
+
blurred = output();
|
|
129
|
+
/** Emits when Enter is pressed. */
|
|
130
|
+
submitted = output();
|
|
131
|
+
// ── Internal state ────────────────────────────────────────────────────────
|
|
132
|
+
isFocused = signal(false, ...(ngDevMode ? [{ debugName: "isFocused" }] : /* istanbul ignore next */ []));
|
|
133
|
+
showPassword = signal(false, ...(ngDevMode ? [{ debugName: "showPassword" }] : /* istanbul ignore next */ []));
|
|
134
|
+
inputRef = viewChild('inputEl', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
|
|
135
|
+
// ── CVA ───────────────────────────────────────────────────────────────────
|
|
136
|
+
_onChange = () => { };
|
|
137
|
+
_onTouched = () => { };
|
|
138
|
+
/** Tracks disabled state set programmatically via formControl.disable(). */
|
|
139
|
+
_formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : /* istanbul ignore next */ []));
|
|
140
|
+
/**
|
|
141
|
+
* Merged disabled state — true when either the [disabled] input or the
|
|
142
|
+
* parent FormControl is disabled. Use this everywhere instead of disabled().
|
|
143
|
+
*/
|
|
144
|
+
resolvedDisabled = computed(() => this.disabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "resolvedDisabled" }] : /* istanbul ignore next */ []));
|
|
145
|
+
writeValue(val) { this.value.set(val ?? ''); }
|
|
146
|
+
registerOnChange(fn) { this._onChange = fn; }
|
|
147
|
+
registerOnTouched(fn) { this._onTouched = fn; }
|
|
148
|
+
setDisabledState(isDisabled) { this._formDisabled.set(isDisabled); }
|
|
149
|
+
validate(_) {
|
|
150
|
+
const errors = {};
|
|
151
|
+
const v = this.value() ?? '';
|
|
152
|
+
const max = this.maxLength();
|
|
153
|
+
const min = this.minLength();
|
|
154
|
+
if (this.required() && v.length === 0) {
|
|
155
|
+
errors['required'] = true;
|
|
156
|
+
}
|
|
157
|
+
if (max > 0 && v.length > max) {
|
|
158
|
+
errors['maxlength'] = { requiredLength: max, actualLength: v.length };
|
|
159
|
+
}
|
|
160
|
+
if (min > 0 && v.length > 0 && v.length < min) {
|
|
161
|
+
errors['minlength'] = { requiredLength: min, actualLength: v.length };
|
|
162
|
+
}
|
|
163
|
+
const pat = this.pattern();
|
|
164
|
+
if (pat && v.length > 0) {
|
|
165
|
+
try {
|
|
166
|
+
if (!new RegExp(`^(?:${pat})$`).test(v)) {
|
|
167
|
+
errors['pattern'] = { requiredPattern: pat, actualValue: v };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Invalid regex supplied — skip pattern validation rather than throw.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
175
|
+
}
|
|
176
|
+
// ── Event handlers ────────────────────────────────────────────────────────
|
|
177
|
+
onNativeChange(event) {
|
|
178
|
+
const val = event.target.value;
|
|
179
|
+
this.value.set(val); // model().set() auto-emits valueChange
|
|
180
|
+
this._onChange(val);
|
|
181
|
+
}
|
|
182
|
+
onFocus(e) {
|
|
183
|
+
this.isFocused.set(true);
|
|
184
|
+
this.focused.emit(e);
|
|
185
|
+
}
|
|
186
|
+
onBlur(e) {
|
|
187
|
+
this.isFocused.set(false);
|
|
188
|
+
this._onTouched();
|
|
189
|
+
this.blurred.emit(e);
|
|
190
|
+
}
|
|
191
|
+
onKeydown(event) {
|
|
192
|
+
if (event.key === 'Enter' && !this.multiline()) {
|
|
193
|
+
this.submitted.emit(this.value());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
togglePassword() { this.showPassword.update(v => !v); }
|
|
197
|
+
// ── Computed helpers ──────────────────────────────────────────────────────
|
|
198
|
+
/** Stable unique fallback id so label + aria associations always work. */
|
|
199
|
+
_autoId = `sv-input-${++_svInputUid}`;
|
|
200
|
+
/** Effective id used by the native control and its <label for>. */
|
|
201
|
+
resolvedId = computed(() => this.inputId() || this._autoId, ...(ngDevMode ? [{ debugName: "resolvedId" }] : /* istanbul ignore next */ []));
|
|
202
|
+
/** Id of the hint/error/success description element. */
|
|
203
|
+
describedById = computed(() => `${this.resolvedId()}-desc`, ...(ngDevMode ? [{ debugName: "describedById" }] : /* istanbul ignore next */ []));
|
|
204
|
+
isSelect = computed(() => this.options().length > 0, ...(ngDevMode ? [{ debugName: "isSelect" }] : /* istanbul ignore next */ []));
|
|
205
|
+
isPassword = computed(() => this.type() === 'password', ...(ngDevMode ? [{ debugName: "isPassword" }] : /* istanbul ignore next */ []));
|
|
206
|
+
isColor = computed(() => this.type() === 'color', ...(ngDevMode ? [{ debugName: "isColor" }] : /* istanbul ignore next */ []));
|
|
207
|
+
isFloating = computed(() => false, ...(ngDevMode ? [{ debugName: "isFloating" }] : /* istanbul ignore next */ []));
|
|
208
|
+
labelIsFloated = computed(() => this.isFocused() || !!this.value(), ...(ngDevMode ? [{ debugName: "labelIsFloated" }] : /* istanbul ignore next */ []));
|
|
209
|
+
resolvedType = computed(() => {
|
|
210
|
+
if (this.isPassword())
|
|
211
|
+
return this.showPassword() ? 'text' : 'password';
|
|
212
|
+
return this.type();
|
|
213
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedType" }] : /* istanbul ignore next */ []));
|
|
214
|
+
resolvedState = computed(() => this.errorMessage() ? 'error' : this.state(), ...(ngDevMode ? [{ debugName: "resolvedState" }] : /* istanbul ignore next */ []));
|
|
215
|
+
charCount = computed(() => this.value().length, ...(ngDevMode ? [{ debugName: "charCount" }] : /* istanbul ignore next */ []));
|
|
216
|
+
showCounter = computed(() => this.maxLength() > 0, ...(ngDevMode ? [{ debugName: "showCounter" }] : /* istanbul ignore next */ []));
|
|
217
|
+
counterOver = computed(() => this.maxLength() > 0 && this.charCount() > this.maxLength(), ...(ngDevMode ? [{ debugName: "counterOver" }] : /* istanbul ignore next */ []));
|
|
218
|
+
showHint = computed(() => !!this.hint() && !this.errorMessage() && !this.successMessage(), ...(ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []));
|
|
219
|
+
showError = computed(() => !!this.errorMessage(), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
|
|
220
|
+
showSuccess = computed(() => !!this.successMessage() && !this.errorMessage(), ...(ngDevMode ? [{ debugName: "showSuccess" }] : /* istanbul ignore next */ []));
|
|
221
|
+
/** True when any below-field description (hint/error/success) is rendered. */
|
|
222
|
+
hasDescription = computed(() => this.showHint() || this.showError() || this.showSuccess(), ...(ngDevMode ? [{ debugName: "hasDescription" }] : /* istanbul ignore next */ []));
|
|
223
|
+
/** Resolved autocomplete token. Smart default for passwords, else explicit value. */
|
|
224
|
+
resolvedAutocomplete = computed(() => {
|
|
225
|
+
if (this.autocomplete())
|
|
226
|
+
return this.autocomplete();
|
|
227
|
+
if (this.isPassword())
|
|
228
|
+
return 'current-password';
|
|
229
|
+
return null;
|
|
230
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedAutocomplete" }] : /* istanbul ignore next */ []));
|
|
231
|
+
hasLeading = computed(() => !!this.leadingIconPath() || !!this.leadingAddon(), ...(ngDevMode ? [{ debugName: "hasLeading" }] : /* istanbul ignore next */ []));
|
|
232
|
+
hasTrailing = computed(() => !!this.trailingIconPath() || !!this.trailingAddon() ||
|
|
233
|
+
this.isPassword() || this.loading(), ...(ngDevMode ? [{ debugName: "hasTrailing" }] : /* istanbul ignore next */ []));
|
|
234
|
+
// ── Class maps ────────────────────────────────────────────────────────────
|
|
235
|
+
rootClasses = computed(() => ['w-full', this.customClass()].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
|
|
236
|
+
floatingLabelClasses = computed(() => {
|
|
237
|
+
const floated = this.labelIsFloated();
|
|
238
|
+
const s = this.resolvedState();
|
|
239
|
+
const focused = this.isFocused();
|
|
240
|
+
// When leading icon/addon present, offset label so it doesn't overlap
|
|
241
|
+
const notFloatedLeft = this.hasLeading() ? 'left-10' : 'left-3.5';
|
|
242
|
+
const floatedLeft = this.hasLeading() ? 'left-8' : 'left-3';
|
|
243
|
+
const left = floated ? floatedLeft : notFloatedLeft;
|
|
244
|
+
// For multiline, label sits near the top (not vertical-center) when not floated
|
|
245
|
+
const isMulti = this.multiline();
|
|
246
|
+
const notFloatedTopMap = { sm: 'top-3', md: 'top-4', lg: 'top-5' };
|
|
247
|
+
const notFloatedTop = isMulti ? notFloatedTopMap[this.size()] : 'top-1/2';
|
|
248
|
+
const notFloatedTranslate = isMulti ? '' : '-translate-y-1/2';
|
|
249
|
+
const textSize = floated ? 'text-[0.7rem] leading-none' : this.labelSizeClass();
|
|
250
|
+
const bg = floated ? 'px-1 bg-white dark:bg-gray-900' : '';
|
|
251
|
+
let color;
|
|
252
|
+
if (floated && focused) {
|
|
253
|
+
color = s === 'error' ? 'text-red-500'
|
|
254
|
+
: s === 'success' ? 'text-emerald-500'
|
|
255
|
+
: s === 'warning' ? 'text-amber-500'
|
|
256
|
+
: 'text-blue-600 dark:text-blue-400';
|
|
257
|
+
}
|
|
258
|
+
else if (floated) {
|
|
259
|
+
color = s === 'error' ? 'text-red-500 dark:text-red-400'
|
|
260
|
+
: s === 'success' ? 'text-emerald-600 dark:text-emerald-400'
|
|
261
|
+
: s === 'warning' ? 'text-amber-600 dark:text-amber-400'
|
|
262
|
+
: 'text-gray-500 dark:text-gray-400';
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
color = 'text-gray-500 dark:text-gray-400';
|
|
266
|
+
}
|
|
267
|
+
const position = floated
|
|
268
|
+
? `${left} -top-px -translate-y-1/2`
|
|
269
|
+
: `${left} ${notFloatedTop} ${notFloatedTranslate}`.trim();
|
|
270
|
+
return [
|
|
271
|
+
'absolute z-10 pointer-events-none select-none',
|
|
272
|
+
'transition-[top,left,font-size,color,padding,background-color] duration-200 ease-in-out',
|
|
273
|
+
position,
|
|
274
|
+
textSize,
|
|
275
|
+
bg,
|
|
276
|
+
color,
|
|
277
|
+
].filter(Boolean).join(' ');
|
|
278
|
+
}, ...(ngDevMode ? [{ debugName: "floatingLabelClasses" }] : /* istanbul ignore next */ []));
|
|
279
|
+
labelSizeClass = computed(() => {
|
|
280
|
+
switch (this.size()) {
|
|
281
|
+
case 'sm': return 'text-xs';
|
|
282
|
+
case 'lg': return 'text-base';
|
|
283
|
+
default: return 'text-sm';
|
|
284
|
+
}
|
|
285
|
+
}, ...(ngDevMode ? [{ debugName: "labelSizeClass" }] : /* istanbul ignore next */ []));
|
|
286
|
+
/** Outer track that wraps the native input + addons + icons. */
|
|
287
|
+
trackClasses = computed(() => {
|
|
288
|
+
const v = this.variant();
|
|
289
|
+
const s = this.resolvedState();
|
|
290
|
+
const focused = this.isFocused();
|
|
291
|
+
// Base layout — multiline uses items-start so leading icons align to the top
|
|
292
|
+
const base = `relative flex ${this.multiline() ? 'items-start' : 'items-center'} w-full transition-all duration-150`;
|
|
293
|
+
// Height / font size
|
|
294
|
+
const sizeMap = {
|
|
295
|
+
sm: 'text-xs',
|
|
296
|
+
md: 'text-sm',
|
|
297
|
+
lg: 'text-base',
|
|
298
|
+
};
|
|
299
|
+
// Variant shape & surface
|
|
300
|
+
const variantBase = {
|
|
301
|
+
default: 'rounded-md border bg-white dark:bg-gray-900',
|
|
302
|
+
filled: 'rounded-md border-0 bg-gray-100 dark:bg-gray-800',
|
|
303
|
+
flushed: 'border-b-2 bg-transparent rounded-none',
|
|
304
|
+
};
|
|
305
|
+
// State-driven border/ring
|
|
306
|
+
const isFlushed = v === 'flushed';
|
|
307
|
+
const stateIdle = {
|
|
308
|
+
default: isFlushed
|
|
309
|
+
? 'border-gray-300 dark:border-gray-600'
|
|
310
|
+
: 'border-gray-300 dark:border-gray-700',
|
|
311
|
+
error: isFlushed
|
|
312
|
+
? 'border-red-500 dark:border-red-500'
|
|
313
|
+
: 'border-red-400 dark:border-red-500',
|
|
314
|
+
success: isFlushed
|
|
315
|
+
? 'border-emerald-500 dark:border-emerald-500'
|
|
316
|
+
: 'border-emerald-400 dark:border-emerald-500',
|
|
317
|
+
warning: isFlushed
|
|
318
|
+
? 'border-amber-500 dark:border-amber-500'
|
|
319
|
+
: 'border-amber-400 dark:border-amber-500',
|
|
320
|
+
};
|
|
321
|
+
const stateFocus = {
|
|
322
|
+
default: isFlushed
|
|
323
|
+
? 'border-blue-500 dark:border-blue-400'
|
|
324
|
+
: 'border-blue-500 ring-2 ring-blue-500/20 dark:border-blue-400 dark:ring-blue-400/20',
|
|
325
|
+
error: isFlushed
|
|
326
|
+
? 'border-red-500'
|
|
327
|
+
: 'border-red-500 ring-2 ring-red-500/20',
|
|
328
|
+
success: isFlushed
|
|
329
|
+
? 'border-emerald-500'
|
|
330
|
+
: 'border-emerald-500 ring-2 ring-emerald-500/20',
|
|
331
|
+
warning: isFlushed
|
|
332
|
+
? 'border-amber-500'
|
|
333
|
+
: 'border-amber-500 ring-2 ring-amber-500/20',
|
|
334
|
+
};
|
|
335
|
+
const disabledClass = this.resolvedDisabled()
|
|
336
|
+
? 'cursor-not-allowed opacity-60 pointer-events-none'
|
|
337
|
+
: '';
|
|
338
|
+
return [
|
|
339
|
+
base,
|
|
340
|
+
sizeMap[this.size()],
|
|
341
|
+
variantBase[v],
|
|
342
|
+
focused ? stateFocus[s] : stateIdle[s],
|
|
343
|
+
disabledClass,
|
|
344
|
+
].filter(Boolean).join(' ');
|
|
345
|
+
}, ...(ngDevMode ? [{ debugName: "trackClasses" }] : /* istanbul ignore next */ []));
|
|
346
|
+
/** The native `<input>`, `<textarea>` or `<select>` element. */
|
|
347
|
+
nativeInputClasses = computed(() => {
|
|
348
|
+
const padL = this.hasLeading() ? 'pl-0' : this.sidepadding();
|
|
349
|
+
const padR = this.hasTrailing() ? 'pr-0' : this.sidepadding();
|
|
350
|
+
const multi = this.multiline();
|
|
351
|
+
return [
|
|
352
|
+
'flex-1 min-w-0 bg-transparent outline-none',
|
|
353
|
+
'text-gray-900 dark:text-white',
|
|
354
|
+
'placeholder-gray-400 dark:placeholder-gray-500',
|
|
355
|
+
'disabled:cursor-not-allowed',
|
|
356
|
+
padL, padR,
|
|
357
|
+
multi ? `resize-y ${this.multilineVertPadding()} min-h-[5rem]` : this.heightClass(),
|
|
358
|
+
this.isColor() ? 'h-10 w-full cursor-pointer rounded-lg p-1' : '',
|
|
359
|
+
].filter(Boolean).join(' ');
|
|
360
|
+
}, ...(ngDevMode ? [{ debugName: "nativeInputClasses" }] : /* istanbul ignore next */ []));
|
|
361
|
+
sidepadding = computed(() => {
|
|
362
|
+
switch (this.size()) {
|
|
363
|
+
case 'sm': return 'px-2.5';
|
|
364
|
+
case 'lg': return 'px-4';
|
|
365
|
+
default: return 'px-3.5';
|
|
366
|
+
}
|
|
367
|
+
}, ...(ngDevMode ? [{ debugName: "sidepadding" }] : /* istanbul ignore next */ []));
|
|
368
|
+
vertPadding = computed(() => {
|
|
369
|
+
switch (this.size()) {
|
|
370
|
+
case 'sm': return 'py-1.5';
|
|
371
|
+
case 'lg': return 'py-3';
|
|
372
|
+
default: return 'py-2';
|
|
373
|
+
}
|
|
374
|
+
}, ...(ngDevMode ? [{ debugName: "vertPadding" }] : /* istanbul ignore next */ []));
|
|
375
|
+
/** Vertical padding for multiline (textarea) — adds extra top space when floating. */
|
|
376
|
+
multilineVertPadding = computed(() => {
|
|
377
|
+
if (this.isFloating()) {
|
|
378
|
+
switch (this.size()) {
|
|
379
|
+
case 'sm': return 'pt-4 pb-2';
|
|
380
|
+
case 'lg': return 'pt-5 pb-3';
|
|
381
|
+
default: return 'pt-5 pb-3';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return this.vertPadding();
|
|
385
|
+
}, ...(ngDevMode ? [{ debugName: "multilineVertPadding" }] : /* istanbul ignore next */ []));
|
|
386
|
+
heightClass = computed(() => {
|
|
387
|
+
if (this.isFloating()) {
|
|
388
|
+
switch (this.size()) {
|
|
389
|
+
case 'sm': return 'h-12 pt-2 pb-1';
|
|
390
|
+
case 'lg': return 'h-16 pt-4 pb-2';
|
|
391
|
+
default: return 'h-14 pt-3 pb-2';
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
switch (this.size()) {
|
|
395
|
+
case 'sm': return 'h-8 py-0';
|
|
396
|
+
case 'lg': return 'h-12 py-0';
|
|
397
|
+
default: return 'h-10 py-0';
|
|
398
|
+
}
|
|
399
|
+
}, ...(ngDevMode ? [{ debugName: "heightClass" }] : /* istanbul ignore next */ []));
|
|
400
|
+
addonClasses = computed(() => `flex shrink-0 items-center ${this.vertPadding()} text-gray-500 dark:text-gray-400 text-sm select-none`, ...(ngDevMode ? [{ debugName: "addonClasses" }] : /* istanbul ignore next */ []));
|
|
401
|
+
leadingAddonClasses = computed(() => `${this.addonClasses()} pl-3.5 border-r border-gray-200 dark:border-gray-700 mr-3.5 pr-3`, ...(ngDevMode ? [{ debugName: "leadingAddonClasses" }] : /* istanbul ignore next */ []));
|
|
402
|
+
trailingAddonClasses = computed(() => `${this.addonClasses()} pr-3.5 border-l border-gray-200 dark:border-gray-700 ml-3.5 pl-3`, ...(ngDevMode ? [{ debugName: "trailingAddonClasses" }] : /* istanbul ignore next */ []));
|
|
403
|
+
iconClass = computed(() => {
|
|
404
|
+
switch (this.size()) {
|
|
405
|
+
case 'sm': return 'h-3.5 w-3.5';
|
|
406
|
+
case 'lg': return 'h-5 w-5';
|
|
407
|
+
default: return 'h-4 w-4';
|
|
408
|
+
}
|
|
409
|
+
}, ...(ngDevMode ? [{ debugName: "iconClass" }] : /* istanbul ignore next */ []));
|
|
410
|
+
iconPadClass = computed(() => {
|
|
411
|
+
switch (this.size()) {
|
|
412
|
+
case 'sm': return 'px-2';
|
|
413
|
+
case 'lg': return 'px-3.5';
|
|
414
|
+
default: return 'px-3';
|
|
415
|
+
}
|
|
416
|
+
}, ...(ngDevMode ? [{ debugName: "iconPadClass" }] : /* istanbul ignore next */ []));
|
|
417
|
+
stateIconPath = computed(() => {
|
|
418
|
+
switch (this.resolvedState()) {
|
|
419
|
+
case 'error': return 'M12 8v4m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z';
|
|
420
|
+
case 'success': return 'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3';
|
|
421
|
+
case 'warning': return 'M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01';
|
|
422
|
+
default: return '';
|
|
423
|
+
}
|
|
424
|
+
}, ...(ngDevMode ? [{ debugName: "stateIconPath" }] : /* istanbul ignore next */ []));
|
|
425
|
+
stateIconColorClass = computed(() => {
|
|
426
|
+
switch (this.resolvedState()) {
|
|
427
|
+
case 'error': return 'text-red-500';
|
|
428
|
+
case 'success': return 'text-emerald-500';
|
|
429
|
+
case 'warning': return 'text-amber-500';
|
|
430
|
+
default: return 'text-gray-400';
|
|
431
|
+
}
|
|
432
|
+
}, ...(ngDevMode ? [{ debugName: "stateIconColorClass" }] : /* istanbul ignore next */ []));
|
|
433
|
+
hintColorClass = computed(() => {
|
|
434
|
+
switch (this.resolvedState()) {
|
|
435
|
+
case 'error': return 'text-red-600 dark:text-red-400';
|
|
436
|
+
case 'success': return 'text-emerald-600 dark:text-emerald-400';
|
|
437
|
+
case 'warning': return 'text-amber-600 dark:text-amber-400';
|
|
438
|
+
default: return 'text-gray-500 dark:text-gray-400';
|
|
439
|
+
}
|
|
440
|
+
}, ...(ngDevMode ? [{ debugName: "hintColorClass" }] : /* istanbul ignore next */ []));
|
|
441
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
442
|
+
focus() {
|
|
443
|
+
this.inputRef()?.nativeElement?.focus();
|
|
444
|
+
}
|
|
445
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
446
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvInputFieldComponent, isStandalone: true, selector: "sv-input-field", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, optionalTag: { classPropertyName: "optionalTag", publicName: "optionalTag", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, successMessage: { classPropertyName: "successMessage", publicName: "successMessage", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, leadingIconPath: { classPropertyName: "leadingIconPath", publicName: "leadingIconPath", isSignal: true, isRequired: false, transformFunction: null }, trailingIconPath: { classPropertyName: "trailingIconPath", publicName: "trailingIconPath", isSignal: true, isRequired: false, transformFunction: null }, leadingAddon: { classPropertyName: "leadingAddon", publicName: "leadingAddon", isSignal: true, isRequired: false, transformFunction: null }, trailingAddon: { classPropertyName: "trailingAddon", publicName: "trailingAddon", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, inputName: { classPropertyName: "inputName", publicName: "inputName", isSignal: true, isRequired: false, transformFunction: null }, customClass: { classPropertyName: "customClass", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, inputMode: { classPropertyName: "inputMode", publicName: "inputMode", isSignal: true, isRequired: false, transformFunction: null }, pattern: { classPropertyName: "pattern", publicName: "pattern", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, spellcheck: { classPropertyName: "spellcheck", publicName: "spellcheck", isSignal: true, isRequired: false, transformFunction: null }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", focused: "focused", blurred: "blurred", submitted: "submitted" }, providers: [
|
|
447
|
+
{
|
|
448
|
+
provide: NG_VALUE_ACCESSOR,
|
|
449
|
+
useExisting: forwardRef(() => SvInputFieldComponent),
|
|
450
|
+
multi: true,
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
provide: NG_VALIDATORS,
|
|
454
|
+
useExisting: forwardRef(() => SvInputFieldComponent),
|
|
455
|
+
multi: true,
|
|
456
|
+
},
|
|
457
|
+
], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Root wrapper -->\n<div [ngClass]=\"rootClasses()\" class=\"flex flex-col gap-1.5\">\n\n <!-- \u2500\u2500 Label row (hidden for floating variant) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (label()) {\n <div class=\"flex items-baseline justify-between gap-2\">\n <label\n [ngClass]=\"['font-medium text-gray-700 dark:text-gray-200', labelSizeClass()]\"\n [attr.for]=\"resolvedId()\"\n >\n {{ label() }}\n @if (required()) {\n <span class=\"ml-0.5 text-red-500\" aria-hidden=\"true\">*</span>\n }\n @if (optionalTag() && !required()) {\n <span class=\"ml-1.5 font-normal text-gray-500 dark:text-gray-400 text-xs\">(optional)</span>\n }\n </label>\n </div>\n }\n\n <!-- \u2500\u2500 Input track \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [ngClass]=\"trackClasses()\">\n <!-- Floating label (renders inside track, animates to border on focus/value) -->\n @if (isFloating() && label()) {\n <label\n [ngClass]=\"floatingLabelClasses()\"\n [attr.for]=\"resolvedId()\"\n >\n {{ label() }}\n @if (required()) {\n <span class=\"ml-0.5 text-red-500\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n <!-- Leading icon -->\n @if (leadingIconPath() && !leadingAddon()) {\n <span\n class=\"flex shrink-0 items-center\"\n [ngClass]=\"[iconPadClass(), 'text-gray-500 dark:text-gray-400']\"\n aria-hidden=\"true\"\n >\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"leadingIconPath()\" />\n </svg>\n </span>\n }\n\n <!-- Leading text addon (e.g. \"https://\") -->\n @if (leadingAddon() && !leadingIconPath()) {\n <span [ngClass]=\"leadingAddonClasses()\">{{ leadingAddon() }}</span>\n }\n\n <!-- \u2500\u2500 Native <select> \u2500\u2500 -->\n @if (isSelect()) {\n <select\n #inputEl\n [ngClass]=\"nativeInputClasses()\"\n [attr.id]=\"resolvedId()\"\n [attr.name]=\"inputName() || null\"\n [disabled]=\"resolvedDisabled()\"\n [attr.autofocus]=\"autofocus() ? '' : null\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"resolvedState() === 'error' || null\"\n [attr.aria-describedby]=\"hasDescription() ? describedById() : null\"\n [value]=\"value()\"\n (change)=\"onNativeChange($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n >\n @if (placeholder()) {\n <option value=\"\" disabled [selected]=\"!value()\">{{ isFloating() && !labelIsFloated() ? '' : placeholder() }}</option>\n }\n @for (opt of options(); track opt.value) {\n <option\n [value]=\"opt.value\"\n [disabled]=\"opt.disabled ?? false\"\n [selected]=\"value() === opt.value\"\n >{{ opt.label }}</option>\n }\n </select>\n\n <!-- \u2500\u2500 Native <textarea> \u2500\u2500 -->\n } @else if (multiline()) {\n <textarea\n #inputEl\n [ngClass]=\"nativeInputClasses()\"\n [attr.id]=\"resolvedId()\"\n [attr.name]=\"inputName() || null\"\n [placeholder]=\"isFloating() && !labelIsFloated() ? ' ' : placeholder()\"\n [disabled]=\"resolvedDisabled()\"\n [readOnly]=\"readonly()\"\n [attr.maxlength]=\"maxLength() > 0 ? maxLength() : null\"\n [attr.minlength]=\"minLength() > 0 ? minLength() : null\"\n [attr.spellcheck]=\"spellcheck()\"\n [attr.autofocus]=\"autofocus() ? '' : null\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"resolvedState() === 'error' || null\"\n [attr.aria-describedby]=\"hasDescription() ? describedById() : null\"\n [value]=\"value()\"\n (input)=\"onNativeChange($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n ></textarea>\n\n <!-- \u2500\u2500 Native <input> (default) \u2500\u2500 -->\n } @else {\n <input\n #inputEl\n [ngClass]=\"nativeInputClasses()\"\n [type]=\"resolvedType()\"\n [attr.id]=\"resolvedId()\"\n [attr.name]=\"inputName() || null\"\n [placeholder]=\"isFloating() && !labelIsFloated() ? ' ' : placeholder()\"\n [disabled]=\"resolvedDisabled()\"\n [readOnly]=\"readonly()\"\n [attr.maxlength]=\"maxLength() > 0 ? maxLength() : null\"\n [attr.minlength]=\"minLength() > 0 ? minLength() : null\"\n [attr.pattern]=\"pattern() || null\"\n [attr.min]=\"min()\"\n [attr.max]=\"max()\"\n [attr.step]=\"step()\"\n [attr.inputmode]=\"inputMode() || null\"\n [attr.spellcheck]=\"spellcheck()\"\n [attr.autofocus]=\"autofocus() ? '' : null\"\n [attr.autocomplete]=\"resolvedAutocomplete()\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"resolvedState() === 'error' || null\"\n [attr.aria-describedby]=\"hasDescription() ? describedById() : null\"\n [value]=\"value()\"\n (input)=\"onNativeChange($event)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n />\n }\n\n <!-- Loading spinner -->\n @if (loading()) {\n <span class=\"flex shrink-0 items-center pr-3 text-gray-400\" aria-hidden=\"true\">\n <svg [ngClass]=\"iconClass()\" class=\"animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\"\n stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-75\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"/>\n </svg>\n </span>\n\n <!-- Password eye toggle -->\n } @else if (isPassword() && !loading()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 items-center pr-3 text-gray-400 hover:text-gray-600\n dark:hover:text-gray-300 focus:outline-none focus-visible:ring-2\n focus-visible:ring-blue-400 rounded\"\n (click)=\"togglePassword()\"\n [attr.aria-label]=\"showPassword() ? 'Hide password' : 'Show password'\"\n [attr.aria-pressed]=\"showPassword()\"\n [attr.aria-controls]=\"resolvedId()\"\n >\n @if (showPassword()) {\n <!-- Eye-off -->\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n } @else {\n <!-- Eye -->\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/>\n <circle cx=\"12\" cy=\"12\" r=\"3\"/>\n </svg>\n }\n </button>\n\n <!-- State icon (error / success / warning) \u2014 only when no trailing icon/addon -->\n } @else if (stateIconPath() && !trailingIconPath() && !trailingAddon()) {\n <span\n class=\"flex shrink-0 items-center pr-3\"\n [ngClass]=\"stateIconColorClass()\"\n aria-hidden=\"true\"\n >\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"stateIconPath()\" />\n </svg>\n </span>\n\n <!-- Custom trailing icon -->\n } @else if (trailingIconPath() && !trailingAddon()) {\n <span\n class=\"flex shrink-0 items-center text-gray-500 dark:text-gray-400\"\n [ngClass]=\"iconPadClass()\"\n aria-hidden=\"true\"\n >\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"trailingIconPath()\" />\n </svg>\n </span>\n }\n\n <!-- Trailing text addon (e.g. \".com\", \"USD\") -->\n @if (trailingAddon() && !trailingIconPath()) {\n <span [ngClass]=\"trailingAddonClasses()\">{{ trailingAddon() }}</span>\n }\n\n </div>\n\n <!-- \u2500\u2500 Below-input row: hint / error / counter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showHint() || showError() || showSuccess() || showCounter()) {\n <div class=\"flex items-start justify-between gap-2\">\n\n <!-- Left: hint / error / success message -->\n <div>\n @if (showError()) {\n <p\n [attr.id]=\"describedById()\"\n class=\"flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400\"\n role=\"alert\"\n >\n <svg class=\"h-3.5 w-3.5 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/>\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n </svg>\n {{ errorMessage() }}\n </p>\n }\n @if (showSuccess()) {\n <p\n [attr.id]=\"describedById()\"\n class=\"flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400\"\n >\n <svg class=\"h-3.5 w-3.5 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3\"/>\n </svg>\n {{ successMessage() }}\n </p>\n }\n @if (showHint()) {\n <p\n [attr.id]=\"describedById()\"\n [ngClass]=\"['text-xs', hintColorClass()]\"\n >\n {{ hint() }}\n </p>\n }\n </div>\n\n <!-- Right: character counter -->\n @if (showCounter()) {\n <p\n class=\"shrink-0 text-xs tabular-nums\"\n [ngClass]=\"counterOver()\n ? 'font-semibold text-red-600 dark:text-red-400'\n : 'text-gray-500 dark:text-gray-400'\"\n aria-live=\"polite\"\n >\n {{ charCount() }} / {{ maxLength() }}\n </p>\n }\n\n </div>\n }\n\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
458
|
+
}
|
|
459
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvInputFieldComponent, decorators: [{
|
|
460
|
+
type: Component,
|
|
461
|
+
args: [{ selector: 'sv-input-field', standalone: true, imports: [NgClass], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
|
|
462
|
+
{
|
|
463
|
+
provide: NG_VALUE_ACCESSOR,
|
|
464
|
+
useExisting: forwardRef(() => SvInputFieldComponent),
|
|
465
|
+
multi: true,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
provide: NG_VALIDATORS,
|
|
469
|
+
useExisting: forwardRef(() => SvInputFieldComponent),
|
|
470
|
+
multi: true,
|
|
471
|
+
},
|
|
472
|
+
], template: "<!-- Root wrapper -->\n<div [ngClass]=\"rootClasses()\" class=\"flex flex-col gap-1.5\">\n\n <!-- \u2500\u2500 Label row (hidden for floating variant) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (label()) {\n <div class=\"flex items-baseline justify-between gap-2\">\n <label\n [ngClass]=\"['font-medium text-gray-700 dark:text-gray-200', labelSizeClass()]\"\n [attr.for]=\"resolvedId()\"\n >\n {{ label() }}\n @if (required()) {\n <span class=\"ml-0.5 text-red-500\" aria-hidden=\"true\">*</span>\n }\n @if (optionalTag() && !required()) {\n <span class=\"ml-1.5 font-normal text-gray-500 dark:text-gray-400 text-xs\">(optional)</span>\n }\n </label>\n </div>\n }\n\n <!-- \u2500\u2500 Input track \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [ngClass]=\"trackClasses()\">\n <!-- Floating label (renders inside track, animates to border on focus/value) -->\n @if (isFloating() && label()) {\n <label\n [ngClass]=\"floatingLabelClasses()\"\n [attr.for]=\"resolvedId()\"\n >\n {{ label() }}\n @if (required()) {\n <span class=\"ml-0.5 text-red-500\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n <!-- Leading icon -->\n @if (leadingIconPath() && !leadingAddon()) {\n <span\n class=\"flex shrink-0 items-center\"\n [ngClass]=\"[iconPadClass(), 'text-gray-500 dark:text-gray-400']\"\n aria-hidden=\"true\"\n >\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"leadingIconPath()\" />\n </svg>\n </span>\n }\n\n <!-- Leading text addon (e.g. \"https://\") -->\n @if (leadingAddon() && !leadingIconPath()) {\n <span [ngClass]=\"leadingAddonClasses()\">{{ leadingAddon() }}</span>\n }\n\n <!-- \u2500\u2500 Native <select> \u2500\u2500 -->\n @if (isSelect()) {\n <select\n #inputEl\n [ngClass]=\"nativeInputClasses()\"\n [attr.id]=\"resolvedId()\"\n [attr.name]=\"inputName() || null\"\n [disabled]=\"resolvedDisabled()\"\n [attr.autofocus]=\"autofocus() ? '' : null\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"resolvedState() === 'error' || null\"\n [attr.aria-describedby]=\"hasDescription() ? describedById() : null\"\n [value]=\"value()\"\n (change)=\"onNativeChange($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n >\n @if (placeholder()) {\n <option value=\"\" disabled [selected]=\"!value()\">{{ isFloating() && !labelIsFloated() ? '' : placeholder() }}</option>\n }\n @for (opt of options(); track opt.value) {\n <option\n [value]=\"opt.value\"\n [disabled]=\"opt.disabled ?? false\"\n [selected]=\"value() === opt.value\"\n >{{ opt.label }}</option>\n }\n </select>\n\n <!-- \u2500\u2500 Native <textarea> \u2500\u2500 -->\n } @else if (multiline()) {\n <textarea\n #inputEl\n [ngClass]=\"nativeInputClasses()\"\n [attr.id]=\"resolvedId()\"\n [attr.name]=\"inputName() || null\"\n [placeholder]=\"isFloating() && !labelIsFloated() ? ' ' : placeholder()\"\n [disabled]=\"resolvedDisabled()\"\n [readOnly]=\"readonly()\"\n [attr.maxlength]=\"maxLength() > 0 ? maxLength() : null\"\n [attr.minlength]=\"minLength() > 0 ? minLength() : null\"\n [attr.spellcheck]=\"spellcheck()\"\n [attr.autofocus]=\"autofocus() ? '' : null\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"resolvedState() === 'error' || null\"\n [attr.aria-describedby]=\"hasDescription() ? describedById() : null\"\n [value]=\"value()\"\n (input)=\"onNativeChange($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n ></textarea>\n\n <!-- \u2500\u2500 Native <input> (default) \u2500\u2500 -->\n } @else {\n <input\n #inputEl\n [ngClass]=\"nativeInputClasses()\"\n [type]=\"resolvedType()\"\n [attr.id]=\"resolvedId()\"\n [attr.name]=\"inputName() || null\"\n [placeholder]=\"isFloating() && !labelIsFloated() ? ' ' : placeholder()\"\n [disabled]=\"resolvedDisabled()\"\n [readOnly]=\"readonly()\"\n [attr.maxlength]=\"maxLength() > 0 ? maxLength() : null\"\n [attr.minlength]=\"minLength() > 0 ? minLength() : null\"\n [attr.pattern]=\"pattern() || null\"\n [attr.min]=\"min()\"\n [attr.max]=\"max()\"\n [attr.step]=\"step()\"\n [attr.inputmode]=\"inputMode() || null\"\n [attr.spellcheck]=\"spellcheck()\"\n [attr.autofocus]=\"autofocus() ? '' : null\"\n [attr.autocomplete]=\"resolvedAutocomplete()\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"resolvedState() === 'error' || null\"\n [attr.aria-describedby]=\"hasDescription() ? describedById() : null\"\n [value]=\"value()\"\n (input)=\"onNativeChange($event)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus($event)\"\n (blur)=\"onBlur($event)\"\n />\n }\n\n <!-- Loading spinner -->\n @if (loading()) {\n <span class=\"flex shrink-0 items-center pr-3 text-gray-400\" aria-hidden=\"true\">\n <svg [ngClass]=\"iconClass()\" class=\"animate-spin\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\"\n stroke=\"currentColor\" stroke-width=\"4\"/>\n <path class=\"opacity-75\" fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z\"/>\n </svg>\n </span>\n\n <!-- Password eye toggle -->\n } @else if (isPassword() && !loading()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 items-center pr-3 text-gray-400 hover:text-gray-600\n dark:hover:text-gray-300 focus:outline-none focus-visible:ring-2\n focus-visible:ring-blue-400 rounded\"\n (click)=\"togglePassword()\"\n [attr.aria-label]=\"showPassword() ? 'Hide password' : 'Show password'\"\n [attr.aria-pressed]=\"showPassword()\"\n [attr.aria-controls]=\"resolvedId()\"\n >\n @if (showPassword()) {\n <!-- Eye-off -->\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24M1 1l22 22\"/>\n </svg>\n } @else {\n <!-- Eye -->\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"/>\n <circle cx=\"12\" cy=\"12\" r=\"3\"/>\n </svg>\n }\n </button>\n\n <!-- State icon (error / success / warning) \u2014 only when no trailing icon/addon -->\n } @else if (stateIconPath() && !trailingIconPath() && !trailingAddon()) {\n <span\n class=\"flex shrink-0 items-center pr-3\"\n [ngClass]=\"stateIconColorClass()\"\n aria-hidden=\"true\"\n >\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"stateIconPath()\" />\n </svg>\n </span>\n\n <!-- Custom trailing icon -->\n } @else if (trailingIconPath() && !trailingAddon()) {\n <span\n class=\"flex shrink-0 items-center text-gray-500 dark:text-gray-400\"\n [ngClass]=\"iconPadClass()\"\n aria-hidden=\"true\"\n >\n <svg [ngClass]=\"iconClass()\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"trailingIconPath()\" />\n </svg>\n </span>\n }\n\n <!-- Trailing text addon (e.g. \".com\", \"USD\") -->\n @if (trailingAddon() && !trailingIconPath()) {\n <span [ngClass]=\"trailingAddonClasses()\">{{ trailingAddon() }}</span>\n }\n\n </div>\n\n <!-- \u2500\u2500 Below-input row: hint / error / counter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (showHint() || showError() || showSuccess() || showCounter()) {\n <div class=\"flex items-start justify-between gap-2\">\n\n <!-- Left: hint / error / success message -->\n <div>\n @if (showError()) {\n <p\n [attr.id]=\"describedById()\"\n class=\"flex items-center gap-1 text-xs font-medium text-red-600 dark:text-red-400\"\n role=\"alert\"\n >\n <svg class=\"h-3.5 w-3.5 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"10\"/>\n <line x1=\"12\" y1=\"8\" x2=\"12\" y2=\"12\"/>\n <line x1=\"12\" y1=\"16\" x2=\"12.01\" y2=\"16\"/>\n </svg>\n {{ errorMessage() }}\n </p>\n }\n @if (showSuccess()) {\n <p\n [attr.id]=\"describedById()\"\n class=\"flex items-center gap-1 text-xs font-medium text-emerald-600 dark:text-emerald-400\"\n >\n <svg class=\"h-3.5 w-3.5 shrink-0\" viewBox=\"0 0 24 24\" fill=\"none\"\n stroke=\"currentColor\" stroke-width=\"2\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3\"/>\n </svg>\n {{ successMessage() }}\n </p>\n }\n @if (showHint()) {\n <p\n [attr.id]=\"describedById()\"\n [ngClass]=\"['text-xs', hintColorClass()]\"\n >\n {{ hint() }}\n </p>\n }\n </div>\n\n <!-- Right: character counter -->\n @if (showCounter()) {\n <p\n class=\"shrink-0 text-xs tabular-nums\"\n [ngClass]=\"counterOver()\n ? 'font-semibold text-red-600 dark:text-red-400'\n : 'text-gray-500 dark:text-gray-400'\"\n aria-live=\"polite\"\n >\n {{ charCount() }} / {{ maxLength() }}\n </p>\n }\n\n </div>\n }\n\n</div>\n" }]
|
|
473
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], optionalTag: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionalTag", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], successMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "successMessage", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], leadingIconPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIconPath", required: false }] }], trailingIconPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIconPath", required: false }] }], leadingAddon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingAddon", required: false }] }], trailingAddon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingAddon", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], inputName: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputName", required: false }] }], customClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], inputMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputMode", required: false }] }], pattern: [{ type: i0.Input, args: [{ isSignal: true, alias: "pattern", required: false }] }], minLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minLength", required: false }] }], 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 }] }], spellcheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "spellcheck", required: false }] }], autofocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autofocus", required: false }] }], focused: [{ type: i0.Output, args: ["focused"] }], blurred: [{ type: i0.Output, args: ["blurred"] }], submitted: [{ type: i0.Output, args: ["submitted"] }], inputRef: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }] } });
|
|
474
|
+
|
|
475
|
+
const WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
476
|
+
const MONTH_NAMES = [
|
|
477
|
+
'January', 'February', 'March', 'April',
|
|
478
|
+
'May', 'June', 'July', 'August',
|
|
479
|
+
'September', 'October', 'November', 'December',
|
|
480
|
+
];
|
|
481
|
+
/** Panel width (px) — matches the 17.5 rem CSS width in the template. */
|
|
482
|
+
const PANEL_W = 280;
|
|
483
|
+
/** Conservative day-view panel height for flip calculation (px). */
|
|
484
|
+
const PANEL_H = 390;
|
|
485
|
+
/** Gap between trigger edge and panel (px). */
|
|
486
|
+
const GAP = 6;
|
|
487
|
+
/**
|
|
488
|
+
* SvDatePickerComponent — premium custom calendar date picker.
|
|
489
|
+
*
|
|
490
|
+
* The popover panel is rendered directly into `document.body` via a lightweight
|
|
491
|
+
* Angular embedded-view portal so it is completely outside any parent
|
|
492
|
+
* `overflow:hidden`, `overflow:auto`, or CSS `transform` containment.
|
|
493
|
+
* A capture-phase, RAF-throttled scroll listener keeps the panel anchored to
|
|
494
|
+
* the trigger — including when a nested scrollable ancestor scrolls, not just
|
|
495
|
+
* the window — and the horizontal position is clamped so it never renders
|
|
496
|
+
* partially outside the viewport. Resize closes the panel instead of
|
|
497
|
+
* repositioning it with stale coordinates.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```html
|
|
501
|
+
* <sv-date-picker label="Start Date" [(value)]="dateStr" minDate="2024-01-01" />
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
class SvDatePickerComponent {
|
|
505
|
+
// ─── Inputs ───────────────────────────────────────────────────────────────
|
|
506
|
+
/** Two-way bindable YYYY-MM-DD date string. */
|
|
507
|
+
value = model('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
508
|
+
/** Label rendered above the trigger. */
|
|
509
|
+
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
510
|
+
/** Placeholder shown when no date is selected. */
|
|
511
|
+
placeholder = input('Select date', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
512
|
+
/** Minimum selectable date (YYYY-MM-DD). Cells before it are disabled. */
|
|
513
|
+
minDate = input('', ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
|
|
514
|
+
/** Maximum selectable date (YYYY-MM-DD). Cells after it are disabled. */
|
|
515
|
+
maxDate = input('', ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
|
|
516
|
+
/** Disables the trigger and prevents the popover from opening. */
|
|
517
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
518
|
+
/** Shows a red asterisk in the label. */
|
|
519
|
+
required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
|
|
520
|
+
/** Explicit accessible name for the trigger (falls back to `label`). */
|
|
521
|
+
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
522
|
+
// ─── Outputs ──────────────────────────────────────────────────────────────
|
|
523
|
+
// Note: the `value` model already emits `valueChange` for `[(value)]`.
|
|
524
|
+
/** Emitted when the calendar popover opens. */
|
|
525
|
+
opened = output();
|
|
526
|
+
/** Emitted when the calendar popover closes. */
|
|
527
|
+
closed = output();
|
|
528
|
+
// ─── ControlValueAccessor ───────────────────────────────────────────────────
|
|
529
|
+
// Lets the picker bind to reactive forms (`formControlName` / `[formControl]`)
|
|
530
|
+
// and template-driven forms (`[(ngModel)]`) in addition to `[(value)]`.
|
|
531
|
+
_onChange = () => { };
|
|
532
|
+
_onTouched = () => { };
|
|
533
|
+
_formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : /* istanbul ignore next */ []));
|
|
534
|
+
/** Merged disabled — the `[disabled]` input OR a disabled parent FormControl. */
|
|
535
|
+
resolvedDisabled = computed(() => this.disabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "resolvedDisabled" }] : /* istanbul ignore next */ []));
|
|
536
|
+
writeValue(v) { this.value.set(v ?? ''); }
|
|
537
|
+
registerOnChange(fn) { this._onChange = fn; }
|
|
538
|
+
registerOnTouched(fn) { this._onTouched = fn; }
|
|
539
|
+
setDisabledState(isDisabled) {
|
|
540
|
+
this._formDisabled.set(isDisabled);
|
|
541
|
+
if (isDisabled && this.isOpen())
|
|
542
|
+
this.close();
|
|
543
|
+
}
|
|
544
|
+
/** Sets the value from a user gesture and notifies the form (change + touched). */
|
|
545
|
+
commitValue(v) {
|
|
546
|
+
this.value.set(v);
|
|
547
|
+
this._onChange(v);
|
|
548
|
+
this._onTouched();
|
|
549
|
+
}
|
|
550
|
+
// ─── View refs ────────────────────────────────────────────────────────────
|
|
551
|
+
/** The ng-template that holds the calendar panel markup. */
|
|
552
|
+
panelTpl;
|
|
553
|
+
// ─── Internal state ───────────────────────────────────────────────────────
|
|
554
|
+
isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
555
|
+
viewYear = signal(new Date().getFullYear(), ...(ngDevMode ? [{ debugName: "viewYear" }] : /* istanbul ignore next */ []));
|
|
556
|
+
viewMonth = signal(new Date().getMonth(), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
|
|
557
|
+
viewMode = signal('days', ...(ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []));
|
|
558
|
+
openAbove = signal(false, ...(ngDevMode ? [{ debugName: "openAbove" }] : /* istanbul ignore next */ []));
|
|
559
|
+
alignRight = signal(false, ...(ngDevMode ? [{ debugName: "alignRight" }] : /* istanbul ignore next */ []));
|
|
560
|
+
/** YYYY-MM-DD of the keyboard-focused day cell (roving tabindex). */
|
|
561
|
+
activeKey = signal('', ...(ngDevMode ? [{ debugName: "activeKey" }] : /* istanbul ignore next */ []));
|
|
562
|
+
/**
|
|
563
|
+
* Inline style object applied to the panel.
|
|
564
|
+
* Uses position:fixed with exact viewport coords so the panel
|
|
565
|
+
* escapes all parent overflow and transform containment.
|
|
566
|
+
*/
|
|
567
|
+
panelStyle = signal({}, ...(ngDevMode ? [{ debugName: "panelStyle" }] : /* istanbul ignore next */ []));
|
|
568
|
+
// ─── Portal state ─────────────────────────────────────────────────────────
|
|
569
|
+
/** Container div appended to document.body while the panel is open. */
|
|
570
|
+
portalEl = null;
|
|
571
|
+
/** Embedded view holding the panel template nodes. */
|
|
572
|
+
portalView = null;
|
|
573
|
+
/**
|
|
574
|
+
* Capture-phase scroll listener — fires for ANY scrollable ancestor
|
|
575
|
+
* (not just the window), RAF-throttled so it never runs more than once
|
|
576
|
+
* per frame. Repositions the panel to keep it anchored to the trigger
|
|
577
|
+
* instead of drifting or closing (see dropdown-instruction.md §6).
|
|
578
|
+
*/
|
|
579
|
+
_rafId = null;
|
|
580
|
+
_scrollHandler = () => {
|
|
581
|
+
if (!this.isOpen())
|
|
582
|
+
return;
|
|
583
|
+
if (this._rafId !== null)
|
|
584
|
+
return;
|
|
585
|
+
this._rafId = requestAnimationFrame(() => {
|
|
586
|
+
this._rafId = null;
|
|
587
|
+
if (this.isOpen()) {
|
|
588
|
+
this._calcPos();
|
|
589
|
+
this.portalView?.detectChanges();
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
// ─── Static data ──────────────────────────────────────────────────────────
|
|
594
|
+
weekDays = WEEKDAYS;
|
|
595
|
+
monthNames = MONTH_NAMES;
|
|
596
|
+
yearRange = computed(() => {
|
|
597
|
+
const base = Math.floor(this.viewYear() / 10) * 10 - 5;
|
|
598
|
+
return Array.from({ length: 20 }, (_, i) => base + i);
|
|
599
|
+
}, ...(ngDevMode ? [{ debugName: "yearRange" }] : /* istanbul ignore next */ []));
|
|
600
|
+
displayValue = computed(() => {
|
|
601
|
+
const v = this.value();
|
|
602
|
+
if (!v)
|
|
603
|
+
return '';
|
|
604
|
+
return new Date(v + 'T00:00:00').toLocaleDateString('en-US', {
|
|
605
|
+
month: 'short', day: 'numeric', year: 'numeric',
|
|
606
|
+
});
|
|
607
|
+
}, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
|
|
608
|
+
monthName = computed(() => MONTH_NAMES[this.viewMonth()], ...(ngDevMode ? [{ debugName: "monthName" }] : /* istanbul ignore next */ []));
|
|
609
|
+
/** Accessible name for the trigger — combines the field name with the selection. */
|
|
610
|
+
triggerAriaLabel = computed(() => {
|
|
611
|
+
const name = this.ariaLabel() || this.label();
|
|
612
|
+
if (!name)
|
|
613
|
+
return null;
|
|
614
|
+
return this.displayValue() ? `${name}: ${this.displayValue()}` : `${name}: ${this.placeholder()}`;
|
|
615
|
+
}, ...(ngDevMode ? [{ debugName: "triggerAriaLabel" }] : /* istanbul ignore next */ []));
|
|
616
|
+
calendarDays = computed(() => {
|
|
617
|
+
const year = this.viewYear();
|
|
618
|
+
const month = this.viewMonth();
|
|
619
|
+
const today = new Date();
|
|
620
|
+
today.setHours(0, 0, 0, 0);
|
|
621
|
+
const sel = this.value();
|
|
622
|
+
const selTs = sel ? new Date(sel + 'T00:00:00').getTime() : NaN;
|
|
623
|
+
const firstOfMonth = new Date(year, month, 1);
|
|
624
|
+
const mondayOffset = (firstOfMonth.getDay() + 6) % 7;
|
|
625
|
+
const cursor = new Date(firstOfMonth);
|
|
626
|
+
cursor.setDate(1 - mondayOffset);
|
|
627
|
+
const days = [];
|
|
628
|
+
for (let i = 0; i < 42; i++) {
|
|
629
|
+
const ts = cursor.getTime();
|
|
630
|
+
days.push({
|
|
631
|
+
date: new Date(cursor),
|
|
632
|
+
label: cursor.getDate(),
|
|
633
|
+
isCurrentMonth: cursor.getMonth() === month,
|
|
634
|
+
isToday: ts === today.getTime(),
|
|
635
|
+
isSelected: ts === selTs,
|
|
636
|
+
isDisabled: this.isDateDisabled(cursor),
|
|
637
|
+
key: this.toDateStr(cursor),
|
|
638
|
+
});
|
|
639
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
640
|
+
}
|
|
641
|
+
return days;
|
|
642
|
+
}, ...(ngDevMode ? [{ debugName: "calendarDays" }] : /* istanbul ignore next */ []));
|
|
643
|
+
/** The 42 calendar days grouped into 6 weeks for `role="row"` grid semantics. */
|
|
644
|
+
calendarWeeks = computed(() => {
|
|
645
|
+
const days = this.calendarDays();
|
|
646
|
+
const weeks = [];
|
|
647
|
+
for (let i = 0; i < days.length; i += 7) {
|
|
648
|
+
weeks.push(days.slice(i, i + 7));
|
|
649
|
+
}
|
|
650
|
+
return weeks;
|
|
651
|
+
}, ...(ngDevMode ? [{ debugName: "calendarWeeks" }] : /* istanbul ignore next */ []));
|
|
652
|
+
// ─── DI ───────────────────────────────────────────────────────────────────
|
|
653
|
+
el = inject(ElementRef);
|
|
654
|
+
appRef = inject(ApplicationRef);
|
|
655
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
656
|
+
ngOnInit() {
|
|
657
|
+
// Capture phase catches scroll from any scrollable ancestor, not just window.
|
|
658
|
+
window.addEventListener('scroll', this._scrollHandler, { passive: true, capture: true });
|
|
659
|
+
}
|
|
660
|
+
ngOnDestroy() {
|
|
661
|
+
window.removeEventListener('scroll', this._scrollHandler, true);
|
|
662
|
+
if (this._rafId !== null)
|
|
663
|
+
cancelAnimationFrame(this._rafId);
|
|
664
|
+
this.destroyPortal();
|
|
665
|
+
}
|
|
666
|
+
// ─── Global listeners ─────────────────────────────────────────────────────
|
|
667
|
+
onDocumentClick(event) {
|
|
668
|
+
if (!this.isOpen())
|
|
669
|
+
return;
|
|
670
|
+
const t = event.target;
|
|
671
|
+
// Keep open if click is inside the trigger OR inside the portal panel
|
|
672
|
+
if (this.el.nativeElement.contains(t))
|
|
673
|
+
return;
|
|
674
|
+
if (this.portalEl?.contains(t))
|
|
675
|
+
return;
|
|
676
|
+
this.close();
|
|
677
|
+
}
|
|
678
|
+
onEscape() {
|
|
679
|
+
if (this.isOpen())
|
|
680
|
+
this.close();
|
|
681
|
+
}
|
|
682
|
+
onResize() {
|
|
683
|
+
// Viewport resize invalidates the layout — close rather than reposition
|
|
684
|
+
// with now-stale coordinates (dropdown-instruction.md §8).
|
|
685
|
+
if (this.isOpen())
|
|
686
|
+
this.close();
|
|
687
|
+
}
|
|
688
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
689
|
+
toggle() {
|
|
690
|
+
if (this.resolvedDisabled())
|
|
691
|
+
return;
|
|
692
|
+
if (this.isOpen()) {
|
|
693
|
+
this.close();
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// 1. Sync view to selected date / today
|
|
697
|
+
const sel = this.value();
|
|
698
|
+
const ref = sel ? new Date(sel + 'T00:00:00') : new Date();
|
|
699
|
+
this.viewYear.set(ref.getFullYear());
|
|
700
|
+
this.viewMonth.set(ref.getMonth());
|
|
701
|
+
this.viewMode.set('days');
|
|
702
|
+
this.activeKey.set(this.toDateStr(ref));
|
|
703
|
+
// 2. Calculate fixed-position coordinates from the trigger's viewport rect.
|
|
704
|
+
this._calcPos();
|
|
705
|
+
// 3. Mount the panel into document.body via a lightweight portal.
|
|
706
|
+
// This escapes ALL parent overflow:hidden and CSS transform contexts.
|
|
707
|
+
if (!this.panelTpl)
|
|
708
|
+
return;
|
|
709
|
+
this.portalEl = document.createElement('div');
|
|
710
|
+
this.portalEl.className = 'sv-dp-portal';
|
|
711
|
+
document.body.appendChild(this.portalEl);
|
|
712
|
+
this.portalView = this.panelTpl.createEmbeddedView({});
|
|
713
|
+
this.appRef.attachView(this.portalView);
|
|
714
|
+
this.portalView.rootNodes.forEach((n) => this.portalEl.appendChild(n));
|
|
715
|
+
this.portalView.detectChanges();
|
|
716
|
+
this.isOpen.set(true);
|
|
717
|
+
this.opened.emit();
|
|
718
|
+
// Move focus into the calendar grid for keyboard users.
|
|
719
|
+
this.focusActiveDayDeferred();
|
|
720
|
+
}
|
|
721
|
+
close(returnFocus = false) {
|
|
722
|
+
if (!this.isOpen())
|
|
723
|
+
return;
|
|
724
|
+
this.destroyPortal();
|
|
725
|
+
this.isOpen.set(false);
|
|
726
|
+
this.viewMode.set('days');
|
|
727
|
+
this._onTouched();
|
|
728
|
+
this.closed.emit();
|
|
729
|
+
if (returnFocus) {
|
|
730
|
+
this.el.nativeElement
|
|
731
|
+
.querySelector('.sv-dp-trigger')?.focus();
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
selectDay(day) {
|
|
735
|
+
if (day.isDisabled)
|
|
736
|
+
return;
|
|
737
|
+
this.commitValue(day.key);
|
|
738
|
+
this.close(true);
|
|
739
|
+
}
|
|
740
|
+
// ─── Keyboard navigation (day grid) ─────────────────────────────────────────
|
|
741
|
+
/** Roving tabindex — only the active day cell is in the tab order. */
|
|
742
|
+
dayTabIndex(day) {
|
|
743
|
+
return day.key === this.activeKey() ? 0 : -1;
|
|
744
|
+
}
|
|
745
|
+
/** Arrow / Home / End / PageUp / PageDown navigation + Enter/Space to select. */
|
|
746
|
+
onDayKeydown(event) {
|
|
747
|
+
if (this.viewMode() !== 'days')
|
|
748
|
+
return;
|
|
749
|
+
const base = this.activeKey() || this.value() || this.toDateStr(new Date());
|
|
750
|
+
const d = new Date(base + 'T00:00:00');
|
|
751
|
+
switch (event.key) {
|
|
752
|
+
case 'ArrowLeft':
|
|
753
|
+
d.setDate(d.getDate() - 1);
|
|
754
|
+
break;
|
|
755
|
+
case 'ArrowRight':
|
|
756
|
+
d.setDate(d.getDate() + 1);
|
|
757
|
+
break;
|
|
758
|
+
case 'ArrowUp':
|
|
759
|
+
d.setDate(d.getDate() - 7);
|
|
760
|
+
break;
|
|
761
|
+
case 'ArrowDown':
|
|
762
|
+
d.setDate(d.getDate() + 7);
|
|
763
|
+
break;
|
|
764
|
+
case 'Home':
|
|
765
|
+
d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
|
|
766
|
+
break; // Monday
|
|
767
|
+
case 'End':
|
|
768
|
+
d.setDate(d.getDate() + (6 - ((d.getDay() + 6) % 7)));
|
|
769
|
+
break; // Sunday
|
|
770
|
+
case 'PageUp':
|
|
771
|
+
event.shiftKey ? d.setFullYear(d.getFullYear() - 1) : d.setMonth(d.getMonth() - 1);
|
|
772
|
+
break;
|
|
773
|
+
case 'PageDown':
|
|
774
|
+
event.shiftKey ? d.setFullYear(d.getFullYear() + 1) : d.setMonth(d.getMonth() + 1);
|
|
775
|
+
break;
|
|
776
|
+
case 'Enter':
|
|
777
|
+
case ' ': {
|
|
778
|
+
event.preventDefault();
|
|
779
|
+
const target = this.calendarDays().find(c => c.key === this.activeKey());
|
|
780
|
+
if (target)
|
|
781
|
+
this.selectDay(target);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
default: return;
|
|
785
|
+
}
|
|
786
|
+
event.preventDefault();
|
|
787
|
+
this.viewYear.set(d.getFullYear());
|
|
788
|
+
this.viewMonth.set(d.getMonth());
|
|
789
|
+
this.activeKey.set(this.toDateStr(d));
|
|
790
|
+
this.portalView?.detectChanges();
|
|
791
|
+
this.focusActiveDayDeferred();
|
|
792
|
+
}
|
|
793
|
+
focusActiveDayDeferred() {
|
|
794
|
+
setTimeout(() => {
|
|
795
|
+
const btn = this.portalEl?.querySelector(`[data-day="${this.activeKey()}"]`);
|
|
796
|
+
btn?.focus();
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
selectMonth(monthIndex) {
|
|
800
|
+
this.viewMonth.set(monthIndex);
|
|
801
|
+
this.viewMode.set('days');
|
|
802
|
+
}
|
|
803
|
+
selectYear(year) {
|
|
804
|
+
this.viewYear.set(year);
|
|
805
|
+
this.viewMode.set('months');
|
|
806
|
+
}
|
|
807
|
+
prevPeriod() {
|
|
808
|
+
if (this.viewMode() === 'years') {
|
|
809
|
+
this.viewYear.update(y => y - 20);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (this.viewMode() === 'months') {
|
|
813
|
+
this.viewYear.update(y => y - 1);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
let m = this.viewMonth() - 1, y = this.viewYear();
|
|
817
|
+
if (m < 0) {
|
|
818
|
+
m = 11;
|
|
819
|
+
y--;
|
|
820
|
+
}
|
|
821
|
+
this.viewMonth.set(m);
|
|
822
|
+
this.viewYear.set(y);
|
|
823
|
+
}
|
|
824
|
+
nextPeriod() {
|
|
825
|
+
if (this.viewMode() === 'years') {
|
|
826
|
+
this.viewYear.update(y => y + 20);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (this.viewMode() === 'months') {
|
|
830
|
+
this.viewYear.update(y => y + 1);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
let m = this.viewMonth() + 1, y = this.viewYear();
|
|
834
|
+
if (m > 11) {
|
|
835
|
+
m = 0;
|
|
836
|
+
y++;
|
|
837
|
+
}
|
|
838
|
+
this.viewMonth.set(m);
|
|
839
|
+
this.viewYear.set(y);
|
|
840
|
+
}
|
|
841
|
+
goToToday() {
|
|
842
|
+
this.commitValue(this.toDateStr(new Date()));
|
|
843
|
+
this.close(true);
|
|
844
|
+
}
|
|
845
|
+
clearDate() {
|
|
846
|
+
this.commitValue('');
|
|
847
|
+
this.close(true);
|
|
848
|
+
}
|
|
849
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
850
|
+
toDateStr(d) {
|
|
851
|
+
const y = d.getFullYear();
|
|
852
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
853
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
854
|
+
return `${y}-${m}-${day}`;
|
|
855
|
+
}
|
|
856
|
+
isDateDisabled(d) {
|
|
857
|
+
const str = this.toDateStr(d);
|
|
858
|
+
const min = this.minDate(), max = this.maxDate();
|
|
859
|
+
return (!!min && str < min) || (!!max && str > max);
|
|
860
|
+
}
|
|
861
|
+
isMonthDisabled(idx) {
|
|
862
|
+
const y = this.viewYear(), min = this.minDate(), max = this.maxDate();
|
|
863
|
+
if (min && this.toDateStr(new Date(y, idx + 1, 0)) < min)
|
|
864
|
+
return true;
|
|
865
|
+
if (max && this.toDateStr(new Date(y, idx, 1)) > max)
|
|
866
|
+
return true;
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
isYearDisabled(year) {
|
|
870
|
+
const min = this.minDate(), max = this.maxDate();
|
|
871
|
+
if (min && this.toDateStr(new Date(year, 11, 31)) < min)
|
|
872
|
+
return true;
|
|
873
|
+
if (max && this.toDateStr(new Date(year, 0, 1)) > max)
|
|
874
|
+
return true;
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
isViewMonthSelected(idx) {
|
|
878
|
+
const v = this.value();
|
|
879
|
+
if (!v)
|
|
880
|
+
return false;
|
|
881
|
+
const d = new Date(v + 'T00:00:00');
|
|
882
|
+
return d.getFullYear() === this.viewYear() && d.getMonth() === idx;
|
|
883
|
+
}
|
|
884
|
+
isViewYearSelected(year) {
|
|
885
|
+
const v = this.value();
|
|
886
|
+
if (!v)
|
|
887
|
+
return false;
|
|
888
|
+
return new Date(v + 'T00:00:00').getFullYear() === year;
|
|
889
|
+
}
|
|
890
|
+
headerLabel() {
|
|
891
|
+
if (this.viewMode() === 'months')
|
|
892
|
+
return String(this.viewYear());
|
|
893
|
+
if (this.viewMode() === 'years') {
|
|
894
|
+
const r = this.yearRange();
|
|
895
|
+
return `${r[0]} – ${r[r.length - 1]}`;
|
|
896
|
+
}
|
|
897
|
+
return `${this.monthName()} ${this.viewYear()}`;
|
|
898
|
+
}
|
|
899
|
+
// ─── Position calculation ───────────────────────────────────────────────────
|
|
900
|
+
/**
|
|
901
|
+
* Computes fixed-position coordinates from the trigger's current viewport
|
|
902
|
+
* rect. position:fixed is viewport-relative — getBoundingClientRect() gives
|
|
903
|
+
* exactly that, regardless of scroll position. Called on open and again on
|
|
904
|
+
* every scroll (see _scrollHandler) so the panel tracks the trigger instead
|
|
905
|
+
* of drifting away from it as the page (or a nested container) scrolls.
|
|
906
|
+
*/
|
|
907
|
+
_calcPos() {
|
|
908
|
+
const rect = this.el.nativeElement.getBoundingClientRect();
|
|
909
|
+
const vw = window.innerWidth;
|
|
910
|
+
const vh = window.innerHeight;
|
|
911
|
+
const spaceBelow = vh - rect.bottom;
|
|
912
|
+
const spaceAbove = rect.top;
|
|
913
|
+
const above = spaceBelow < PANEL_H && spaceAbove > spaceBelow;
|
|
914
|
+
const right = rect.left + PANEL_W > vw;
|
|
915
|
+
this.openAbove.set(above);
|
|
916
|
+
this.alignRight.set(right);
|
|
917
|
+
// Unify left/right alignment into a single clamped `left` value so the
|
|
918
|
+
// panel can never render partially outside the viewport horizontally.
|
|
919
|
+
const desiredLeft = right ? rect.right - PANEL_W : rect.left;
|
|
920
|
+
const maxLeft = Math.max(GAP, vw - PANEL_W - GAP);
|
|
921
|
+
const clampedLeft = Math.min(Math.max(desiredLeft, GAP), maxLeft);
|
|
922
|
+
const style = {
|
|
923
|
+
position: 'fixed',
|
|
924
|
+
zIndex: '9999',
|
|
925
|
+
width: `${PANEL_W}px`,
|
|
926
|
+
left: `${clampedLeft}px`,
|
|
927
|
+
};
|
|
928
|
+
style[above ? 'bottom' : 'top'] = above
|
|
929
|
+
? `${vh - rect.top + GAP}px`
|
|
930
|
+
: `${rect.bottom + GAP}px`;
|
|
931
|
+
this.panelStyle.set(style);
|
|
932
|
+
}
|
|
933
|
+
// ─── Portal lifecycle ──────────────────────────────────────────────────────
|
|
934
|
+
destroyPortal() {
|
|
935
|
+
if (this.portalView) {
|
|
936
|
+
this.appRef.detachView(this.portalView);
|
|
937
|
+
this.portalView.destroy();
|
|
938
|
+
this.portalView = null;
|
|
939
|
+
}
|
|
940
|
+
this.portalEl?.remove();
|
|
941
|
+
this.portalEl = null;
|
|
942
|
+
}
|
|
943
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvDatePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
944
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvDatePickerComponent, isStandalone: true, selector: "sv-date-picker", inputs: { value: { classPropertyName: "value", publicName: "value", 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 }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "keydown.escape": "onEscape()", "window:resize": "onResize()" } }, providers: [
|
|
945
|
+
{
|
|
946
|
+
provide: NG_VALUE_ACCESSOR,
|
|
947
|
+
useExisting: forwardRef(() => SvDatePickerComponent),
|
|
948
|
+
multi: true,
|
|
949
|
+
},
|
|
950
|
+
], viewQueries: [{ propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, read: TemplateRef }], ngImport: i0, template: "<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n SvDatePickerComponent \u2014 premium custom calendar date picker\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n\n<div class=\"sv-dp relative w-full\">\n\n <!-- \u2500\u2500 Label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (label()) {\n <label class=\"mb-1.5 block text-xs font-semibold text-gray-600 dark:text-gray-400\">\n {{ label() }}\n @if (required()) {\n <span class=\"ml-0.5 text-rose-500\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <!-- \u2500\u2500 Trigger Button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"relative\">\n <button\n type=\"button\"\n class=\"sv-dp-trigger flex w-full items-center gap-2.5 rounded-md border border-gray-200 bg-white px-3.5 py-2.5 text-left text-sm shadow-sm transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/70 dark:border-gray-700 dark:bg-gray-900\"\n [class.opacity-50]=\"resolvedDisabled()\"\n [class.cursor-not-allowed]=\"resolvedDisabled()\"\n [class.hover:border-primary-400]=\"!resolvedDisabled()\"\n [class.hover:shadow-md]=\"!resolvedDisabled()\"\n [class.border-primary-400]=\"isOpen()\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-primary-400\\/30]=\"isOpen()\"\n [disabled]=\"resolvedDisabled()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-label]=\"triggerAriaLabel()\"\n aria-haspopup=\"dialog\"\n (click)=\"toggle()\">\n\n <!-- Calendar icon -->\n <svg class=\"h-4 w-4 shrink-0 text-gray-400 transition-colors duration-150 dark:text-gray-300\"\n [class.text-primary-500]=\"isOpen()\"\n [class.dark:text-primary-400]=\"isOpen()\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5\"/>\n </svg>\n\n <!-- Date display / placeholder -->\n <span class=\"flex-1 truncate\" [class.pr-6]=\"value() && !resolvedDisabled()\">\n @if (displayValue()) {\n <span class=\"font-medium text-gray-800 dark:text-gray-100\">{{ displayValue() }}</span>\n } @else {\n <span class=\"text-gray-500 dark:text-gray-400\">{{ placeholder() }}</span>\n }\n </span>\n\n <!-- Chevron indicator -->\n <svg\n class=\"h-4 w-4 shrink-0 text-gray-300 transition-transform duration-200 dark:text-gray-500\"\n [class.rotate-180]=\"isOpen()\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m6 9 6 6 6-6\"/>\n </svg>\n </button>\n\n <!-- Clear button \u2014 sibling of the trigger (not nested inside it) -->\n @if (value() && !resolvedDisabled()) {\n <button\n type=\"button\"\n class=\"absolute right-9 top-1/2 flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded-full text-gray-300 transition-colors duration-100 hover:bg-gray-100 hover:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 dark:hover:bg-gray-800 dark:hover:text-gray-400\"\n aria-label=\"Clear date\"\n (click)=\"clearDate(); $event.stopPropagation()\">\n <svg class=\"h-3 w-3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\"/>\n </svg>\n </button>\n }\n </div>\n\n <!-- \u2500\u2500 Calendar Popover \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered into document.body via a body portal in toggle() so it escapes\n all parent overflow:hidden / CSS transform containment.\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <ng-template #panelTpl>\n <div\n class=\"sv-dp-panel overflow-hidden rounded-2xl border border-gray-100/80 bg-white shadow-[0_24px_64px_-12px_rgba(0,0,0,0.18),0_8px_24px_-8px_rgba(0,0,0,0.08)] dark:border-gray-700/50 dark:bg-gray-900 dark:shadow-[0_24px_64px_-12px_rgba(0,0,0,0.7),0_8px_24px_-8px_rgba(0,0,0,0.5)]\"\n [class.sv-dp-panel--above]=\"openAbove()\"\n [class.sv-dp-panel--right]=\"alignRight()\"\n [style.position]=\"panelStyle()['position']\"\n [style.zIndex]=\"panelStyle()['zIndex']\"\n [style.width]=\"panelStyle()['width']\"\n [style.top]=\"panelStyle()['top']\"\n [style.bottom]=\"panelStyle()['bottom']\"\n [style.left]=\"panelStyle()['left']\"\n [style.right]=\"panelStyle()['right']\"\n role=\"dialog\"\n aria-modal=\"false\"\n aria-label=\"Date picker calendar\">\n\n <!-- \u2500\u2500 Gradient Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"sv-dp-header bg-gradient-to-br from-primary-500 via-primary-600 to-indigo-600 px-4 pt-3.5 pb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n\n <!-- Prev button -->\n <button\n type=\"button\"\n class=\"flex h-7 w-7 items-center justify-center rounded-lg text-white/80 transition-all duration-150 hover:bg-white/15 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50\"\n aria-label=\"Previous\"\n (click)=\"prevPeriod()\">\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m15 19-7-7 7-7\"/>\n </svg>\n </button>\n\n <!-- Month / Year title \u2014 clicking cycles view mode -->\n <button\n type=\"button\"\n class=\"flex flex-1 items-center justify-center gap-1.5 rounded-lg px-2 py-1 text-sm font-semibold tracking-wide text-white transition-all duration-150 hover:bg-white/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50\"\n [attr.aria-label]=\"'Switch view, currently ' + headerLabel()\"\n (click)=\"viewMode.set(viewMode() === 'days' ? 'months' : viewMode() === 'months' ? 'years' : 'days')\">\n {{ headerLabel() }}\n <svg class=\"h-3.5 w-3.5 text-white/70\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m6 9 6 6 6-6\"/>\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"flex h-7 w-7 items-center justify-center rounded-lg text-white/80 transition-all duration-150 hover:bg-white/15 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50\"\n aria-label=\"Next\"\n (click)=\"nextPeriod()\">\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m9 5 7 7-7 7\"/>\n </svg>\n </button>\n </div>\n @if (displayValue()) {\n <div class=\"mt-2.5 rounded-xl bg-white/15 py-1.5 text-center text-[13px] font-semibold tracking-wide text-white backdrop-blur-sm\">\n {{ displayValue() }}\n </div>\n }\n </div>\n\n <!-- \u2500\u2500 Day View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode() === 'days') {\n <div class=\"sv-dp-body p-3\">\n\n <!-- Calendar grid -->\n <div role=\"grid\" [attr.aria-label]=\"monthName() + ' ' + viewYear()\">\n\n <!-- Weekday headers -->\n <div role=\"row\" class=\"mb-2 grid grid-cols-7 overflow-hidden rounded-xl bg-gray-50 dark:bg-gray-800/60\">\n @for (wd of weekDays; track wd) {\n <div role=\"columnheader\" class=\"py-1.5 text-center text-[10px] font-bold uppercase tracking-widest text-gray-500 dark:text-gray-400\">\n {{ wd }}\n </div>\n }\n </div>\n\n <!-- Day rows -->\n <div role=\"rowgroup\" class=\"grid grid-cols-7\" (keydown)=\"onDayKeydown($event)\">\n @for (week of calendarWeeks(); track $index) {\n <div role=\"row\" class=\"contents\">\n @for (day of week; track day.key) {\n <div role=\"gridcell\" [attr.aria-selected]=\"day.isSelected\" class=\"flex items-center justify-center p-0.5\">\n <button\n type=\"button\"\n [attr.data-day]=\"day.key\"\n [attr.tabindex]=\"dayTabIndex(day)\"\n class=\"sv-dp-day relative flex h-8 w-8 items-center justify-center rounded-md text-[13px] font-medium transition-all duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60\"\n [ngClass]=\"{\n 'bg-gradient-to-br from-primary-500 to-indigo-600 text-white shadow-md shadow-primary-500/30 hover:from-primary-400 hover:to-indigo-500 scale-105': day.isSelected,\n 'text-gray-800 hover:bg-primary-50 hover:text-primary-700 dark:text-gray-200 dark:hover:bg-primary-950/60 dark:hover:text-primary-300': !day.isSelected && !day.isDisabled && day.isCurrentMonth,\n 'font-semibold text-primary-600 dark:text-primary-400': day.isToday && !day.isSelected,\n 'text-gray-300 hover:bg-gray-50 dark:text-gray-700 dark:hover:bg-gray-900': !day.isSelected && !day.isDisabled && !day.isCurrentMonth,\n 'cursor-not-allowed text-gray-200 dark:text-gray-800': day.isDisabled\n }\"\n [disabled]=\"day.isDisabled\"\n [attr.aria-label]=\"day.date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"selectDay(day)\">\n {{ day.label }}\n <!-- Today dot -->\n @if (day.isToday && !day.isSelected) {\n <span class=\"absolute bottom-0.5 left-1/2 h-1.5 w-1.5 -translate-x-1/2 rounded-full bg-primary-500 dark:bg-primary-400\"></span>\n }\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n </div>\n\n <!-- Footer actions -->\n <div class=\"mt-2 flex items-center justify-between border-t border-gray-100 pt-2.5 dark:border-gray-800/80\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2.5 py-1.5 text-xs font-medium text-gray-400 transition-all duration-100 hover:bg-gray-100 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-300\"\n (click)=\"clearDate()\">\n Clear\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg bg-primary-500 px-3.5 py-1.5 text-xs font-semibold text-white shadow-sm shadow-primary-500/30 transition-all duration-100 hover:bg-primary-600 active:bg-primary-700\"\n (click)=\"goToToday()\">\n Today\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Month View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode() === 'months') {\n <div class=\"sv-dp-body grid grid-cols-3 gap-1.5 p-4\">\n @for (month of monthNames; track $index) {\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-2.5 text-center text-[12px] font-medium transition-all duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60\"\n [ngClass]=\"{\n 'bg-gradient-to-br from-primary-500 to-indigo-600 text-white shadow-sm shadow-primary-500/25': isViewMonthSelected($index),\n 'bg-primary-50 text-primary-700 dark:bg-primary-950/60 dark:text-primary-300': !isViewMonthSelected($index) && $index === viewMonth() && !isMonthDisabled($index),\n 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800/70': !isViewMonthSelected($index) && $index !== viewMonth() && !isMonthDisabled($index),\n 'cursor-not-allowed text-gray-300 dark:text-gray-700': isMonthDisabled($index)\n }\"\n [disabled]=\"isMonthDisabled($index)\"\n (click)=\"selectMonth($index)\">\n {{ month.slice(0, 3) }}\n </button>\n }\n </div>\n }\n\n <!-- \u2500\u2500 Year View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode() === 'years') {\n <div class=\"sv-dp-body grid grid-cols-4 gap-1 p-4\">\n @for (year of yearRange(); track year) {\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-2.5 text-center text-[12px] font-medium transition-all duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60\"\n [ngClass]=\"{\n 'bg-gradient-to-br from-primary-500 to-indigo-600 text-white shadow-sm shadow-primary-500/25': isViewYearSelected(year),\n 'bg-primary-50 text-primary-700 dark:bg-primary-950/60 dark:text-primary-300': !isViewYearSelected(year) && year === viewYear() && !isYearDisabled(year),\n 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800/70': !isViewYearSelected(year) && year !== viewYear() && !isYearDisabled(year),\n 'cursor-not-allowed text-gray-300 dark:text-gray-700': isYearDisabled(year)\n }\"\n [disabled]=\"isYearDisabled(year)\"\n (click)=\"selectYear(year)\">\n {{ year }}\n </button>\n }\n </div>\n }\n\n </div><!-- /sv-dp-panel -->\n </ng-template>\n\n</div><!-- /sv-dp -->\n", styles: [".sv-dp-panel{animation:dp-pop-in .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}.sv-dp-panel--above{transform-origin:bottom left;animation:dp-pop-in-above .18s cubic-bezier(.22,1,.36,1) both}.sv-dp-panel--right{transform-origin:top right}.sv-dp-panel--above.sv-dp-panel--right{transform-origin:bottom right}@keyframes dp-pop-in{0%{opacity:0;transform:scale(.95) translateY(-6px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes dp-pop-in-above{0%{opacity:0;transform:scale(.95) translateY(6px)}to{opacity:1;transform:scale(1) translateY(0)}}.sv-dp-body{animation:dp-fade-slide .15s ease-out both}@keyframes dp-fade-slide{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.sv-dp{min-width:0}.sv-dp-trigger:focus-visible{outline:none}.sv-dp-day:not(:disabled):active{transform:scale(.92)}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
951
|
+
}
|
|
952
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvDatePickerComponent, decorators: [{
|
|
953
|
+
type: Component,
|
|
954
|
+
args: [{ selector: 'sv-date-picker', standalone: true, imports: [NgClass], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, providers: [
|
|
955
|
+
{
|
|
956
|
+
provide: NG_VALUE_ACCESSOR,
|
|
957
|
+
useExisting: forwardRef(() => SvDatePickerComponent),
|
|
958
|
+
multi: true,
|
|
959
|
+
},
|
|
960
|
+
], template: "<!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n SvDatePickerComponent \u2014 premium custom calendar date picker\n \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n\n<div class=\"sv-dp relative w-full\">\n\n <!-- \u2500\u2500 Label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (label()) {\n <label class=\"mb-1.5 block text-xs font-semibold text-gray-600 dark:text-gray-400\">\n {{ label() }}\n @if (required()) {\n <span class=\"ml-0.5 text-rose-500\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <!-- \u2500\u2500 Trigger Button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"relative\">\n <button\n type=\"button\"\n class=\"sv-dp-trigger flex w-full items-center gap-2.5 rounded-md border border-gray-200 bg-white px-3.5 py-2.5 text-left text-sm shadow-sm transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/70 dark:border-gray-700 dark:bg-gray-900\"\n [class.opacity-50]=\"resolvedDisabled()\"\n [class.cursor-not-allowed]=\"resolvedDisabled()\"\n [class.hover:border-primary-400]=\"!resolvedDisabled()\"\n [class.hover:shadow-md]=\"!resolvedDisabled()\"\n [class.border-primary-400]=\"isOpen()\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-primary-400\\/30]=\"isOpen()\"\n [disabled]=\"resolvedDisabled()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-label]=\"triggerAriaLabel()\"\n aria-haspopup=\"dialog\"\n (click)=\"toggle()\">\n\n <!-- Calendar icon -->\n <svg class=\"h-4 w-4 shrink-0 text-gray-400 transition-colors duration-150 dark:text-gray-300\"\n [class.text-primary-500]=\"isOpen()\"\n [class.dark:text-primary-400]=\"isOpen()\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\n d=\"M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5\"/>\n </svg>\n\n <!-- Date display / placeholder -->\n <span class=\"flex-1 truncate\" [class.pr-6]=\"value() && !resolvedDisabled()\">\n @if (displayValue()) {\n <span class=\"font-medium text-gray-800 dark:text-gray-100\">{{ displayValue() }}</span>\n } @else {\n <span class=\"text-gray-500 dark:text-gray-400\">{{ placeholder() }}</span>\n }\n </span>\n\n <!-- Chevron indicator -->\n <svg\n class=\"h-4 w-4 shrink-0 text-gray-300 transition-transform duration-200 dark:text-gray-500\"\n [class.rotate-180]=\"isOpen()\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m6 9 6 6 6-6\"/>\n </svg>\n </button>\n\n <!-- Clear button \u2014 sibling of the trigger (not nested inside it) -->\n @if (value() && !resolvedDisabled()) {\n <button\n type=\"button\"\n class=\"absolute right-9 top-1/2 flex h-4 w-4 -translate-y-1/2 items-center justify-center rounded-full text-gray-300 transition-colors duration-100 hover:bg-gray-100 hover:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 dark:hover:bg-gray-800 dark:hover:text-gray-400\"\n aria-label=\"Clear date\"\n (click)=\"clearDate(); $event.stopPropagation()\">\n <svg class=\"h-3 w-3\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\"/>\n </svg>\n </button>\n }\n </div>\n\n <!-- \u2500\u2500 Calendar Popover \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered into document.body via a body portal in toggle() so it escapes\n all parent overflow:hidden / CSS transform containment.\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <ng-template #panelTpl>\n <div\n class=\"sv-dp-panel overflow-hidden rounded-2xl border border-gray-100/80 bg-white shadow-[0_24px_64px_-12px_rgba(0,0,0,0.18),0_8px_24px_-8px_rgba(0,0,0,0.08)] dark:border-gray-700/50 dark:bg-gray-900 dark:shadow-[0_24px_64px_-12px_rgba(0,0,0,0.7),0_8px_24px_-8px_rgba(0,0,0,0.5)]\"\n [class.sv-dp-panel--above]=\"openAbove()\"\n [class.sv-dp-panel--right]=\"alignRight()\"\n [style.position]=\"panelStyle()['position']\"\n [style.zIndex]=\"panelStyle()['zIndex']\"\n [style.width]=\"panelStyle()['width']\"\n [style.top]=\"panelStyle()['top']\"\n [style.bottom]=\"panelStyle()['bottom']\"\n [style.left]=\"panelStyle()['left']\"\n [style.right]=\"panelStyle()['right']\"\n role=\"dialog\"\n aria-modal=\"false\"\n aria-label=\"Date picker calendar\">\n\n <!-- \u2500\u2500 Gradient Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"sv-dp-header bg-gradient-to-br from-primary-500 via-primary-600 to-indigo-600 px-4 pt-3.5 pb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n\n <!-- Prev button -->\n <button\n type=\"button\"\n class=\"flex h-7 w-7 items-center justify-center rounded-lg text-white/80 transition-all duration-150 hover:bg-white/15 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50\"\n aria-label=\"Previous\"\n (click)=\"prevPeriod()\">\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m15 19-7-7 7-7\"/>\n </svg>\n </button>\n\n <!-- Month / Year title \u2014 clicking cycles view mode -->\n <button\n type=\"button\"\n class=\"flex flex-1 items-center justify-center gap-1.5 rounded-lg px-2 py-1 text-sm font-semibold tracking-wide text-white transition-all duration-150 hover:bg-white/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50\"\n [attr.aria-label]=\"'Switch view, currently ' + headerLabel()\"\n (click)=\"viewMode.set(viewMode() === 'days' ? 'months' : viewMode() === 'months' ? 'years' : 'days')\">\n {{ headerLabel() }}\n <svg class=\"h-3.5 w-3.5 text-white/70\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m6 9 6 6 6-6\"/>\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"flex h-7 w-7 items-center justify-center rounded-lg text-white/80 transition-all duration-150 hover:bg-white/15 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/50\"\n aria-label=\"Next\"\n (click)=\"nextPeriod()\">\n <svg class=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m9 5 7 7-7 7\"/>\n </svg>\n </button>\n </div>\n @if (displayValue()) {\n <div class=\"mt-2.5 rounded-xl bg-white/15 py-1.5 text-center text-[13px] font-semibold tracking-wide text-white backdrop-blur-sm\">\n {{ displayValue() }}\n </div>\n }\n </div>\n\n <!-- \u2500\u2500 Day View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode() === 'days') {\n <div class=\"sv-dp-body p-3\">\n\n <!-- Calendar grid -->\n <div role=\"grid\" [attr.aria-label]=\"monthName() + ' ' + viewYear()\">\n\n <!-- Weekday headers -->\n <div role=\"row\" class=\"mb-2 grid grid-cols-7 overflow-hidden rounded-xl bg-gray-50 dark:bg-gray-800/60\">\n @for (wd of weekDays; track wd) {\n <div role=\"columnheader\" class=\"py-1.5 text-center text-[10px] font-bold uppercase tracking-widest text-gray-500 dark:text-gray-400\">\n {{ wd }}\n </div>\n }\n </div>\n\n <!-- Day rows -->\n <div role=\"rowgroup\" class=\"grid grid-cols-7\" (keydown)=\"onDayKeydown($event)\">\n @for (week of calendarWeeks(); track $index) {\n <div role=\"row\" class=\"contents\">\n @for (day of week; track day.key) {\n <div role=\"gridcell\" [attr.aria-selected]=\"day.isSelected\" class=\"flex items-center justify-center p-0.5\">\n <button\n type=\"button\"\n [attr.data-day]=\"day.key\"\n [attr.tabindex]=\"dayTabIndex(day)\"\n class=\"sv-dp-day relative flex h-8 w-8 items-center justify-center rounded-md text-[13px] font-medium transition-all duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60\"\n [ngClass]=\"{\n 'bg-gradient-to-br from-primary-500 to-indigo-600 text-white shadow-md shadow-primary-500/30 hover:from-primary-400 hover:to-indigo-500 scale-105': day.isSelected,\n 'text-gray-800 hover:bg-primary-50 hover:text-primary-700 dark:text-gray-200 dark:hover:bg-primary-950/60 dark:hover:text-primary-300': !day.isSelected && !day.isDisabled && day.isCurrentMonth,\n 'font-semibold text-primary-600 dark:text-primary-400': day.isToday && !day.isSelected,\n 'text-gray-300 hover:bg-gray-50 dark:text-gray-700 dark:hover:bg-gray-900': !day.isSelected && !day.isDisabled && !day.isCurrentMonth,\n 'cursor-not-allowed text-gray-200 dark:text-gray-800': day.isDisabled\n }\"\n [disabled]=\"day.isDisabled\"\n [attr.aria-label]=\"day.date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })\"\n [attr.aria-current]=\"day.isToday ? 'date' : null\"\n (click)=\"selectDay(day)\">\n {{ day.label }}\n <!-- Today dot -->\n @if (day.isToday && !day.isSelected) {\n <span class=\"absolute bottom-0.5 left-1/2 h-1.5 w-1.5 -translate-x-1/2 rounded-full bg-primary-500 dark:bg-primary-400\"></span>\n }\n </button>\n </div>\n }\n </div>\n }\n </div>\n\n </div>\n\n <!-- Footer actions -->\n <div class=\"mt-2 flex items-center justify-between border-t border-gray-100 pt-2.5 dark:border-gray-800/80\">\n <button\n type=\"button\"\n class=\"rounded-lg px-2.5 py-1.5 text-xs font-medium text-gray-400 transition-all duration-100 hover:bg-gray-100 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-300\"\n (click)=\"clearDate()\">\n Clear\n </button>\n <button\n type=\"button\"\n class=\"rounded-lg bg-primary-500 px-3.5 py-1.5 text-xs font-semibold text-white shadow-sm shadow-primary-500/30 transition-all duration-100 hover:bg-primary-600 active:bg-primary-700\"\n (click)=\"goToToday()\">\n Today\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Month View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode() === 'months') {\n <div class=\"sv-dp-body grid grid-cols-3 gap-1.5 p-4\">\n @for (month of monthNames; track $index) {\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-2.5 text-center text-[12px] font-medium transition-all duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60\"\n [ngClass]=\"{\n 'bg-gradient-to-br from-primary-500 to-indigo-600 text-white shadow-sm shadow-primary-500/25': isViewMonthSelected($index),\n 'bg-primary-50 text-primary-700 dark:bg-primary-950/60 dark:text-primary-300': !isViewMonthSelected($index) && $index === viewMonth() && !isMonthDisabled($index),\n 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800/70': !isViewMonthSelected($index) && $index !== viewMonth() && !isMonthDisabled($index),\n 'cursor-not-allowed text-gray-300 dark:text-gray-700': isMonthDisabled($index)\n }\"\n [disabled]=\"isMonthDisabled($index)\"\n (click)=\"selectMonth($index)\">\n {{ month.slice(0, 3) }}\n </button>\n }\n </div>\n }\n\n <!-- \u2500\u2500 Year View \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (viewMode() === 'years') {\n <div class=\"sv-dp-body grid grid-cols-4 gap-1 p-4\">\n @for (year of yearRange(); track year) {\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-2.5 text-center text-[12px] font-medium transition-all duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60\"\n [ngClass]=\"{\n 'bg-gradient-to-br from-primary-500 to-indigo-600 text-white shadow-sm shadow-primary-500/25': isViewYearSelected(year),\n 'bg-primary-50 text-primary-700 dark:bg-primary-950/60 dark:text-primary-300': !isViewYearSelected(year) && year === viewYear() && !isYearDisabled(year),\n 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800/70': !isViewYearSelected(year) && year !== viewYear() && !isYearDisabled(year),\n 'cursor-not-allowed text-gray-300 dark:text-gray-700': isYearDisabled(year)\n }\"\n [disabled]=\"isYearDisabled(year)\"\n (click)=\"selectYear(year)\">\n {{ year }}\n </button>\n }\n </div>\n }\n\n </div><!-- /sv-dp-panel -->\n </ng-template>\n\n</div><!-- /sv-dp -->\n", styles: [".sv-dp-panel{animation:dp-pop-in .18s cubic-bezier(.22,1,.36,1) both;transform-origin:top left}.sv-dp-panel--above{transform-origin:bottom left;animation:dp-pop-in-above .18s cubic-bezier(.22,1,.36,1) both}.sv-dp-panel--right{transform-origin:top right}.sv-dp-panel--above.sv-dp-panel--right{transform-origin:bottom right}@keyframes dp-pop-in{0%{opacity:0;transform:scale(.95) translateY(-6px)}to{opacity:1;transform:scale(1) translateY(0)}}@keyframes dp-pop-in-above{0%{opacity:0;transform:scale(.95) translateY(6px)}to{opacity:1;transform:scale(1) translateY(0)}}.sv-dp-body{animation:dp-fade-slide .15s ease-out both}@keyframes dp-fade-slide{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.sv-dp{min-width:0}.sv-dp-trigger:focus-visible{outline:none}.sv-dp-day:not(:disabled):active{transform:scale(.92)}\n"] }]
|
|
961
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], panelTpl: [{
|
|
962
|
+
type: ViewChild,
|
|
963
|
+
args: ['panelTpl', { read: TemplateRef }]
|
|
964
|
+
}], onDocumentClick: [{
|
|
965
|
+
type: HostListener,
|
|
966
|
+
args: ['document:click', ['$event']]
|
|
967
|
+
}], onEscape: [{
|
|
968
|
+
type: HostListener,
|
|
969
|
+
args: ['keydown.escape']
|
|
970
|
+
}], onResize: [{
|
|
971
|
+
type: HostListener,
|
|
972
|
+
args: ['window:resize']
|
|
973
|
+
}] } });
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Generated bundle index. Do not edit.
|
|
977
|
+
*/
|
|
978
|
+
|
|
979
|
+
export { SvDatePickerComponent, SvInputFieldComponent };
|
|
980
|
+
//# sourceMappingURL=styloviz-input-field.mjs.map
|