commons-shared-web-ui 0.0.15 → 0.0.17
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.
|
@@ -3687,6 +3687,9 @@ class SmartFormTranslationUtils {
|
|
|
3687
3687
|
field.placeholder = translate(field.placeholder);
|
|
3688
3688
|
if (field.hint)
|
|
3689
3689
|
field.hint = translate(field.hint);
|
|
3690
|
+
if (field.richTextConfig?.placeholder) {
|
|
3691
|
+
field.richTextConfig.placeholder = translate(field.richTextConfig.placeholder);
|
|
3692
|
+
}
|
|
3690
3693
|
if (field.textConfig?.patternMessage) {
|
|
3691
3694
|
field.textConfig.patternMessage = translate(field.textConfig.patternMessage);
|
|
3692
3695
|
}
|
|
@@ -3926,6 +3929,23 @@ class FormFieldComponent {
|
|
|
3926
3929
|
libraryLoading = false;
|
|
3927
3930
|
libraryError = '';
|
|
3928
3931
|
mediaUploadError = ''; // transient error message for max limits
|
|
3932
|
+
// ── LOCATION field support ────────────────────────────────────────────────
|
|
3933
|
+
/** Active tab: VENUE | ONLINE | TBA */
|
|
3934
|
+
locationActiveTab = 'VENUE';
|
|
3935
|
+
/** Current text in the venue search box */
|
|
3936
|
+
locationSearchText = '';
|
|
3937
|
+
/** Google Places autocomplete suggestions */
|
|
3938
|
+
locationSuggestions = [];
|
|
3939
|
+
/** Show the suggestions dropdown */
|
|
3940
|
+
locationShowSuggestions = false;
|
|
3941
|
+
/** Cached Google AutocompleteService instance */
|
|
3942
|
+
_googleAcService = null;
|
|
3943
|
+
/** Whether Google Maps is loaded */
|
|
3944
|
+
locationMapLoaded = false;
|
|
3945
|
+
/** Map instance */
|
|
3946
|
+
_googleMap = null;
|
|
3947
|
+
/** Map markers */
|
|
3948
|
+
_mapMarkers = [];
|
|
3929
3949
|
// ── AUTOCOMPLETE support ────────────────────────────────────────────────
|
|
3930
3950
|
/** FormControl used ONLY for the autocomplete text-input display value */
|
|
3931
3951
|
autocompleteInputCtrl = new FormControl('');
|
|
@@ -3978,6 +3998,9 @@ class FormFieldComponent {
|
|
|
3978
3998
|
if (this.isAutocomplete) {
|
|
3979
3999
|
this.initAutocomplete();
|
|
3980
4000
|
}
|
|
4001
|
+
if (this.isLocation) {
|
|
4002
|
+
this.initLocationField();
|
|
4003
|
+
}
|
|
3981
4004
|
}
|
|
3982
4005
|
// ── GROUP initialisation ──────────────────────────────────────────────────
|
|
3983
4006
|
get addMultiLabel() {
|
|
@@ -4356,9 +4379,13 @@ class FormFieldComponent {
|
|
|
4356
4379
|
next: responses => {
|
|
4357
4380
|
let mergedData = [];
|
|
4358
4381
|
responses.forEach(response => {
|
|
4359
|
-
|
|
4382
|
+
let data = optionConfig?.dataPath
|
|
4360
4383
|
? this.getValueByPath(response, optionConfig.dataPath)
|
|
4361
|
-
: (Array.isArray(response) ? response : response.data || response.items || []);
|
|
4384
|
+
: (Array.isArray(response) ? response : response.data || response.items || response || []);
|
|
4385
|
+
// Handle Dictionary Objects by converting them to Arrays
|
|
4386
|
+
if (data !== null && typeof data === 'object' && !Array.isArray(data)) {
|
|
4387
|
+
data = Object.values(data);
|
|
4388
|
+
}
|
|
4362
4389
|
if (Array.isArray(data))
|
|
4363
4390
|
mergedData = [...mergedData, ...data];
|
|
4364
4391
|
});
|
|
@@ -4503,6 +4530,7 @@ class FormFieldComponent {
|
|
|
4503
4530
|
get isGenerated() { return this.config.type === 'GENERATED'; }
|
|
4504
4531
|
get isRow() { return this.config.type === 'ROW'; }
|
|
4505
4532
|
get isGroup() { return this.config.type === 'GROUP'; }
|
|
4533
|
+
get isLocation() { return this.config.type === 'LOCATION'; }
|
|
4506
4534
|
// ── AUTOCOMPLETE helpers ────────────────────────────────────────────────
|
|
4507
4535
|
/**
|
|
4508
4536
|
* Initialise the separate display-control that drives the mat-autocomplete
|
|
@@ -5068,12 +5096,279 @@ class FormFieldComponent {
|
|
|
5068
5096
|
this.mediaUploadError = '';
|
|
5069
5097
|
}, 4000);
|
|
5070
5098
|
}
|
|
5099
|
+
// ── LOCATION field methods ─────────────────────────────────────────────────
|
|
5100
|
+
initLocationField() {
|
|
5101
|
+
const cfg = this.config.locationConfig;
|
|
5102
|
+
this.locationActiveTab = cfg?.defaultTab ?? 'VENUE';
|
|
5103
|
+
// Restore from existing form value using the robust getter
|
|
5104
|
+
if (this.locationValue.tab) {
|
|
5105
|
+
this.locationActiveTab = this.locationValue.tab;
|
|
5106
|
+
}
|
|
5107
|
+
// Subscribe to external patchValue events (prefills)
|
|
5108
|
+
this.formGroup.get(this.config.name)?.valueChanges
|
|
5109
|
+
.pipe(takeUntil(this.destroy$))
|
|
5110
|
+
.subscribe(() => {
|
|
5111
|
+
if (this.locationValue.tab) {
|
|
5112
|
+
this.locationActiveTab = this.locationValue.tab;
|
|
5113
|
+
}
|
|
5114
|
+
setTimeout(() => this._renderMap());
|
|
5115
|
+
});
|
|
5116
|
+
// Load Google Maps script if not already loaded
|
|
5117
|
+
this._ensureGoogleMapsScript();
|
|
5118
|
+
}
|
|
5119
|
+
_ensureGoogleMapsScript() {
|
|
5120
|
+
const apiKey = this.config.locationConfig?.googleMapsApiKey;
|
|
5121
|
+
if (!apiKey)
|
|
5122
|
+
return;
|
|
5123
|
+
if (window.google?.maps?.places) {
|
|
5124
|
+
this._googleAcService = new window.google.maps.places.AutocompleteService();
|
|
5125
|
+
this.locationMapLoaded = true;
|
|
5126
|
+
setTimeout(() => this._renderMap(), 100);
|
|
5127
|
+
return;
|
|
5128
|
+
}
|
|
5129
|
+
// Only inject once
|
|
5130
|
+
if (document.querySelector('script[data-sf-gmaps]')) {
|
|
5131
|
+
// Wait for the script to load
|
|
5132
|
+
const wait = () => {
|
|
5133
|
+
if (window.google?.maps?.places) {
|
|
5134
|
+
this._googleAcService = new window.google.maps.places.AutocompleteService();
|
|
5135
|
+
this.locationMapLoaded = true;
|
|
5136
|
+
setTimeout(() => this._renderMap(), 100);
|
|
5137
|
+
}
|
|
5138
|
+
else {
|
|
5139
|
+
setTimeout(wait, 300);
|
|
5140
|
+
}
|
|
5141
|
+
};
|
|
5142
|
+
wait();
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
const script = document.createElement('script');
|
|
5146
|
+
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
|
|
5147
|
+
script.async = true;
|
|
5148
|
+
script.defer = true;
|
|
5149
|
+
script.setAttribute('data-sf-gmaps', 'true');
|
|
5150
|
+
script.onload = () => {
|
|
5151
|
+
this._googleAcService = new window.google.maps.places.AutocompleteService();
|
|
5152
|
+
this.locationMapLoaded = true;
|
|
5153
|
+
setTimeout(() => this._renderMap(), 100);
|
|
5154
|
+
};
|
|
5155
|
+
document.head.appendChild(script);
|
|
5156
|
+
}
|
|
5157
|
+
onLocationTabChange(tab) {
|
|
5158
|
+
if (this.locationActiveTab === 'VENUE' && tab !== 'VENUE') {
|
|
5159
|
+
this._googleMap = null;
|
|
5160
|
+
this._mapMarkers = [];
|
|
5161
|
+
}
|
|
5162
|
+
this.locationActiveTab = tab;
|
|
5163
|
+
this.locationSuggestions = [];
|
|
5164
|
+
this.locationShowSuggestions = false;
|
|
5165
|
+
this.locationSearchText = '';
|
|
5166
|
+
const existing = this.formGroup.get(this.config.name)?.value || { tab, venues: [], onlineUrl: '' };
|
|
5167
|
+
existing.tab = tab;
|
|
5168
|
+
this.updateValue(existing);
|
|
5169
|
+
if (tab === 'VENUE') {
|
|
5170
|
+
setTimeout(() => this._renderMap());
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
get locationValue() {
|
|
5174
|
+
let v = this.formGroup.get(this.config.name)?.value;
|
|
5175
|
+
if (typeof v === 'string') {
|
|
5176
|
+
try {
|
|
5177
|
+
v = JSON.parse(v);
|
|
5178
|
+
}
|
|
5179
|
+
catch { /* ignore */ }
|
|
5180
|
+
}
|
|
5181
|
+
// Automatically unwrap if prefill payload was mistakenly double-nested like `{ location: { tab... } }`
|
|
5182
|
+
if (v && !v.tab && v[this.config.name]) {
|
|
5183
|
+
v = v[this.config.name];
|
|
5184
|
+
}
|
|
5185
|
+
if (v && v.tab)
|
|
5186
|
+
return v;
|
|
5187
|
+
return { tab: this.locationActiveTab, venues: [], onlineUrl: '' };
|
|
5188
|
+
}
|
|
5189
|
+
get locationVenues() {
|
|
5190
|
+
return this.locationValue.venues || [];
|
|
5191
|
+
}
|
|
5192
|
+
get locationOnlineUrl() {
|
|
5193
|
+
return this.locationValue.onlineUrl || '';
|
|
5194
|
+
}
|
|
5195
|
+
get locationMaxReached() {
|
|
5196
|
+
const isMulti = this.config.locationConfig?.allowMulti;
|
|
5197
|
+
const max = isMulti ? (this.config.locationConfig?.maxLocations ?? 5) : 1;
|
|
5198
|
+
return this.locationVenues.length >= max;
|
|
5199
|
+
}
|
|
5200
|
+
handleLocationSearchInput(event) {
|
|
5201
|
+
const input = event.target.value;
|
|
5202
|
+
this.locationSearchText = input;
|
|
5203
|
+
if (!input || input.length < 2) {
|
|
5204
|
+
this.locationSuggestions = [];
|
|
5205
|
+
this.locationShowSuggestions = false;
|
|
5206
|
+
return;
|
|
5207
|
+
}
|
|
5208
|
+
if (!this._googleAcService) {
|
|
5209
|
+
return;
|
|
5210
|
+
}
|
|
5211
|
+
this._googleAcService.getPlacePredictions({ input }, (predictions, status) => {
|
|
5212
|
+
if (status === 'OK' && predictions) {
|
|
5213
|
+
this.locationSuggestions = predictions;
|
|
5214
|
+
this.locationShowSuggestions = true;
|
|
5215
|
+
}
|
|
5216
|
+
else {
|
|
5217
|
+
this.locationSuggestions = [];
|
|
5218
|
+
this.locationShowSuggestions = false;
|
|
5219
|
+
}
|
|
5220
|
+
});
|
|
5221
|
+
}
|
|
5222
|
+
onLocationSuggestionSelect(prediction) {
|
|
5223
|
+
if (!prediction)
|
|
5224
|
+
return;
|
|
5225
|
+
const isMulti = this.config.locationConfig?.allowMulti;
|
|
5226
|
+
const max = isMulti ? (this.config.locationConfig?.maxLocations ?? 5) : 1;
|
|
5227
|
+
const current = this.locationVenues;
|
|
5228
|
+
if (current.length >= max)
|
|
5229
|
+
return;
|
|
5230
|
+
const newItem = {
|
|
5231
|
+
description: prediction.description,
|
|
5232
|
+
placeId: prediction.place_id,
|
|
5233
|
+
address: prediction.description,
|
|
5234
|
+
type: 'GOOGLE',
|
|
5235
|
+
isActive: true
|
|
5236
|
+
};
|
|
5237
|
+
// Geocode to get lat/lng and components
|
|
5238
|
+
if (window.google?.maps?.Geocoder) {
|
|
5239
|
+
const geocoder = new window.google.maps.Geocoder();
|
|
5240
|
+
geocoder.geocode({ placeId: prediction.place_id }, (results, status) => {
|
|
5241
|
+
if (status === 'OK' && results[0]) {
|
|
5242
|
+
const res = results[0];
|
|
5243
|
+
newItem.latitude = res.geometry?.location?.lat();
|
|
5244
|
+
newItem.longitude = res.geometry?.location?.lng();
|
|
5245
|
+
newItem.address = res.formatted_address || prediction.description;
|
|
5246
|
+
let name = prediction.description;
|
|
5247
|
+
if (prediction.structured_formatting && prediction.structured_formatting.main_text) {
|
|
5248
|
+
name = prediction.structured_formatting.main_text;
|
|
5249
|
+
}
|
|
5250
|
+
newItem.name = name;
|
|
5251
|
+
res.address_components?.forEach((c) => {
|
|
5252
|
+
if (c.types.includes('locality'))
|
|
5253
|
+
newItem.cityLabel = c.long_name;
|
|
5254
|
+
if (c.types.includes('administrative_area_level_1'))
|
|
5255
|
+
newItem.stateLabel = c.long_name;
|
|
5256
|
+
if (c.types.includes('country'))
|
|
5257
|
+
newItem.countryLabel = c.long_name;
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
this._addVenueAndUpdate(newItem);
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
5263
|
+
else {
|
|
5264
|
+
this._addVenueAndUpdate(newItem);
|
|
5265
|
+
}
|
|
5266
|
+
this.locationSearchText = '';
|
|
5267
|
+
this.locationSuggestions = [];
|
|
5268
|
+
this.locationShowSuggestions = false;
|
|
5269
|
+
}
|
|
5270
|
+
_addVenueAndUpdate(item) {
|
|
5271
|
+
const val = { ...this.locationValue };
|
|
5272
|
+
val.venues = [...(val.venues || []), item];
|
|
5273
|
+
this.updateValue(val);
|
|
5274
|
+
setTimeout(() => this._renderMap());
|
|
5275
|
+
}
|
|
5276
|
+
removeLocationVenue(index) {
|
|
5277
|
+
const val = { ...this.locationValue };
|
|
5278
|
+
val.venues = (val.venues || []).filter((_, i) => i !== index);
|
|
5279
|
+
this.updateValue(val);
|
|
5280
|
+
setTimeout(() => this._renderMap());
|
|
5281
|
+
}
|
|
5282
|
+
onLocationUrlChange(url) {
|
|
5283
|
+
const val = { ...this.locationValue, onlineUrl: url };
|
|
5284
|
+
this.updateValue(val);
|
|
5285
|
+
}
|
|
5286
|
+
hideLocationSuggestions() {
|
|
5287
|
+
setTimeout(() => { this.locationShowSuggestions = false; }, 200);
|
|
5288
|
+
}
|
|
5289
|
+
getLocationMapEmbedUrl() {
|
|
5290
|
+
const venues = this.locationVenues.filter(v => v.placeId || (typeof v.latitude === 'number' && typeof v.longitude === 'number'));
|
|
5291
|
+
const apiKey = this.config.locationConfig?.googleMapsApiKey || '';
|
|
5292
|
+
// No locations currently selected: Show the default map view (India)
|
|
5293
|
+
if (!venues.length) {
|
|
5294
|
+
if (apiKey) {
|
|
5295
|
+
// Embed API center fallback if they have an API Key but no marker yet
|
|
5296
|
+
const lat = this.config.locationConfig?.defaultLat ?? 20.5937;
|
|
5297
|
+
const lng = this.config.locationConfig?.defaultLng ?? 78.9629;
|
|
5298
|
+
const zoom = this.config.locationConfig?.defaultZoom ?? 4;
|
|
5299
|
+
return `https://www.google.com/maps/embed/v1/view?key=${apiKey}¢er=${lat},${lng}&zoom=${zoom}`;
|
|
5300
|
+
}
|
|
5301
|
+
else {
|
|
5302
|
+
// Static iframe fallback text query
|
|
5303
|
+
return `https://maps.google.com/maps?q=India&output=embed`;
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
if (venues.length === 1) {
|
|
5307
|
+
const v = venues[0];
|
|
5308
|
+
const q = v.placeId ? `place_id:${v.placeId}` : encodeURIComponent(v.address || v.description || '');
|
|
5309
|
+
return `https://www.google.com/maps/embed/v1/place?key=${apiKey}&q=${q}`;
|
|
5310
|
+
}
|
|
5311
|
+
// Multiple locations — use directions/search embed
|
|
5312
|
+
const q = encodeURIComponent(venues.map(v => v.address || v.description).join('|'));
|
|
5313
|
+
return `https://www.google.com/maps/embed/v1/search?key=${apiKey}&q=${q}`;
|
|
5314
|
+
}
|
|
5315
|
+
_renderMap() {
|
|
5316
|
+
if (!window.google?.maps)
|
|
5317
|
+
return;
|
|
5318
|
+
if (!this.config.locationConfig?.showMap && this.config.locationConfig?.showMap !== undefined)
|
|
5319
|
+
return;
|
|
5320
|
+
const venues = this.locationVenues.filter(v => typeof v.latitude === 'number' && typeof v.longitude === 'number');
|
|
5321
|
+
const mapEl = document.getElementById(`loc-map-${this.config.name}`);
|
|
5322
|
+
if (!mapEl)
|
|
5323
|
+
return;
|
|
5324
|
+
if (!this._googleMap) {
|
|
5325
|
+
// Default to India if no venues exist
|
|
5326
|
+
const defaultLat = this.config.locationConfig?.defaultLat ?? 20.5937;
|
|
5327
|
+
const defaultLng = this.config.locationConfig?.defaultLng ?? 78.9629;
|
|
5328
|
+
const defaultZoom = this.config.locationConfig?.defaultZoom ?? 4;
|
|
5329
|
+
this._googleMap = new window.google.maps.Map(mapEl, {
|
|
5330
|
+
zoom: venues.length === 1 ? 12 : defaultZoom,
|
|
5331
|
+
center: venues.length > 0
|
|
5332
|
+
? { lat: venues[0].latitude, lng: venues[0].longitude }
|
|
5333
|
+
: { lat: defaultLat, lng: defaultLng }
|
|
5334
|
+
});
|
|
5335
|
+
}
|
|
5336
|
+
else {
|
|
5337
|
+
if (venues.length === 0) {
|
|
5338
|
+
// Re-center on default
|
|
5339
|
+
const lat = this.config.locationConfig?.defaultLat ?? 20.5937;
|
|
5340
|
+
const lng = this.config.locationConfig?.defaultLng ?? 78.9629;
|
|
5341
|
+
this._googleMap.setCenter({ lat, lng });
|
|
5342
|
+
this._googleMap.setZoom(this.config.locationConfig?.defaultZoom ?? 4);
|
|
5343
|
+
}
|
|
5344
|
+
else if (venues.length === 1) {
|
|
5345
|
+
this._googleMap.setCenter({ lat: venues[0].latitude, lng: venues[0].longitude });
|
|
5346
|
+
this._googleMap.setZoom(12);
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
// Clear old markers
|
|
5350
|
+
this._mapMarkers.forEach(m => m.setMap(null));
|
|
5351
|
+
this._mapMarkers = [];
|
|
5352
|
+
venues.forEach(v => {
|
|
5353
|
+
const marker = new window.google.maps.Marker({
|
|
5354
|
+
position: { lat: v.latitude, lng: v.longitude },
|
|
5355
|
+
map: this._googleMap,
|
|
5356
|
+
title: v.name || v.address || v.description
|
|
5357
|
+
});
|
|
5358
|
+
this._mapMarkers.push(marker);
|
|
5359
|
+
});
|
|
5360
|
+
if (venues.length > 1) {
|
|
5361
|
+
const bounds = new window.google.maps.LatLngBounds();
|
|
5362
|
+
venues.forEach(v => bounds.extend({ lat: v.latitude, lng: v.longitude }));
|
|
5363
|
+
this._googleMap.fitBounds(bounds);
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5071
5366
|
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 });
|
|
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" }] });
|
|
5367
|
+
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 <!-- \u2550\u2550 Location Field \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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=\"isLocation\" class=\"field-wrapper location-field-wrapper\" [formGroup]=\"formGroup\">\r\n\r\n <!-- Field label -->\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 <p class=\"location-subtitle\" *ngIf=\"config.hint\">{{ config.hint }}</p>\r\n\r\n <!-- Three-tab bar -->\r\n <div class=\"location-tabs\">\r\n <lib-button class=\"loc-tab-btn\" [variant]=\"locationActiveTab === 'VENUE' ? 'warning' : 'outline'\"\r\n (click)=\"onLocationTabChange('VENUE')\">\r\n {{ controller.labels['LBL_LOC_VENUE'] || 'Venue' }}\r\n </lib-button>\r\n <lib-button class=\"loc-tab-btn\" [variant]=\"locationActiveTab === 'ONLINE' ? 'warning' : 'outline'\"\r\n (click)=\"onLocationTabChange('ONLINE')\">\r\n {{ controller.labels['LBL_LOC_ONLINE'] || 'Online Event' }}\r\n </lib-button>\r\n <lib-button class=\"loc-tab-btn\" [variant]=\"locationActiveTab === 'TBA' ? 'warning' : 'outline'\"\r\n (click)=\"onLocationTabChange('TBA')\">\r\n {{ controller.labels['LBL_LOC_TBA'] || 'To be Announced' }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- VENUE TAB -->\r\n <div *ngIf=\"locationActiveTab === 'VENUE'\" class=\"loc-panel loc-venue-panel\">\r\n\r\n <p class=\"loc-section-label\">\r\n {{ controller.labels['LBL_LOC_ADDRESS'] || 'Location address' }}\r\n </p>\r\n\r\n <!-- Added venue rows -->\r\n <div class=\"loc-venue-list\" *ngIf=\"locationVenues.length > 0\">\r\n <div *ngFor=\"let venue of locationVenues; let i = index\" class=\"loc-venue-item\">\r\n <mat-icon class=\"loc-venue-search-icon\">search</mat-icon>\r\n <span class=\"loc-venue-text\">{{ venue.address || venue.name || venue.description }}</span>\r\n <button type=\"button\" class=\"loc-action-btn loc-delete-btn\" (click)=\"removeLocationVenue(i)\">\r\n <mat-icon>delete_outline</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Location count badge -->\r\n <p class=\"loc-count-text\" *ngIf=\"locationVenues.length > 0 && config.locationConfig?.allowMulti\">\r\n {{ locationVenues.length }} {{ controller.labels['LBL_LOC_COUNT_SUFFIX'] || 'Locations Added!' }}\r\n </p>\r\n\r\n <!-- Search input (hide when max reached) -->\r\n <div class=\"loc-search-container\" *ngIf=\"!locationMaxReached\">\r\n <div class=\"loc-search-wrapper\">\r\n <mat-icon class=\"loc-search-icon\">search</mat-icon>\r\n <input class=\"field-input loc-search-input\"\r\n [placeholder]=\"config.locationConfig?.venuePlaceholder || (controller.labels['PH_LOC_VENUE'] || 'Type to search venue...')\"\r\n [value]=\"locationSearchText\" (input)=\"handleLocationSearchInput($event)\" (blur)=\"hideLocationSuggestions()\"\r\n autocomplete=\"off\" [class.is-invalid]=\"errorMessage\">\r\n </div>\r\n <!-- Suggestions dropdown -->\r\n <div class=\"loc-suggestions-panel\" *ngIf=\"locationShowSuggestions && locationSuggestions.length\">\r\n <div *ngFor=\"let sug of locationSuggestions\" class=\"loc-suggestion-item\"\r\n (mousedown)=\"onLocationSuggestionSelect(sug)\">\r\n <mat-icon class=\"loc-suggestion-icon\">place</mat-icon>\r\n <span class=\"loc-suggestion-text\">{{ sug.description }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Add another button -->\r\n <button type=\"button\" class=\"loc-add-btn\"\r\n *ngIf=\"locationVenues.length > 0 && !locationMaxReached && config.locationConfig?.allowMulti\">\r\n <mat-icon>add_circle_outline</mat-icon>\r\n <span>{{ controller.labels['LBL_LOC_ADD_ANOTHER'] || 'Add another Location' }}</span>\r\n </button>\r\n\r\n <!-- Map -->\r\n <div class=\"loc-map-container\" *ngIf=\"config.locationConfig?.showMap !== false\">\r\n <ng-container *ngIf=\"config.locationConfig?.googleMapsApiKey; else simpleEmbed\">\r\n <div [id]=\"'loc-map-' + config.name\" class=\"loc-map-frame\"\r\n [style.height]=\"config.locationConfig?.mapHeight || '300px'\"></div>\r\n </ng-container>\r\n <ng-template #simpleEmbed>\r\n <iframe class=\"loc-map-frame\" [style.height]=\"config.locationConfig?.mapHeight || '300px'\"\r\n [src]=\"getLocationMapEmbedUrl() | trustedUrl\" frameborder=\"0\" allowfullscreen loading=\"lazy\">\r\n </iframe>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Map hint -->\r\n <p class=\"loc-map-hint\">\r\n {{ controller.labels['LBL_LOC_MAP_HINT'] || 'Type the venue address. A map will appear to assist you.' }}\r\n </p>\r\n </div>\r\n\r\n <!-- ONLINE TAB -->\r\n <div *ngIf=\"locationActiveTab === 'ONLINE'\" class=\"loc-panel loc-online-panel\">\r\n <p class=\"loc-section-label\">\r\n {{ controller.labels['LBL_LOC_ONLINE_URL'] || 'Event URL' }}\r\n </p>\r\n <div class=\"loc-search-wrapper\">\r\n <mat-icon class=\"loc-search-icon\">link</mat-icon>\r\n <input class=\"field-input loc-search-input\" type=\"url\"\r\n [placeholder]=\"config.locationConfig?.onlinePlaceholder || (controller.labels['PH_LOC_ONLINE'] || 'https://zoom.us/j/...')\"\r\n [value]=\"locationOnlineUrl\" (input)=\"onLocationUrlChange($any($event.target).value)\"\r\n [class.is-invalid]=\"errorMessage\">\r\n </div>\r\n </div>\r\n\r\n <!-- TBA TAB -->\r\n <div *ngIf=\"locationActiveTab === 'TBA'\" class=\"loc-panel loc-tba-panel\">\r\n <div class=\"loc-tba-content\">\r\n <mat-icon class=\"loc-tba-icon\">schedule</mat-icon>\r\n <p class=\"loc-tba-text\">\r\n {{ controller.labels['LBL_LOC_TBA_DESC'] || \"This event's location is yet to be announced. Check back later\r\n for updates.\" }}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <!-- Hidden real form control -->\r\n <input type=\"hidden\" [formControlName]=\"config.name!\">\r\n\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</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;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}.location-field-wrapper{gap:12px}.location-subtitle{margin:0;font-size:var(--cc-sf-hint-size, .8125rem);color:var(--cc-sf-hint-color, #6B7280);line-height:1.5}.location-tabs{display:flex;gap:12px;margin-bottom:24px}.loc-tab-btn{flex:1}.loc-tab-btn ::ng-deep button{width:100%}.loc-tab-btn ::ng-deep button:not(.cc-btn-warning){background-color:#fff!important;color:#000!important;border:1px solid #E5E7EB}.loc-tab-btn ::ng-deep button:not(.cc-btn-warning):hover{background-color:#f3f4f6!important}.loc-panel{display:flex;flex-direction:column;gap:12px}.loc-section-label{margin:0;font-size:var(--cc-sf-label-size, .9rem);font-weight:600;color:var(--cc-sf-label-color, #111827)}.loc-venue-list{display:flex;flex-direction:column;gap:8px}.loc-venue-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--loc-venue-item-bg, #ffffff);border:1px solid var(--loc-venue-item-border, #D1D5DB);border-radius:var(--cc-sf-input-radius, 7px);transition:box-shadow .15s ease,border-color .15s ease}.loc-venue-item:hover{box-shadow:0 2px 8px #0000000f;border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.loc-venue-search-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--cc-sf-hint-color, #9CA3AF);flex-shrink:0}.loc-venue-text{flex:1;font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-input-color, #111827);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.loc-action-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:4px;border-radius:50%;transition:background .15s ease,color .15s ease;flex-shrink:0}.loc-action-btn mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.loc-action-btn.loc-delete-btn{color:var(--loc-delete-color, #E53E3E)}.loc-action-btn.loc-delete-btn:hover{background:var(--cc-sf-error-bg, #FEF2F2)}.loc-action-btn.loc-edit-btn{color:var(--cc-sf-hint-color, #9CA3AF)}.loc-action-btn.loc-edit-btn:hover{color:var(--cc-sf-input-focus-border, #3B82F6);background:var(--cc-sf-dropzone-hover-bg, #EFF6FF)}.loc-count-text{margin:0;font-size:.8125rem;font-weight:600;color:var(--cc-sf-input-focus-border, #3B82F6)}.loc-search-container{position:relative}.loc-search-wrapper{position:relative;display:flex;align-items:center}.loc-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}.loc-search-input{flex:1;padding-left:2.4rem!important}.loc-suggestions-panel{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:300;background:var(--loc-suggestion-bg, #ffffff);border:1px solid var(--cc-sf-input-border, #D1D5DB);border-radius:var(--cc-sf-input-radius, 8px);box-shadow:0 8px 24px #0000001a;overflow:hidden;animation:mu-fade-in .15s ease;max-height:260px;overflow-y:auto}.loc-suggestion-item{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;transition:background .12s ease;font-family:var(--cc-sf-font-family, inherit)}.loc-suggestion-item:hover,.loc-suggestion-item:focus{background:var(--loc-suggestion-hover-bg, #F0F9FF)}.loc-suggestion-item:not(:last-child){border-bottom:1px solid var(--cc-sf-input-disabled-border, #F3F4F6)}.loc-suggestion-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--loc-delete-color, #E53E3E);flex-shrink:0}.loc-suggestion-text{font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-label-color, #374151);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.loc-add-btn{display:inline-flex;align-items:center;gap:6px;background:none;border:none;cursor:pointer;color:var(--loc-add-color, #1A56DB);font-family:var(--cc-sf-font-family, inherit);font-size:var(--cc-sf-input-font-size, .875rem);font-weight:600;padding:0;transition:opacity .15s ease}.loc-add-btn mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.loc-add-btn:hover{opacity:.8}.loc-map-container{border-radius:var(--loc-map-radius, 10px);overflow:hidden;border:1px solid var(--cc-sf-input-disabled-border, #E5E7EB);box-shadow:0 2px 10px #0000000f}.loc-map-frame{width:100%;display:block;border:none}.loc-map-hint{margin:0;font-size:.78rem;color:var(--cc-sf-hint-color, #6B7280);text-align:center}.loc-tba-panel{min-height:120px;justify-content:center}.loc-tba-content{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;padding:32px 24px;background:var(--loc-tba-bg, #F9FAFB);border:1px dashed var(--cc-sf-input-disabled-border, #D1D5DB);border-radius:var(--cc-sf-input-radius, 10px)}.loc-tba-icon{font-size:40px;width:40px;height:40px;line-height:40px;color:var(--loc-tba-icon-color, #9CA3AF);opacity:.6}.loc-tba-text{margin:0;font-size:var(--cc-sf-input-font-size, .9rem);color:var(--cc-sf-hint-color, #6B7280);line-height:1.6;max-width:360px}.loc-online-panel .loc-search-wrapper{margin-top:4px}\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" }] });
|
|
5073
5368
|
}
|
|
5074
5369
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: FormFieldComponent, decorators: [{
|
|
5075
5370
|
type: Component,
|
|
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"] }]
|
|
5371
|
+
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 <!-- \u2550\u2550 Location Field \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\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=\"isLocation\" class=\"field-wrapper location-field-wrapper\" [formGroup]=\"formGroup\">\r\n\r\n <!-- Field label -->\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 <p class=\"location-subtitle\" *ngIf=\"config.hint\">{{ config.hint }}</p>\r\n\r\n <!-- Three-tab bar -->\r\n <div class=\"location-tabs\">\r\n <lib-button class=\"loc-tab-btn\" [variant]=\"locationActiveTab === 'VENUE' ? 'warning' : 'outline'\"\r\n (click)=\"onLocationTabChange('VENUE')\">\r\n {{ controller.labels['LBL_LOC_VENUE'] || 'Venue' }}\r\n </lib-button>\r\n <lib-button class=\"loc-tab-btn\" [variant]=\"locationActiveTab === 'ONLINE' ? 'warning' : 'outline'\"\r\n (click)=\"onLocationTabChange('ONLINE')\">\r\n {{ controller.labels['LBL_LOC_ONLINE'] || 'Online Event' }}\r\n </lib-button>\r\n <lib-button class=\"loc-tab-btn\" [variant]=\"locationActiveTab === 'TBA' ? 'warning' : 'outline'\"\r\n (click)=\"onLocationTabChange('TBA')\">\r\n {{ controller.labels['LBL_LOC_TBA'] || 'To be Announced' }}\r\n </lib-button>\r\n </div>\r\n\r\n <!-- VENUE TAB -->\r\n <div *ngIf=\"locationActiveTab === 'VENUE'\" class=\"loc-panel loc-venue-panel\">\r\n\r\n <p class=\"loc-section-label\">\r\n {{ controller.labels['LBL_LOC_ADDRESS'] || 'Location address' }}\r\n </p>\r\n\r\n <!-- Added venue rows -->\r\n <div class=\"loc-venue-list\" *ngIf=\"locationVenues.length > 0\">\r\n <div *ngFor=\"let venue of locationVenues; let i = index\" class=\"loc-venue-item\">\r\n <mat-icon class=\"loc-venue-search-icon\">search</mat-icon>\r\n <span class=\"loc-venue-text\">{{ venue.address || venue.name || venue.description }}</span>\r\n <button type=\"button\" class=\"loc-action-btn loc-delete-btn\" (click)=\"removeLocationVenue(i)\">\r\n <mat-icon>delete_outline</mat-icon>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <!-- Location count badge -->\r\n <p class=\"loc-count-text\" *ngIf=\"locationVenues.length > 0 && config.locationConfig?.allowMulti\">\r\n {{ locationVenues.length }} {{ controller.labels['LBL_LOC_COUNT_SUFFIX'] || 'Locations Added!' }}\r\n </p>\r\n\r\n <!-- Search input (hide when max reached) -->\r\n <div class=\"loc-search-container\" *ngIf=\"!locationMaxReached\">\r\n <div class=\"loc-search-wrapper\">\r\n <mat-icon class=\"loc-search-icon\">search</mat-icon>\r\n <input class=\"field-input loc-search-input\"\r\n [placeholder]=\"config.locationConfig?.venuePlaceholder || (controller.labels['PH_LOC_VENUE'] || 'Type to search venue...')\"\r\n [value]=\"locationSearchText\" (input)=\"handleLocationSearchInput($event)\" (blur)=\"hideLocationSuggestions()\"\r\n autocomplete=\"off\" [class.is-invalid]=\"errorMessage\">\r\n </div>\r\n <!-- Suggestions dropdown -->\r\n <div class=\"loc-suggestions-panel\" *ngIf=\"locationShowSuggestions && locationSuggestions.length\">\r\n <div *ngFor=\"let sug of locationSuggestions\" class=\"loc-suggestion-item\"\r\n (mousedown)=\"onLocationSuggestionSelect(sug)\">\r\n <mat-icon class=\"loc-suggestion-icon\">place</mat-icon>\r\n <span class=\"loc-suggestion-text\">{{ sug.description }}</span>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- Add another button -->\r\n <button type=\"button\" class=\"loc-add-btn\"\r\n *ngIf=\"locationVenues.length > 0 && !locationMaxReached && config.locationConfig?.allowMulti\">\r\n <mat-icon>add_circle_outline</mat-icon>\r\n <span>{{ controller.labels['LBL_LOC_ADD_ANOTHER'] || 'Add another Location' }}</span>\r\n </button>\r\n\r\n <!-- Map -->\r\n <div class=\"loc-map-container\" *ngIf=\"config.locationConfig?.showMap !== false\">\r\n <ng-container *ngIf=\"config.locationConfig?.googleMapsApiKey; else simpleEmbed\">\r\n <div [id]=\"'loc-map-' + config.name\" class=\"loc-map-frame\"\r\n [style.height]=\"config.locationConfig?.mapHeight || '300px'\"></div>\r\n </ng-container>\r\n <ng-template #simpleEmbed>\r\n <iframe class=\"loc-map-frame\" [style.height]=\"config.locationConfig?.mapHeight || '300px'\"\r\n [src]=\"getLocationMapEmbedUrl() | trustedUrl\" frameborder=\"0\" allowfullscreen loading=\"lazy\">\r\n </iframe>\r\n </ng-template>\r\n </div>\r\n\r\n <!-- Map hint -->\r\n <p class=\"loc-map-hint\">\r\n {{ controller.labels['LBL_LOC_MAP_HINT'] || 'Type the venue address. A map will appear to assist you.' }}\r\n </p>\r\n </div>\r\n\r\n <!-- ONLINE TAB -->\r\n <div *ngIf=\"locationActiveTab === 'ONLINE'\" class=\"loc-panel loc-online-panel\">\r\n <p class=\"loc-section-label\">\r\n {{ controller.labels['LBL_LOC_ONLINE_URL'] || 'Event URL' }}\r\n </p>\r\n <div class=\"loc-search-wrapper\">\r\n <mat-icon class=\"loc-search-icon\">link</mat-icon>\r\n <input class=\"field-input loc-search-input\" type=\"url\"\r\n [placeholder]=\"config.locationConfig?.onlinePlaceholder || (controller.labels['PH_LOC_ONLINE'] || 'https://zoom.us/j/...')\"\r\n [value]=\"locationOnlineUrl\" (input)=\"onLocationUrlChange($any($event.target).value)\"\r\n [class.is-invalid]=\"errorMessage\">\r\n </div>\r\n </div>\r\n\r\n <!-- TBA TAB -->\r\n <div *ngIf=\"locationActiveTab === 'TBA'\" class=\"loc-panel loc-tba-panel\">\r\n <div class=\"loc-tba-content\">\r\n <mat-icon class=\"loc-tba-icon\">schedule</mat-icon>\r\n <p class=\"loc-tba-text\">\r\n {{ controller.labels['LBL_LOC_TBA_DESC'] || \"This event's location is yet to be announced. Check back later\r\n for updates.\" }}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <!-- Hidden real form control -->\r\n <input type=\"hidden\" [formControlName]=\"config.name!\">\r\n\r\n <span class=\"field-error\" *ngIf=\"errorMessage\">{{ errorMessage }}</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;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}.location-field-wrapper{gap:12px}.location-subtitle{margin:0;font-size:var(--cc-sf-hint-size, .8125rem);color:var(--cc-sf-hint-color, #6B7280);line-height:1.5}.location-tabs{display:flex;gap:12px;margin-bottom:24px}.loc-tab-btn{flex:1}.loc-tab-btn ::ng-deep button{width:100%}.loc-tab-btn ::ng-deep button:not(.cc-btn-warning){background-color:#fff!important;color:#000!important;border:1px solid #E5E7EB}.loc-tab-btn ::ng-deep button:not(.cc-btn-warning):hover{background-color:#f3f4f6!important}.loc-panel{display:flex;flex-direction:column;gap:12px}.loc-section-label{margin:0;font-size:var(--cc-sf-label-size, .9rem);font-weight:600;color:var(--cc-sf-label-color, #111827)}.loc-venue-list{display:flex;flex-direction:column;gap:8px}.loc-venue-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--loc-venue-item-bg, #ffffff);border:1px solid var(--loc-venue-item-border, #D1D5DB);border-radius:var(--cc-sf-input-radius, 7px);transition:box-shadow .15s ease,border-color .15s ease}.loc-venue-item:hover{box-shadow:0 2px 8px #0000000f;border-color:var(--cc-sf-input-hover-border, #9CA3AF)}.loc-venue-search-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--cc-sf-hint-color, #9CA3AF);flex-shrink:0}.loc-venue-text{flex:1;font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-input-color, #111827);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.loc-action-btn{display:flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:4px;border-radius:50%;transition:background .15s ease,color .15s ease;flex-shrink:0}.loc-action-btn mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}.loc-action-btn.loc-delete-btn{color:var(--loc-delete-color, #E53E3E)}.loc-action-btn.loc-delete-btn:hover{background:var(--cc-sf-error-bg, #FEF2F2)}.loc-action-btn.loc-edit-btn{color:var(--cc-sf-hint-color, #9CA3AF)}.loc-action-btn.loc-edit-btn:hover{color:var(--cc-sf-input-focus-border, #3B82F6);background:var(--cc-sf-dropzone-hover-bg, #EFF6FF)}.loc-count-text{margin:0;font-size:.8125rem;font-weight:600;color:var(--cc-sf-input-focus-border, #3B82F6)}.loc-search-container{position:relative}.loc-search-wrapper{position:relative;display:flex;align-items:center}.loc-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}.loc-search-input{flex:1;padding-left:2.4rem!important}.loc-suggestions-panel{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:300;background:var(--loc-suggestion-bg, #ffffff);border:1px solid var(--cc-sf-input-border, #D1D5DB);border-radius:var(--cc-sf-input-radius, 8px);box-shadow:0 8px 24px #0000001a;overflow:hidden;animation:mu-fade-in .15s ease;max-height:260px;overflow-y:auto}.loc-suggestion-item{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;transition:background .12s ease;font-family:var(--cc-sf-font-family, inherit)}.loc-suggestion-item:hover,.loc-suggestion-item:focus{background:var(--loc-suggestion-hover-bg, #F0F9FF)}.loc-suggestion-item:not(:last-child){border-bottom:1px solid var(--cc-sf-input-disabled-border, #F3F4F6)}.loc-suggestion-icon{font-size:18px;width:18px;height:18px;line-height:18px;color:var(--loc-delete-color, #E53E3E);flex-shrink:0}.loc-suggestion-text{font-size:var(--cc-sf-input-font-size, .875rem);color:var(--cc-sf-label-color, #374151);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.loc-add-btn{display:inline-flex;align-items:center;gap:6px;background:none;border:none;cursor:pointer;color:var(--loc-add-color, #1A56DB);font-family:var(--cc-sf-font-family, inherit);font-size:var(--cc-sf-input-font-size, .875rem);font-weight:600;padding:0;transition:opacity .15s ease}.loc-add-btn mat-icon{font-size:20px;width:20px;height:20px;line-height:20px}.loc-add-btn:hover{opacity:.8}.loc-map-container{border-radius:var(--loc-map-radius, 10px);overflow:hidden;border:1px solid var(--cc-sf-input-disabled-border, #E5E7EB);box-shadow:0 2px 10px #0000000f}.loc-map-frame{width:100%;display:block;border:none}.loc-map-hint{margin:0;font-size:.78rem;color:var(--cc-sf-hint-color, #6B7280);text-align:center}.loc-tba-panel{min-height:120px;justify-content:center}.loc-tba-content{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;padding:32px 24px;background:var(--loc-tba-bg, #F9FAFB);border:1px dashed var(--cc-sf-input-disabled-border, #D1D5DB);border-radius:var(--cc-sf-input-radius, 10px)}.loc-tba-icon{font-size:40px;width:40px;height:40px;line-height:40px;color:var(--loc-tba-icon-color, #9CA3AF);opacity:.6}.loc-tba-text{margin:0;font-size:var(--cc-sf-input-font-size, .9rem);color:var(--cc-sf-hint-color, #6B7280);line-height:1.6;max-width:360px}.loc-online-panel .loc-search-wrapper{margin-top:4px}\n"] }]
|
|
5077
5372
|
}], ctorParameters: () => [{ type: i1$2.FormBuilder }, { type: ExpressionService }, { type: i3.HttpClient }], propDecorators: { config: [{
|
|
5078
5373
|
type: Input
|
|
5079
5374
|
}], controller: [{
|
|
@@ -6939,7 +7234,14 @@ class SmartTableComponent {
|
|
|
6939
7234
|
this.config.filters.forEach(filter => {
|
|
6940
7235
|
if (filter.apiUrl && !filter.options) {
|
|
6941
7236
|
const headers = this.getHeaders();
|
|
6942
|
-
|
|
7237
|
+
let params = new HttpParams();
|
|
7238
|
+
if (filter.handling === 'nested_string' && filter.nestedStringConfig) {
|
|
7239
|
+
const { paramName, baseValue } = filter.nestedStringConfig;
|
|
7240
|
+
if (baseValue) {
|
|
7241
|
+
params = params.set(paramName, baseValue);
|
|
7242
|
+
}
|
|
7243
|
+
}
|
|
7244
|
+
this.http.get(filter.apiUrl, { headers, params }).subscribe({
|
|
6943
7245
|
next: (response) => {
|
|
6944
7246
|
const data = filter.dataPath ? this.getValueByPath(response, filter.dataPath) : response;
|
|
6945
7247
|
if (!Array.isArray(data)) {
|
|
@@ -7111,6 +7413,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
7111
7413
|
}]
|
|
7112
7414
|
}] });
|
|
7113
7415
|
|
|
7416
|
+
function appendBaseUrlRecursively(obj, baseURL) {
|
|
7417
|
+
if (obj && typeof obj === 'object') {
|
|
7418
|
+
if (obj.apiUrl && typeof obj.apiUrl === 'string' && !obj.apiUrl.startsWith('http://') && !obj.apiUrl.startsWith('https://')) {
|
|
7419
|
+
let finalApiUrl = obj.apiUrl;
|
|
7420
|
+
if (baseURL.endsWith('/') && finalApiUrl.startsWith('/')) {
|
|
7421
|
+
finalApiUrl = finalApiUrl.substring(1);
|
|
7422
|
+
}
|
|
7423
|
+
else if (!baseURL.endsWith('/') && !finalApiUrl.startsWith('/')) {
|
|
7424
|
+
finalApiUrl = '/' + finalApiUrl;
|
|
7425
|
+
}
|
|
7426
|
+
obj.apiUrl = baseURL + finalApiUrl;
|
|
7427
|
+
}
|
|
7428
|
+
for (const key in obj) {
|
|
7429
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
7430
|
+
appendBaseUrlRecursively(obj[key], baseURL);
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
|
|
7114
7436
|
class ValidationUtils {
|
|
7115
7437
|
static email() {
|
|
7116
7438
|
return (control) => {
|
|
@@ -8480,7 +8802,49 @@ const SAMPLE_FORMS = {
|
|
|
8480
8802
|
]
|
|
8481
8803
|
}
|
|
8482
8804
|
}`,
|
|
8805
|
+
// ── Location field example ──────────────────────────────────────────────
|
|
8806
|
+
locationForm: `{
|
|
8807
|
+
"entityType": "EVENT",
|
|
8808
|
+
"label": "Event Location",
|
|
8809
|
+
"formType": "SECTION",
|
|
8810
|
+
"sectionConfig": {
|
|
8811
|
+
"children": [
|
|
8812
|
+
{
|
|
8813
|
+
"name": "eventLocation",
|
|
8814
|
+
"label": "Location",
|
|
8815
|
+
"type": "LOCATION",
|
|
8816
|
+
"required": true,
|
|
8817
|
+
"hint": "Set where your event takes place",
|
|
8818
|
+
"locationConfig": {
|
|
8819
|
+
"defaultTab": "VENUE",
|
|
8820
|
+
"maxLocations": 5,
|
|
8821
|
+
"venuePlaceholder": "Search for a venue address...",
|
|
8822
|
+
"onlinePlaceholder": "https://zoom.us/j/meeting-id",
|
|
8823
|
+
"showMap": true,
|
|
8824
|
+
"mapHeight": "300px",
|
|
8825
|
+
"googleMapsApiKey": "YOUR_GOOGLE_MAPS_API_KEY"
|
|
8826
|
+
}
|
|
8827
|
+
}
|
|
8828
|
+
]
|
|
8829
|
+
}
|
|
8830
|
+
}`,
|
|
8483
8831
|
};
|
|
8832
|
+
/**
|
|
8833
|
+
* i18n label keys used by the LOCATION field type.
|
|
8834
|
+
* Add these to your labels JSON file(s) for all supported languages.
|
|
8835
|
+
*
|
|
8836
|
+
* LBL_LOC_VENUE – Tab: "Venue"
|
|
8837
|
+
* LBL_LOC_ONLINE – Tab: "Online Event"
|
|
8838
|
+
* LBL_LOC_TBA – Tab: "To be Announced"
|
|
8839
|
+
* LBL_LOC_ADDRESS – Section heading inside Venue tab: "Location address"
|
|
8840
|
+
* LBL_LOC_COUNT_SUFFIX – e.g. "Locations Added!"
|
|
8841
|
+
* LBL_LOC_ADD_ANOTHER – "Add another Location" button label
|
|
8842
|
+
* LBL_LOC_MAP_HINT – hint below the embedded map
|
|
8843
|
+
* LBL_LOC_ONLINE_URL – Label above online URL input: "Event URL"
|
|
8844
|
+
* LBL_LOC_TBA_DESC – Message shown inside TBA tab
|
|
8845
|
+
* PH_LOC_VENUE – Placeholder for venue search input
|
|
8846
|
+
* PH_LOC_ONLINE – Placeholder for online URL input
|
|
8847
|
+
*/
|
|
8484
8848
|
|
|
8485
8849
|
var smartForm_examples = /*#__PURE__*/Object.freeze({
|
|
8486
8850
|
__proto__: null,
|
|
@@ -8509,5 +8873,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
8509
8873
|
* Generated bundle index. Do not edit.
|
|
8510
8874
|
*/
|
|
8511
8875
|
|
|
8512
|
-
export { AlertComponent, AlertModule, ButtonComponent, ButtonModule, CheckboxComponent, ConfigurableFormComponent, configurableForm_examples as ConfigurableFormExamples, ConfigurableFormModule, ConfirmationModalComponent, ConfirmationModalModule, DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_SIDE_NAV_TOOLTIP_POSITION, DatepickerComponent, DropdownComponent, ExpressionService, FilterComponent, FilterModule, FilterSidebarComponent, FilterSidebarModule, FormComponentsModule, InputComponent, MaterialModule, NAV_ORIENTATION_DEFAULT, NAV_VARIANT_DEFAULT, NavComponent, NavModule, PAGINATION_THEME_DARK, PAGINATION_THEME_DEFAULT, PaginationComponent, PaginationModule, RadioComponent, SearchComponent, SharedUiModule, SideNavComponent, SideNavModule, SmartFormComponent, SmartFormController, smartForm_examples as SmartFormExamples, SmartFormModule, SmartTableComponent, SmartTableModule, SnackbarComponent, SnackbarModule, SnackbarService, StringUtils, SummaryCardComponent, SummaryCardModule, ToggleComponent, ValidationUtils, clearLocalStorage, clearSessionStorage, getLocalStorageItem, getSessionStorageItem, removeLocalStorageItem, removeSessionStorageItem, setLocalStorageItem, setSessionStorageItem, translateConfig };
|
|
8876
|
+
export { AlertComponent, AlertModule, ButtonComponent, ButtonModule, CheckboxComponent, ConfigurableFormComponent, configurableForm_examples as ConfigurableFormExamples, ConfigurableFormModule, ConfirmationModalComponent, ConfirmationModalModule, DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_SIDE_NAV_TOOLTIP_POSITION, DatepickerComponent, DropdownComponent, ExpressionService, FilterComponent, FilterModule, FilterSidebarComponent, FilterSidebarModule, FormComponentsModule, InputComponent, MaterialModule, NAV_ORIENTATION_DEFAULT, NAV_VARIANT_DEFAULT, NavComponent, NavModule, PAGINATION_THEME_DARK, PAGINATION_THEME_DEFAULT, PaginationComponent, PaginationModule, RadioComponent, SearchComponent, SharedUiModule, SideNavComponent, SideNavModule, SmartFormComponent, SmartFormController, smartForm_examples as SmartFormExamples, SmartFormModule, SmartTableComponent, SmartTableModule, SnackbarComponent, SnackbarModule, SnackbarService, StringUtils, SummaryCardComponent, SummaryCardModule, ToggleComponent, ValidationUtils, appendBaseUrlRecursively, clearLocalStorage, clearSessionStorage, getLocalStorageItem, getSessionStorageItem, removeLocalStorageItem, removeSessionStorageItem, setLocalStorageItem, setSessionStorageItem, translateConfig };
|
|
8513
8877
|
//# sourceMappingURL=commons-shared-web-ui.mjs.map
|