@zellvora/ng-input 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.
@@ -0,0 +1,2203 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, Injector, signal, computed, effect, untracked, DestroyRef, InjectionToken, input, model, numberAttribute, booleanAttribute, output, Directive, forwardRef, ChangeDetectionStrategy, Component, ChangeDetectorRef, EventEmitter, Input, Output } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
4
+ import * as i1 from '@angular/cdk/scrolling';
5
+ import { ScrollingModule } from '@angular/cdk/scrolling';
6
+ import { DecimalPipe } from '@angular/common';
7
+
8
+ /**
9
+ * Signal Form Engine — the single source of truth for the whole library.
10
+ *
11
+ * A form is a `WritableSignal<T>` model plus a lazily-created `ZvControl` per
12
+ * field. Every piece of derived state (value, errors, validity, status) is a
13
+ * `computed()`, so it recomputes automatically and never needs subscriptions,
14
+ * `patchValue`, `setValue`, or manual change detection.
15
+ *
16
+ * The Reactive and Template adapters wrap *this* — they add no validation or
17
+ * state logic of their own.
18
+ */
19
+ /**
20
+ * Create a Signal Form.
21
+ *
22
+ * @example
23
+ * const form = createForm(
24
+ * { firstName: '', email: '', age: null as number | null },
25
+ * { email: { validators: [ZvValidators.required, ZvValidators.email] } },
26
+ * );
27
+ * form.control('email').invalid(); // computed signal
28
+ */
29
+ function createForm(initial, schema = {}, options = {}) {
30
+ const injector = options.injector ?? inject(Injector);
31
+ const initialSnapshot = structuredClone(initial);
32
+ const model = signal(initial, ...(ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []));
33
+ const controls = new Map();
34
+ function build(key) {
35
+ const cfg = (schema[key] ?? {});
36
+ const validators = cfg.validators ?? [];
37
+ const asyncValidators = cfg.asyncValidators ?? [];
38
+ const disabled = signal(cfg.disabled ?? false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
39
+ const dirty = signal(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
40
+ const touched = signal(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
41
+ const value = computed(() => model()[key], ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
42
+ const required = computed(() => hasRequired(validators), ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
43
+ // Synchronous errors recompute whenever value (or referenced model) changes.
44
+ const syncErrors = computed(() => {
45
+ if (disabled())
46
+ return null;
47
+ const v = value();
48
+ const m = model();
49
+ for (const fn of validators) {
50
+ const err = fn(v, m);
51
+ if (err)
52
+ return err;
53
+ }
54
+ return null;
55
+ }, ...(ngDevMode ? [{ debugName: "syncErrors" }] : /* istanbul ignore next */ []));
56
+ // Async errors live in their own writable signal, fed by an effect.
57
+ const asyncErrors = signal(null, ...(ngDevMode ? [{ debugName: "asyncErrors" }] : /* istanbul ignore next */ []));
58
+ const pending = signal(false, ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
59
+ if (asyncValidators.length) {
60
+ const debounce = cfg.debounce ?? 300;
61
+ let timer;
62
+ let runId = 0;
63
+ effect((onCleanup) => {
64
+ const v = value();
65
+ const m = untracked(model);
66
+ // Skip async while a sync error already fails the field.
67
+ if (untracked(syncErrors)) {
68
+ pending.set(false);
69
+ asyncErrors.set(null);
70
+ return;
71
+ }
72
+ pending.set(true);
73
+ const id = ++runId;
74
+ timer = setTimeout(async () => {
75
+ let result = null;
76
+ for (const fn of asyncValidators) {
77
+ const err = await fn(v, m);
78
+ if (err) {
79
+ result = err;
80
+ break;
81
+ }
82
+ }
83
+ if (id === runId) {
84
+ asyncErrors.set(result);
85
+ pending.set(false);
86
+ }
87
+ }, debounce);
88
+ onCleanup(() => clearTimeout(timer));
89
+ }, { injector });
90
+ }
91
+ const errors = computed(() => syncErrors() ?? asyncErrors(), ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
92
+ const valid = computed(() => !pending() && errors() === null, ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
93
+ const invalid = computed(() => errors() !== null, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
94
+ const status = computed(() => disabled() ? 'DISABLED' : pending() ? 'PENDING' : invalid() ? 'INVALID' : 'VALID', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
95
+ const ctrl = {
96
+ key,
97
+ value,
98
+ model: value,
99
+ disabled: disabled.asReadonly(),
100
+ required,
101
+ dirty: dirty.asReadonly(),
102
+ touched: touched.asReadonly(),
103
+ pending: pending.asReadonly(),
104
+ errors,
105
+ valid,
106
+ invalid,
107
+ status,
108
+ setValue(v) {
109
+ model.update((m) => ({ ...m, [key]: v }));
110
+ dirty.set(true);
111
+ },
112
+ update(fn) {
113
+ model.update((m) => ({ ...m, [key]: fn(m[key]) }));
114
+ dirty.set(true);
115
+ },
116
+ setDisabled(d) {
117
+ disabled.set(d);
118
+ },
119
+ markTouched(t = true) {
120
+ touched.set(t);
121
+ },
122
+ markDirty(d = true) {
123
+ dirty.set(d);
124
+ },
125
+ reset() {
126
+ model.update((m) => ({ ...m, [key]: structuredClone(initialSnapshot[key]) }));
127
+ dirty.set(false);
128
+ touched.set(false);
129
+ },
130
+ };
131
+ return ctrl;
132
+ }
133
+ function control(key) {
134
+ let c = controls.get(key);
135
+ if (!c) {
136
+ c = build(key);
137
+ controls.set(key, c);
138
+ }
139
+ return c;
140
+ }
141
+ // Ensure every declared field has a control so aggregates see them.
142
+ const keys = Object.keys(initial);
143
+ const all = () => keys.map((k) => control(k));
144
+ const errors = computed(() => {
145
+ const out = {};
146
+ for (const c of all())
147
+ if (c.errors())
148
+ out[c.key] = c.errors();
149
+ return out;
150
+ }, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
151
+ const pending = computed(() => all().some((c) => c.pending()), ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
152
+ const valid = computed(() => !pending() && all().every((c) => c.valid()), ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
153
+ const invalid = computed(() => all().some((c) => c.invalid()), ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
154
+ const dirty = computed(() => all().some((c) => c.dirty()), ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
155
+ const touched = computed(() => all().some((c) => c.touched()), ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
156
+ const status = computed(() => pending() ? 'PENDING' : invalid() ? 'INVALID' : 'VALID', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
157
+ // Cleanup map on destroy when in an injection context.
158
+ try {
159
+ inject(DestroyRef, { optional: true })?.onDestroy(() => controls.clear());
160
+ }
161
+ catch {
162
+ /* called outside injection context — caller owns lifecycle */
163
+ }
164
+ return {
165
+ model,
166
+ control,
167
+ value: model.asReadonly(),
168
+ errors,
169
+ valid,
170
+ invalid,
171
+ dirty,
172
+ touched,
173
+ pending,
174
+ status,
175
+ patch(partial) {
176
+ model.update((m) => ({ ...m, ...partial }));
177
+ },
178
+ setValue(v) {
179
+ model.set(v);
180
+ },
181
+ reset(value) {
182
+ model.set(value ? structuredClone(value) : structuredClone(initialSnapshot));
183
+ for (const c of all())
184
+ c.markDirty(false), c.markTouched(false);
185
+ },
186
+ markAllTouched() {
187
+ for (const c of all())
188
+ c.markTouched(true);
189
+ },
190
+ };
191
+ }
192
+ function hasRequired(validators) {
193
+ return validators.some((f) => f.zvRequired === true);
194
+ }
195
+
196
+ /** null/undefined/'' are "absent" — `required` is the only thing that rejects them. */
197
+ const absent$1 = (v) => v === null || v === undefined || v === '';
198
+ const re = (pattern, key) => (v) => absent$1(v) || pattern.test(String(v)) ? null : { [key]: true };
199
+ // ---- Pattern library -------------------------------------------------------
200
+ const ZV_PATTERNS = {
201
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
202
+ phone: /^\+?[0-9]{7,15}$/,
203
+ mobile: /^[6-9][0-9]{9}$/, // Indian mobile
204
+ url: /^(https?:\/\/)([\w-]+\.)+[\w-]+(\/[\w./?%&=#-]*)?$/i,
205
+ pan: /^[A-Z]{5}[0-9]{4}[A-Z]$/,
206
+ gst: /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/,
207
+ aadhaar: /^[2-9][0-9]{11}$/,
208
+ cvv: /^[0-9]{3,4}$/,
209
+ passport: /^[A-PR-WY][0-9]{7}$/,
210
+ drivingLicense: /^[A-Z]{2}[0-9]{2}\s?[0-9]{11}$/,
211
+ pincode: /^[1-9][0-9]{5}$/,
212
+ ifsc: /^[A-Z]{4}0[A-Z0-9]{6}$/,
213
+ swift: /^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/,
214
+ iban: /^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/,
215
+ time: /^([01]?\d|2[0-3]):[0-5]\d$/,
216
+ password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/,
217
+ };
218
+ const requiredFn$1 = (v) => {
219
+ const empty = absent$1(v) || (Array.isArray(v) && v.length === 0) || v === false;
220
+ return empty ? { required: true } : null;
221
+ };
222
+ const required = Object.assign(requiredFn$1, { zvRequired: true });
223
+ const ZvValidators$1 = {
224
+ required,
225
+ email: re(ZV_PATTERNS.email, 'email'),
226
+ phone: re(ZV_PATTERNS.phone, 'phone'),
227
+ mobile: re(ZV_PATTERNS.mobile, 'mobile'),
228
+ url: re(ZV_PATTERNS.url, 'url'),
229
+ pincode: re(ZV_PATTERNS.pincode, 'pincode'),
230
+ cvv: re(ZV_PATTERNS.cvv, 'cvv'),
231
+ passport: re(ZV_PATTERNS.passport, 'passport'),
232
+ drivingLicense: re(ZV_PATTERNS.drivingLicense, 'drivingLicense'),
233
+ ifsc: re(ZV_PATTERNS.ifsc, 'ifsc'),
234
+ swift: re(ZV_PATTERNS.swift, 'swift'),
235
+ iban: re(ZV_PATTERNS.iban, 'iban'),
236
+ time: re(ZV_PATTERNS.time, 'time'),
237
+ pan: re(ZV_PATTERNS.pan, 'pan'),
238
+ gst: re(ZV_PATTERNS.gst, 'gst'),
239
+ password: re(ZV_PATTERNS.password, 'password'),
240
+ pattern: (p) => (v) => {
241
+ const pat = typeof p === 'string' ? new RegExp(p) : p;
242
+ return absent$1(v) || pat.test(String(v)) ? null : { pattern: true };
243
+ },
244
+ regex: (p) => ZvValidators$1.pattern(p),
245
+ min: (n) => (v) => absent$1(v) || Number(v) >= n ? null : { min: { min: n, actual: Number(v) } },
246
+ max: (n) => (v) => absent$1(v) || Number(v) <= n ? null : { max: { max: n, actual: Number(v) } },
247
+ minLength: (n) => (v) => {
248
+ const len = absent$1(v) ? 0 : String(v).length;
249
+ return absent$1(v) || len >= n
250
+ ? null
251
+ : { minlength: { requiredLength: n, actualLength: len } };
252
+ },
253
+ maxLength: (n) => (v) => {
254
+ const len = absent$1(v) ? 0 : String(v).length;
255
+ return len <= n
256
+ ? null
257
+ : { maxlength: { requiredLength: n, actualLength: len } };
258
+ },
259
+ date: ((v) => absent$1(v) || !isNaN(new Date(v).getTime())
260
+ ? null
261
+ : { date: true }),
262
+ /** Minimum whole-year age relative to today. */
263
+ age: (minYears) => (v) => {
264
+ if (absent$1(v))
265
+ return null;
266
+ const dob = new Date(v);
267
+ if (isNaN(dob.getTime()))
268
+ return { date: true };
269
+ const now = new Date();
270
+ let age = now.getFullYear() - dob.getFullYear();
271
+ const m = now.getMonth() - dob.getMonth();
272
+ if (m < 0 || (m === 0 && now.getDate() < dob.getDate()))
273
+ age--;
274
+ return age >= minYears ? null : { age: { required: minYears, actual: age } };
275
+ },
276
+ aadhaar: ((v) => {
277
+ if (absent$1(v))
278
+ return null;
279
+ const s = String(v).replace(/\s/g, '');
280
+ return ZV_PATTERNS.aadhaar.test(s) && verhoeff$1(s) ? null : { aadhaar: true };
281
+ }),
282
+ creditCard: ((v) => {
283
+ if (absent$1(v))
284
+ return null;
285
+ const s = String(v).replace(/[\s-]/g, '');
286
+ return /^[0-9]{12,19}$/.test(s) && luhn$1(s) ? null : { creditCard: true };
287
+ }),
288
+ /** Max file size in bytes (works on File | File[]). */
289
+ fileSize: (maxBytes) => (v) => {
290
+ const files = toFiles(v);
291
+ const big = files.find((f) => f.size > maxBytes);
292
+ return big ? { fileSize: { max: maxBytes, actual: big.size } } : null;
293
+ },
294
+ /** Allowed MIME types (e.g. ['image/png','application/pdf']). */
295
+ mimeType: (allowed) => (v) => {
296
+ const bad = toFiles(v).find((f) => !allowed.some((a) => matchMime(f.type, a)));
297
+ return bad ? { mimeType: { allowed, actual: bad.type } } : null;
298
+ },
299
+ /** Image max width/height (async-free: relies on already-measured dims). */
300
+ imageSize: (maxW, maxH) => (v) => !v || (v.width <= maxW && v.height <= maxH)
301
+ ? null
302
+ : { imageSize: { maxW, maxH, actual: v } },
303
+ /**
304
+ * Cross-field: a control validator that compares against another model key.
305
+ * Use as a field validator; it receives the full model as 2nd arg.
306
+ */
307
+ confirm: (otherField, key = 'mismatch') => (value, model) => value === model[otherField]
308
+ ? null
309
+ : { [key]: true },
310
+ };
311
+ // ---- File helpers ----------------------------------------------------------
312
+ function toFiles(v) {
313
+ if (!v)
314
+ return [];
315
+ if (v instanceof File)
316
+ return [v];
317
+ if (Array.isArray(v))
318
+ return v.filter((x) => x instanceof File);
319
+ if (typeof FileList !== 'undefined' && v instanceof FileList)
320
+ return Array.from(v);
321
+ return [];
322
+ }
323
+ function matchMime(actual, allowed) {
324
+ if (allowed.endsWith('/*'))
325
+ return actual.startsWith(allowed.slice(0, -1));
326
+ return actual === allowed;
327
+ }
328
+ // ---- Checksums -------------------------------------------------------------
329
+ function luhn$1(num) {
330
+ let sum = 0;
331
+ let alt = false;
332
+ for (let i = num.length - 1; i >= 0; i--) {
333
+ let d = +num[i];
334
+ if (alt) {
335
+ d *= 2;
336
+ if (d > 9)
337
+ d -= 9;
338
+ }
339
+ sum += d;
340
+ alt = !alt;
341
+ }
342
+ return sum % 10 === 0;
343
+ }
344
+ const D$1 = [
345
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
346
+ [2, 3, 4, 0, 1, 7, 8, 9, 5, 6], [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
347
+ [4, 0, 1, 2, 3, 9, 5, 6, 7, 8], [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
348
+ [6, 5, 9, 8, 7, 1, 0, 4, 3, 2], [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
349
+ [8, 7, 6, 5, 9, 3, 2, 1, 0, 4], [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
350
+ ];
351
+ const P$1 = [
352
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
353
+ [5, 8, 0, 3, 7, 9, 6, 1, 4, 2], [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
354
+ [9, 4, 5, 3, 1, 2, 6, 8, 7, 0], [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
355
+ [2, 7, 9, 3, 8, 0, 6, 4, 1, 5], [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
356
+ ];
357
+ function verhoeff$1(num) {
358
+ let c = 0;
359
+ const digits = num.split('').reverse().map(Number);
360
+ for (let i = 0; i < digits.length; i++)
361
+ c = D$1[c][P$1[i % 8][digits[i]]];
362
+ return c === 0;
363
+ }
364
+
365
+ /** Shared types for all Zellvora form controls. */
366
+ const ZV_DEFAULT_CONFIG = {
367
+ appearance: 'outlined',
368
+ size: 'md',
369
+ theme: 'material',
370
+ color: 'primary',
371
+ floatingLabel: true,
372
+ dir: 'ltr',
373
+ clearable: false,
374
+ debounce: 300,
375
+ };
376
+
377
+ /**
378
+ * App-wide defaults for every Zellvora control. Override at root or per
379
+ * lazy-loaded scope. Tree-shakable: the default is the bundled constant.
380
+ */
381
+ const ZV_INPUT_CONFIG = new InjectionToken('ZV_INPUT_CONFIG', { providedIn: 'root', factory: () => ZV_DEFAULT_CONFIG });
382
+ /** Provide partial overrides without restating the full config. */
383
+ function provideZvInput(overrides = {}) {
384
+ return {
385
+ provide: ZV_INPUT_CONFIG,
386
+ useValue: { ...ZV_DEFAULT_CONFIG, ...overrides },
387
+ };
388
+ }
389
+
390
+ let counter = 0;
391
+ /** Small, SSR-safe unique id (no crypto / DOM dependency). */
392
+ function uid() {
393
+ return (++counter).toString(36);
394
+ }
395
+
396
+ /** Default human-readable messages for built-in validator error keys. */
397
+ const ZV_DEFAULT_MESSAGES = {
398
+ required: () => 'This field is required.',
399
+ email: () => 'Enter a valid email address.',
400
+ phone: () => 'Enter a valid phone number.',
401
+ mobile: () => 'Enter a valid 10-digit mobile number.',
402
+ url: () => 'Enter a valid URL.',
403
+ password: () => 'Min 8 chars with upper, lower, number and symbol.',
404
+ pattern: () => 'Value does not match the required format.',
405
+ min: (i) => `Must be at least ${i.min}.`,
406
+ max: (i) => `Must be at most ${i.max}.`,
407
+ minlength: (i) => `Min length is ${i.requiredLength}.`,
408
+ maxlength: (i) => `Max length is ${i.requiredLength}.`,
409
+ date: () => 'Enter a valid date.',
410
+ time: () => 'Enter a valid time.',
411
+ age: (i) => `Must be at least ${i.required} years old.`,
412
+ pan: () => 'Enter a valid PAN.',
413
+ gst: () => 'Enter a valid GSTIN.',
414
+ aadhaar: () => 'Enter a valid Aadhaar number.',
415
+ passport: () => 'Enter a valid passport number.',
416
+ drivingLicense: () => 'Enter a valid driving licence number.',
417
+ pincode: () => 'Enter a valid PIN code.',
418
+ creditCard: () => 'Enter a valid card number.',
419
+ cvv: () => 'Enter a valid CVV.',
420
+ ifsc: () => 'Enter a valid IFSC code.',
421
+ swift: () => 'Enter a valid SWIFT/BIC code.',
422
+ iban: () => 'Enter a valid IBAN.',
423
+ fileSize: () => 'File is too large.',
424
+ mimeType: () => 'File type is not allowed.',
425
+ imageSize: () => 'Image dimensions are too large.',
426
+ mismatch: () => 'Values do not match.',
427
+ };
428
+ /** Resolve the first error in `errors` to a display string. */
429
+ function formatError(errors, overrides) {
430
+ if (!errors)
431
+ return null;
432
+ const [key, info] = Object.entries(errors)[0];
433
+ if (overrides?.[key])
434
+ return overrides[key];
435
+ return ZV_DEFAULT_MESSAGES[key]?.(info) ?? 'Invalid value.';
436
+ }
437
+
438
+ /**
439
+ * SignalControlAccessor — signal-first base for every Zellvora control.
440
+ *
441
+ * 100% signal API: `input()`, `model()`, `output()`. No `@Input`/`@Output`
442
+ * decorators, no `ControlValueAccessor`, no `FormControl`. A control can bind
443
+ * in three interchangeable ways, all backed by the same internal signals:
444
+ *
445
+ * 1. Standalone two-way: <zv-input [(value)]="name" />
446
+ * 2. Signal Forms: <zv-input [form]="form" controlName="name" />
447
+ * 3. Reactive/Template: via the adapter directives (zvControl / ngModel)
448
+ *
449
+ * The connection to the Signal Form Engine is a single bidirectional `effect`
450
+ * pair: model -> control on read, control -> model on write.
451
+ */
452
+ class SignalControlAccessor {
453
+ config = inject(ZV_INPUT_CONFIG);
454
+ // ---- Identity ----------------------------------------------------------
455
+ id = signal(`zv-${uid()}`, ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
456
+ name = input(...(ngDevMode ? [undefined, { debugName: "name" }] : /* istanbul ignore next */ []));
457
+ // ---- Signal Forms binding ----------------------------------------------
458
+ /** The Signal Form to bind to (use with `controlName`). */
459
+ form = input(...(ngDevMode ? [undefined, { debugName: "form" }] : /* istanbul ignore next */ []));
460
+ controlName = input(...(ngDevMode ? [undefined, { debugName: "controlName" }] : /* istanbul ignore next */ []));
461
+ // ---- Two-way model (standalone use): [(value)] and [(model)] ------------
462
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
463
+ /** Alias of `value` for `[(model)]` ergonomics. */
464
+ modelValue = model(null, { ...(ngDevMode ? { debugName: "modelValue" } : /* istanbul ignore next */ {}), alias: 'model' });
465
+ // ---- Developer-experience inputs ---------------------------------------
466
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
467
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
468
+ hint = input(...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
469
+ helperText = input(...(ngDevMode ? [undefined, { debugName: "helperText" }] : /* istanbul ignore next */ []));
470
+ errorMessage = input(...(ngDevMode ? [undefined, { debugName: "errorMessage" }] : /* istanbul ignore next */ []));
471
+ prefixIcon = input(...(ngDevMode ? [undefined, { debugName: "prefixIcon" }] : /* istanbul ignore next */ []));
472
+ suffixIcon = input(...(ngDevMode ? [undefined, { debugName: "suffixIcon" }] : /* istanbul ignore next */ []));
473
+ tooltip = input(...(ngDevMode ? [undefined, { debugName: "tooltip" }] : /* istanbul ignore next */ []));
474
+ ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
475
+ autocomplete = input('off', ...(ngDevMode ? [{ debugName: "autocomplete" }] : /* istanbul ignore next */ []));
476
+ loadingText = input(...(ngDevMode ? [undefined, { debugName: "loadingText" }] : /* istanbul ignore next */ []));
477
+ tabIndex = input(0, { ...(ngDevMode ? { debugName: "tabIndex" } : /* istanbul ignore next */ {}), transform: numberAttribute });
478
+ clearable = input(this.config.clearable, { ...(ngDevMode ? { debugName: "clearable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
479
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
480
+ requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), transform: booleanAttribute, alias: 'required' });
481
+ loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
482
+ floatingLabel = input(this.config.floatingLabel, { ...(ngDevMode ? { debugName: "floatingLabel" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
483
+ minlength = input(undefined, { ...(ngDevMode ? { debugName: "minlength" } : /* istanbul ignore next */ {}), transform: numberAttribute });
484
+ maxlength = input(undefined, { ...(ngDevMode ? { debugName: "maxlength" } : /* istanbul ignore next */ {}), transform: numberAttribute });
485
+ min = input(...(ngDevMode ? [undefined, { debugName: "min" }] : /* istanbul ignore next */ []));
486
+ max = input(...(ngDevMode ? [undefined, { debugName: "max" }] : /* istanbul ignore next */ []));
487
+ mask = input(...(ngDevMode ? [undefined, { debugName: "mask" }] : /* istanbul ignore next */ []));
488
+ appearance = input(this.config.appearance, ...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
489
+ size = input(this.config.size, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
490
+ theme = input(this.config.theme, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
491
+ color = input(this.config.color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
492
+ dir = input(this.config.dir, ...(ngDevMode ? [{ debugName: "dir" }] : /* istanbul ignore next */ []));
493
+ variant = input(...(ngDevMode ? [undefined, { debugName: "variant" }] : /* istanbul ignore next */ []));
494
+ density = input(...(ngDevMode ? [undefined, { debugName: "density" }] : /* istanbul ignore next */ []));
495
+ rounded = input(false, { ...(ngDevMode ? { debugName: "rounded" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
496
+ statusInput = input(null, { ...(ngDevMode ? { debugName: "statusInput" } : /* istanbul ignore next */ {}), alias: 'status' });
497
+ // ---- Disabled: input that seeds an internal writable signal ------------
498
+ disabledInput = input(false, { ...(ngDevMode ? { debugName: "disabledInput" } : /* istanbul ignore next */ {}), transform: booleanAttribute, alias: 'disabled' });
499
+ _disabledOverride = signal(null, ...(ngDevMode ? [{ debugName: "_disabledOverride" }] : /* istanbul ignore next */ []));
500
+ // ---- Native-parity events ----------------------------------------------
501
+ // `value`/`model` models auto-emit `valueChange`/`modelChange`.
502
+ change = output();
503
+ inputEvent = output({ alias: 'input' });
504
+ focusEvent = output({ alias: 'focus' });
505
+ blurEvent = output({ alias: 'blur' });
506
+ // ---- Internal reactive state -------------------------------------------
507
+ _touched = signal(false, ...(ngDevMode ? [{ debugName: "_touched" }] : /* istanbul ignore next */ []));
508
+ _focused = signal(false, ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
509
+ _dirty = signal(false, ...(ngDevMode ? [{ debugName: "_dirty" }] : /* istanbul ignore next */ []));
510
+ /** The bound engine control, if `[form] + controlName` are set. */
511
+ boundControl = computed(() => {
512
+ const f = this.form();
513
+ const key = this.controlName();
514
+ if (!f || !key)
515
+ return null;
516
+ return f.control(key);
517
+ }, ...(ngDevMode ? [{ debugName: "boundControl" }] : /* istanbul ignore next */ []));
518
+ // ---- Public reactive surface (the spec's control.* signals) ------------
519
+ current = computed(() => {
520
+ const bc = this.boundControl();
521
+ return bc ? bc.value() : this.value();
522
+ }, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
523
+ disabled = computed(() => {
524
+ const o = this._disabledOverride();
525
+ if (o !== null)
526
+ return o;
527
+ return this.boundControl()?.disabled() ?? this.disabledInput();
528
+ }, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
529
+ /** Errors injected by the Reactive/Template adapter (no bound signal form). */
530
+ _externalErrors = signal(null, ...(ngDevMode ? [{ debugName: "_externalErrors" }] : /* istanbul ignore next */ []));
531
+ errors = computed(() => this.boundControl()?.errors() ?? this._externalErrors(), ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
532
+ valid = computed(() => this.boundControl()?.valid() ?? this.errors() === null, ...(ngDevMode ? [{ debugName: "valid" }] : /* istanbul ignore next */ []));
533
+ invalid = computed(() => !this.valid(), ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
534
+ pending = computed(() => this.boundControl()?.pending() ?? false, ...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
535
+ dirty = computed(() => this.boundControl()?.dirty() ?? this._dirty(), ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
536
+ touched = computed(() => this.boundControl()?.touched() ?? this._touched(), ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
537
+ focused = this._focused.asReadonly();
538
+ required = computed(() => this.boundControl()?.required() ?? this.requiredInput(), ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
539
+ status = computed(() => this.boundControl()?.status() ?? (this.disabled() ? 'DISABLED' : this.invalid() ? 'INVALID' : 'VALID'), ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
540
+ /** Show validation error only once the field has been interacted with. */
541
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
542
+ /** Resolved error text: explicit `errorMessage` wins, else the first error. */
543
+ errorText = computed(() => this.errorMessage() ?? formatError(this.errors()), ...(ngDevMode ? [{ debugName: "errorText" }] : /* istanbul ignore next */ []));
544
+ hasValue = computed(() => {
545
+ const v = this.current();
546
+ return v !== null && v !== undefined && v !== '';
547
+ }, ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
548
+ constructor() {
549
+ // Keep `value` and `model` aliases in sync for standalone two-way use.
550
+ effect(() => this.modelValue.set(this.value()));
551
+ effect(() => {
552
+ const m = this.modelValue();
553
+ if (m !== this.value())
554
+ this.value.set(m);
555
+ });
556
+ }
557
+ // ---- Mutators used by concrete templates -------------------------------
558
+ commit(v, kind) {
559
+ const bc = this.boundControl();
560
+ if (bc)
561
+ bc.setValue(v);
562
+ // Always update the two-way `value` model so `valueChange` fires in every mode.
563
+ this.value.set(v);
564
+ this._dirty.set(true);
565
+ this.committed.emit(v);
566
+ if (kind === 'input')
567
+ this.inputEvent.emit(v);
568
+ else
569
+ this.change.emit(v);
570
+ }
571
+ onFocus(ev) {
572
+ this._focused.set(true);
573
+ this.focusEvent.emit(ev);
574
+ }
575
+ onBlur(ev) {
576
+ this._focused.set(false);
577
+ this._touched.set(true);
578
+ this.boundControl()?.markTouched(true);
579
+ this.blurEvent.emit(ev);
580
+ }
581
+ setDisabled(disabled) {
582
+ this._disabledOverride.set(disabled);
583
+ this.boundControl()?.setDisabled(disabled);
584
+ }
585
+ // ---- Adapter hooks (used by ZvControlValueAccessor; no logic duplicated) -
586
+ /** Accept a value from a Reactive/Template control without re-emitting. */
587
+ acceptExternalValue(v) {
588
+ this.value.set(v);
589
+ }
590
+ /** Push validation errors computed by the adapter (same ZvValidators). */
591
+ setExternalErrors(e) {
592
+ this._externalErrors.set(e);
593
+ }
594
+ /** Mark touched from the adapter (e.g. on blur). */
595
+ markTouched() {
596
+ this.onBlur(new FocusEvent('blur'));
597
+ }
598
+ /** Subscribe to value commits (adapter -> NgControl onChange). */
599
+ committed = output({ alias: 'zvCommitted' });
600
+ clear() {
601
+ this.commit(null, 'change');
602
+ }
603
+ hostClasses = computed(() => {
604
+ const cls = [
605
+ `zv-${this.controlType}`,
606
+ `zv-appearance-${this.appearance()}`,
607
+ `zv-size-${this.size()}`,
608
+ `zv-theme-${this.theme()}`,
609
+ `zv-color-${this.color()}`,
610
+ ];
611
+ if (this.dir() === 'rtl')
612
+ cls.push('zv-rtl');
613
+ if (this.disabled())
614
+ cls.push('zv-disabled');
615
+ if (this.readonly())
616
+ cls.push('zv-readonly');
617
+ if (this.focused())
618
+ cls.push('zv-focused');
619
+ if (this.hasValue())
620
+ cls.push('zv-has-value');
621
+ if (this.loading())
622
+ cls.push('zv-loading');
623
+ if (this.floatingLabel())
624
+ cls.push('zv-floating');
625
+ if (this.rounded())
626
+ cls.push('zv-rounded');
627
+ if (this.showError())
628
+ cls.push('zv-invalid');
629
+ const s = this.statusInput();
630
+ if (s)
631
+ cls.push(`zv-status-${s}`);
632
+ return cls.join(' ');
633
+ }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
634
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SignalControlAccessor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
635
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: SignalControlAccessor, isStandalone: true, inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: false, transformFunction: null }, controlName: { classPropertyName: "controlName", publicName: "controlName", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, modelValue: { classPropertyName: "modelValue", publicName: "model", 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 }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, prefixIcon: { classPropertyName: "prefixIcon", publicName: "prefixIcon", isSignal: true, isRequired: false, transformFunction: null }, suffixIcon: { classPropertyName: "suffixIcon", publicName: "suffixIcon", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, requiredInput: { classPropertyName: "requiredInput", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, floatingLabel: { classPropertyName: "floatingLabel", publicName: "floatingLabel", isSignal: true, isRequired: false, transformFunction: null }, minlength: { classPropertyName: "minlength", publicName: "minlength", isSignal: true, isRequired: false, transformFunction: null }, maxlength: { classPropertyName: "maxlength", publicName: "maxlength", 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 }, mask: { classPropertyName: "mask", publicName: "mask", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, dir: { classPropertyName: "dir", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, statusInput: { classPropertyName: "statusInput", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, disabledInput: { classPropertyName: "disabledInput", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", modelValue: "modelChange", change: "change", inputEvent: "input", focusEvent: "focus", blurEvent: "blur", committed: "zvCommitted" }, host: { properties: { "class": "hostClasses()", "attr.dir": "dir()" } }, ngImport: i0 });
636
+ }
637
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: SignalControlAccessor, decorators: [{
638
+ type: Directive,
639
+ args: [{ host: { '[class]': 'hostClasses()', '[attr.dir]': 'dir()' } }]
640
+ }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: false }] }], controlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlName", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], modelValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "model", required: false }] }, { type: i0.Output, args: ["modelChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], tabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabIndex", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], requiredInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], floatingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "floatingLabel", required: false }] }], minlength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minlength", required: false }] }], maxlength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxlength", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], mask: [{ type: i0.Input, args: [{ isSignal: true, alias: "mask", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], dir: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], statusInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], disabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], inputEvent: [{ type: i0.Output, args: ["input"] }], focusEvent: [{ type: i0.Output, args: ["focus"] }], blurEvent: [{ type: i0.Output, args: ["blur"] }], committed: [{ type: i0.Output, args: ["zvCommitted"] }] } });
641
+
642
+ /**
643
+ * Alias a concrete control to the abstract `SignalControlAccessor` token so the
644
+ * Reactive/Template adapter directive can inject it from the same element.
645
+ */
646
+ function provideSignalControl(cmp) {
647
+ return { provide: SignalControlAccessor, useExisting: forwardRef(() => cmp) };
648
+ }
649
+ /**
650
+ * Standard provider trio every control registers so it integrates with
651
+ * Template-Driven and Reactive forms. `async` is opt-in.
652
+ */
653
+ function controlProviders(cmp, opts = {}) {
654
+ const providers = [
655
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => cmp), multi: true },
656
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => cmp), multi: true },
657
+ ];
658
+ if (opts.async) {
659
+ providers.push({
660
+ provide: NG_ASYNC_VALIDATORS,
661
+ useExisting: forwardRef(() => cmp),
662
+ multi: true,
663
+ });
664
+ }
665
+ return providers;
666
+ }
667
+
668
+ /**
669
+ * Presentational wrapper: label, prefix/suffix icons, hint/error row,
670
+ * loading + clear affordances. Controls project their native element into it.
671
+ * Signal-first and OnPush — it never triggers extra change detection.
672
+ */
673
+ class ZvFieldShell {
674
+ for = input(...(ngDevMode ? [undefined, { debugName: "for" }] : /* istanbul ignore next */ []));
675
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
676
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
677
+ hint = input(...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
678
+ helperText = input(...(ngDevMode ? [undefined, { debugName: "helperText" }] : /* istanbul ignore next */ []));
679
+ errorText = input(...(ngDevMode ? [undefined, { debugName: "errorText" }] : /* istanbul ignore next */ []));
680
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
681
+ prefixIcon = input(...(ngDevMode ? [undefined, { debugName: "prefixIcon" }] : /* istanbul ignore next */ []));
682
+ suffixIcon = input(...(ngDevMode ? [undefined, { debugName: "suffixIcon" }] : /* istanbul ignore next */ []));
683
+ loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
684
+ loadingText = input(...(ngDevMode ? [undefined, { debugName: "loadingText" }] : /* istanbul ignore next */ []));
685
+ clearable = input(false, ...(ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []));
686
+ hasValue = input(false, ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
687
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
688
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
689
+ clear = output();
690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvFieldShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
691
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvFieldShell, isStandalone: true, selector: "zv-field-shell", inputs: { for: { classPropertyName: "for", publicName: "for", 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 }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, errorText: { classPropertyName: "errorText", publicName: "errorText", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, prefixIcon: { classPropertyName: "prefixIcon", publicName: "prefixIcon", isSignal: true, isRequired: false, transformFunction: null }, suffixIcon: { classPropertyName: "suffixIcon", publicName: "suffixIcon", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, hasValue: { classPropertyName: "hasValue", publicName: "hasValue", 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 } }, outputs: { clear: "clear" }, ngImport: i0, template: `
692
+ @if (label()) {
693
+ <label class="zv-label" [attr.for]="for()">
694
+ {{ label() }}@if (required()) {<span class="zv-req" aria-hidden="true">*</span>}
695
+ </label>
696
+ }
697
+ <div class="zv-field" [class.zv-field--invalid]="invalid()">
698
+ @if (prefixIcon()) {
699
+ <span class="zv-affix zv-prefix" aria-hidden="true"><i [class]="prefixIcon()"></i></span>
700
+ }
701
+
702
+ <ng-content></ng-content>
703
+
704
+ @if (loading()) {
705
+ <span class="zv-spinner" role="status" [attr.aria-label]="loadingText() || 'Loading'"></span>
706
+ } @else if (clearable() && hasValue() && !disabled() && !readonly()) {
707
+ <button type="button" class="zv-clear" (click)="clear.emit()" aria-label="Clear" tabindex="-1">×</button>
708
+ }
709
+
710
+ @if (suffixIcon()) {
711
+ <span class="zv-affix zv-suffix" aria-hidden="true"><i [class]="suffixIcon()"></i></span>
712
+ }
713
+ </div>
714
+
715
+ @if (invalid() && errorText()) {
716
+ <p class="zv-error" role="alert">{{ errorText() }}</p>
717
+ } @else if (hint() || helperText()) {
718
+ <p class="zv-hint">{{ hint() || helperText() }}</p>
719
+ }
720
+ `, isInline: true, styles: [":host{display:block;font-family:var(--zv-font, inherit);margin-bottom:var(--zv-gap, 1rem)}.zv-label{display:inline-block;margin-bottom:.25rem;font-size:.85rem;font-weight:500;color:var(--zv-label-color, #374151)}.zv-req{color:var(--zv-warn, #dc2626);margin-inline-start:2px}.zv-field{display:flex;align-items:center;gap:.5rem;border:1px solid var(--zv-border, #d1d5db);border-radius:var(--zv-radius, .5rem);padding:0 .75rem;background:var(--zv-bg, #fff);transition:border-color .15s,box-shadow .15s}.zv-field:focus-within{border-color:var(--zv-primary, #2563eb);box-shadow:0 0 0 3px var(--zv-primary-ring, rgba(37, 99, 235, .2))}.zv-field--invalid{border-color:var(--zv-warn, #dc2626)}::ng-deep .zv-field>input,::ng-deep .zv-field>textarea,::ng-deep .zv-field>select{flex:1;border:0;outline:0;background:transparent;font:inherit;padding:.55rem 0;color:var(--zv-text, #111827);width:100%}.zv-clear{border:0;background:transparent;cursor:pointer;font-size:1.1rem;line-height:1;color:var(--zv-muted, #6b7280)}.zv-spinner{width:1rem;height:1rem;border-radius:50%;border:2px solid var(--zv-border, #d1d5db);border-top-color:var(--zv-primary, #2563eb);animation:zv-spin .7s linear infinite}@keyframes zv-spin{to{transform:rotate(360deg)}}.zv-hint{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-muted, #6b7280)}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn, #dc2626)}:host-context(.zv-appearance-filled) .zv-field{background:var(--zv-filled-bg, #f3f4f6);border-color:transparent}:host-context(.zv-appearance-standard) .zv-field{border:0;border-bottom:1px solid var(--zv-border, #d1d5db);border-radius:0;padding-inline:0}:host-context(.zv-size-sm) .zv-field,:host-context(.zv-size-compact) .zv-field{padding-block:0}:host-context(.zv-size-dense) .zv-field ::ng-deep input{padding-block:.35rem}:host-context(.zv-rtl){direction:rtl}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
721
+ }
722
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvFieldShell, decorators: [{
723
+ type: Component,
724
+ args: [{ selector: 'zv-field-shell', changeDetection: ChangeDetectionStrategy.OnPush, template: `
725
+ @if (label()) {
726
+ <label class="zv-label" [attr.for]="for()">
727
+ {{ label() }}@if (required()) {<span class="zv-req" aria-hidden="true">*</span>}
728
+ </label>
729
+ }
730
+ <div class="zv-field" [class.zv-field--invalid]="invalid()">
731
+ @if (prefixIcon()) {
732
+ <span class="zv-affix zv-prefix" aria-hidden="true"><i [class]="prefixIcon()"></i></span>
733
+ }
734
+
735
+ <ng-content></ng-content>
736
+
737
+ @if (loading()) {
738
+ <span class="zv-spinner" role="status" [attr.aria-label]="loadingText() || 'Loading'"></span>
739
+ } @else if (clearable() && hasValue() && !disabled() && !readonly()) {
740
+ <button type="button" class="zv-clear" (click)="clear.emit()" aria-label="Clear" tabindex="-1">×</button>
741
+ }
742
+
743
+ @if (suffixIcon()) {
744
+ <span class="zv-affix zv-suffix" aria-hidden="true"><i [class]="suffixIcon()"></i></span>
745
+ }
746
+ </div>
747
+
748
+ @if (invalid() && errorText()) {
749
+ <p class="zv-error" role="alert">{{ errorText() }}</p>
750
+ } @else if (hint() || helperText()) {
751
+ <p class="zv-hint">{{ hint() || helperText() }}</p>
752
+ }
753
+ `, styles: [":host{display:block;font-family:var(--zv-font, inherit);margin-bottom:var(--zv-gap, 1rem)}.zv-label{display:inline-block;margin-bottom:.25rem;font-size:.85rem;font-weight:500;color:var(--zv-label-color, #374151)}.zv-req{color:var(--zv-warn, #dc2626);margin-inline-start:2px}.zv-field{display:flex;align-items:center;gap:.5rem;border:1px solid var(--zv-border, #d1d5db);border-radius:var(--zv-radius, .5rem);padding:0 .75rem;background:var(--zv-bg, #fff);transition:border-color .15s,box-shadow .15s}.zv-field:focus-within{border-color:var(--zv-primary, #2563eb);box-shadow:0 0 0 3px var(--zv-primary-ring, rgba(37, 99, 235, .2))}.zv-field--invalid{border-color:var(--zv-warn, #dc2626)}::ng-deep .zv-field>input,::ng-deep .zv-field>textarea,::ng-deep .zv-field>select{flex:1;border:0;outline:0;background:transparent;font:inherit;padding:.55rem 0;color:var(--zv-text, #111827);width:100%}.zv-clear{border:0;background:transparent;cursor:pointer;font-size:1.1rem;line-height:1;color:var(--zv-muted, #6b7280)}.zv-spinner{width:1rem;height:1rem;border-radius:50%;border:2px solid var(--zv-border, #d1d5db);border-top-color:var(--zv-primary, #2563eb);animation:zv-spin .7s linear infinite}@keyframes zv-spin{to{transform:rotate(360deg)}}.zv-hint{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-muted, #6b7280)}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn, #dc2626)}:host-context(.zv-appearance-filled) .zv-field{background:var(--zv-filled-bg, #f3f4f6);border-color:transparent}:host-context(.zv-appearance-standard) .zv-field{border:0;border-bottom:1px solid var(--zv-border, #d1d5db);border-radius:0;padding-inline:0}:host-context(.zv-size-sm) .zv-field,:host-context(.zv-size-compact) .zv-field{padding-block:0}:host-context(.zv-size-dense) .zv-field ::ng-deep input{padding-block:.35rem}:host-context(.zv-rtl){direction:rtl}\n"] }]
754
+ }], propDecorators: { for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorText", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], hasValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasValue", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], clear: [{ type: i0.Output, args: ["clear"] }] } });
755
+
756
+ /**
757
+ * Single-line text input. Signal-first: binds via `[(value)]`, or via a Signal
758
+ * Form using `[form]` + `controlName`.
759
+ *
760
+ * @example <zv-input [form]="form" controlName="firstName" label="First name" />
761
+ */
762
+ class ZvInput extends SignalControlAccessor {
763
+ controlType = 'input';
764
+ inputType = input('text', ...(ngDevMode ? [{ debugName: "inputType" }] : /* istanbul ignore next */ []));
765
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvInput, deps: null, target: i0.ɵɵFactoryTarget.Component });
766
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.17", type: ZvInput, isStandalone: true, selector: "zv-input", inputs: { inputType: { classPropertyName: "inputType", publicName: "inputType", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideSignalControl(ZvInput)], usesInheritance: true, ngImport: i0, template: `
767
+ <zv-field-shell
768
+ [for]="id()"
769
+ [label]="label()"
770
+ [required]="required()"
771
+ [hint]="hint()"
772
+ [helperText]="helperText()"
773
+ [errorText]="errorText()"
774
+ [invalid]="showError()"
775
+ [prefixIcon]="prefixIcon()"
776
+ [suffixIcon]="suffixIcon()"
777
+ [loading]="loading()"
778
+ [loadingText]="loadingText()"
779
+ [clearable]="clearable()"
780
+ [hasValue]="hasValue()"
781
+ [disabled]="disabled()"
782
+ [readonly]="readonly()"
783
+ (clear)="clear()"
784
+ >
785
+ <input
786
+ [id]="id()"
787
+ [type]="inputType()"
788
+ [name]="name()"
789
+ [value]="current() ?? ''"
790
+ [placeholder]="placeholder()"
791
+ [disabled]="disabled()"
792
+ [readOnly]="readonly()"
793
+ [attr.maxlength]="maxlength()"
794
+ [attr.minlength]="minlength()"
795
+ [attr.autocomplete]="autocomplete()"
796
+ [attr.aria-label]="ariaLabel() || label()"
797
+ [attr.aria-invalid]="showError()"
798
+ [attr.aria-required]="required()"
799
+ [attr.title]="tooltip()"
800
+ [tabIndex]="tabIndex()"
801
+ (input)="commit($any($event.target).value, 'input')"
802
+ (change)="commit($any($event.target).value, 'change')"
803
+ (focus)="onFocus($event)"
804
+ (blur)="onBlur($event)"
805
+ />
806
+ </zv-field-shell>
807
+ `, isInline: true, dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
808
+ }
809
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvInput, decorators: [{
810
+ type: Component,
811
+ args: [{
812
+ selector: 'zv-input',
813
+ imports: [ZvFieldShell],
814
+ changeDetection: ChangeDetectionStrategy.OnPush,
815
+ providers: [provideSignalControl(ZvInput)],
816
+ template: `
817
+ <zv-field-shell
818
+ [for]="id()"
819
+ [label]="label()"
820
+ [required]="required()"
821
+ [hint]="hint()"
822
+ [helperText]="helperText()"
823
+ [errorText]="errorText()"
824
+ [invalid]="showError()"
825
+ [prefixIcon]="prefixIcon()"
826
+ [suffixIcon]="suffixIcon()"
827
+ [loading]="loading()"
828
+ [loadingText]="loadingText()"
829
+ [clearable]="clearable()"
830
+ [hasValue]="hasValue()"
831
+ [disabled]="disabled()"
832
+ [readonly]="readonly()"
833
+ (clear)="clear()"
834
+ >
835
+ <input
836
+ [id]="id()"
837
+ [type]="inputType()"
838
+ [name]="name()"
839
+ [value]="current() ?? ''"
840
+ [placeholder]="placeholder()"
841
+ [disabled]="disabled()"
842
+ [readOnly]="readonly()"
843
+ [attr.maxlength]="maxlength()"
844
+ [attr.minlength]="minlength()"
845
+ [attr.autocomplete]="autocomplete()"
846
+ [attr.aria-label]="ariaLabel() || label()"
847
+ [attr.aria-invalid]="showError()"
848
+ [attr.aria-required]="required()"
849
+ [attr.title]="tooltip()"
850
+ [tabIndex]="tabIndex()"
851
+ (input)="commit($any($event.target).value, 'input')"
852
+ (change)="commit($any($event.target).value, 'change')"
853
+ (focus)="onFocus($event)"
854
+ (blur)="onBlur($event)"
855
+ />
856
+ </zv-field-shell>
857
+ `,
858
+ }]
859
+ }], propDecorators: { inputType: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputType", required: false }] }] } });
860
+
861
+ /**
862
+ * Base class shared by every Zellvora control.
863
+ *
864
+ * Implements `ControlValueAccessor` + `Validator` so the same component works
865
+ * unchanged across Template-Driven (`ngModel`), Reactive (`formControlName`)
866
+ * and Signal Forms (via the `zvSignal` directive). Internal state is held in
867
+ * signals, so views can run fully `OnPush` with no manual `markForCheck`.
868
+ *
869
+ * Concrete controls extend this, set `controlType`, and bind `[value]` /
870
+ * `(handleInput)` / `(handleBlur)` in their template.
871
+ */
872
+ class ZvBaseControl {
873
+ config = inject(ZV_INPUT_CONFIG);
874
+ cdr = inject(ChangeDetectorRef);
875
+ destroyRef = inject(DestroyRef);
876
+ // ---- Identity -----------------------------------------------------------
877
+ get id() {
878
+ return `zv-${this.controlType ?? 'ctrl'}-${uid()}`;
879
+ }
880
+ name;
881
+ // ---- Developer-experience inputs ---------------------------------------
882
+ label;
883
+ placeholder = '';
884
+ hint;
885
+ helperText;
886
+ errorMessage;
887
+ prefixIcon;
888
+ suffixIcon;
889
+ tooltip;
890
+ ariaLabel;
891
+ autocomplete = 'off';
892
+ loadingText;
893
+ tabIndex = 0;
894
+ // ---- Boolean state (attribute-transformed for `<zv-x clearable>`) -------
895
+ clearable = this.config.clearable;
896
+ readonly = false;
897
+ required = false;
898
+ loading = false;
899
+ floatingLabel = this.config.floatingLabel;
900
+ // ---- Constraint inputs (also fed to validators) -------------------------
901
+ minlength;
902
+ maxlength;
903
+ min;
904
+ max;
905
+ pattern;
906
+ mask;
907
+ // ---- Appearance ---------------------------------------------------------
908
+ appearance = this.config.appearance;
909
+ size = this.config.size;
910
+ theme = this.config.theme;
911
+ color = this.config.color;
912
+ dir = this.config.dir;
913
+ status = null;
914
+ // ---- Reactive internal state -------------------------------------------
915
+ _value = signal(null, ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
916
+ _disabled = signal(false, ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
917
+ _touched = signal(false, ...(ngDevMode ? [{ debugName: "_touched" }] : /* istanbul ignore next */ []));
918
+ _focused = signal(false, ...(ngDevMode ? [{ debugName: "_focused" }] : /* istanbul ignore next */ []));
919
+ /** Public readonly value signal — useful for Signal Forms & templates. */
920
+ valueSig = this._value.asReadonly();
921
+ disabledSig = this._disabled.asReadonly();
922
+ focusedSig = this._focused.asReadonly();
923
+ /** Has content for floating-label / empty-state styling. */
924
+ hasValue = computed(() => {
925
+ const v = this._value();
926
+ return v !== null && v !== undefined && v !== '';
927
+ }, ...(ngDevMode ? [{ debugName: "hasValue" }] : /* istanbul ignore next */ []));
928
+ // ---- Two-way binding & native-parity events -----------------------------
929
+ /** Enables `[(value)]`. */
930
+ valueChange = new EventEmitter();
931
+ input = new EventEmitter();
932
+ change = new EventEmitter();
933
+ focus = new EventEmitter();
934
+ blur = new EventEmitter();
935
+ /** `[(value)]` setter for direct (non-forms) usage. */
936
+ set value(v) {
937
+ if (v !== this._value()) {
938
+ this._value.set(v);
939
+ }
940
+ }
941
+ get value() {
942
+ return this._value();
943
+ }
944
+ set disabled(v) {
945
+ this._disabled.set(v);
946
+ }
947
+ get disabled() {
948
+ return this._disabled();
949
+ }
950
+ // ---- Signal Forms mode --------------------------------------------------
951
+ /** A writable signal holding the form model, e.g. `signal({ name: '' })`. */
952
+ signal;
953
+ /** Key within the signal model this control is bound to. */
954
+ controlName;
955
+ // ---- CVA plumbing -------------------------------------------------------
956
+ _onChange = () => { };
957
+ _onTouched = () => { };
958
+ _onValidatorChange = () => { };
959
+ constructor() {
960
+ // Bridge the value signal to the OnPush view without manual CD.
961
+ effect(() => {
962
+ this._value();
963
+ this._disabled();
964
+ this._focused();
965
+ this.cdr.markForCheck();
966
+ });
967
+ // Signal Forms: pull model -> control whenever the signal/key changes.
968
+ effect(() => {
969
+ if (this.signal && this.controlName) {
970
+ const model = this.signal();
971
+ const next = model[this.controlName];
972
+ if (next !== this._value())
973
+ this._value.set(next);
974
+ }
975
+ });
976
+ }
977
+ /** Signal Forms: push control -> model (called from setValue). */
978
+ syncToSignal(v) {
979
+ if (this.signal && this.controlName) {
980
+ this.signal.update((m) => ({ ...m, [this.controlName]: v }));
981
+ }
982
+ }
983
+ // ---- ControlValueAccessor ----------------------------------------------
984
+ writeValue(value) {
985
+ this._value.set(value);
986
+ }
987
+ registerOnChange(fn) {
988
+ this._onChange = fn;
989
+ }
990
+ registerOnTouched(fn) {
991
+ this._onTouched = fn;
992
+ }
993
+ setDisabledState(isDisabled) {
994
+ this._disabled.set(isDisabled);
995
+ }
996
+ // ---- Validator (subclasses add via `ownValidators`) --------------------
997
+ validate(control) {
998
+ const fns = this.ownValidators();
999
+ if (this.required)
1000
+ fns.unshift(requiredFn);
1001
+ for (const fn of fns) {
1002
+ const err = fn(control);
1003
+ if (err)
1004
+ return err;
1005
+ }
1006
+ return null;
1007
+ }
1008
+ registerOnValidatorChange(fn) {
1009
+ this._onValidatorChange = fn;
1010
+ }
1011
+ /** Override to contribute control-specific validators (email, min, etc.). */
1012
+ ownValidators() {
1013
+ return [];
1014
+ }
1015
+ /** Call when constraint inputs change so the parent re-runs validation. */
1016
+ notifyValidatorChange() {
1017
+ this._onValidatorChange();
1018
+ }
1019
+ // ---- Event handlers used by subclass templates -------------------------
1020
+ setValue(v, emitInput = true) {
1021
+ this._value.set(v);
1022
+ this._onChange(v);
1023
+ this.syncToSignal(v);
1024
+ this.valueChange.emit(v);
1025
+ if (emitInput)
1026
+ this.input.emit(v);
1027
+ }
1028
+ handleInput(v) {
1029
+ this.setValue(v, true);
1030
+ }
1031
+ handleChange(v) {
1032
+ this.setValue(v, false);
1033
+ this.change.emit(v);
1034
+ }
1035
+ handleFocus(ev) {
1036
+ this._focused.set(true);
1037
+ this.focus.emit(ev);
1038
+ }
1039
+ handleBlur(ev) {
1040
+ this._focused.set(false);
1041
+ this._touched.set(true);
1042
+ this._onTouched();
1043
+ this.blur.emit(ev);
1044
+ }
1045
+ /** Clear button handler. */
1046
+ clear() {
1047
+ this.setValue(null);
1048
+ this.change.emit(null);
1049
+ }
1050
+ // ---- Host class map for styling ----------------------------------------
1051
+ get hostClasses() {
1052
+ return {
1053
+ [`zv-${this.controlType}`]: true,
1054
+ [`zv-appearance-${this.appearance}`]: true,
1055
+ [`zv-size-${this.size}`]: true,
1056
+ [`zv-theme-${this.theme}`]: true,
1057
+ [`zv-color-${this.color}`]: true,
1058
+ 'zv-rtl': this.dir === 'rtl',
1059
+ 'zv-disabled': this._disabled(),
1060
+ 'zv-readonly': this.readonly,
1061
+ 'zv-focused': this._focused(),
1062
+ 'zv-has-value': this.hasValue(),
1063
+ 'zv-loading': this.loading,
1064
+ 'zv-floating': this.floatingLabel,
1065
+ [`zv-status-${this.status}`]: !!this.status,
1066
+ };
1067
+ }
1068
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvBaseControl, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1069
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "21.2.17", type: ZvBaseControl, isStandalone: true, inputs: { name: "name", label: "label", placeholder: "placeholder", hint: "hint", helperText: "helperText", errorMessage: "errorMessage", prefixIcon: "prefixIcon", suffixIcon: "suffixIcon", tooltip: "tooltip", ariaLabel: "ariaLabel", autocomplete: "autocomplete", loadingText: "loadingText", tabIndex: ["tabIndex", "tabIndex", numberAttribute], clearable: ["clearable", "clearable", booleanAttribute], readonly: ["readonly", "readonly", booleanAttribute], required: ["required", "required", booleanAttribute], loading: ["loading", "loading", booleanAttribute], floatingLabel: ["floatingLabel", "floatingLabel", booleanAttribute], minlength: ["minlength", "minlength", numberAttribute], maxlength: ["maxlength", "maxlength", numberAttribute], min: "min", max: "max", pattern: "pattern", mask: "mask", appearance: "appearance", size: "size", theme: "theme", color: "color", dir: "dir", status: "status", value: "value", disabled: ["disabled", "disabled", booleanAttribute], signal: "signal", controlName: "controlName" }, outputs: { valueChange: "valueChange", input: "input", change: "change", focus: "focus", blur: "blur" }, ngImport: i0 });
1070
+ }
1071
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvBaseControl, decorators: [{
1072
+ type: Directive
1073
+ }], ctorParameters: () => [], propDecorators: { name: [{
1074
+ type: Input
1075
+ }], label: [{
1076
+ type: Input
1077
+ }], placeholder: [{
1078
+ type: Input
1079
+ }], hint: [{
1080
+ type: Input
1081
+ }], helperText: [{
1082
+ type: Input
1083
+ }], errorMessage: [{
1084
+ type: Input
1085
+ }], prefixIcon: [{
1086
+ type: Input
1087
+ }], suffixIcon: [{
1088
+ type: Input
1089
+ }], tooltip: [{
1090
+ type: Input
1091
+ }], ariaLabel: [{
1092
+ type: Input
1093
+ }], autocomplete: [{
1094
+ type: Input
1095
+ }], loadingText: [{
1096
+ type: Input
1097
+ }], tabIndex: [{
1098
+ type: Input,
1099
+ args: [{ transform: numberAttribute }]
1100
+ }], clearable: [{
1101
+ type: Input,
1102
+ args: [{ transform: booleanAttribute }]
1103
+ }], readonly: [{
1104
+ type: Input,
1105
+ args: [{ transform: booleanAttribute }]
1106
+ }], required: [{
1107
+ type: Input,
1108
+ args: [{ transform: booleanAttribute }]
1109
+ }], loading: [{
1110
+ type: Input,
1111
+ args: [{ transform: booleanAttribute }]
1112
+ }], floatingLabel: [{
1113
+ type: Input,
1114
+ args: [{ transform: booleanAttribute }]
1115
+ }], minlength: [{
1116
+ type: Input,
1117
+ args: [{ transform: numberAttribute }]
1118
+ }], maxlength: [{
1119
+ type: Input,
1120
+ args: [{ transform: numberAttribute }]
1121
+ }], min: [{
1122
+ type: Input
1123
+ }], max: [{
1124
+ type: Input
1125
+ }], pattern: [{
1126
+ type: Input
1127
+ }], mask: [{
1128
+ type: Input
1129
+ }], appearance: [{
1130
+ type: Input
1131
+ }], size: [{
1132
+ type: Input
1133
+ }], theme: [{
1134
+ type: Input
1135
+ }], color: [{
1136
+ type: Input
1137
+ }], dir: [{
1138
+ type: Input
1139
+ }], status: [{
1140
+ type: Input
1141
+ }], valueChange: [{
1142
+ type: Output
1143
+ }], input: [{
1144
+ type: Output
1145
+ }], change: [{
1146
+ type: Output
1147
+ }], focus: [{
1148
+ type: Output
1149
+ }], blur: [{
1150
+ type: Output
1151
+ }], value: [{
1152
+ type: Input
1153
+ }], disabled: [{
1154
+ type: Input,
1155
+ args: [{ transform: booleanAttribute }]
1156
+ }], signal: [{
1157
+ type: Input
1158
+ }], controlName: [{
1159
+ type: Input
1160
+ }] } });
1161
+ const requiredFn = (c) => {
1162
+ const v = c.value;
1163
+ const empty = v === null || v === undefined || v === '' || (Array.isArray(v) && !v.length);
1164
+ return empty ? { required: true } : null;
1165
+ };
1166
+
1167
+ /** Treat null/undefined/'' as "absent" — required is a separate validator. */
1168
+ const absent = (v) => v === null || v === undefined || v === '';
1169
+ const test = (re, key) => (c) => absent(c.value) || re.test(String(c.value)) ? null : { [key]: true };
1170
+ // ---- Patterns --------------------------------------------------------------
1171
+ const RE = {
1172
+ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
1173
+ phone: /^\+?[0-9]{7,15}$/,
1174
+ url: /^(https?:\/\/)([\w-]+\.)+[\w-]+(\/[\w./?%&=#-]*)?$/i,
1175
+ pan: /^[A-Z]{5}[0-9]{4}[A-Z]$/,
1176
+ gst: /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/,
1177
+ aadhaar: /^[2-9][0-9]{11}$/,
1178
+ cvv: /^[0-9]{3,4}$/,
1179
+ passport: /^[A-PR-WY][0-9]{7}$/, // Indian passport format
1180
+ drivingLicense: /^[A-Z]{2}[0-9]{2}\s?[0-9]{11}$/,
1181
+ pincode: /^[1-9][0-9]{5}$/,
1182
+ // Strong password: 8+, upper, lower, digit, special.
1183
+ password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/,
1184
+ };
1185
+ const ZvValidators = {
1186
+ required(c) {
1187
+ const v = c.value;
1188
+ const empty = absent(v) || (Array.isArray(v) && v.length === 0) || v === false;
1189
+ return empty ? { required: true } : null;
1190
+ },
1191
+ email: test(RE.email, 'email'),
1192
+ phone: test(RE.phone, 'phone'),
1193
+ url: test(RE.url, 'url'),
1194
+ pincode: test(RE.pincode, 'pincode'),
1195
+ cvv: test(RE.cvv, 'cvv'),
1196
+ passport: test(RE.passport, 'passport'),
1197
+ drivingLicense: test(RE.drivingLicense, 'drivingLicense'),
1198
+ password: test(RE.password, 'password'),
1199
+ pattern(p) {
1200
+ const re = typeof p === 'string' ? new RegExp(p) : p;
1201
+ return test(re, 'pattern');
1202
+ },
1203
+ regex(p) {
1204
+ return ZvValidators.pattern(p);
1205
+ },
1206
+ min(n) {
1207
+ return (c) => (absent(c.value) || +c.value >= n ? null : { min: { min: n, actual: +c.value } });
1208
+ },
1209
+ max(n) {
1210
+ return (c) => (absent(c.value) || +c.value <= n ? null : { max: { max: n, actual: +c.value } });
1211
+ },
1212
+ minLength(n) {
1213
+ return (c) => absent(c.value) || String(c.value).length >= n
1214
+ ? null
1215
+ : { minlength: { requiredLength: n, actualLength: String(c.value).length } };
1216
+ },
1217
+ maxLength(n) {
1218
+ return (c) => absent(c.value) || String(c.value).length <= n
1219
+ ? null
1220
+ : { maxlength: { requiredLength: n, actualLength: String(c.value).length } };
1221
+ },
1222
+ date(c) {
1223
+ if (absent(c.value))
1224
+ return null;
1225
+ const d = new Date(c.value);
1226
+ return isNaN(d.getTime()) ? { date: true } : null;
1227
+ },
1228
+ /** Minimum age in whole years relative to today. */
1229
+ age(minYears) {
1230
+ return (c) => {
1231
+ if (absent(c.value))
1232
+ return null;
1233
+ const dob = new Date(c.value);
1234
+ if (isNaN(dob.getTime()))
1235
+ return { date: true };
1236
+ const now = new Date();
1237
+ let age = now.getFullYear() - dob.getFullYear();
1238
+ const m = now.getMonth() - dob.getMonth();
1239
+ if (m < 0 || (m === 0 && now.getDate() < dob.getDate()))
1240
+ age--;
1241
+ return age >= minYears ? null : { age: { required: minYears, actual: age } };
1242
+ };
1243
+ },
1244
+ pan: test(RE.pan, 'pan'),
1245
+ gst: test(RE.gst, 'gst'),
1246
+ /** Aadhaar: format + Verhoeff checksum. */
1247
+ aadhaar(c) {
1248
+ if (absent(c.value))
1249
+ return null;
1250
+ const s = String(c.value).replace(/\s/g, '');
1251
+ return RE.aadhaar.test(s) && verhoeff(s) ? null : { aadhaar: true };
1252
+ },
1253
+ /** Credit card: digits + Luhn checksum. */
1254
+ creditCard(c) {
1255
+ if (absent(c.value))
1256
+ return null;
1257
+ const s = String(c.value).replace(/[\s-]/g, '');
1258
+ return /^[0-9]{12,19}$/.test(s) && luhn(s) ? null : { creditCard: true };
1259
+ },
1260
+ /** Cross-field: confirm `field` matches `other`. Apply on a FormGroup. */
1261
+ confirm(field, other, key = 'mismatch') {
1262
+ return (group) => {
1263
+ const a = group.get(field)?.value;
1264
+ const b = group.get(other)?.value;
1265
+ return a === b ? null : { [key]: true };
1266
+ };
1267
+ },
1268
+ };
1269
+ // ---- Checksum helpers ------------------------------------------------------
1270
+ function luhn(num) {
1271
+ let sum = 0;
1272
+ let alt = false;
1273
+ for (let i = num.length - 1; i >= 0; i--) {
1274
+ let d = +num[i];
1275
+ if (alt) {
1276
+ d *= 2;
1277
+ if (d > 9)
1278
+ d -= 9;
1279
+ }
1280
+ sum += d;
1281
+ alt = !alt;
1282
+ }
1283
+ return sum % 10 === 0;
1284
+ }
1285
+ const D = [
1286
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
1287
+ [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
1288
+ [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
1289
+ [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
1290
+ [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
1291
+ [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
1292
+ [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
1293
+ [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
1294
+ [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
1295
+ [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
1296
+ ];
1297
+ const P = [
1298
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
1299
+ [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
1300
+ [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
1301
+ [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
1302
+ [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
1303
+ [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
1304
+ [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
1305
+ [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
1306
+ ];
1307
+ function verhoeff(num) {
1308
+ let c = 0;
1309
+ const digits = num.split('').reverse().map(Number);
1310
+ for (let i = 0; i < digits.length; i++)
1311
+ c = D[c][P[i % 8][digits[i]]];
1312
+ return c === 0;
1313
+ }
1314
+
1315
+ /**
1316
+ * Base for all single-line / multi-line text controls. Subclasses set
1317
+ * `controlType`, the native `inputType`, and any extra validators.
1318
+ */
1319
+ class ZvTextControlBase extends ZvBaseControl {
1320
+ maxRows = 4; // textarea only
1321
+ onNativeInput(ev) {
1322
+ const target = ev.target;
1323
+ this.handleInput(target.value);
1324
+ }
1325
+ onNativeChange(ev) {
1326
+ const target = ev.target;
1327
+ this.handleChange(target.value);
1328
+ }
1329
+ ownValidators() {
1330
+ const v = [];
1331
+ if (this.minlength != null)
1332
+ v.push(ZvValidators.minLength(this.minlength));
1333
+ if (this.maxlength != null)
1334
+ v.push(ZvValidators.maxLength(this.maxlength));
1335
+ if (this.pattern)
1336
+ v.push(ZvValidators.pattern(this.pattern));
1337
+ return v;
1338
+ }
1339
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvTextControlBase, deps: null, target: i0.ɵɵFactoryTarget.Directive });
1340
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.17", type: ZvTextControlBase, isStandalone: true, inputs: { maxRows: "maxRows" }, usesInheritance: true, ngImport: i0 });
1341
+ }
1342
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvTextControlBase, decorators: [{
1343
+ type: Directive
1344
+ }], propDecorators: { maxRows: [{
1345
+ type: Input
1346
+ }] } });
1347
+
1348
+ /** Shared template for single-line text controls (input/email/password/number). */
1349
+ const TEXT_TEMPLATE = `
1350
+ <zv-field-shell
1351
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1352
+ [helperText]="helperText" [errorText]="errorMessage"
1353
+ [invalid]="status === 'error'" [prefixIcon]="prefixIcon" [suffixIcon]="suffixIcon"
1354
+ [loading]="loading" [loadingText]="loadingText" [clearable]="clearable"
1355
+ [hasValue]="hasValue()" [disabled]="disabledSig()" [readonly]="readonly"
1356
+ (clear)="clear()">
1357
+ <input
1358
+ [id]="id" [type]="inputType" [name]="name" [value]="valueSig() ?? ''"
1359
+ [placeholder]="placeholder" [disabled]="disabledSig()" [readOnly]="readonly"
1360
+ [attr.maxlength]="maxlength" [attr.minlength]="minlength"
1361
+ [attr.min]="min" [attr.max]="max"
1362
+ [attr.autocomplete]="autocomplete" [attr.aria-label]="ariaLabel || label"
1363
+ [attr.aria-invalid]="status === 'error'" [attr.title]="tooltip" [tabIndex]="tabIndex"
1364
+ (input)="onNativeInput($event)" (change)="onNativeChange($event)"
1365
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1366
+ </zv-field-shell>
1367
+ `;
1368
+
1369
+ class ZvEmail extends ZvTextControlBase {
1370
+ controlType = 'email';
1371
+ inputType = 'email';
1372
+ autocomplete = 'email';
1373
+ ownValidators() {
1374
+ return [...super.ownValidators(), ZvValidators.email];
1375
+ }
1376
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvEmail, deps: null, target: i0.ɵɵFactoryTarget.Component });
1377
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: ZvEmail, isStandalone: true, selector: "zv-email", providers: controlProviders(ZvEmail), usesInheritance: true, ngImport: i0, template: "\n <zv-field-shell\n [for]=\"id\" [label]=\"label\" [required]=\"required\" [hint]=\"hint\"\n [helperText]=\"helperText\" [errorText]=\"errorMessage\"\n [invalid]=\"status === 'error'\" [prefixIcon]=\"prefixIcon\" [suffixIcon]=\"suffixIcon\"\n [loading]=\"loading\" [loadingText]=\"loadingText\" [clearable]=\"clearable\"\n [hasValue]=\"hasValue()\" [disabled]=\"disabledSig()\" [readonly]=\"readonly\"\n (clear)=\"clear()\">\n <input\n [id]=\"id\" [type]=\"inputType\" [name]=\"name\" [value]=\"valueSig() ?? ''\"\n [placeholder]=\"placeholder\" [disabled]=\"disabledSig()\" [readOnly]=\"readonly\"\n [attr.maxlength]=\"maxlength\" [attr.minlength]=\"minlength\"\n [attr.min]=\"min\" [attr.max]=\"max\"\n [attr.autocomplete]=\"autocomplete\" [attr.aria-label]=\"ariaLabel || label\"\n [attr.aria-invalid]=\"status === 'error'\" [attr.title]=\"tooltip\" [tabIndex]=\"tabIndex\"\n (input)=\"onNativeInput($event)\" (change)=\"onNativeChange($event)\"\n (focus)=\"handleFocus($event)\" (blur)=\"handleBlur($event)\" />\n </zv-field-shell>\n", isInline: true, dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1378
+ }
1379
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvEmail, decorators: [{
1380
+ type: Component,
1381
+ args: [{
1382
+ selector: 'zv-email',
1383
+ standalone: true,
1384
+ imports: [ZvFieldShell],
1385
+ changeDetection: ChangeDetectionStrategy.OnPush,
1386
+ providers: controlProviders(ZvEmail),
1387
+ template: TEXT_TEMPLATE,
1388
+ }]
1389
+ }] });
1390
+
1391
+ class ZvPassword extends ZvTextControlBase {
1392
+ controlType = 'password';
1393
+ inputType = 'password';
1394
+ autocomplete = 'current-password';
1395
+ toggle = true;
1396
+ strong = false;
1397
+ reveal = signal(false, ...(ngDevMode ? [{ debugName: "reveal" }] : /* istanbul ignore next */ []));
1398
+ ownValidators() {
1399
+ const v = super.ownValidators();
1400
+ if (this.strong)
1401
+ v.push(ZvValidators.password);
1402
+ return v;
1403
+ }
1404
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvPassword, deps: null, target: i0.ɵɵFactoryTarget.Component });
1405
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvPassword, isStandalone: true, selector: "zv-password", inputs: { toggle: ["toggle", "toggle", booleanAttribute], strong: ["strong", "strong", booleanAttribute] }, providers: controlProviders(ZvPassword), usesInheritance: true, ngImport: i0, template: `
1406
+ <zv-field-shell
1407
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1408
+ [helperText]="helperText" [errorText]="errorMessage"
1409
+ [invalid]="status === 'error'" [prefixIcon]="prefixIcon"
1410
+ [loading]="loading" [clearable]="false" [hasValue]="hasValue()"
1411
+ [disabled]="disabledSig()" [readonly]="readonly">
1412
+ <input
1413
+ [id]="id" [type]="reveal() ? 'text' : 'password'" [name]="name"
1414
+ [value]="valueSig() ?? ''" [placeholder]="placeholder"
1415
+ [disabled]="disabledSig()" [readOnly]="readonly"
1416
+ [attr.minlength]="minlength" [attr.maxlength]="maxlength"
1417
+ [attr.autocomplete]="autocomplete" [attr.aria-label]="ariaLabel || label"
1418
+ [tabIndex]="tabIndex"
1419
+ (input)="onNativeInput($event)" (change)="onNativeChange($event)"
1420
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1421
+ @if (toggle) {
1422
+ <button type="button" class="zv-clear" tabindex="-1"
1423
+ [attr.aria-label]="reveal() ? 'Hide password' : 'Show password'"
1424
+ (click)="reveal.set(!reveal())">{{ reveal() ? '🙈' : '👁' }}</button>
1425
+ }
1426
+ </zv-field-shell>
1427
+ `, isInline: true, dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1428
+ }
1429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvPassword, decorators: [{
1430
+ type: Component,
1431
+ args: [{
1432
+ selector: 'zv-password',
1433
+ standalone: true,
1434
+ imports: [ZvFieldShell],
1435
+ changeDetection: ChangeDetectionStrategy.OnPush,
1436
+ providers: controlProviders(ZvPassword),
1437
+ template: `
1438
+ <zv-field-shell
1439
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1440
+ [helperText]="helperText" [errorText]="errorMessage"
1441
+ [invalid]="status === 'error'" [prefixIcon]="prefixIcon"
1442
+ [loading]="loading" [clearable]="false" [hasValue]="hasValue()"
1443
+ [disabled]="disabledSig()" [readonly]="readonly">
1444
+ <input
1445
+ [id]="id" [type]="reveal() ? 'text' : 'password'" [name]="name"
1446
+ [value]="valueSig() ?? ''" [placeholder]="placeholder"
1447
+ [disabled]="disabledSig()" [readOnly]="readonly"
1448
+ [attr.minlength]="minlength" [attr.maxlength]="maxlength"
1449
+ [attr.autocomplete]="autocomplete" [attr.aria-label]="ariaLabel || label"
1450
+ [tabIndex]="tabIndex"
1451
+ (input)="onNativeInput($event)" (change)="onNativeChange($event)"
1452
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1453
+ @if (toggle) {
1454
+ <button type="button" class="zv-clear" tabindex="-1"
1455
+ [attr.aria-label]="reveal() ? 'Hide password' : 'Show password'"
1456
+ (click)="reveal.set(!reveal())">{{ reveal() ? '🙈' : '👁' }}</button>
1457
+ }
1458
+ </zv-field-shell>
1459
+ `,
1460
+ }]
1461
+ }], propDecorators: { toggle: [{
1462
+ type: Input,
1463
+ args: [{ transform: booleanAttribute }]
1464
+ }], strong: [{
1465
+ type: Input,
1466
+ args: [{ transform: booleanAttribute }]
1467
+ }] } });
1468
+
1469
+ class ZvNumber extends ZvBaseControl {
1470
+ controlType = 'number';
1471
+ step = 1;
1472
+ onInput(ev) {
1473
+ const raw = ev.target.value;
1474
+ this.handleInput(raw === '' ? null : Number(raw));
1475
+ }
1476
+ ownValidators() {
1477
+ const v = [];
1478
+ if (this.min != null)
1479
+ v.push(ZvValidators.min(Number(this.min)));
1480
+ if (this.max != null)
1481
+ v.push(ZvValidators.max(Number(this.max)));
1482
+ return v;
1483
+ }
1484
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvNumber, deps: null, target: i0.ɵɵFactoryTarget.Component });
1485
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "21.2.17", type: ZvNumber, isStandalone: true, selector: "zv-number", inputs: { step: ["step", "step", numberAttribute] }, providers: controlProviders(ZvNumber), usesInheritance: true, ngImport: i0, template: `
1486
+ <zv-field-shell
1487
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1488
+ [helperText]="helperText" [errorText]="errorMessage"
1489
+ [invalid]="status === 'error'" [prefixIcon]="prefixIcon" [suffixIcon]="suffixIcon"
1490
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1491
+ [readonly]="readonly" (clear)="clear()">
1492
+ <input type="number"
1493
+ [id]="id" [name]="name" [value]="valueSig() ?? ''" [placeholder]="placeholder"
1494
+ [disabled]="disabledSig()" [readOnly]="readonly"
1495
+ [attr.min]="min" [attr.max]="max" [attr.step]="step"
1496
+ [attr.aria-label]="ariaLabel || label" [tabIndex]="tabIndex"
1497
+ (input)="onInput($event)" (change)="onInput($event)"
1498
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1499
+ </zv-field-shell>
1500
+ `, isInline: true, dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1501
+ }
1502
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvNumber, decorators: [{
1503
+ type: Component,
1504
+ args: [{
1505
+ selector: 'zv-number',
1506
+ standalone: true,
1507
+ imports: [ZvFieldShell],
1508
+ changeDetection: ChangeDetectionStrategy.OnPush,
1509
+ providers: controlProviders(ZvNumber),
1510
+ template: `
1511
+ <zv-field-shell
1512
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1513
+ [helperText]="helperText" [errorText]="errorMessage"
1514
+ [invalid]="status === 'error'" [prefixIcon]="prefixIcon" [suffixIcon]="suffixIcon"
1515
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1516
+ [readonly]="readonly" (clear)="clear()">
1517
+ <input type="number"
1518
+ [id]="id" [name]="name" [value]="valueSig() ?? ''" [placeholder]="placeholder"
1519
+ [disabled]="disabledSig()" [readOnly]="readonly"
1520
+ [attr.min]="min" [attr.max]="max" [attr.step]="step"
1521
+ [attr.aria-label]="ariaLabel || label" [tabIndex]="tabIndex"
1522
+ (input)="onInput($event)" (change)="onInput($event)"
1523
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1524
+ </zv-field-shell>
1525
+ `,
1526
+ }]
1527
+ }], propDecorators: { step: [{
1528
+ type: Input,
1529
+ args: [{ transform: numberAttribute }]
1530
+ }] } });
1531
+
1532
+ class ZvTextarea extends ZvTextControlBase {
1533
+ controlType = 'textarea';
1534
+ inputType = 'textarea';
1535
+ rows = 4;
1536
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvTextarea, deps: null, target: i0.ɵɵFactoryTarget.Component });
1537
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "21.2.17", type: ZvTextarea, isStandalone: true, selector: "zv-textarea", inputs: { rows: ["rows", "rows", numberAttribute] }, providers: controlProviders(ZvTextarea), usesInheritance: true, ngImport: i0, template: `
1538
+ <zv-field-shell
1539
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1540
+ [helperText]="helperText" [errorText]="errorMessage"
1541
+ [invalid]="status === 'error'" [clearable]="clearable" [hasValue]="hasValue()"
1542
+ [disabled]="disabledSig()" [readonly]="readonly" (clear)="clear()">
1543
+ <textarea
1544
+ [id]="id" [name]="name" [value]="valueSig() ?? ''" [placeholder]="placeholder"
1545
+ [disabled]="disabledSig()" [readOnly]="readonly" [rows]="rows"
1546
+ [attr.maxlength]="maxlength" [attr.minlength]="minlength"
1547
+ [attr.aria-label]="ariaLabel || label" [tabIndex]="tabIndex"
1548
+ (input)="onNativeInput($event)" (change)="onNativeChange($event)"
1549
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)"></textarea>
1550
+ </zv-field-shell>
1551
+ `, isInline: true, dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1552
+ }
1553
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvTextarea, decorators: [{
1554
+ type: Component,
1555
+ args: [{
1556
+ selector: 'zv-textarea',
1557
+ standalone: true,
1558
+ imports: [ZvFieldShell],
1559
+ changeDetection: ChangeDetectionStrategy.OnPush,
1560
+ providers: controlProviders(ZvTextarea),
1561
+ template: `
1562
+ <zv-field-shell
1563
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1564
+ [helperText]="helperText" [errorText]="errorMessage"
1565
+ [invalid]="status === 'error'" [clearable]="clearable" [hasValue]="hasValue()"
1566
+ [disabled]="disabledSig()" [readonly]="readonly" (clear)="clear()">
1567
+ <textarea
1568
+ [id]="id" [name]="name" [value]="valueSig() ?? ''" [placeholder]="placeholder"
1569
+ [disabled]="disabledSig()" [readOnly]="readonly" [rows]="rows"
1570
+ [attr.maxlength]="maxlength" [attr.minlength]="minlength"
1571
+ [attr.aria-label]="ariaLabel || label" [tabIndex]="tabIndex"
1572
+ (input)="onNativeInput($event)" (change)="onNativeChange($event)"
1573
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)"></textarea>
1574
+ </zv-field-shell>
1575
+ `,
1576
+ }]
1577
+ }], propDecorators: { rows: [{
1578
+ type: Input,
1579
+ args: [{ transform: numberAttribute }]
1580
+ }] } });
1581
+
1582
+ /**
1583
+ * Wraps the native date input for full a11y + parity, normalizing the value to
1584
+ * an ISO `yyyy-MM-dd` string. Swap the inner element for a custom calendar
1585
+ * without touching the CVA contract.
1586
+ */
1587
+ class ZvDatepicker extends ZvBaseControl {
1588
+ controlType = 'datepicker';
1589
+ onInput(ev) {
1590
+ this.handleInput(ev.target.value || null);
1591
+ }
1592
+ ownValidators() {
1593
+ return [ZvValidators.date];
1594
+ }
1595
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvDatepicker, deps: null, target: i0.ɵɵFactoryTarget.Component });
1596
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: ZvDatepicker, isStandalone: true, selector: "zv-datepicker", providers: controlProviders(ZvDatepicker), usesInheritance: true, ngImport: i0, template: `
1597
+ <zv-field-shell
1598
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1599
+ [errorText]="errorMessage" [invalid]="status === 'error'" [prefixIcon]="prefixIcon"
1600
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1601
+ (clear)="clear()">
1602
+ <input type="date" [id]="id" [name]="name" [value]="valueSig() ?? ''"
1603
+ [disabled]="disabledSig()" [readOnly]="readonly"
1604
+ [attr.min]="min" [attr.max]="max" [attr.aria-label]="ariaLabel || label"
1605
+ [tabIndex]="tabIndex" (input)="onInput($event)" (change)="onInput($event)"
1606
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1607
+ </zv-field-shell>
1608
+ `, isInline: true, dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1609
+ }
1610
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvDatepicker, decorators: [{
1611
+ type: Component,
1612
+ args: [{
1613
+ selector: 'zv-datepicker',
1614
+ standalone: true,
1615
+ imports: [ZvFieldShell],
1616
+ changeDetection: ChangeDetectionStrategy.OnPush,
1617
+ providers: controlProviders(ZvDatepicker),
1618
+ template: `
1619
+ <zv-field-shell
1620
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1621
+ [errorText]="errorMessage" [invalid]="status === 'error'" [prefixIcon]="prefixIcon"
1622
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1623
+ (clear)="clear()">
1624
+ <input type="date" [id]="id" [name]="name" [value]="valueSig() ?? ''"
1625
+ [disabled]="disabledSig()" [readOnly]="readonly"
1626
+ [attr.min]="min" [attr.max]="max" [attr.aria-label]="ariaLabel || label"
1627
+ [tabIndex]="tabIndex" (input)="onInput($event)" (change)="onInput($event)"
1628
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1629
+ </zv-field-shell>
1630
+ `,
1631
+ }]
1632
+ }] });
1633
+
1634
+ class ZvTimepicker extends ZvBaseControl {
1635
+ controlType = 'timepicker';
1636
+ onInput(ev) {
1637
+ this.handleInput(ev.target.value || null);
1638
+ }
1639
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvTimepicker, deps: null, target: i0.ɵɵFactoryTarget.Component });
1640
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.17", type: ZvTimepicker, isStandalone: true, selector: "zv-timepicker", providers: controlProviders(ZvTimepicker), usesInheritance: true, ngImport: i0, template: `
1641
+ <zv-field-shell
1642
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1643
+ [errorText]="errorMessage" [invalid]="status === 'error'" [prefixIcon]="prefixIcon"
1644
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1645
+ (clear)="clear()">
1646
+ <input type="time" [id]="id" [name]="name" [value]="valueSig() ?? ''"
1647
+ [disabled]="disabledSig()" [readOnly]="readonly"
1648
+ [attr.min]="min" [attr.max]="max" [attr.aria-label]="ariaLabel || label"
1649
+ [tabIndex]="tabIndex" (input)="onInput($event)" (change)="onInput($event)"
1650
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1651
+ </zv-field-shell>
1652
+ `, isInline: true, dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1653
+ }
1654
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvTimepicker, decorators: [{
1655
+ type: Component,
1656
+ args: [{
1657
+ selector: 'zv-timepicker',
1658
+ standalone: true,
1659
+ imports: [ZvFieldShell],
1660
+ changeDetection: ChangeDetectionStrategy.OnPush,
1661
+ providers: controlProviders(ZvTimepicker),
1662
+ template: `
1663
+ <zv-field-shell
1664
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1665
+ [errorText]="errorMessage" [invalid]="status === 'error'" [prefixIcon]="prefixIcon"
1666
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1667
+ (clear)="clear()">
1668
+ <input type="time" [id]="id" [name]="name" [value]="valueSig() ?? ''"
1669
+ [disabled]="disabledSig()" [readOnly]="readonly"
1670
+ [attr.min]="min" [attr.max]="max" [attr.aria-label]="ariaLabel || label"
1671
+ [tabIndex]="tabIndex" (input)="onInput($event)" (change)="onInput($event)"
1672
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
1673
+ </zv-field-shell>
1674
+ `,
1675
+ }]
1676
+ }] });
1677
+
1678
+ /**
1679
+ * Single-select with virtual-scrolled listbox + keyboard support.
1680
+ * Accepts either `ZvOption[]` or a raw `T[]` via `bindLabel`/`bindValue`.
1681
+ */
1682
+ class ZvSelect extends SignalControlAccessor {
1683
+ controlType = 'select';
1684
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1685
+ bindLabel = input('label', ...(ngDevMode ? [{ debugName: "bindLabel" }] : /* istanbul ignore next */ []));
1686
+ bindValue = input('value', ...(ngDevMode ? [{ debugName: "bindValue" }] : /* istanbul ignore next */ []));
1687
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1688
+ options = computed(() => {
1689
+ const label = this.bindLabel();
1690
+ const value = this.bindValue();
1691
+ return this.items().map((it) => it && typeof it === 'object' && label in it
1692
+ ? {
1693
+ label: String(it[label]),
1694
+ value: (it[value] ?? it),
1695
+ }
1696
+ : { label: String(it), value: it });
1697
+ }, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
1698
+ selectedLabel = computed(() => {
1699
+ const v = this.current();
1700
+ return this.options().find((o) => o.value === v)?.label ?? '';
1701
+ }, ...(ngDevMode ? [{ debugName: "selectedLabel" }] : /* istanbul ignore next */ []));
1702
+ toggle() {
1703
+ if (!this.disabled() && !this.readonly())
1704
+ this.open.set(!this.open());
1705
+ }
1706
+ pick(o) {
1707
+ this.commit(o.value, 'change');
1708
+ this.open.set(false);
1709
+ }
1710
+ onKeydown(ev) {
1711
+ if (ev.key === 'Enter' || ev.key === ' ') {
1712
+ ev.preventDefault();
1713
+ this.toggle();
1714
+ }
1715
+ else if (ev.key === 'Escape') {
1716
+ this.open.set(false);
1717
+ }
1718
+ }
1719
+ trackByValue = (_, o) => o.value;
1720
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvSelect, deps: null, target: i0.ɵɵFactoryTarget.Component });
1721
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvSelect, isStandalone: true, selector: "zv-select", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, bindLabel: { classPropertyName: "bindLabel", publicName: "bindLabel", isSignal: true, isRequired: false, transformFunction: null }, bindValue: { classPropertyName: "bindValue", publicName: "bindValue", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideSignalControl(ZvSelect)], usesInheritance: true, ngImport: i0, template: `
1722
+ <zv-field-shell
1723
+ [for]="id()" [label]="label()" [required]="required()" [hint]="hint()"
1724
+ [helperText]="helperText()" [errorText]="errorText()"
1725
+ [invalid]="showError()" [clearable]="clearable()" [hasValue]="hasValue()"
1726
+ [disabled]="disabled()" [readonly]="readonly()" (clear)="clear()">
1727
+ <button type="button" class="zv-select-trigger" role="combobox"
1728
+ [id]="id()" [disabled]="disabled()" [attr.aria-expanded]="open()"
1729
+ [attr.aria-haspopup]="'listbox'" [attr.aria-label]="ariaLabel() || label()"
1730
+ [tabIndex]="tabIndex()"
1731
+ (click)="toggle()" (keydown)="onKeydown($event)"
1732
+ (blur)="onBlur($event)" (focus)="onFocus($event)">
1733
+ {{ selectedLabel() || placeholder() }}
1734
+ </button>
1735
+ </zv-field-shell>
1736
+
1737
+ @if (open()) {
1738
+ <ul class="zv-listbox" role="listbox">
1739
+ <cdk-virtual-scroll-viewport [itemSize]="36" class="zv-vs">
1740
+ <li *cdkVirtualFor="let opt of options(); trackBy: trackByValue"
1741
+ role="option" class="zv-option"
1742
+ [class.zv-selected]="opt.value === current()"
1743
+ [attr.aria-selected]="opt.value === current()"
1744
+ [class.zv-disabled]="opt.disabled"
1745
+ (click)="!opt.disabled && pick(opt)">
1746
+ {{ opt.label }}
1747
+ </li>
1748
+ </cdk-virtual-scroll-viewport>
1749
+ </ul>
1750
+ }
1751
+ `, isInline: true, styles: [".zv-select-trigger{all:unset;flex:1;cursor:pointer;padding:.55rem 0}.zv-listbox{list-style:none;margin:.25rem 0 0;padding:0;border:1px solid var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem);background:var(--zv-bg,#fff);box-shadow:0 8px 24px #0000001f}.zv-vs{height:240px}.zv-option{height:36px;display:flex;align-items:center;padding:0 .75rem;cursor:pointer}.zv-option:hover{background:var(--zv-hover,#f3f4f6)}.zv-selected{background:var(--zv-primary,#2563eb);color:#fff}.zv-disabled{opacity:.5;pointer-events:none}\n"], dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i1.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1752
+ }
1753
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvSelect, decorators: [{
1754
+ type: Component,
1755
+ args: [{ selector: 'zv-select', imports: [ZvFieldShell, ScrollingModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [provideSignalControl(ZvSelect)], template: `
1756
+ <zv-field-shell
1757
+ [for]="id()" [label]="label()" [required]="required()" [hint]="hint()"
1758
+ [helperText]="helperText()" [errorText]="errorText()"
1759
+ [invalid]="showError()" [clearable]="clearable()" [hasValue]="hasValue()"
1760
+ [disabled]="disabled()" [readonly]="readonly()" (clear)="clear()">
1761
+ <button type="button" class="zv-select-trigger" role="combobox"
1762
+ [id]="id()" [disabled]="disabled()" [attr.aria-expanded]="open()"
1763
+ [attr.aria-haspopup]="'listbox'" [attr.aria-label]="ariaLabel() || label()"
1764
+ [tabIndex]="tabIndex()"
1765
+ (click)="toggle()" (keydown)="onKeydown($event)"
1766
+ (blur)="onBlur($event)" (focus)="onFocus($event)">
1767
+ {{ selectedLabel() || placeholder() }}
1768
+ </button>
1769
+ </zv-field-shell>
1770
+
1771
+ @if (open()) {
1772
+ <ul class="zv-listbox" role="listbox">
1773
+ <cdk-virtual-scroll-viewport [itemSize]="36" class="zv-vs">
1774
+ <li *cdkVirtualFor="let opt of options(); trackBy: trackByValue"
1775
+ role="option" class="zv-option"
1776
+ [class.zv-selected]="opt.value === current()"
1777
+ [attr.aria-selected]="opt.value === current()"
1778
+ [class.zv-disabled]="opt.disabled"
1779
+ (click)="!opt.disabled && pick(opt)">
1780
+ {{ opt.label }}
1781
+ </li>
1782
+ </cdk-virtual-scroll-viewport>
1783
+ </ul>
1784
+ }
1785
+ `, styles: [".zv-select-trigger{all:unset;flex:1;cursor:pointer;padding:.55rem 0}.zv-listbox{list-style:none;margin:.25rem 0 0;padding:0;border:1px solid var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem);background:var(--zv-bg,#fff);box-shadow:0 8px 24px #0000001f}.zv-vs{height:240px}.zv-option{height:36px;display:flex;align-items:center;padding:0 .75rem;cursor:pointer}.zv-option:hover{background:var(--zv-hover,#f3f4f6)}.zv-selected{background:var(--zv-primary,#2563eb);color:#fff}.zv-disabled{opacity:.5;pointer-events:none}\n"] }]
1786
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], bindLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "bindLabel", required: false }] }], bindValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "bindValue", required: false }] }] } });
1787
+
1788
+ class ZvMultiselect extends ZvBaseControl {
1789
+ controlType = 'multiselect';
1790
+ items = [];
1791
+ bindLabel = 'label';
1792
+ bindValue = 'value';
1793
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1794
+ get options() {
1795
+ return this.items.map((it) => it && typeof it === 'object' && this.bindLabel in it
1796
+ ? { label: it[this.bindLabel], value: it[this.bindValue] ?? it }
1797
+ : { label: String(it), value: it });
1798
+ }
1799
+ summary = computed(() => {
1800
+ const v = this.valueSig() ?? [];
1801
+ return v.length ? `${v.length} selected` : '';
1802
+ }, ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
1803
+ isSelected = (v) => (this.valueSig() ?? []).includes(v);
1804
+ toggle() { if (!this.disabledSig())
1805
+ this.open.set(!this.open()); }
1806
+ picktoggle(o) {
1807
+ const cur = [...(this.valueSig() ?? [])];
1808
+ const i = cur.indexOf(o.value);
1809
+ i >= 0 ? cur.splice(i, 1) : cur.push(o.value);
1810
+ this.handleChange(cur);
1811
+ }
1812
+ trackByValue = (_, o) => o.value;
1813
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvMultiselect, deps: null, target: i0.ɵɵFactoryTarget.Component });
1814
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvMultiselect, isStandalone: true, selector: "zv-multiselect", inputs: { items: "items", bindLabel: "bindLabel", bindValue: "bindValue" }, providers: controlProviders(ZvMultiselect), usesInheritance: true, ngImport: i0, template: `
1815
+ <zv-field-shell
1816
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1817
+ [errorText]="errorMessage" [invalid]="status === 'error'"
1818
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1819
+ (clear)="clear()">
1820
+ <button type="button" class="zv-select-trigger" role="combobox"
1821
+ [id]="id" [disabled]="disabledSig()" [attr.aria-expanded]="open()"
1822
+ [tabIndex]="tabIndex" (click)="toggle()" (blur)="handleBlur($event)">
1823
+ {{ summary() || placeholder }}
1824
+ </button>
1825
+ </zv-field-shell>
1826
+ @if (open()) {
1827
+ <ul class="zv-listbox" role="listbox" aria-multiselectable="true">
1828
+ <cdk-virtual-scroll-viewport [itemSize]="36" class="zv-vs">
1829
+ <li *cdkVirtualFor="let opt of options; trackBy: trackByValue"
1830
+ role="option" class="zv-option" [class.zv-selected]="isSelected(opt.value)"
1831
+ [attr.aria-selected]="isSelected(opt.value)" (click)="picktoggle(opt)">
1832
+ <input type="checkbox" [checked]="isSelected(opt.value)" tabindex="-1" />
1833
+ {{ opt.label }}
1834
+ </li>
1835
+ </cdk-virtual-scroll-viewport>
1836
+ </ul>
1837
+ }
1838
+ `, isInline: true, styles: [".zv-select-trigger{all:unset;flex:1;cursor:pointer;padding:.55rem 0}.zv-listbox{list-style:none;margin:.25rem 0 0;padding:0;background:var(--zv-bg,#fff);border:1px solid var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem);box-shadow:0 8px 24px #0000001f}.zv-vs{height:240px}.zv-option{height:36px;display:flex;align-items:center;gap:.5rem;padding:0 .75rem;cursor:pointer}.zv-option:hover{background:var(--zv-hover,#f3f4f6)}\n"], dependencies: [{ kind: "component", type: ZvFieldShell, selector: "zv-field-shell", inputs: ["for", "label", "required", "hint", "helperText", "errorText", "invalid", "prefixIcon", "suffixIcon", "loading", "loadingText", "clearable", "hasValue", "disabled", "readonly"], outputs: ["clear"] }, { kind: "ngmodule", type: ScrollingModule }, { kind: "directive", type: i1.CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: i1.CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: i1.CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1839
+ }
1840
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvMultiselect, decorators: [{
1841
+ type: Component,
1842
+ args: [{ selector: 'zv-multiselect', standalone: true, imports: [ZvFieldShell, ScrollingModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: controlProviders(ZvMultiselect), template: `
1843
+ <zv-field-shell
1844
+ [for]="id" [label]="label" [required]="required" [hint]="hint"
1845
+ [errorText]="errorMessage" [invalid]="status === 'error'"
1846
+ [clearable]="clearable" [hasValue]="hasValue()" [disabled]="disabledSig()"
1847
+ (clear)="clear()">
1848
+ <button type="button" class="zv-select-trigger" role="combobox"
1849
+ [id]="id" [disabled]="disabledSig()" [attr.aria-expanded]="open()"
1850
+ [tabIndex]="tabIndex" (click)="toggle()" (blur)="handleBlur($event)">
1851
+ {{ summary() || placeholder }}
1852
+ </button>
1853
+ </zv-field-shell>
1854
+ @if (open()) {
1855
+ <ul class="zv-listbox" role="listbox" aria-multiselectable="true">
1856
+ <cdk-virtual-scroll-viewport [itemSize]="36" class="zv-vs">
1857
+ <li *cdkVirtualFor="let opt of options; trackBy: trackByValue"
1858
+ role="option" class="zv-option" [class.zv-selected]="isSelected(opt.value)"
1859
+ [attr.aria-selected]="isSelected(opt.value)" (click)="picktoggle(opt)">
1860
+ <input type="checkbox" [checked]="isSelected(opt.value)" tabindex="-1" />
1861
+ {{ opt.label }}
1862
+ </li>
1863
+ </cdk-virtual-scroll-viewport>
1864
+ </ul>
1865
+ }
1866
+ `, styles: [".zv-select-trigger{all:unset;flex:1;cursor:pointer;padding:.55rem 0}.zv-listbox{list-style:none;margin:.25rem 0 0;padding:0;background:var(--zv-bg,#fff);border:1px solid var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem);box-shadow:0 8px 24px #0000001f}.zv-vs{height:240px}.zv-option{height:36px;display:flex;align-items:center;gap:.5rem;padding:0 .75rem;cursor:pointer}.zv-option:hover{background:var(--zv-hover,#f3f4f6)}\n"] }]
1867
+ }], propDecorators: { items: [{
1868
+ type: Input
1869
+ }], bindLabel: [{
1870
+ type: Input
1871
+ }], bindValue: [{
1872
+ type: Input
1873
+ }] } });
1874
+
1875
+ class ZvCheckbox extends ZvBaseControl {
1876
+ controlType = 'checkbox';
1877
+ onToggle(ev) {
1878
+ this.handleChange(ev.target.checked);
1879
+ }
1880
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvCheckbox, deps: null, target: i0.ɵɵFactoryTarget.Component });
1881
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvCheckbox, isStandalone: true, selector: "zv-checkbox", providers: controlProviders(ZvCheckbox), usesInheritance: true, ngImport: i0, template: `
1882
+ <label class="zv-check">
1883
+ <input type="checkbox" [id]="id" [name]="name" [checked]="!!valueSig()"
1884
+ [disabled]="disabledSig()" [attr.aria-label]="ariaLabel || label"
1885
+ [tabIndex]="tabIndex" (change)="onToggle($event)" (blur)="handleBlur($event)" />
1886
+ <span class="zv-box" aria-hidden="true"></span>
1887
+ @if (label) { <span class="zv-text">{{ label }}</span> }
1888
+ </label>
1889
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
1890
+ `, isInline: true, styles: [".zv-check{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}input{position:absolute;opacity:0;width:1.1rem;height:1.1rem}.zv-box{width:1.1rem;height:1.1rem;border:2px solid var(--zv-border,#d1d5db);border-radius:.25rem;display:inline-block;transition:.15s}input:checked+.zv-box{background:var(--zv-primary,#2563eb);border-color:var(--zv-primary,#2563eb);background-image:url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3'><path d='M5 13l4 4L19 7'/></svg>\");background-size:contain}input:focus-visible+.zv-box{box-shadow:0 0 0 3px var(--zv-primary-ring,rgba(37,99,235,.3))}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1891
+ }
1892
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvCheckbox, decorators: [{
1893
+ type: Component,
1894
+ args: [{ selector: 'zv-checkbox', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: controlProviders(ZvCheckbox), template: `
1895
+ <label class="zv-check">
1896
+ <input type="checkbox" [id]="id" [name]="name" [checked]="!!valueSig()"
1897
+ [disabled]="disabledSig()" [attr.aria-label]="ariaLabel || label"
1898
+ [tabIndex]="tabIndex" (change)="onToggle($event)" (blur)="handleBlur($event)" />
1899
+ <span class="zv-box" aria-hidden="true"></span>
1900
+ @if (label) { <span class="zv-text">{{ label }}</span> }
1901
+ </label>
1902
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
1903
+ `, styles: [".zv-check{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}input{position:absolute;opacity:0;width:1.1rem;height:1.1rem}.zv-box{width:1.1rem;height:1.1rem;border:2px solid var(--zv-border,#d1d5db);border-radius:.25rem;display:inline-block;transition:.15s}input:checked+.zv-box{background:var(--zv-primary,#2563eb);border-color:var(--zv-primary,#2563eb);background-image:url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3'><path d='M5 13l4 4L19 7'/></svg>\");background-size:contain}input:focus-visible+.zv-box{box-shadow:0 0 0 3px var(--zv-primary-ring,rgba(37,99,235,.3))}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"] }]
1904
+ }] });
1905
+
1906
+ class ZvRadio extends ZvBaseControl {
1907
+ controlType = 'radio';
1908
+ items = [];
1909
+ bindLabel = 'label';
1910
+ bindValue = 'value';
1911
+ get options() {
1912
+ return this.items.map((it) => it && typeof it === 'object' && this.bindLabel in it
1913
+ ? { label: it[this.bindLabel], value: it[this.bindValue] ?? it, disabled: it.disabled }
1914
+ : { label: String(it), value: it });
1915
+ }
1916
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvRadio, deps: null, target: i0.ɵɵFactoryTarget.Component });
1917
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvRadio, isStandalone: true, selector: "zv-radio", inputs: { items: "items", bindLabel: "bindLabel", bindValue: "bindValue" }, providers: controlProviders(ZvRadio), usesInheritance: true, ngImport: i0, template: `
1918
+ @if (label) { <span class="zv-group-label">{{ label }}</span> }
1919
+ <div class="zv-radio-group" role="radiogroup" [attr.aria-label]="ariaLabel || label">
1920
+ @for (opt of options; track opt.value) {
1921
+ <label class="zv-radio">
1922
+ <input type="radio" [name]="name || id" [checked]="opt.value === valueSig()"
1923
+ [disabled]="disabledSig() || opt.disabled" [tabIndex]="tabIndex"
1924
+ (change)="handleChange(opt.value)" (blur)="handleBlur($event)" />
1925
+ <span class="zv-dot" aria-hidden="true"></span>
1926
+ <span class="zv-text">{{ opt.label }}</span>
1927
+ </label>
1928
+ }
1929
+ </div>
1930
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
1931
+ `, isInline: true, styles: [".zv-radio-group{display:flex;flex-direction:column;gap:.4rem}.zv-radio{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}input{position:absolute;opacity:0}.zv-dot{width:1.1rem;height:1.1rem;border:2px solid var(--zv-border,#d1d5db);border-radius:50%;position:relative}input:checked+.zv-dot{border-color:var(--zv-primary,#2563eb)}input:checked+.zv-dot:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background:var(--zv-primary,#2563eb)}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1932
+ }
1933
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvRadio, decorators: [{
1934
+ type: Component,
1935
+ args: [{ selector: 'zv-radio', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: controlProviders(ZvRadio), template: `
1936
+ @if (label) { <span class="zv-group-label">{{ label }}</span> }
1937
+ <div class="zv-radio-group" role="radiogroup" [attr.aria-label]="ariaLabel || label">
1938
+ @for (opt of options; track opt.value) {
1939
+ <label class="zv-radio">
1940
+ <input type="radio" [name]="name || id" [checked]="opt.value === valueSig()"
1941
+ [disabled]="disabledSig() || opt.disabled" [tabIndex]="tabIndex"
1942
+ (change)="handleChange(opt.value)" (blur)="handleBlur($event)" />
1943
+ <span class="zv-dot" aria-hidden="true"></span>
1944
+ <span class="zv-text">{{ opt.label }}</span>
1945
+ </label>
1946
+ }
1947
+ </div>
1948
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
1949
+ `, styles: [".zv-radio-group{display:flex;flex-direction:column;gap:.4rem}.zv-radio{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}input{position:absolute;opacity:0}.zv-dot{width:1.1rem;height:1.1rem;border:2px solid var(--zv-border,#d1d5db);border-radius:50%;position:relative}input:checked+.zv-dot{border-color:var(--zv-primary,#2563eb)}input:checked+.zv-dot:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background:var(--zv-primary,#2563eb)}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"] }]
1950
+ }], propDecorators: { items: [{
1951
+ type: Input
1952
+ }], bindLabel: [{
1953
+ type: Input
1954
+ }], bindValue: [{
1955
+ type: Input
1956
+ }] } });
1957
+
1958
+ class ZvSwitch extends ZvBaseControl {
1959
+ controlType = 'switch';
1960
+ onToggle(ev) {
1961
+ this.handleChange(ev.target.checked);
1962
+ }
1963
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvSwitch, deps: null, target: i0.ɵɵFactoryTarget.Component });
1964
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvSwitch, isStandalone: true, selector: "zv-switch", providers: controlProviders(ZvSwitch), usesInheritance: true, ngImport: i0, template: `
1965
+ <label class="zv-switch">
1966
+ <input type="checkbox" role="switch" [id]="id" [name]="name"
1967
+ [checked]="!!valueSig()" [disabled]="disabledSig()"
1968
+ [attr.aria-checked]="!!valueSig()" [attr.aria-label]="ariaLabel || label"
1969
+ [tabIndex]="tabIndex" (change)="onToggle($event)" (blur)="handleBlur($event)" />
1970
+ <span class="zv-track" aria-hidden="true"><span class="zv-thumb"></span></span>
1971
+ @if (label) { <span class="zv-text">{{ label }}</span> }
1972
+ </label>
1973
+ `, isInline: true, styles: [".zv-switch{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}input{position:absolute;opacity:0}.zv-track{width:2.4rem;height:1.3rem;border-radius:1rem;background:var(--zv-border,#d1d5db);position:relative;transition:.2s}.zv-thumb{position:absolute;top:2px;left:2px;width:1.1rem;height:1.1rem;border-radius:50%;background:#fff;transition:.2s;box-shadow:0 1px 3px #0000004d}input:checked+.zv-track{background:var(--zv-primary,#2563eb)}input:checked+.zv-track .zv-thumb{transform:translate(1.1rem)}input:focus-visible+.zv-track{box-shadow:0 0 0 3px var(--zv-primary-ring,rgba(37,99,235,.3))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1974
+ }
1975
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvSwitch, decorators: [{
1976
+ type: Component,
1977
+ args: [{ selector: 'zv-switch', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: controlProviders(ZvSwitch), template: `
1978
+ <label class="zv-switch">
1979
+ <input type="checkbox" role="switch" [id]="id" [name]="name"
1980
+ [checked]="!!valueSig()" [disabled]="disabledSig()"
1981
+ [attr.aria-checked]="!!valueSig()" [attr.aria-label]="ariaLabel || label"
1982
+ [tabIndex]="tabIndex" (change)="onToggle($event)" (blur)="handleBlur($event)" />
1983
+ <span class="zv-track" aria-hidden="true"><span class="zv-thumb"></span></span>
1984
+ @if (label) { <span class="zv-text">{{ label }}</span> }
1985
+ </label>
1986
+ `, styles: [".zv-switch{display:inline-flex;align-items:center;gap:.5rem;cursor:pointer}input{position:absolute;opacity:0}.zv-track{width:2.4rem;height:1.3rem;border-radius:1rem;background:var(--zv-border,#d1d5db);position:relative;transition:.2s}.zv-thumb{position:absolute;top:2px;left:2px;width:1.1rem;height:1.1rem;border-radius:50%;background:#fff;transition:.2s;box-shadow:0 1px 3px #0000004d}input:checked+.zv-track{background:var(--zv-primary,#2563eb)}input:checked+.zv-track .zv-thumb{transform:translate(1.1rem)}input:focus-visible+.zv-track{box-shadow:0 0 0 3px var(--zv-primary-ring,rgba(37,99,235,.3))}\n"] }]
1987
+ }] });
1988
+
1989
+ class ZvPin extends ZvBaseControl {
1990
+ controlType = 'pin';
1991
+ length = 6;
1992
+ masked = false;
1993
+ get cells() { return Array.from({ length: this.length }); }
1994
+ digitAt(i) { return (this.valueSig() ?? '')[i] ?? ''; }
1995
+ onCell(i, ev) {
1996
+ const el = ev.target;
1997
+ const ch = el.value.replace(/\D/g, '').slice(-1);
1998
+ el.value = ch;
1999
+ const arr = (this.valueSig() ?? '').padEnd(this.length, ' ').split('');
2000
+ arr[i] = ch || ' ';
2001
+ this.handleInput(arr.join('').trimEnd());
2002
+ if (ch && i < this.length - 1)
2003
+ this.focusCell(el, 1);
2004
+ }
2005
+ onKey(i, ev) {
2006
+ if (ev.key === 'Backspace' && !ev.target.value && i > 0)
2007
+ this.focusCell(ev.target, -1);
2008
+ }
2009
+ focusCell(from, delta) {
2010
+ const sib = delta > 0 ? from.nextElementSibling : from.previousElementSibling;
2011
+ sib?.focus();
2012
+ }
2013
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvPin, deps: null, target: i0.ɵɵFactoryTarget.Component });
2014
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvPin, isStandalone: true, selector: "zv-pin", inputs: { length: ["length", "length", numberAttribute], masked: "masked" }, providers: controlProviders(ZvPin), usesInheritance: true, ngImport: i0, template: `
2015
+ @if (label) { <span class="zv-group-label">{{ label }}</span> }
2016
+ <div class="zv-pin" [attr.aria-label]="ariaLabel || label || 'One time code'">
2017
+ @for (cell of cells; track $index) {
2018
+ <input class="zv-pin-cell" inputmode="numeric" maxlength="1"
2019
+ [type]="masked ? 'password' : 'text'" [value]="digitAt($index)"
2020
+ [disabled]="disabledSig()" [attr.aria-label]="'Digit ' + ($index + 1)"
2021
+ (input)="onCell($index, $event)" (keydown)="onKey($index, $event)"
2022
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
2023
+ }
2024
+ </div>
2025
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
2026
+ `, isInline: true, styles: [".zv-pin{display:flex;gap:.5rem}.zv-pin-cell{width:2.75rem;height:3rem;text-align:center;font-size:1.25rem;border:1px solid var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem)}.zv-pin-cell:focus{outline:0;border-color:var(--zv-primary,#2563eb);box-shadow:0 0 0 3px var(--zv-primary-ring,rgba(37,99,235,.2))}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2027
+ }
2028
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvPin, decorators: [{
2029
+ type: Component,
2030
+ args: [{ selector: 'zv-pin', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: controlProviders(ZvPin), template: `
2031
+ @if (label) { <span class="zv-group-label">{{ label }}</span> }
2032
+ <div class="zv-pin" [attr.aria-label]="ariaLabel || label || 'One time code'">
2033
+ @for (cell of cells; track $index) {
2034
+ <input class="zv-pin-cell" inputmode="numeric" maxlength="1"
2035
+ [type]="masked ? 'password' : 'text'" [value]="digitAt($index)"
2036
+ [disabled]="disabledSig()" [attr.aria-label]="'Digit ' + ($index + 1)"
2037
+ (input)="onCell($index, $event)" (keydown)="onKey($index, $event)"
2038
+ (focus)="handleFocus($event)" (blur)="handleBlur($event)" />
2039
+ }
2040
+ </div>
2041
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
2042
+ `, styles: [".zv-pin{display:flex;gap:.5rem}.zv-pin-cell{width:2.75rem;height:3rem;text-align:center;font-size:1.25rem;border:1px solid var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem)}.zv-pin-cell:focus{outline:0;border-color:var(--zv-primary,#2563eb);box-shadow:0 0 0 3px var(--zv-primary-ring,rgba(37,99,235,.2))}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"] }]
2043
+ }], propDecorators: { length: [{
2044
+ type: Input,
2045
+ args: [{ transform: numberAttribute }]
2046
+ }], masked: [{
2047
+ type: Input
2048
+ }] } });
2049
+
2050
+ class ZvUpload extends ZvBaseControl {
2051
+ controlType = 'upload';
2052
+ multiple = false;
2053
+ accept = '';
2054
+ dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
2055
+ files = signal([], ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
2056
+ onPick(ev) { this.commit(ev.target.files); }
2057
+ onDrop(ev) {
2058
+ ev.preventDefault();
2059
+ this.dragging.set(false);
2060
+ this.commit(ev.dataTransfer?.files ?? null);
2061
+ }
2062
+ commit(list) {
2063
+ const arr = list ? Array.from(list) : [];
2064
+ this.files.set(arr);
2065
+ this.handleChange(arr);
2066
+ }
2067
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvUpload, deps: null, target: i0.ɵɵFactoryTarget.Component });
2068
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: ZvUpload, isStandalone: true, selector: "zv-upload", inputs: { multiple: ["multiple", "multiple", booleanAttribute], accept: "accept" }, providers: controlProviders(ZvUpload), usesInheritance: true, ngImport: i0, template: `
2069
+ @if (label) { <span class="zv-group-label">{{ label }}</span> }
2070
+ <div class="zv-drop" [class.zv-over]="dragging()" tabindex="0"
2071
+ [attr.aria-label]="ariaLabel || label || 'File upload'"
2072
+ (click)="picker.click()" (keydown.enter)="picker.click()"
2073
+ (dragover)="$event.preventDefault(); dragging.set(true)"
2074
+ (dragleave)="dragging.set(false)" (drop)="onDrop($event)">
2075
+ <input #picker type="file" hidden [multiple]="multiple" [accept]="accept"
2076
+ [disabled]="disabledSig()" (change)="onPick($event)" />
2077
+ <p>{{ placeholder || 'Drop files or click to browse' }}</p>
2078
+ </div>
2079
+ @for (f of files(); track f.name) { <div class="zv-file">{{ f.name }} ({{ (f.size/1024)|number:'1.0-0' }} KB)</div> }
2080
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
2081
+ `, isInline: true, styles: [".zv-drop{border:2px dashed var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem);padding:1.5rem;text-align:center;cursor:pointer;color:var(--zv-muted,#6b7280)}.zv-drop.zv-over,.zv-drop:focus-visible{border-color:var(--zv-primary,#2563eb);outline:0}.zv-file{font-size:.8rem;margin-top:.4rem}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"], dependencies: [{ kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2082
+ }
2083
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvUpload, decorators: [{
2084
+ type: Component,
2085
+ args: [{ selector: 'zv-upload', standalone: true, imports: [DecimalPipe], changeDetection: ChangeDetectionStrategy.OnPush, providers: controlProviders(ZvUpload), template: `
2086
+ @if (label) { <span class="zv-group-label">{{ label }}</span> }
2087
+ <div class="zv-drop" [class.zv-over]="dragging()" tabindex="0"
2088
+ [attr.aria-label]="ariaLabel || label || 'File upload'"
2089
+ (click)="picker.click()" (keydown.enter)="picker.click()"
2090
+ (dragover)="$event.preventDefault(); dragging.set(true)"
2091
+ (dragleave)="dragging.set(false)" (drop)="onDrop($event)">
2092
+ <input #picker type="file" hidden [multiple]="multiple" [accept]="accept"
2093
+ [disabled]="disabledSig()" (change)="onPick($event)" />
2094
+ <p>{{ placeholder || 'Drop files or click to browse' }}</p>
2095
+ </div>
2096
+ @for (f of files(); track f.name) { <div class="zv-file">{{ f.name }} ({{ (f.size/1024)|number:'1.0-0' }} KB)</div> }
2097
+ @if (status === 'error' && errorMessage) { <p class="zv-error" role="alert">{{ errorMessage }}</p> }
2098
+ `, styles: [".zv-drop{border:2px dashed var(--zv-border,#d1d5db);border-radius:var(--zv-radius,.5rem);padding:1.5rem;text-align:center;cursor:pointer;color:var(--zv-muted,#6b7280)}.zv-drop.zv-over,.zv-drop:focus-visible{border-color:var(--zv-primary,#2563eb);outline:0}.zv-file{font-size:.8rem;margin-top:.4rem}.zv-error{margin:.25rem 0 0;font-size:.75rem;color:var(--zv-warn,#dc2626)}\n"] }]
2099
+ }], propDecorators: { multiple: [{
2100
+ type: Input,
2101
+ args: [{ transform: booleanAttribute }]
2102
+ }], accept: [{
2103
+ type: Input
2104
+ }] } });
2105
+
2106
+ /**
2107
+ * Reactive + Template Forms adapter.
2108
+ *
2109
+ * A single `ControlValueAccessor` that bridges any Zellvora control to Angular's
2110
+ * `formControl` / `formControlName` / `ngModel`. It owns NO state and NO
2111
+ * validation — it simply forwards to the host `SignalControlAccessor`, whose
2112
+ * signals remain the source of truth. This is the entire "Reactive/Template
2113
+ * wraps Signals" story.
2114
+ */
2115
+ class ZvControlValueAccessor {
2116
+ host = inject(SignalControlAccessor);
2117
+ onChange = () => { };
2118
+ onTouched = () => { };
2119
+ constructor() {
2120
+ const ref = inject(DestroyRef);
2121
+ // host commit -> Angular onChange + touched
2122
+ const sub = this.host.committed.subscribe((v) => {
2123
+ this.onChange(v);
2124
+ this.onTouched();
2125
+ });
2126
+ ref.onDestroy(() => sub.unsubscribe());
2127
+ // host disabled state stays in sync if the host changes it locally.
2128
+ effect(() => {
2129
+ this.host.disabled();
2130
+ });
2131
+ }
2132
+ writeValue(value) {
2133
+ this.host.acceptExternalValue(value);
2134
+ }
2135
+ registerOnChange(fn) {
2136
+ this.onChange = fn;
2137
+ }
2138
+ registerOnTouched(fn) {
2139
+ this.onTouched = fn;
2140
+ }
2141
+ setDisabledState(isDisabled) {
2142
+ this.host.setDisabled(isDisabled);
2143
+ }
2144
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvControlValueAccessor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2145
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.17", type: ZvControlValueAccessor, isStandalone: true, selector: "zv-input[formControl],zv-input[formControlName],zv-input[ngModel],zv-email[formControl],zv-email[formControlName],zv-email[ngModel],zv-password[formControl],zv-password[formControlName],zv-password[ngModel],zv-number[formControl],zv-number[formControlName],zv-number[ngModel],zv-textarea[formControl],zv-textarea[formControlName],zv-textarea[ngModel],zv-select[formControl],zv-select[formControlName],zv-select[ngModel],zv-multiselect[formControl],zv-multiselect[formControlName],zv-multiselect[ngModel],zv-checkbox[formControl],zv-checkbox[formControlName],zv-checkbox[ngModel],zv-radio[formControl],zv-radio[formControlName],zv-radio[ngModel],zv-switch[formControl],zv-switch[formControlName],zv-switch[ngModel],zv-datepicker[formControl],zv-datepicker[formControlName],zv-datepicker[ngModel],zv-timepicker[formControl],zv-timepicker[formControlName],zv-timepicker[ngModel],zv-pin[formControl],zv-pin[formControlName],zv-pin[ngModel],zv-upload[formControl],zv-upload[formControlName],zv-upload[ngModel]", providers: [
2146
+ {
2147
+ provide: NG_VALUE_ACCESSOR,
2148
+ useExisting: forwardRef(() => ZvControlValueAccessor),
2149
+ multi: true,
2150
+ },
2151
+ ], ngImport: i0 });
2152
+ }
2153
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: ZvControlValueAccessor, decorators: [{
2154
+ type: Directive,
2155
+ args: [{
2156
+ selector: 'zv-input[formControl],zv-input[formControlName],zv-input[ngModel],' +
2157
+ 'zv-email[formControl],zv-email[formControlName],zv-email[ngModel],' +
2158
+ 'zv-password[formControl],zv-password[formControlName],zv-password[ngModel],' +
2159
+ 'zv-number[formControl],zv-number[formControlName],zv-number[ngModel],' +
2160
+ 'zv-textarea[formControl],zv-textarea[formControlName],zv-textarea[ngModel],' +
2161
+ 'zv-select[formControl],zv-select[formControlName],zv-select[ngModel],' +
2162
+ 'zv-multiselect[formControl],zv-multiselect[formControlName],zv-multiselect[ngModel],' +
2163
+ 'zv-checkbox[formControl],zv-checkbox[formControlName],zv-checkbox[ngModel],' +
2164
+ 'zv-radio[formControl],zv-radio[formControlName],zv-radio[ngModel],' +
2165
+ 'zv-switch[formControl],zv-switch[formControlName],zv-switch[ngModel],' +
2166
+ 'zv-datepicker[formControl],zv-datepicker[formControlName],zv-datepicker[ngModel],' +
2167
+ 'zv-timepicker[formControl],zv-timepicker[formControlName],zv-timepicker[ngModel],' +
2168
+ 'zv-pin[formControl],zv-pin[formControlName],zv-pin[ngModel],' +
2169
+ 'zv-upload[formControl],zv-upload[formControlName],zv-upload[ngModel]',
2170
+ providers: [
2171
+ {
2172
+ provide: NG_VALUE_ACCESSOR,
2173
+ useExisting: forwardRef(() => ZvControlValueAccessor),
2174
+ multi: true,
2175
+ },
2176
+ ],
2177
+ }]
2178
+ }], ctorParameters: () => [] });
2179
+
2180
+ function toNgValidator(v) {
2181
+ return (c) => v(c.value, c.parent?.value ?? {});
2182
+ }
2183
+ function toNgAsyncValidator(v) {
2184
+ return async (c) => (await v(c.value, c.parent?.value ?? {}));
2185
+ }
2186
+ /** Convenience: map many ZvValidators to one Angular ValidatorFn list. */
2187
+ const ngValidators = (...vs) => vs.map(toNgValidator);
2188
+
2189
+ /*
2190
+ * Public API — @zellvora/ng-input
2191
+ *
2192
+ * Signal-first enterprise form controls for Angular 22.
2193
+ * The Signal Form Engine is the single source of truth; Reactive and Template
2194
+ * forms are thin adapters over it.
2195
+ */
2196
+ // ---- Signal Form Engine (source of truth) ---------------------------------
2197
+
2198
+ /**
2199
+ * Generated bundle index. Do not edit.
2200
+ */
2201
+
2202
+ export { SignalControlAccessor, ZV_DEFAULT_CONFIG, ZV_DEFAULT_MESSAGES, ZV_INPUT_CONFIG, ZV_PATTERNS, ZvCheckbox, ZvControlValueAccessor, ZvDatepicker, ZvEmail, ZvFieldShell, ZvInput, ZvMultiselect, ZvNumber, ZvPassword, ZvPin, ZvRadio, ZvSelect, ZvSwitch, ZvTextarea, ZvTimepicker, ZvUpload, ZvValidators$1 as ZvValidators, createForm, formatError, ngValidators, provideSignalControl, provideZvInput, toNgAsyncValidator, toNgValidator };
2203
+ //# sourceMappingURL=zellvora-ng-input.mjs.map