ngx-dynamic-entity-builder 2.1.1 → 2.3.0
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.
|
@@ -449,18 +449,37 @@ class BuilderStore {
|
|
|
449
449
|
return this.findFieldBy(tabs, f => f.refererField === key) ?? this.findFieldBy(tabs, f => f.id === key);
|
|
450
450
|
}
|
|
451
451
|
findFieldBy(tabs = [], match) {
|
|
452
|
-
for (const
|
|
453
|
-
const found =
|
|
452
|
+
for (const list of this.fieldLists(tabs)) {
|
|
453
|
+
const found = list.find(match);
|
|
454
454
|
if (found)
|
|
455
455
|
return found;
|
|
456
|
-
if (t.children) {
|
|
457
|
-
const nested = this.findFieldBy(t.children, match);
|
|
458
|
-
if (nested)
|
|
459
|
-
return nested;
|
|
460
|
-
}
|
|
461
456
|
}
|
|
462
457
|
return null;
|
|
463
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Every array a field can sit in: each tab's `fields` and each `group`/`array`'s `children`.
|
|
461
|
+
*
|
|
462
|
+
* The lookup walked tab `fields` only, so a field inside a group could be drawn and clicked
|
|
463
|
+
* but never resolved: its row highlighted while the inspector stayed empty, and remove,
|
|
464
|
+
* duplicate, move and every property setter did nothing at all.
|
|
465
|
+
*/
|
|
466
|
+
fieldLists(tabs = []) {
|
|
467
|
+
const lists = [];
|
|
468
|
+
const visit = (fields) => {
|
|
469
|
+
lists.push(fields);
|
|
470
|
+
for (const f of fields)
|
|
471
|
+
if (f.children?.length)
|
|
472
|
+
visit(f.children);
|
|
473
|
+
};
|
|
474
|
+
for (const tab of this.flattenTabs(tabs))
|
|
475
|
+
if (tab.fields)
|
|
476
|
+
visit(tab.fields);
|
|
477
|
+
return lists;
|
|
478
|
+
}
|
|
479
|
+
/** The array holding this exact field object, wherever it is nested. */
|
|
480
|
+
listContaining(tabs = [], field) {
|
|
481
|
+
return this.fieldLists(tabs).find(list => list.includes(field)) ?? null;
|
|
482
|
+
}
|
|
464
483
|
/**
|
|
465
484
|
* The key a caller should hand back to address this exact field. Templates use it so a
|
|
466
485
|
* click on the second of two same-named fields selects that one.
|
|
@@ -627,12 +646,11 @@ class BuilderStore {
|
|
|
627
646
|
const target = this.findFieldInTabs(draft.tabs, key);
|
|
628
647
|
if (!target)
|
|
629
648
|
return;
|
|
649
|
+
const list = this.listContaining(draft.tabs, target);
|
|
650
|
+
if (!list)
|
|
651
|
+
return;
|
|
630
652
|
removedId = target.id;
|
|
631
|
-
|
|
632
|
-
if (tab.fields) {
|
|
633
|
-
tab.fields = tab.fields.filter(f => f !== target);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
653
|
+
list.splice(list.indexOf(target), 1);
|
|
636
654
|
});
|
|
637
655
|
if (removedId === null)
|
|
638
656
|
return;
|
|
@@ -655,15 +673,14 @@ class BuilderStore {
|
|
|
655
673
|
const prefix = meta?.idPrefix ?? 'field';
|
|
656
674
|
const newId = this.uniqueId(prefix, this.allFieldIds(this._config().tabs));
|
|
657
675
|
this.mutate(draft => {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
676
|
+
// The same key resolves to the same field in the draft, whatever list holds it.
|
|
677
|
+
const wanted = this.findFieldInTabs(draft.tabs, id);
|
|
678
|
+
const list = wanted && this.listContaining(draft.tabs, wanted);
|
|
679
|
+
if (!wanted || !list)
|
|
680
|
+
return;
|
|
681
|
+
const copy = clone(source);
|
|
682
|
+
copy.id = newId;
|
|
683
|
+
list.splice(list.indexOf(wanted) + 1, 0, copy);
|
|
667
684
|
});
|
|
668
685
|
this._selectedFieldId.set(newId);
|
|
669
686
|
return newId;
|
|
@@ -674,20 +691,16 @@ class BuilderStore {
|
|
|
674
691
|
// field the walk reached first, which for two `address` fields was never the one the
|
|
675
692
|
// arrow button belonged to.
|
|
676
693
|
const wanted = this.findFieldInTabs(draft.tabs, key);
|
|
677
|
-
|
|
694
|
+
const fields = wanted && this.listContaining(draft.tabs, wanted);
|
|
695
|
+
if (!wanted || !fields)
|
|
678
696
|
return;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const [item] = fields.splice(from, 1);
|
|
687
|
-
fields.splice(to, 0, item);
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
697
|
+
// Within its own list: a group child moves among its siblings, never out of the group.
|
|
698
|
+
const from = fields.indexOf(wanted);
|
|
699
|
+
const to = from + direction;
|
|
700
|
+
if (to < 0 || to >= fields.length)
|
|
701
|
+
return;
|
|
702
|
+
const [item] = fields.splice(from, 1);
|
|
703
|
+
fields.splice(to, 0, item);
|
|
691
704
|
});
|
|
692
705
|
}
|
|
693
706
|
/**
|
|
@@ -715,18 +728,13 @@ class BuilderStore {
|
|
|
715
728
|
const wanted = this.findFieldInTabs(draft.tabs, id);
|
|
716
729
|
if (!wanted)
|
|
717
730
|
return;
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
targetTab.fields = targetTab.fields ?? [];
|
|
726
|
-
targetTab.fields.push(field);
|
|
727
|
-
moved = true;
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
731
|
+
const list = this.listContaining(draft.tabs, wanted);
|
|
732
|
+
if (!list || list === targetTab.fields)
|
|
733
|
+
return; // already there; nothing to do
|
|
734
|
+
list.splice(list.indexOf(wanted), 1);
|
|
735
|
+
targetTab.fields = targetTab.fields ?? [];
|
|
736
|
+
targetTab.fields.push(wanted);
|
|
737
|
+
moved = true;
|
|
730
738
|
});
|
|
731
739
|
return moved;
|
|
732
740
|
}
|
|
@@ -837,6 +845,12 @@ class BuilderStore {
|
|
|
837
845
|
if (field.validators.pattern === LEGACY_EMAIL_PATTERN)
|
|
838
846
|
delete field.validators.pattern;
|
|
839
847
|
}
|
|
848
|
+
else if (validator === 'url' || validator === 'phone') {
|
|
849
|
+
if (on)
|
|
850
|
+
field.validators[validator] = true;
|
|
851
|
+
else
|
|
852
|
+
delete field.validators[validator];
|
|
853
|
+
}
|
|
840
854
|
});
|
|
841
855
|
}
|
|
842
856
|
setParamValidator(id, validator, value) {
|
|
@@ -880,6 +894,8 @@ class BuilderStore {
|
|
|
880
894
|
// the box. Recognising it keeps older configs showing Email as ticked.
|
|
881
895
|
return !!field.validators?.email || field.validators?.pattern === LEGACY_EMAIL_PATTERN;
|
|
882
896
|
}
|
|
897
|
+
if (validator === 'url' || validator === 'phone')
|
|
898
|
+
return !!field.validators?.[validator];
|
|
883
899
|
return false;
|
|
884
900
|
}
|
|
885
901
|
// ─── Options (dropdown / multiSelect / radio) ────────────────────────────────
|
|
@@ -1981,6 +1997,8 @@ const DEFAULT_BUILDER_TEXT = {
|
|
|
1981
1997
|
hintInLanguage: 'Help text ({language})',
|
|
1982
1998
|
hintHint: 'An info icon beside the label, shown on hover and while the field has focus. Read out with the field.',
|
|
1983
1999
|
defaultValue: 'Default value',
|
|
2000
|
+
sliderStep: 'Step',
|
|
2001
|
+
sliderStepHint: 'How far one notch moves the slider. Leave empty for 1.',
|
|
1984
2002
|
// Field inspector — section headings and the layout control.
|
|
1985
2003
|
sectionBasics: 'Basics',
|
|
1986
2004
|
sectionLayout: 'Layout',
|
|
@@ -2950,6 +2968,7 @@ class FieldRulesListComponent {
|
|
|
2950
2968
|
<div class="deb-rule-item__actions">
|
|
2951
2969
|
<mat-slide-toggle
|
|
2952
2970
|
color="primary"
|
|
2971
|
+
[attr.data-testid]="'rule-enabled-' + rule.id"
|
|
2953
2972
|
[checked]="rule.enabled"
|
|
2954
2973
|
[matTooltip]="ui.text('toggleRule')"
|
|
2955
2974
|
(change)="store.toggleRule(rule.id!, $event.checked)"
|
|
@@ -3035,6 +3054,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3035
3054
|
<div class="deb-rule-item__actions">
|
|
3036
3055
|
<mat-slide-toggle
|
|
3037
3056
|
color="primary"
|
|
3057
|
+
[attr.data-testid]="'rule-enabled-' + rule.id"
|
|
3038
3058
|
[checked]="rule.enabled"
|
|
3039
3059
|
[matTooltip]="ui.text('toggleRule')"
|
|
3040
3060
|
(change)="store.toggleRule(rule.id!, $event.checked)"
|
|
@@ -3379,6 +3399,11 @@ class FieldInspectorComponent {
|
|
|
3379
3399
|
];
|
|
3380
3400
|
return flags.filter(Boolean).length;
|
|
3381
3401
|
}
|
|
3402
|
+
/** A step must be above zero; anything else clears it, and the slider uses 1. */
|
|
3403
|
+
positiveOrUnset(value) {
|
|
3404
|
+
const n = this.toNum(value);
|
|
3405
|
+
return n !== null && n > 0 ? n : undefined;
|
|
3406
|
+
}
|
|
3382
3407
|
toNum(value) {
|
|
3383
3408
|
if (value === '' || value === null || value === undefined)
|
|
3384
3409
|
return null;
|
|
@@ -3534,7 +3559,7 @@ class FieldInspectorComponent {
|
|
|
3534
3559
|
{ label: 'Slug', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
3535
3560
|
];
|
|
3536
3561
|
supportsPattern(type) {
|
|
3537
|
-
return ['text', 'textarea', 'markdown', 'email', 'password'].includes(type);
|
|
3562
|
+
return ['text', 'textarea', 'markdown', 'email', 'password', 'url', 'phone'].includes(type);
|
|
3538
3563
|
}
|
|
3539
3564
|
applyPreset(field, pattern) {
|
|
3540
3565
|
this.store.setPatternValidator(this.store.keyOf(field), pattern);
|
|
@@ -3562,7 +3587,7 @@ class FieldInspectorComponent {
|
|
|
3562
3587
|
this.store.selectField(key);
|
|
3563
3588
|
}
|
|
3564
3589
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3565
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('condition') }}\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('mapping') }}\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: EntityReferenceConfigComponent, selector: "ngx-entity-reference-config" }, { kind: "component", type: FieldRulesListComponent, selector: "ngx-field-rules-list" }, { kind: "component", type: ReferencedFieldConfigComponent, selector: "ngx-referenced-field-config" }] });
|
|
3590
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n @if (f.type === 'slider') {\n <!-- The slider's range is min and max above; its increment is the one number left. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('sliderStep') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n data-testid=\"field-step\"\n [ngModel]=\"f.step ?? null\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { step: positiveOrUnset($event) })\"\n />\n <mat-hint>{{ ui.text('sliderStepHint') }}</mat-hint>\n </mat-form-field>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('condition') }}\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('mapping') }}\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: EntityReferenceConfigComponent, selector: "ngx-entity-reference-config" }, { kind: "component", type: FieldRulesListComponent, selector: "ngx-field-rules-list" }, { kind: "component", type: ReferencedFieldConfigComponent, selector: "ngx-referenced-field-config" }] });
|
|
3566
3591
|
}
|
|
3567
3592
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, decorators: [{
|
|
3568
3593
|
type: Component,
|
|
@@ -3580,7 +3605,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3580
3605
|
EntityReferenceConfigComponent,
|
|
3581
3606
|
FieldRulesListComponent,
|
|
3582
3607
|
ReferencedFieldConfigComponent,
|
|
3583
|
-
], template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('condition') }}\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('mapping') }}\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"] }]
|
|
3608
|
+
], template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n @if (f.type === 'slider') {\n <!-- The slider's range is min and max above; its increment is the one number left. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('sliderStep') }}</mat-label>\n <input\n matInput\n type=\"number\"\n min=\"0\"\n data-testid=\"field-step\"\n [ngModel]=\"f.step ?? null\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { step: positiveOrUnset($event) })\"\n />\n <mat-hint>{{ ui.text('sliderStepHint') }}</mat-hint>\n </mat-form-field>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('condition') }}\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('mapping') }}\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"] }]
|
|
3584
3609
|
}] });
|
|
3585
3610
|
|
|
3586
3611
|
/**
|
|
@@ -3599,12 +3624,18 @@ const GROUP_OF = {
|
|
|
3599
3624
|
currency: 'basic',
|
|
3600
3625
|
email: 'basic',
|
|
3601
3626
|
password: 'basic',
|
|
3627
|
+
url: 'basic',
|
|
3628
|
+
phone: 'basic',
|
|
3629
|
+
slider: 'basic',
|
|
3630
|
+
color: 'basic',
|
|
3602
3631
|
dropdown: 'choice',
|
|
3603
3632
|
radio: 'choice',
|
|
3604
3633
|
multiSelect: 'choice',
|
|
3605
3634
|
checkbox: 'choice',
|
|
3606
3635
|
boolean: 'choice',
|
|
3607
3636
|
'entity-ref': 'choice',
|
|
3637
|
+
rating: 'choice',
|
|
3638
|
+
tags: 'choice',
|
|
3608
3639
|
date: 'datetime',
|
|
3609
3640
|
datetime: 'datetime',
|
|
3610
3641
|
time: 'datetime',
|
|
@@ -3795,7 +3826,7 @@ class TabManagerComponent {
|
|
|
3795
3826
|
<div class="deb-tabs">
|
|
3796
3827
|
<div class="deb-tabs__head">
|
|
3797
3828
|
<span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
|
|
3798
|
-
<button mat-stroked-button type="button" (click)="store.addTab()">
|
|
3829
|
+
<button mat-stroked-button type="button" data-testid="add-tab" (click)="store.addTab()">
|
|
3799
3830
|
<mat-icon>add</mat-icon> {{ ui.text('addTab') }}
|
|
3800
3831
|
</button>
|
|
3801
3832
|
</div>
|
|
@@ -3811,6 +3842,7 @@ class TabManagerComponent {
|
|
|
3811
3842
|
<mat-label>{{ tab.id }} {{ tab.systemDefault ? ui.text('systemTag') : '' }}</mat-label>
|
|
3812
3843
|
<input
|
|
3813
3844
|
matInput
|
|
3845
|
+
[attr.data-testid]="'tab-label-' + tab.id"
|
|
3814
3846
|
[disabled]="!canEditTab(tab)"
|
|
3815
3847
|
[ngModel]="tabLabel(tab)"
|
|
3816
3848
|
(ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
|
|
@@ -3820,6 +3852,7 @@ class TabManagerComponent {
|
|
|
3820
3852
|
mat-icon-button
|
|
3821
3853
|
type="button"
|
|
3822
3854
|
[disabled]="i === 0 || !canEditTab(tab)"
|
|
3855
|
+
[attr.data-testid]="'tab-up-' + tab.id"
|
|
3823
3856
|
(click)="store.moveTab(tab.id, -1)"
|
|
3824
3857
|
[matTooltip]="ui.text('moveUp')"
|
|
3825
3858
|
[attr.aria-label]="ui.text('moveTabUp')"
|
|
@@ -3830,6 +3863,7 @@ class TabManagerComponent {
|
|
|
3830
3863
|
mat-icon-button
|
|
3831
3864
|
type="button"
|
|
3832
3865
|
[disabled]="i === count - 1 || !canEditTab(tab)"
|
|
3866
|
+
[attr.data-testid]="'tab-down-' + tab.id"
|
|
3833
3867
|
(click)="store.moveTab(tab.id, 1)"
|
|
3834
3868
|
[matTooltip]="ui.text('moveDown')"
|
|
3835
3869
|
[attr.aria-label]="ui.text('moveTabDown')"
|
|
@@ -3883,7 +3917,14 @@ class TabManagerComponent {
|
|
|
3883
3917
|
>
|
|
3884
3918
|
{{ ui.text('systemDefault') }}
|
|
3885
3919
|
</mat-checkbox>
|
|
3886
|
-
<button
|
|
3920
|
+
<button
|
|
3921
|
+
mat-button
|
|
3922
|
+
type="button"
|
|
3923
|
+
color="primary"
|
|
3924
|
+
[attr.data-testid]="'add-subtab-' + tab.id"
|
|
3925
|
+
[disabled]="!canEditTab(tab)"
|
|
3926
|
+
(click)="store.addSubTab(tab.id)"
|
|
3927
|
+
>
|
|
3887
3928
|
{{ ui.text('addSubTab') }}
|
|
3888
3929
|
</button>
|
|
3889
3930
|
</div>
|
|
@@ -3912,6 +3953,7 @@ class TabManagerComponent {
|
|
|
3912
3953
|
<mat-label>{{ sub.id }}</mat-label>
|
|
3913
3954
|
<input
|
|
3914
3955
|
matInput
|
|
3956
|
+
[attr.data-testid]="'subtab-label-' + sub.id"
|
|
3915
3957
|
[disabled]="!canEditTab(sub)"
|
|
3916
3958
|
[ngModel]="tabLabel(sub)"
|
|
3917
3959
|
(ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
|
|
@@ -3952,7 +3994,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3952
3994
|
<div class="deb-tabs">
|
|
3953
3995
|
<div class="deb-tabs__head">
|
|
3954
3996
|
<span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
|
|
3955
|
-
<button mat-stroked-button type="button" (click)="store.addTab()">
|
|
3997
|
+
<button mat-stroked-button type="button" data-testid="add-tab" (click)="store.addTab()">
|
|
3956
3998
|
<mat-icon>add</mat-icon> {{ ui.text('addTab') }}
|
|
3957
3999
|
</button>
|
|
3958
4000
|
</div>
|
|
@@ -3968,6 +4010,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3968
4010
|
<mat-label>{{ tab.id }} {{ tab.systemDefault ? ui.text('systemTag') : '' }}</mat-label>
|
|
3969
4011
|
<input
|
|
3970
4012
|
matInput
|
|
4013
|
+
[attr.data-testid]="'tab-label-' + tab.id"
|
|
3971
4014
|
[disabled]="!canEditTab(tab)"
|
|
3972
4015
|
[ngModel]="tabLabel(tab)"
|
|
3973
4016
|
(ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
|
|
@@ -3977,6 +4020,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3977
4020
|
mat-icon-button
|
|
3978
4021
|
type="button"
|
|
3979
4022
|
[disabled]="i === 0 || !canEditTab(tab)"
|
|
4023
|
+
[attr.data-testid]="'tab-up-' + tab.id"
|
|
3980
4024
|
(click)="store.moveTab(tab.id, -1)"
|
|
3981
4025
|
[matTooltip]="ui.text('moveUp')"
|
|
3982
4026
|
[attr.aria-label]="ui.text('moveTabUp')"
|
|
@@ -3987,6 +4031,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
3987
4031
|
mat-icon-button
|
|
3988
4032
|
type="button"
|
|
3989
4033
|
[disabled]="i === count - 1 || !canEditTab(tab)"
|
|
4034
|
+
[attr.data-testid]="'tab-down-' + tab.id"
|
|
3990
4035
|
(click)="store.moveTab(tab.id, 1)"
|
|
3991
4036
|
[matTooltip]="ui.text('moveDown')"
|
|
3992
4037
|
[attr.aria-label]="ui.text('moveTabDown')"
|
|
@@ -4040,7 +4085,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
4040
4085
|
>
|
|
4041
4086
|
{{ ui.text('systemDefault') }}
|
|
4042
4087
|
</mat-checkbox>
|
|
4043
|
-
<button
|
|
4088
|
+
<button
|
|
4089
|
+
mat-button
|
|
4090
|
+
type="button"
|
|
4091
|
+
color="primary"
|
|
4092
|
+
[attr.data-testid]="'add-subtab-' + tab.id"
|
|
4093
|
+
[disabled]="!canEditTab(tab)"
|
|
4094
|
+
(click)="store.addSubTab(tab.id)"
|
|
4095
|
+
>
|
|
4044
4096
|
{{ ui.text('addSubTab') }}
|
|
4045
4097
|
</button>
|
|
4046
4098
|
</div>
|
|
@@ -4069,6 +4121,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
4069
4121
|
<mat-label>{{ sub.id }}</mat-label>
|
|
4070
4122
|
<input
|
|
4071
4123
|
matInput
|
|
4124
|
+
[attr.data-testid]="'subtab-label-' + sub.id"
|
|
4072
4125
|
[disabled]="!canEditTab(sub)"
|
|
4073
4126
|
[ngModel]="tabLabel(sub)"
|
|
4074
4127
|
(ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
|
|
@@ -5168,7 +5221,7 @@ class EntityBuilderComponent {
|
|
|
5168
5221
|
this.store.setPermission(action, roles);
|
|
5169
5222
|
}
|
|
5170
5223
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5171
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: false, isRequired: false, transformFunction: null }, rules: { classPropertyName: "rules", publicName: "rules", isSignal: false, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: false, isRequired: false, transformFunction: null }, uiLanguage: { classPropertyName: "uiLanguage", publicName: "uiLanguage", isSignal: false, isRequired: false, transformFunction: null }, availableRoles: { classPropertyName: "availableRoles", publicName: "availableRoles", isSignal: false, isRequired: false, transformFunction: null }, userRoles: { classPropertyName: "userRoles", publicName: "userRoles", isSignal: false, isRequired: false, transformFunction: null }, commonModules: { classPropertyName: "commonModules", publicName: "commonModules", isSignal: false, isRequired: false, transformFunction: null }, leftSidebarOpen: { classPropertyName: "leftSidebarOpen", publicName: "leftSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, rightSidebarOpen: { classPropertyName: "rightSidebarOpen", publicName: "rightSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, fieldsOpen: { classPropertyName: "fieldsOpen", publicName: "fieldsOpen", isSignal: true, isRequired: false, transformFunction: null }, previewOpen: { classPropertyName: "previewOpen", publicName: "previewOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", rulesChange: "rulesChange", save: "save", leftSidebarOpen: "leftSidebarOpenChange", rightSidebarOpen: "rightSidebarOpenChange", fieldsOpen: "fieldsOpenChange", previewOpen: "previewOpenChange" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('permissions') }}</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRoles', { action }) }}</mat-label>\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: the exported JSON \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n\n@if (ruleGraphOpen()) {\n <ngx-rule-dependency-graph\n (dismiss)=\"closeRuleGraph()\"\n (selectField)=\"onSelectFieldFromGraph($event)\"\n />\n}\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{margin-left:auto}}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#dc2626;font-weight:700;flex:none}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:var(--deb-muted)}.cdk-drag-preview.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4$3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: "ngmodule", type: MatMenuModule }, { kind: "component", type: i8.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i11.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: FieldPaletteComponent, selector: "ngx-field-palette", outputs: ["pick"] }, { kind: "component", type: TabManagerComponent, selector: "ngx-tab-manager" }, { kind: "component", type: FieldInspectorComponent, selector: "ngx-field-inspector" }, { kind: "component", type: EntityBuilderCanvasComponent, selector: "ngx-entity-builder-canvas", outputs: ["collapse"] }, { kind: "component", type: RuleDependencyGraphComponent, selector: "ngx-rule-dependency-graph", outputs: ["dismiss", "selectField"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
5224
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: false, isRequired: false, transformFunction: null }, rules: { classPropertyName: "rules", publicName: "rules", isSignal: false, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: false, isRequired: false, transformFunction: null }, uiLanguage: { classPropertyName: "uiLanguage", publicName: "uiLanguage", isSignal: false, isRequired: false, transformFunction: null }, availableRoles: { classPropertyName: "availableRoles", publicName: "availableRoles", isSignal: false, isRequired: false, transformFunction: null }, userRoles: { classPropertyName: "userRoles", publicName: "userRoles", isSignal: false, isRequired: false, transformFunction: null }, commonModules: { classPropertyName: "commonModules", publicName: "commonModules", isSignal: false, isRequired: false, transformFunction: null }, leftSidebarOpen: { classPropertyName: "leftSidebarOpen", publicName: "leftSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, rightSidebarOpen: { classPropertyName: "rightSidebarOpen", publicName: "rightSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, fieldsOpen: { classPropertyName: "fieldsOpen", publicName: "fieldsOpen", isSignal: true, isRequired: false, transformFunction: null }, previewOpen: { classPropertyName: "previewOpen", publicName: "previewOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", rulesChange: "rulesChange", save: "save", leftSidebarOpen: "leftSidebarOpenChange", rightSidebarOpen: "rightSidebarOpenChange", fieldsOpen: "fieldsOpenChange", previewOpen: "previewOpenChange" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n data-testid=\"builder-entity-name\"\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('permissions') }}</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRoles', { action }) }}</mat-label>\n <mat-select\n multiple\n [attr.data-testid]=\"'permission-roles-' + action\"\n [ngModel]=\"rolesFor(action)\"\n (ngModelChange)=\"setRoles(action, $event)\"\n >\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'permission-roles-' + action\"\n [ngModel]=\"rolesFor(action).join(', ')\"\n (ngModelChange)=\"setRolesFromText(action, $event)\"\n />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: the exported JSON \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n\n@if (ruleGraphOpen()) {\n <ngx-rule-dependency-graph\n (dismiss)=\"closeRuleGraph()\"\n (selectField)=\"onSelectFieldFromGraph($event)\"\n />\n}\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{flex-basis:100%;justify-content:flex-end;margin-left:auto}}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#dc2626;font-weight:700;flex:none}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:var(--deb-muted)}.cdk-drag-preview.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4$3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.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: "ngmodule", type: MatMenuModule }, { kind: "component", type: i8.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i11.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: FieldPaletteComponent, selector: "ngx-field-palette", outputs: ["pick"] }, { kind: "component", type: TabManagerComponent, selector: "ngx-tab-manager" }, { kind: "component", type: FieldInspectorComponent, selector: "ngx-field-inspector" }, { kind: "component", type: EntityBuilderCanvasComponent, selector: "ngx-entity-builder-canvas", outputs: ["collapse"] }, { kind: "component", type: RuleDependencyGraphComponent, selector: "ngx-rule-dependency-graph", outputs: ["dismiss", "selectField"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
5172
5225
|
}
|
|
5173
5226
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, decorators: [{
|
|
5174
5227
|
type: Component,
|
|
@@ -5191,7 +5244,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
5191
5244
|
FieldInspectorComponent,
|
|
5192
5245
|
EntityBuilderCanvasComponent,
|
|
5193
5246
|
RuleDependencyGraphComponent,
|
|
5194
|
-
], encapsulation: ViewEncapsulation.None, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('permissions') }}</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRoles', { action }) }}</mat-label>\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: the exported JSON \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n\n@if (ruleGraphOpen()) {\n <ngx-rule-dependency-graph\n (dismiss)=\"closeRuleGraph()\"\n (selectField)=\"onSelectFieldFromGraph($event)\"\n />\n}\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{margin-left:auto}}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#dc2626;font-weight:700;flex:none}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:var(--deb-muted)}.cdk-drag-preview.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\n"] }]
|
|
5247
|
+
], encapsulation: ViewEncapsulation.None, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n data-testid=\"builder-entity-name\"\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('permissions') }}</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRoles', { action }) }}</mat-label>\n <mat-select\n multiple\n [attr.data-testid]=\"'permission-roles-' + action\"\n [ngModel]=\"rolesFor(action)\"\n (ngModelChange)=\"setRoles(action, $event)\"\n >\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'permission-roles-' + action\"\n [ngModel]=\"rolesFor(action).join(', ')\"\n (ngModelChange)=\"setRolesFromText(action, $event)\"\n />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: the exported JSON \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n\n@if (ruleGraphOpen()) {\n <ngx-rule-dependency-graph\n (dismiss)=\"closeRuleGraph()\"\n (selectField)=\"onSelectFieldFromGraph($event)\"\n />\n}\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{flex-basis:100%;justify-content:flex-end;margin-left:auto}}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#dc2626;font-weight:700;flex:none}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:var(--deb-muted)}.cdk-drag-preview.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\n"] }]
|
|
5195
5248
|
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
5196
5249
|
type: Input
|
|
5197
5250
|
}], rules: [{
|