commons-shared-web-ui 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { NgModule, Input, Component, EventEmitter, HostListener, Output, forwardRef, Directive, ViewChild, Injectable, inject, HostBinding, LOCALE_ID, ViewChildren } from '@angular/core';
|
|
2
|
+
import { NgModule, Input, Component, EventEmitter, HostListener, Output, forwardRef, Directive, ViewChild, Injectable, inject, SecurityContext, Pipe, HostBinding, LOCALE_ID, ViewChildren } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { CommonModule, formatDate } from '@angular/common';
|
|
5
5
|
import { MatCardModule } from '@angular/material/card';
|
|
@@ -52,6 +52,7 @@ import * as i3 from '@angular/common/http';
|
|
|
52
52
|
import { HttpHeaders, HttpParams } from '@angular/common/http';
|
|
53
53
|
import * as i11 from 'ngx-quill';
|
|
54
54
|
import { QuillModule } from 'ngx-quill';
|
|
55
|
+
import * as i1$3 from '@angular/platform-browser';
|
|
55
56
|
|
|
56
57
|
class MaterialModule {
|
|
57
58
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: MaterialModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
@@ -3857,6 +3858,36 @@ class StringUtils {
|
|
|
3857
3858
|
}
|
|
3858
3859
|
}
|
|
3859
3860
|
|
|
3861
|
+
/**
|
|
3862
|
+
* Bypasses Angular's DomSanitizer for resource URLs (e.g. YouTube embed iframes).
|
|
3863
|
+
* Used only for trusted URLs such as YouTube embed links derived from user-provided video IDs.
|
|
3864
|
+
*/
|
|
3865
|
+
class TrustedUrlPipe {
|
|
3866
|
+
sanitizer;
|
|
3867
|
+
constructor(sanitizer) {
|
|
3868
|
+
this.sanitizer = sanitizer;
|
|
3869
|
+
}
|
|
3870
|
+
transform(url) {
|
|
3871
|
+
if (!url) {
|
|
3872
|
+
return null;
|
|
3873
|
+
}
|
|
3874
|
+
const cleaned = this.sanitizer.sanitize(SecurityContext.URL, url);
|
|
3875
|
+
if (!cleaned) {
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(cleaned);
|
|
3879
|
+
}
|
|
3880
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TrustedUrlPipe, deps: [{ token: i1$3.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe });
|
|
3881
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: TrustedUrlPipe, isStandalone: false, name: "trustedUrl" });
|
|
3882
|
+
}
|
|
3883
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TrustedUrlPipe, decorators: [{
|
|
3884
|
+
type: Pipe,
|
|
3885
|
+
args: [{
|
|
3886
|
+
name: 'trustedUrl',
|
|
3887
|
+
standalone: false
|
|
3888
|
+
}]
|
|
3889
|
+
}], ctorParameters: () => [{ type: i1$3.DomSanitizer }] });
|
|
3890
|
+
|
|
3860
3891
|
class FormFieldComponent {
|
|
3861
3892
|
fb;
|
|
3862
3893
|
expressionService;
|
|
@@ -3882,6 +3913,19 @@ class FormFieldComponent {
|
|
|
3882
3913
|
fileUploadError = ''; // per-field file validation error
|
|
3883
3914
|
multiSaveError = ''; // error for multisave validation
|
|
3884
3915
|
destroy$ = new Subject();
|
|
3916
|
+
// ── MEDIA_UPLOAD support ────────────────────────────────────────────────
|
|
3917
|
+
mediaDeviceInput;
|
|
3918
|
+
showMediaMenu = false; // hover/click dropdown menu
|
|
3919
|
+
showYoutubeInput = false; // inline YouTube URL input panel
|
|
3920
|
+
youtubeUrlInput = ''; // model for the YT URL field
|
|
3921
|
+
youtubeUrlError = ''; // validation message for YT url
|
|
3922
|
+
mediaCarouselIndex = 0; // active slide index for carousel
|
|
3923
|
+
showLibraryModal = false; // library picker modal
|
|
3924
|
+
libraryImages = []; // raw list fetched from library API
|
|
3925
|
+
librarySelectedIds = new Set(); // selected library items
|
|
3926
|
+
libraryLoading = false;
|
|
3927
|
+
libraryError = '';
|
|
3928
|
+
mediaUploadError = ''; // transient error message for max limits
|
|
3885
3929
|
// ── AUTOCOMPLETE support ────────────────────────────────────────────────
|
|
3886
3930
|
/** FormControl used ONLY for the autocomplete text-input display value */
|
|
3887
3931
|
autocompleteInputCtrl = new FormControl('');
|
|
@@ -4173,6 +4217,17 @@ class FormFieldComponent {
|
|
|
4173
4217
|
if (max !== undefined)
|
|
4174
4218
|
validators.push(Validators.max(max));
|
|
4175
4219
|
}
|
|
4220
|
+
if (this.config.type === 'RICH_TEXT' && this.config.richTextConfig?.maxLength) {
|
|
4221
|
+
const max = this.config.richTextConfig.maxLength;
|
|
4222
|
+
validators.push((control) => {
|
|
4223
|
+
if (!control.value)
|
|
4224
|
+
return null;
|
|
4225
|
+
const plainText = String(control.value).replace(/<[^>]*>/g, '');
|
|
4226
|
+
return plainText.length > max
|
|
4227
|
+
? { 'maxlength': { requiredLength: max, actualLength: plainText.length } }
|
|
4228
|
+
: null;
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4176
4231
|
return validators;
|
|
4177
4232
|
}
|
|
4178
4233
|
// ── Cross-field match validation (password === confirmPassword) ───────────
|
|
@@ -4408,6 +4463,28 @@ class FormFieldComponent {
|
|
|
4408
4463
|
}
|
|
4409
4464
|
return '';
|
|
4410
4465
|
}
|
|
4466
|
+
get showCharCount() {
|
|
4467
|
+
if (this.isTextField && this.config.subType !== 'PHONE') {
|
|
4468
|
+
return !!(this.config.textConfig?.showCharCount && this.config.textConfig?.length?.max);
|
|
4469
|
+
}
|
|
4470
|
+
if (this.isRichText) {
|
|
4471
|
+
return !!(this.config.richTextConfig?.showCharCount && this.config.richTextConfig?.maxLength);
|
|
4472
|
+
}
|
|
4473
|
+
return false;
|
|
4474
|
+
}
|
|
4475
|
+
get remainingCharacters() {
|
|
4476
|
+
if (!this.showCharCount || !this.formGroup || !this.config.name)
|
|
4477
|
+
return null;
|
|
4478
|
+
const currentVal = this.formGroup.get(this.config.name)?.value || '';
|
|
4479
|
+
if (this.isTextField && this.config.textConfig?.length?.max) {
|
|
4480
|
+
return Math.max(0, this.config.textConfig.length.max - currentVal.length);
|
|
4481
|
+
}
|
|
4482
|
+
if (this.isRichText && this.config.richTextConfig?.maxLength) {
|
|
4483
|
+
const plainText = String(currentVal).replace(/<[^>]*>/g, '');
|
|
4484
|
+
return Math.max(0, this.config.richTextConfig.maxLength - plainText.length);
|
|
4485
|
+
}
|
|
4486
|
+
return null;
|
|
4487
|
+
}
|
|
4411
4488
|
// ── Type guards ──────────────────────────────────────────────────────────
|
|
4412
4489
|
get isTextField() { return this.config.type === 'TEXT_INPUT'; }
|
|
4413
4490
|
get isNumberField() { return this.config.type === 'NUMBER_INPUT'; }
|
|
@@ -4416,6 +4493,7 @@ class FormFieldComponent {
|
|
|
4416
4493
|
get isDropdown() { return this.config.type === 'DROPDOWN'; }
|
|
4417
4494
|
get isAutocomplete() { return this.config.type === 'AUTOCOMPLETE'; }
|
|
4418
4495
|
get isFileUpload() { return this.config.type === 'FILE_UPLOAD'; }
|
|
4496
|
+
get isMediaUpload() { return this.config.type === 'MEDIA_UPLOAD'; }
|
|
4419
4497
|
get isRadio() { return this.config.type === 'RADIO'; }
|
|
4420
4498
|
get isCheckbox() { return this.config.type === 'CHECKBOX'; }
|
|
4421
4499
|
get isChip() { return this.config.type === 'CHIP'; }
|
|
@@ -4539,6 +4617,59 @@ class FormFieldComponent {
|
|
|
4539
4617
|
input.value = '';
|
|
4540
4618
|
}
|
|
4541
4619
|
processFiles(files) {
|
|
4620
|
+
// ── MEDIA_UPLOAD branch — uses attachmentConfig, stores MediaItem[] ─────
|
|
4621
|
+
if (this.isMediaUpload) {
|
|
4622
|
+
const mediaCfg = this.config.attachmentConfig;
|
|
4623
|
+
if (!mediaCfg)
|
|
4624
|
+
return;
|
|
4625
|
+
const maxItems = mediaCfg.maxFiles ?? 10;
|
|
4626
|
+
let errorShown = false;
|
|
4627
|
+
Array.from(files).forEach(file => {
|
|
4628
|
+
if (this.mediaItems.length >= maxItems) {
|
|
4629
|
+
if (!errorShown) {
|
|
4630
|
+
this.showMediaError(this.controller.labels['ERR_MEDIA_MAX'] || `Maximum ${maxItems} media items allowed.`);
|
|
4631
|
+
errorShown = true;
|
|
4632
|
+
}
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
const placeholder = { mediaType: 'image', url: '', isUploading: true };
|
|
4636
|
+
this._appendMediaItem(placeholder);
|
|
4637
|
+
const formData = new FormData();
|
|
4638
|
+
formData.append('file', file);
|
|
4639
|
+
if (mediaCfg.entityType)
|
|
4640
|
+
formData.append('entity_type', mediaCfg.entityType);
|
|
4641
|
+
if (!mediaCfg.uploadUrl)
|
|
4642
|
+
return;
|
|
4643
|
+
this.http.post(mediaCfg.uploadUrl, formData, { headers: this.getHeaders() })
|
|
4644
|
+
.pipe(takeUntil(this.destroy$))
|
|
4645
|
+
.subscribe({
|
|
4646
|
+
next: (response) => {
|
|
4647
|
+
// API returns { completeURL, id, mimeType, fileName, ... }
|
|
4648
|
+
const resolvedUrl = (response?.completeURL ?? response)?.toString().trim();
|
|
4649
|
+
const current = [...this.mediaItems];
|
|
4650
|
+
const idx = current.indexOf(placeholder);
|
|
4651
|
+
if (idx !== -1) {
|
|
4652
|
+
current[idx] = {
|
|
4653
|
+
mediaType: 'image',
|
|
4654
|
+
url: resolvedUrl,
|
|
4655
|
+
id: response?.id,
|
|
4656
|
+
mimeType: response?.mimeType,
|
|
4657
|
+
fileName: response?.fileName ?? response?.name,
|
|
4658
|
+
isUploading: false
|
|
4659
|
+
};
|
|
4660
|
+
this.updateValue(current);
|
|
4661
|
+
this.mediaCarouselIndex = current.length - 1;
|
|
4662
|
+
}
|
|
4663
|
+
},
|
|
4664
|
+
error: () => {
|
|
4665
|
+
const cleaned = this.mediaItems.filter(m => m !== placeholder);
|
|
4666
|
+
this.updateValue(cleaned.length ? cleaned : null);
|
|
4667
|
+
}
|
|
4668
|
+
});
|
|
4669
|
+
});
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
// ── FILE_UPLOAD branch — uses attachmentConfig, stores UploadedFile[] ────
|
|
4542
4673
|
this.fileUploadError = '';
|
|
4543
4674
|
const cfg = this.config.attachmentConfig;
|
|
4544
4675
|
const maxSizeBytes = (cfg?.maxSizeMB ?? 10) * 1024 * 1024;
|
|
@@ -4623,10 +4754,46 @@ class FormFieldComponent {
|
|
|
4623
4754
|
}
|
|
4624
4755
|
removeUploadedFile(index) {
|
|
4625
4756
|
const current = this.value || [];
|
|
4626
|
-
const
|
|
4627
|
-
|
|
4757
|
+
const fileToRemove = current[index];
|
|
4758
|
+
if (!fileToRemove)
|
|
4759
|
+
return;
|
|
4760
|
+
const removeLocally = () => {
|
|
4761
|
+
const updated = current.filter((_, i) => i !== index);
|
|
4762
|
+
this.updateValue(updated);
|
|
4763
|
+
};
|
|
4764
|
+
const deleteUrl = this.config.attachmentConfig?.deleteUrl;
|
|
4765
|
+
if (fileToRemove.id && fileToRemove.id !== 0 && deleteUrl) {
|
|
4766
|
+
fileToRemove.isUploading = true; // Use this flag to disable the remove button
|
|
4767
|
+
this.updateValue([...current]); // trigger UI update
|
|
4768
|
+
let params = new HttpParams();
|
|
4769
|
+
if (!deleteUrl.includes('attachmentId=')) {
|
|
4770
|
+
params = params.set('attachmentId', fileToRemove.id.toString());
|
|
4771
|
+
}
|
|
4772
|
+
if (!deleteUrl.includes('reason=')) {
|
|
4773
|
+
// As requested in the API link, the reason is passed
|
|
4774
|
+
params = params.set('reason', 'DELECTED BY CREATOR');
|
|
4775
|
+
}
|
|
4776
|
+
this.http.delete(deleteUrl, { headers: this.getHeaders(), params })
|
|
4777
|
+
.pipe(takeUntil(this.destroy$))
|
|
4778
|
+
.subscribe({
|
|
4779
|
+
next: () => {
|
|
4780
|
+
removeLocally();
|
|
4781
|
+
},
|
|
4782
|
+
error: (err) => {
|
|
4783
|
+
console.error('Failed to delete attachment', err);
|
|
4784
|
+
fileToRemove.isUploading = false;
|
|
4785
|
+
this.fileUploadError = `Failed to delete "${fileToRemove.name}". Please try again.`;
|
|
4786
|
+
this.updateValue([...current]);
|
|
4787
|
+
}
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
else {
|
|
4791
|
+
removeLocally();
|
|
4792
|
+
}
|
|
4628
4793
|
}
|
|
4629
4794
|
getFileIcon(mimeType) {
|
|
4795
|
+
if (!mimeType)
|
|
4796
|
+
return 'attach_file'; // guard: undefined/null from non-FILE_UPLOAD values
|
|
4630
4797
|
if (mimeType.includes('pdf'))
|
|
4631
4798
|
return 'picture_as_pdf';
|
|
4632
4799
|
if (mimeType.includes('image'))
|
|
@@ -4655,12 +4822,258 @@ class FormFieldComponent {
|
|
|
4655
4822
|
const key = this.controller.actionLabels?.removeLabel || 'Remove';
|
|
4656
4823
|
return this.controller.labels[key] || key;
|
|
4657
4824
|
}
|
|
4825
|
+
// ── MEDIA_UPLOAD helpers ─────────────────────────────────────────────────
|
|
4826
|
+
get mediaItems() {
|
|
4827
|
+
return this.value || [];
|
|
4828
|
+
}
|
|
4829
|
+
/** Number of active items (used to clamp carousel index) */
|
|
4830
|
+
get mediaCount() { return this.mediaItems.length; }
|
|
4831
|
+
/** The currently visible carousel item */
|
|
4832
|
+
get activeMediaItem() {
|
|
4833
|
+
if (!this.mediaItems.length)
|
|
4834
|
+
return null;
|
|
4835
|
+
return this.mediaItems[Math.min(this.mediaCarouselIndex, this.mediaItems.length - 1)];
|
|
4836
|
+
}
|
|
4837
|
+
/** Thumbnail strip items */
|
|
4838
|
+
get mediaThumbnails() { return this.mediaItems; }
|
|
4839
|
+
mediaCarouselPrev() {
|
|
4840
|
+
if (this.mediaCarouselIndex > 0)
|
|
4841
|
+
this.mediaCarouselIndex--;
|
|
4842
|
+
}
|
|
4843
|
+
mediaCarouselNext() {
|
|
4844
|
+
if (this.mediaCarouselIndex < this.mediaItems.length - 1)
|
|
4845
|
+
this.mediaCarouselIndex++;
|
|
4846
|
+
}
|
|
4847
|
+
mediaGoTo(index) {
|
|
4848
|
+
this.mediaCarouselIndex = index;
|
|
4849
|
+
}
|
|
4850
|
+
// ── YouTube ──────────────────────────────────────────────────────────────
|
|
4851
|
+
onMediaMenuVideo() {
|
|
4852
|
+
this.showMediaMenu = false;
|
|
4853
|
+
this.showYoutubeInput = !this.showYoutubeInput;
|
|
4854
|
+
this.youtubeUrlInput = '';
|
|
4855
|
+
this.youtubeUrlError = '';
|
|
4856
|
+
}
|
|
4857
|
+
addYoutubeMedia() {
|
|
4858
|
+
const cfg = this.config.attachmentConfig;
|
|
4859
|
+
const maxItems = cfg?.maxFiles ?? 10;
|
|
4860
|
+
if (this.mediaItems.length >= maxItems) {
|
|
4861
|
+
this.showMediaError(this.controller.labels['ERR_MEDIA_MAX'] || `Maximum ${maxItems} media items allowed.`);
|
|
4862
|
+
return;
|
|
4863
|
+
}
|
|
4864
|
+
const url = this.youtubeUrlInput.trim();
|
|
4865
|
+
const videoId = this._extractYoutubeId(url);
|
|
4866
|
+
if (!videoId) {
|
|
4867
|
+
this.youtubeUrlError = this.controller.labels['ERR_INVALID_YOUTUBE_URL'] || 'Please enter a valid YouTube URL.';
|
|
4868
|
+
return;
|
|
4869
|
+
}
|
|
4870
|
+
const item = {
|
|
4871
|
+
mediaType: 'youtube',
|
|
4872
|
+
url: `https://www.youtube.com/embed/${videoId}`,
|
|
4873
|
+
thumbnailUrl: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
|
|
4874
|
+
};
|
|
4875
|
+
this._appendMediaItem(item);
|
|
4876
|
+
this.showYoutubeInput = false;
|
|
4877
|
+
this.youtubeUrlInput = '';
|
|
4878
|
+
this.youtubeUrlError = '';
|
|
4879
|
+
}
|
|
4880
|
+
_extractYoutubeId(url) {
|
|
4881
|
+
const patterns = [
|
|
4882
|
+
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([\w-]{11})/,
|
|
4883
|
+
/youtube\.com\/shorts\/([\w-]{11})/
|
|
4884
|
+
];
|
|
4885
|
+
for (const p of patterns) {
|
|
4886
|
+
const m = url.match(p);
|
|
4887
|
+
if (m)
|
|
4888
|
+
return m[1];
|
|
4889
|
+
}
|
|
4890
|
+
return null;
|
|
4891
|
+
}
|
|
4892
|
+
// ── Device upload ────────────────────────────────────────────────────────
|
|
4893
|
+
onMediaMenuDevice() {
|
|
4894
|
+
this.showMediaMenu = false;
|
|
4895
|
+
this.showYoutubeInput = false;
|
|
4896
|
+
// Use setTimeout so Angular finishes closing the dropdown before the
|
|
4897
|
+
// browser opens the native file picker (the input lives outside *ngIf).
|
|
4898
|
+
setTimeout(() => this.mediaDeviceInput?.nativeElement?.click());
|
|
4899
|
+
}
|
|
4900
|
+
onMediaFileSelected(event) {
|
|
4901
|
+
const input = event.target;
|
|
4902
|
+
if (input.files && input.files.length > 0) {
|
|
4903
|
+
this.processFiles(input.files);
|
|
4904
|
+
}
|
|
4905
|
+
input.value = '';
|
|
4906
|
+
}
|
|
4907
|
+
// ── Library picker ───────────────────────────────────────────────────────
|
|
4908
|
+
onMediaMenuLibrary() {
|
|
4909
|
+
this.showMediaMenu = false;
|
|
4910
|
+
this.showYoutubeInput = false;
|
|
4911
|
+
this.libraryImages = [];
|
|
4912
|
+
this.librarySelectedIds = new Set();
|
|
4913
|
+
this.libraryError = '';
|
|
4914
|
+
this.showLibraryModal = true;
|
|
4915
|
+
this._loadLibraryImages();
|
|
4916
|
+
}
|
|
4917
|
+
_loadLibraryImages() {
|
|
4918
|
+
const cfg = this.config.attachmentConfig;
|
|
4919
|
+
if (!cfg?.libraryApiUrl) {
|
|
4920
|
+
this.libraryError = 'Library not configured.';
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
this.libraryLoading = true;
|
|
4924
|
+
this.http.get(cfg.libraryApiUrl, { headers: this.getHeaders() })
|
|
4925
|
+
.pipe(takeUntil(this.destroy$))
|
|
4926
|
+
.subscribe({
|
|
4927
|
+
next: (res) => {
|
|
4928
|
+
let data;
|
|
4929
|
+
if (cfg.libraryDataPath) {
|
|
4930
|
+
// Try explicit dot-notation path configured by the dev (e.g. 'data.items')
|
|
4931
|
+
data = this.getValueByPath(res, cfg.libraryDataPath);
|
|
4932
|
+
}
|
|
4933
|
+
// If no array was found at the configured path, try auto-detecting
|
|
4934
|
+
// the array from common structures
|
|
4935
|
+
if (!Array.isArray(data)) {
|
|
4936
|
+
if (Array.isArray(res)) {
|
|
4937
|
+
// API returned a plain array
|
|
4938
|
+
data = res;
|
|
4939
|
+
}
|
|
4940
|
+
else if (res && typeof res === 'object') {
|
|
4941
|
+
// API returned an object keyed by entity-id e.g. { "1": [...], "2": [...] }
|
|
4942
|
+
// Flatten all value-arrays into one list
|
|
4943
|
+
data = Object.values(res)
|
|
4944
|
+
.filter(v => Array.isArray(v))
|
|
4945
|
+
.reduce((acc, arr) => acc.concat(arr), []);
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
this.libraryImages = Array.isArray(data) ? data : [];
|
|
4949
|
+
this.libraryLoading = false;
|
|
4950
|
+
},
|
|
4951
|
+
error: () => {
|
|
4952
|
+
this.libraryError = this.controller.labels['ERR_LIBRARY_LOAD_FAILED'] || 'Failed to load library images.';
|
|
4953
|
+
this.libraryLoading = false;
|
|
4954
|
+
}
|
|
4955
|
+
});
|
|
4956
|
+
}
|
|
4957
|
+
getLibraryItemUrl(item) {
|
|
4958
|
+
const cfg = this.config.attachmentConfig;
|
|
4959
|
+
return cfg?.libraryUrlPath ? this.getValueByPath(item, cfg.libraryUrlPath) : (item?.url || item?.completeURL || '');
|
|
4960
|
+
}
|
|
4961
|
+
getLibraryItemId(item) {
|
|
4962
|
+
const cfg = this.config.attachmentConfig;
|
|
4963
|
+
return cfg?.libraryIdPath ? this.getValueByPath(item, cfg.libraryIdPath) : (item?.id ?? item);
|
|
4964
|
+
}
|
|
4965
|
+
isLibraryItemSelected(item) {
|
|
4966
|
+
return this.librarySelectedIds.has(this.getLibraryItemId(item));
|
|
4967
|
+
}
|
|
4968
|
+
toggleLibraryItem(item) {
|
|
4969
|
+
const id = this.getLibraryItemId(item);
|
|
4970
|
+
if (this.librarySelectedIds.has(id)) {
|
|
4971
|
+
this.librarySelectedIds.delete(id);
|
|
4972
|
+
}
|
|
4973
|
+
else {
|
|
4974
|
+
this.librarySelectedIds.add(id);
|
|
4975
|
+
}
|
|
4976
|
+
}
|
|
4977
|
+
closeLibraryModal() {
|
|
4978
|
+
this.showLibraryModal = false;
|
|
4979
|
+
}
|
|
4980
|
+
confirmLibrarySelection() {
|
|
4981
|
+
// We already have completeURL on each library item — no upload API call needed.
|
|
4982
|
+
// Directly build MediaItem[] from the selected library images and append to form value.
|
|
4983
|
+
const selectedItems = this.libraryImages.filter(item => this.isLibraryItemSelected(item));
|
|
4984
|
+
if (!selectedItems.length) {
|
|
4985
|
+
this.showLibraryModal = false;
|
|
4986
|
+
return;
|
|
4987
|
+
}
|
|
4988
|
+
const cfg = this.config.attachmentConfig;
|
|
4989
|
+
const maxItems = cfg?.maxFiles ?? 10;
|
|
4990
|
+
const currentItems = [...this.mediaItems];
|
|
4991
|
+
let errorShown = false;
|
|
4992
|
+
selectedItems.forEach(item => {
|
|
4993
|
+
if (currentItems.length >= maxItems) {
|
|
4994
|
+
if (!errorShown) {
|
|
4995
|
+
this.showMediaError(this.controller.labels['ERR_MEDIA_MAX'] || `Maximum ${maxItems} media items allowed.`);
|
|
4996
|
+
errorShown = true;
|
|
4997
|
+
}
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
const url = this.getLibraryItemUrl(item); // item.completeURL
|
|
5001
|
+
if (!url)
|
|
5002
|
+
return;
|
|
5003
|
+
// Do NOT store the library item's id — library images were not POSTed to
|
|
5004
|
+
// the upload API, so there is no server-side attachment record to DELETE.
|
|
5005
|
+
// Without an id, removeMediaItem() always takes the local-remove path. ✓
|
|
5006
|
+
currentItems.push({
|
|
5007
|
+
mediaType: 'image',
|
|
5008
|
+
url,
|
|
5009
|
+
mimeType: item.mimeType,
|
|
5010
|
+
fileName: item.fileName ?? item.name,
|
|
5011
|
+
isUploading: false
|
|
5012
|
+
});
|
|
5013
|
+
});
|
|
5014
|
+
this.updateValue(currentItems);
|
|
5015
|
+
this.mediaCarouselIndex = currentItems.length - 1;
|
|
5016
|
+
this.showLibraryModal = false;
|
|
5017
|
+
}
|
|
5018
|
+
// ── Remove item ──────────────────────────────────────────────────────────
|
|
5019
|
+
removeMediaItem(index) {
|
|
5020
|
+
const items = [...this.mediaItems];
|
|
5021
|
+
const item = items[index];
|
|
5022
|
+
if (!item)
|
|
5023
|
+
return;
|
|
5024
|
+
const removeLocally = () => {
|
|
5025
|
+
const updated = items.filter((_, i) => i !== index);
|
|
5026
|
+
this.updateValue(updated.length ? updated : null);
|
|
5027
|
+
// Clamp carousel index so it never points past the end
|
|
5028
|
+
this.mediaCarouselIndex = Math.max(0, this.mediaCarouselIndex - (index <= this.mediaCarouselIndex ? 1 : 0));
|
|
5029
|
+
};
|
|
5030
|
+
const deleteUrl = this.config.attachmentConfig?.deleteUrl;
|
|
5031
|
+
if (item.id && item.id !== 0 && deleteUrl) {
|
|
5032
|
+
// ── API delete path (same logic as removeUploadedFile) ───────────────
|
|
5033
|
+
item.isUploading = true; // disable the remove button while in-flight
|
|
5034
|
+
this.updateValue([...items]); // trigger change detection
|
|
5035
|
+
let params = new HttpParams();
|
|
5036
|
+
if (!deleteUrl.includes('attachmentId=')) {
|
|
5037
|
+
params = params.set('attachmentId', item.id.toString());
|
|
5038
|
+
}
|
|
5039
|
+
if (!deleteUrl.includes('reason=')) {
|
|
5040
|
+
params = params.set('reason', 'DELETED BY CREATOR');
|
|
5041
|
+
}
|
|
5042
|
+
this.http.delete(deleteUrl, { headers: this.getHeaders(), params })
|
|
5043
|
+
.pipe(takeUntil(this.destroy$))
|
|
5044
|
+
.subscribe({
|
|
5045
|
+
next: () => removeLocally(),
|
|
5046
|
+
error: (err) => {
|
|
5047
|
+
console.error('Failed to delete media attachment', err);
|
|
5048
|
+
item.isUploading = false;
|
|
5049
|
+
this.updateValue([...items]); // restore UI state
|
|
5050
|
+
}
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
else {
|
|
5054
|
+
// ── No id or no deleteUrl: remove locally, no API call ───────────────
|
|
5055
|
+
removeLocally();
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
_appendMediaItem(item) {
|
|
5059
|
+
const current = [...this.mediaItems];
|
|
5060
|
+
current.push(item);
|
|
5061
|
+
this.updateValue(current);
|
|
5062
|
+
this.mediaCarouselIndex = current.length - 1;
|
|
5063
|
+
}
|
|
5064
|
+
showMediaError(msg) {
|
|
5065
|
+
this.mediaUploadError = msg;
|
|
5066
|
+
setTimeout(() => {
|
|
5067
|
+
if (this.mediaUploadError === msg)
|
|
5068
|
+
this.mediaUploadError = '';
|
|
5069
|
+
}, 4000);
|
|
5070
|
+
}
|
|
4658
5071
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FormFieldComponent, deps: [{ token: i1$2.FormBuilder }, { token: ExpressionService }, { token: i3.HttpClient }], target: i0.ɵɵFactoryTarget.Component });
|
|
4659
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: FormFieldComponent, isStandalone: false, selector: "lib-form-field", inputs: { config: "config", controller: "controller", formGroup: "formGroup", allowMulti: "allowMulti" }, ngImport: i0, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\r\n\r\n <!-- \u2550\u2550 ROW Layout \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRow\" class=\"form-row grid-row\">\r\n <ng-container *ngFor=\"let child of config.children\">\r\n <div class=\"row-field\" [style.gridColumn]=\"'span ' + getChildColSpan(child)\">\r\n <lib-form-field [config]=\"child\" [controller]=\"controller\" [formGroup]=\"formGroup\" [allowMulti]=\"allowMulti\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 allowMulti (repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig?.allowMulti\" class=\"group-section-wrapper\"\r\n [class.multi-save-active]=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n\r\n <!-- Multi-Save Header with Add button (top-right style) -->\r\n <div class=\"multi-save-header\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label\">{{ config.sectionConfig!.label }}</h3>\r\n <lib-button [variant]=\"'outline'\" [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\"\r\n class=\"btn-add-multi\">\r\n {{ addMultiLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Standard Header for non-multiSave -->\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label && !config.sectionConfig?.multiSaveConfig?.active\">{{\r\n config.sectionConfig!.label }}</h3>\r\n\r\n <div *ngFor=\"let instance of instanceList; trackBy: trackByInstanceId; let i = index\" class=\"group-instance\"\r\n [class.is-editing]=\"instance.isEditing\"\r\n [class.is-card]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n\r\n <!-- 1. EDIT MODE / UNSAVED / STANDARD STATE -->\r\n <div [hidden]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <!-- Instance header \u2014 show remove only when more than 1 instance -->\r\n <div class=\"group-header\" *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active && instanceList.length > 1\">\r\n <span class=\"group-number\">{{ config.sectionConfig!.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" [icon]=\"{type: 'material', value: 'delete_outline'}\"\r\n (click)=\"removeGroupInstance(i)\">\r\n {{ removeLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig!.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instance.fg\" [allowMulti]=\"true\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- SAVE / CANCEL BUTTONS for MultiSave in Editing phase -->\r\n <div class=\"group-footer\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <span class=\"field-error\" *ngIf=\"multiSaveError\">{{ multiSaveError }}</span>\r\n <div class=\"footer-actions\">\r\n <lib-button [variant]=\"'outline'\" (click)=\"cancelGroupInstance(i)\">Cancel</lib-button>\r\n <lib-button [variant]=\"'primary'\" (click)=\"saveGroupInstance(i)\">Save</lib-button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- 2. CARD VIEW (Saved State) -->\r\n <ng-container *ngIf=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <div class=\"card-view\" [class.is-expanded]=\"instance.isExpanded\">\r\n <div class=\"card-content\">\r\n <span class=\"card-title\">{{ instance.fg.get(config.sectionConfig.multiSaveConfig.summaryField || '')?.value\r\n || '\u2014' }}</span>\r\n <span class=\"card-desc\" *ngIf=\"config.sectionConfig.multiSaveConfig.descriptionField\">\r\n {{ instance.fg.get(config.sectionConfig.multiSaveConfig.descriptionField)?.value }}\r\n </span>\r\n </div>\r\n <div class=\"card-actions\">\r\n <mat-icon class=\"icon-delete\" (click)=\"removeGroupInstance(i, true)\">delete_outline</mat-icon>\r\n <mat-icon class=\"icon-edit\" (click)=\"editGroupInstance(i)\">edit_outline</mat-icon>\r\n <mat-icon class=\"icon-expand\" (click)=\"toggleExpandGroupInstance(i)\">\r\n {{ instance.isExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Standard Add Button for non-multiSave -->\r\n <lib-button *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active\" [variant]=\"'outline'\"\r\n [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\" class=\"btn-add-group-wrapper\">\r\n {{ addLabel }} {{ config.sectionConfig!.label }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 single (non-repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig && !config.sectionConfig.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig.label\">{{ config.sectionConfig.label }}</h3>\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"groupFormGroup\" [allowMulti]=\"false\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Text Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTextField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <textarea *ngIf=\"config.subType === 'LONG'\" class=\"field-input textarea\" [placeholder]=\"config.placeholder || ''\"\r\n [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\" rows=\"4\">\r\n </textarea>\r\n\r\n <!-- Password input with show/hide toggle -->\r\n <div *ngIf=\"config.subType === 'PASSWORD'\" class=\"password-wrapper\">\r\n <input [type]=\"showPassword ? 'text' : 'password'\" class=\"field-input password-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <button type=\"button\" class=\"password-toggle\" (click)=\"showPassword = !showPassword\" tabindex=\"-1\"\r\n [attr.aria-label]=\"showPassword ? 'Hide password' : 'Show password'\">\r\n <mat-icon class=\"eye-icon\">{{ showPassword ? 'visibility' : 'visibility_off' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input *ngIf=\"config.subType !== 'LONG' && config.subType !== 'PASSWORD'\"\r\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\" class=\"field-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\"\r\n [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Number Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input type=\"number\" class=\"field-input\" [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\"\r\n [min]=\"config.numberConfig?.min\" [max]=\"config.numberConfig?.max\" [step]=\"config.numberConfig?.step || 1\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Date Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDateField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input matInput [matDatepicker]=\"datePicker\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [min]=\"config.dateConfig?.minDate\" [max]=\"config.dateConfig?.maxDate\" [class.is-invalid]=\"errorMessage\"\r\n [placeholder]=\"config.placeholder || ''\" [readonly]=\"config.readonly\"\r\n (click)=\"!config.readonly && datePicker.open()\">\r\n <div class=\"input-suffix\"\r\n style=\"padding: 0; background: transparent; border: none; display: flex; align-items: center; justify-content: center; width: 40px; cursor: pointer;\">\r\n <mat-datepicker-toggle matSuffix [for]=\"datePicker\" [disabled]=\"config.readonly\"></mat-datepicker-toggle>\r\n </div>\r\n <mat-datepicker #datePicker></mat-datepicker>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Time Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTimeField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input type=\"time\" class=\"field-input time-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"!!config.readonly\">\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Autocomplete \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isAutocomplete\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Hidden real control (stores the code value) -->\r\n <input type=\"hidden\" [formControlName]=\"config.name!\">\r\n\r\n <div class=\"autocomplete-wrapper\" [class.is-invalid]=\"errorMessage\" [class.readonly]=\"config.readonly\">\r\n <!-- Search icon -->\r\n <mat-icon class=\"ac-search-icon\">search</mat-icon>\r\n\r\n <input class=\"field-input ac-input\" [formControl]=\"autocompleteInputCtrl\" [matAutocomplete]=\"auto\"\r\n [placeholder]=\"config.placeholder || 'Search\u2026'\" [readonly]=\"!!config.readonly\" [class.is-invalid]=\"errorMessage\"\r\n (blur)=\"onAutocompleteClear()\" autocomplete=\"off\">\r\n\r\n <!-- Clear button -->\r\n <button type=\"button\" class=\"ac-clear-btn\" *ngIf=\"autocompleteInputCtrl.value && !config.readonly\"\r\n (click)=\"autocompleteInputCtrl.setValue(''); updateValue(null)\" tabindex=\"-1\" aria-label=\"Clear\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n\r\n <mat-autocomplete #auto=\"matAutocomplete\" [panelWidth]=\"'auto'\">\r\n <mat-option *ngFor=\"let option of filteredOptions\" [value]=\"option.label\"\r\n (onSelectionChange)=\"onAutocompleteSelected(option)\">\r\n {{ option.label }}\r\n </mat-option>\r\n <mat-option *ngIf=\"filteredOptions.length === 0\" disabled class=\"ac-no-results\">\r\n No results found\r\n </mat-option>\r\n </mat-autocomplete>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Dropdown \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <select *ngIf=\"config.subType === 'SINGLE'\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option [ngValue]=\"null\" disabled selected>{{ config.placeholder || 'Select' }}</option>\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <select *ngIf=\"config.subType === 'MULTIPLE'\" class=\"field-input\" multiple [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Radio \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRadio\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"radio-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\r\n <input type=\"radio\" [formControlName]=\"config.name!\" [value]=\"option.code\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Checkbox \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\r\n <label class=\"checkbox-label\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span>{{ config.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Chip \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isChip\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"chip-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"chip-label\"\r\n [class.selected]=\"isChecked(option.code)\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\" style=\"display: none;\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Switch \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label class=\"switch-container\">\r\n <span class=\"field-label\">{{ config.label }}</span>\r\n <div class=\"switch\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span class=\"slider\"></span>\r\n </div>\r\n </label>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rich Text \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRichText\" class=\"field-wrapper component-rich-text\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rich-text-container\" [class.is-invalid]=\"errorMessage\">\r\n <quill-editor [formControlName]=\"config.name!\"\r\n [placeholder]=\"config.richTextConfig?.placeholder || config.placeholder || ''\"\r\n [styles]=\"{height: config.richTextConfig?.height || '200px'}\">\r\n </quill-editor>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rating \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRating\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rating-group\" [class.is-invalid]=\"errorMessage\">\r\n <span *ngFor=\"let star of getStarArray()\" class=\"star\" [class.filled]=\"isStarFilled(star)\"\r\n [class.half]=\"isStarHalf(star)\" (click)=\"onRatingChange(star, $event)\">\r\n <mat-icon>{{ isStarFilled(star) || isStarHalf(star) ? 'star' : 'star_border' }}</mat-icon>\r\n </span>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Generated Field (read-only) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\r\n <div class=\"generated-value\">{{ value || '-' }}</div>\r\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 File Upload \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isFileUpload\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Drop Zone -->\r\n <div class=\"upload-drop-zone\" [class.drag-over]=\"isDragOver\" [class.has-files]=\"value?.length\"\r\n [class.is-invalid]=\"errorMessage\" (dragover)=\"onDragOver($event)\" (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onFileDrop($event)\" (click)=\"fileInput.click()\">\r\n\r\n <div class=\"upload-icon-wrap\">\r\n <mat-icon class=\"upload-cloud-icon\">cloud_upload</mat-icon>\r\n </div>\r\n\r\n <p class=\"upload-main-text\">Drag and drop files here or <span class=\"upload-link\">click to upload</span></p>\r\n <p class=\"upload-hint-text\" *ngIf=\"config.attachmentConfig?.acceptLabel\">\r\n Supported formats:\r\n <span class=\"upload-formats\">{{ config.attachmentConfig!.acceptLabel }}</span>\r\n </p>\r\n <p class=\"upload-hint-text\" *ngIf=\"!config.attachmentConfig?.acceptLabel && config.hint\">\r\n {{ config.hint }}\r\n </p>\r\n\r\n <!-- Hidden native file input -->\r\n <input #fileInput type=\"file\" style=\"display:none\"\r\n [attr.multiple]=\"config.attachmentConfig?.multiple ? true : null\"\r\n [attr.accept]=\"config.attachmentConfig?.accept || null\" (change)=\"onFileSelected($event)\">\r\n </div>\r\n\r\n <!-- Uploaded file list -->\r\n <div class=\"uploaded-list\" *ngIf=\"value?.length\">\r\n <div *ngFor=\"let f of value; let i = index\" class=\"uploaded-item\" [class.uploading]=\"f.isUploading\">\r\n\r\n <!-- Uploading spinner (shown while API call is in progress) -->\r\n <ng-container *ngIf=\"f.isUploading; else fileReady\">\r\n <div class=\"upload-spinner\"></div>\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size uploading-label\">Uploading...</span>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Normal state once upload is done -->\r\n <ng-template #fileReady>\r\n <!-- File type icon -->\r\n <mat-icon class=\"file-type-icon\">{{ getFileIcon(f.type) }}</mat-icon>\r\n\r\n <!-- Image thumbnail (only for images) -->\r\n <img *ngIf=\"f.type?.startsWith('image') && f.dataUrl\" [src]=\"f.dataUrl\" class=\"file-thumb\" alt=\"preview\">\r\n\r\n <!-- Name & size -->\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size\">{{ formatFileSize(f.size) }}</span>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Remove button \u2014 disabled while uploading -->\r\n <lib-button [variant]=\"'danger-outline'\" [disabled]=\"f.isUploading\"\r\n (click)=\"!f.isUploading && removeUploadedFile(i)\">\r\n <mat-icon>close</mat-icon>\r\n </lib-button>\r\n </div>\r\n </div>\r\n\r\n <!-- Validation / file errors -->\r\n <span class=\"field-error\" *ngIf=\"fileUploadError\">{{ fileUploadError }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage && !fileUploadError\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\"\r\n *ngIf=\"config.hint && !errorMessage && !fileUploadError && !config.attachmentConfig?.acceptLabel\">\r\n {{ config.hint }}\r\n </span>\r\n </div>\r\n\r\n</div>", styles: [".form-field{margin-bottom:var(--cc-sf-grid-gap, 16px)}.form-field.has-error .field-input{border-color:var(--cc-sf-error-border, #DC2626)}.form-row{display:flex;gap:var(--cc-sf-grid-gap, 16px)}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.form-row.grid-row{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-row.grid-row{grid-template-columns:1fr}.form-row.grid-row .row-field{grid-column:span 12!important}}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:var(--cc-sf-label-weight, 500);color:var(--cc-sf-label-color, #111827);margin-bottom:.5rem;line-height:1.25rem}.field-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.field-input{width:100%;padding:var(--cc-sf-input-padding, .625rem .875rem);font-size:var(--cc-sf-input-font-size, .875rem);line-height:1.5;color:var(--cc-sf-input-color, #111827);background-color:var(--cc-sf-input-bg, #ffffff);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);border-radius:var(--cc-sf-input-radius, 8px);box-shadow:var(--cc-sf-input-shadow, none);transition:var(--cc-sf-input-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.field-input::placeholder{color:var(--cc-sf-input-placeholder, #9CA3AF)}.field-input:hover:not(:disabled):not([readonly]){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.field-input:focus{outline:none;border-color:var(--cc-sf-input-focus-border, #3B82F6);box-shadow:var(--cc-sf-input-focus-shadow, 0 0 0 3px rgba(59, 130, 246, .12))}.field-input:disabled,.field-input[readonly]{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.field-input.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.field-input.is-invalid:focus{box-shadow:var(--cc-sf-error-focus-shadow, 0 0 0 3px rgba(220, 38, 38, .1))}.field-input.textarea{resize:vertical;min-height:100px;font-family:var(--cc-sf-font-family, inherit)}input[type=time].time-input{cursor:pointer}input[type=time].time-input::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.7;filter:invert(30%)}input[type=time].time-input::-webkit-calendar-picker-indicator:hover{opacity:1}select.field-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}select.field-input:disabled{cursor:not-allowed}.field-hint{font-size:var(--cc-sf-hint-size, .75rem);color:var(--cc-sf-hint-color, #6B7280)}.field-error{font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--cc-sf-label-size, .875rem);color:var(--cc-sf-label-color, #111827)}.radio-label input,.checkbox-label input{cursor:pointer;accent-color:var(--cc-sf-chip-selected-bg, #3B82F6)}.checkbox-single .checkbox-label{font-weight:var(--cc-sf-label-weight, 500)}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:var(--cc-sf-chip-padding, 6px 14px);background:var(--cc-sf-chip-bg, #ffffff);color:var(--cc-sf-chip-color, #374151);border:var(--cc-sf-chip-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-chip-radius, 20px);cursor:pointer;font-size:var(--cc-sf-font-size-base, .875rem);transition:var(--cc-sf-input-transition, all .2s ease)}.chip-label:hover{background:var(--cc-sf-chip-hover-bg, #F3F4F6)}.chip-label.selected{background:var(--cc-sf-chip-selected-bg, #3B82F6);color:var(--cc-sf-chip-selected-color, #ffffff);border-color:var(--cc-sf-chip-selected-border, #3B82F6)}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:var(--cc-sf-switch-track-on, #3B82F6)}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--cc-sf-switch-track-off, #D1D5DB);transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:var(--cc-sf-switch-thumb, #ffffff);transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{display:inline-flex;align-items:center;cursor:pointer;transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star mat-icon{font-size:var(--cc-sf-star-size, 28px);width:var(--cc-sf-star-size, 28px);height:var(--cc-sf-star-size, 28px);line-height:var(--cc-sf-star-size, 28px);color:var(--cc-sf-star-empty, #D1D5DB);transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star.filled mat-icon,.rating-group .star.half mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.rating-group .star:hover mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.password-wrapper{position:relative;display:flex;align-items:center}.password-wrapper .password-input{padding-right:2.75rem;width:100%}.password-wrapper .password-toggle{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:.25rem;line-height:1;color:var(--cc-sf-hint-color, #6B7280);display:flex;align-items:center;justify-content:center;transition:color var(--cc-sf-input-transition, .2s ease)}.password-wrapper .password-toggle mat-icon.eye-icon{font-size:1.125rem;width:1.125rem;height:1.125rem;line-height:1.125rem}.password-wrapper .password-toggle:hover{color:var(--cc-sf-label-color, #374151)}.password-wrapper .password-toggle:focus{outline:none}.generated-value{padding:var(--cc-sf-generated-padding, .625rem .875rem);background:var(--cc-sf-generated-bg, #F3F4F6);border:var(--cc-sf-generated-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-generated-radius, 8px);font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-generated-color, #6B7280);font-family:var(--cc-sf-font-family, inherit)}.group-section-wrapper{margin-bottom:var(--cc-sf-section-gap, 20px);border:var(--cc-sf-section-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-section-radius, 10px);padding:var(--cc-sf-section-padding, 20px);background-color:var(--cc-sf-section-bg, #ffffff);box-shadow:var(--cc-sf-section-shadow, 0 1px 4px rgba(0, 0, 0, .05))}.group-section-wrapper .group-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.group-section-wrapper .group-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.group-section-wrapper .group-fields.sf-grid{grid-template-columns:1fr}.group-section-wrapper .group-fields.sf-grid .sf-col{grid-column:span 12!important}}.group-section-wrapper .group-instance{position:relative;margin-bottom:16px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px)}.group-section-wrapper .group-instance:last-child{margin-bottom:0}.group-section-wrapper .group-instance .group-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:10px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.group-section-wrapper .group-instance .group-header .group-number{font-weight:500;color:var(--cc-sf-instance-num-color, #4B5563);font-size:var(--cc-sf-instance-num-size, .8125rem)}.group-section-wrapper.multi-save-active{border:none;box-shadow:none;padding:0;background:transparent}.group-section-wrapper.multi-save-active .group-label{border:none;padding-bottom:0;margin-bottom:0}.group-section-wrapper.multi-save-active .multi-save-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button{color:var(--ms-btn-add-color, #3B82F6);font-weight:600;font-size:.875rem;padding:8px 12px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button:hover{color:var(--ms-btn-add-hover, #2563EB);background-color:var(--cc-sf-btn-add-hover-bg, #EFF6FF)}.group-section-wrapper.multi-save-active .group-instance{background:var(--ms-card-bg, #ffffff);border:1px solid var(--ms-card-border, #E5E7EB);border-radius:var(--ms-card-radius, 10px);box-shadow:var(--ms-card-shadow, 0 1px 4px rgba(0, 0, 0, .05));padding:0;margin-bottom:16px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-editing{padding:24px}.group-section-wrapper.multi-save-active .group-instance.is-card{cursor:pointer;transition:all .2s ease-in-out}.group-section-wrapper.multi-save-active .group-instance.is-card:hover{box-shadow:var(--ms-card-shadow-hover, 0 8px 24px rgba(0, 0, 0, .08));border-color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view{display:flex;justify-content:space-between;align-items:center;padding:18px 24px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content{flex:1;display:flex;flex-direction:column;gap:4px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-title{font-size:1rem;font-weight:600;color:var(--ms-title-color, #111827);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-desc{font-size:.875rem;color:var(--ms-desc-color, #6B7280);line-height:1.4;display:-webkit-box;-webkit-line-clamp:1;line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view.is-expanded .card-content .card-desc{-webkit-line-clamp:unset;line-clamp:unset}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions{display:flex;align-items:center;gap:16px;margin-left:20px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon{font-size:22px;width:22px;height:22px;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .2s}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-delete:hover{color:var(--cc-sf-error-border, #DC2626)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-edit:hover{color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-expand{color:var(--cc-sf-input-disabled-border, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:24px;padding-top:20px;border-top:1px solid var(--cc-sf-instance-divider, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer .footer-actions{display:flex;gap:12px}.btn-remove{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:var(--cc-sf-error-text-size, .75rem);transition:var(--cc-sf-btn-transition, all .2s ease)}.btn-remove mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.btn-add-group{display:flex;align-items:center;justify-content:center;gap:4px;width:100%;padding:8px 16px;margin-top:12px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.btn-add-group mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.btn-add-group:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}.upload-drop-zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 24px;border:var(--cc-sf-dropzone-border, 1.5px dashed #CBD5E1);border-radius:var(--cc-sf-dropzone-radius, 12px);background-color:var(--cc-sf-dropzone-bg, #F8FAFC);cursor:pointer;transition:background-color .2s ease,border-color .2s ease;text-align:center;-webkit-user-select:none;user-select:none}.upload-drop-zone:hover{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-hover-border, #93C5FD)}.upload-drop-zone.drag-over{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-over-border, #3B82F6);box-shadow:var(--cc-sf-dropzone-over-shadow, 0 0 0 4px rgba(59, 130, 246, .12))}.upload-drop-zone.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.upload-icon-wrap{margin-bottom:4px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-icon-wrap mat-icon.upload-cloud-icon{font-size:56px;width:56px;height:56px;line-height:56px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-main-text{font-size:.9rem;font-weight:600;color:var(--cc-sf-label-color, #1E293B);margin:0}.upload-main-text .upload-link{color:var(--cc-sf-dropzone-link-color, #3B82F6)}.upload-hint-text{font-size:.78rem;color:var(--cc-sf-dropzone-hint-color, #64748B);margin:0}.upload-hint-text .upload-formats{color:var(--cc-sf-dropzone-link-color, #3B82F6);font-weight:500}.uploaded-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}.uploaded-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cc-sf-uploaded-item-bg, #ffffff);border:var(--cc-sf-uploaded-item-border, 1px solid #E2E8F0);border-radius:var(--cc-sf-uploaded-item-radius, 8px);transition:box-shadow .15s ease}.uploaded-item:hover{box-shadow:0 2px 6px #0000000f}.uploaded-item mat-icon.file-type-icon{font-size:20px;width:20px;height:20px;line-height:20px;flex-shrink:0;color:var(--cc-sf-hint-color, #64748B)}.uploaded-item .file-thumb{width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0}.uploaded-item .file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.uploaded-item .file-info .file-name{font-size:.85rem;font-weight:500;color:var(--cc-sf-label-color, #1E293B);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uploaded-item .file-info .file-size{font-size:.72rem;color:var(--cc-sf-hint-color, #94A3B8)}.uploaded-item .file-remove-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:var(--cc-sf-uploaded-remove-color, #94A3B8);padding:4px;border-radius:4px;flex-shrink:0;transition:color .15s ease,background .15s ease}.uploaded-item .file-remove-btn mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.uploaded-item .file-remove-btn:hover{color:var(--cc-sf-uploaded-remove-hover-color, #DC2626);background:var(--cc-sf-uploaded-remove-hover-bg, #FEF2F2)}.uploaded-item.uploading{background:var(--cc-sf-uploaded-uploading-bg, #F8FAFC);border-color:var(--cc-sf-uploaded-uploading-border, #CBD5E1);opacity:.85}.upload-spinner{width:20px;height:20px;flex-shrink:0;border:2px solid var(--cc-sf-spinner-track, #E2E8F0);border-top-color:var(--cc-sf-spinner-color, #3B82F6);border-radius:50%;animation:cc-spin .7s linear infinite}@keyframes cc-spin{to{transform:rotate(360deg)}}.uploading-label{color:var(--cc-sf-spinner-color, #3B82F6)!important;font-style:italic}.input-group{position:relative;display:flex;align-items:stretch;width:100%}.input-group .field-input{flex:1;border-radius:0}.input-group .field-input:first-child{border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-group .field-input:last-child{border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px)}.input-group.readonly .field-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);cursor:default;padding-right:3.5rem}.input-group.readonly .input-prefix,.input-group.readonly .input-suffix{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6)}.input-prefix,.input-suffix{display:flex;align-items:center;padding:0 .875rem;background-color:var(--cc-sf-input-bg, #FFFFFF);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);color:var(--cc-sf-hint-color, #6B7280);font-size:var(--cc-sf-input-font-size, .875rem);white-space:nowrap;-webkit-user-select:none;user-select:none}.input-prefix{border-right:none;border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-suffix{border-left:none;border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px);color:var(--cc-sf-chip-selected-bg, #3B82F6);font-weight:500}.readonly-icons{position:absolute;right:.875rem;top:50%;transform:translateY(-50%);display:flex;gap:8px;pointer-events:none}.readonly-icons mat-icon.lock-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem;opacity:.5;color:var(--cc-sf-hint-color, #6B7280)}.subfields-group-wrapper{margin-bottom:var(--cc-sf-grid-gap, 16px)}.subfields-group-wrapper .group-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:600;color:var(--cc-sf-label-color, #111827);margin-bottom:.75rem}.subfields-group-wrapper .group-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.subfields-group-wrapper .subfields-row{display:flex;align-items:flex-end;gap:12px;border-radius:var(--cc-sf-input-radius, 8px);transition:all .2s ease}.subfields-group-wrapper .subfields-row.is-invalid .subfield-item ::ng-deep .field-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.subfields-group-wrapper .subfields-row .subfield-item{flex:1;min-width:0}.subfields-group-wrapper .subfields-row .subfield-item ::ng-deep .field-label{font-size:.75rem!important;margin-bottom:4px!important;font-weight:500!important;color:var(--cc-sf-hint-color, #6B7280)!important}.subfields-group-wrapper .subfields-row .subfield-separator{margin-bottom:24px;font-weight:700;color:#94a3b8}.subfields-group-wrapper .subfields-group-error{display:block;margin-top:6px;font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.autocomplete-wrapper{position:relative;display:flex;align-items:center;width:100%}.autocomplete-wrapper .ac-search-icon{position:absolute;left:.75rem;font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem;color:var(--cc-sf-hint-color, #9CA3AF);pointer-events:none;z-index:1;transition:color var(--cc-sf-input-transition, .2s ease)}.autocomplete-wrapper .ac-input{flex:1;padding-left:2.4rem;padding-right:2.4rem}.autocomplete-wrapper .ac-clear-btn{position:absolute;right:.6rem;display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:.2rem;border-radius:50%;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .15s ease,background .15s ease}.autocomplete-wrapper .ac-clear-btn mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.autocomplete-wrapper .ac-clear-btn:hover{color:var(--cc-sf-label-color, #374151);background:var(--cc-sf-input-disabled-bg, #F3F4F6)}.autocomplete-wrapper .ac-clear-btn:focus{outline:none}.autocomplete-wrapper:focus-within .ac-search-icon{color:var(--cc-sf-input-focus-border, #3B82F6)}.autocomplete-wrapper.is-invalid .ac-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.autocomplete-wrapper.readonly .ac-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.ac-no-results{font-style:italic;font-size:.8125rem;color:var(--cc-sf-hint-color, #6B7280)}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.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: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i7.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i7.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "directive", type: i8.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i9.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "component", type: ButtonComponent, selector: "lib-button", inputs: ["variant", "type", "disabled", "width", "height", "borderRadius", "fontSize", "fontWeight", "backgroundColor", "color", "border", "icon", "labels"] }, { kind: "component", type: i11.QuillEditorComponent, selector: "quill-editor" }, { kind: "component", type: FormFieldComponent, selector: "lib-form-field", inputs: ["config", "controller", "formGroup", "allowMulti"] }] });
|
|
5072
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: FormFieldComponent, isStandalone: false, selector: "lib-form-field", inputs: { config: "config", controller: "controller", formGroup: "formGroup", allowMulti: "allowMulti" }, viewQueries: [{ propertyName: "mediaDeviceInput", first: true, predicate: ["mediaDeviceInput"], descendants: true }], ngImport: i0, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\r\n\r\n <!-- \u2550\u2550 ROW Layout \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRow\" class=\"form-row grid-row\">\r\n <ng-container *ngFor=\"let child of config.children\">\r\n <div class=\"row-field\" [style.gridColumn]=\"'span ' + getChildColSpan(child)\">\r\n <lib-form-field [config]=\"child\" [controller]=\"controller\" [formGroup]=\"formGroup\" [allowMulti]=\"allowMulti\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 allowMulti (repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig?.allowMulti\" class=\"group-section-wrapper\"\r\n [class.multi-save-active]=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n\r\n <!-- Multi-Save Header with Add button (top-right style) -->\r\n <div class=\"multi-save-header\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label\">{{ config.sectionConfig!.label }}</h3>\r\n <lib-button [variant]=\"'outline'\" [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\"\r\n class=\"btn-add-multi\">\r\n {{ addMultiLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Standard Header for non-multiSave -->\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label && !config.sectionConfig?.multiSaveConfig?.active\">{{\r\n config.sectionConfig!.label }}</h3>\r\n\r\n <div *ngFor=\"let instance of instanceList; trackBy: trackByInstanceId; let i = index\" class=\"group-instance\"\r\n [class.is-editing]=\"instance.isEditing\"\r\n [class.is-card]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n\r\n <!-- 1. EDIT MODE / UNSAVED / STANDARD STATE -->\r\n <div [hidden]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <!-- Instance header \u2014 show remove only when more than 1 instance -->\r\n <div class=\"group-header\" *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active && instanceList.length > 1\">\r\n <span class=\"group-number\">{{ config.sectionConfig!.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" [icon]=\"{type: 'material', value: 'delete_outline'}\"\r\n (click)=\"removeGroupInstance(i)\">\r\n {{ removeLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig!.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instance.fg\" [allowMulti]=\"true\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- SAVE / CANCEL BUTTONS for MultiSave in Editing phase -->\r\n <div class=\"group-footer\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <span class=\"field-error\" *ngIf=\"multiSaveError\">{{ multiSaveError }}</span>\r\n <div class=\"footer-actions\">\r\n <lib-button [variant]=\"'outline'\" (click)=\"cancelGroupInstance(i)\">Cancel</lib-button>\r\n <lib-button [variant]=\"'primary'\" (click)=\"saveGroupInstance(i)\">Save</lib-button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- 2. CARD VIEW (Saved State) -->\r\n <ng-container *ngIf=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <div class=\"card-view\" [class.is-expanded]=\"instance.isExpanded\">\r\n <div class=\"card-content\">\r\n <span class=\"card-title\">{{ instance.fg.get(config.sectionConfig.multiSaveConfig.summaryField || '')?.value\r\n || '\u2014' }}</span>\r\n <span class=\"card-desc\" *ngIf=\"config.sectionConfig.multiSaveConfig.descriptionField\">\r\n {{ instance.fg.get(config.sectionConfig.multiSaveConfig.descriptionField)?.value }}\r\n </span>\r\n </div>\r\n <div class=\"card-actions\">\r\n <mat-icon class=\"icon-delete\" (click)=\"removeGroupInstance(i, true)\">delete_outline</mat-icon>\r\n <mat-icon class=\"icon-edit\" (click)=\"editGroupInstance(i)\">edit_outline</mat-icon>\r\n <mat-icon class=\"icon-expand\" (click)=\"toggleExpandGroupInstance(i)\">\r\n {{ instance.isExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Standard Add Button for non-multiSave -->\r\n <lib-button *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active\" [variant]=\"'outline'\"\r\n [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\" class=\"btn-add-group-wrapper\">\r\n {{ addLabel }} {{ config.sectionConfig!.label }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 single (non-repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig && !config.sectionConfig.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig.label\">{{ config.sectionConfig.label }}</h3>\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"groupFormGroup\" [allowMulti]=\"false\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Text Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTextField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <textarea *ngIf=\"config.subType === 'LONG'\" class=\"field-input textarea\" [placeholder]=\"config.placeholder || ''\"\r\n [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\" rows=\"4\">\r\n </textarea>\r\n\r\n <!-- Password input with show/hide toggle -->\r\n <div *ngIf=\"config.subType === 'PASSWORD'\" class=\"password-wrapper\">\r\n <input [type]=\"showPassword ? 'text' : 'password'\" class=\"field-input password-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <button type=\"button\" class=\"password-toggle\" (click)=\"showPassword = !showPassword\" tabindex=\"-1\"\r\n [attr.aria-label]=\"showPassword ? 'Hide password' : 'Show password'\">\r\n <mat-icon class=\"eye-icon\">{{ showPassword ? 'visibility' : 'visibility_off' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input *ngIf=\"config.subType !== 'LONG' && config.subType !== 'PASSWORD'\"\r\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\" class=\"field-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\"\r\n [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n <div class=\"char-count-hint\" *ngIf=\"showCharCount\">\r\n {{ remainingCharacters }} characters remaining\r\n </div>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Number Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input type=\"number\" class=\"field-input\" [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\"\r\n [min]=\"config.numberConfig?.min\" [max]=\"config.numberConfig?.max\" [step]=\"config.numberConfig?.step || 1\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Date Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDateField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input matInput [matDatepicker]=\"datePicker\" class=\"field-input date-input has-icon-right\"\r\n [formControlName]=\"config.name!\" [min]=\"config.dateConfig?.minDate\" [max]=\"config.dateConfig?.maxDate\"\r\n [class.is-invalid]=\"errorMessage\" [placeholder]=\"config.placeholder || ''\" [readonly]=\"config.readonly\"\r\n (click)=\"!config.readonly && datePicker.open()\">\r\n <div class=\"date-icon-wrapper\" *ngIf=\"!config.readonly\">\r\n <mat-datepicker-toggle matSuffix [for]=\"datePicker\"></mat-datepicker-toggle>\r\n </div>\r\n <mat-datepicker #datePicker></mat-datepicker>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Time Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTimeField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input type=\"time\" class=\"field-input time-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"!!config.readonly\">\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Autocomplete \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isAutocomplete\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Hidden real control (stores the code value) -->\r\n <input type=\"hidden\" [formControlName]=\"config.name!\">\r\n\r\n <div class=\"autocomplete-wrapper\" [class.is-invalid]=\"errorMessage\" [class.readonly]=\"config.readonly\">\r\n <!-- Search icon -->\r\n <mat-icon class=\"ac-search-icon\">search</mat-icon>\r\n\r\n <input class=\"field-input ac-input\" [formControl]=\"autocompleteInputCtrl\" [matAutocomplete]=\"auto\"\r\n [placeholder]=\"config.placeholder || 'Search\u2026'\" [readonly]=\"!!config.readonly\" [class.is-invalid]=\"errorMessage\"\r\n (blur)=\"onAutocompleteClear()\" autocomplete=\"off\">\r\n\r\n <!-- Clear button -->\r\n <button type=\"button\" class=\"ac-clear-btn\" *ngIf=\"autocompleteInputCtrl.value && !config.readonly\"\r\n (click)=\"autocompleteInputCtrl.setValue(''); updateValue(null)\" tabindex=\"-1\" aria-label=\"Clear\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n\r\n <mat-autocomplete #auto=\"matAutocomplete\" [panelWidth]=\"'auto'\">\r\n <mat-option *ngFor=\"let option of filteredOptions\" [value]=\"option.label\"\r\n (onSelectionChange)=\"onAutocompleteSelected(option)\">\r\n {{ option.label }}\r\n </mat-option>\r\n <mat-option *ngIf=\"filteredOptions.length === 0\" disabled class=\"ac-no-results\">\r\n No results found\r\n </mat-option>\r\n </mat-autocomplete>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Dropdown \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <select *ngIf=\"config.subType === 'SINGLE'\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option [ngValue]=\"null\" disabled selected>{{ config.placeholder || 'Select' }}</option>\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <select *ngIf=\"config.subType === 'MULTIPLE'\" class=\"field-input\" multiple [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Radio \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRadio\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"radio-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\r\n <input type=\"radio\" [formControlName]=\"config.name!\" [value]=\"option.code\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Checkbox \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\r\n <label class=\"checkbox-label\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span>{{ config.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Chip \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isChip\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"chip-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"chip-label\"\r\n [class.selected]=\"isChecked(option.code)\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\" hidden>\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Switch \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label class=\"switch-container\">\r\n <span class=\"field-label\">{{ config.label }}</span>\r\n <div class=\"switch\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span class=\"slider\"></span>\r\n </div>\r\n </label>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rich Text \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRichText\" class=\"field-wrapper component-rich-text\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rich-text-container\" [class.is-invalid]=\"errorMessage\">\r\n <quill-editor [formControlName]=\"config.name!\" class=\"rich-text-editor\"\r\n [placeholder]=\"config.richTextConfig?.placeholder || config.placeholder || ''\"\r\n [styles]=\"{height: config.richTextConfig?.height || '200px'}\">\r\n </quill-editor>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n <div class=\"char-count-hint\" *ngIf=\"showCharCount\">\r\n {{ remainingCharacters }} characters remaining\r\n </div>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rating \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRating\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rating-group\" [class.is-invalid]=\"errorMessage\">\r\n <span *ngFor=\"let star of getStarArray()\" class=\"star\" [class.filled]=\"isStarFilled(star)\"\r\n [class.half]=\"isStarHalf(star)\" (click)=\"onRatingChange(star, $event)\">\r\n <mat-icon>{{ isStarFilled(star) || isStarHalf(star) ? 'star' : 'star_border' }}</mat-icon>\r\n </span>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Generated Field (read-only) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\r\n <div class=\"generated-value\">{{ value || '-' }}</div>\r\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 File Upload \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isFileUpload\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Drop Zone -->\r\n <div class=\"upload-drop-zone\" [class.drag-over]=\"isDragOver\" [class.has-files]=\"value?.length\"\r\n [class.is-invalid]=\"errorMessage\" (dragover)=\"onDragOver($event)\" (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onFileDrop($event)\" (click)=\"fileInput.click()\">\r\n\r\n <div class=\"upload-icon-wrap\">\r\n <mat-icon class=\"upload-cloud-icon\">cloud_upload</mat-icon>\r\n </div>\r\n\r\n <p class=\"upload-main-text\">Drag and drop files here or <span class=\"upload-link\">click to upload</span></p>\r\n <p class=\"upload-hint-text\" *ngIf=\"config.attachmentConfig?.acceptLabel\">\r\n Supported formats:\r\n <span class=\"upload-formats\">{{ config.attachmentConfig!.acceptLabel }}</span>\r\n </p>\r\n <p class=\"upload-hint-text\" *ngIf=\"!config.attachmentConfig?.acceptLabel && config.hint\">\r\n {{ config.hint }}\r\n </p>\r\n\r\n <!-- Hidden native file input -->\r\n <input #fileInput type=\"file\" hidden [attr.multiple]=\"config.attachmentConfig?.multiple ? true : null\"\r\n [attr.accept]=\"config.attachmentConfig?.accept || null\" (change)=\"onFileSelected($event)\">\r\n </div>\r\n\r\n <!-- Uploaded file list -->\r\n <div class=\"uploaded-list\" *ngIf=\"value?.length\">\r\n <div *ngFor=\"let f of value; let i = index\" class=\"uploaded-item\" [class.uploading]=\"f.isUploading\">\r\n\r\n <!-- Uploading spinner (shown while API call is in progress) -->\r\n <ng-container *ngIf=\"f.isUploading; else fileReady\">\r\n <div class=\"upload-spinner\"></div>\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size uploading-label\">Uploading...</span>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Normal state once upload is done -->\r\n <ng-template #fileReady>\r\n <!-- File type icon -->\r\n <mat-icon class=\"file-type-icon\">{{ getFileIcon(f.type) }}</mat-icon>\r\n\r\n <!-- Image thumbnail (only for images) -->\r\n <img *ngIf=\"f.type?.startsWith('image') && f.dataUrl\" [src]=\"f.dataUrl\" class=\"file-thumb\" alt=\"preview\">\r\n\r\n <!-- Name & size -->\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size\">{{ formatFileSize(f.size) }}</span>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Remove button \u2014 disabled while uploading -->\r\n <lib-button [variant]=\"'danger-outline'\" [disabled]=\"f.isUploading\"\r\n (click)=\"!f.isUploading && removeUploadedFile(i)\">\r\n <mat-icon>close</mat-icon>\r\n </lib-button>\r\n </div>\r\n </div>\r\n\r\n <!-- Validation / file errors -->\r\n <span class=\"field-error\" *ngIf=\"fileUploadError\">{{ fileUploadError }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage && !fileUploadError\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\"\r\n *ngIf=\"config.hint && !errorMessage && !fileUploadError && !config.attachmentConfig?.acceptLabel\">\r\n {{ config.hint }}\r\n </span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Media Upload (Type 2) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isMediaUpload\" class=\"field-wrapper media-upload-wrapper\" [formGroup]=\"formGroup\">\r\n\r\n <!-- Hidden file input lives outside *ngIf \u2014 triggered via ViewChild -->\r\n <input #mediaDeviceInput type=\"file\" hidden multiple accept=\"image/*\" (change)=\"onMediaFileSelected($event)\">\r\n\r\n <!-- Two-column layout -->\r\n <div class=\"mu-layout\">\r\n\r\n <!-- \u2500\u2500 LEFT PANEL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"mu-left\">\r\n\r\n <!-- Header: title + max-items badge -->\r\n <div class=\"mu-header\">\r\n <h3 class=\"mu-title\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </h3>\r\n <span class=\"mu-badge\" *ngIf=\"config.attachmentConfig?.maxFiles\">\r\n {{ controller.labels['LBL_MEDIA_MAX_PREFIX'] || 'Maximum' }}\r\n {{ config.attachmentConfig?.maxFiles }}\r\n {{ controller.labels['LBL_MEDIA_MAX_SUFFIX'] || 'Image & Videos' }}\r\n </span>\r\n </div>\r\n\r\n <!-- Description -->\r\n <p class=\"mu-description\" *ngIf=\"config.attachmentConfig?.description\">\r\n {{ config.attachmentConfig?.description }}\r\n </p>\r\n <p class=\"mu-description\" *ngIf=\"!config.attachmentConfig?.description && controller.labels['LBL_MEDIA_DESC']\">\r\n {{ controller.labels['LBL_MEDIA_DESC'] }}\r\n </p>\r\n\r\n <!-- Feature bullet list -->\r\n <ul class=\"mu-features\"\r\n *ngIf=\"config.attachmentConfig?.features?.length || controller.labels['LBL_MEDIA_FEATURE_1']\">\r\n <ng-container *ngIf=\"config.attachmentConfig?.features?.length\">\r\n <li *ngFor=\"let f of config.attachmentConfig?.features\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ f }}\r\n </li>\r\n </ng-container>\r\n <ng-container *ngIf=\"!config.attachmentConfig?.features?.length\">\r\n <li *ngIf=\"controller.labels['LBL_MEDIA_FEATURE_1']\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ controller.labels['LBL_MEDIA_FEATURE_1'] }}\r\n </li>\r\n <li *ngIf=\"controller.labels['LBL_MEDIA_FEATURE_2']\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ controller.labels['LBL_MEDIA_FEATURE_2'] }}\r\n </li>\r\n <li *ngIf=\"controller.labels['LBL_MEDIA_FEATURE_3']\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ controller.labels['LBL_MEDIA_FEATURE_3'] }}\r\n </li>\r\n </ng-container>\r\n </ul>\r\n\r\n <!-- Backdrop to close dropdown on outside click -->\r\n <div class=\"media-menu-backdrop\" *ngIf=\"showMediaMenu\"\r\n (click)=\"$event.stopPropagation(); showMediaMenu = false\"></div>\r\n\r\n <!-- Add Media button + dropdown -->\r\n <div class=\"media-add-container\" (click)=\"showMediaMenu = !showMediaMenu\">\r\n <lib-button id=\"btn-add-media-{{ config.name }}\" [variant]=\"'warning'\"\r\n [icon]=\"{type: 'material', value: 'add_photo_alternate'}\">\r\n {{ controller.labels['LBL_ADD_MEDIA'] || 'Add media' }}\r\n <mat-icon class=\"menu-chevron\">add</mat-icon>\r\n </lib-button>\r\n\r\n <div class=\"media-dropdown\" *ngIf=\"showMediaMenu\" role=\"menu\" (click)=\"$event.stopPropagation()\">\r\n <!-- Video -->\r\n <button id=\"btn-media-video-{{ config.name }}\" type=\"button\" class=\"media-dropdown-item\"\r\n (click)=\"onMediaMenuVideo(); showMediaMenu = false\" role=\"menuitem\">\r\n <span class=\"media-drop-icon media-drop-icon--video\"><mat-icon>videocam</mat-icon></span>\r\n <span class=\"media-drop-text\">\r\n <span class=\"media-drop-label\">{{ controller.labels['LBL_MEDIA_VIDEO'] || 'Video' }}</span>\r\n <span class=\"media-drop-desc\">{{ controller.labels['LBL_MEDIA_VIDEO_DESC'] || 'Add YouTube URL'\r\n }}</span>\r\n </span>\r\n </button>\r\n <!-- Device -->\r\n <button id=\"btn-media-device-{{ config.name }}\" type=\"button\" class=\"media-dropdown-item\"\r\n (click)=\"onMediaMenuDevice(); showMediaMenu = false\" role=\"menuitem\">\r\n <span class=\"media-drop-icon media-drop-icon--device\"><mat-icon>upload</mat-icon></span>\r\n <span class=\"media-drop-text\">\r\n <span class=\"media-drop-label\">{{ controller.labels['LBL_MEDIA_DEVICE'] || 'Upload from device'\r\n }}</span>\r\n <span class=\"media-drop-desc\">{{ controller.labels['LBL_MEDIA_DEVICE_DESC'] || 'Select images from your\r\n computer' }}</span>\r\n </span>\r\n </button>\r\n <!-- Library -->\r\n <button id=\"btn-media-library-{{ config.name }}\" type=\"button\" class=\"media-dropdown-item\"\r\n (click)=\"onMediaMenuLibrary(); showMediaMenu = false\" role=\"menuitem\">\r\n <span class=\"media-drop-icon media-drop-icon--library\"><mat-icon>photo_library</mat-icon></span>\r\n <span class=\"media-drop-text\">\r\n <span class=\"media-drop-label\">{{ controller.labels['LBL_MEDIA_LIBRARY'] || 'Upload from library'\r\n }}</span>\r\n <span class=\"media-drop-desc\">{{ controller.labels['LBL_MEDIA_LIBRARY_DESC'] || 'Choose from default\r\n images' }}</span>\r\n </span>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- YouTube URL Input (inline below button) -->\r\n <div class=\"youtube-input-panel\" *ngIf=\"showYoutubeInput\">\r\n <label class=\"youtube-panel-label\">\r\n {{ controller.labels['LBL_YOUTUBE_URL'] || 'Video URL' }}\r\n </label>\r\n <div class=\"youtube-input-row\">\r\n <input id=\"input-youtube-url-{{ config.name }}\" type=\"url\" class=\"field-input youtube-url-input\"\r\n [(ngModel)]=\"youtubeUrlInput\" [ngModelOptions]=\"{standalone: true}\"\r\n [placeholder]=\"controller.labels['PH_YOUTUBE_URL'] || 'Video URL'\" (keyup.enter)=\"addYoutubeMedia()\">\r\n <lib-button id=\"btn-add-youtube-{{ config.name }}\" [variant]=\"'secondary'\" (click)=\"addYoutubeMedia()\">\r\n {{ controller.labels['LBL_ADD'] || 'Add' }}\r\n </lib-button>\r\n </div>\r\n <span class=\"field-error\" *ngIf=\"youtubeUrlError\">{{ youtubeUrlError }}</span>\r\n </div>\r\n\r\n <div class=\"media-upload-status\" *ngIf=\"mediaUploadError\">\r\n <mat-icon class=\"status-icon\">error_outline</mat-icon>\r\n <span>{{ mediaUploadError }}</span>\r\n </div>\r\n\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n </div>\r\n <!-- end left panel -->\r\n\r\n <!-- \u2500\u2500 RIGHT PANEL (carousel) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"mu-right\">\r\n\r\n <!-- Carousel (when items exist) -->\r\n <div class=\"media-carousel-section\" *ngIf=\"mediaItems.length\">\r\n <div class=\"media-carousel-main\">\r\n <button id=\"btn-carousel-prev-{{ config.name }}\" type=\"button\" class=\"carousel-nav carousel-nav--prev\"\r\n (click)=\"mediaCarouselPrev()\" [disabled]=\"mediaCarouselIndex === 0\" aria-label=\"Previous\">\r\n <mat-icon>chevron_left</mat-icon>\r\n </button>\r\n\r\n <div class=\"carousel-viewer\" *ngFor=\"let item of mediaItems; let i = index\"\r\n [hidden]=\"i !== mediaCarouselIndex\">\r\n <div *ngIf=\"item.isUploading\" class=\"carousel-uploading\">\r\n <div class=\"carousel-spinner\"></div>\r\n <span>{{ controller.labels['LBL_UPLOADING'] || 'Uploading\u2026' }}</span>\r\n </div>\r\n <ng-container *ngIf=\"!item.isUploading && item.mediaType === 'youtube'\">\r\n <iframe class=\"carousel-iframe\" [src]=\"item.url | trustedUrl\" frameborder=\"0\" allowfullscreen\r\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\">\r\n </iframe>\r\n </ng-container>\r\n <ng-container *ngIf=\"!item.isUploading && item.mediaType === 'image'\">\r\n <img class=\"carousel-image\" [src]=\"item.url\" alt=\"Media\">\r\n </ng-container>\r\n <button id=\"btn-remove-media-{{ config.name }}-{{ i }}\" type=\"button\" class=\"carousel-remove-btn\"\r\n [disabled]=\"item.isUploading\" (click)=\"removeMediaItem(i)\" aria-label=\"Remove\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <button id=\"btn-carousel-next-{{ config.name }}\" type=\"button\" class=\"carousel-nav carousel-nav--next\"\r\n (click)=\"mediaCarouselNext()\" [disabled]=\"mediaCarouselIndex === mediaItems.length - 1\" aria-label=\"Next\">\r\n <mat-icon>chevron_right</mat-icon>\r\n </button>\r\n\r\n <div class=\"carousel-dots\">\r\n <span *ngFor=\"let item of mediaItems; let i = index\" class=\"carousel-dot\"\r\n [class.active]=\"i === mediaCarouselIndex\" (click)=\"mediaGoTo(i)\"></span>\r\n </div>\r\n </div>\r\n\r\n <!-- Thumbnail strip -->\r\n <div class=\"media-thumbnail-strip\">\r\n <div *ngFor=\"let item of mediaThumbnails; let i = index\" class=\"media-thumb\"\r\n [class.active]=\"i === mediaCarouselIndex\" (click)=\"mediaGoTo(i)\">\r\n <div *ngIf=\"item.isUploading\" class=\"thumb-uploading\">\r\n <div class=\"thumb-spinner\"></div>\r\n </div>\r\n <img *ngIf=\"!item.isUploading && item.mediaType === 'youtube' && item.thumbnailUrl\"\r\n [src]=\"item.thumbnailUrl\" class=\"thumb-img\" alt=\"Video thumbnail\">\r\n <div *ngIf=\"!item.isUploading && item.mediaType === 'youtube' && !item.thumbnailUrl\"\r\n class=\"thumb-yt-placeholder\">\r\n <mat-icon>play_circle</mat-icon>\r\n </div>\r\n <img *ngIf=\"!item.isUploading && item.mediaType === 'image' && item.url\" [src]=\"item.url\"\r\n class=\"thumb-img\" alt=\"Image thumbnail\">\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Empty right-side placeholder -->\r\n <div class=\"mu-right-empty\" *ngIf=\"!mediaItems.length\" (click)=\"onMediaMenuDevice()\">\r\n <mat-icon class=\"mu-right-empty-icon\">perm_media</mat-icon>\r\n <p>{{ controller.labels['LBL_ADD_MEDIA'] || 'Add media' }}</p>\r\n </div>\r\n\r\n </div>\r\n <!-- end right panel -->\r\n\r\n </div><!-- end mu-layout -->\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Library Image Picker Modal \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div class=\"media-library-overlay\" *ngIf=\"showLibraryModal\" (click)=\"closeLibraryModal()\"></div>\r\n <div class=\"media-library-modal\" *ngIf=\"showLibraryModal\" role=\"dialog\" aria-modal=\"true\">\r\n <div class=\"library-modal-header\">\r\n <h3 class=\"library-modal-title\">\r\n <mat-icon>photo_library</mat-icon>\r\n {{ controller.labels['LBL_LIBRARY_TITLE'] || 'Media Library' }}\r\n </h3>\r\n <button id=\"btn-close-library-{{ config.name }}\" type=\"button\" class=\"library-close-btn\"\r\n (click)=\"closeLibraryModal()\" aria-label=\"Close\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <!-- Loading -->\r\n <div class=\"library-loading\" *ngIf=\"libraryLoading\">\r\n <div class=\"lib-spinner\"></div>\r\n <span>{{ controller.labels['LBL_LOADING'] || 'Loading\u2026' }}</span>\r\n </div>\r\n\r\n <!-- Error -->\r\n <div class=\"library-error\" *ngIf=\"libraryError && !libraryLoading\">\r\n <mat-icon>error_outline</mat-icon>\r\n {{ libraryError }}\r\n </div>\r\n\r\n <!-- Image grid -->\r\n <div class=\"library-grid\" *ngIf=\"!libraryLoading && libraryImages.length\">\r\n <div *ngFor=\"let img of libraryImages; let li = index\" id=\"lib-img-{{ config.name }}-{{ li }}\"\r\n class=\"library-grid-item\" [class.selected]=\"isLibraryItemSelected(img)\" (click)=\"toggleLibraryItem(img)\">\r\n <img [src]=\"getLibraryItemUrl(img)\" class=\"library-grid-img\" alt=\"Library image\">\r\n <div class=\"library-check\" *ngIf=\"isLibraryItemSelected(img)\">\r\n <mat-icon>check_circle</mat-icon>\r\n </div>\r\n <div class=\"library-overlay-hover\"></div>\r\n </div>\r\n </div>\r\n\r\n <!-- Empty library -->\r\n <div class=\"library-empty\" *ngIf=\"!libraryLoading && !libraryError && libraryImages.length === 0\">\r\n <mat-icon>image_not_supported</mat-icon>\r\n <span>{{ controller.labels['LBL_LIBRARY_EMPTY'] || 'No images found in library.' }}</span>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"library-modal-footer\">\r\n <span class=\"library-selected-count\">\r\n {{ librarySelectedIds.size }} {{ controller.labels['LBL_LIBRARY_SELECTED'] || 'selected' }}\r\n </span>\r\n <div class=\"library-footer-actions\">\r\n <lib-button id=\"btn-library-cancel-{{ config.name }}\" [variant]=\"'outline'\" (click)=\"closeLibraryModal()\">\r\n {{ controller.labels['LBL_CANCEL'] || 'Cancel' }}\r\n </lib-button>\r\n <lib-button id=\"btn-library-confirm-{{ config.name }}\" [variant]=\"'primary'\"\r\n [disabled]=\"librarySelectedIds.size === 0\" (click)=\"confirmLibrarySelection()\">\r\n {{ controller.labels['LBL_LIBRARY_ADD_SELECTED'] || 'Add Selected' }}\r\n </lib-button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n</div>", styles: [".form-field{margin-bottom:var(--cc-sf-grid-gap, 16px)}.form-field.has-error .field-input{border-color:var(--cc-sf-error-border, #DC2626)}.form-row{display:flex;gap:var(--cc-sf-grid-gap, 16px)}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.form-row.grid-row{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-row.grid-row{grid-template-columns:1fr}.form-row.grid-row .row-field{grid-column:span 12!important}}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{display:block;color:var(--cc-sf-label-color, #202124);font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif);font-weight:var(--cc-sf-label-weight, 500);font-size:var(--cc-sf-label-size, 18px);line-height:var(--cc-sf-label-line-height, 100%);letter-spacing:var(--cc-sf-label-letter-spacing, 0%);margin-bottom:.5rem}.field-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.field-input{width:100%;opacity:var(--cc-sf-input-opacity, 1);gap:var(--cc-sf-input-gap, 10px);padding-top:var(--cc-sf-input-padding-y, .625rem);padding-bottom:var(--cc-sf-input-padding-y, .625rem);padding-left:var(--cc-sf-input-padding-x, 16px);padding-right:var(--cc-sf-input-padding-x, 16px);font-size:var(--cc-sf-input-font-size, .875rem);line-height:var(--cc-sf-input-line-height, 1.5);color:var(--cc-sf-input-color, #111827);background-color:var(--cc-sf-input-bg, #ffffff);border:var(--cc-sf-input-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-input-radius, 7px);box-shadow:var(--cc-sf-input-shadow, none);transition:var(--cc-sf-input-transition, all .2s ease);font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif)}.field-input::placeholder{font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif);font-weight:var(--cc-sf-placeholder-weight, 400);font-size:var(--cc-sf-placeholder-size, 16px);line-height:var(--cc-sf-placeholder-line-height, 100%);letter-spacing:var(--cc-sf-placeholder-letter-spacing, 0%);color:var(--cc-sf-input-placeholder, #9CA3AF)}.field-input:hover:not(:disabled):not([readonly]){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.field-input:focus{outline:none;border-color:var(--cc-sf-input-focus-border, #3B82F6);box-shadow:var(--cc-sf-input-focus-shadow, 0 0 0 3px rgba(59, 130, 246, .12))}.field-input:disabled,.field-input[readonly]{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.field-input.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.field-input.is-invalid:focus{box-shadow:var(--cc-sf-error-focus-shadow, 0 0 0 3px rgba(220, 38, 38, .1))}.field-input.textarea{resize:vertical;min-height:100px;font-family:var(--cc-sf-font-family, inherit)}input[type=time].time-input{cursor:pointer}input[type=time].time-input::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.7;filter:invert(30%)}input[type=time].time-input::-webkit-calendar-picker-indicator:hover{opacity:1}select.field-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}select.field-input:disabled{cursor:not-allowed}.field-hint{font-size:var(--cc-sf-hint-size, .75rem);color:var(--cc-sf-hint-color, #6B7280)}.char-count-hint{font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif);font-weight:400;font-style:normal;font-size:14px;line-height:100%;letter-spacing:.02em;text-align:right;color:var(--cc-sf-hint-color, #6B7280);margin-top:4px}.field-error{font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--cc-sf-label-size, .875rem);color:var(--cc-sf-label-color, #111827)}.radio-label input,.checkbox-label input{cursor:pointer;accent-color:var(--cc-sf-chip-selected-bg, #3B82F6)}.checkbox-single .checkbox-label{font-weight:var(--cc-sf-label-weight, 500)}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:var(--cc-sf-chip-padding, 6px 14px);background:var(--cc-sf-chip-bg, #ffffff);color:var(--cc-sf-chip-color, #374151);border:var(--cc-sf-chip-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-chip-radius, 20px);cursor:pointer;font-size:var(--cc-sf-font-size-base, .875rem);transition:var(--cc-sf-input-transition, all .2s ease)}.chip-label:hover{background:var(--cc-sf-chip-hover-bg, #F3F4F6)}.chip-label.selected{background:var(--cc-sf-chip-selected-bg, #3B82F6);color:var(--cc-sf-chip-selected-color, #ffffff);border-color:var(--cc-sf-chip-selected-border, #3B82F6)}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:var(--cc-sf-switch-track-on, #3B82F6)}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--cc-sf-switch-track-off, #D1D5DB);transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:var(--cc-sf-switch-thumb, #ffffff);transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{display:inline-flex;align-items:center;cursor:pointer;transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star mat-icon{font-size:var(--cc-sf-star-size, 28px);width:var(--cc-sf-star-size, 28px);height:var(--cc-sf-star-size, 28px);line-height:var(--cc-sf-star-size, 28px);color:var(--cc-sf-star-empty, #D1D5DB);transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star.filled mat-icon,.rating-group .star.half mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.rating-group .star:hover mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.password-wrapper{position:relative;display:flex;align-items:center}.password-wrapper .password-input{padding-right:2.75rem;width:100%}.password-wrapper .password-toggle{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:.25rem;line-height:1;color:var(--cc-sf-hint-color, #6B7280);display:flex;align-items:center;justify-content:center;transition:color var(--cc-sf-input-transition, .2s ease)}.password-wrapper .password-toggle mat-icon.eye-icon{font-size:1.125rem;width:1.125rem;height:1.125rem;line-height:1.125rem}.password-wrapper .password-toggle:hover{color:var(--cc-sf-label-color, #374151)}.password-wrapper .password-toggle:focus{outline:none}.generated-value{padding:var(--cc-sf-generated-padding, .625rem .875rem);background:var(--cc-sf-generated-bg, #F3F4F6);border:var(--cc-sf-generated-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-generated-radius, 8px);font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-generated-color, #6B7280);font-family:var(--cc-sf-font-family, inherit)}.group-section-wrapper{margin-bottom:var(--cc-sf-section-gap, 20px);border:var(--cc-sf-section-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-section-radius, 10px);padding:var(--cc-sf-section-padding, 20px);background-color:var(--cc-sf-section-bg, #ffffff);box-shadow:var(--cc-sf-section-shadow, 0 1px 4px rgba(0, 0, 0, .05))}.group-section-wrapper .group-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.group-section-wrapper .group-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.group-section-wrapper .group-fields.sf-grid{grid-template-columns:1fr}.group-section-wrapper .group-fields.sf-grid .sf-col{grid-column:span 12!important}}.group-section-wrapper .group-instance{position:relative;margin-bottom:16px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px)}.group-section-wrapper .group-instance:last-child{margin-bottom:0}.group-section-wrapper .group-instance .group-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:10px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.group-section-wrapper .group-instance .group-header .group-number{font-weight:500;color:var(--cc-sf-instance-num-color, #4B5563);font-size:var(--cc-sf-instance-num-size, .8125rem)}.group-section-wrapper.multi-save-active{border:none;box-shadow:none;padding:0;background:transparent}.group-section-wrapper.multi-save-active .group-label{border:none;padding-bottom:0;margin-bottom:0}.group-section-wrapper.multi-save-active .multi-save-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button{color:var(--ms-btn-add-color, #3B82F6);font-weight:600;font-size:.875rem;padding:8px 12px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button:hover{color:var(--ms-btn-add-hover, #2563EB);background-color:var(--cc-sf-btn-add-hover-bg, #EFF6FF)}.group-section-wrapper.multi-save-active .group-instance{background:var(--ms-card-bg, #ffffff);border:1px solid var(--ms-card-border, #E5E7EB);border-radius:var(--ms-card-radius, 10px);box-shadow:var(--ms-card-shadow, 0 1px 4px rgba(0, 0, 0, .05));padding:0;margin-bottom:16px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-editing{padding:24px}.group-section-wrapper.multi-save-active .group-instance.is-card{cursor:pointer;transition:all .2s ease-in-out}.group-section-wrapper.multi-save-active .group-instance.is-card:hover{box-shadow:var(--ms-card-shadow-hover, 0 8px 24px rgba(0, 0, 0, .08));border-color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view{display:flex;justify-content:space-between;align-items:center;padding:18px 24px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content{flex:1;display:flex;flex-direction:column;gap:4px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-title{font-size:1rem;font-weight:600;color:var(--ms-title-color, #111827);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-desc{font-size:.875rem;color:var(--ms-desc-color, #6B7280);line-height:1.4;display:-webkit-box;-webkit-line-clamp:1;line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view.is-expanded .card-content .card-desc{-webkit-line-clamp:unset;line-clamp:unset}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions{display:flex;align-items:center;gap:16px;margin-left:20px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon{font-size:22px;width:22px;height:22px;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .2s}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-delete:hover{color:var(--cc-sf-error-border, #DC2626)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-edit:hover{color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-expand{color:var(--cc-sf-input-disabled-border, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:24px;padding-top:20px;border-top:1px solid var(--cc-sf-instance-divider, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer .footer-actions{display:flex;gap:12px}.btn-remove{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:var(--cc-sf-error-text-size, .75rem);transition:var(--cc-sf-btn-transition, all .2s ease)}.btn-remove mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.btn-add-group{display:flex;align-items:center;justify-content:center;gap:4px;width:100%;padding:8px 16px;margin-top:12px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.btn-add-group mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.btn-add-group:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}.upload-drop-zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 24px;border:var(--cc-sf-dropzone-border, 1.5px dashed #CBD5E1);border-radius:var(--cc-sf-dropzone-radius, 12px);background-color:var(--cc-sf-dropzone-bg, #FFFAF1);cursor:pointer;transition:background-color .2s ease,border-color .2s ease;text-align:center;-webkit-user-select:none;user-select:none}.upload-drop-zone:hover{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-hover-border, #93C5FD)}.upload-drop-zone.drag-over{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-over-border, #3B82F6);box-shadow:var(--cc-sf-dropzone-over-shadow, 0 0 0 4px rgba(59, 130, 246, .12))}.upload-drop-zone.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.upload-icon-wrap{margin-bottom:4px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-icon-wrap mat-icon.upload-cloud-icon{font-size:56px;width:56px;height:56px;line-height:56px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-main-text{font-size:.9rem;font-weight:600;color:var(--cc-sf-label-color, #1E293B);margin:0}.upload-main-text .upload-link{color:var(--cc-sf-dropzone-link-color, #3B82F6)}.upload-hint-text{font-size:.78rem;color:var(--cc-sf-dropzone-hint-color, #64748B);margin:0}.upload-hint-text .upload-formats{color:var(--cc-sf-dropzone-link-color, #3B82F6);font-weight:500}.uploaded-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}.uploaded-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cc-sf-uploaded-item-bg, #ffffff);border:var(--cc-sf-uploaded-item-border, 1px solid #E2E8F0);border-radius:var(--cc-sf-uploaded-item-radius, 8px);transition:box-shadow .15s ease}.uploaded-item:hover{box-shadow:0 2px 6px #0000000f}.uploaded-item mat-icon.file-type-icon{font-size:20px;width:20px;height:20px;line-height:20px;flex-shrink:0;color:var(--cc-sf-hint-color, #64748B)}.uploaded-item .file-thumb{width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0}.uploaded-item .file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.uploaded-item .file-info .file-name{font-size:.85rem;font-weight:500;color:var(--cc-sf-label-color, #1E293B);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uploaded-item .file-info .file-size{font-size:.72rem;color:var(--cc-sf-hint-color, #94A3B8)}.uploaded-item .file-remove-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:var(--cc-sf-uploaded-remove-color, #94A3B8);padding:4px;border-radius:4px;flex-shrink:0;transition:color .15s ease,background .15s ease}.uploaded-item .file-remove-btn mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.uploaded-item .file-remove-btn:hover{color:var(--cc-sf-uploaded-remove-hover-color, #DC2626);background:var(--cc-sf-uploaded-remove-hover-bg, #FEF2F2)}.uploaded-item.uploading{background:var(--cc-sf-uploaded-uploading-bg, #F8FAFC);border-color:var(--cc-sf-uploaded-uploading-border, #CBD5E1);opacity:.85}.upload-spinner{width:20px;height:20px;flex-shrink:0;border:2px solid var(--cc-sf-spinner-track, #E2E8F0);border-top-color:var(--cc-sf-spinner-color, #3B82F6);border-radius:50%;animation:cc-spin .7s linear infinite}@keyframes cc-spin{to{transform:rotate(360deg)}}.rich-text-editor{display:block;width:100%}.uploading-label{color:var(--cc-sf-spinner-color, #3B82F6)!important;font-style:italic}.input-group{position:relative;display:flex;align-items:stretch;width:100%}.input-group .field-input{flex:1;border-radius:var(--cc-sf-input-radius, 8px)}.input-prefix+.input-group .field-input{border-top-left-radius:0;border-bottom-left-radius:0}.input-group .field-input:has(+.input-suffix){border-top-right-radius:0;border-bottom-right-radius:0}.input-group .field-input.has-icon-right{padding-right:3rem}.input-group.readonly .field-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);cursor:default;padding-right:3.5rem}.input-group.readonly .input-prefix,.input-group.readonly .input-suffix{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6)}.input-prefix,.input-suffix{display:flex;align-items:center;padding:0 .875rem;background-color:var(--cc-sf-input-bg, #FFFFFF);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);color:var(--cc-sf-hint-color, #6B7280);font-size:var(--cc-sf-input-font-size, .875rem);white-space:nowrap;-webkit-user-select:none;user-select:none}.input-prefix{border-right:none;border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-suffix{border-left:none;border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px);color:var(--cc-sf-chip-selected-bg, #3B82F6);font-weight:500}.readonly-icons{position:absolute;right:.875rem;top:50%;transform:translateY(-50%);display:flex;gap:8px;pointer-events:none}.readonly-icons mat-icon.lock-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem;opacity:.5;color:var(--cc-sf-hint-color, #6B7280)}.date-icon-wrapper{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;pointer-events:auto}.date-icon-wrapper .mat-icon-button{width:32px;height:32px;line-height:32px}.subfields-group-wrapper{margin-bottom:var(--cc-sf-grid-gap, 16px)}.subfields-group-wrapper .group-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:600;color:var(--cc-sf-label-color, #111827);margin-bottom:.75rem}.subfields-group-wrapper .group-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.subfields-group-wrapper .subfields-row{display:flex;align-items:flex-end;gap:12px;border-radius:var(--cc-sf-input-radius, 8px);transition:all .2s ease}.subfields-group-wrapper .subfields-row.is-invalid .subfield-item ::ng-deep .field-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.subfields-group-wrapper .subfields-row .subfield-item{flex:1;min-width:0}.subfields-group-wrapper .subfields-row .subfield-item ::ng-deep .field-label{font-size:.75rem!important;margin-bottom:4px!important;font-weight:500!important;color:var(--cc-sf-hint-color, #6B7280)!important}.subfields-group-wrapper .subfields-row .subfield-separator{margin-bottom:24px;font-weight:700;color:#94a3b8}.subfields-group-wrapper .subfields-group-error{display:block;margin-top:6px;font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.autocomplete-wrapper{position:relative;display:flex;align-items:center;width:100%}.autocomplete-wrapper .ac-search-icon{position:absolute;left:.75rem;font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem;color:var(--cc-sf-hint-color, #9CA3AF);pointer-events:none;z-index:1;transition:color var(--cc-sf-input-transition, .2s ease)}.autocomplete-wrapper .ac-input{flex:1;padding-left:2.4rem;padding-right:2.4rem}.autocomplete-wrapper .ac-clear-btn{position:absolute;right:.6rem;display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:.2rem;border-radius:50%;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .15s ease,background .15s ease}.autocomplete-wrapper .ac-clear-btn mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.autocomplete-wrapper .ac-clear-btn:hover{color:var(--cc-sf-label-color, #374151);background:var(--cc-sf-input-disabled-bg, #F3F4F6)}.autocomplete-wrapper .ac-clear-btn:focus{outline:none}.autocomplete-wrapper:focus-within .ac-search-icon{color:var(--cc-sf-input-focus-border, #3B82F6)}.autocomplete-wrapper.is-invalid .ac-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.autocomplete-wrapper.readonly .ac-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.ac-no-results{font-style:italic;font-size:.8125rem;color:var(--cc-sf-hint-color, #6B7280)}.media-upload-wrapper{padding:0;border:none;background:none}.mu-layout{display:grid;grid-template-columns:1fr 1fr;gap:32px;align-items:flex-start}@media(max-width:768px){.mu-layout{grid-template-columns:1fr}}.mu-left{display:flex;flex-direction:column;gap:20px}.mu-header{display:flex;align-items:flex-start;flex-wrap:wrap;gap:10px}.mu-title{margin:0;font-size:1.35rem;font-weight:700;color:var(--cc-sf-label-color, #111827);line-height:1.3}.mu-badge{display:inline-flex;align-items:center;padding:4px 12px;border-radius:20px;background:var(--cc-sf-label-color, #111827);color:#fff;font-size:.72rem;font-weight:600;white-space:nowrap;flex-shrink:0}.mu-description{margin:0;font-size:.875rem;color:var(--cc-sf-hint-color, #6B7280);line-height:1.6}.mu-features{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.mu-feature-item{display:flex;align-items:center;gap:8px;font-size:.875rem;color:var(--cc-sf-hint-color, #374151)}.mu-feature-item .mu-check{font-size:16px;width:16px;height:16px;line-height:16px;color:var(--cc-sf-chip-selected-bg, #3B82F6);flex-shrink:0}.mu-right{display:flex;flex-direction:column;gap:12px;min-height:260px}.mu-right-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;height:100%;min-height:250px;max-width:400px;border:2px dashed var(--cc-sf-dropzone-border, #CBD5E1);border-radius:var(--mu-carousel-radius, 12px);background:var(--cc-sf-dropzone-bg, #FFFAF1);text-align:center;color:var(--cc-sf-hint-color, #94A3B8);padding:24px;box-shadow:0 2px 10px #0000000d;transition:box-shadow .2s ease}.mu-right-empty:hover{cursor:pointer;box-shadow:0 4px 16px #0000001a}.mu-right-empty .mu-right-empty-icon{font-size:52px;width:52px;height:52px;line-height:52px;opacity:.3}.mu-right-empty p{margin:0;font-size:.85rem}.media-add-container{position:relative;display:inline-block}.media-add-container ::ng-deep button{display:flex;align-items:center;gap:6px}.media-add-container ::ng-deep button .menu-chevron{font-size:18px;width:18px;height:18px;line-height:18px;transition:transform .2s ease}.media-dropdown{position:absolute;top:calc(100% + 6px);left:0;z-index:200;min-width:240px;background:var(--mu-dropdown-bg, #ffffff);border:var(--mu-dropdown-border, 1px solid #E5E7EB);border-radius:12px;box-shadow:var(--mu-dropdown-shadow, 0 8px 32px rgba(0, 0, 0, .12));overflow:hidden;animation:mu-fade-in .15s ease}@keyframes mu-fade-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}.media-dropdown-item{display:flex;align-items:center;gap:12px;width:100%;padding:12px 16px;background:none;border:none;border-bottom:1px solid var(--cc-sf-input-disabled-border, #F3F4F6);cursor:pointer;text-align:left;transition:background .15s ease;font-family:var(--cc-sf-font-family, inherit)}.media-dropdown-item:last-child{border-bottom:none}.media-dropdown-item:hover{background:var(--cc-sf-dropzone-hover-bg, #F0F9FF)}.media-drop-icon{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:8px;flex-shrink:0}.media-drop-icon mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.media-drop-icon--video{background:var(--mu-icon-video-bg, #FFF0F0);color:var(--mu-icon-video-color, #EF4444)}.media-drop-icon--device{background:var(--mu-icon-device-bg, #EFF6FF);color:var(--mu-icon-device-color, #3B82F6)}.media-drop-icon--library{background:var(--mu-icon-library-bg, #F0FDF4);color:var(--mu-icon-library-color, #22C55E)}.media-drop-text{display:flex;flex-direction:column;gap:2px;flex:1}.media-drop-label{font-size:.875rem;font-weight:600;color:var(--cc-sf-label-color, #111827)}.media-drop-desc{font-size:.75rem;color:var(--cc-sf-hint-color, #6B7280)}.youtube-input-panel{display:flex;flex-direction:column;gap:8px;padding:16px;background:var(--cc-sf-dropzone-bg, #FFFAF1);border:1px solid var(--cc-sf-input-border, #E5E7EB);border-radius:10px;animation:mu-fade-in .18s ease}.youtube-panel-label{display:flex;align-items:center;gap:6px;font-size:.875rem;font-weight:600;color:var(--cc-sf-label-color, #111827)}.youtube-panel-label mat-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--mu-icon-video-color, #EF4444)}.youtube-input-row{display:flex;gap:8px;align-items:stretch}.youtube-input-row .youtube-url-input{flex:1}.media-menu-backdrop{position:fixed;inset:0;z-index:100}.media-upload-status{display:flex;align-items:center;gap:8px;padding:10px 14px;margin-top:4px;background:var(--cc-sf-error-bg, #FEF2F2);color:var(--cc-sf-error-text-color, #DC2626);border-radius:8px;font-size:.85rem;font-weight:500;animation:mu-fade-in .2s ease}.media-upload-status .status-icon{font-size:18px;width:18px;height:18px;line-height:18px}.media-carousel-section{display:flex;flex-direction:column;gap:12px}.media-carousel-main{position:relative;width:100%;max-width:400px;height:var(--mu-carousel-height, 250px);background:var(--mu-carousel-bg, #0F172A);border-radius:var(--mu-carousel-radius, 12px);overflow:hidden;display:flex;align-items:center;justify-content:center}.carousel-viewer{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.carousel-viewer .carousel-image{width:100%;height:100%;object-fit:cover;border-radius:var(--mu-carousel-radius, 12px)}.carousel-viewer .carousel-iframe{width:100%;height:100%;border-radius:var(--mu-carousel-radius, 12px)}.carousel-viewer .carousel-uploading{display:flex;flex-direction:column;align-items:center;gap:12px;color:#94a3b8;font-size:.85rem}.carousel-viewer .carousel-spinner{width:36px;height:36px;border:3px solid rgba(255,255,255,.15);border-top-color:#3b82f6;border-radius:50%;animation:cc-spin .7s linear infinite}.carousel-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:10;width:40px;height:40px;border-radius:50%;background:#ffffffd9;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px #0003;transition:background .2s ease,opacity .2s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.carousel-nav mat-icon{font-size:22px;width:22px;height:22px;line-height:22px;color:#1e293b}.carousel-nav:hover:not(:disabled){background:#fff}.carousel-nav:disabled{opacity:.3;cursor:not-allowed}.carousel-nav--prev{left:12px}.carousel-nav--next{right:12px}.carousel-remove-btn{position:absolute;top:10px;right:10px;z-index:10;width:32px;height:32px;border-radius:50%;background:#0000008c;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease}.carousel-remove-btn mat-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:#fff}.carousel-remove-btn:hover:not(:disabled){background:#dc2626d9}.carousel-remove-btn:disabled{opacity:.4;cursor:not-allowed}.carousel-dots{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:6px;z-index:10}.carousel-dot{width:8px;height:8px;border-radius:50%;background:#ffffff73;cursor:pointer;transition:background .2s ease,transform .2s ease}.carousel-dot.active{background:#fff;transform:scale(1.3)}.media-thumbnail-strip{display:flex;flex-wrap:wrap;max-width:400px;gap:8px;overflow-x:auto;padding-bottom:4px}.media-thumbnail-strip::-webkit-scrollbar{height:4px}.media-thumbnail-strip::-webkit-scrollbar-thumb{background:var(--cc-sf-input-disabled-border, #D1D5DB);border-radius:2px}.media-thumb{flex-shrink:0;width:72px;height:52px;border-radius:8px;overflow:hidden;cursor:pointer;border:2px solid transparent;background:var(--mu-thumb-bg, #E2E8F0);display:flex;align-items:center;justify-content:center;transition:border-color .2s ease,transform .15s ease}.media-thumb.active{border-color:var(--mu-thumb-active-border, #3B82F6);transform:scale(1.04)}.media-thumb:hover:not(.active){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.media-thumb .thumb-img{width:100%;height:100%;object-fit:cover}.media-thumb .thumb-yt-placeholder{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:#1e293b;color:#ef4444}.media-thumb .thumb-yt-placeholder mat-icon{font-size:28px;width:28px;height:28px;line-height:28px}.media-thumb .thumb-uploading{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.media-thumb .thumb-uploading .thumb-spinner{width:20px;height:20px;border:2px solid #E2E8F0;border-top-color:#3b82f6;border-radius:50%;animation:cc-spin .7s linear infinite}.media-library-overlay{position:fixed;inset:0;background:#00000080;z-index:999;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:mu-fade-in .2s ease}.media-library-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1000;width:min(90vw,780px);max-height:85vh;background:var(--mu-modal-bg, #ffffff);border-radius:var(--mu-modal-radius, 16px);box-shadow:var(--mu-modal-shadow, 0 20px 60px rgba(0, 0, 0, .2));display:flex;flex-direction:column;overflow:hidden;animation:mu-modal-in .22s cubic-bezier(.34,1.56,.64,1)}@keyframes mu-modal-in{0%{opacity:0;transform:translate(-50%,-48%) scale(.95)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}.library-modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--cc-sf-input-disabled-border, #F3F4F6);flex-shrink:0}.library-modal-title{display:flex;align-items:center;gap:8px;margin:0;font-size:1.05rem;font-weight:700;color:var(--cc-sf-label-color, #111827)}.library-modal-title mat-icon{font-size:22px;width:22px;height:22px;line-height:22px;color:var(--mu-icon-library-color, #22C55E)}.library-close-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;background:none;cursor:pointer;border-radius:50%;color:var(--cc-sf-hint-color, #9CA3AF);transition:background .15s ease,color .15s ease}.library-close-btn mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.library-close-btn:hover{background:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-label-color, #374151)}.library-loading,.library-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:48px 24px;color:var(--cc-sf-hint-color, #9CA3AF);font-size:.875rem;flex:1}.library-loading mat-icon,.library-empty mat-icon{font-size:40px;width:40px;height:40px;line-height:40px;opacity:.5}.lib-spinner{width:36px;height:36px;border:3px solid #E2E8F0;border-top-color:#3b82f6;border-radius:50%;animation:cc-spin .7s linear infinite}.library-error{display:flex;align-items:center;gap:8px;padding:16px 24px;background:var(--cc-sf-error-bg, #FEF2F2);color:var(--cc-sf-error-text-color, #DC2626);font-size:.875rem;flex-shrink:0}.library-error mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.library-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;padding:24px;overflow-y:auto;flex:1}.library-grid::-webkit-scrollbar{width:6px}.library-grid::-webkit-scrollbar-thumb{background:var(--cc-sf-input-disabled-border, #D1D5DB);border-radius:3px}.library-grid-item{position:relative;aspect-ratio:4/3;border-radius:10px;overflow:hidden;cursor:pointer;border:2.5px solid transparent;transition:border-color .2s ease,transform .15s ease}.library-grid-item.selected{border-color:var(--cc-sf-chip-selected-bg, #3B82F6);transform:scale(.97)}.library-grid-item:hover .library-overlay-hover{opacity:1}.library-grid-img{width:100%;height:100%;object-fit:cover;display:block}.library-overlay-hover{position:absolute;inset:0;background:#3b82f61f;opacity:0;transition:opacity .15s ease}.library-check{position:absolute;top:6px;right:6px;color:var(--cc-sf-chip-selected-bg, #3B82F6);background:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;width:22px;height:22px;box-shadow:0 1px 4px #00000026}.library-check mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.library-modal-footer{display:flex;align-items:center;justify-content:space-between;padding:16px 24px 20px;border-top:1px solid var(--cc-sf-input-disabled-border, #F3F4F6);flex-shrink:0}.library-selected-count{font-size:.875rem;font-weight:600;color:var(--cc-sf-hint-color, #6B7280)}.library-footer-actions{display:flex;gap:10px}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.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: i1$2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.SelectMultipleControlValueAccessor, selector: "select[multiple][formControlName],select[multiple][formControl],select[multiple][ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$2.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i7.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i7.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i7.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "directive", type: i8.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "directive", type: i9.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "component", type: ButtonComponent, selector: "lib-button", inputs: ["variant", "type", "disabled", "width", "height", "borderRadius", "fontSize", "fontWeight", "backgroundColor", "color", "border", "icon", "labels"] }, { kind: "component", type: i11.QuillEditorComponent, selector: "quill-editor" }, { kind: "component", type: FormFieldComponent, selector: "lib-form-field", inputs: ["config", "controller", "formGroup", "allowMulti"] }, { kind: "pipe", type: TrustedUrlPipe, name: "trustedUrl" }] });
|
|
4660
5073
|
}
|
|
4661
5074
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FormFieldComponent, decorators: [{
|
|
4662
5075
|
type: Component,
|
|
4663
|
-
args: [{ selector: 'lib-form-field', standalone: false, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\r\n\r\n <!-- \u2550\u2550 ROW Layout \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRow\" class=\"form-row grid-row\">\r\n <ng-container *ngFor=\"let child of config.children\">\r\n <div class=\"row-field\" [style.gridColumn]=\"'span ' + getChildColSpan(child)\">\r\n <lib-form-field [config]=\"child\" [controller]=\"controller\" [formGroup]=\"formGroup\" [allowMulti]=\"allowMulti\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 allowMulti (repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig?.allowMulti\" class=\"group-section-wrapper\"\r\n [class.multi-save-active]=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n\r\n <!-- Multi-Save Header with Add button (top-right style) -->\r\n <div class=\"multi-save-header\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label\">{{ config.sectionConfig!.label }}</h3>\r\n <lib-button [variant]=\"'outline'\" [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\"\r\n class=\"btn-add-multi\">\r\n {{ addMultiLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Standard Header for non-multiSave -->\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label && !config.sectionConfig?.multiSaveConfig?.active\">{{\r\n config.sectionConfig!.label }}</h3>\r\n\r\n <div *ngFor=\"let instance of instanceList; trackBy: trackByInstanceId; let i = index\" class=\"group-instance\"\r\n [class.is-editing]=\"instance.isEditing\"\r\n [class.is-card]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n\r\n <!-- 1. EDIT MODE / UNSAVED / STANDARD STATE -->\r\n <div [hidden]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <!-- Instance header \u2014 show remove only when more than 1 instance -->\r\n <div class=\"group-header\" *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active && instanceList.length > 1\">\r\n <span class=\"group-number\">{{ config.sectionConfig!.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" [icon]=\"{type: 'material', value: 'delete_outline'}\"\r\n (click)=\"removeGroupInstance(i)\">\r\n {{ removeLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig!.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instance.fg\" [allowMulti]=\"true\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- SAVE / CANCEL BUTTONS for MultiSave in Editing phase -->\r\n <div class=\"group-footer\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <span class=\"field-error\" *ngIf=\"multiSaveError\">{{ multiSaveError }}</span>\r\n <div class=\"footer-actions\">\r\n <lib-button [variant]=\"'outline'\" (click)=\"cancelGroupInstance(i)\">Cancel</lib-button>\r\n <lib-button [variant]=\"'primary'\" (click)=\"saveGroupInstance(i)\">Save</lib-button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- 2. CARD VIEW (Saved State) -->\r\n <ng-container *ngIf=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <div class=\"card-view\" [class.is-expanded]=\"instance.isExpanded\">\r\n <div class=\"card-content\">\r\n <span class=\"card-title\">{{ instance.fg.get(config.sectionConfig.multiSaveConfig.summaryField || '')?.value\r\n || '\u2014' }}</span>\r\n <span class=\"card-desc\" *ngIf=\"config.sectionConfig.multiSaveConfig.descriptionField\">\r\n {{ instance.fg.get(config.sectionConfig.multiSaveConfig.descriptionField)?.value }}\r\n </span>\r\n </div>\r\n <div class=\"card-actions\">\r\n <mat-icon class=\"icon-delete\" (click)=\"removeGroupInstance(i, true)\">delete_outline</mat-icon>\r\n <mat-icon class=\"icon-edit\" (click)=\"editGroupInstance(i)\">edit_outline</mat-icon>\r\n <mat-icon class=\"icon-expand\" (click)=\"toggleExpandGroupInstance(i)\">\r\n {{ instance.isExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Standard Add Button for non-multiSave -->\r\n <lib-button *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active\" [variant]=\"'outline'\"\r\n [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\" class=\"btn-add-group-wrapper\">\r\n {{ addLabel }} {{ config.sectionConfig!.label }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 single (non-repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig && !config.sectionConfig.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig.label\">{{ config.sectionConfig.label }}</h3>\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"groupFormGroup\" [allowMulti]=\"false\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Text Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTextField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <textarea *ngIf=\"config.subType === 'LONG'\" class=\"field-input textarea\" [placeholder]=\"config.placeholder || ''\"\r\n [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\" rows=\"4\">\r\n </textarea>\r\n\r\n <!-- Password input with show/hide toggle -->\r\n <div *ngIf=\"config.subType === 'PASSWORD'\" class=\"password-wrapper\">\r\n <input [type]=\"showPassword ? 'text' : 'password'\" class=\"field-input password-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <button type=\"button\" class=\"password-toggle\" (click)=\"showPassword = !showPassword\" tabindex=\"-1\"\r\n [attr.aria-label]=\"showPassword ? 'Hide password' : 'Show password'\">\r\n <mat-icon class=\"eye-icon\">{{ showPassword ? 'visibility' : 'visibility_off' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input *ngIf=\"config.subType !== 'LONG' && config.subType !== 'PASSWORD'\"\r\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\" class=\"field-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\"\r\n [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Number Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input type=\"number\" class=\"field-input\" [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\"\r\n [min]=\"config.numberConfig?.min\" [max]=\"config.numberConfig?.max\" [step]=\"config.numberConfig?.step || 1\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Date Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDateField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input matInput [matDatepicker]=\"datePicker\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [min]=\"config.dateConfig?.minDate\" [max]=\"config.dateConfig?.maxDate\" [class.is-invalid]=\"errorMessage\"\r\n [placeholder]=\"config.placeholder || ''\" [readonly]=\"config.readonly\"\r\n (click)=\"!config.readonly && datePicker.open()\">\r\n <div class=\"input-suffix\"\r\n style=\"padding: 0; background: transparent; border: none; display: flex; align-items: center; justify-content: center; width: 40px; cursor: pointer;\">\r\n <mat-datepicker-toggle matSuffix [for]=\"datePicker\" [disabled]=\"config.readonly\"></mat-datepicker-toggle>\r\n </div>\r\n <mat-datepicker #datePicker></mat-datepicker>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Time Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTimeField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input type=\"time\" class=\"field-input time-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"!!config.readonly\">\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Autocomplete \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isAutocomplete\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Hidden real control (stores the code value) -->\r\n <input type=\"hidden\" [formControlName]=\"config.name!\">\r\n\r\n <div class=\"autocomplete-wrapper\" [class.is-invalid]=\"errorMessage\" [class.readonly]=\"config.readonly\">\r\n <!-- Search icon -->\r\n <mat-icon class=\"ac-search-icon\">search</mat-icon>\r\n\r\n <input class=\"field-input ac-input\" [formControl]=\"autocompleteInputCtrl\" [matAutocomplete]=\"auto\"\r\n [placeholder]=\"config.placeholder || 'Search\u2026'\" [readonly]=\"!!config.readonly\" [class.is-invalid]=\"errorMessage\"\r\n (blur)=\"onAutocompleteClear()\" autocomplete=\"off\">\r\n\r\n <!-- Clear button -->\r\n <button type=\"button\" class=\"ac-clear-btn\" *ngIf=\"autocompleteInputCtrl.value && !config.readonly\"\r\n (click)=\"autocompleteInputCtrl.setValue(''); updateValue(null)\" tabindex=\"-1\" aria-label=\"Clear\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n\r\n <mat-autocomplete #auto=\"matAutocomplete\" [panelWidth]=\"'auto'\">\r\n <mat-option *ngFor=\"let option of filteredOptions\" [value]=\"option.label\"\r\n (onSelectionChange)=\"onAutocompleteSelected(option)\">\r\n {{ option.label }}\r\n </mat-option>\r\n <mat-option *ngIf=\"filteredOptions.length === 0\" disabled class=\"ac-no-results\">\r\n No results found\r\n </mat-option>\r\n </mat-autocomplete>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Dropdown \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <select *ngIf=\"config.subType === 'SINGLE'\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option [ngValue]=\"null\" disabled selected>{{ config.placeholder || 'Select' }}</option>\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <select *ngIf=\"config.subType === 'MULTIPLE'\" class=\"field-input\" multiple [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Radio \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRadio\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"radio-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\r\n <input type=\"radio\" [formControlName]=\"config.name!\" [value]=\"option.code\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Checkbox \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\r\n <label class=\"checkbox-label\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span>{{ config.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Chip \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isChip\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"chip-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"chip-label\"\r\n [class.selected]=\"isChecked(option.code)\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\" style=\"display: none;\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Switch \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label class=\"switch-container\">\r\n <span class=\"field-label\">{{ config.label }}</span>\r\n <div class=\"switch\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span class=\"slider\"></span>\r\n </div>\r\n </label>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rich Text \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRichText\" class=\"field-wrapper component-rich-text\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rich-text-container\" [class.is-invalid]=\"errorMessage\">\r\n <quill-editor [formControlName]=\"config.name!\"\r\n [placeholder]=\"config.richTextConfig?.placeholder || config.placeholder || ''\"\r\n [styles]=\"{height: config.richTextConfig?.height || '200px'}\">\r\n </quill-editor>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rating \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRating\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rating-group\" [class.is-invalid]=\"errorMessage\">\r\n <span *ngFor=\"let star of getStarArray()\" class=\"star\" [class.filled]=\"isStarFilled(star)\"\r\n [class.half]=\"isStarHalf(star)\" (click)=\"onRatingChange(star, $event)\">\r\n <mat-icon>{{ isStarFilled(star) || isStarHalf(star) ? 'star' : 'star_border' }}</mat-icon>\r\n </span>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Generated Field (read-only) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\r\n <div class=\"generated-value\">{{ value || '-' }}</div>\r\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 File Upload \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isFileUpload\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Drop Zone -->\r\n <div class=\"upload-drop-zone\" [class.drag-over]=\"isDragOver\" [class.has-files]=\"value?.length\"\r\n [class.is-invalid]=\"errorMessage\" (dragover)=\"onDragOver($event)\" (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onFileDrop($event)\" (click)=\"fileInput.click()\">\r\n\r\n <div class=\"upload-icon-wrap\">\r\n <mat-icon class=\"upload-cloud-icon\">cloud_upload</mat-icon>\r\n </div>\r\n\r\n <p class=\"upload-main-text\">Drag and drop files here or <span class=\"upload-link\">click to upload</span></p>\r\n <p class=\"upload-hint-text\" *ngIf=\"config.attachmentConfig?.acceptLabel\">\r\n Supported formats:\r\n <span class=\"upload-formats\">{{ config.attachmentConfig!.acceptLabel }}</span>\r\n </p>\r\n <p class=\"upload-hint-text\" *ngIf=\"!config.attachmentConfig?.acceptLabel && config.hint\">\r\n {{ config.hint }}\r\n </p>\r\n\r\n <!-- Hidden native file input -->\r\n <input #fileInput type=\"file\" style=\"display:none\"\r\n [attr.multiple]=\"config.attachmentConfig?.multiple ? true : null\"\r\n [attr.accept]=\"config.attachmentConfig?.accept || null\" (change)=\"onFileSelected($event)\">\r\n </div>\r\n\r\n <!-- Uploaded file list -->\r\n <div class=\"uploaded-list\" *ngIf=\"value?.length\">\r\n <div *ngFor=\"let f of value; let i = index\" class=\"uploaded-item\" [class.uploading]=\"f.isUploading\">\r\n\r\n <!-- Uploading spinner (shown while API call is in progress) -->\r\n <ng-container *ngIf=\"f.isUploading; else fileReady\">\r\n <div class=\"upload-spinner\"></div>\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size uploading-label\">Uploading...</span>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Normal state once upload is done -->\r\n <ng-template #fileReady>\r\n <!-- File type icon -->\r\n <mat-icon class=\"file-type-icon\">{{ getFileIcon(f.type) }}</mat-icon>\r\n\r\n <!-- Image thumbnail (only for images) -->\r\n <img *ngIf=\"f.type?.startsWith('image') && f.dataUrl\" [src]=\"f.dataUrl\" class=\"file-thumb\" alt=\"preview\">\r\n\r\n <!-- Name & size -->\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size\">{{ formatFileSize(f.size) }}</span>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Remove button \u2014 disabled while uploading -->\r\n <lib-button [variant]=\"'danger-outline'\" [disabled]=\"f.isUploading\"\r\n (click)=\"!f.isUploading && removeUploadedFile(i)\">\r\n <mat-icon>close</mat-icon>\r\n </lib-button>\r\n </div>\r\n </div>\r\n\r\n <!-- Validation / file errors -->\r\n <span class=\"field-error\" *ngIf=\"fileUploadError\">{{ fileUploadError }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage && !fileUploadError\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\"\r\n *ngIf=\"config.hint && !errorMessage && !fileUploadError && !config.attachmentConfig?.acceptLabel\">\r\n {{ config.hint }}\r\n </span>\r\n </div>\r\n\r\n</div>", styles: [".form-field{margin-bottom:var(--cc-sf-grid-gap, 16px)}.form-field.has-error .field-input{border-color:var(--cc-sf-error-border, #DC2626)}.form-row{display:flex;gap:var(--cc-sf-grid-gap, 16px)}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.form-row.grid-row{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-row.grid-row{grid-template-columns:1fr}.form-row.grid-row .row-field{grid-column:span 12!important}}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:var(--cc-sf-label-weight, 500);color:var(--cc-sf-label-color, #111827);margin-bottom:.5rem;line-height:1.25rem}.field-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.field-input{width:100%;padding:var(--cc-sf-input-padding, .625rem .875rem);font-size:var(--cc-sf-input-font-size, .875rem);line-height:1.5;color:var(--cc-sf-input-color, #111827);background-color:var(--cc-sf-input-bg, #ffffff);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);border-radius:var(--cc-sf-input-radius, 8px);box-shadow:var(--cc-sf-input-shadow, none);transition:var(--cc-sf-input-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.field-input::placeholder{color:var(--cc-sf-input-placeholder, #9CA3AF)}.field-input:hover:not(:disabled):not([readonly]){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.field-input:focus{outline:none;border-color:var(--cc-sf-input-focus-border, #3B82F6);box-shadow:var(--cc-sf-input-focus-shadow, 0 0 0 3px rgba(59, 130, 246, .12))}.field-input:disabled,.field-input[readonly]{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.field-input.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.field-input.is-invalid:focus{box-shadow:var(--cc-sf-error-focus-shadow, 0 0 0 3px rgba(220, 38, 38, .1))}.field-input.textarea{resize:vertical;min-height:100px;font-family:var(--cc-sf-font-family, inherit)}input[type=time].time-input{cursor:pointer}input[type=time].time-input::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.7;filter:invert(30%)}input[type=time].time-input::-webkit-calendar-picker-indicator:hover{opacity:1}select.field-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}select.field-input:disabled{cursor:not-allowed}.field-hint{font-size:var(--cc-sf-hint-size, .75rem);color:var(--cc-sf-hint-color, #6B7280)}.field-error{font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--cc-sf-label-size, .875rem);color:var(--cc-sf-label-color, #111827)}.radio-label input,.checkbox-label input{cursor:pointer;accent-color:var(--cc-sf-chip-selected-bg, #3B82F6)}.checkbox-single .checkbox-label{font-weight:var(--cc-sf-label-weight, 500)}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:var(--cc-sf-chip-padding, 6px 14px);background:var(--cc-sf-chip-bg, #ffffff);color:var(--cc-sf-chip-color, #374151);border:var(--cc-sf-chip-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-chip-radius, 20px);cursor:pointer;font-size:var(--cc-sf-font-size-base, .875rem);transition:var(--cc-sf-input-transition, all .2s ease)}.chip-label:hover{background:var(--cc-sf-chip-hover-bg, #F3F4F6)}.chip-label.selected{background:var(--cc-sf-chip-selected-bg, #3B82F6);color:var(--cc-sf-chip-selected-color, #ffffff);border-color:var(--cc-sf-chip-selected-border, #3B82F6)}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:var(--cc-sf-switch-track-on, #3B82F6)}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--cc-sf-switch-track-off, #D1D5DB);transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:var(--cc-sf-switch-thumb, #ffffff);transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{display:inline-flex;align-items:center;cursor:pointer;transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star mat-icon{font-size:var(--cc-sf-star-size, 28px);width:var(--cc-sf-star-size, 28px);height:var(--cc-sf-star-size, 28px);line-height:var(--cc-sf-star-size, 28px);color:var(--cc-sf-star-empty, #D1D5DB);transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star.filled mat-icon,.rating-group .star.half mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.rating-group .star:hover mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.password-wrapper{position:relative;display:flex;align-items:center}.password-wrapper .password-input{padding-right:2.75rem;width:100%}.password-wrapper .password-toggle{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:.25rem;line-height:1;color:var(--cc-sf-hint-color, #6B7280);display:flex;align-items:center;justify-content:center;transition:color var(--cc-sf-input-transition, .2s ease)}.password-wrapper .password-toggle mat-icon.eye-icon{font-size:1.125rem;width:1.125rem;height:1.125rem;line-height:1.125rem}.password-wrapper .password-toggle:hover{color:var(--cc-sf-label-color, #374151)}.password-wrapper .password-toggle:focus{outline:none}.generated-value{padding:var(--cc-sf-generated-padding, .625rem .875rem);background:var(--cc-sf-generated-bg, #F3F4F6);border:var(--cc-sf-generated-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-generated-radius, 8px);font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-generated-color, #6B7280);font-family:var(--cc-sf-font-family, inherit)}.group-section-wrapper{margin-bottom:var(--cc-sf-section-gap, 20px);border:var(--cc-sf-section-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-section-radius, 10px);padding:var(--cc-sf-section-padding, 20px);background-color:var(--cc-sf-section-bg, #ffffff);box-shadow:var(--cc-sf-section-shadow, 0 1px 4px rgba(0, 0, 0, .05))}.group-section-wrapper .group-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.group-section-wrapper .group-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.group-section-wrapper .group-fields.sf-grid{grid-template-columns:1fr}.group-section-wrapper .group-fields.sf-grid .sf-col{grid-column:span 12!important}}.group-section-wrapper .group-instance{position:relative;margin-bottom:16px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px)}.group-section-wrapper .group-instance:last-child{margin-bottom:0}.group-section-wrapper .group-instance .group-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:10px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.group-section-wrapper .group-instance .group-header .group-number{font-weight:500;color:var(--cc-sf-instance-num-color, #4B5563);font-size:var(--cc-sf-instance-num-size, .8125rem)}.group-section-wrapper.multi-save-active{border:none;box-shadow:none;padding:0;background:transparent}.group-section-wrapper.multi-save-active .group-label{border:none;padding-bottom:0;margin-bottom:0}.group-section-wrapper.multi-save-active .multi-save-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button{color:var(--ms-btn-add-color, #3B82F6);font-weight:600;font-size:.875rem;padding:8px 12px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button:hover{color:var(--ms-btn-add-hover, #2563EB);background-color:var(--cc-sf-btn-add-hover-bg, #EFF6FF)}.group-section-wrapper.multi-save-active .group-instance{background:var(--ms-card-bg, #ffffff);border:1px solid var(--ms-card-border, #E5E7EB);border-radius:var(--ms-card-radius, 10px);box-shadow:var(--ms-card-shadow, 0 1px 4px rgba(0, 0, 0, .05));padding:0;margin-bottom:16px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-editing{padding:24px}.group-section-wrapper.multi-save-active .group-instance.is-card{cursor:pointer;transition:all .2s ease-in-out}.group-section-wrapper.multi-save-active .group-instance.is-card:hover{box-shadow:var(--ms-card-shadow-hover, 0 8px 24px rgba(0, 0, 0, .08));border-color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view{display:flex;justify-content:space-between;align-items:center;padding:18px 24px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content{flex:1;display:flex;flex-direction:column;gap:4px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-title{font-size:1rem;font-weight:600;color:var(--ms-title-color, #111827);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-desc{font-size:.875rem;color:var(--ms-desc-color, #6B7280);line-height:1.4;display:-webkit-box;-webkit-line-clamp:1;line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view.is-expanded .card-content .card-desc{-webkit-line-clamp:unset;line-clamp:unset}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions{display:flex;align-items:center;gap:16px;margin-left:20px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon{font-size:22px;width:22px;height:22px;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .2s}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-delete:hover{color:var(--cc-sf-error-border, #DC2626)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-edit:hover{color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-expand{color:var(--cc-sf-input-disabled-border, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:24px;padding-top:20px;border-top:1px solid var(--cc-sf-instance-divider, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer .footer-actions{display:flex;gap:12px}.btn-remove{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:var(--cc-sf-error-text-size, .75rem);transition:var(--cc-sf-btn-transition, all .2s ease)}.btn-remove mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.btn-add-group{display:flex;align-items:center;justify-content:center;gap:4px;width:100%;padding:8px 16px;margin-top:12px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.btn-add-group mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.btn-add-group:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}.upload-drop-zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 24px;border:var(--cc-sf-dropzone-border, 1.5px dashed #CBD5E1);border-radius:var(--cc-sf-dropzone-radius, 12px);background-color:var(--cc-sf-dropzone-bg, #F8FAFC);cursor:pointer;transition:background-color .2s ease,border-color .2s ease;text-align:center;-webkit-user-select:none;user-select:none}.upload-drop-zone:hover{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-hover-border, #93C5FD)}.upload-drop-zone.drag-over{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-over-border, #3B82F6);box-shadow:var(--cc-sf-dropzone-over-shadow, 0 0 0 4px rgba(59, 130, 246, .12))}.upload-drop-zone.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.upload-icon-wrap{margin-bottom:4px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-icon-wrap mat-icon.upload-cloud-icon{font-size:56px;width:56px;height:56px;line-height:56px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-main-text{font-size:.9rem;font-weight:600;color:var(--cc-sf-label-color, #1E293B);margin:0}.upload-main-text .upload-link{color:var(--cc-sf-dropzone-link-color, #3B82F6)}.upload-hint-text{font-size:.78rem;color:var(--cc-sf-dropzone-hint-color, #64748B);margin:0}.upload-hint-text .upload-formats{color:var(--cc-sf-dropzone-link-color, #3B82F6);font-weight:500}.uploaded-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}.uploaded-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cc-sf-uploaded-item-bg, #ffffff);border:var(--cc-sf-uploaded-item-border, 1px solid #E2E8F0);border-radius:var(--cc-sf-uploaded-item-radius, 8px);transition:box-shadow .15s ease}.uploaded-item:hover{box-shadow:0 2px 6px #0000000f}.uploaded-item mat-icon.file-type-icon{font-size:20px;width:20px;height:20px;line-height:20px;flex-shrink:0;color:var(--cc-sf-hint-color, #64748B)}.uploaded-item .file-thumb{width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0}.uploaded-item .file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.uploaded-item .file-info .file-name{font-size:.85rem;font-weight:500;color:var(--cc-sf-label-color, #1E293B);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uploaded-item .file-info .file-size{font-size:.72rem;color:var(--cc-sf-hint-color, #94A3B8)}.uploaded-item .file-remove-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:var(--cc-sf-uploaded-remove-color, #94A3B8);padding:4px;border-radius:4px;flex-shrink:0;transition:color .15s ease,background .15s ease}.uploaded-item .file-remove-btn mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.uploaded-item .file-remove-btn:hover{color:var(--cc-sf-uploaded-remove-hover-color, #DC2626);background:var(--cc-sf-uploaded-remove-hover-bg, #FEF2F2)}.uploaded-item.uploading{background:var(--cc-sf-uploaded-uploading-bg, #F8FAFC);border-color:var(--cc-sf-uploaded-uploading-border, #CBD5E1);opacity:.85}.upload-spinner{width:20px;height:20px;flex-shrink:0;border:2px solid var(--cc-sf-spinner-track, #E2E8F0);border-top-color:var(--cc-sf-spinner-color, #3B82F6);border-radius:50%;animation:cc-spin .7s linear infinite}@keyframes cc-spin{to{transform:rotate(360deg)}}.uploading-label{color:var(--cc-sf-spinner-color, #3B82F6)!important;font-style:italic}.input-group{position:relative;display:flex;align-items:stretch;width:100%}.input-group .field-input{flex:1;border-radius:0}.input-group .field-input:first-child{border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-group .field-input:last-child{border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px)}.input-group.readonly .field-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);cursor:default;padding-right:3.5rem}.input-group.readonly .input-prefix,.input-group.readonly .input-suffix{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6)}.input-prefix,.input-suffix{display:flex;align-items:center;padding:0 .875rem;background-color:var(--cc-sf-input-bg, #FFFFFF);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);color:var(--cc-sf-hint-color, #6B7280);font-size:var(--cc-sf-input-font-size, .875rem);white-space:nowrap;-webkit-user-select:none;user-select:none}.input-prefix{border-right:none;border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-suffix{border-left:none;border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px);color:var(--cc-sf-chip-selected-bg, #3B82F6);font-weight:500}.readonly-icons{position:absolute;right:.875rem;top:50%;transform:translateY(-50%);display:flex;gap:8px;pointer-events:none}.readonly-icons mat-icon.lock-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem;opacity:.5;color:var(--cc-sf-hint-color, #6B7280)}.subfields-group-wrapper{margin-bottom:var(--cc-sf-grid-gap, 16px)}.subfields-group-wrapper .group-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:600;color:var(--cc-sf-label-color, #111827);margin-bottom:.75rem}.subfields-group-wrapper .group-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.subfields-group-wrapper .subfields-row{display:flex;align-items:flex-end;gap:12px;border-radius:var(--cc-sf-input-radius, 8px);transition:all .2s ease}.subfields-group-wrapper .subfields-row.is-invalid .subfield-item ::ng-deep .field-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.subfields-group-wrapper .subfields-row .subfield-item{flex:1;min-width:0}.subfields-group-wrapper .subfields-row .subfield-item ::ng-deep .field-label{font-size:.75rem!important;margin-bottom:4px!important;font-weight:500!important;color:var(--cc-sf-hint-color, #6B7280)!important}.subfields-group-wrapper .subfields-row .subfield-separator{margin-bottom:24px;font-weight:700;color:#94a3b8}.subfields-group-wrapper .subfields-group-error{display:block;margin-top:6px;font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.autocomplete-wrapper{position:relative;display:flex;align-items:center;width:100%}.autocomplete-wrapper .ac-search-icon{position:absolute;left:.75rem;font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem;color:var(--cc-sf-hint-color, #9CA3AF);pointer-events:none;z-index:1;transition:color var(--cc-sf-input-transition, .2s ease)}.autocomplete-wrapper .ac-input{flex:1;padding-left:2.4rem;padding-right:2.4rem}.autocomplete-wrapper .ac-clear-btn{position:absolute;right:.6rem;display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:.2rem;border-radius:50%;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .15s ease,background .15s ease}.autocomplete-wrapper .ac-clear-btn mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.autocomplete-wrapper .ac-clear-btn:hover{color:var(--cc-sf-label-color, #374151);background:var(--cc-sf-input-disabled-bg, #F3F4F6)}.autocomplete-wrapper .ac-clear-btn:focus{outline:none}.autocomplete-wrapper:focus-within .ac-search-icon{color:var(--cc-sf-input-focus-border, #3B82F6)}.autocomplete-wrapper.is-invalid .ac-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.autocomplete-wrapper.readonly .ac-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.ac-no-results{font-style:italic;font-size:.8125rem;color:var(--cc-sf-hint-color, #6B7280)}\n"] }]
|
|
5076
|
+
args: [{ selector: 'lib-form-field', standalone: false, template: "<div class=\"form-field\" *ngIf=\"isVisible\" [class.has-error]=\"errorMessage\">\r\n\r\n <!-- \u2550\u2550 ROW Layout \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRow\" class=\"form-row grid-row\">\r\n <ng-container *ngFor=\"let child of config.children\">\r\n <div class=\"row-field\" [style.gridColumn]=\"'span ' + getChildColSpan(child)\">\r\n <lib-form-field [config]=\"child\" [controller]=\"controller\" [formGroup]=\"formGroup\" [allowMulti]=\"allowMulti\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 allowMulti (repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig?.allowMulti\" class=\"group-section-wrapper\"\r\n [class.multi-save-active]=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n\r\n <!-- Multi-Save Header with Add button (top-right style) -->\r\n <div class=\"multi-save-header\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label\">{{ config.sectionConfig!.label }}</h3>\r\n <lib-button [variant]=\"'outline'\" [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\"\r\n class=\"btn-add-multi\">\r\n {{ addMultiLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- Standard Header for non-multiSave -->\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig?.label && !config.sectionConfig?.multiSaveConfig?.active\">{{\r\n config.sectionConfig!.label }}</h3>\r\n\r\n <div *ngFor=\"let instance of instanceList; trackBy: trackByInstanceId; let i = index\" class=\"group-instance\"\r\n [class.is-editing]=\"instance.isEditing\"\r\n [class.is-card]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n\r\n <!-- 1. EDIT MODE / UNSAVED / STANDARD STATE -->\r\n <div [hidden]=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <!-- Instance header \u2014 show remove only when more than 1 instance -->\r\n <div class=\"group-header\" *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active && instanceList.length > 1\">\r\n <span class=\"group-number\">{{ config.sectionConfig!.label }} #{{ i + 1 }}</span>\r\n <lib-button [variant]=\"'danger-outline'\" [icon]=\"{type: 'material', value: 'delete_outline'}\"\r\n (click)=\"removeGroupInstance(i)\">\r\n {{ removeLabel }}\r\n </lib-button>\r\n </div>\r\n\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig!.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"instance.fg\" [allowMulti]=\"true\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- SAVE / CANCEL BUTTONS for MultiSave in Editing phase -->\r\n <div class=\"group-footer\" *ngIf=\"config.sectionConfig?.multiSaveConfig?.active\">\r\n <span class=\"field-error\" *ngIf=\"multiSaveError\">{{ multiSaveError }}</span>\r\n <div class=\"footer-actions\">\r\n <lib-button [variant]=\"'outline'\" (click)=\"cancelGroupInstance(i)\">Cancel</lib-button>\r\n <lib-button [variant]=\"'primary'\" (click)=\"saveGroupInstance(i)\">Save</lib-button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- 2. CARD VIEW (Saved State) -->\r\n <ng-container *ngIf=\"config.sectionConfig?.multiSaveConfig?.active && !instance.isEditing\">\r\n <div class=\"card-view\" [class.is-expanded]=\"instance.isExpanded\">\r\n <div class=\"card-content\">\r\n <span class=\"card-title\">{{ instance.fg.get(config.sectionConfig.multiSaveConfig.summaryField || '')?.value\r\n || '\u2014' }}</span>\r\n <span class=\"card-desc\" *ngIf=\"config.sectionConfig.multiSaveConfig.descriptionField\">\r\n {{ instance.fg.get(config.sectionConfig.multiSaveConfig.descriptionField)?.value }}\r\n </span>\r\n </div>\r\n <div class=\"card-actions\">\r\n <mat-icon class=\"icon-delete\" (click)=\"removeGroupInstance(i, true)\">delete_outline</mat-icon>\r\n <mat-icon class=\"icon-edit\" (click)=\"editGroupInstance(i)\">edit_outline</mat-icon>\r\n <mat-icon class=\"icon-expand\" (click)=\"toggleExpandGroupInstance(i)\">\r\n {{ instance.isExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}\r\n </mat-icon>\r\n </div>\r\n </div>\r\n </ng-container>\r\n </div>\r\n\r\n <!-- Standard Add Button for non-multiSave -->\r\n <lib-button *ngIf=\"!config.sectionConfig?.multiSaveConfig?.active\" [variant]=\"'outline'\"\r\n [icon]=\"{type: 'material', value: 'add'}\" (click)=\"addGroupInstance()\" class=\"btn-add-group-wrapper\">\r\n {{ addLabel }} {{ config.sectionConfig!.label }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- \u2550\u2550 GROUP \u2014 single (non-repeater) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGroup && config.sectionConfig && !config.sectionConfig.allowMulti\" class=\"group-section-wrapper\">\r\n <h3 class=\"group-label\" *ngIf=\"config.sectionConfig.label\">{{ config.sectionConfig.label }}</h3>\r\n <div class=\"group-fields sf-grid\">\r\n <ng-container *ngFor=\"let field of config.sectionConfig.children\">\r\n <div class=\"sf-col\" [style.gridColumn]=\"'span ' + (field.colSpan || 12)\">\r\n <lib-form-field [config]=\"field\" [controller]=\"controller\" [formGroup]=\"groupFormGroup\" [allowMulti]=\"false\">\r\n </lib-form-field>\r\n </div>\r\n </ng-container>\r\n </div>\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Text Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTextField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <textarea *ngIf=\"config.subType === 'LONG'\" class=\"field-input textarea\" [placeholder]=\"config.placeholder || ''\"\r\n [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\" rows=\"4\">\r\n </textarea>\r\n\r\n <!-- Password input with show/hide toggle -->\r\n <div *ngIf=\"config.subType === 'PASSWORD'\" class=\"password-wrapper\">\r\n <input [type]=\"showPassword ? 'text' : 'password'\" class=\"field-input password-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <button type=\"button\" class=\"password-toggle\" (click)=\"showPassword = !showPassword\" tabindex=\"-1\"\r\n [attr.aria-label]=\"showPassword ? 'Hide password' : 'Show password'\">\r\n <mat-icon class=\"eye-icon\">{{ showPassword ? 'visibility' : 'visibility_off' }}</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input *ngIf=\"config.subType !== 'LONG' && config.subType !== 'PASSWORD'\"\r\n [type]=\"config.subType === 'EMAIL' ? 'email' : config.subType === 'PHONE' ? 'tel' : 'text'\" class=\"field-input\"\r\n [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\"\r\n [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n <div class=\"char-count-hint\" *ngIf=\"showCharCount\">\r\n {{ remainingCharacters }} characters remaining\r\n </div>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Number Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isNumberField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <span class=\"input-prefix\" *ngIf=\"config.prefix\">{{ config.prefix }}</span>\r\n\r\n <input type=\"number\" class=\"field-input\" [placeholder]=\"config.placeholder || ''\" [formControlName]=\"config.name!\"\r\n [min]=\"config.numberConfig?.min\" [max]=\"config.numberConfig?.max\" [step]=\"config.numberConfig?.step || 1\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"config.readonly\">\r\n\r\n <span class=\"input-suffix\" *ngIf=\"config.suffix\">{{ config.suffix }}</span>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Date Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDateField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input matInput [matDatepicker]=\"datePicker\" class=\"field-input date-input has-icon-right\"\r\n [formControlName]=\"config.name!\" [min]=\"config.dateConfig?.minDate\" [max]=\"config.dateConfig?.maxDate\"\r\n [class.is-invalid]=\"errorMessage\" [placeholder]=\"config.placeholder || ''\" [readonly]=\"config.readonly\"\r\n (click)=\"!config.readonly && datePicker.open()\">\r\n <div class=\"date-icon-wrapper\" *ngIf=\"!config.readonly\">\r\n <mat-datepicker-toggle matSuffix [for]=\"datePicker\"></mat-datepicker-toggle>\r\n </div>\r\n <mat-datepicker #datePicker></mat-datepicker>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Time Input \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isTimeField\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"input-group\" [class.readonly]=\"config.readonly\">\r\n <input type=\"time\" class=\"field-input time-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\" [readonly]=\"!!config.readonly\">\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Autocomplete \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isAutocomplete\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Hidden real control (stores the code value) -->\r\n <input type=\"hidden\" [formControlName]=\"config.name!\">\r\n\r\n <div class=\"autocomplete-wrapper\" [class.is-invalid]=\"errorMessage\" [class.readonly]=\"config.readonly\">\r\n <!-- Search icon -->\r\n <mat-icon class=\"ac-search-icon\">search</mat-icon>\r\n\r\n <input class=\"field-input ac-input\" [formControl]=\"autocompleteInputCtrl\" [matAutocomplete]=\"auto\"\r\n [placeholder]=\"config.placeholder || 'Search\u2026'\" [readonly]=\"!!config.readonly\" [class.is-invalid]=\"errorMessage\"\r\n (blur)=\"onAutocompleteClear()\" autocomplete=\"off\">\r\n\r\n <!-- Clear button -->\r\n <button type=\"button\" class=\"ac-clear-btn\" *ngIf=\"autocompleteInputCtrl.value && !config.readonly\"\r\n (click)=\"autocompleteInputCtrl.setValue(''); updateValue(null)\" tabindex=\"-1\" aria-label=\"Clear\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n\r\n <mat-autocomplete #auto=\"matAutocomplete\" [panelWidth]=\"'auto'\">\r\n <mat-option *ngFor=\"let option of filteredOptions\" [value]=\"option.label\"\r\n (onSelectionChange)=\"onAutocompleteSelected(option)\">\r\n {{ option.label }}\r\n </mat-option>\r\n <mat-option *ngIf=\"filteredOptions.length === 0\" disabled class=\"ac-no-results\">\r\n No results found\r\n </mat-option>\r\n </mat-autocomplete>\r\n\r\n <div class=\"readonly-icons\" *ngIf=\"config.readonly\">\r\n <mat-icon class=\"lock-icon\">lock</mat-icon>\r\n </div>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Dropdown \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isDropdown\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <select *ngIf=\"config.subType === 'SINGLE'\" class=\"field-input\" [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option [ngValue]=\"null\" disabled selected>{{ config.placeholder || 'Select' }}</option>\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <select *ngIf=\"config.subType === 'MULTIPLE'\" class=\"field-input\" multiple [formControlName]=\"config.name!\"\r\n [class.is-invalid]=\"errorMessage\">\r\n <option *ngFor=\"let option of config.optionConfig?.optionList\" [value]=\"option.code\">\r\n {{ option.label }}\r\n </option>\r\n </select>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Radio \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRadio\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"radio-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"radio-label\">\r\n <input type=\"radio\" [formControlName]=\"config.name!\" [value]=\"option.code\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Checkbox \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isCheckbox\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label && config.subType === 'LIST'\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div *ngIf=\"config.subType === 'BOOL'\" class=\"checkbox-single\">\r\n <label class=\"checkbox-label\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span>{{ config.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <div *ngIf=\"config.subType === 'LIST'\" class=\"checkbox-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"checkbox-label\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\">\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Chip \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isChip\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"chip-group\" [class.is-invalid]=\"errorMessage\">\r\n <label *ngFor=\"let option of config.optionConfig?.optionList\" class=\"chip-label\"\r\n [class.selected]=\"isChecked(option.code)\">\r\n <input type=\"checkbox\" [checked]=\"isChecked(option.code)\" [disabled]=\"!!config.disabled\"\r\n (change)=\"onCheckboxListChange(option.code, $any($event.target).checked)\" hidden>\r\n <span>{{ option.label }}</span>\r\n </label>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Switch \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isSwitch\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label class=\"switch-container\">\r\n <span class=\"field-label\">{{ config.label }}</span>\r\n <div class=\"switch\">\r\n <input type=\"checkbox\" [formControlName]=\"config.name!\" [class.is-invalid]=\"errorMessage\">\r\n <span class=\"slider\"></span>\r\n </div>\r\n </label>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rich Text \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRichText\" class=\"field-wrapper component-rich-text\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rich-text-container\" [class.is-invalid]=\"errorMessage\">\r\n <quill-editor [formControlName]=\"config.name!\" class=\"rich-text-editor\"\r\n [placeholder]=\"config.richTextConfig?.placeholder || config.placeholder || ''\"\r\n [styles]=\"{height: config.richTextConfig?.height || '200px'}\">\r\n </quill-editor>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n <div class=\"char-count-hint\" *ngIf=\"showCharCount\">\r\n {{ remainingCharacters }} characters remaining\r\n </div>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Rating \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isRating\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <div class=\"rating-group\" [class.is-invalid]=\"errorMessage\">\r\n <span *ngFor=\"let star of getStarArray()\" class=\"star\" [class.filled]=\"isStarFilled(star)\"\r\n [class.half]=\"isStarHalf(star)\" (click)=\"onRatingChange(star, $event)\">\r\n <mat-icon>{{ isStarFilled(star) || isStarHalf(star) ? 'star' : 'star_border' }}</mat-icon>\r\n </span>\r\n </div>\r\n\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Generated Field (read-only) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isGenerated\" class=\"field-wrapper\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">{{ config.label }}</label>\r\n <div class=\"generated-value\">{{ value || '-' }}</div>\r\n <span class=\"field-hint\" *ngIf=\"config.hint\">{{ config.hint }}</span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 File Upload \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isFileUpload\" class=\"field-wrapper\" [formGroup]=\"formGroup\">\r\n <label *ngIf=\"config.label\" class=\"field-label\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </label>\r\n\r\n <!-- Drop Zone -->\r\n <div class=\"upload-drop-zone\" [class.drag-over]=\"isDragOver\" [class.has-files]=\"value?.length\"\r\n [class.is-invalid]=\"errorMessage\" (dragover)=\"onDragOver($event)\" (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onFileDrop($event)\" (click)=\"fileInput.click()\">\r\n\r\n <div class=\"upload-icon-wrap\">\r\n <mat-icon class=\"upload-cloud-icon\">cloud_upload</mat-icon>\r\n </div>\r\n\r\n <p class=\"upload-main-text\">Drag and drop files here or <span class=\"upload-link\">click to upload</span></p>\r\n <p class=\"upload-hint-text\" *ngIf=\"config.attachmentConfig?.acceptLabel\">\r\n Supported formats:\r\n <span class=\"upload-formats\">{{ config.attachmentConfig!.acceptLabel }}</span>\r\n </p>\r\n <p class=\"upload-hint-text\" *ngIf=\"!config.attachmentConfig?.acceptLabel && config.hint\">\r\n {{ config.hint }}\r\n </p>\r\n\r\n <!-- Hidden native file input -->\r\n <input #fileInput type=\"file\" hidden [attr.multiple]=\"config.attachmentConfig?.multiple ? true : null\"\r\n [attr.accept]=\"config.attachmentConfig?.accept || null\" (change)=\"onFileSelected($event)\">\r\n </div>\r\n\r\n <!-- Uploaded file list -->\r\n <div class=\"uploaded-list\" *ngIf=\"value?.length\">\r\n <div *ngFor=\"let f of value; let i = index\" class=\"uploaded-item\" [class.uploading]=\"f.isUploading\">\r\n\r\n <!-- Uploading spinner (shown while API call is in progress) -->\r\n <ng-container *ngIf=\"f.isUploading; else fileReady\">\r\n <div class=\"upload-spinner\"></div>\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size uploading-label\">Uploading...</span>\r\n </div>\r\n </ng-container>\r\n\r\n <!-- Normal state once upload is done -->\r\n <ng-template #fileReady>\r\n <!-- File type icon -->\r\n <mat-icon class=\"file-type-icon\">{{ getFileIcon(f.type) }}</mat-icon>\r\n\r\n <!-- Image thumbnail (only for images) -->\r\n <img *ngIf=\"f.type?.startsWith('image') && f.dataUrl\" [src]=\"f.dataUrl\" class=\"file-thumb\" alt=\"preview\">\r\n\r\n <!-- Name & size -->\r\n <div class=\"file-info\">\r\n <span class=\"file-name\" [title]=\"f.name\">{{ f.name }}</span>\r\n <span class=\"file-size\">{{ formatFileSize(f.size) }}</span>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Remove button \u2014 disabled while uploading -->\r\n <lib-button [variant]=\"'danger-outline'\" [disabled]=\"f.isUploading\"\r\n (click)=\"!f.isUploading && removeUploadedFile(i)\">\r\n <mat-icon>close</mat-icon>\r\n </lib-button>\r\n </div>\r\n </div>\r\n\r\n <!-- Validation / file errors -->\r\n <span class=\"field-error\" *ngIf=\"fileUploadError\">{{ fileUploadError }}</span>\r\n <span class=\"field-error\" *ngIf=\"errorMessage && !fileUploadError\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\"\r\n *ngIf=\"config.hint && !errorMessage && !fileUploadError && !config.attachmentConfig?.acceptLabel\">\r\n {{ config.hint }}\r\n </span>\r\n </div>\r\n\r\n <!-- \u2550\u2550 Media Upload (Type 2) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div *ngIf=\"isMediaUpload\" class=\"field-wrapper media-upload-wrapper\" [formGroup]=\"formGroup\">\r\n\r\n <!-- Hidden file input lives outside *ngIf \u2014 triggered via ViewChild -->\r\n <input #mediaDeviceInput type=\"file\" hidden multiple accept=\"image/*\" (change)=\"onMediaFileSelected($event)\">\r\n\r\n <!-- Two-column layout -->\r\n <div class=\"mu-layout\">\r\n\r\n <!-- \u2500\u2500 LEFT PANEL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"mu-left\">\r\n\r\n <!-- Header: title + max-items badge -->\r\n <div class=\"mu-header\">\r\n <h3 class=\"mu-title\">\r\n {{ config.label }}\r\n <span class=\"required\" *ngIf=\"config.required\">*</span>\r\n </h3>\r\n <span class=\"mu-badge\" *ngIf=\"config.attachmentConfig?.maxFiles\">\r\n {{ controller.labels['LBL_MEDIA_MAX_PREFIX'] || 'Maximum' }}\r\n {{ config.attachmentConfig?.maxFiles }}\r\n {{ controller.labels['LBL_MEDIA_MAX_SUFFIX'] || 'Image & Videos' }}\r\n </span>\r\n </div>\r\n\r\n <!-- Description -->\r\n <p class=\"mu-description\" *ngIf=\"config.attachmentConfig?.description\">\r\n {{ config.attachmentConfig?.description }}\r\n </p>\r\n <p class=\"mu-description\" *ngIf=\"!config.attachmentConfig?.description && controller.labels['LBL_MEDIA_DESC']\">\r\n {{ controller.labels['LBL_MEDIA_DESC'] }}\r\n </p>\r\n\r\n <!-- Feature bullet list -->\r\n <ul class=\"mu-features\"\r\n *ngIf=\"config.attachmentConfig?.features?.length || controller.labels['LBL_MEDIA_FEATURE_1']\">\r\n <ng-container *ngIf=\"config.attachmentConfig?.features?.length\">\r\n <li *ngFor=\"let f of config.attachmentConfig?.features\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ f }}\r\n </li>\r\n </ng-container>\r\n <ng-container *ngIf=\"!config.attachmentConfig?.features?.length\">\r\n <li *ngIf=\"controller.labels['LBL_MEDIA_FEATURE_1']\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ controller.labels['LBL_MEDIA_FEATURE_1'] }}\r\n </li>\r\n <li *ngIf=\"controller.labels['LBL_MEDIA_FEATURE_2']\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ controller.labels['LBL_MEDIA_FEATURE_2'] }}\r\n </li>\r\n <li *ngIf=\"controller.labels['LBL_MEDIA_FEATURE_3']\" class=\"mu-feature-item\">\r\n <mat-icon class=\"mu-check\">check</mat-icon>{{ controller.labels['LBL_MEDIA_FEATURE_3'] }}\r\n </li>\r\n </ng-container>\r\n </ul>\r\n\r\n <!-- Backdrop to close dropdown on outside click -->\r\n <div class=\"media-menu-backdrop\" *ngIf=\"showMediaMenu\"\r\n (click)=\"$event.stopPropagation(); showMediaMenu = false\"></div>\r\n\r\n <!-- Add Media button + dropdown -->\r\n <div class=\"media-add-container\" (click)=\"showMediaMenu = !showMediaMenu\">\r\n <lib-button id=\"btn-add-media-{{ config.name }}\" [variant]=\"'warning'\"\r\n [icon]=\"{type: 'material', value: 'add_photo_alternate'}\">\r\n {{ controller.labels['LBL_ADD_MEDIA'] || 'Add media' }}\r\n <mat-icon class=\"menu-chevron\">add</mat-icon>\r\n </lib-button>\r\n\r\n <div class=\"media-dropdown\" *ngIf=\"showMediaMenu\" role=\"menu\" (click)=\"$event.stopPropagation()\">\r\n <!-- Video -->\r\n <button id=\"btn-media-video-{{ config.name }}\" type=\"button\" class=\"media-dropdown-item\"\r\n (click)=\"onMediaMenuVideo(); showMediaMenu = false\" role=\"menuitem\">\r\n <span class=\"media-drop-icon media-drop-icon--video\"><mat-icon>videocam</mat-icon></span>\r\n <span class=\"media-drop-text\">\r\n <span class=\"media-drop-label\">{{ controller.labels['LBL_MEDIA_VIDEO'] || 'Video' }}</span>\r\n <span class=\"media-drop-desc\">{{ controller.labels['LBL_MEDIA_VIDEO_DESC'] || 'Add YouTube URL'\r\n }}</span>\r\n </span>\r\n </button>\r\n <!-- Device -->\r\n <button id=\"btn-media-device-{{ config.name }}\" type=\"button\" class=\"media-dropdown-item\"\r\n (click)=\"onMediaMenuDevice(); showMediaMenu = false\" role=\"menuitem\">\r\n <span class=\"media-drop-icon media-drop-icon--device\"><mat-icon>upload</mat-icon></span>\r\n <span class=\"media-drop-text\">\r\n <span class=\"media-drop-label\">{{ controller.labels['LBL_MEDIA_DEVICE'] || 'Upload from device'\r\n }}</span>\r\n <span class=\"media-drop-desc\">{{ controller.labels['LBL_MEDIA_DEVICE_DESC'] || 'Select images from your\r\n computer' }}</span>\r\n </span>\r\n </button>\r\n <!-- Library -->\r\n <button id=\"btn-media-library-{{ config.name }}\" type=\"button\" class=\"media-dropdown-item\"\r\n (click)=\"onMediaMenuLibrary(); showMediaMenu = false\" role=\"menuitem\">\r\n <span class=\"media-drop-icon media-drop-icon--library\"><mat-icon>photo_library</mat-icon></span>\r\n <span class=\"media-drop-text\">\r\n <span class=\"media-drop-label\">{{ controller.labels['LBL_MEDIA_LIBRARY'] || 'Upload from library'\r\n }}</span>\r\n <span class=\"media-drop-desc\">{{ controller.labels['LBL_MEDIA_LIBRARY_DESC'] || 'Choose from default\r\n images' }}</span>\r\n </span>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- YouTube URL Input (inline below button) -->\r\n <div class=\"youtube-input-panel\" *ngIf=\"showYoutubeInput\">\r\n <label class=\"youtube-panel-label\">\r\n {{ controller.labels['LBL_YOUTUBE_URL'] || 'Video URL' }}\r\n </label>\r\n <div class=\"youtube-input-row\">\r\n <input id=\"input-youtube-url-{{ config.name }}\" type=\"url\" class=\"field-input youtube-url-input\"\r\n [(ngModel)]=\"youtubeUrlInput\" [ngModelOptions]=\"{standalone: true}\"\r\n [placeholder]=\"controller.labels['PH_YOUTUBE_URL'] || 'Video URL'\" (keyup.enter)=\"addYoutubeMedia()\">\r\n <lib-button id=\"btn-add-youtube-{{ config.name }}\" [variant]=\"'secondary'\" (click)=\"addYoutubeMedia()\">\r\n {{ controller.labels['LBL_ADD'] || 'Add' }}\r\n </lib-button>\r\n </div>\r\n <span class=\"field-error\" *ngIf=\"youtubeUrlError\">{{ youtubeUrlError }}</span>\r\n </div>\r\n\r\n <div class=\"media-upload-status\" *ngIf=\"mediaUploadError\">\r\n <mat-icon class=\"status-icon\">error_outline</mat-icon>\r\n <span>{{ mediaUploadError }}</span>\r\n </div>\r\n\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</span>\r\n <span class=\"field-hint\" *ngIf=\"config.hint && !errorMessage\">{{ config.hint }}</span>\r\n </div>\r\n <!-- end left panel -->\r\n\r\n <!-- \u2500\u2500 RIGHT PANEL (carousel) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div class=\"mu-right\">\r\n\r\n <!-- Carousel (when items exist) -->\r\n <div class=\"media-carousel-section\" *ngIf=\"mediaItems.length\">\r\n <div class=\"media-carousel-main\">\r\n <button id=\"btn-carousel-prev-{{ config.name }}\" type=\"button\" class=\"carousel-nav carousel-nav--prev\"\r\n (click)=\"mediaCarouselPrev()\" [disabled]=\"mediaCarouselIndex === 0\" aria-label=\"Previous\">\r\n <mat-icon>chevron_left</mat-icon>\r\n </button>\r\n\r\n <div class=\"carousel-viewer\" *ngFor=\"let item of mediaItems; let i = index\"\r\n [hidden]=\"i !== mediaCarouselIndex\">\r\n <div *ngIf=\"item.isUploading\" class=\"carousel-uploading\">\r\n <div class=\"carousel-spinner\"></div>\r\n <span>{{ controller.labels['LBL_UPLOADING'] || 'Uploading\u2026' }}</span>\r\n </div>\r\n <ng-container *ngIf=\"!item.isUploading && item.mediaType === 'youtube'\">\r\n <iframe class=\"carousel-iframe\" [src]=\"item.url | trustedUrl\" frameborder=\"0\" allowfullscreen\r\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\">\r\n </iframe>\r\n </ng-container>\r\n <ng-container *ngIf=\"!item.isUploading && item.mediaType === 'image'\">\r\n <img class=\"carousel-image\" [src]=\"item.url\" alt=\"Media\">\r\n </ng-container>\r\n <button id=\"btn-remove-media-{{ config.name }}-{{ i }}\" type=\"button\" class=\"carousel-remove-btn\"\r\n [disabled]=\"item.isUploading\" (click)=\"removeMediaItem(i)\" aria-label=\"Remove\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <button id=\"btn-carousel-next-{{ config.name }}\" type=\"button\" class=\"carousel-nav carousel-nav--next\"\r\n (click)=\"mediaCarouselNext()\" [disabled]=\"mediaCarouselIndex === mediaItems.length - 1\" aria-label=\"Next\">\r\n <mat-icon>chevron_right</mat-icon>\r\n </button>\r\n\r\n <div class=\"carousel-dots\">\r\n <span *ngFor=\"let item of mediaItems; let i = index\" class=\"carousel-dot\"\r\n [class.active]=\"i === mediaCarouselIndex\" (click)=\"mediaGoTo(i)\"></span>\r\n </div>\r\n </div>\r\n\r\n <!-- Thumbnail strip -->\r\n <div class=\"media-thumbnail-strip\">\r\n <div *ngFor=\"let item of mediaThumbnails; let i = index\" class=\"media-thumb\"\r\n [class.active]=\"i === mediaCarouselIndex\" (click)=\"mediaGoTo(i)\">\r\n <div *ngIf=\"item.isUploading\" class=\"thumb-uploading\">\r\n <div class=\"thumb-spinner\"></div>\r\n </div>\r\n <img *ngIf=\"!item.isUploading && item.mediaType === 'youtube' && item.thumbnailUrl\"\r\n [src]=\"item.thumbnailUrl\" class=\"thumb-img\" alt=\"Video thumbnail\">\r\n <div *ngIf=\"!item.isUploading && item.mediaType === 'youtube' && !item.thumbnailUrl\"\r\n class=\"thumb-yt-placeholder\">\r\n <mat-icon>play_circle</mat-icon>\r\n </div>\r\n <img *ngIf=\"!item.isUploading && item.mediaType === 'image' && item.url\" [src]=\"item.url\"\r\n class=\"thumb-img\" alt=\"Image thumbnail\">\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Empty right-side placeholder -->\r\n <div class=\"mu-right-empty\" *ngIf=\"!mediaItems.length\" (click)=\"onMediaMenuDevice()\">\r\n <mat-icon class=\"mu-right-empty-icon\">perm_media</mat-icon>\r\n <p>{{ controller.labels['LBL_ADD_MEDIA'] || 'Add media' }}</p>\r\n </div>\r\n\r\n </div>\r\n <!-- end right panel -->\r\n\r\n </div><!-- end mu-layout -->\r\n </div>\r\n\r\n\r\n <!-- \u2550\u2550 Library Image Picker Modal \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n <div class=\"media-library-overlay\" *ngIf=\"showLibraryModal\" (click)=\"closeLibraryModal()\"></div>\r\n <div class=\"media-library-modal\" *ngIf=\"showLibraryModal\" role=\"dialog\" aria-modal=\"true\">\r\n <div class=\"library-modal-header\">\r\n <h3 class=\"library-modal-title\">\r\n <mat-icon>photo_library</mat-icon>\r\n {{ controller.labels['LBL_LIBRARY_TITLE'] || 'Media Library' }}\r\n </h3>\r\n <button id=\"btn-close-library-{{ config.name }}\" type=\"button\" class=\"library-close-btn\"\r\n (click)=\"closeLibraryModal()\" aria-label=\"Close\">\r\n <mat-icon>close</mat-icon>\r\n </button>\r\n </div>\r\n\r\n <!-- Loading -->\r\n <div class=\"library-loading\" *ngIf=\"libraryLoading\">\r\n <div class=\"lib-spinner\"></div>\r\n <span>{{ controller.labels['LBL_LOADING'] || 'Loading\u2026' }}</span>\r\n </div>\r\n\r\n <!-- Error -->\r\n <div class=\"library-error\" *ngIf=\"libraryError && !libraryLoading\">\r\n <mat-icon>error_outline</mat-icon>\r\n {{ libraryError }}\r\n </div>\r\n\r\n <!-- Image grid -->\r\n <div class=\"library-grid\" *ngIf=\"!libraryLoading && libraryImages.length\">\r\n <div *ngFor=\"let img of libraryImages; let li = index\" id=\"lib-img-{{ config.name }}-{{ li }}\"\r\n class=\"library-grid-item\" [class.selected]=\"isLibraryItemSelected(img)\" (click)=\"toggleLibraryItem(img)\">\r\n <img [src]=\"getLibraryItemUrl(img)\" class=\"library-grid-img\" alt=\"Library image\">\r\n <div class=\"library-check\" *ngIf=\"isLibraryItemSelected(img)\">\r\n <mat-icon>check_circle</mat-icon>\r\n </div>\r\n <div class=\"library-overlay-hover\"></div>\r\n </div>\r\n </div>\r\n\r\n <!-- Empty library -->\r\n <div class=\"library-empty\" *ngIf=\"!libraryLoading && !libraryError && libraryImages.length === 0\">\r\n <mat-icon>image_not_supported</mat-icon>\r\n <span>{{ controller.labels['LBL_LIBRARY_EMPTY'] || 'No images found in library.' }}</span>\r\n </div>\r\n\r\n <!-- Footer -->\r\n <div class=\"library-modal-footer\">\r\n <span class=\"library-selected-count\">\r\n {{ librarySelectedIds.size }} {{ controller.labels['LBL_LIBRARY_SELECTED'] || 'selected' }}\r\n </span>\r\n <div class=\"library-footer-actions\">\r\n <lib-button id=\"btn-library-cancel-{{ config.name }}\" [variant]=\"'outline'\" (click)=\"closeLibraryModal()\">\r\n {{ controller.labels['LBL_CANCEL'] || 'Cancel' }}\r\n </lib-button>\r\n <lib-button id=\"btn-library-confirm-{{ config.name }}\" [variant]=\"'primary'\"\r\n [disabled]=\"librarySelectedIds.size === 0\" (click)=\"confirmLibrarySelection()\">\r\n {{ controller.labels['LBL_LIBRARY_ADD_SELECTED'] || 'Add Selected' }}\r\n </lib-button>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n</div>", styles: [".form-field{margin-bottom:var(--cc-sf-grid-gap, 16px)}.form-field.has-error .field-input{border-color:var(--cc-sf-error-border, #DC2626)}.form-row{display:flex;gap:var(--cc-sf-grid-gap, 16px)}.form-row.horizontal{flex-direction:row}.form-row.horizontal>*{flex:1}.form-row:not(.horizontal){flex-direction:column}.form-row.grid-row{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.form-row.grid-row{grid-template-columns:1fr}.form-row.grid-row .row-field{grid-column:span 12!important}}.field-wrapper{display:flex;flex-direction:column;gap:6px}.field-label{display:block;color:var(--cc-sf-label-color, #202124);font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif);font-weight:var(--cc-sf-label-weight, 500);font-size:var(--cc-sf-label-size, 18px);line-height:var(--cc-sf-label-line-height, 100%);letter-spacing:var(--cc-sf-label-letter-spacing, 0%);margin-bottom:.5rem}.field-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.field-input{width:100%;opacity:var(--cc-sf-input-opacity, 1);gap:var(--cc-sf-input-gap, 10px);padding-top:var(--cc-sf-input-padding-y, .625rem);padding-bottom:var(--cc-sf-input-padding-y, .625rem);padding-left:var(--cc-sf-input-padding-x, 16px);padding-right:var(--cc-sf-input-padding-x, 16px);font-size:var(--cc-sf-input-font-size, .875rem);line-height:var(--cc-sf-input-line-height, 1.5);color:var(--cc-sf-input-color, #111827);background-color:var(--cc-sf-input-bg, #ffffff);border:var(--cc-sf-input-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-input-radius, 7px);box-shadow:var(--cc-sf-input-shadow, none);transition:var(--cc-sf-input-transition, all .2s ease);font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif)}.field-input::placeholder{font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif);font-weight:var(--cc-sf-placeholder-weight, 400);font-size:var(--cc-sf-placeholder-size, 16px);line-height:var(--cc-sf-placeholder-line-height, 100%);letter-spacing:var(--cc-sf-placeholder-letter-spacing, 0%);color:var(--cc-sf-input-placeholder, #9CA3AF)}.field-input:hover:not(:disabled):not([readonly]){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.field-input:focus{outline:none;border-color:var(--cc-sf-input-focus-border, #3B82F6);box-shadow:var(--cc-sf-input-focus-shadow, 0 0 0 3px rgba(59, 130, 246, .12))}.field-input:disabled,.field-input[readonly]{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.field-input.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.field-input.is-invalid:focus{box-shadow:var(--cc-sf-error-focus-shadow, 0 0 0 3px rgba(220, 38, 38, .1))}.field-input.textarea{resize:vertical;min-height:100px;font-family:var(--cc-sf-font-family, inherit)}input[type=time].time-input{cursor:pointer}input[type=time].time-input::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.7;filter:invert(30%)}input[type=time].time-input::-webkit-calendar-picker-indicator:hover{opacity:1}select.field-input{appearance:none;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;cursor:pointer}select.field-input:disabled{cursor:not-allowed}.field-hint{font-size:var(--cc-sf-hint-size, .75rem);color:var(--cc-sf-hint-color, #6B7280)}.char-count-hint{font-family:var(--cc-sf-font-family, \"Poppins\", sans-serif);font-weight:400;font-style:normal;font-size:14px;line-height:100%;letter-spacing:.02em;text-align:right;color:var(--cc-sf-hint-color, #6B7280);margin-top:4px}.field-error{font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.radio-group,.checkbox-group{display:flex;flex-direction:column;gap:8px}.radio-label,.checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--cc-sf-label-size, .875rem);color:var(--cc-sf-label-color, #111827)}.radio-label input,.checkbox-label input{cursor:pointer;accent-color:var(--cc-sf-chip-selected-bg, #3B82F6)}.checkbox-single .checkbox-label{font-weight:var(--cc-sf-label-weight, 500)}.chip-group{display:flex;flex-wrap:wrap;gap:8px}.chip-label{padding:var(--cc-sf-chip-padding, 6px 14px);background:var(--cc-sf-chip-bg, #ffffff);color:var(--cc-sf-chip-color, #374151);border:var(--cc-sf-chip-border, 1px solid #D1D5DB);border-radius:var(--cc-sf-chip-radius, 20px);cursor:pointer;font-size:var(--cc-sf-font-size-base, .875rem);transition:var(--cc-sf-input-transition, all .2s ease)}.chip-label:hover{background:var(--cc-sf-chip-hover-bg, #F3F4F6)}.chip-label.selected{background:var(--cc-sf-chip-selected-bg, #3B82F6);color:var(--cc-sf-chip-selected-color, #ffffff);border-color:var(--cc-sf-chip-selected-border, #3B82F6)}.switch-container{display:flex;justify-content:space-between;align-items:center;cursor:pointer}.switch{position:relative;width:50px;height:24px}.switch input{opacity:0;width:0;height:0}.switch input:checked+.slider{background-color:var(--cc-sf-switch-track-on, #3B82F6)}.switch input:checked+.slider:before{transform:translate(26px)}.switch .slider{position:absolute;cursor:pointer;inset:0;background-color:var(--cc-sf-switch-track-off, #D1D5DB);transition:.4s;border-radius:24px}.switch .slider:before{position:absolute;content:\"\";height:18px;width:18px;left:3px;bottom:3px;background-color:var(--cc-sf-switch-thumb, #ffffff);transition:.4s;border-radius:50%}.rating-group{display:flex;gap:4px}.rating-group .star{display:inline-flex;align-items:center;cursor:pointer;transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star mat-icon{font-size:var(--cc-sf-star-size, 28px);width:var(--cc-sf-star-size, 28px);height:var(--cc-sf-star-size, 28px);line-height:var(--cc-sf-star-size, 28px);color:var(--cc-sf-star-empty, #D1D5DB);transition:var(--cc-sf-input-transition, all .2s ease)}.rating-group .star.filled mat-icon,.rating-group .star.half mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.rating-group .star:hover mat-icon{color:var(--cc-sf-star-filled, #F59E0B)}.password-wrapper{position:relative;display:flex;align-items:center}.password-wrapper .password-input{padding-right:2.75rem;width:100%}.password-wrapper .password-toggle{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;padding:.25rem;line-height:1;color:var(--cc-sf-hint-color, #6B7280);display:flex;align-items:center;justify-content:center;transition:color var(--cc-sf-input-transition, .2s ease)}.password-wrapper .password-toggle mat-icon.eye-icon{font-size:1.125rem;width:1.125rem;height:1.125rem;line-height:1.125rem}.password-wrapper .password-toggle:hover{color:var(--cc-sf-label-color, #374151)}.password-wrapper .password-toggle:focus{outline:none}.generated-value{padding:var(--cc-sf-generated-padding, .625rem .875rem);background:var(--cc-sf-generated-bg, #F3F4F6);border:var(--cc-sf-generated-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-generated-radius, 8px);font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-generated-color, #6B7280);font-family:var(--cc-sf-font-family, inherit)}.group-section-wrapper{margin-bottom:var(--cc-sf-section-gap, 20px);border:var(--cc-sf-section-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-section-radius, 10px);padding:var(--cc-sf-section-padding, 20px);background-color:var(--cc-sf-section-bg, #ffffff);box-shadow:var(--cc-sf-section-shadow, 0 1px 4px rgba(0, 0, 0, .05))}.group-section-wrapper .group-label{font-size:var(--cc-sf-section-label-size, 1rem);font-weight:var(--cc-sf-section-label-weight, 600);color:var(--cc-sf-section-label-color, #1F2937);margin:0 0 16px;padding-bottom:10px;border-bottom:var(--cc-sf-section-label-border, 2px solid #E5E7EB)}.group-section-wrapper .group-fields.sf-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:var(--cc-sf-grid-gap, 16px);align-items:start}@media(max-width:640px){.group-section-wrapper .group-fields.sf-grid{grid-template-columns:1fr}.group-section-wrapper .group-fields.sf-grid .sf-col{grid-column:span 12!important}}.group-section-wrapper .group-instance{position:relative;margin-bottom:16px;padding:var(--cc-sf-instance-padding, 16px);background:var(--cc-sf-instance-bg, #F9FAFB);border:var(--cc-sf-instance-border, 1px solid #E5E7EB);border-radius:var(--cc-sf-instance-radius, 8px)}.group-section-wrapper .group-instance:last-child{margin-bottom:0}.group-section-wrapper .group-instance .group-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding-bottom:10px;border-bottom:var(--cc-sf-instance-divider, 1px dashed #D1D5DB)}.group-section-wrapper .group-instance .group-header .group-number{font-weight:500;color:var(--cc-sf-instance-num-color, #4B5563);font-size:var(--cc-sf-instance-num-size, .8125rem)}.group-section-wrapper.multi-save-active{border:none;box-shadow:none;padding:0;background:transparent}.group-section-wrapper.multi-save-active .group-label{border:none;padding-bottom:0;margin-bottom:0}.group-section-wrapper.multi-save-active .multi-save-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button{color:var(--ms-btn-add-color, #3B82F6);font-weight:600;font-size:.875rem;padding:8px 12px}.group-section-wrapper.multi-save-active .multi-save-header .btn-add-multi ::ng-deep button:hover{color:var(--ms-btn-add-hover, #2563EB);background-color:var(--cc-sf-btn-add-hover-bg, #EFF6FF)}.group-section-wrapper.multi-save-active .group-instance{background:var(--ms-card-bg, #ffffff);border:1px solid var(--ms-card-border, #E5E7EB);border-radius:var(--ms-card-radius, 10px);box-shadow:var(--ms-card-shadow, 0 1px 4px rgba(0, 0, 0, .05));padding:0;margin-bottom:16px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-editing{padding:24px}.group-section-wrapper.multi-save-active .group-instance.is-card{cursor:pointer;transition:all .2s ease-in-out}.group-section-wrapper.multi-save-active .group-instance.is-card:hover{box-shadow:var(--ms-card-shadow-hover, 0 8px 24px rgba(0, 0, 0, .08));border-color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view{display:flex;justify-content:space-between;align-items:center;padding:18px 24px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content{flex:1;display:flex;flex-direction:column;gap:4px;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-title{font-size:1rem;font-weight:600;color:var(--ms-title-color, #111827);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-content .card-desc{font-size:.875rem;color:var(--ms-desc-color, #6B7280);line-height:1.4;display:-webkit-box;-webkit-line-clamp:1;line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view.is-expanded .card-content .card-desc{-webkit-line-clamp:unset;line-clamp:unset}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions{display:flex;align-items:center;gap:16px;margin-left:20px}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon{font-size:22px;width:22px;height:22px;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .2s}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-delete:hover{color:var(--cc-sf-error-border, #DC2626)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-edit:hover{color:var(--cc-sf-input-focus-border, #3B82F6)}.group-section-wrapper.multi-save-active .group-instance.is-card .card-view .card-actions mat-icon.icon-expand{color:var(--cc-sf-input-disabled-border, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-top:24px;padding-top:20px;border-top:1px solid var(--cc-sf-instance-divider, #E5E7EB)}.group-section-wrapper.multi-save-active .group-footer .footer-actions{display:flex;gap:12px}.btn-remove{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:var(--cc-sf-btn-remove-bg, #FFF5F5);color:var(--cc-sf-btn-remove-color, #E53E3E);border:var(--cc-sf-btn-remove-border, 1px solid #FED7D7);border-radius:var(--cc-sf-btn-remove-radius, 4px);cursor:pointer;font-size:var(--cc-sf-error-text-size, .75rem);transition:var(--cc-sf-btn-transition, all .2s ease)}.btn-remove mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.btn-remove:hover{background:var(--cc-sf-btn-remove-hover-bg, #FED7D7)}.btn-add-group{display:flex;align-items:center;justify-content:center;gap:4px;width:100%;padding:8px 16px;margin-top:12px;background:var(--cc-sf-btn-add-bg, transparent);color:var(--cc-sf-btn-add-color, #3B82F6);border:var(--cc-sf-btn-add-border, 1px dashed #CBD5E1);border-radius:var(--cc-sf-btn-add-radius, 6px);cursor:pointer;font-size:var(--cc-sf-btn-font-size, .875rem);font-weight:var(--cc-sf-btn-font-weight, 600);transition:var(--cc-sf-btn-transition, all .2s ease);font-family:var(--cc-sf-font-family, inherit)}.btn-add-group mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.btn-add-group:hover{background:var(--cc-sf-btn-add-hover-bg, #EFF6FF);border-color:var(--cc-sf-btn-add-hover-border, #BFDBFE)}.upload-drop-zone{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 24px;border:var(--cc-sf-dropzone-border, 1.5px dashed #CBD5E1);border-radius:var(--cc-sf-dropzone-radius, 12px);background-color:var(--cc-sf-dropzone-bg, #FFFAF1);cursor:pointer;transition:background-color .2s ease,border-color .2s ease;text-align:center;-webkit-user-select:none;user-select:none}.upload-drop-zone:hover{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-hover-border, #93C5FD)}.upload-drop-zone.drag-over{background-color:var(--cc-sf-dropzone-hover-bg, #EFF6FF);border-color:var(--cc-sf-dropzone-over-border, #3B82F6);box-shadow:var(--cc-sf-dropzone-over-shadow, 0 0 0 4px rgba(59, 130, 246, .12))}.upload-drop-zone.is-invalid{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.upload-icon-wrap{margin-bottom:4px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-icon-wrap mat-icon.upload-cloud-icon{font-size:56px;width:56px;height:56px;line-height:56px;color:var(--cc-sf-dropzone-icon-color, #94A3B8)}.upload-main-text{font-size:.9rem;font-weight:600;color:var(--cc-sf-label-color, #1E293B);margin:0}.upload-main-text .upload-link{color:var(--cc-sf-dropzone-link-color, #3B82F6)}.upload-hint-text{font-size:.78rem;color:var(--cc-sf-dropzone-hint-color, #64748B);margin:0}.upload-hint-text .upload-formats{color:var(--cc-sf-dropzone-link-color, #3B82F6);font-weight:500}.uploaded-list{display:flex;flex-direction:column;gap:8px;margin-top:10px}.uploaded-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--cc-sf-uploaded-item-bg, #ffffff);border:var(--cc-sf-uploaded-item-border, 1px solid #E2E8F0);border-radius:var(--cc-sf-uploaded-item-radius, 8px);transition:box-shadow .15s ease}.uploaded-item:hover{box-shadow:0 2px 6px #0000000f}.uploaded-item mat-icon.file-type-icon{font-size:20px;width:20px;height:20px;line-height:20px;flex-shrink:0;color:var(--cc-sf-hint-color, #64748B)}.uploaded-item .file-thumb{width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0}.uploaded-item .file-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.uploaded-item .file-info .file-name{font-size:.85rem;font-weight:500;color:var(--cc-sf-label-color, #1E293B);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uploaded-item .file-info .file-size{font-size:.72rem;color:var(--cc-sf-hint-color, #94A3B8)}.uploaded-item .file-remove-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;color:var(--cc-sf-uploaded-remove-color, #94A3B8);padding:4px;border-radius:4px;flex-shrink:0;transition:color .15s ease,background .15s ease}.uploaded-item .file-remove-btn mat-icon{font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem}.uploaded-item .file-remove-btn:hover{color:var(--cc-sf-uploaded-remove-hover-color, #DC2626);background:var(--cc-sf-uploaded-remove-hover-bg, #FEF2F2)}.uploaded-item.uploading{background:var(--cc-sf-uploaded-uploading-bg, #F8FAFC);border-color:var(--cc-sf-uploaded-uploading-border, #CBD5E1);opacity:.85}.upload-spinner{width:20px;height:20px;flex-shrink:0;border:2px solid var(--cc-sf-spinner-track, #E2E8F0);border-top-color:var(--cc-sf-spinner-color, #3B82F6);border-radius:50%;animation:cc-spin .7s linear infinite}@keyframes cc-spin{to{transform:rotate(360deg)}}.rich-text-editor{display:block;width:100%}.uploading-label{color:var(--cc-sf-spinner-color, #3B82F6)!important;font-style:italic}.input-group{position:relative;display:flex;align-items:stretch;width:100%}.input-group .field-input{flex:1;border-radius:var(--cc-sf-input-radius, 8px)}.input-prefix+.input-group .field-input{border-top-left-radius:0;border-bottom-left-radius:0}.input-group .field-input:has(+.input-suffix){border-top-right-radius:0;border-bottom-right-radius:0}.input-group .field-input.has-icon-right{padding-right:3rem}.input-group.readonly .field-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);cursor:default;padding-right:3.5rem}.input-group.readonly .input-prefix,.input-group.readonly .input-suffix{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6)}.input-prefix,.input-suffix{display:flex;align-items:center;padding:0 .875rem;background-color:var(--cc-sf-input-bg, #FFFFFF);border:var(--cc-sf-input-border, 1.5px solid #D1D5DB);color:var(--cc-sf-hint-color, #6B7280);font-size:var(--cc-sf-input-font-size, .875rem);white-space:nowrap;-webkit-user-select:none;user-select:none}.input-prefix{border-right:none;border-top-left-radius:var(--cc-sf-input-radius, 8px);border-bottom-left-radius:var(--cc-sf-input-radius, 8px)}.input-suffix{border-left:none;border-top-right-radius:var(--cc-sf-input-radius, 8px);border-bottom-right-radius:var(--cc-sf-input-radius, 8px);color:var(--cc-sf-chip-selected-bg, #3B82F6);font-weight:500}.readonly-icons{position:absolute;right:.875rem;top:50%;transform:translateY(-50%);display:flex;gap:8px;pointer-events:none}.readonly-icons mat-icon.lock-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem;opacity:.5;color:var(--cc-sf-hint-color, #6B7280)}.date-icon-wrapper{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;pointer-events:auto}.date-icon-wrapper .mat-icon-button{width:32px;height:32px;line-height:32px}.subfields-group-wrapper{margin-bottom:var(--cc-sf-grid-gap, 16px)}.subfields-group-wrapper .group-label{display:block;font-size:var(--cc-sf-label-size, .875rem);font-weight:600;color:var(--cc-sf-label-color, #111827);margin-bottom:.75rem}.subfields-group-wrapper .group-label .required{color:var(--cc-sf-label-required-color, #DC2626);margin-left:.125rem}.subfields-group-wrapper .subfields-row{display:flex;align-items:flex-end;gap:12px;border-radius:var(--cc-sf-input-radius, 8px);transition:all .2s ease}.subfields-group-wrapper .subfields-row.is-invalid .subfield-item ::ng-deep .field-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.subfields-group-wrapper .subfields-row .subfield-item{flex:1;min-width:0}.subfields-group-wrapper .subfields-row .subfield-item ::ng-deep .field-label{font-size:.75rem!important;margin-bottom:4px!important;font-weight:500!important;color:var(--cc-sf-hint-color, #6B7280)!important}.subfields-group-wrapper .subfields-row .subfield-separator{margin-bottom:24px;font-weight:700;color:#94a3b8}.subfields-group-wrapper .subfields-group-error{display:block;margin-top:6px;font-size:var(--cc-sf-error-text-size, .75rem);color:var(--cc-sf-error-text-color, #DC2626)}.autocomplete-wrapper{position:relative;display:flex;align-items:center;width:100%}.autocomplete-wrapper .ac-search-icon{position:absolute;left:.75rem;font-size:1.1rem;width:1.1rem;height:1.1rem;line-height:1.1rem;color:var(--cc-sf-hint-color, #9CA3AF);pointer-events:none;z-index:1;transition:color var(--cc-sf-input-transition, .2s ease)}.autocomplete-wrapper .ac-input{flex:1;padding-left:2.4rem;padding-right:2.4rem}.autocomplete-wrapper .ac-clear-btn{position:absolute;right:.6rem;display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:.2rem;border-radius:50%;color:var(--cc-sf-hint-color, #9CA3AF);transition:color .15s ease,background .15s ease}.autocomplete-wrapper .ac-clear-btn mat-icon{font-size:1rem;width:1rem;height:1rem;line-height:1rem}.autocomplete-wrapper .ac-clear-btn:hover{color:var(--cc-sf-label-color, #374151);background:var(--cc-sf-input-disabled-bg, #F3F4F6)}.autocomplete-wrapper .ac-clear-btn:focus{outline:none}.autocomplete-wrapper:focus-within .ac-search-icon{color:var(--cc-sf-input-focus-border, #3B82F6)}.autocomplete-wrapper.is-invalid .ac-input{border-color:var(--cc-sf-error-border, #DC2626);background-color:var(--cc-sf-error-bg, #FEF2F2)}.autocomplete-wrapper.readonly .ac-input{background-color:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-input-disabled-color, #6B7280);cursor:not-allowed;border-color:var(--cc-sf-input-disabled-border, #E5E7EB)}.ac-no-results{font-style:italic;font-size:.8125rem;color:var(--cc-sf-hint-color, #6B7280)}.media-upload-wrapper{padding:0;border:none;background:none}.mu-layout{display:grid;grid-template-columns:1fr 1fr;gap:32px;align-items:flex-start}@media(max-width:768px){.mu-layout{grid-template-columns:1fr}}.mu-left{display:flex;flex-direction:column;gap:20px}.mu-header{display:flex;align-items:flex-start;flex-wrap:wrap;gap:10px}.mu-title{margin:0;font-size:1.35rem;font-weight:700;color:var(--cc-sf-label-color, #111827);line-height:1.3}.mu-badge{display:inline-flex;align-items:center;padding:4px 12px;border-radius:20px;background:var(--cc-sf-label-color, #111827);color:#fff;font-size:.72rem;font-weight:600;white-space:nowrap;flex-shrink:0}.mu-description{margin:0;font-size:.875rem;color:var(--cc-sf-hint-color, #6B7280);line-height:1.6}.mu-features{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.mu-feature-item{display:flex;align-items:center;gap:8px;font-size:.875rem;color:var(--cc-sf-hint-color, #374151)}.mu-feature-item .mu-check{font-size:16px;width:16px;height:16px;line-height:16px;color:var(--cc-sf-chip-selected-bg, #3B82F6);flex-shrink:0}.mu-right{display:flex;flex-direction:column;gap:12px;min-height:260px}.mu-right-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;height:100%;min-height:250px;max-width:400px;border:2px dashed var(--cc-sf-dropzone-border, #CBD5E1);border-radius:var(--mu-carousel-radius, 12px);background:var(--cc-sf-dropzone-bg, #FFFAF1);text-align:center;color:var(--cc-sf-hint-color, #94A3B8);padding:24px;box-shadow:0 2px 10px #0000000d;transition:box-shadow .2s ease}.mu-right-empty:hover{cursor:pointer;box-shadow:0 4px 16px #0000001a}.mu-right-empty .mu-right-empty-icon{font-size:52px;width:52px;height:52px;line-height:52px;opacity:.3}.mu-right-empty p{margin:0;font-size:.85rem}.media-add-container{position:relative;display:inline-block}.media-add-container ::ng-deep button{display:flex;align-items:center;gap:6px}.media-add-container ::ng-deep button .menu-chevron{font-size:18px;width:18px;height:18px;line-height:18px;transition:transform .2s ease}.media-dropdown{position:absolute;top:calc(100% + 6px);left:0;z-index:200;min-width:240px;background:var(--mu-dropdown-bg, #ffffff);border:var(--mu-dropdown-border, 1px solid #E5E7EB);border-radius:12px;box-shadow:var(--mu-dropdown-shadow, 0 8px 32px rgba(0, 0, 0, .12));overflow:hidden;animation:mu-fade-in .15s ease}@keyframes mu-fade-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}.media-dropdown-item{display:flex;align-items:center;gap:12px;width:100%;padding:12px 16px;background:none;border:none;border-bottom:1px solid var(--cc-sf-input-disabled-border, #F3F4F6);cursor:pointer;text-align:left;transition:background .15s ease;font-family:var(--cc-sf-font-family, inherit)}.media-dropdown-item:last-child{border-bottom:none}.media-dropdown-item:hover{background:var(--cc-sf-dropzone-hover-bg, #F0F9FF)}.media-drop-icon{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:8px;flex-shrink:0}.media-drop-icon mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.media-drop-icon--video{background:var(--mu-icon-video-bg, #FFF0F0);color:var(--mu-icon-video-color, #EF4444)}.media-drop-icon--device{background:var(--mu-icon-device-bg, #EFF6FF);color:var(--mu-icon-device-color, #3B82F6)}.media-drop-icon--library{background:var(--mu-icon-library-bg, #F0FDF4);color:var(--mu-icon-library-color, #22C55E)}.media-drop-text{display:flex;flex-direction:column;gap:2px;flex:1}.media-drop-label{font-size:.875rem;font-weight:600;color:var(--cc-sf-label-color, #111827)}.media-drop-desc{font-size:.75rem;color:var(--cc-sf-hint-color, #6B7280)}.youtube-input-panel{display:flex;flex-direction:column;gap:8px;padding:16px;background:var(--cc-sf-dropzone-bg, #FFFAF1);border:1px solid var(--cc-sf-input-border, #E5E7EB);border-radius:10px;animation:mu-fade-in .18s ease}.youtube-panel-label{display:flex;align-items:center;gap:6px;font-size:.875rem;font-weight:600;color:var(--cc-sf-label-color, #111827)}.youtube-panel-label mat-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--mu-icon-video-color, #EF4444)}.youtube-input-row{display:flex;gap:8px;align-items:stretch}.youtube-input-row .youtube-url-input{flex:1}.media-menu-backdrop{position:fixed;inset:0;z-index:100}.media-upload-status{display:flex;align-items:center;gap:8px;padding:10px 14px;margin-top:4px;background:var(--cc-sf-error-bg, #FEF2F2);color:var(--cc-sf-error-text-color, #DC2626);border-radius:8px;font-size:.85rem;font-weight:500;animation:mu-fade-in .2s ease}.media-upload-status .status-icon{font-size:18px;width:18px;height:18px;line-height:18px}.media-carousel-section{display:flex;flex-direction:column;gap:12px}.media-carousel-main{position:relative;width:100%;max-width:400px;height:var(--mu-carousel-height, 250px);background:var(--mu-carousel-bg, #0F172A);border-radius:var(--mu-carousel-radius, 12px);overflow:hidden;display:flex;align-items:center;justify-content:center}.carousel-viewer{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}.carousel-viewer .carousel-image{width:100%;height:100%;object-fit:cover;border-radius:var(--mu-carousel-radius, 12px)}.carousel-viewer .carousel-iframe{width:100%;height:100%;border-radius:var(--mu-carousel-radius, 12px)}.carousel-viewer .carousel-uploading{display:flex;flex-direction:column;align-items:center;gap:12px;color:#94a3b8;font-size:.85rem}.carousel-viewer .carousel-spinner{width:36px;height:36px;border:3px solid rgba(255,255,255,.15);border-top-color:#3b82f6;border-radius:50%;animation:cc-spin .7s linear infinite}.carousel-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:10;width:40px;height:40px;border-radius:50%;background:#ffffffd9;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px #0003;transition:background .2s ease,opacity .2s ease;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.carousel-nav mat-icon{font-size:22px;width:22px;height:22px;line-height:22px;color:#1e293b}.carousel-nav:hover:not(:disabled){background:#fff}.carousel-nav:disabled{opacity:.3;cursor:not-allowed}.carousel-nav--prev{left:12px}.carousel-nav--next{right:12px}.carousel-remove-btn{position:absolute;top:10px;right:10px;z-index:10;width:32px;height:32px;border-radius:50%;background:#0000008c;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease}.carousel-remove-btn mat-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:#fff}.carousel-remove-btn:hover:not(:disabled){background:#dc2626d9}.carousel-remove-btn:disabled{opacity:.4;cursor:not-allowed}.carousel-dots{position:absolute;bottom:10px;left:50%;transform:translate(-50%);display:flex;gap:6px;z-index:10}.carousel-dot{width:8px;height:8px;border-radius:50%;background:#ffffff73;cursor:pointer;transition:background .2s ease,transform .2s ease}.carousel-dot.active{background:#fff;transform:scale(1.3)}.media-thumbnail-strip{display:flex;flex-wrap:wrap;max-width:400px;gap:8px;overflow-x:auto;padding-bottom:4px}.media-thumbnail-strip::-webkit-scrollbar{height:4px}.media-thumbnail-strip::-webkit-scrollbar-thumb{background:var(--cc-sf-input-disabled-border, #D1D5DB);border-radius:2px}.media-thumb{flex-shrink:0;width:72px;height:52px;border-radius:8px;overflow:hidden;cursor:pointer;border:2px solid transparent;background:var(--mu-thumb-bg, #E2E8F0);display:flex;align-items:center;justify-content:center;transition:border-color .2s ease,transform .15s ease}.media-thumb.active{border-color:var(--mu-thumb-active-border, #3B82F6);transform:scale(1.04)}.media-thumb:hover:not(.active){border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.media-thumb .thumb-img{width:100%;height:100%;object-fit:cover}.media-thumb .thumb-yt-placeholder{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:#1e293b;color:#ef4444}.media-thumb .thumb-yt-placeholder mat-icon{font-size:28px;width:28px;height:28px;line-height:28px}.media-thumb .thumb-uploading{display:flex;align-items:center;justify-content:center;width:100%;height:100%}.media-thumb .thumb-uploading .thumb-spinner{width:20px;height:20px;border:2px solid #E2E8F0;border-top-color:#3b82f6;border-radius:50%;animation:cc-spin .7s linear infinite}.media-library-overlay{position:fixed;inset:0;background:#00000080;z-index:999;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:mu-fade-in .2s ease}.media-library-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:1000;width:min(90vw,780px);max-height:85vh;background:var(--mu-modal-bg, #ffffff);border-radius:var(--mu-modal-radius, 16px);box-shadow:var(--mu-modal-shadow, 0 20px 60px rgba(0, 0, 0, .2));display:flex;flex-direction:column;overflow:hidden;animation:mu-modal-in .22s cubic-bezier(.34,1.56,.64,1)}@keyframes mu-modal-in{0%{opacity:0;transform:translate(-50%,-48%) scale(.95)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}.library-modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--cc-sf-input-disabled-border, #F3F4F6);flex-shrink:0}.library-modal-title{display:flex;align-items:center;gap:8px;margin:0;font-size:1.05rem;font-weight:700;color:var(--cc-sf-label-color, #111827)}.library-modal-title mat-icon{font-size:22px;width:22px;height:22px;line-height:22px;color:var(--mu-icon-library-color, #22C55E)}.library-close-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;background:none;cursor:pointer;border-radius:50%;color:var(--cc-sf-hint-color, #9CA3AF);transition:background .15s ease,color .15s ease}.library-close-btn mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.library-close-btn:hover{background:var(--cc-sf-input-disabled-bg, #F3F4F6);color:var(--cc-sf-label-color, #374151)}.library-loading,.library-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:48px 24px;color:var(--cc-sf-hint-color, #9CA3AF);font-size:.875rem;flex:1}.library-loading mat-icon,.library-empty mat-icon{font-size:40px;width:40px;height:40px;line-height:40px;opacity:.5}.lib-spinner{width:36px;height:36px;border:3px solid #E2E8F0;border-top-color:#3b82f6;border-radius:50%;animation:cc-spin .7s linear infinite}.library-error{display:flex;align-items:center;gap:8px;padding:16px 24px;background:var(--cc-sf-error-bg, #FEF2F2);color:var(--cc-sf-error-text-color, #DC2626);font-size:.875rem;flex-shrink:0}.library-error mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.library-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;padding:24px;overflow-y:auto;flex:1}.library-grid::-webkit-scrollbar{width:6px}.library-grid::-webkit-scrollbar-thumb{background:var(--cc-sf-input-disabled-border, #D1D5DB);border-radius:3px}.library-grid-item{position:relative;aspect-ratio:4/3;border-radius:10px;overflow:hidden;cursor:pointer;border:2.5px solid transparent;transition:border-color .2s ease,transform .15s ease}.library-grid-item.selected{border-color:var(--cc-sf-chip-selected-bg, #3B82F6);transform:scale(.97)}.library-grid-item:hover .library-overlay-hover{opacity:1}.library-grid-img{width:100%;height:100%;object-fit:cover;display:block}.library-overlay-hover{position:absolute;inset:0;background:#3b82f61f;opacity:0;transition:opacity .15s ease}.library-check{position:absolute;top:6px;right:6px;color:var(--cc-sf-chip-selected-bg, #3B82F6);background:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;width:22px;height:22px;box-shadow:0 1px 4px #00000026}.library-check mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.library-modal-footer{display:flex;align-items:center;justify-content:space-between;padding:16px 24px 20px;border-top:1px solid var(--cc-sf-input-disabled-border, #F3F4F6);flex-shrink:0}.library-selected-count{font-size:.875rem;font-weight:600;color:var(--cc-sf-hint-color, #6B7280)}.library-footer-actions{display:flex;gap:10px}\n"] }]
|
|
4664
5077
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: ExpressionService }, { type: i3.HttpClient }], propDecorators: { config: [{
|
|
4665
5078
|
type: Input
|
|
4666
5079
|
}], controller: [{
|
|
@@ -4669,6 +5082,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
4669
5082
|
type: Input
|
|
4670
5083
|
}], allowMulti: [{
|
|
4671
5084
|
type: Input
|
|
5085
|
+
}], mediaDeviceInput: [{
|
|
5086
|
+
type: ViewChild,
|
|
5087
|
+
args: ['mediaDeviceInput']
|
|
4672
5088
|
}] } });
|
|
4673
5089
|
|
|
4674
5090
|
class FormSectionComponent {
|
|
@@ -4955,6 +5371,34 @@ class SmartFormComponent {
|
|
|
4955
5371
|
if (field.type === 'ROW' && field.children) {
|
|
4956
5372
|
Object.assign(payload, this.buildNestedPayload(rawValue, field.children));
|
|
4957
5373
|
}
|
|
5374
|
+
else if (field.type === 'GROUP' && field.sectionConfig?.children) {
|
|
5375
|
+
// FormFieldComponent generates unnamed groups using their label inside the root FormGroup.
|
|
5376
|
+
const generatedKey = field.sectionConfig.label
|
|
5377
|
+
? field.sectionConfig.label.replace(/(?:^\w|[A-Z]|\b\w)/g, (w, i) => i === 0 ? w.toLowerCase() : w.toUpperCase()).replace(/\s+/g, '')
|
|
5378
|
+
: '';
|
|
5379
|
+
const groupKey = field.sectionConfig.name || field.name || generatedKey || '__group__';
|
|
5380
|
+
const groupRawValue = rawValue[groupKey];
|
|
5381
|
+
if (groupRawValue !== undefined) {
|
|
5382
|
+
// Identify if it's purely a visual section vs an explicit structural data block
|
|
5383
|
+
const isStructural = field.sectionConfig.name || field.name || field.sectionConfig.allowMulti;
|
|
5384
|
+
if (!isStructural) {
|
|
5385
|
+
// Visual section: Flatten its contents directly onto the target payload layer
|
|
5386
|
+
Object.assign(payload, this.buildNestedPayload(groupRawValue, field.sectionConfig.children));
|
|
5387
|
+
}
|
|
5388
|
+
else {
|
|
5389
|
+
// Structural block: process nested mappings / array instances
|
|
5390
|
+
const nestedData = (field.sectionConfig.allowMulti && Array.isArray(groupRawValue))
|
|
5391
|
+
? groupRawValue.map(item => this.buildNestedPayload(item, field.sectionConfig.children))
|
|
5392
|
+
: this.buildNestedPayload(groupRawValue, field.sectionConfig.children);
|
|
5393
|
+
if (field.payloadPath) {
|
|
5394
|
+
this.setNestedValue(payload, field.payloadPath, nestedData);
|
|
5395
|
+
}
|
|
5396
|
+
else {
|
|
5397
|
+
payload[groupKey] = nestedData;
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
4958
5402
|
else if (field.name && rawValue[field.name] !== undefined) {
|
|
4959
5403
|
const val = rawValue[field.name];
|
|
4960
5404
|
if (field.payloadPath) {
|
|
@@ -5138,7 +5582,8 @@ class SmartFormModule {
|
|
|
5138
5582
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SmartFormModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
5139
5583
|
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.15", ngImport: i0, type: SmartFormModule, declarations: [SmartFormComponent,
|
|
5140
5584
|
FormSectionComponent,
|
|
5141
|
-
FormFieldComponent
|
|
5585
|
+
FormFieldComponent,
|
|
5586
|
+
TrustedUrlPipe], imports: [CommonModule,
|
|
5142
5587
|
ReactiveFormsModule,
|
|
5143
5588
|
FormsModule,
|
|
5144
5589
|
MaterialModule,
|
|
@@ -5160,7 +5605,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
5160
5605
|
declarations: [
|
|
5161
5606
|
SmartFormComponent,
|
|
5162
5607
|
FormSectionComponent,
|
|
5163
|
-
FormFieldComponent
|
|
5608
|
+
FormFieldComponent,
|
|
5609
|
+
TrustedUrlPipe
|
|
5164
5610
|
],
|
|
5165
5611
|
imports: [
|
|
5166
5612
|
CommonModule,
|