@maro-tech/form 0.0.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,3191 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, Injectable, inject, InjectionToken, EventEmitter, Output, Input, Component, input, output, computed, ChangeDetectorRef, forwardRef, ChangeDetectionStrategy, ViewContainerRef, ViewChild, DestroyRef, ElementRef, booleanAttribute } from '@angular/core';
3
+ import { HttpClient } from '@angular/common/http';
4
+ import * as i1 from '@angular/common';
5
+ import { CommonModule } from '@angular/common';
6
+ import * as i2 from '@angular/forms';
7
+ import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
8
+ import { forkJoin, Observable, from, Subject, debounceTime, distinctUntilChanged, switchMap, of } from 'rxjs';
9
+ import { UiBarcodeScannerInputComponent as UiBarcodeScannerInputComponent$1 } from '@forms/inputs/barcode/ui-barcode-scanner-input.component';
10
+ import { UiSelectComponent as UiSelectComponent$1 } from '@forms/inputs/select/ui-select.component';
11
+ import { UiFormInputHostComponent as UiFormInputHostComponent$1 } from '@forms/inputs/form-input-host.component';
12
+ import { UiFileInputComponent as UiFileInputComponent$1 } from '@forms/inputs/file/ui-file-input.component';
13
+ import { UiPhotoInputComponent as UiPhotoInputComponent$1 } from '@forms/inputs/photo/ui-photo-input.component';
14
+ import { UiCheckboxInputComponent as UiCheckboxInputComponent$1 } from '@forms/inputs/checkbox/ui-checkbox-input.component';
15
+ import { UiColorInputComponent as UiColorInputComponent$1 } from '@forms/inputs/color/ui-color-input.component';
16
+ import { UiSlugInputComponent as UiSlugInputComponent$1 } from '@forms/inputs/slug/ui-slug-input.component';
17
+ import { UiFormContext as UiFormContext$1 } from '@forms/context/form-context.service';
18
+ import { MaroFormsApiService as MaroFormsApiService$1 } from '@forms/forms-api.service';
19
+ import { UiButtonComponent } from '@maro-tech/ui';
20
+ import { Router } from '@angular/router';
21
+ import { UiDynamicFormComponent as UiDynamicFormComponent$1 } from '@forms/dynamic/ui-dynamic-form.component';
22
+ import { BrowserMultiFormatOneDReader } from '@zxing/browser';
23
+ import { DecodeHintType, BarcodeFormat } from '@zxing/library';
24
+ import * as i1$1 from '@angular/cdk/drag-drop';
25
+ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
26
+ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
27
+ import * as i1$2 from 'lucide-angular';
28
+ import { X, ChevronDown, Check, LucideAngularModule } from 'lucide-angular';
29
+
30
+ class UiFormContext {
31
+ value = signal({}, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
32
+ errors = signal({}, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
33
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
34
+ disabled = signal(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
35
+ setValue(value) {
36
+ this.value.set({ ...value });
37
+ }
38
+ patchValue(value) {
39
+ this.value.update((current) => ({ ...current, ...value }));
40
+ }
41
+ setErrors(errors) {
42
+ this.errors.set({ ...errors });
43
+ }
44
+ reset(value = {}) {
45
+ this.value.set({ ...value });
46
+ this.errors.set({});
47
+ this.loading.set(false);
48
+ }
49
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
50
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormContext });
51
+ }
52
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormContext, decorators: [{
53
+ type: Injectable
54
+ }] });
55
+
56
+ class MaroFormsApiService {
57
+ http = inject(HttpClient);
58
+ getSuggestByName(name, query = '') {
59
+ const suffix = query ? `?search=${encodeURIComponent(query)}` : '';
60
+ return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
61
+ }
62
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: MaroFormsApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
63
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: MaroFormsApiService, providedIn: 'root' });
64
+ }
65
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: MaroFormsApiService, decorators: [{
66
+ type: Injectable,
67
+ args: [{ providedIn: 'root' }]
68
+ }] });
69
+
70
+ class UiFormLookupService {
71
+ http = inject(HttpClient);
72
+ lookup(field, value) {
73
+ const api = String(field.lookupApi || '').trim();
74
+ const endpoint = api
75
+ .replace(/:value/g, encodeURIComponent(value))
76
+ .replace(new RegExp(`:${field.id}\\b`, 'g'), encodeURIComponent(value));
77
+ if (/^https?:\/\//i.test(endpoint)) {
78
+ return this.http.get(endpoint);
79
+ }
80
+ const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
81
+ return this.http.get(`/api${path}`);
82
+ }
83
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormLookupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
84
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormLookupService });
85
+ }
86
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormLookupService, decorators: [{
87
+ type: Injectable
88
+ }] });
89
+
90
+ const UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS = new InjectionToken('UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS');
91
+
92
+ const isDynamicFormContainerField = (field) => field.type === 'container';
93
+ const isDynamicFormTemplateField = (field) => field.type === 'template';
94
+ const flattenDynamicFormFields = (fields) => {
95
+ const result = [];
96
+ for (const field of fields || []) {
97
+ if (!field) {
98
+ continue;
99
+ }
100
+ result.push(field);
101
+ if (isDynamicFormContainerField(field) && Array.isArray(field.fields) && field.fields.length) {
102
+ result.push(...flattenDynamicFormFields(field.fields));
103
+ }
104
+ }
105
+ return result;
106
+ };
107
+
108
+ class UiDynamicFormComponent {
109
+ formsApi = inject(MaroFormsApiService$1);
110
+ lookupService = inject(UiFormLookupService);
111
+ formContext = inject(UiFormContext$1);
112
+ optionsSourceResolvers = inject(UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS, { optional: true }) ?? [];
113
+ fields = [];
114
+ value = {};
115
+ errors = {};
116
+ submitLabel = 'Submit';
117
+ showSubmit = true;
118
+ disabled = false;
119
+ uploadPhoto = null;
120
+ valueChange = new EventEmitter();
121
+ lookupCompleted = new EventEmitter();
122
+ submitted = new EventEmitter();
123
+ filesChange = new EventEmitter();
124
+ uploadingChange = new EventEmitter();
125
+ fieldActionTriggered = new EventEmitter();
126
+ modelSignal = signal({}, ...(ngDevMode ? [{ debugName: "modelSignal" }] : /* istanbul ignore next */ []));
127
+ dictionaryOptionsSignal = signal({}, ...(ngDevMode ? [{ debugName: "dictionaryOptionsSignal" }] : /* istanbul ignore next */ []));
128
+ lookupLoadingSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupLoadingSignal" }] : /* istanbul ignore next */ []));
129
+ lookupErrorsSignal = signal({}, ...(ngDevMode ? [{ debugName: "lookupErrorsSignal" }] : /* istanbul ignore next */ []));
130
+ selectSearchChanges = new Map();
131
+ selectSearchSubscriptions = new Map();
132
+ optionsSourceRequestVersions = new Map();
133
+ ngOnInit() {
134
+ this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
135
+ }
136
+ ngOnChanges(changes) {
137
+ if ('fields' in changes || 'value' in changes) {
138
+ const next = { ...this.value };
139
+ for (const field of this.getLeafFields()) {
140
+ const defaultValue = field.type === 'checkbox' ? false : '';
141
+ const nextValue = this.value[field.id] !== undefined
142
+ ? this.value[field.id]
143
+ : field.default !== undefined
144
+ ? field.default
145
+ : defaultValue;
146
+ next[field.id] = nextValue;
147
+ }
148
+ this.modelSignal.set(next);
149
+ this.formContext.setValue(next);
150
+ this.formContext.setErrors(this.errors);
151
+ this.formContext.disabled.set(this.disabled);
152
+ }
153
+ if ('fields' in changes || 'value' in changes) {
154
+ this.ensureOptionsSourceOptionsLoaded(this.modelSignal());
155
+ }
156
+ }
157
+ ngOnDestroy() {
158
+ this.cleanupSelectSearchStreams();
159
+ }
160
+ updateField(fieldId, nextValue) {
161
+ this.modelSignal.update((current) => {
162
+ const updated = { ...current, [fieldId]: nextValue };
163
+ this.formContext.setValue(updated);
164
+ this.valueChange.emit(updated);
165
+ this.ensureOptionsSourceOptionsLoaded(updated);
166
+ return updated;
167
+ });
168
+ }
169
+ onBarcodeValueChange(field, value) {
170
+ this.updateField(field.id, value);
171
+ this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
172
+ }
173
+ onBarcodeScanned(field, value) {
174
+ this.updateField(field.id, value);
175
+ this.runFieldLookup(field, value);
176
+ }
177
+ onSelectValueChange(field, value) {
178
+ this.updateField(field.id, value);
179
+ const nextValue = Array.isArray(value) ? '' : String(value || '').trim();
180
+ if (field.lookupApi) {
181
+ this.runFieldLookup(field, nextValue);
182
+ return;
183
+ }
184
+ this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
185
+ }
186
+ runBarcodeLookup(field) {
187
+ this.runFieldLookup(field);
188
+ }
189
+ runFieldLookup(field, overrideValue) {
190
+ const api = String(field.lookupApi || '').trim();
191
+ if (!api)
192
+ return;
193
+ const value = String(overrideValue ?? this.getStringValue(field.id)).trim();
194
+ if (!value) {
195
+ this.lookupErrorsSignal.update((current) => ({
196
+ ...current,
197
+ [field.id]: field.type === 'select' ? 'Вкажіть значення для пошуку' : 'Вкажіть штрихкод для пошуку',
198
+ }));
199
+ return;
200
+ }
201
+ this.lookupLoadingSignal.update((current) => ({ ...current, [field.id]: true }));
202
+ this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: '' }));
203
+ this.lookupService.lookup(field, value).subscribe({
204
+ next: (response) => {
205
+ this.applyLookupResponse(field, response || {});
206
+ this.lookupCompleted.emit({ fieldId: field.id, response: response || {} });
207
+ this.lookupLoadingSignal.update((current) => ({ ...current, [field.id]: false }));
208
+ },
209
+ error: (error) => {
210
+ const message = error?.error?.message || 'Не вдалося знайти запис';
211
+ this.lookupErrorsSignal.update((current) => ({ ...current, [field.id]: message }));
212
+ this.lookupLoadingSignal.update((current) => ({ ...current, [field.id]: false }));
213
+ const errorField = String(field.lookupErrorField || '').trim();
214
+ if (errorField) {
215
+ this.updateField(errorField, message);
216
+ }
217
+ },
218
+ });
219
+ }
220
+ onFieldActionTriggered(fieldId, action) {
221
+ this.fieldActionTriggered.emit({ fieldId, action });
222
+ }
223
+ resolveFieldActions(field) {
224
+ const model = this.modelSignal();
225
+ return (field.actions || []).filter((action) => {
226
+ const condition = String(action.condition || '').trim();
227
+ if (!condition) {
228
+ return true;
229
+ }
230
+ try {
231
+ const evaluator = new Function('model', `with (model) { return !!(${condition}); }`);
232
+ return evaluator(model);
233
+ }
234
+ catch {
235
+ return true;
236
+ }
237
+ });
238
+ }
239
+ getActions(field) {
240
+ return this.resolveFieldActions(field);
241
+ }
242
+ isFieldDisabled(field) {
243
+ if (this.disabled)
244
+ return true;
245
+ if (typeof field.disabled === 'boolean')
246
+ return field.disabled;
247
+ const raw = String(field.disabled || '').trim();
248
+ if (!raw)
249
+ return false;
250
+ const condition = raw.startsWith('${') && raw.endsWith('}') ? raw.slice(2, -1).trim() : raw;
251
+ try {
252
+ return Boolean(Function('model', `with (model) { return Boolean(${condition}); }`)(this.modelSignal()));
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ }
258
+ submitForm() {
259
+ this.submitted.emit(this.modelSignal());
260
+ }
261
+ isTextArea(field) {
262
+ return (field.type ?? 'text') === 'textarea';
263
+ }
264
+ isInputField(field) {
265
+ const type = field.type ?? 'text';
266
+ return type === 'text' || type === 'number' || type === 'date' || type === 'datetime' || type === 'email' || type === 'password' || type === 'textarea';
267
+ }
268
+ isBarcodeField(field) {
269
+ return field.type === 'barcode';
270
+ }
271
+ getStringValue(fieldId) {
272
+ return String(this.modelSignal()[fieldId] || '');
273
+ }
274
+ getLookupError(fieldId) {
275
+ return this.lookupErrorsSignal()[fieldId] || '';
276
+ }
277
+ isLookupLoading(fieldId) {
278
+ return !!this.lookupLoadingSignal()[fieldId];
279
+ }
280
+ resolveClassName(value) {
281
+ return String(value || '').trim();
282
+ }
283
+ isContainerField(field) {
284
+ return isDynamicFormContainerField(field);
285
+ }
286
+ isTemplateField(field) {
287
+ return isDynamicFormTemplateField(field);
288
+ }
289
+ isFieldVisible(field) {
290
+ if (typeof field.hidden === 'boolean') {
291
+ return !field.hidden;
292
+ }
293
+ const hidden = String(field.hidden || '').trim();
294
+ if (!hidden) {
295
+ return true;
296
+ }
297
+ const expression = hidden.startsWith('${') && hidden.endsWith('}')
298
+ ? hidden.slice(2, -1)
299
+ : hidden;
300
+ try {
301
+ const model = this.modelSignal();
302
+ const evaluator = new Function('model', `with (model) { return !!(${expression}); }`);
303
+ return !evaluator(model);
304
+ }
305
+ catch {
306
+ return false;
307
+ }
308
+ }
309
+ resolveInputType(field) {
310
+ const type = field.type ?? 'text';
311
+ if (type === 'number' || type === 'date' || type === 'email' || type === 'password') {
312
+ return type;
313
+ }
314
+ if (type === 'datetime')
315
+ return 'datetime-local';
316
+ return 'text';
317
+ }
318
+ resolveSelectOptions(field) {
319
+ if (field.options?.length) {
320
+ return field.options;
321
+ }
322
+ const sourceKey = String(field.optionsSource || '').trim();
323
+ if (!sourceKey) {
324
+ return [];
325
+ }
326
+ return this.dictionaryOptionsSignal()[sourceKey] ?? [];
327
+ }
328
+ renderTemplate(field) {
329
+ const template = this.isTemplateField(field) ? field.template : '';
330
+ if (!template) {
331
+ return '';
332
+ }
333
+ const model = this.modelSignal();
334
+ const context = this.isTemplateField(field) ? { ...(field.templateContext || {}), ...model } : model;
335
+ return String(template).replace(/\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g, (_match, path) => {
336
+ const value = this.resolveTemplateValue(context, path);
337
+ return value == null ? '' : String(value);
338
+ });
339
+ }
340
+ getContainerTag(field) {
341
+ if (!this.isContainerField(field) && !this.isTemplateField(field)) {
342
+ return 'div';
343
+ }
344
+ return field.tag || 'section';
345
+ }
346
+ getContainerClass(field) {
347
+ const className = String(field.className || '').trim();
348
+ const tag = this.getContainerTag(field);
349
+ return [
350
+ this.resolveColSpanClass(field),
351
+ tag === 'aside'
352
+ ? 'space-y-4 rounded-3xl border border-slate-200 bg-white p-4 shadow-sm'
353
+ : 'rounded-3xl border border-slate-200 bg-white p-6 shadow-sm',
354
+ className,
355
+ ].filter(Boolean).join(' ');
356
+ }
357
+ getContainerContentClass(field) {
358
+ const className = String(field.contentClassName || '').trim();
359
+ const layout = this.isContainerField(field) ? field.layout : undefined;
360
+ return [
361
+ layout === 'stack' ? 'space-y-4' : 'grid grid-cols-1 gap-4 md:grid-cols-12',
362
+ className,
363
+ ].filter(Boolean).join(' ');
364
+ }
365
+ getFieldClass(field) {
366
+ const span = Math.min(12, Math.max(1, Number(field.col ?? 12)));
367
+ return {
368
+ 'md:col-span-1': span === 1,
369
+ 'md:col-span-2': span === 2,
370
+ 'md:col-span-3': span === 3,
371
+ 'md:col-span-4': span === 4,
372
+ 'md:col-span-5': span === 5,
373
+ 'md:col-span-6': span === 6,
374
+ 'md:col-span-7': span === 7,
375
+ 'md:col-span-8': span === 8,
376
+ 'md:col-span-9': span === 9,
377
+ 'md:col-span-10': span === 10,
378
+ 'md:col-span-11': span === 11,
379
+ 'md:col-span-12': span === 12,
380
+ };
381
+ }
382
+ resolveColSpanClass(field) {
383
+ const span = Math.min(12, Math.max(1, Number(field.col ?? 12)));
384
+ const classes = {
385
+ 1: 'md:col-span-1',
386
+ 2: 'md:col-span-2',
387
+ 3: 'md:col-span-3',
388
+ 4: 'md:col-span-4',
389
+ 5: 'md:col-span-5',
390
+ 6: 'md:col-span-6',
391
+ 7: 'md:col-span-7',
392
+ 8: 'md:col-span-8',
393
+ 9: 'md:col-span-9',
394
+ 10: 'md:col-span-10',
395
+ 11: 'md:col-span-11',
396
+ 12: 'md:col-span-12',
397
+ };
398
+ return classes[span] || classes[12];
399
+ }
400
+ onSelectSearchChange(field, query) {
401
+ const key = String(field.optionsSource || '').trim();
402
+ if (!key)
403
+ return;
404
+ this.getSelectSearchStream(field, key).next(String(query ?? ''));
405
+ }
406
+ ensureOptionsSourceOptionsLoaded(model) {
407
+ if (!this.optionsSourceResolvers.length) {
408
+ return;
409
+ }
410
+ const sourceKeys = [...new Set(this.getLeafFields()
411
+ .filter((field) => (field.type ?? 'text') === 'select' && !!field.optionsSource)
412
+ .map((field) => String(field.optionsSource || '').trim())
413
+ .filter(Boolean))];
414
+ if (!sourceKeys.length) {
415
+ return;
416
+ }
417
+ for (const sourceKey of sourceKeys) {
418
+ const resolver = this.optionsSourceResolvers.find((item) => item.source === sourceKey);
419
+ if (!resolver) {
420
+ continue;
421
+ }
422
+ const sourceFields = this.getLeafFields().filter((field) => String(field.optionsSource || '').trim() === sourceKey);
423
+ if (!sourceFields.length) {
424
+ continue;
425
+ }
426
+ const version = (this.optionsSourceRequestVersions.get(sourceKey) || 0) + 1;
427
+ this.optionsSourceRequestVersions.set(sourceKey, version);
428
+ forkJoin(sourceFields.map((field) => {
429
+ const resolved = resolver.resolve({ source: sourceKey, field, model });
430
+ return resolved instanceof Observable ? resolved : from(Promise.resolve(resolved));
431
+ })).subscribe({
432
+ next: (results) => {
433
+ if ((this.optionsSourceRequestVersions.get(sourceKey) || 0) !== version) {
434
+ return;
435
+ }
436
+ const options = results
437
+ .flat()
438
+ .map((item) => ({
439
+ label: String(item?.label ?? ''),
440
+ value: item?.value == null ? null : String(item.value),
441
+ disabled: item?.disabled,
442
+ imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
443
+ }))
444
+ .filter((item) => !!item.label || item.value !== null);
445
+ this.dictionaryOptionsSignal.update((current) => ({
446
+ ...current,
447
+ [sourceKey]: options,
448
+ }));
449
+ },
450
+ });
451
+ }
452
+ }
453
+ getSelectSearchStream(field, key) {
454
+ const normalizedKey = String(key || '').trim();
455
+ const existing = this.selectSearchChanges.get(normalizedKey);
456
+ if (existing) {
457
+ return existing;
458
+ }
459
+ const search$ = new Subject();
460
+ this.selectSearchChanges.set(normalizedKey, search$);
461
+ const subscription = search$
462
+ .pipe(debounceTime(300), distinctUntilChanged(), switchMap((query) => {
463
+ const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
464
+ if (resolver) {
465
+ return resolver.resolve({ source: normalizedKey, field, model: this.modelSignal(), query });
466
+ }
467
+ return this.formsApi.getSuggestByName(normalizedKey, query);
468
+ }))
469
+ .subscribe({
470
+ next: (items) => {
471
+ const resolver = this.optionsSourceResolvers.find((item) => item.source === normalizedKey);
472
+ this.dictionaryOptionsSignal.update((current) => ({
473
+ ...current,
474
+ [normalizedKey]: resolver
475
+ ? (items ?? []).map((item) => ({
476
+ label: String(item?.label ?? ''),
477
+ value: item?.value == null ? null : String(item.value),
478
+ disabled: item?.disabled,
479
+ imageUrl: item?.imageUrl ? String(item.imageUrl) : undefined,
480
+ }))
481
+ : (items ?? []).map((item) => ({
482
+ label: String(item?.label ?? ''),
483
+ value: item?.['key'] != null ? String(item['key']) : null,
484
+ })),
485
+ }));
486
+ },
487
+ });
488
+ this.selectSearchSubscriptions.set(normalizedKey, subscription);
489
+ return search$;
490
+ }
491
+ cleanupSelectSearchStreams() {
492
+ for (const subscription of this.selectSearchSubscriptions.values()) {
493
+ subscription.unsubscribe();
494
+ }
495
+ this.selectSearchSubscriptions.clear();
496
+ this.selectSearchChanges.clear();
497
+ }
498
+ applyLookupResponse(field, response) {
499
+ const lookupMap = field.lookupMap || {};
500
+ const mapped = Object.entries(lookupMap).reduce((acc, [targetField, sourcePath]) => {
501
+ acc[targetField] = this.resolveTemplateValue(response, sourcePath);
502
+ return acc;
503
+ }, {});
504
+ this.modelSignal.update((current) => {
505
+ const updated = { ...current, ...mapped };
506
+ this.formContext.setValue(updated);
507
+ this.valueChange.emit(updated);
508
+ this.ensureOptionsSourceOptionsLoaded(updated);
509
+ return updated;
510
+ });
511
+ }
512
+ resolveTemplateValue(model, pathRaw) {
513
+ const path = String(pathRaw || '').trim();
514
+ if (!path) {
515
+ return undefined;
516
+ }
517
+ return path.split('.').reduce((current, part) => {
518
+ if (current == null || typeof current !== 'object') {
519
+ return undefined;
520
+ }
521
+ return current[part];
522
+ }, model);
523
+ }
524
+ getFlatFields() {
525
+ return flattenDynamicFormFields(this.fields);
526
+ }
527
+ getLeafFields() {
528
+ return this.getFlatFields().filter((field) => field.type !== 'container' && field.type !== 'template');
529
+ }
530
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
531
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDynamicFormComponent, isStandalone: true, selector: "forms-dynamic-form", inputs: { fields: "fields", value: "value", errors: "errors", submitLabel: "submitLabel", showSubmit: "showSubmit", disabled: "disabled", uploadPhoto: "uploadPhoto" }, outputs: { valueChange: "valueChange", lookupCompleted: "lookupCompleted", submitted: "submitted", filesChange: "filesChange", uploadingChange: "uploadingChange", fieldActionTriggered: "fieldActionTriggered" }, providers: [UiFormContext$1, UiFormLookupService], usesOnChanges: true, ngImport: i0, template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n @if (isBarcodeField(field)) {\n <forms-barcode-scanner-input\n [label]=\"field.label || '\u0428\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [placeholder]=\"field.placeholder || '\u0412\u0456\u0434\u0441\u043A\u0430\u043D\u0443\u0439\u0442\u0435 \u0430\u0431\u043E \u0432\u0432\u0435\u0434\u0456\u0442\u044C \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [hint]=\"field.hint || '\u0421\u043A\u0430\u043D\u0435\u0440 \u043C\u043E\u0436\u0435 \u043F\u0440\u043E\u0441\u0442\u043E \u0432\u0432\u0435\u0441\u0442\u0438 \u043A\u043E\u0434 \u0456 \u043D\u0430\u0442\u0438\u0441\u043D\u0443\u0442\u0438 Enter.'\"\n [disabled]=\"isFieldDisabled(field) || isLookupLoading(field.id)\"\n [autoOpen]=\"field.autoOpen !== false\"\n [value]=\"getStringValue(field.id)\"\n [searchButtonLabel]=\"field.searchButtonLabel || '\u0417\u043D\u0430\u0439\u0442\u0438'\"\n [openCameraLabel]=\"field.openCameraLabel || '\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [closeCameraLabel]=\"field.closeCameraLabel || '\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [cameraTitle]=\"field.cameraTitle || '\u041A\u0430\u043C\u0435\u0440\u0430'\"\n [cameraStartingLabel]=\"field.cameraStartingLabel || '\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u0454\u043C\u043E \u043A\u0430\u043C\u0435\u0440\u0443\u2026'\"\n [cameraHint]=\"field.cameraHint || '\u041D\u0430\u0432\u0435\u0434\u0456\u0442\u044C \u043A\u0430\u043C\u0435\u0440\u0443 \u043D\u0430 \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434. \u041F\u0456\u0441\u043B\u044F \u0437\u0447\u0438\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043A\u043E\u0434 \u043F\u0456\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E.'\"\n (valueChange)=\"onBarcodeValueChange(field, $event)\"\n (searchRequested)=\"runBarcodeLookup(field)\"\n (scanned)=\"onBarcodeScanned(field, $event)\"\n ></forms-barcode-scanner-input>\n @if (getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (isInputField(field)) {\n <forms-form-input-host\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled\"\n [error]=\"errors[field.id] ?? ''\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"updateField(field.id, $any($event))\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n ></forms-form-input-host>\n }\n\n @if (field.type === 'select') {\n <forms-select\n [name]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [data]=\"field.data\"\n [required]=\"!!field.required\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [options]=\"resolveSelectOptions(field)\"\n [searchable]=\"(field.searchable ?? !!field.data) || !!field.optionsSource\"\n [remoteSearch]=\"!!field.optionsSource\"\n (searchChange)=\"field.optionsSource ? onSelectSearchChange(field, $event) : null\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"onSelectValueChange(field, $event)\">\n </forms-select>\n @if (field.lookupApi && getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (field.type === 'checkbox') {\n <forms-checkbox-input\n [label]=\"field.label\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"!!modelSignal()[field.id]\"\n (valueChange)=\"updateField(field.id, $event)\"\n ></forms-checkbox-input>\n }\n\n @if (field.type === 'color') {\n <forms-color-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-color-input>\n }\n\n @if (field.type === 'slug') {\n <forms-slug-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [readonly]=\"!!field.readonly\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-slug-input>\n }\n\n @if (field.type === 'photo') {\n <forms-photo-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept ?? 'image/*'\"\n [multiple]=\"!!field.multiple\"\n [limit]=\"field.limit ?? 0\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n [uploadPhoto]=\"uploadPhoto\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></forms-photo-input>\n }\n @if (field.type === 'file') {\n <forms-file-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n ></forms-file-input>\n }\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiFormInputHostComponent$1, selector: "forms-form-input-host", inputs: ["field", "value", "error", "disabled", "actions", "options"], outputs: ["valueChange", "actionTriggered"] }, { kind: "component", type: UiFileInputComponent$1, selector: "forms-file-input", inputs: ["label", "hint", "accept", "multiple", "disabled", "value"], outputs: ["valueChange", "filesChange"] }, { kind: "component", type: UiPhotoInputComponent$1, selector: "forms-photo-input", inputs: ["label", "hint", "accept", "multiple", "limit", "disabled", "value", "uploadPhoto"], outputs: ["valueChange", "filesChange", "uploadingChange"] }, { kind: "component", type: UiCheckboxInputComponent$1, selector: "forms-checkbox-input", inputs: ["label", "disabled", "value"], outputs: ["valueChange"] }, { kind: "component", type: UiColorInputComponent$1, selector: "forms-color-input", inputs: ["label", "hint", "name", "id", "required", "fullWidth", "error"] }, { kind: "component", type: UiSlugInputComponent$1, selector: "forms-slug-input", inputs: ["label", "hint", "placeholder", "name", "id", "required", "fullWidth", "autocomplete", "readonly", "error"], outputs: ["invalidInput"] }, { kind: "component", type: UiBarcodeScannerInputComponent$1, selector: "forms-barcode-scanner-input", inputs: ["value", "label", "placeholder", "hint", "disabled", "autoOpen", "searchButtonLabel", "openCameraLabel", "closeCameraLabel", "cameraTitle", "cameraStartingLabel", "cameraHint"], outputs: ["valueChange", "scanned", "searchRequested", "cameraStateChange"] }, { kind: "component", type: UiSelectComponent$1, selector: "forms-select", inputs: ["label", "placeholder", "name", "data", "required", "fullWidth", "disabled", "multiple", "includeBlankOption", "blankOptionLabel", "emptyStateLabel", "searchable", "remoteSearch", "searchPlaceholder", "options", "error", "hint", "triggerClass", "panelClass"], outputs: ["valueChange", "searchChange"] }] });
532
+ }
533
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDynamicFormComponent, decorators: [{
534
+ type: Component,
535
+ args: [{ selector: 'forms-dynamic-form', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent, UiFormInputHostComponent$1, UiFileInputComponent$1, UiPhotoInputComponent$1, UiCheckboxInputComponent$1, UiColorInputComponent$1, UiSlugInputComponent$1, UiBarcodeScannerInputComponent$1, UiSelectComponent$1], providers: [UiFormContext$1, UiFormLookupService], template: "<form class=\"grid grid-cols-1 gap-4 md:grid-cols-12\" (ngSubmit)=\"submitForm()\" #dynamicForm=\"ngForm\">\n <ng-template #renderField let-field>\n @if (isFieldVisible(field)) {\n @if (isContainerField(field)) {\n @if (getContainerTag(field) === 'aside') {\n <aside [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <p class=\"text-xs font-semibold uppercase tracking-[0.22em] text-slate-500\">\n {{ field.label || field.title }}\n </p>\n }\n @if (field.hint) {\n <h3 class=\"mt-1 text-lg font-semibold text-slate-900\">{{ field.hint }}</h3>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </aside>\n } @else if (getContainerTag(field) === 'fieldset') {\n <fieldset [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <legend class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</legend>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </fieldset>\n } @else {\n <section [ngClass]=\"getContainerClass(field)\">\n @if (field.label || field.title || getActions(field).length || field.hint) {\n <header class=\"flex flex-col gap-3 border-b border-slate-100 pb-3 md:flex-row md:items-center md:justify-between\">\n <div class=\"min-w-0\">\n @if (field.label || field.title) {\n <h3 class=\"text-lg font-semibold text-slate-900\">{{ field.label || field.title }}</h3>\n }\n @if (field.hint) {\n <p class=\"mt-1 text-sm text-slate-500\">{{ field.hint }}</p>\n }\n </div>\n\n @if (getActions(field).length) {\n <div class=\"flex flex-wrap gap-2 md:justify-end\">\n @for (action of getActions(field); track action.id; let i = $index) {\n <ui-button\n [variant]=\"action.variant || (i === 0 ? 'primary' : 'ghost')\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n (click)=\"onFieldActionTriggered(field.id, action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </header>\n }\n\n <div [ngClass]=\"getContainerContentClass(field)\">\n @for (child of field.fields; track child.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: child }\"></ng-container>\n }\n </div>\n </section>\n }\n } @else if (isTemplateField(field)) {\n <div [ngClass]=\"resolveClassName(field.className)\" [innerHTML]=\"renderTemplate(field)\"></div>\n } @else {\n <div [ngClass]=\"getFieldClass(field)\">\n @if (isBarcodeField(field)) {\n <forms-barcode-scanner-input\n [label]=\"field.label || '\u0428\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [placeholder]=\"field.placeholder || '\u0412\u0456\u0434\u0441\u043A\u0430\u043D\u0443\u0439\u0442\u0435 \u0430\u0431\u043E \u0432\u0432\u0435\u0434\u0456\u0442\u044C \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434'\"\n [hint]=\"field.hint || '\u0421\u043A\u0430\u043D\u0435\u0440 \u043C\u043E\u0436\u0435 \u043F\u0440\u043E\u0441\u0442\u043E \u0432\u0432\u0435\u0441\u0442\u0438 \u043A\u043E\u0434 \u0456 \u043D\u0430\u0442\u0438\u0441\u043D\u0443\u0442\u0438 Enter.'\"\n [disabled]=\"isFieldDisabled(field) || isLookupLoading(field.id)\"\n [autoOpen]=\"field.autoOpen !== false\"\n [value]=\"getStringValue(field.id)\"\n [searchButtonLabel]=\"field.searchButtonLabel || '\u0417\u043D\u0430\u0439\u0442\u0438'\"\n [openCameraLabel]=\"field.openCameraLabel || '\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [closeCameraLabel]=\"field.closeCameraLabel || '\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u043A\u0430\u043C\u0435\u0440\u0443'\"\n [cameraTitle]=\"field.cameraTitle || '\u041A\u0430\u043C\u0435\u0440\u0430'\"\n [cameraStartingLabel]=\"field.cameraStartingLabel || '\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u0454\u043C\u043E \u043A\u0430\u043C\u0435\u0440\u0443\u2026'\"\n [cameraHint]=\"field.cameraHint || '\u041D\u0430\u0432\u0435\u0434\u0456\u0442\u044C \u043A\u0430\u043C\u0435\u0440\u0443 \u043D\u0430 \u0448\u0442\u0440\u0438\u0445\u043A\u043E\u0434. \u041F\u0456\u0441\u043B\u044F \u0437\u0447\u0438\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043A\u043E\u0434 \u043F\u0456\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E.'\"\n (valueChange)=\"onBarcodeValueChange(field, $event)\"\n (searchRequested)=\"runBarcodeLookup(field)\"\n (scanned)=\"onBarcodeScanned(field, $event)\"\n ></forms-barcode-scanner-input>\n @if (getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (isInputField(field)) {\n <forms-form-input-host\n [field]=\"field\"\n [value]=\"modelSignal()[field.id]\"\n [disabled]=\"disabled\"\n [error]=\"errors[field.id] ?? ''\"\n [actions]=\"resolveFieldActions(field)\"\n (valueChange)=\"updateField(field.id, $any($event))\"\n (actionTriggered)=\"onFieldActionTriggered(field.id, $event)\"\n ></forms-form-input-host>\n }\n\n @if (field.type === 'select') {\n <forms-select\n [name]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [data]=\"field.data\"\n [required]=\"!!field.required\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [options]=\"resolveSelectOptions(field)\"\n [searchable]=\"(field.searchable ?? !!field.data) || !!field.optionsSource\"\n [remoteSearch]=\"!!field.optionsSource\"\n (searchChange)=\"field.optionsSource ? onSelectSearchChange(field, $event) : null\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"onSelectValueChange(field, $event)\">\n </forms-select>\n @if (field.lookupApi && getLookupError(field.id)) {\n <div class=\"mt-3 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700\">\n {{ getLookupError(field.id) }}\n </div>\n }\n }\n\n @if (field.type === 'checkbox') {\n <forms-checkbox-input\n [label]=\"field.label\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"!!modelSignal()[field.id]\"\n (valueChange)=\"updateField(field.id, $event)\"\n ></forms-checkbox-input>\n }\n\n @if (field.type === 'color') {\n <forms-color-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-color-input>\n }\n\n @if (field.type === 'slug') {\n <forms-slug-input\n [name]=\"field.id\"\n [id]=\"field.id\"\n [label]=\"field.label\"\n [placeholder]=\"field.placeholder\"\n [hint]=\"field.hint\"\n [required]=\"!!field.required\"\n [readonly]=\"!!field.readonly\"\n [disabled]=\"isFieldDisabled(field)\"\n [fullWidth]=\"field.fullWidth !== false\"\n [error]=\"errors[field.id] ?? ''\"\n [ngModel]=\"modelSignal()[field.id]\"\n (ngModelChange)=\"updateField(field.id, $event)\"\n ></forms-slug-input>\n }\n\n @if (field.type === 'photo') {\n <forms-photo-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept ?? 'image/*'\"\n [multiple]=\"!!field.multiple\"\n [limit]=\"field.limit ?? 0\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n [uploadPhoto]=\"uploadPhoto\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n (uploadingChange)=\"uploadingChange.emit($event)\"\n ></forms-photo-input>\n }\n @if (field.type === 'file') {\n <forms-file-input\n [label]=\"field.label\"\n [hint]=\"field.hint\"\n [accept]=\"field.accept\"\n [multiple]=\"!!field.multiple\"\n [disabled]=\"isFieldDisabled(field)\"\n [value]=\"$any(modelSignal()[field.id])\"\n (valueChange)=\"updateField(field.id, $event)\"\n (filesChange)=\"filesChange.emit({ fieldId: field.id, files: $event })\"\n ></forms-file-input>\n }\n </div>\n }\n }\n </ng-template>\n\n @for (field of fields; track field.id) {\n <ng-container *ngTemplateOutlet=\"renderField; context: { $implicit: field }\"></ng-container>\n }\n\n @if (showSubmit) {\n <div class=\"flex justify-end md:col-span-12\">\n <button\n type=\"submit\"\n class=\"inline-flex items-center rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 disabled:opacity-50\"\n [disabled]=\"disabled || !dynamicForm.form.valid\"\n >\n {{ submitLabel }}\n </button>\n </div>\n }\n</form>\n" }]
536
+ }], propDecorators: { fields: [{
537
+ type: Input
538
+ }], value: [{
539
+ type: Input
540
+ }], errors: [{
541
+ type: Input
542
+ }], submitLabel: [{
543
+ type: Input
544
+ }], showSubmit: [{
545
+ type: Input
546
+ }], disabled: [{
547
+ type: Input
548
+ }], uploadPhoto: [{
549
+ type: Input
550
+ }], valueChange: [{
551
+ type: Output
552
+ }], lookupCompleted: [{
553
+ type: Output
554
+ }], submitted: [{
555
+ type: Output
556
+ }], filesChange: [{
557
+ type: Output
558
+ }], uploadingChange: [{
559
+ type: Output
560
+ }], fieldActionTriggered: [{
561
+ type: Output
562
+ }] } });
563
+
564
+ class UiActionFormModalComponent {
565
+ formId = input('', ...(ngDevMode ? [{ debugName: "formId" }] : /* istanbul ignore next */ []));
566
+ recordId = input('', ...(ngDevMode ? [{ debugName: "recordId" }] : /* istanbul ignore next */ []));
567
+ isOpen = input(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
568
+ initialValue = input({}, ...(ngDevMode ? [{ debugName: "initialValue" }] : /* istanbul ignore next */ []));
569
+ uploadPhoto = input(null, ...(ngDevMode ? [{ debugName: "uploadPhoto" }] : /* istanbul ignore next */ []));
570
+ closed = output();
571
+ completed = output();
572
+ isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
573
+ isSubmitting = signal(false, ...(ngDevMode ? [{ debugName: "isSubmitting" }] : /* istanbul ignore next */ []));
574
+ formConfig = signal(null, ...(ngDevMode ? [{ debugName: "formConfig" }] : /* istanbul ignore next */ []));
575
+ formValue = signal({}, ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
576
+ fields = computed(() => this.formConfig()?.fields ?? [], ...(ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []));
577
+ submitAction = computed(() => this.formConfig()?.actions?.find((action) => action.type === 'submit' || action.type === 'api'), ...(ngDevMode ? [{ debugName: "submitAction" }] : /* istanbul ignore next */ []));
578
+ submitLabel = computed(() => this.submitAction()?.submit || this.submitAction()?.label || 'Save', ...(ngDevMode ? [{ debugName: "submitLabel" }] : /* istanbul ignore next */ []));
579
+ http = inject(HttpClient);
580
+ router = inject(Router);
581
+ ngOnChanges(changes) {
582
+ if (this.isOpen() && ('isOpen' in changes || 'formId' in changes || 'recordId' in changes)) {
583
+ this.loadForm();
584
+ }
585
+ }
586
+ close() {
587
+ this.closed.emit();
588
+ }
589
+ onValueChange(value) {
590
+ this.formValue.set(value);
591
+ }
592
+ onFieldActionTriggered(event) {
593
+ const endpoint = this.resolveEndpoint(event.action.api);
594
+ if (!endpoint)
595
+ return;
596
+ this.http.request(this.resolveMethod(event.action.method), endpoint, { body: { ...this.formValue(), ...(event.action.payload || {}) } }).subscribe({
597
+ next: (response) => {
598
+ const data = this.extractData(response);
599
+ if (data)
600
+ this.formValue.update((current) => ({ ...current, ...data }));
601
+ },
602
+ });
603
+ }
604
+ confirm() {
605
+ if (this.isSubmitting())
606
+ return;
607
+ const action = this.submitAction();
608
+ const endpoint = this.resolveEndpoint(this.formConfig()?.api || action?.api);
609
+ if (!endpoint)
610
+ return;
611
+ const payload = { ...(this.initialValue() || {}), ...(action?.payload || {}), ...this.formValue() };
612
+ const recordId = String(this.recordId() || '').trim();
613
+ if (recordId && !Object.prototype.hasOwnProperty.call(payload, 'id'))
614
+ payload['id'] = recordId;
615
+ this.isSubmitting.set(true);
616
+ this.http.request(this.resolveMethod(action?.method || (recordId ? 'PATCH' : 'POST')), endpoint, { body: payload }).subscribe({
617
+ next: (response) => {
618
+ this.isSubmitting.set(false);
619
+ this.completed.emit(response);
620
+ this.navigateAfterSubmit(response);
621
+ this.close();
622
+ },
623
+ error: () => this.isSubmitting.set(false),
624
+ });
625
+ }
626
+ loadForm() {
627
+ const formId = String(this.formId() || '').trim();
628
+ if (!formId)
629
+ return;
630
+ this.isLoading.set(true);
631
+ const recordId = String(this.recordId() || '').trim();
632
+ const endpoint = `/api/crud/form/${encodeURIComponent(formId)}${recordId ? `/${encodeURIComponent(recordId)}` : ''}`;
633
+ this.http.get(endpoint).subscribe({
634
+ next: (response) => {
635
+ const config = response.form || response;
636
+ this.formConfig.set(config);
637
+ this.formValue.set({ ...(config.defaults || {}), ...(config.item || {}), ...(this.initialValue() || {}), ...(recordId ? { id: recordId } : {}) });
638
+ this.isLoading.set(false);
639
+ },
640
+ error: () => {
641
+ this.formConfig.set(null);
642
+ this.isLoading.set(false);
643
+ },
644
+ });
645
+ }
646
+ resolveEndpoint(endpoint) {
647
+ const value = String(endpoint || '').trim();
648
+ if (!value)
649
+ return '';
650
+ if (/^https?:\/\//i.test(value) || value.startsWith('/api/'))
651
+ return value;
652
+ return `/api${value.startsWith('/') ? value : `/${value}`}`;
653
+ }
654
+ resolveMethod(method) {
655
+ return String(method || 'POST').toUpperCase();
656
+ }
657
+ extractData(response) {
658
+ if (!response || typeof response !== 'object')
659
+ return null;
660
+ const value = response.data;
661
+ return value && typeof value === 'object' ? value : response;
662
+ }
663
+ navigateAfterSubmit(response) {
664
+ const redirect = this.formConfig()?.redirectTo || this.submitAction()?.redirectTo;
665
+ if (!redirect)
666
+ return;
667
+ const data = this.extractData(response) || {};
668
+ const target = redirect.replace(/:([a-zA-Z_][a-zA-Z0-9_.]*)/g, (_match, key) => {
669
+ const value = String(key).split('.').reduce((current, part) => current && typeof current === 'object' ? current[part] : undefined, data);
670
+ return value == null ? '' : String(value);
671
+ });
672
+ if (target && !target.includes(':'))
673
+ void this.router.navigateByUrl(target.startsWith('/') ? target : `/${target}`);
674
+ }
675
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiActionFormModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
676
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiActionFormModalComponent, isStandalone: true, selector: "forms-action-form-modal", inputs: { formId: { classPropertyName: "formId", publicName: "formId", isSignal: true, isRequired: false, transformFunction: null }, recordId: { classPropertyName: "recordId", publicName: "recordId", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, initialValue: { classPropertyName: "initialValue", publicName: "initialValue", isSignal: true, isRequired: false, transformFunction: null }, uploadPhoto: { classPropertyName: "uploadPhoto", publicName: "uploadPhoto", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed", completed: "completed" }, usesOnChanges: true, ngImport: i0, template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 p-4\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-slate-50 shadow-2xl\">\n <div class=\"flex items-center justify-between border-b border-slate-200 bg-white px-6 py-4\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto()\" (valueChange)=\"onValueChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"flex justify-end gap-3 border-t border-slate-200 bg-white px-6 py-4\">\n <ui-button variant=\"ghost\" (click)=\"close()\">Cancel</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }, { kind: "component", type: UiDynamicFormComponent$1, selector: "forms-dynamic-form", inputs: ["fields", "value", "errors", "submitLabel", "showSubmit", "disabled", "uploadPhoto"], outputs: ["valueChange", "lookupCompleted", "submitted", "filesChange", "uploadingChange", "fieldActionTriggered"] }] });
677
+ }
678
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiActionFormModalComponent, decorators: [{
679
+ type: Component,
680
+ args: [{ selector: 'forms-action-form-modal', standalone: true, imports: [CommonModule, UiButtonComponent, UiDynamicFormComponent$1], template: "@if (isOpen()) {\n <div class=\"fixed inset-0 z-50 flex items-center justify-center bg-slate-950/40 p-4\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-slate-50 shadow-2xl\">\n <div class=\"flex items-center justify-between border-b border-slate-200 bg-white px-6 py-4\">\n <h2 class=\"text-lg font-semibold\">{{ formConfig()?.title || 'Form' }}</h2>\n <button type=\"button\" class=\"text-xl text-slate-400 hover:text-slate-900\" aria-label=\"Close\" (click)=\"close()\">\u00D7</button>\n </div>\n <div class=\"p-6\">\n @if (isLoading()) {\n <p class=\"text-sm text-slate-500\">Loading form\u2026</p>\n } @else {\n <forms-dynamic-form [fields]=\"fields()\" [value]=\"formValue()\" [showSubmit]=\"false\" [uploadPhoto]=\"uploadPhoto()\" (valueChange)=\"onValueChange($event)\" (fieldActionTriggered)=\"onFieldActionTriggered($event)\"></forms-dynamic-form>\n }\n </div>\n <div class=\"flex justify-end gap-3 border-t border-slate-200 bg-white px-6 py-4\">\n <ui-button variant=\"ghost\" (click)=\"close()\">Cancel</ui-button>\n @if (submitAction()) { <ui-button variant=\"primary\" [disabled]=\"isLoading() || isSubmitting()\" (click)=\"confirm()\">{{ submitLabel() }}</ui-button> }\n </div>\n </div>\n </div>\n}\n" }]
681
+ }], propDecorators: { formId: [{ type: i0.Input, args: [{ isSignal: true, alias: "formId", required: false }] }], recordId: [{ type: i0.Input, args: [{ isSignal: true, alias: "recordId", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }], initialValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialValue", required: false }] }], uploadPhoto: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadPhoto", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }], completed: [{ type: i0.Output, args: ["completed"] }] } });
682
+
683
+ class UiDateInputComponent {
684
+ cdr = inject(ChangeDetectorRef);
685
+ label;
686
+ hint;
687
+ placeholder;
688
+ name;
689
+ id;
690
+ type = 'date';
691
+ element = 'input';
692
+ rows = 4;
693
+ required = false;
694
+ fullWidth = false;
695
+ autocomplete;
696
+ readonly = false;
697
+ pattern;
698
+ error;
699
+ actions = [];
700
+ disabled = false;
701
+ value = '';
702
+ static nextId = 0;
703
+ generatedId = `ui-input-${UiDateInputComponent.nextId++}`;
704
+ actionTriggered = new EventEmitter();
705
+ get controlId() {
706
+ return this.id ?? this.generatedId;
707
+ }
708
+ onChange = () => undefined;
709
+ onTouched = () => undefined;
710
+ writeValue(value) {
711
+ this.value = value ?? '';
712
+ this.cdr.markForCheck();
713
+ }
714
+ registerOnChange(fn) {
715
+ this.onChange = fn;
716
+ }
717
+ registerOnTouched(fn) {
718
+ this.onTouched = fn;
719
+ }
720
+ setDisabledState(isDisabled) {
721
+ this.disabled = isDisabled;
722
+ this.cdr.markForCheck();
723
+ }
724
+ onValueChange(value) {
725
+ const parsed = value;
726
+ this.value = parsed;
727
+ this.onChange(parsed);
728
+ }
729
+ triggerAction(action) {
730
+ this.actionTriggered.emit(action);
731
+ }
732
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
733
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDateInputComponent, isStandalone: true, selector: "forms-date-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
734
+ {
735
+ provide: NG_VALUE_ACCESSOR,
736
+ useExisting: forwardRef(() => UiDateInputComponent),
737
+ multi: true,
738
+ },
739
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
740
+ }
741
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDateInputComponent, decorators: [{
742
+ type: Component,
743
+ args: [{ selector: 'forms-date-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
744
+ {
745
+ provide: NG_VALUE_ACCESSOR,
746
+ useExisting: forwardRef(() => UiDateInputComponent),
747
+ multi: true,
748
+ },
749
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
750
+ }], propDecorators: { label: [{
751
+ type: Input
752
+ }], hint: [{
753
+ type: Input
754
+ }], placeholder: [{
755
+ type: Input
756
+ }], name: [{
757
+ type: Input
758
+ }], id: [{
759
+ type: Input
760
+ }], rows: [{
761
+ type: Input
762
+ }], required: [{
763
+ type: Input
764
+ }], fullWidth: [{
765
+ type: Input
766
+ }], autocomplete: [{
767
+ type: Input
768
+ }], readonly: [{
769
+ type: Input
770
+ }], pattern: [{
771
+ type: Input
772
+ }], error: [{
773
+ type: Input
774
+ }], actions: [{
775
+ type: Input
776
+ }], actionTriggered: [{
777
+ type: Output
778
+ }] } });
779
+
780
+ class UiDatetimeInputComponent {
781
+ cdr = inject(ChangeDetectorRef);
782
+ label;
783
+ hint;
784
+ placeholder;
785
+ name;
786
+ id;
787
+ type = 'datetime-local';
788
+ element = 'input';
789
+ rows = 4;
790
+ required = false;
791
+ fullWidth = false;
792
+ autocomplete;
793
+ readonly = false;
794
+ pattern;
795
+ error;
796
+ actions = [];
797
+ disabled = false;
798
+ value = '';
799
+ static nextId = 0;
800
+ generatedId = `ui-input-${UiDatetimeInputComponent.nextId++}`;
801
+ actionTriggered = new EventEmitter();
802
+ get controlId() {
803
+ return this.id ?? this.generatedId;
804
+ }
805
+ onChange = () => undefined;
806
+ onTouched = () => undefined;
807
+ writeValue(value) {
808
+ this.value = value ?? '';
809
+ this.cdr.markForCheck();
810
+ }
811
+ registerOnChange(fn) {
812
+ this.onChange = fn;
813
+ }
814
+ registerOnTouched(fn) {
815
+ this.onTouched = fn;
816
+ }
817
+ setDisabledState(isDisabled) {
818
+ this.disabled = isDisabled;
819
+ this.cdr.markForCheck();
820
+ }
821
+ onValueChange(value) {
822
+ const parsed = value;
823
+ this.value = parsed;
824
+ this.onChange(parsed);
825
+ }
826
+ triggerAction(action) {
827
+ this.actionTriggered.emit(action);
828
+ }
829
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDatetimeInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
830
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiDatetimeInputComponent, isStandalone: true, selector: "forms-datetime-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
831
+ {
832
+ provide: NG_VALUE_ACCESSOR,
833
+ useExisting: forwardRef(() => UiDatetimeInputComponent),
834
+ multi: true,
835
+ },
836
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
837
+ }
838
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiDatetimeInputComponent, decorators: [{
839
+ type: Component,
840
+ args: [{ selector: 'forms-datetime-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
841
+ {
842
+ provide: NG_VALUE_ACCESSOR,
843
+ useExisting: forwardRef(() => UiDatetimeInputComponent),
844
+ multi: true,
845
+ },
846
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
847
+ }], propDecorators: { label: [{
848
+ type: Input
849
+ }], hint: [{
850
+ type: Input
851
+ }], placeholder: [{
852
+ type: Input
853
+ }], name: [{
854
+ type: Input
855
+ }], id: [{
856
+ type: Input
857
+ }], rows: [{
858
+ type: Input
859
+ }], required: [{
860
+ type: Input
861
+ }], fullWidth: [{
862
+ type: Input
863
+ }], autocomplete: [{
864
+ type: Input
865
+ }], readonly: [{
866
+ type: Input
867
+ }], pattern: [{
868
+ type: Input
869
+ }], error: [{
870
+ type: Input
871
+ }], actions: [{
872
+ type: Input
873
+ }], actionTriggered: [{
874
+ type: Output
875
+ }] } });
876
+
877
+ class UiEmailInputComponent {
878
+ cdr = inject(ChangeDetectorRef);
879
+ label;
880
+ hint;
881
+ placeholder;
882
+ name;
883
+ id;
884
+ type = 'email';
885
+ element = 'input';
886
+ rows = 4;
887
+ required = false;
888
+ fullWidth = false;
889
+ autocomplete;
890
+ readonly = false;
891
+ pattern;
892
+ error;
893
+ actions = [];
894
+ disabled = false;
895
+ value = '';
896
+ static nextId = 0;
897
+ generatedId = `ui-input-${UiEmailInputComponent.nextId++}`;
898
+ actionTriggered = new EventEmitter();
899
+ get controlId() {
900
+ return this.id ?? this.generatedId;
901
+ }
902
+ onChange = () => undefined;
903
+ onTouched = () => undefined;
904
+ writeValue(value) {
905
+ this.value = value ?? '';
906
+ this.cdr.markForCheck();
907
+ }
908
+ registerOnChange(fn) {
909
+ this.onChange = fn;
910
+ }
911
+ registerOnTouched(fn) {
912
+ this.onTouched = fn;
913
+ }
914
+ setDisabledState(isDisabled) {
915
+ this.disabled = isDisabled;
916
+ this.cdr.markForCheck();
917
+ }
918
+ onValueChange(value) {
919
+ const parsed = value;
920
+ this.value = parsed;
921
+ this.onChange(parsed);
922
+ }
923
+ triggerAction(action) {
924
+ this.actionTriggered.emit(action);
925
+ }
926
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
927
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiEmailInputComponent, isStandalone: true, selector: "forms-email-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
928
+ {
929
+ provide: NG_VALUE_ACCESSOR,
930
+ useExisting: forwardRef(() => UiEmailInputComponent),
931
+ multi: true,
932
+ },
933
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
934
+ }
935
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiEmailInputComponent, decorators: [{
936
+ type: Component,
937
+ args: [{ selector: 'forms-email-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
938
+ {
939
+ provide: NG_VALUE_ACCESSOR,
940
+ useExisting: forwardRef(() => UiEmailInputComponent),
941
+ multi: true,
942
+ },
943
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
944
+ }], propDecorators: { label: [{
945
+ type: Input
946
+ }], hint: [{
947
+ type: Input
948
+ }], placeholder: [{
949
+ type: Input
950
+ }], name: [{
951
+ type: Input
952
+ }], id: [{
953
+ type: Input
954
+ }], rows: [{
955
+ type: Input
956
+ }], required: [{
957
+ type: Input
958
+ }], fullWidth: [{
959
+ type: Input
960
+ }], autocomplete: [{
961
+ type: Input
962
+ }], readonly: [{
963
+ type: Input
964
+ }], pattern: [{
965
+ type: Input
966
+ }], error: [{
967
+ type: Input
968
+ }], actions: [{
969
+ type: Input
970
+ }], actionTriggered: [{
971
+ type: Output
972
+ }] } });
973
+
974
+ class UiNumberInputComponent {
975
+ cdr = inject(ChangeDetectorRef);
976
+ label;
977
+ hint;
978
+ placeholder;
979
+ name;
980
+ id;
981
+ type = 'number';
982
+ element = 'input';
983
+ rows = 4;
984
+ required = false;
985
+ fullWidth = false;
986
+ autocomplete;
987
+ readonly = false;
988
+ disabled = false;
989
+ pattern;
990
+ error;
991
+ actions = [];
992
+ value = '';
993
+ static nextId = 0;
994
+ generatedId = `ui-input-${UiNumberInputComponent.nextId++}`;
995
+ actionTriggered = new EventEmitter();
996
+ get controlId() {
997
+ return this.id ?? this.generatedId;
998
+ }
999
+ onChange = () => undefined;
1000
+ onTouched = () => undefined;
1001
+ writeValue(value) {
1002
+ this.value = value ?? '';
1003
+ this.cdr.markForCheck();
1004
+ }
1005
+ registerOnChange(fn) {
1006
+ this.onChange = fn;
1007
+ }
1008
+ registerOnTouched(fn) {
1009
+ this.onTouched = fn;
1010
+ }
1011
+ setDisabledState(isDisabled) {
1012
+ this.disabled = isDisabled;
1013
+ this.cdr.markForCheck();
1014
+ }
1015
+ onValueChange(value) {
1016
+ const parsed = value === '' ? null : Number(value);
1017
+ this.value = parsed;
1018
+ this.onChange(parsed);
1019
+ }
1020
+ triggerAction(action) {
1021
+ this.actionTriggered.emit(action);
1022
+ }
1023
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiNumberInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1024
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiNumberInputComponent, isStandalone: true, selector: "forms-number-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", disabled: "disabled", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1025
+ {
1026
+ provide: NG_VALUE_ACCESSOR,
1027
+ useExisting: forwardRef(() => UiNumberInputComponent),
1028
+ multi: true,
1029
+ },
1030
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1031
+ }
1032
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiNumberInputComponent, decorators: [{
1033
+ type: Component,
1034
+ args: [{ selector: 'forms-number-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1035
+ {
1036
+ provide: NG_VALUE_ACCESSOR,
1037
+ useExisting: forwardRef(() => UiNumberInputComponent),
1038
+ multi: true,
1039
+ },
1040
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
1041
+ }], propDecorators: { label: [{
1042
+ type: Input
1043
+ }], hint: [{
1044
+ type: Input
1045
+ }], placeholder: [{
1046
+ type: Input
1047
+ }], name: [{
1048
+ type: Input
1049
+ }], id: [{
1050
+ type: Input
1051
+ }], rows: [{
1052
+ type: Input
1053
+ }], required: [{
1054
+ type: Input
1055
+ }], fullWidth: [{
1056
+ type: Input
1057
+ }], autocomplete: [{
1058
+ type: Input
1059
+ }], readonly: [{
1060
+ type: Input
1061
+ }], disabled: [{
1062
+ type: Input
1063
+ }], pattern: [{
1064
+ type: Input
1065
+ }], error: [{
1066
+ type: Input
1067
+ }], actions: [{
1068
+ type: Input
1069
+ }], actionTriggered: [{
1070
+ type: Output
1071
+ }] } });
1072
+
1073
+ class UiPasswordInputComponent {
1074
+ cdr = inject(ChangeDetectorRef);
1075
+ label;
1076
+ hint;
1077
+ placeholder;
1078
+ name;
1079
+ id;
1080
+ type = 'password';
1081
+ element = 'input';
1082
+ rows = 4;
1083
+ required = false;
1084
+ fullWidth = false;
1085
+ autocomplete;
1086
+ readonly = false;
1087
+ pattern;
1088
+ error;
1089
+ actions = [];
1090
+ disabled = false;
1091
+ value = '';
1092
+ static nextId = 0;
1093
+ generatedId = `ui-input-${UiPasswordInputComponent.nextId++}`;
1094
+ actionTriggered = new EventEmitter();
1095
+ get controlId() {
1096
+ return this.id ?? this.generatedId;
1097
+ }
1098
+ onChange = () => undefined;
1099
+ onTouched = () => undefined;
1100
+ writeValue(value) {
1101
+ this.value = value ?? '';
1102
+ this.cdr.markForCheck();
1103
+ }
1104
+ registerOnChange(fn) {
1105
+ this.onChange = fn;
1106
+ }
1107
+ registerOnTouched(fn) {
1108
+ this.onTouched = fn;
1109
+ }
1110
+ setDisabledState(isDisabled) {
1111
+ this.disabled = isDisabled;
1112
+ this.cdr.markForCheck();
1113
+ }
1114
+ onValueChange(value) {
1115
+ const parsed = value;
1116
+ this.value = parsed;
1117
+ this.onChange(parsed);
1118
+ }
1119
+ triggerAction(action) {
1120
+ this.actionTriggered.emit(action);
1121
+ }
1122
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPasswordInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPasswordInputComponent, isStandalone: true, selector: "forms-password-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1124
+ {
1125
+ provide: NG_VALUE_ACCESSOR,
1126
+ useExisting: forwardRef(() => UiPasswordInputComponent),
1127
+ multi: true,
1128
+ },
1129
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1130
+ }
1131
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPasswordInputComponent, decorators: [{
1132
+ type: Component,
1133
+ args: [{ selector: 'forms-password-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1134
+ {
1135
+ provide: NG_VALUE_ACCESSOR,
1136
+ useExisting: forwardRef(() => UiPasswordInputComponent),
1137
+ multi: true,
1138
+ },
1139
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
1140
+ }], propDecorators: { label: [{
1141
+ type: Input
1142
+ }], hint: [{
1143
+ type: Input
1144
+ }], placeholder: [{
1145
+ type: Input
1146
+ }], name: [{
1147
+ type: Input
1148
+ }], id: [{
1149
+ type: Input
1150
+ }], rows: [{
1151
+ type: Input
1152
+ }], required: [{
1153
+ type: Input
1154
+ }], fullWidth: [{
1155
+ type: Input
1156
+ }], autocomplete: [{
1157
+ type: Input
1158
+ }], readonly: [{
1159
+ type: Input
1160
+ }], pattern: [{
1161
+ type: Input
1162
+ }], error: [{
1163
+ type: Input
1164
+ }], actions: [{
1165
+ type: Input
1166
+ }], actionTriggered: [{
1167
+ type: Output
1168
+ }] } });
1169
+
1170
+ class UiTextInputComponent {
1171
+ cdr = inject(ChangeDetectorRef);
1172
+ label;
1173
+ hint;
1174
+ placeholder;
1175
+ name;
1176
+ id;
1177
+ type = 'text';
1178
+ element = 'input';
1179
+ rows = 4;
1180
+ required = false;
1181
+ fullWidth = false;
1182
+ autocomplete;
1183
+ readonly = false;
1184
+ pattern;
1185
+ error;
1186
+ actions = [];
1187
+ disabled = false;
1188
+ value = '';
1189
+ static nextId = 0;
1190
+ generatedId = `ui-input-${UiTextInputComponent.nextId++}`;
1191
+ actionTriggered = new EventEmitter();
1192
+ get controlId() {
1193
+ return this.id ?? this.generatedId;
1194
+ }
1195
+ onChange = () => undefined;
1196
+ onTouched = () => undefined;
1197
+ writeValue(value) {
1198
+ this.value = value ?? '';
1199
+ this.cdr.markForCheck();
1200
+ }
1201
+ registerOnChange(fn) {
1202
+ this.onChange = fn;
1203
+ }
1204
+ registerOnTouched(fn) {
1205
+ this.onTouched = fn;
1206
+ }
1207
+ setDisabledState(isDisabled) {
1208
+ this.disabled = isDisabled;
1209
+ this.cdr.markForCheck();
1210
+ }
1211
+ onValueChange(value) {
1212
+ const parsed = value;
1213
+ this.value = parsed;
1214
+ this.onChange(parsed);
1215
+ }
1216
+ triggerAction(action) {
1217
+ this.actionTriggered.emit(action);
1218
+ }
1219
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1220
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiTextInputComponent, isStandalone: true, selector: "forms-text-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1221
+ {
1222
+ provide: NG_VALUE_ACCESSOR,
1223
+ useExisting: forwardRef(() => UiTextInputComponent),
1224
+ multi: true,
1225
+ },
1226
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1227
+ }
1228
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextInputComponent, decorators: [{
1229
+ type: Component,
1230
+ args: [{ selector: 'forms-text-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1231
+ {
1232
+ provide: NG_VALUE_ACCESSOR,
1233
+ useExisting: forwardRef(() => UiTextInputComponent),
1234
+ multi: true,
1235
+ },
1236
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
1237
+ }], propDecorators: { label: [{
1238
+ type: Input
1239
+ }], hint: [{
1240
+ type: Input
1241
+ }], placeholder: [{
1242
+ type: Input
1243
+ }], name: [{
1244
+ type: Input
1245
+ }], id: [{
1246
+ type: Input
1247
+ }], rows: [{
1248
+ type: Input
1249
+ }], required: [{
1250
+ type: Input
1251
+ }], fullWidth: [{
1252
+ type: Input
1253
+ }], autocomplete: [{
1254
+ type: Input
1255
+ }], readonly: [{
1256
+ type: Input
1257
+ }], pattern: [{
1258
+ type: Input
1259
+ }], error: [{
1260
+ type: Input
1261
+ }], actions: [{
1262
+ type: Input
1263
+ }], actionTriggered: [{
1264
+ type: Output
1265
+ }] } });
1266
+
1267
+ class UiTextareaInputComponent {
1268
+ cdr = inject(ChangeDetectorRef);
1269
+ label;
1270
+ hint;
1271
+ placeholder;
1272
+ name;
1273
+ id;
1274
+ type = 'text';
1275
+ element = 'textarea';
1276
+ rows = 4;
1277
+ required = false;
1278
+ fullWidth = false;
1279
+ autocomplete;
1280
+ readonly = false;
1281
+ pattern;
1282
+ error;
1283
+ actions = [];
1284
+ disabled = false;
1285
+ value = '';
1286
+ static nextId = 0;
1287
+ generatedId = `ui-input-${UiTextareaInputComponent.nextId++}`;
1288
+ actionTriggered = new EventEmitter();
1289
+ get controlId() {
1290
+ return this.id ?? this.generatedId;
1291
+ }
1292
+ onChange = () => undefined;
1293
+ onTouched = () => undefined;
1294
+ writeValue(value) {
1295
+ this.value = value ?? '';
1296
+ this.cdr.markForCheck();
1297
+ }
1298
+ registerOnChange(fn) {
1299
+ this.onChange = fn;
1300
+ }
1301
+ registerOnTouched(fn) {
1302
+ this.onTouched = fn;
1303
+ }
1304
+ setDisabledState(isDisabled) {
1305
+ this.disabled = isDisabled;
1306
+ this.cdr.markForCheck();
1307
+ }
1308
+ onValueChange(value) {
1309
+ const parsed = value;
1310
+ this.value = parsed;
1311
+ this.onChange(parsed);
1312
+ }
1313
+ triggerAction(action) {
1314
+ this.actionTriggered.emit(action);
1315
+ }
1316
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextareaInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1317
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiTextareaInputComponent, isStandalone: true, selector: "forms-textarea-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
1318
+ {
1319
+ provide: NG_VALUE_ACCESSOR,
1320
+ useExisting: forwardRef(() => UiTextareaInputComponent),
1321
+ multi: true,
1322
+ },
1323
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1324
+ }
1325
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiTextareaInputComponent, decorators: [{
1326
+ type: Component,
1327
+ args: [{ selector: 'forms-textarea-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
1328
+ {
1329
+ provide: NG_VALUE_ACCESSOR,
1330
+ useExisting: forwardRef(() => UiTextareaInputComponent),
1331
+ multi: true,
1332
+ },
1333
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n\n" }]
1334
+ }], propDecorators: { label: [{
1335
+ type: Input
1336
+ }], hint: [{
1337
+ type: Input
1338
+ }], placeholder: [{
1339
+ type: Input
1340
+ }], name: [{
1341
+ type: Input
1342
+ }], id: [{
1343
+ type: Input
1344
+ }], rows: [{
1345
+ type: Input
1346
+ }], required: [{
1347
+ type: Input
1348
+ }], fullWidth: [{
1349
+ type: Input
1350
+ }], autocomplete: [{
1351
+ type: Input
1352
+ }], readonly: [{
1353
+ type: Input
1354
+ }], pattern: [{
1355
+ type: Input
1356
+ }], error: [{
1357
+ type: Input
1358
+ }], actions: [{
1359
+ type: Input
1360
+ }], actionTriggered: [{
1361
+ type: Output
1362
+ }] } });
1363
+
1364
+ const UI_FORM_INPUT_REGISTRY = {
1365
+ text: UiTextInputComponent,
1366
+ number: UiNumberInputComponent,
1367
+ date: UiDateInputComponent,
1368
+ datetime: UiDatetimeInputComponent,
1369
+ email: UiEmailInputComponent,
1370
+ password: UiPasswordInputComponent,
1371
+ textarea: UiTextareaInputComponent,
1372
+ };
1373
+
1374
+ class UiFormInputHostComponent {
1375
+ field;
1376
+ value;
1377
+ error = '';
1378
+ disabled = false;
1379
+ actions = [];
1380
+ options = [];
1381
+ valueChange = new EventEmitter();
1382
+ actionTriggered = new EventEmitter();
1383
+ viewContainer = inject(ViewContainerRef);
1384
+ componentRef;
1385
+ ngOnChanges(changes) {
1386
+ if (changes['field']) {
1387
+ this.render();
1388
+ return;
1389
+ }
1390
+ this.syncInputs();
1391
+ }
1392
+ render() {
1393
+ this.viewContainer.clear();
1394
+ const inputType = String(this.field?.type || 'text');
1395
+ const componentType = UI_FORM_INPUT_REGISTRY[inputType] || UI_FORM_INPUT_REGISTRY.text;
1396
+ this.componentRef = this.viewContainer.createComponent(componentType);
1397
+ this.syncInputs();
1398
+ const instance = this.componentRef.instance;
1399
+ instance.registerOnChange?.((nextValue) => this.valueChange.emit(nextValue));
1400
+ instance.registerOnTouched?.(() => undefined);
1401
+ instance.actionTriggered?.subscribe((action) => this.actionTriggered.emit(action));
1402
+ }
1403
+ syncInputs() {
1404
+ if (!this.componentRef || !this.field)
1405
+ return;
1406
+ const field = this.field;
1407
+ const setInput = (name, value) => this.componentRef?.setInput(name, value);
1408
+ setInput('name', field.id);
1409
+ setInput('id', field.id);
1410
+ setInput('label', field.label);
1411
+ setInput('placeholder', field.placeholder);
1412
+ setInput('hint', field.hint);
1413
+ setInput('required', !!field.required);
1414
+ setInput('readonly', !!field.readonly);
1415
+ setInput('disabled', this.disabled || !!field.disabled);
1416
+ setInput('pattern', field.pattern);
1417
+ setInput('rows', field.rows ?? 4);
1418
+ setInput('fullWidth', field.fullWidth !== false);
1419
+ setInput('error', this.error);
1420
+ setInput('actions', this.actions);
1421
+ const instance = this.componentRef.instance;
1422
+ instance.writeValue?.(this.value);
1423
+ instance.setDisabledState?.(this.disabled || !!field.disabled);
1424
+ }
1425
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1426
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiFormInputHostComponent, isStandalone: true, selector: "forms-form-input-host", inputs: { field: "field", value: "value", error: "error", disabled: "disabled", actions: "actions", options: "options" }, outputs: { valueChange: "valueChange", actionTriggered: "actionTriggered" }, usesOnChanges: true, ngImport: i0, template: '', isInline: true });
1427
+ }
1428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFormInputHostComponent, decorators: [{
1429
+ type: Component,
1430
+ args: [{
1431
+ selector: 'forms-form-input-host',
1432
+ standalone: true,
1433
+ template: '',
1434
+ }]
1435
+ }], propDecorators: { field: [{
1436
+ type: Input
1437
+ }], value: [{
1438
+ type: Input
1439
+ }], error: [{
1440
+ type: Input
1441
+ }], disabled: [{
1442
+ type: Input
1443
+ }], actions: [{
1444
+ type: Input
1445
+ }], options: [{
1446
+ type: Input
1447
+ }], valueChange: [{
1448
+ type: Output
1449
+ }], actionTriggered: [{
1450
+ type: Output
1451
+ }] } });
1452
+
1453
+ class UiBarcodeScannerInputComponent {
1454
+ value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1455
+ label = input('Штрихкод', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
1456
+ placeholder = input('Відскануйте або введіть штрихкод', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
1457
+ hint = input('Сканер може просто ввести код і натиснути Enter.', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
1458
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1459
+ autoOpen = input(true, ...(ngDevMode ? [{ debugName: "autoOpen" }] : /* istanbul ignore next */ []));
1460
+ searchButtonLabel = input('Знайти', ...(ngDevMode ? [{ debugName: "searchButtonLabel" }] : /* istanbul ignore next */ []));
1461
+ openCameraLabel = input('Відкрити камеру', ...(ngDevMode ? [{ debugName: "openCameraLabel" }] : /* istanbul ignore next */ []));
1462
+ closeCameraLabel = input('Закрити камеру', ...(ngDevMode ? [{ debugName: "closeCameraLabel" }] : /* istanbul ignore next */ []));
1463
+ cameraTitle = input('Камера', ...(ngDevMode ? [{ debugName: "cameraTitle" }] : /* istanbul ignore next */ []));
1464
+ cameraStartingLabel = input('Запускаємо камеру…', ...(ngDevMode ? [{ debugName: "cameraStartingLabel" }] : /* istanbul ignore next */ []));
1465
+ cameraHint = input('Наведіть камеру на штрихкод. Після зчитування код підставиться автоматично.', ...(ngDevMode ? [{ debugName: "cameraHint" }] : /* istanbul ignore next */ []));
1466
+ valueChange = output();
1467
+ scanned = output();
1468
+ searchRequested = output();
1469
+ cameraStateChange = output();
1470
+ isCameraOpen = signal(false, ...(ngDevMode ? [{ debugName: "isCameraOpen" }] : /* istanbul ignore next */ []));
1471
+ isCameraStarting = signal(false, ...(ngDevMode ? [{ debugName: "isCameraStarting" }] : /* istanbul ignore next */ []));
1472
+ cameraError = signal('', ...(ngDevMode ? [{ debugName: "cameraError" }] : /* istanbul ignore next */ []));
1473
+ cameraStatus = signal('Камеру не відкрито', ...(ngDevMode ? [{ debugName: "cameraStatus" }] : /* istanbul ignore next */ []));
1474
+ cameraVideo;
1475
+ barcodeReader = new BrowserMultiFormatOneDReader(new Map([[DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.CODE_128]]]), {
1476
+ delayBetweenScanAttempts: 75,
1477
+ delayBetweenScanSuccess: 250,
1478
+ tryPlayVideoTimeout: 5000,
1479
+ });
1480
+ nativeBarcodeDetector = null;
1481
+ cameraControls = null;
1482
+ cameraStream = null;
1483
+ scanLoopId = null;
1484
+ autoOpenQueued = false;
1485
+ openToken = 0;
1486
+ ngOnChanges(changes) {
1487
+ if (changes['disabled']?.currentValue === true) {
1488
+ this.stopCamera();
1489
+ this.autoOpenQueued = false;
1490
+ return;
1491
+ }
1492
+ if (changes['autoOpen'] && this.autoOpen() && !this.disabled()) {
1493
+ this.scheduleAutoOpen();
1494
+ }
1495
+ }
1496
+ ngAfterViewInit() {
1497
+ this.scheduleAutoOpen();
1498
+ }
1499
+ ngOnDestroy() {
1500
+ this.stopCamera();
1501
+ }
1502
+ onValueChange(nextValue) {
1503
+ this.valueChange.emit(String(nextValue ?? ''));
1504
+ }
1505
+ requestSearch() {
1506
+ this.searchRequested.emit();
1507
+ }
1508
+ async openCamera() {
1509
+ if (this.disabled() || this.isCameraStarting() || this.isCameraOpen()) {
1510
+ return;
1511
+ }
1512
+ this.cameraError.set('');
1513
+ this.cameraStatus.set('Запускаємо камеру…');
1514
+ this.isCameraStarting.set(true);
1515
+ this.cameraStateChange.emit('starting');
1516
+ this.isCameraOpen.set(true);
1517
+ const token = ++this.openToken;
1518
+ try {
1519
+ await new Promise((resolve) => {
1520
+ window.requestAnimationFrame(() => resolve());
1521
+ });
1522
+ if (token !== this.openToken) {
1523
+ return;
1524
+ }
1525
+ const video = this.cameraVideo?.nativeElement;
1526
+ if (!video) {
1527
+ throw new Error('Не вдалося підготувати камеру');
1528
+ }
1529
+ if (await this.tryOpenNativeCamera(token, video)) {
1530
+ this.isCameraOpen.set(true);
1531
+ this.cameraStatus.set('Камера відкрита, наведіть на штрихкод');
1532
+ this.cameraStateChange.emit('open');
1533
+ return;
1534
+ }
1535
+ await this.openZxingCamera(token, video);
1536
+ if (token !== this.openToken) {
1537
+ this.cameraControls?.stop();
1538
+ this.cameraControls = null;
1539
+ return;
1540
+ }
1541
+ this.isCameraOpen.set(true);
1542
+ this.cameraStatus.set('Камера відкрита, наведіть на штрихкод');
1543
+ this.cameraStateChange.emit('open');
1544
+ }
1545
+ catch (error) {
1546
+ if (token !== this.openToken) {
1547
+ return;
1548
+ }
1549
+ this.cameraError.set(error?.message || 'Не вдалося відкрити камеру');
1550
+ this.cameraStatus.set('Камеру не відкрито');
1551
+ this.cameraStateChange.emit('error');
1552
+ this.stopCamera();
1553
+ }
1554
+ finally {
1555
+ if (token === this.openToken) {
1556
+ this.isCameraStarting.set(false);
1557
+ }
1558
+ }
1559
+ }
1560
+ closeCamera() {
1561
+ this.stopCamera();
1562
+ }
1563
+ scheduleAutoOpen() {
1564
+ if (typeof window === 'undefined' || !this.autoOpen() || this.disabled() || this.isCameraOpen() || this.isCameraStarting() || this.autoOpenQueued) {
1565
+ return;
1566
+ }
1567
+ this.autoOpenQueued = true;
1568
+ window.requestAnimationFrame(() => {
1569
+ this.autoOpenQueued = false;
1570
+ if (!this.autoOpen() || this.disabled() || this.isCameraOpen() || this.isCameraStarting()) {
1571
+ return;
1572
+ }
1573
+ void this.openCamera();
1574
+ });
1575
+ }
1576
+ onBarcodeDetected(value) {
1577
+ this.valueChange.emit(value);
1578
+ this.scanned.emit(value);
1579
+ this.cameraStatus.set('Штрихкод знайдено');
1580
+ this.cameraControls?.stop();
1581
+ this.cameraControls = null;
1582
+ this.stopCamera();
1583
+ }
1584
+ stopCamera() {
1585
+ this.openToken += 1;
1586
+ if (this.scanLoopId != null) {
1587
+ window.cancelAnimationFrame(this.scanLoopId);
1588
+ this.scanLoopId = null;
1589
+ }
1590
+ this.cameraControls?.stop();
1591
+ this.cameraControls = null;
1592
+ if (this.cameraStream) {
1593
+ for (const track of this.cameraStream.getTracks()) {
1594
+ track.stop();
1595
+ }
1596
+ this.cameraStream = null;
1597
+ }
1598
+ const video = this.cameraVideo?.nativeElement;
1599
+ if (video) {
1600
+ video.pause();
1601
+ video.srcObject = null;
1602
+ }
1603
+ this.nativeBarcodeDetector = null;
1604
+ this.isCameraOpen.set(false);
1605
+ this.isCameraStarting.set(false);
1606
+ this.cameraStateChange.emit(this.cameraError() ? 'error' : 'idle');
1607
+ if (!this.cameraError()) {
1608
+ this.cameraStatus.set('Камеру зупинено');
1609
+ }
1610
+ }
1611
+ async tryOpenNativeCamera(token, video) {
1612
+ const detector = await this.createNativeBarcodeDetector();
1613
+ if (!detector) {
1614
+ return false;
1615
+ }
1616
+ try {
1617
+ const stream = await navigator.mediaDevices.getUserMedia({
1618
+ audio: false,
1619
+ video: {
1620
+ facingMode: { ideal: 'environment' },
1621
+ width: { ideal: 1280 },
1622
+ height: { ideal: 720 },
1623
+ },
1624
+ });
1625
+ if (token !== this.openToken) {
1626
+ stream.getTracks().forEach((track) => track.stop());
1627
+ return true;
1628
+ }
1629
+ this.nativeBarcodeDetector = detector;
1630
+ this.cameraStream = stream;
1631
+ video.srcObject = stream;
1632
+ await video.play().catch(() => undefined);
1633
+ this.scheduleNativeScan(token);
1634
+ return true;
1635
+ }
1636
+ catch {
1637
+ return false;
1638
+ }
1639
+ }
1640
+ async openZxingCamera(token, video) {
1641
+ this.cameraControls = await this.barcodeReader.decodeFromConstraints({
1642
+ audio: false,
1643
+ video: {
1644
+ facingMode: { ideal: 'environment' },
1645
+ width: { ideal: 960 },
1646
+ height: { ideal: 540 },
1647
+ },
1648
+ }, video, (result, error) => {
1649
+ if (token !== this.openToken) {
1650
+ return;
1651
+ }
1652
+ if (error || !result) {
1653
+ return;
1654
+ }
1655
+ const value = String(result.getText()).trim();
1656
+ if (value) {
1657
+ this.onBarcodeDetected(value);
1658
+ }
1659
+ });
1660
+ }
1661
+ scheduleNativeScan(token) {
1662
+ if (typeof window === 'undefined') {
1663
+ return;
1664
+ }
1665
+ const loop = async () => {
1666
+ if (token !== this.openToken) {
1667
+ return;
1668
+ }
1669
+ const video = this.cameraVideo?.nativeElement;
1670
+ const detector = this.nativeBarcodeDetector;
1671
+ if (!video || !detector || video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) {
1672
+ this.scanLoopId = window.requestAnimationFrame(() => {
1673
+ void loop();
1674
+ });
1675
+ return;
1676
+ }
1677
+ try {
1678
+ const barcodes = await detector.detect(video);
1679
+ const value = String(barcodes?.[0]?.rawValue || '').trim();
1680
+ if (value) {
1681
+ this.onBarcodeDetected(value);
1682
+ return;
1683
+ }
1684
+ }
1685
+ catch {
1686
+ // Native detection can fail on a frame; keep scanning.
1687
+ }
1688
+ this.scanLoopId = window.requestAnimationFrame(() => {
1689
+ void loop();
1690
+ });
1691
+ };
1692
+ this.scanLoopId = window.requestAnimationFrame(() => {
1693
+ void loop();
1694
+ });
1695
+ }
1696
+ async createNativeBarcodeDetector() {
1697
+ if (typeof window === 'undefined') {
1698
+ return null;
1699
+ }
1700
+ const detectorCtor = window.BarcodeDetector;
1701
+ if (!detectorCtor) {
1702
+ return null;
1703
+ }
1704
+ try {
1705
+ if (detectorCtor.getSupportedFormats) {
1706
+ const supportedFormats = await detectorCtor.getSupportedFormats();
1707
+ if (!supportedFormats.includes('code_128')) {
1708
+ return null;
1709
+ }
1710
+ }
1711
+ return new detectorCtor({ formats: ['code_128'] });
1712
+ }
1713
+ catch {
1714
+ return null;
1715
+ }
1716
+ }
1717
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiBarcodeScannerInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1718
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiBarcodeScannerInputComponent, isStandalone: true, selector: "forms-barcode-scanner-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoOpen: { classPropertyName: "autoOpen", publicName: "autoOpen", isSignal: true, isRequired: false, transformFunction: null }, searchButtonLabel: { classPropertyName: "searchButtonLabel", publicName: "searchButtonLabel", isSignal: true, isRequired: false, transformFunction: null }, openCameraLabel: { classPropertyName: "openCameraLabel", publicName: "openCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, closeCameraLabel: { classPropertyName: "closeCameraLabel", publicName: "closeCameraLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraTitle: { classPropertyName: "cameraTitle", publicName: "cameraTitle", isSignal: true, isRequired: false, transformFunction: null }, cameraStartingLabel: { classPropertyName: "cameraStartingLabel", publicName: "cameraStartingLabel", isSignal: true, isRequired: false, transformFunction: null }, cameraHint: { classPropertyName: "cameraHint", publicName: "cameraHint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange", scanned: "scanned", searchRequested: "searchRequested", cameraStateChange: "cameraStateChange" }, viewQueries: [{ propertyName: "cameraVideo", first: true, predicate: ["cameraVideo"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1719
+ <div class="space-y-2">
1720
+ @if (label()) {
1721
+ <label class="flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500" for="barcode-scanner-input">
1722
+ <span class="text-[0.7rem] font-semibold text-slate-600">
1723
+ {{ label() }}
1724
+ </span>
1725
+ </label>
1726
+ }
1727
+
1728
+ <div class="flex items-start gap-2">
1729
+ <div class="min-w-0 flex-1">
1730
+ <input
1731
+ #barcodeInput
1732
+ id="barcode-scanner-input"
1733
+ type="text"
1734
+ autocomplete="off"
1735
+ spellcheck="false"
1736
+ [disabled]="disabled()"
1737
+ [ngModel]="value()"
1738
+ (ngModelChange)="onValueChange($event)"
1739
+ (keydown.enter)="requestSearch()"
1740
+ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none"
1741
+ [placeholder]="placeholder()"
1742
+ />
1743
+ @if (hint()) {
1744
+ <p class="mt-2 text-[0.5rem] text-slate-500">
1745
+ {{ hint() }}
1746
+ </p>
1747
+ }
1748
+ </div>
1749
+
1750
+ <div class="mt-0.5 flex flex-col gap-2">
1751
+ <ui-button
1752
+ type="button"
1753
+ variant="secondary"
1754
+ size="sm"
1755
+ [disabled]="disabled()"
1756
+ (click)="requestSearch()"
1757
+ >
1758
+ {{ searchButtonLabel() }}
1759
+ </ui-button>
1760
+ <ui-button
1761
+ type="button"
1762
+ variant="ghost"
1763
+ size="sm"
1764
+ [disabled]="disabled() || isCameraStarting()"
1765
+ (click)="isCameraOpen() ? closeCamera() : openCamera()"
1766
+ >
1767
+ {{ isCameraOpen() ? closeCameraLabel() : openCameraLabel() }}
1768
+ </ui-button>
1769
+ </div>
1770
+ </div>
1771
+
1772
+ @if (isCameraStarting()) {
1773
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-500">
1774
+ {{ cameraStartingLabel() }}
1775
+ </div>
1776
+ }
1777
+
1778
+ @if (isCameraOpen()) {
1779
+ <section class="space-y-3 rounded-3xl border border-slate-200 bg-slate-950 p-3 text-white">
1780
+ <div class="flex items-center justify-between gap-3">
1781
+ <div>
1782
+ <p class="text-xs font-semibold uppercase tracking-[0.22em] text-slate-300">{{ cameraTitle() }}</p>
1783
+ <p class="text-sm text-slate-200">{{ cameraStatus() }}</p>
1784
+ </div>
1785
+ <button
1786
+ type="button"
1787
+ class="rounded-full border border-slate-700 px-3 py-1 text-xs font-semibold text-slate-200 transition hover:border-slate-500 hover:text-white"
1788
+ (click)="closeCamera()"
1789
+ >
1790
+ {{ closeCameraLabel() }}
1791
+ </button>
1792
+ </div>
1793
+
1794
+ <div class="overflow-hidden rounded-2xl bg-black">
1795
+ <video #cameraVideo class="h-[260px] w-full object-cover" autoplay muted playsinline></video>
1796
+ </div>
1797
+
1798
+ <p class="text-xs text-slate-300">
1799
+ {{ cameraHint() }}
1800
+ </p>
1801
+ </section>
1802
+ }
1803
+
1804
+ @if (cameraError()) {
1805
+ <div class="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
1806
+ {{ cameraError() }}
1807
+ </div>
1808
+ }
1809
+ </div>
1810
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }] });
1811
+ }
1812
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiBarcodeScannerInputComponent, decorators: [{
1813
+ type: Component,
1814
+ args: [{
1815
+ selector: 'forms-barcode-scanner-input',
1816
+ standalone: true,
1817
+ imports: [CommonModule, FormsModule, UiButtonComponent],
1818
+ template: `
1819
+ <div class="space-y-2">
1820
+ @if (label()) {
1821
+ <label class="flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500" for="barcode-scanner-input">
1822
+ <span class="text-[0.7rem] font-semibold text-slate-600">
1823
+ {{ label() }}
1824
+ </span>
1825
+ </label>
1826
+ }
1827
+
1828
+ <div class="flex items-start gap-2">
1829
+ <div class="min-w-0 flex-1">
1830
+ <input
1831
+ #barcodeInput
1832
+ id="barcode-scanner-input"
1833
+ type="text"
1834
+ autocomplete="off"
1835
+ spellcheck="false"
1836
+ [disabled]="disabled()"
1837
+ [ngModel]="value()"
1838
+ (ngModelChange)="onValueChange($event)"
1839
+ (keydown.enter)="requestSearch()"
1840
+ class="w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none"
1841
+ [placeholder]="placeholder()"
1842
+ />
1843
+ @if (hint()) {
1844
+ <p class="mt-2 text-[0.5rem] text-slate-500">
1845
+ {{ hint() }}
1846
+ </p>
1847
+ }
1848
+ </div>
1849
+
1850
+ <div class="mt-0.5 flex flex-col gap-2">
1851
+ <ui-button
1852
+ type="button"
1853
+ variant="secondary"
1854
+ size="sm"
1855
+ [disabled]="disabled()"
1856
+ (click)="requestSearch()"
1857
+ >
1858
+ {{ searchButtonLabel() }}
1859
+ </ui-button>
1860
+ <ui-button
1861
+ type="button"
1862
+ variant="ghost"
1863
+ size="sm"
1864
+ [disabled]="disabled() || isCameraStarting()"
1865
+ (click)="isCameraOpen() ? closeCamera() : openCamera()"
1866
+ >
1867
+ {{ isCameraOpen() ? closeCameraLabel() : openCameraLabel() }}
1868
+ </ui-button>
1869
+ </div>
1870
+ </div>
1871
+
1872
+ @if (isCameraStarting()) {
1873
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-500">
1874
+ {{ cameraStartingLabel() }}
1875
+ </div>
1876
+ }
1877
+
1878
+ @if (isCameraOpen()) {
1879
+ <section class="space-y-3 rounded-3xl border border-slate-200 bg-slate-950 p-3 text-white">
1880
+ <div class="flex items-center justify-between gap-3">
1881
+ <div>
1882
+ <p class="text-xs font-semibold uppercase tracking-[0.22em] text-slate-300">{{ cameraTitle() }}</p>
1883
+ <p class="text-sm text-slate-200">{{ cameraStatus() }}</p>
1884
+ </div>
1885
+ <button
1886
+ type="button"
1887
+ class="rounded-full border border-slate-700 px-3 py-1 text-xs font-semibold text-slate-200 transition hover:border-slate-500 hover:text-white"
1888
+ (click)="closeCamera()"
1889
+ >
1890
+ {{ closeCameraLabel() }}
1891
+ </button>
1892
+ </div>
1893
+
1894
+ <div class="overflow-hidden rounded-2xl bg-black">
1895
+ <video #cameraVideo class="h-[260px] w-full object-cover" autoplay muted playsinline></video>
1896
+ </div>
1897
+
1898
+ <p class="text-xs text-slate-300">
1899
+ {{ cameraHint() }}
1900
+ </p>
1901
+ </section>
1902
+ }
1903
+
1904
+ @if (cameraError()) {
1905
+ <div class="rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
1906
+ {{ cameraError() }}
1907
+ </div>
1908
+ }
1909
+ </div>
1910
+ `,
1911
+ }]
1912
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], 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 }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autoOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoOpen", required: false }] }], searchButtonLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchButtonLabel", required: false }] }], openCameraLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "openCameraLabel", required: false }] }], closeCameraLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeCameraLabel", required: false }] }], cameraTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameraTitle", required: false }] }], cameraStartingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameraStartingLabel", required: false }] }], cameraHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameraHint", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], scanned: [{ type: i0.Output, args: ["scanned"] }], searchRequested: [{ type: i0.Output, args: ["searchRequested"] }], cameraStateChange: [{ type: i0.Output, args: ["cameraStateChange"] }], cameraVideo: [{
1913
+ type: ViewChild,
1914
+ args: ['cameraVideo']
1915
+ }] } });
1916
+
1917
+ class UiCheckboxInputComponent {
1918
+ label;
1919
+ disabled = false;
1920
+ value = false;
1921
+ valueChange = new EventEmitter();
1922
+ onValueChange(event) {
1923
+ this.valueChange.emit(event.target.checked);
1924
+ }
1925
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1926
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiCheckboxInputComponent, isStandalone: true, selector: "forms-checkbox-input", inputs: { label: "label", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n />\n <span>{{ label }}</span>\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
1927
+ }
1928
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiCheckboxInputComponent, decorators: [{
1929
+ type: Component,
1930
+ args: [{ selector: 'forms-checkbox-input', standalone: true, imports: [CommonModule], template: "<label class=\"inline-flex items-center gap-3 text-sm text-slate-700\">\n <input\n type=\"checkbox\"\n class=\"h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-400\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onValueChange($event)\"\n />\n <span>{{ label }}</span>\n</label>\n" }]
1931
+ }], propDecorators: { label: [{
1932
+ type: Input
1933
+ }], disabled: [{
1934
+ type: Input
1935
+ }], value: [{
1936
+ type: Input
1937
+ }], valueChange: [{
1938
+ type: Output
1939
+ }] } });
1940
+
1941
+ class UiColorInputComponent {
1942
+ cdr = inject(ChangeDetectorRef);
1943
+ label;
1944
+ hint;
1945
+ name;
1946
+ id;
1947
+ required = false;
1948
+ fullWidth = false;
1949
+ error;
1950
+ disabled = false;
1951
+ value = '#000000';
1952
+ static nextId = 0;
1953
+ generatedId = `ui-color-input-${UiColorInputComponent.nextId++}`;
1954
+ onChange = () => undefined;
1955
+ onTouched = () => undefined;
1956
+ get controlId() {
1957
+ return this.id ?? this.generatedId;
1958
+ }
1959
+ get pickerValue() {
1960
+ return this.isColorValue(this.value) ? this.value : '#000000';
1961
+ }
1962
+ writeValue(value) {
1963
+ this.value = value?.trim() || '#000000';
1964
+ this.cdr.markForCheck();
1965
+ }
1966
+ registerOnChange(fn) {
1967
+ this.onChange = fn;
1968
+ }
1969
+ registerOnTouched(fn) {
1970
+ this.onTouched = fn;
1971
+ }
1972
+ setDisabledState(isDisabled) {
1973
+ this.disabled = isDisabled;
1974
+ this.cdr.markForCheck();
1975
+ }
1976
+ onPickerChange(value) {
1977
+ this.value = value;
1978
+ this.onChange(value);
1979
+ }
1980
+ onHexInput(value) {
1981
+ this.value = this.normalizeHexInput(value);
1982
+ this.onChange(this.value);
1983
+ }
1984
+ markTouched() {
1985
+ this.onTouched();
1986
+ }
1987
+ normalizeHexInput(value) {
1988
+ const trimmed = value.trim();
1989
+ if (!trimmed) {
1990
+ return '';
1991
+ }
1992
+ return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
1993
+ }
1994
+ isColorValue(value) {
1995
+ return /^#([0-9a-fA-F]{6})$/.test(value.trim());
1996
+ }
1997
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiColorInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1998
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiColorInputComponent, isStandalone: true, selector: "forms-color-input", inputs: { label: "label", hint: "hint", name: "name", id: "id", required: "required", fullWidth: "fullWidth", error: "error" }, providers: [
1999
+ {
2000
+ provide: NG_VALUE_ACCESSOR,
2001
+ useExisting: forwardRef(() => UiColorInputComponent),
2002
+ multi: true,
2003
+ },
2004
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-sky-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2005
+ }
2006
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiColorInputComponent, decorators: [{
2007
+ type: Component,
2008
+ args: [{ selector: 'forms-color-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
2009
+ {
2010
+ provide: NG_VALUE_ACCESSOR,
2011
+ useExisting: forwardRef(() => UiColorInputComponent),
2012
+ multi: true,
2013
+ },
2014
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-center gap-3\">\n <label\n class=\"relative inline-flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white shadow-sm transition hover:border-slate-300\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n [attr.for]=\"controlId\"\n >\n <span\n class=\"block h-9 w-9 rounded-full border border-black/10\"\n [style.backgroundColor]=\"pickerValue\"\n ></span>\n <input\n class=\"absolute inset-0 h-full w-full cursor-pointer opacity-0\"\n type=\"color\"\n [attr.id]=\"controlId\"\n [attr.name]=\"name ? name + '-picker' : null\"\n [disabled]=\"disabled\"\n [value]=\"pickerValue\"\n (input)=\"onPickerChange($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </label>\n <input\n class=\"min-w-[110px] rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold uppercase text-slate-700 transition focus:border-sky-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60\"\n type=\"text\"\n [attr.name]=\"name\"\n [disabled]=\"disabled\"\n [value]=\"value\"\n (input)=\"onHexInput($event.target.value)\"\n (blur)=\"markTouched()\"\n />\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n" }]
2015
+ }], propDecorators: { label: [{
2016
+ type: Input
2017
+ }], hint: [{
2018
+ type: Input
2019
+ }], name: [{
2020
+ type: Input
2021
+ }], id: [{
2022
+ type: Input
2023
+ }], required: [{
2024
+ type: Input
2025
+ }], fullWidth: [{
2026
+ type: Input
2027
+ }], error: [{
2028
+ type: Input
2029
+ }] } });
2030
+
2031
+ class UiFileInputComponent {
2032
+ label;
2033
+ hint;
2034
+ accept;
2035
+ multiple = false;
2036
+ disabled = false;
2037
+ value = null;
2038
+ valueChange = new EventEmitter();
2039
+ filesChange = new EventEmitter();
2040
+ get selectedFiles() {
2041
+ if (this.value instanceof File)
2042
+ return [this.value];
2043
+ return Array.isArray(this.value) ? this.value.filter((item) => item instanceof File) : [];
2044
+ }
2045
+ onFilesSelected(event) {
2046
+ const input = event.target;
2047
+ const files = Array.from(input.files ?? []);
2048
+ const value = this.multiple ? files : (files[0] ?? null);
2049
+ this.valueChange.emit(value);
2050
+ this.filesChange.emit(files);
2051
+ input.value = '';
2052
+ }
2053
+ remove(index) {
2054
+ const files = this.selectedFiles.filter((_file, fileIndex) => fileIndex !== index);
2055
+ const value = this.multiple ? files : (files[0] ?? null);
2056
+ this.valueChange.emit(value);
2057
+ this.filesChange.emit(files);
2058
+ }
2059
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2060
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiFileInputComponent, isStandalone: true, selector: "forms-file-input", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", disabled: "disabled", value: "value" }, outputs: { valueChange: "valueChange", filesChange: "filesChange" }, ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
2061
+ }
2062
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiFileInputComponent, decorators: [{
2063
+ type: Component,
2064
+ args: [{ selector: 'forms-file-input', standalone: true, imports: [CommonModule], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <input\n type=\"file\"\n class=\"rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm normal-case tracking-normal text-slate-800 file:mr-3 file:rounded-lg file:border-0 file:bg-slate-900 file:px-3 file:py-2 file:text-sm file:font-medium file:text-white\"\n [attr.accept]=\"accept ?? null\"\n [attr.multiple]=\"multiple ? '' : null\"\n [disabled]=\"disabled\"\n (change)=\"onFilesSelected($event)\"\n />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (selectedFiles.length) {\n <div class=\"flex flex-col gap-1 text-[0.7rem] font-normal normal-case tracking-normal text-slate-700\">\n @for (file of selectedFiles; track file.name + file.size; let i = $index) {\n <div class=\"flex items-center justify-between rounded border border-slate-200 bg-slate-50 px-2 py-1\">\n <span class=\"truncate pr-2\">{{ file.name }}</span>\n <button type=\"button\" class=\"text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
2065
+ }], propDecorators: { label: [{
2066
+ type: Input
2067
+ }], hint: [{
2068
+ type: Input
2069
+ }], accept: [{
2070
+ type: Input
2071
+ }], multiple: [{
2072
+ type: Input
2073
+ }], disabled: [{
2074
+ type: Input
2075
+ }], value: [{
2076
+ type: Input
2077
+ }], valueChange: [{
2078
+ type: Output
2079
+ }], filesChange: [{
2080
+ type: Output
2081
+ }] } });
2082
+
2083
+ class UiInputComponent {
2084
+ cdr = inject(ChangeDetectorRef);
2085
+ label;
2086
+ hint;
2087
+ placeholder;
2088
+ name;
2089
+ id;
2090
+ type = 'text';
2091
+ element = 'input';
2092
+ rows = 4;
2093
+ required = false;
2094
+ fullWidth = false;
2095
+ autocomplete;
2096
+ readonly = false;
2097
+ pattern;
2098
+ error;
2099
+ actions = [];
2100
+ disabled = false;
2101
+ value = '';
2102
+ static nextId = 0;
2103
+ generatedId = `ui-input-${UiInputComponent.nextId++}`;
2104
+ actionTriggered = new EventEmitter();
2105
+ get controlId() {
2106
+ return this.id ?? this.generatedId;
2107
+ }
2108
+ onChange = () => undefined;
2109
+ onTouched = () => undefined;
2110
+ writeValue(value) {
2111
+ this.value = value ?? '';
2112
+ this.cdr.markForCheck();
2113
+ }
2114
+ registerOnChange(fn) {
2115
+ this.onChange = fn;
2116
+ }
2117
+ registerOnTouched(fn) {
2118
+ this.onTouched = fn;
2119
+ }
2120
+ setDisabledState(isDisabled) {
2121
+ this.disabled = isDisabled;
2122
+ this.cdr.markForCheck();
2123
+ }
2124
+ onValueChange(value) {
2125
+ const parsed = this.type === 'number' ? (value === '' ? null : Number(value)) : value;
2126
+ this.value = parsed;
2127
+ this.onChange(parsed);
2128
+ }
2129
+ triggerAction(action) {
2130
+ this.actionTriggered.emit(action);
2131
+ }
2132
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2133
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiInputComponent, isStandalone: true, selector: "forms-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", type: "type", element: "element", rows: "rows", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", pattern: "pattern", error: "error", actions: "actions" }, outputs: { actionTriggered: "actionTriggered" }, providers: [
2134
+ {
2135
+ provide: NG_VALUE_ACCESSOR,
2136
+ useExisting: forwardRef(() => UiInputComponent),
2137
+ multi: true,
2138
+ },
2139
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: UiButtonComponent, selector: "ui-button", inputs: ["variant", "size", "disabled", "fullWidth", "type", "withShadow", "icon", "iconOnly", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2140
+ }
2141
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiInputComponent, decorators: [{
2142
+ type: Component,
2143
+ args: [{ selector: 'forms-input', standalone: true, imports: [CommonModule, FormsModule, UiButtonComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
2144
+ {
2145
+ provide: NG_VALUE_ACCESSOR,
2146
+ useExisting: forwardRef(() => UiInputComponent),
2147
+ multi: true,
2148
+ },
2149
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <div class=\"flex items-end gap-2\">\n <div class=\"min-w-0 flex-1\">\n @if (element === 'textarea') {\n <textarea\n [attr.rows]=\"rows\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.id]=\"controlId\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n ></textarea>\n } @else {\n <input\n [attr.type]=\"type\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [attr.pattern]=\"pattern\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"w-full rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target).value)\"\n (blur)=\"onTouched()\"\n />\n }\n </div>\n\n @if (actions.length) {\n <div class=\"flex flex-none items-center gap-2 pb-0.5\">\n @for (action of actions; track action.id) {\n <ui-button\n type=\"button\"\n [variant]=\"action.variant ?? 'secondary'\"\n [icon]=\"action.icon || ''\"\n size=\"sm\"\n [disabled]=\"disabled\"\n (click)=\"triggerAction(action)\"\n >\n {{ action.label }}\n </ui-button>\n }\n </div>\n }\n </div>\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n\n" }]
2150
+ }], propDecorators: { label: [{
2151
+ type: Input
2152
+ }], hint: [{
2153
+ type: Input
2154
+ }], placeholder: [{
2155
+ type: Input
2156
+ }], name: [{
2157
+ type: Input
2158
+ }], id: [{
2159
+ type: Input
2160
+ }], type: [{
2161
+ type: Input
2162
+ }], element: [{
2163
+ type: Input
2164
+ }], rows: [{
2165
+ type: Input
2166
+ }], required: [{
2167
+ type: Input
2168
+ }], fullWidth: [{
2169
+ type: Input
2170
+ }], autocomplete: [{
2171
+ type: Input
2172
+ }], readonly: [{
2173
+ type: Input
2174
+ }], pattern: [{
2175
+ type: Input
2176
+ }], error: [{
2177
+ type: Input
2178
+ }], actions: [{
2179
+ type: Input
2180
+ }], actionTriggered: [{
2181
+ type: Output
2182
+ }] } });
2183
+
2184
+ class UiPhotoInputComponent {
2185
+ label;
2186
+ hint;
2187
+ accept = 'image/*';
2188
+ multiple = false;
2189
+ limit = Number.MAX_SAFE_INTEGER;
2190
+ disabled = false;
2191
+ value = null;
2192
+ uploadPhoto = null;
2193
+ valueChange = new EventEmitter();
2194
+ filesChange = new EventEmitter();
2195
+ uploadingChange = new EventEmitter();
2196
+ pendingFiles = [];
2197
+ previews = [];
2198
+ uploading = false;
2199
+ fileTypes = new Map();
2200
+ ngOnChanges(changes) {
2201
+ if (changes['value'] || changes['multiple'])
2202
+ this.rebuildPreviews();
2203
+ }
2204
+ ngOnDestroy() {
2205
+ this.revokeBlobUrls();
2206
+ }
2207
+ get canSelectMore() {
2208
+ return !this.multiple || this.ids.length + this.pendingFiles.length < this.normalizedLimit;
2209
+ }
2210
+ onFilesSelected(event) {
2211
+ const input = event.target;
2212
+ const files = Array.from(input.files ?? []);
2213
+ input.value = '';
2214
+ if (!files.length || this.disabled)
2215
+ return;
2216
+ const accepted = this.multiple ? files.slice(0, Math.max(0, this.normalizedLimit - this.ids.length - this.pendingFiles.length)) : [files[0]];
2217
+ this.pendingFiles = this.multiple ? [...this.pendingFiles, ...accepted] : [accepted[0]];
2218
+ this.filesChange.emit(this.pendingFiles);
2219
+ this.rebuildPreviews();
2220
+ if (this.uploadPhoto)
2221
+ this.uploadFiles(accepted);
2222
+ }
2223
+ remove(index) {
2224
+ const ids = [...this.ids];
2225
+ if (index < ids.length) {
2226
+ ids.splice(index, 1);
2227
+ this.emitValue(this.multiple ? ids : (ids[0] ?? null));
2228
+ }
2229
+ else {
2230
+ this.pendingFiles.splice(index - ids.length, 1);
2231
+ this.filesChange.emit(this.pendingFiles);
2232
+ this.rebuildPreviews();
2233
+ }
2234
+ }
2235
+ drop(event) {
2236
+ if (this.disabled || this.pendingFiles.length || this.ids.length < 2)
2237
+ return;
2238
+ const ids = [...this.ids];
2239
+ moveItemInArray(ids, event.previousIndex, event.currentIndex);
2240
+ this.emitValue(this.multiple ? ids : (ids[0] ?? null));
2241
+ }
2242
+ get ids() {
2243
+ if (typeof this.value === 'string')
2244
+ return this.value.trim() ? [this.value] : [];
2245
+ return Array.isArray(this.value) ? this.value.filter((item) => !!String(item || '').trim()) : [];
2246
+ }
2247
+ get normalizedLimit() {
2248
+ return Number.isInteger(Number(this.limit)) && Number(this.limit) > 0 ? Number(this.limit) : Number.MAX_SAFE_INTEGER;
2249
+ }
2250
+ emitValue(value) {
2251
+ this.valueChange.emit(value);
2252
+ this.rebuildPreviews(value);
2253
+ }
2254
+ uploadFiles(files) {
2255
+ if (!this.uploadPhoto || !files.length)
2256
+ return;
2257
+ this.uploading = true;
2258
+ this.uploadingChange.emit(true);
2259
+ forkJoin(files.map((file) => this.uploadPhoto(file))).subscribe({
2260
+ next: (results) => {
2261
+ const uploaded = results.map((item) => String(item.imageId || '').trim()).filter(Boolean);
2262
+ results.forEach((item) => {
2263
+ const id = String(item.imageId || '').trim();
2264
+ if (id && item.type)
2265
+ this.fileTypes.set(id, item.type);
2266
+ });
2267
+ const next = this.multiple ? [...this.ids, ...uploaded].slice(0, this.normalizedLimit) : (uploaded[0] ?? null);
2268
+ this.pendingFiles = this.pendingFiles.filter((file) => !files.includes(file));
2269
+ this.emitValue(next);
2270
+ this.filesChange.emit(this.pendingFiles);
2271
+ this.uploading = false;
2272
+ this.uploadingChange.emit(false);
2273
+ },
2274
+ error: () => {
2275
+ this.pendingFiles = this.pendingFiles.filter((file) => !files.includes(file));
2276
+ this.rebuildPreviews();
2277
+ this.uploading = false;
2278
+ this.uploadingChange.emit(false);
2279
+ },
2280
+ });
2281
+ }
2282
+ rebuildPreviews(value = this.value) {
2283
+ this.revokeBlobUrls();
2284
+ const ids = typeof value === 'string' ? (value.trim() ? [value] : []) : (Array.isArray(value) ? value : []);
2285
+ this.previews = [
2286
+ ...ids.map((id) => ({ name: id, url: this.resolveImageUrl(id), isImage: this.isPreviewableType(this.fileTypes.get(id)), source: 'id' })),
2287
+ ...this.pendingFiles.map((file) => ({ name: file.name, url: URL.createObjectURL(file), isImage: file.type.startsWith('image/'), source: 'file' })),
2288
+ ];
2289
+ }
2290
+ revokeBlobUrls() {
2291
+ for (const preview of this.previews)
2292
+ if (preview.url.startsWith('blob:'))
2293
+ URL.revokeObjectURL(preview.url);
2294
+ }
2295
+ resolveImageUrl(value) {
2296
+ if (/^(https?:\/\/|\/|data:)/i.test(value))
2297
+ return value;
2298
+ return `/api/files/${encodeURIComponent(value)}/preview?scale=25`;
2299
+ }
2300
+ isPreviewableType(value) {
2301
+ if (!value)
2302
+ return true;
2303
+ const normalized = value.toLowerCase();
2304
+ return normalized.startsWith('image/') || normalized === 'application/pdf';
2305
+ }
2306
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2307
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPhotoInputComponent, isStandalone: true, selector: "forms-photo-input", inputs: { label: "label", hint: "hint", accept: "accept", multiple: "multiple", limit: "limit", disabled: "disabled", value: "value", uploadPhoto: "uploadPhoto" }, outputs: { valueChange: "valueChange", filesChange: "filesChange", uploadingChange: "uploadingChange" }, usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
2308
+ }
2309
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoInputComponent, decorators: [{
2310
+ type: Component,
2311
+ args: [{ selector: 'forms-photo-input', standalone: true, imports: [CommonModule, DragDropModule], template: "<label class=\"flex flex-col gap-2 text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-slate-600\">\n @if (label) { <span>{{ label }}</span> }\n <button type=\"button\" class=\"flex min-h-24 items-center justify-center rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-3 text-xs font-semibold uppercase tracking-[0.15em] text-slate-600 transition hover:bg-slate-100 disabled:opacity-60\" [disabled]=\"disabled || !canSelectMore\" (click)=\"photoInput.click()\">\n {{ previews.length ? '\u0414\u043E\u0434\u0430\u0442\u0438 \u0449\u0435 \u0444\u0430\u0439\u043B' : '\u041E\u0431\u0440\u0430\u0442\u0438 \u0444\u0430\u0439\u043B' }}\n </button>\n <input #photoInput type=\"file\" class=\"hidden\" [attr.accept]=\"accept\" [attr.multiple]=\"multiple && canSelectMore ? '' : null\" [disabled]=\"disabled\" (change)=\"onFilesSelected($event)\" />\n @if (hint) { <span class=\"text-[0.7rem] font-normal normal-case tracking-normal text-slate-500\">{{ hint }}</span> }\n @if (previews.length) {\n <div class=\"grid grid-cols-2 gap-2 md:grid-cols-4\" cdkDropList cdkDropListOrientation=\"mixed\" [cdkDropListData]=\"previews\" (cdkDropListDropped)=\"drop($event)\">\n @for (preview of previews; track preview.url; let i = $index) {\n <div class=\"relative overflow-hidden rounded-lg border border-slate-200 bg-slate-50\" cdkDrag [cdkDragDisabled]=\"disabled || preview.source === 'file'\">\n @if (preview.isImage) { <img [src]=\"preview.url\" [alt]=\"preview.name\" class=\"h-28 w-full object-cover\" /> }\n @else { <div class=\"flex h-28 items-center justify-center px-2 text-center text-[0.65rem] text-slate-600\">{{ preview.name }}</div> }\n <div class=\"absolute left-1 top-1 rounded bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</div>\n <button type=\"button\" class=\"absolute right-1 top-1 rounded bg-white/95 px-2 py-1 text-[10px] text-rose-600\" [disabled]=\"disabled\" (click)=\"remove(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n </div>\n }\n </div>\n }\n</label>\n" }]
2312
+ }], propDecorators: { label: [{
2313
+ type: Input
2314
+ }], hint: [{
2315
+ type: Input
2316
+ }], accept: [{
2317
+ type: Input
2318
+ }], multiple: [{
2319
+ type: Input
2320
+ }], limit: [{
2321
+ type: Input
2322
+ }], disabled: [{
2323
+ type: Input
2324
+ }], value: [{
2325
+ type: Input
2326
+ }], uploadPhoto: [{
2327
+ type: Input
2328
+ }], valueChange: [{
2329
+ type: Output
2330
+ }], filesChange: [{
2331
+ type: Output
2332
+ }], uploadingChange: [{
2333
+ type: Output
2334
+ }] } });
2335
+
2336
+ class UiPhotoGalleryFieldComponent {
2337
+ label = 'Фото';
2338
+ orderLabel = 'Порядок фото (перетягуй)';
2339
+ imageIds = [];
2340
+ disabled = false;
2341
+ afterUpload = null;
2342
+ uploadPhoto = null;
2343
+ resolveImageUrl = null;
2344
+ notify = null;
2345
+ imageIdsChange = new EventEmitter();
2346
+ entries = [];
2347
+ imageEntrySequence = 0;
2348
+ get photoCount() {
2349
+ return this.entries.filter((entry) => !!entry.imageId || !!entry.file).length;
2350
+ }
2351
+ ngOnInit() {
2352
+ if (!this.entries.length)
2353
+ this.entries = this.normalizeImageEntries([]);
2354
+ }
2355
+ ngOnChanges(changes) {
2356
+ if (!changes['imageIds'])
2357
+ return;
2358
+ const nextIds = this.imageIds ?? [];
2359
+ const currentIds = this.entries.filter((entry) => !!entry.imageId).map((entry) => entry.imageId);
2360
+ const hasPendingLocalFiles = this.entries.some((entry) => !!entry.file || !!entry.isUploading);
2361
+ const sameIds = nextIds.length === currentIds.length && nextIds.every((id, idx) => id === currentIds[idx]);
2362
+ if (sameIds || hasPendingLocalFiles)
2363
+ return;
2364
+ this.entries = this.normalizeImageEntries(nextIds.map((imageId) => ({
2365
+ clientKey: imageId,
2366
+ imageId,
2367
+ preview: this.resolveImageUrl?.(imageId) || '',
2368
+ })));
2369
+ }
2370
+ ngOnDestroy() {
2371
+ this.entries.forEach((entry) => {
2372
+ if (entry.file && entry.preview)
2373
+ URL.revokeObjectURL(entry.preview);
2374
+ });
2375
+ }
2376
+ onFilesChange(event, index) {
2377
+ const input = event.target;
2378
+ const files = Array.from(input.files ?? []);
2379
+ if (this.disabled || !files.length)
2380
+ return;
2381
+ const next = [...this.entries];
2382
+ const baseIndex = Math.max(0, Math.min(index, next.length - 1));
2383
+ files.forEach((file, offset) => {
2384
+ const targetIndex = baseIndex + offset;
2385
+ const existing = next[targetIndex];
2386
+ if (existing?.file && existing.preview)
2387
+ URL.revokeObjectURL(existing.preview);
2388
+ next[targetIndex] = {
2389
+ clientKey: existing?.clientKey ?? `local-${this.imageEntrySequence++}`,
2390
+ file,
2391
+ imageId: existing?.imageId,
2392
+ preview: URL.createObjectURL(file),
2393
+ };
2394
+ });
2395
+ this.entries = this.normalizeImageEntries(next);
2396
+ this.uploadPendingImages();
2397
+ input.value = '';
2398
+ }
2399
+ openFilePicker(input, event) {
2400
+ event?.preventDefault();
2401
+ event?.stopPropagation();
2402
+ if (!this.disabled && input)
2403
+ input.click();
2404
+ }
2405
+ onDrop(event) {
2406
+ if (this.disabled)
2407
+ return;
2408
+ const next = [...this.entries];
2409
+ const nonEmptyCount = next.filter((entry) => entry.imageId || entry.file).length;
2410
+ if (event.previousIndex >= nonEmptyCount || event.currentIndex >= nonEmptyCount) {
2411
+ this.entries = this.normalizeImageEntries(next);
2412
+ return;
2413
+ }
2414
+ moveItemInArray(next, event.previousIndex, event.currentIndex);
2415
+ this.entries = this.normalizeImageEntries(next);
2416
+ this.emitImageIds();
2417
+ }
2418
+ removeAt(index) {
2419
+ if (this.disabled || index < 0 || index >= this.entries.length)
2420
+ return;
2421
+ const next = [...this.entries];
2422
+ const [removed] = next.splice(index, 1);
2423
+ if (removed?.file && removed.preview)
2424
+ URL.revokeObjectURL(removed.preview);
2425
+ this.entries = this.normalizeImageEntries(next);
2426
+ this.emitImageIds();
2427
+ }
2428
+ createImageEntry() {
2429
+ return { clientKey: `local-${this.imageEntrySequence++}` };
2430
+ }
2431
+ normalizeImageEntries(entries) {
2432
+ const nonEmpty = entries.filter((entry) => !!entry?.imageId || !!entry?.file);
2433
+ return nonEmpty.length ? [...nonEmpty, this.createImageEntry()] : [this.createImageEntry()];
2434
+ }
2435
+ emitImageIds() {
2436
+ this.imageIdsChange.emit(this.entries.filter((entry) => !!entry.imageId).map((entry) => entry.imageId));
2437
+ }
2438
+ uploadPendingImages() {
2439
+ const nextIndex = this.entries.findIndex((entry) => !!entry.file && !entry.isUploading);
2440
+ if (nextIndex < 0 || !this.uploadPhoto)
2441
+ return;
2442
+ const entry = this.entries[nextIndex];
2443
+ entry.isUploading = true;
2444
+ const file = entry.file;
2445
+ this.uploadPhoto(file).subscribe({
2446
+ next: (response) => {
2447
+ const persist$ = this.afterUpload ? this.afterUpload(response.imageId) : of(null);
2448
+ persist$.subscribe({
2449
+ next: () => {
2450
+ entry.imageId = response.imageId;
2451
+ entry.file = undefined;
2452
+ entry.isUploading = undefined;
2453
+ entry.preview = this.resolveImageUrl?.(response.imageId) || entry.preview;
2454
+ this.entries = this.normalizeImageEntries([...this.entries]);
2455
+ this.emitImageIds();
2456
+ this.notify?.('Фото завантажено', 'success', 2000);
2457
+ this.uploadPendingImages();
2458
+ },
2459
+ error: (error) => {
2460
+ entry.isUploading = undefined;
2461
+ this.notify?.(error?.error?.message || 'Не вдалося привʼязати фото', 'error', 3500);
2462
+ this.uploadPendingImages();
2463
+ },
2464
+ });
2465
+ },
2466
+ error: (error) => {
2467
+ entry.isUploading = undefined;
2468
+ this.notify?.(error?.error?.message || 'Не вдалося завантажити фото', 'error', 3500);
2469
+ this.uploadPendingImages();
2470
+ },
2471
+ });
2472
+ }
2473
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoGalleryFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2474
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiPhotoGalleryFieldComponent, isStandalone: true, selector: "forms-photo-gallery-field", inputs: { label: "label", orderLabel: "orderLabel", imageIds: "imageIds", disabled: "disabled", afterUpload: "afterUpload", uploadPhoto: "uploadPhoto", resolveImageUrl: "resolveImageUrl", notify: "notify" }, outputs: { imageIdsChange: "imageIdsChange" }, usesOnChanges: true, ngImport: i0, template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }] });
2475
+ }
2476
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiPhotoGalleryFieldComponent, decorators: [{
2477
+ type: Component,
2478
+ args: [{ selector: 'forms-photo-gallery-field', standalone: true, imports: [CommonModule, DragDropModule], template: "<label class=\"flex flex-col gap-1 md:col-span-12\">\n <span class=\"text-sm\">{{ label }}</span>\n <div class=\"text-xs text-slate-500\">\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0444\u043E\u0442\u043E: {{ photoCount }}</div>\n</label>\n\n<div class=\"md:col-span-12\">\n <p class=\"mb-2 text-xs uppercase tracking-[0.2em] text-slate-500\">{{ orderLabel }}</p>\n <div class=\"grid gap-3 md:grid-cols-4\" cdkDropList [cdkDropListData]=\"entries\" (cdkDropListDropped)=\"onDrop($event)\">\n @for (entry of entries; let i = $index; track entry.clientKey + '-' + i) {\n <div class=\"image-drag-item relative overflow-hidden rounded-xl border border-slate-200 bg-white\" cdkDrag [cdkDragDisabled]=\"disabled || (!entry.imageId && !entry.file)\">\n <label class=\"block h-full w-full cursor-pointer\">\n @if (entry.preview) {\n <img [src]=\"entry.preview\" alt=\"photo\" class=\"h-48 w-full object-cover\" />\n } @else {\n <button type=\"button\" class=\"flex h-48 w-full items-center justify-center bg-slate-50 text-xs text-slate-500\" (click)=\"openFilePicker(fileInput, $event)\">+ \u0414\u043E\u0434\u0430\u0442\u0438 \u0444\u043E\u0442\u043E</button>\n }\n <input #fileInput type=\"file\" class=\"hidden\" [disabled]=\"disabled\" multiple (change)=\"onFilesChange($event, i)\" />\n </label>\n @if (entry.imageId || entry.file) {\n @if (i === 0) {\n <span class=\"absolute left-2 top-2 rounded-full border border-slate-200 bg-white/95 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.15em] text-slate-700\">\u041E\u0431\u043A\u043B\u0430\u0434\u0438\u043D\u043A\u0430</span>\n }\n <span class=\"absolute left-2 bottom-2 rounded-full bg-slate-900/85 px-2 py-1 text-[10px] font-semibold text-white\">#{{ i + 1 }}</span>\n <button type=\"button\" class=\"absolute right-2 top-2 rounded-full bg-white/95 px-2 py-1 text-xs text-rose-600 shadow\" [disabled]=\"disabled\" (click)=\"removeAt(i)\">\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438</button>\n }\n </div>\n }\n </div>\n</div>\n", styles: [".image-drag-item.cdk-drag-preview{transform:scale(1.03) rotate(.6deg);box-shadow:0 28px 60px #0f172a66;border-radius:.9rem}.image-drag-item.cdk-drag-placeholder{opacity:.4;outline:2px dashed #94a3b8;outline-offset:4px}\n"] }]
2479
+ }], propDecorators: { label: [{
2480
+ type: Input
2481
+ }], orderLabel: [{
2482
+ type: Input
2483
+ }], imageIds: [{
2484
+ type: Input
2485
+ }], disabled: [{
2486
+ type: Input
2487
+ }], afterUpload: [{
2488
+ type: Input
2489
+ }], uploadPhoto: [{
2490
+ type: Input
2491
+ }], resolveImageUrl: [{
2492
+ type: Input
2493
+ }], notify: [{
2494
+ type: Input
2495
+ }], imageIdsChange: [{
2496
+ type: Output
2497
+ }] } });
2498
+
2499
+ class UiRangeSliderComponent {
2500
+ min = 0;
2501
+ max = 100;
2502
+ step = 1;
2503
+ startValue = null;
2504
+ endValue = null;
2505
+ startLabel = 'Мінімальне значення';
2506
+ endLabel = 'Максимальне значення';
2507
+ startValueChange = new EventEmitter();
2508
+ endValueChange = new EventEmitter();
2509
+ interactionEnd = new EventEmitter();
2510
+ startInput;
2511
+ endInput;
2512
+ get safeStart() {
2513
+ const min = this.safeMin;
2514
+ const max = this.safeMax;
2515
+ const step = this.safeStep;
2516
+ const start = this.toFiniteNumber(this.startValue, min);
2517
+ const end = this.toFiniteNumber(this.endValue, max);
2518
+ return Math.max(min, Math.min(start, end - step));
2519
+ }
2520
+ get safeEnd() {
2521
+ const min = this.safeMin;
2522
+ const max = this.safeMax;
2523
+ const step = this.safeStep;
2524
+ const start = this.toFiniteNumber(this.startValue, min);
2525
+ const end = this.toFiniteNumber(this.endValue, max);
2526
+ return Math.min(max, Math.max(end, start + step));
2527
+ }
2528
+ get startPercent() {
2529
+ return this.toPercent(this.safeStart);
2530
+ }
2531
+ get endPercent() {
2532
+ return this.toPercent(this.safeEnd);
2533
+ }
2534
+ get rangePercent() {
2535
+ return Math.max(0, this.endPercent - this.startPercent);
2536
+ }
2537
+ get safeMin() {
2538
+ return this.toFiniteNumber(this.min, 0);
2539
+ }
2540
+ get safeMax() {
2541
+ const min = this.safeMin;
2542
+ const max = this.toFiniteNumber(this.max, min + this.safeStep);
2543
+ return max > min ? max : min + this.safeStep;
2544
+ }
2545
+ get safeStep() {
2546
+ const step = this.toFiniteNumber(this.step, 1);
2547
+ return step > 0 ? step : 1;
2548
+ }
2549
+ ngOnChanges(_) {
2550
+ this.syncNativeInputs();
2551
+ }
2552
+ ngAfterViewInit() {
2553
+ this.syncNativeInputs();
2554
+ }
2555
+ onStartInput(event) {
2556
+ const value = Number(event.target.value);
2557
+ this.startValueChange.emit(Math.min(value, this.safeEnd - this.safeStep));
2558
+ }
2559
+ onEndInput(event) {
2560
+ const value = Number(event.target.value);
2561
+ this.endValueChange.emit(Math.max(value, this.safeStart + this.safeStep));
2562
+ }
2563
+ toPercent(value) {
2564
+ const span = this.safeMax - this.safeMin;
2565
+ if (span <= 0) {
2566
+ return 0;
2567
+ }
2568
+ return ((value - this.safeMin) / span) * 100;
2569
+ }
2570
+ toFiniteNumber(value, fallback) {
2571
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
2572
+ }
2573
+ syncNativeInputs() {
2574
+ queueMicrotask(() => {
2575
+ this.syncNativeInput(this.startInput?.nativeElement, this.safeStart);
2576
+ this.syncNativeInput(this.endInput?.nativeElement, this.safeEnd);
2577
+ });
2578
+ }
2579
+ syncNativeInput(input, value) {
2580
+ if (!input) {
2581
+ return;
2582
+ }
2583
+ input.min = String(this.safeMin);
2584
+ input.max = String(this.safeMax);
2585
+ input.step = String(this.safeStep);
2586
+ input.value = String(value);
2587
+ }
2588
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiRangeSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2589
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.19", type: UiRangeSliderComponent, isStandalone: true, selector: "forms-range-slider", inputs: { min: "min", max: "max", step: "step", startValue: "startValue", endValue: "endValue", startLabel: "startLabel", endLabel: "endLabel" }, outputs: { startValueChange: "startValueChange", endValueChange: "endValueChange", interactionEnd: "interactionEnd" }, viewQueries: [{ propertyName: "startInput", first: true, predicate: ["startInput"], descendants: true }, { propertyName: "endInput", first: true, predicate: ["endInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
2590
+ <div class="range-slider">
2591
+ <div class="range-slider__track"></div>
2592
+ <div
2593
+ class="range-slider__range"
2594
+ [style.left.%]="startPercent"
2595
+ [style.width.%]="rangePercent">
2596
+ </div>
2597
+
2598
+ <input
2599
+ #startInput
2600
+ class="range-slider__input"
2601
+ type="range"
2602
+ [min]="safeMin"
2603
+ [max]="safeMax"
2604
+ [step]="safeStep"
2605
+ [value]="safeStart"
2606
+ [attr.aria-label]="startLabel"
2607
+ (input)="onStartInput($event)"
2608
+ (change)="interactionEnd.emit()">
2609
+
2610
+ <input
2611
+ #endInput
2612
+ class="range-slider__input"
2613
+ type="range"
2614
+ [min]="safeMin"
2615
+ [max]="safeMax"
2616
+ [step]="safeStep"
2617
+ [value]="safeEnd"
2618
+ [attr.aria-label]="endLabel"
2619
+ (input)="onEndInput($event)"
2620
+ (change)="interactionEnd.emit()">
2621
+ </div>
2622
+ `, isInline: true, styles: [".range-slider{position:relative;height:2rem;width:100%;display:flex;align-items:center}.range-slider__track,.range-slider__range{position:absolute;left:0;right:0;height:6px;border-radius:999px}.range-slider__track{background:#dbe1ea}.range-slider__range{background:linear-gradient(90deg,#334155,#0f172a)}.range-slider__input{pointer-events:none;position:absolute;left:0;width:100%;height:2rem;margin:0;background:transparent;appearance:none;-webkit-appearance:none}.range-slider__input::-webkit-slider-runnable-track{height:6px;background:transparent}.range-slider__input::-moz-range-track{height:6px;background:transparent}.range-slider__input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;pointer-events:auto;width:20px;height:20px;margin-top:-7px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}.range-slider__input::-moz-range-thumb{pointer-events:auto;width:20px;height:20px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
2623
+ }
2624
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiRangeSliderComponent, decorators: [{
2625
+ type: Component,
2626
+ args: [{ selector: 'forms-range-slider', standalone: true, imports: [CommonModule], template: `
2627
+ <div class="range-slider">
2628
+ <div class="range-slider__track"></div>
2629
+ <div
2630
+ class="range-slider__range"
2631
+ [style.left.%]="startPercent"
2632
+ [style.width.%]="rangePercent">
2633
+ </div>
2634
+
2635
+ <input
2636
+ #startInput
2637
+ class="range-slider__input"
2638
+ type="range"
2639
+ [min]="safeMin"
2640
+ [max]="safeMax"
2641
+ [step]="safeStep"
2642
+ [value]="safeStart"
2643
+ [attr.aria-label]="startLabel"
2644
+ (input)="onStartInput($event)"
2645
+ (change)="interactionEnd.emit()">
2646
+
2647
+ <input
2648
+ #endInput
2649
+ class="range-slider__input"
2650
+ type="range"
2651
+ [min]="safeMin"
2652
+ [max]="safeMax"
2653
+ [step]="safeStep"
2654
+ [value]="safeEnd"
2655
+ [attr.aria-label]="endLabel"
2656
+ (input)="onEndInput($event)"
2657
+ (change)="interactionEnd.emit()">
2658
+ </div>
2659
+ `, styles: [".range-slider{position:relative;height:2rem;width:100%;display:flex;align-items:center}.range-slider__track,.range-slider__range{position:absolute;left:0;right:0;height:6px;border-radius:999px}.range-slider__track{background:#dbe1ea}.range-slider__range{background:linear-gradient(90deg,#334155,#0f172a)}.range-slider__input{pointer-events:none;position:absolute;left:0;width:100%;height:2rem;margin:0;background:transparent;appearance:none;-webkit-appearance:none}.range-slider__input::-webkit-slider-runnable-track{height:6px;background:transparent}.range-slider__input::-moz-range-track{height:6px;background:transparent}.range-slider__input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;pointer-events:auto;width:20px;height:20px;margin-top:-7px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}.range-slider__input::-moz-range-thumb{pointer-events:auto;width:20px;height:20px;border-radius:50%;border:2px solid #ffffff;background:#0f172a;box-shadow:0 8px 22px #0f172a38;cursor:pointer}\n"] }]
2660
+ }], propDecorators: { min: [{
2661
+ type: Input
2662
+ }], max: [{
2663
+ type: Input
2664
+ }], step: [{
2665
+ type: Input
2666
+ }], startValue: [{
2667
+ type: Input
2668
+ }], endValue: [{
2669
+ type: Input
2670
+ }], startLabel: [{
2671
+ type: Input
2672
+ }], endLabel: [{
2673
+ type: Input
2674
+ }], startValueChange: [{
2675
+ type: Output
2676
+ }], endValueChange: [{
2677
+ type: Output
2678
+ }], interactionEnd: [{
2679
+ type: Output
2680
+ }], startInput: [{
2681
+ type: ViewChild,
2682
+ args: ['startInput']
2683
+ }], endInput: [{
2684
+ type: ViewChild,
2685
+ args: ['endInput']
2686
+ }] } });
2687
+
2688
+ class UiSelectComponent {
2689
+ closeIcon = X;
2690
+ chevronDownIcon = ChevronDown;
2691
+ checkIcon = Check;
2692
+ static openInstance = null;
2693
+ cdr = inject(ChangeDetectorRef);
2694
+ destroyRef = inject(DestroyRef);
2695
+ http = inject(HttpClient);
2696
+ label;
2697
+ placeholder;
2698
+ name;
2699
+ data;
2700
+ required = false;
2701
+ fullWidth = false;
2702
+ disabled = false;
2703
+ multiple = false;
2704
+ includeBlankOption = false;
2705
+ blankOptionLabel = 'Оберіть…';
2706
+ emptyStateLabel = 'Немає варіантів для вибору';
2707
+ searchable = true;
2708
+ remoteSearch = false;
2709
+ searchPlaceholder = 'Пошук...';
2710
+ _options = [];
2711
+ set options(value) {
2712
+ this._options = value ?? [];
2713
+ this.updateSelectionFromValue();
2714
+ this.cdr.markForCheck();
2715
+ }
2716
+ get options() {
2717
+ return this._options;
2718
+ }
2719
+ error;
2720
+ hint;
2721
+ triggerClass = '';
2722
+ panelClass = '';
2723
+ valueChange = new EventEmitter();
2724
+ searchChange = new EventEmitter();
2725
+ dropdownTrigger;
2726
+ dropdownPositions = [
2727
+ {
2728
+ originX: 'start',
2729
+ originY: 'bottom',
2730
+ overlayX: 'start',
2731
+ overlayY: 'top',
2732
+ offsetY: 4,
2733
+ },
2734
+ {
2735
+ originX: 'start',
2736
+ originY: 'top',
2737
+ overlayX: 'start',
2738
+ overlayY: 'bottom',
2739
+ offsetY: -4,
2740
+ },
2741
+ ];
2742
+ isOpen = false;
2743
+ triggerWidth = 0;
2744
+ value = '';
2745
+ multiValue = [];
2746
+ selectedOptions = [];
2747
+ searchTerm = '';
2748
+ isDictionaryLoading = false;
2749
+ searchChanges$ = new Subject();
2750
+ dictionaryRequestVersions = new Map();
2751
+ dictionaryOptions = [];
2752
+ onChange = () => undefined;
2753
+ onTouched = () => undefined;
2754
+ constructor() {
2755
+ this.searchChanges$.pipe(debounceTime(300), distinctUntilChanged()).subscribe((next) => {
2756
+ this.searchTerm = next;
2757
+ if (this.getDictionaryName()) {
2758
+ this.loadDictionaryOptions(next);
2759
+ return;
2760
+ }
2761
+ this.updateSelectionFromValue();
2762
+ this.cdr.markForCheck();
2763
+ });
2764
+ this.destroyRef.onDestroy(() => {
2765
+ this.searchChanges$.complete();
2766
+ this.dictionaryRequestVersions.clear();
2767
+ if (UiSelectComponent.openInstance === this) {
2768
+ UiSelectComponent.openInstance = null;
2769
+ }
2770
+ });
2771
+ }
2772
+ ngOnChanges(changes) {
2773
+ if ('data' in changes) {
2774
+ this.reloadDictionaryOptions();
2775
+ }
2776
+ }
2777
+ writeValue(value) {
2778
+ if (this.multiple) {
2779
+ this.multiValue = Array.isArray(value) ? value : [];
2780
+ }
2781
+ else {
2782
+ this.value = value ?? '';
2783
+ }
2784
+ this.updateSelectionFromValue();
2785
+ this.ensureSelectedDictionaryOptionsLoaded();
2786
+ this.cdr.markForCheck();
2787
+ }
2788
+ registerOnChange(fn) {
2789
+ this.onChange = fn;
2790
+ }
2791
+ registerOnTouched(fn) {
2792
+ this.onTouched = fn;
2793
+ }
2794
+ setDisabledState(isDisabled) {
2795
+ this.disabled = isDisabled;
2796
+ this.cdr.markForCheck();
2797
+ }
2798
+ toggleDropdown(event) {
2799
+ event.stopPropagation();
2800
+ if (this.disabled) {
2801
+ return;
2802
+ }
2803
+ if (this.isOpen) {
2804
+ this.closeDropdown();
2805
+ return;
2806
+ }
2807
+ if (UiSelectComponent.openInstance && UiSelectComponent.openInstance !== this) {
2808
+ UiSelectComponent.openInstance.closeDropdown();
2809
+ }
2810
+ this.triggerWidth = this.dropdownTrigger?.nativeElement.getBoundingClientRect().width ?? 0;
2811
+ this.isOpen = true;
2812
+ this.searchTerm = '';
2813
+ this.searchChanges$.next('');
2814
+ UiSelectComponent.openInstance = this;
2815
+ this.cdr.markForCheck();
2816
+ }
2817
+ selectOption(option, event) {
2818
+ event.stopPropagation();
2819
+ const key = this.getOptionValue(option);
2820
+ if (this.multiple) {
2821
+ const normalized = this.multiValue.includes(key)
2822
+ ? this.multiValue.filter((item) => item !== key)
2823
+ : [...this.multiValue, key];
2824
+ this.multiValue = normalized;
2825
+ this.onChange(normalized);
2826
+ this.valueChange.emit(normalized);
2827
+ }
2828
+ else {
2829
+ const nextValue = this.value === key ? null : key;
2830
+ this.value = nextValue;
2831
+ this.onChange(nextValue);
2832
+ this.valueChange.emit(nextValue);
2833
+ this.closeDropdown();
2834
+ }
2835
+ this.updateSelectionFromValue();
2836
+ }
2837
+ markTouched() {
2838
+ this.onTouched();
2839
+ }
2840
+ closeDropdown() {
2841
+ if (!this.isOpen) {
2842
+ return;
2843
+ }
2844
+ this.isOpen = false;
2845
+ this.onTouched();
2846
+ this.searchTerm = '';
2847
+ this.searchChanges$.next('');
2848
+ if (UiSelectComponent.openInstance === this) {
2849
+ UiSelectComponent.openInstance = null;
2850
+ }
2851
+ this.cdr.markForCheck();
2852
+ }
2853
+ get displayLabel() {
2854
+ if (this.multiple) {
2855
+ if (!this.selectedOptions.length) {
2856
+ return this.placeholder ?? this.blankOptionLabel;
2857
+ }
2858
+ return `${this.selectedOptions.length}`;
2859
+ }
2860
+ return this.selectedOptions[0]?.label ?? this.placeholder ?? this.blankOptionLabel;
2861
+ }
2862
+ isSelected(option) {
2863
+ const value = this.getOptionValue(option);
2864
+ if (this.multiple) {
2865
+ return this.multiValue.includes(value);
2866
+ }
2867
+ return this.value === value;
2868
+ }
2869
+ getOptionValue(option) {
2870
+ return option.value ?? '';
2871
+ }
2872
+ trackOption(index, option) {
2873
+ return `${this.getOptionValue(option)}::${String(option.label || '').trim()}::${index}`;
2874
+ }
2875
+ get visibleOptions() {
2876
+ const options = this.getRenderOptions();
2877
+ if (!this.searchable || this.remoteSearch || !this.searchTerm.trim()) {
2878
+ return options;
2879
+ }
2880
+ const search = this.searchTerm.trim().toLowerCase();
2881
+ return options.filter((option) => {
2882
+ if (this.includeBlankOption && !this.multiple && this.isBlankOption(option)) {
2883
+ return true;
2884
+ }
2885
+ return String(option.label ?? '').toLowerCase().includes(search);
2886
+ });
2887
+ }
2888
+ onSearchChange(value) {
2889
+ const next = String(value ?? '');
2890
+ this.searchChanges$.next(next);
2891
+ this.searchChange.emit(next);
2892
+ }
2893
+ removeOption(option, event) {
2894
+ event.stopPropagation();
2895
+ const key = this.getOptionValue(option);
2896
+ if (this.multiple) {
2897
+ this.multiValue = this.multiValue.filter((v) => v !== key);
2898
+ this.onChange(this.multiValue);
2899
+ this.valueChange.emit(this.multiValue);
2900
+ this.updateSelectionFromValue();
2901
+ }
2902
+ }
2903
+ updateSelectionFromValue() {
2904
+ const options = this.getRenderOptions();
2905
+ if (this.multiple) {
2906
+ this.selectedOptions = options.filter((option) => this.multiValue.includes(this.getOptionValue(option)));
2907
+ return;
2908
+ }
2909
+ const current = options.find((option) => this.getOptionValue(option) === (this.value ?? ''));
2910
+ this.selectedOptions = current ? [current] : [];
2911
+ }
2912
+ getRenderOptions() {
2913
+ const merged = this.mergeOptions(this.options, this.dictionaryOptions);
2914
+ if (!this.includeBlankOption || this.multiple) {
2915
+ return merged;
2916
+ }
2917
+ return [{ label: this.blankOptionLabel, value: '' }, ...merged];
2918
+ }
2919
+ mergeOptions(...optionSets) {
2920
+ const merged = new Map();
2921
+ for (const options of optionSets) {
2922
+ for (const option of options || []) {
2923
+ const value = this.getOptionValue(option);
2924
+ const key = `${value}::${String(option.label || '').trim()}`;
2925
+ if (!merged.has(key)) {
2926
+ merged.set(key, option);
2927
+ }
2928
+ }
2929
+ }
2930
+ return [...merged.values()];
2931
+ }
2932
+ isBlankOption(option) {
2933
+ return String(option.value ?? '') === '';
2934
+ }
2935
+ getDictionaryName() {
2936
+ return String(this.data || '').trim();
2937
+ }
2938
+ reloadDictionaryOptions() {
2939
+ const dictionaryName = this.getDictionaryName();
2940
+ this.dictionaryRequestVersions.clear();
2941
+ this.dictionaryOptions = [];
2942
+ this.isDictionaryLoading = false;
2943
+ if (!dictionaryName) {
2944
+ this.updateSelectionFromValue();
2945
+ this.cdr.markForCheck();
2946
+ return;
2947
+ }
2948
+ this.loadDictionaryOptions(this.searchTerm);
2949
+ }
2950
+ loadDictionaryOptions(query) {
2951
+ const dictionaryName = this.getDictionaryName();
2952
+ if (!dictionaryName) {
2953
+ return;
2954
+ }
2955
+ const requestVersion = (this.dictionaryRequestVersions.get(dictionaryName) || 0) + 1;
2956
+ this.dictionaryRequestVersions.set(dictionaryName, requestVersion);
2957
+ this.isDictionaryLoading = true;
2958
+ this.loadSuggestItems(dictionaryName, String(query || '').trim()).subscribe({
2959
+ next: (items) => {
2960
+ if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
2961
+ return;
2962
+ }
2963
+ this.dictionaryOptions = this.normalizeSuggestItems(items);
2964
+ this.isDictionaryLoading = false;
2965
+ this.ensureSelectedDictionaryOptionsLoaded();
2966
+ this.updateSelectionFromValue();
2967
+ this.cdr.markForCheck();
2968
+ },
2969
+ error: () => {
2970
+ if ((this.dictionaryRequestVersions.get(dictionaryName) || 0) !== requestVersion) {
2971
+ return;
2972
+ }
2973
+ this.dictionaryOptions = [];
2974
+ this.isDictionaryLoading = false;
2975
+ this.updateSelectionFromValue();
2976
+ this.cdr.markForCheck();
2977
+ },
2978
+ });
2979
+ }
2980
+ ensureSelectedDictionaryOptionsLoaded() {
2981
+ const dictionaryName = this.getDictionaryName();
2982
+ if (!dictionaryName) {
2983
+ return;
2984
+ }
2985
+ const existingValues = new Set(this.getRenderOptions().map((option) => this.getOptionValue(option)));
2986
+ const selectedValues = this.multiple ? this.multiValue : [this.value];
2987
+ const missingValues = selectedValues
2988
+ .map((item) => String(item || '').trim())
2989
+ .filter((item) => !!item && !existingValues.has(item));
2990
+ if (!missingValues.length) {
2991
+ return;
2992
+ }
2993
+ forkJoin(missingValues.map((value) => this.loadSuggestItem(dictionaryName, value))).subscribe({
2994
+ next: (items) => {
2995
+ const nextOptions = [...this.dictionaryOptions];
2996
+ for (const item of items) {
2997
+ const normalized = this.normalizeSuggestItem(item);
2998
+ if (!nextOptions.some((option) => this.getOptionValue(option) === normalized.value)) {
2999
+ nextOptions.push(normalized);
3000
+ }
3001
+ }
3002
+ this.dictionaryOptions = nextOptions;
3003
+ this.updateSelectionFromValue();
3004
+ this.cdr.markForCheck();
3005
+ },
3006
+ });
3007
+ }
3008
+ normalizeSuggestItems(items) {
3009
+ return (items || [])
3010
+ .map((item) => this.normalizeSuggestItem(item))
3011
+ .filter((item) => !!item.label || item.value !== null);
3012
+ }
3013
+ normalizeSuggestItem(item) {
3014
+ return {
3015
+ label: String(item?.label ?? ''),
3016
+ value: item?.key != null ? String(item.key) : null,
3017
+ imageUrl: item?.imageId ? `/api/files/${item.imageId}/preview?scale=25` : undefined,
3018
+ };
3019
+ }
3020
+ loadSuggestItems(name, query) {
3021
+ const params = new URLSearchParams();
3022
+ if (query?.trim())
3023
+ params.set('q', query.trim());
3024
+ const suffix = params.toString() ? `?${params.toString()}` : '';
3025
+ return this.http.get(`/api/suggest/${encodeURIComponent(name)}${suffix}`);
3026
+ }
3027
+ loadSuggestItem(name, key) {
3028
+ return this.http.get(`/api/suggest/${encodeURIComponent(name)}/${encodeURIComponent(key)}`);
3029
+ }
3030
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3031
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiSelectComponent, isStandalone: true, selector: "forms-select", inputs: { label: "label", placeholder: "placeholder", name: "name", data: "data", required: "required", fullWidth: "fullWidth", disabled: "disabled", multiple: ["multiple", "multiple", booleanAttribute], includeBlankOption: "includeBlankOption", blankOptionLabel: "blankOptionLabel", emptyStateLabel: "emptyStateLabel", searchable: "searchable", remoteSearch: "remoteSearch", searchPlaceholder: "searchPlaceholder", options: "options", error: "error", hint: "hint", triggerClass: "triggerClass", panelClass: "panelClass" }, outputs: { valueChange: "valueChange", searchChange: "searchChange" }, providers: [
3032
+ {
3033
+ provide: NG_VALUE_ACCESSOR,
3034
+ useExisting: forwardRef(() => UiSelectComponent),
3035
+ multi: true,
3036
+ },
3037
+ ], viewQueries: [{ propertyName: "dropdownTrigger", first: true, predicate: ["dropdownTrigger"], descendants: true, read: ElementRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex w-full flex-wrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <button\n type=\"button\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"text-[10px]\"></lucide-icon>\n </button>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"ml-auto text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayWidth]=\"triggerWidth || 0\"\n [cdkConnectedOverlayViewportMargin]=\"8\"\n (backdropClick)=\"closeDropdown()\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 max-h-60 w-full overflow-auto rounded-xl border border-slate-200 bg-white shadow-lg\"\n [class]=\"panelClass\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-300 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-center justify-between px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"truncate\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n", dependencies: [{ kind: "ngmodule", type: LucideAngularModule }, { kind: "component", type: i1$2.LucideAngularComponent, selector: "lucide-angular, lucide-icon, i-lucide, span-lucide", inputs: ["class", "name", "img", "color", "absoluteStrokeWidth", "size", "strokeWidth"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3038
+ }
3039
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSelectComponent, decorators: [{
3040
+ type: Component,
3041
+ args: [{ selector: 'forms-select', standalone: true, imports: [LucideAngularModule, CommonModule, FormsModule, CdkConnectedOverlay, CdkOverlayOrigin], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
3042
+ {
3043
+ provide: NG_VALUE_ACCESSOR,
3044
+ useExisting: forwardRef(() => UiSelectComponent),
3045
+ multi: true,
3046
+ },
3047
+ ], template: "<div\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n>\n <!-- LABEL -->\n @if (label || (hint && !error)) {\n <div class=\"flex items-center justify-between\">\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) { <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span> }\n </span>\n\n @if (hint && !error) {\n <span class=\"text-[0.65rem] text-slate-400\">\n {{ hint }}\n </span>\n }\n </div>\n }\n\n <!-- SELECT -->\n <div class=\"relative\" [class.w-full]=\"fullWidth\">\n\n <button\n cdkOverlayOrigin\n #dropdownTrigger=\"cdkOverlayOrigin\"\n type=\"button\"\n class=\"flex w-full flex-wrap items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 transition hover:border-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-200\"\n [class]=\"triggerClass\"\n [class.opacity-50]=\"disabled\"\n [disabled]=\"disabled\"\n (click)=\"toggleDropdown($event)\"\n >\n\n <!-- MULTIPLE MODE -->\n @if (multiple) {\n\n @if (selectedOptions.length) {\n\n @for (option of selectedOptions; track option.value) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-700\"\n >\n {{ option.label }}\n\n <button\n type=\"button\"\n class=\"text-slate-400 hover:text-rose-500 transition-colors\"\n (click)=\"removeOption(option, $event)\"\n >\n <lucide-icon [img]=\"closeIcon\" class=\"text-[10px]\"></lucide-icon>\n </button>\n </span>\n }\n\n } @else {\n <span class=\"text-slate-400\">\n {{ placeholder ?? blankOptionLabel }}\n </span>\n }\n\n } @else {\n\n <!-- SINGLE MODE -->\n @if (selectedOptions[0]?.imageUrl) {\n <img [src]=\"selectedOptions[0]?.imageUrl\" alt=\"\" class=\"h-7 w-7 rounded object-cover\" />\n }\n <span class=\"truncate\">\n {{ selectedOptions[0]?.label ?? placeholder ?? blankOptionLabel }}\n </span>\n\n }\n\n <lucide-icon [img]=\"chevronDownIcon\" class=\"ml-auto text-xs text-slate-400\"></lucide-icon>\n\n </button>\n\n <ng-template\n cdkConnectedOverlay\n [cdkConnectedOverlayOrigin]=\"dropdownTrigger\"\n [cdkConnectedOverlayOpen]=\"isOpen\"\n [cdkConnectedOverlayHasBackdrop]=\"true\"\n cdkConnectedOverlayBackdropClass=\"cdk-overlay-transparent-backdrop\"\n [cdkConnectedOverlayPositions]=\"dropdownPositions\"\n [cdkConnectedOverlayWidth]=\"triggerWidth || 0\"\n [cdkConnectedOverlayViewportMargin]=\"8\"\n (backdropClick)=\"closeDropdown()\"\n (detach)=\"closeDropdown()\"\n >\n <!-- DROPDOWN -->\n <div\n class=\"z-50 max-h-60 w-full overflow-auto rounded-xl border border-slate-200 bg-white shadow-lg\"\n [class]=\"panelClass\"\n >\n @if (searchable) {\n <div class=\"border-b border-slate-100 p-2\">\n <input\n type=\"text\"\n class=\"w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 outline-none transition focus:border-slate-300 focus:ring-2 focus:ring-slate-200\"\n [placeholder]=\"searchPlaceholder\"\n [ngModel]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange($event)\"\n (click)=\"$event.stopPropagation()\"\n (mousedown)=\"$event.stopPropagation()\"\n />\n </div>\n }\n\n @if (!visibleOptions.length) {\n <div class=\"px-4 py-3 text-sm text-slate-500\">\n @if (isDictionaryLoading && data) {\n \u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F...\n } @else {\n {{ emptyStateLabel }}\n }\n </div>\n } @else {\n @for (option of visibleOptions; track trackOption($index, option)) {\n <button\n type=\"button\"\n class=\"flex w-full items-center justify-between px-4 py-2 text-left text-sm hover:bg-slate-50\"\n [class.bg-slate-100]=\"isSelected(option)\"\n (click)=\"selectOption(option, $event)\"\n >\n <span class=\"flex min-w-0 items-center gap-2\">\n @if (option.imageUrl) {\n <img [src]=\"option.imageUrl\" alt=\"\" class=\"h-8 w-8 shrink-0 rounded object-cover\" />\n }\n <span class=\"truncate\">{{ option.label }}</span>\n </span>\n\n @if (isSelected(option)) {\n <lucide-icon [img]=\"checkIcon\" class=\"text-xs text-sky-500\"></lucide-icon>\n }\n </button>\n }\n }\n\n </div>\n </ng-template>\n\n </div>\n\n @if (error) {\n <p class=\"text-[0.7rem] text-rose-500\">\n {{ error }}\n </p>\n }\n\n</div>\n" }]
3048
+ }], ctorParameters: () => [], propDecorators: { label: [{
3049
+ type: Input
3050
+ }], placeholder: [{
3051
+ type: Input
3052
+ }], name: [{
3053
+ type: Input
3054
+ }], data: [{
3055
+ type: Input
3056
+ }], required: [{
3057
+ type: Input
3058
+ }], fullWidth: [{
3059
+ type: Input
3060
+ }], disabled: [{
3061
+ type: Input
3062
+ }], multiple: [{
3063
+ type: Input,
3064
+ args: [{ transform: booleanAttribute }]
3065
+ }], includeBlankOption: [{
3066
+ type: Input
3067
+ }], blankOptionLabel: [{
3068
+ type: Input
3069
+ }], emptyStateLabel: [{
3070
+ type: Input
3071
+ }], searchable: [{
3072
+ type: Input
3073
+ }], remoteSearch: [{
3074
+ type: Input
3075
+ }], searchPlaceholder: [{
3076
+ type: Input
3077
+ }], options: [{
3078
+ type: Input
3079
+ }], error: [{
3080
+ type: Input
3081
+ }], hint: [{
3082
+ type: Input
3083
+ }], triggerClass: [{
3084
+ type: Input
3085
+ }], panelClass: [{
3086
+ type: Input
3087
+ }], valueChange: [{
3088
+ type: Output
3089
+ }], searchChange: [{
3090
+ type: Output
3091
+ }], dropdownTrigger: [{
3092
+ type: ViewChild,
3093
+ args: ['dropdownTrigger', { static: true, read: ElementRef }]
3094
+ }] } });
3095
+
3096
+ class UiSlugInputComponent {
3097
+ label;
3098
+ hint;
3099
+ placeholder;
3100
+ name;
3101
+ id;
3102
+ required = false;
3103
+ fullWidth = false;
3104
+ autocomplete = 'off';
3105
+ readonly = false;
3106
+ error;
3107
+ invalidInput = new EventEmitter();
3108
+ disabled = false;
3109
+ value = '';
3110
+ static nextId = 0;
3111
+ generatedId = `ui-slug-input-${UiSlugInputComponent.nextId++}`;
3112
+ onChange = () => undefined;
3113
+ onTouched = () => undefined;
3114
+ get controlId() {
3115
+ return this.id ?? this.generatedId;
3116
+ }
3117
+ writeValue(value) {
3118
+ this.value = value ?? '';
3119
+ }
3120
+ registerOnChange(fn) {
3121
+ this.onChange = fn;
3122
+ }
3123
+ registerOnTouched(fn) {
3124
+ this.onTouched = fn;
3125
+ }
3126
+ setDisabledState(isDisabled) {
3127
+ this.disabled = isDisabled;
3128
+ }
3129
+ onValueChange(target) {
3130
+ const normalized = this.normalizeSlug(target.value);
3131
+ if (target.value !== normalized) {
3132
+ target.value = normalized;
3133
+ this.invalidInput.emit();
3134
+ }
3135
+ this.value = normalized;
3136
+ this.onChange(normalized);
3137
+ }
3138
+ normalizeSlug(value) {
3139
+ return (value ?? '')
3140
+ .toLowerCase()
3141
+ .replace(/\s+/g, '_')
3142
+ .replace(/[^a-z0-9_]/g, '');
3143
+ }
3144
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSlugInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3145
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: UiSlugInputComponent, isStandalone: true, selector: "forms-slug-input", inputs: { label: "label", hint: "hint", placeholder: "placeholder", name: "name", id: "id", required: "required", fullWidth: "fullWidth", autocomplete: "autocomplete", readonly: "readonly", error: "error" }, outputs: { invalidInput: "invalidInput" }, providers: [
3146
+ {
3147
+ provide: NG_VALUE_ACCESSOR,
3148
+ useExisting: forwardRef(() => UiSlugInputComponent),
3149
+ multi: true,
3150
+ },
3151
+ ], ngImport: i0, template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3152
+ }
3153
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: UiSlugInputComponent, decorators: [{
3154
+ type: Component,
3155
+ args: [{ selector: 'forms-slug-input', standalone: true, imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
3156
+ {
3157
+ provide: NG_VALUE_ACCESSOR,
3158
+ useExisting: forwardRef(() => UiSlugInputComponent),
3159
+ multi: true,
3160
+ },
3161
+ ], template: "<label\n class=\"flex flex-col gap-1 text-[0.65rem] uppercase tracking-[0.3em] text-slate-500\"\n [class.w-full]=\"fullWidth\"\n [attr.for]=\"controlId\"\n>\n @if (label) {\n <span class=\"text-[0.7rem] font-semibold text-slate-600\">\n {{ label }}\n @if (required) {\n <span aria-hidden=\"true\" class=\"text-rose-500\"> *</span>\n }\n </span>\n }\n\n <input\n type=\"text\"\n [attr.placeholder]=\"placeholder\"\n [attr.name]=\"name\"\n [attr.autocomplete]=\"autocomplete\"\n [readonly]=\"readonly\"\n [disabled]=\"disabled\"\n class=\"rounded-2xl border border-slate-200 bg-white px-4 py-2 text-sm text-slate-900 placeholder:text-slate-400 transition focus:border-sky-400 focus:outline-none\"\n [attr.id]=\"controlId\"\n [value]=\"value\"\n (input)=\"onValueChange($any($event.target))\"\n (blur)=\"onTouched()\"\n />\n\n @if (hint) {\n <p class=\"text-[0.5rem] text-slate-500\">{{ hint }}</p>\n }\n @if (error) {\n <p class=\"text-[0.5rem] text-rose-400\">{{ error }}</p>\n }\n</label>\n\n\n" }]
3162
+ }], propDecorators: { label: [{
3163
+ type: Input
3164
+ }], hint: [{
3165
+ type: Input
3166
+ }], placeholder: [{
3167
+ type: Input
3168
+ }], name: [{
3169
+ type: Input
3170
+ }], id: [{
3171
+ type: Input
3172
+ }], required: [{
3173
+ type: Input
3174
+ }], fullWidth: [{
3175
+ type: Input
3176
+ }], autocomplete: [{
3177
+ type: Input
3178
+ }], readonly: [{
3179
+ type: Input
3180
+ }], error: [{
3181
+ type: Input
3182
+ }], invalidInput: [{
3183
+ type: Output
3184
+ }] } });
3185
+
3186
+ /**
3187
+ * Generated bundle index. Do not edit.
3188
+ */
3189
+
3190
+ export { MaroFormsApiService, UI_DYNAMIC_FORM_OPTIONS_SOURCE_RESOLVERS, UI_FORM_INPUT_REGISTRY, UiActionFormModalComponent, UiBarcodeScannerInputComponent, UiCheckboxInputComponent, UiColorInputComponent, UiDateInputComponent, UiDatetimeInputComponent, UiDynamicFormComponent, UiEmailInputComponent, UiFileInputComponent, UiFormContext, UiFormInputHostComponent, UiFormLookupService, UiInputComponent, UiNumberInputComponent, UiPasswordInputComponent, UiPhotoGalleryFieldComponent, UiPhotoInputComponent, UiRangeSliderComponent, UiSelectComponent, UiSlugInputComponent, UiTextInputComponent, UiTextareaInputComponent, flattenDynamicFormFields, isDynamicFormContainerField, isDynamicFormTemplateField };
3191
+ //# sourceMappingURL=maro-tech-form.mjs.map