ngx-dynamic-entity-builder 2.1.0 → 2.2.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.
@@ -56,6 +56,95 @@ function deepClone(value) {
56
56
  return JSON.parse(JSON.stringify(value));
57
57
  }
58
58
 
59
+ /**
60
+ * Bounded undo/redo over immutable states, with keystroke coalescing.
61
+ *
62
+ * Entries hold **references, not clones**. The store builds a new state on every edit and
63
+ * shares unchanged subtrees, so every entry already refers to immutable objects. Undo puts
64
+ * the stored object back, so a `record` that follows sees the reference already sitting at
65
+ * the cursor and skips it — no suppression flag to get out of step.
66
+ *
67
+ * Deliberately free of Angular DI: signals need no injection context, so `new BuilderStore()`
68
+ * keeps working and a test can assert straight after an edit without flushing effects.
69
+ */
70
+ class BuilderHistory {
71
+ shapeOf;
72
+ same;
73
+ maxEntries;
74
+ coalesceMs;
75
+ entries = [];
76
+ cursor = signal(-1, ...(ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []));
77
+ length = signal(0, ...(ngDevMode ? [{ debugName: "length" }] : /* istanbul ignore next */ []));
78
+ lastEditAt = 0;
79
+ canUndo = computed(() => this.cursor() > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
80
+ canRedo = computed(() => this.cursor() < this.length() - 1, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
81
+ /**
82
+ * @param shapeOf Counts that distinguish a structural edit from a value edit. Two
83
+ * consecutive edits merge only when their shapes match.
84
+ * @param same Whether two states are the same state (reference identity, in practice).
85
+ */
86
+ constructor(shapeOf, same, maxEntries = 200, coalesceMs = 400) {
87
+ this.shapeOf = shapeOf;
88
+ this.same = same;
89
+ this.maxEntries = maxEntries;
90
+ this.coalesceMs = coalesceMs;
91
+ }
92
+ /**
93
+ * Fold a state into history.
94
+ *
95
+ * Recording the same state twice is harmless. A label bound to a keystroke would otherwise
96
+ * make undo walk back one character at a time, so two consecutive edits merge when they
97
+ * land inside `coalesceMs` *and* the shape is unchanged — a rename coalesces, while adding,
98
+ * removing or moving anything always earns its own step however fast it is clicked.
99
+ */
100
+ record(state) {
101
+ const at = this.cursor();
102
+ const top = at >= 0 ? this.entries[at] : undefined;
103
+ if (top && this.same(top.state, state))
104
+ return;
105
+ const now = Date.now();
106
+ const entry = { state, shape: this.shapeOf(state) };
107
+ if (top && now - this.lastEditAt < this.coalesceMs && top.shape === entry.shape && at === this.entries.length - 1) {
108
+ this.entries[at] = entry;
109
+ this.lastEditAt = now;
110
+ return;
111
+ }
112
+ // A new edit after an undo discards the redo branch, which is what every editor does.
113
+ this.entries.length = at + 1;
114
+ this.entries.push(entry);
115
+ // Bounded: a builder session is long, and the oldest steps are the ones nobody walks back to.
116
+ if (this.entries.length > this.maxEntries) {
117
+ this.entries.splice(0, this.entries.length - this.maxEntries);
118
+ }
119
+ this.cursor.set(this.entries.length - 1);
120
+ this.length.set(this.entries.length);
121
+ this.lastEditAt = now;
122
+ }
123
+ /** Start again from `state`. Nothing before it is undoable. */
124
+ reset(state) {
125
+ this.entries.length = 0;
126
+ this.entries.push({ state, shape: this.shapeOf(state) });
127
+ this.cursor.set(0);
128
+ this.length.set(1);
129
+ this.lastEditAt = 0;
130
+ }
131
+ /** Step back, returning the state to restore, or `null` when there is none. */
132
+ undo() {
133
+ return this.canUndo() ? this.moveTo(this.cursor() - 1) : null;
134
+ }
135
+ /** Step forward, returning the state to restore, or `null` when there is none. */
136
+ redo() {
137
+ return this.canRedo() ? this.moveTo(this.cursor() + 1) : null;
138
+ }
139
+ moveTo(index) {
140
+ const entry = this.entries[index];
141
+ if (!entry)
142
+ return null;
143
+ this.cursor.set(index);
144
+ return entry.state;
145
+ }
146
+ }
147
+
59
148
  const clone = deepClone;
60
149
  const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
61
150
  /**
@@ -66,6 +155,26 @@ const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
66
155
  * un-ticked. Nothing writes it any more.
67
156
  */
68
157
  const LEGACY_EMAIL_PATTERN = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
158
+ /** Counts that distinguish a structural edit from a value edit. */
159
+ function shapeOf({ config, rules }) {
160
+ let fields = 0;
161
+ let tabs = 0;
162
+ const walkFields = (list) => {
163
+ for (const f of list ?? []) {
164
+ fields += 1;
165
+ walkFields(f.children);
166
+ }
167
+ };
168
+ const walkTabs = (list) => {
169
+ for (const t of list ?? []) {
170
+ tabs += 1;
171
+ walkFields(t.fields);
172
+ walkTabs(t.children);
173
+ }
174
+ };
175
+ walkTabs(config.tabs);
176
+ return `${tabs}:${fields}:${rules.length}`;
177
+ }
69
178
  /**
70
179
  * Follow a field rename through a rule reference, in either spelling.
71
180
  *
@@ -113,129 +222,34 @@ class BuilderStore {
113
222
  /**
114
223
  * Roles of the person using the builder — distinct from `availableRoles`, which is the
115
224
  * vocabulary a schema may reference. Needed so the SYSTEM_DEFAULT_CAN_EDIT predicate can
116
- * be asked a real question; it was previously called with a hardcoded empty array, which
117
- * made any role-checking predicate answer false for everyone.
225
+ * be asked a real question rather than answering for an empty role list.
118
226
  */
119
227
  _userRoles = signal([], ...(ngDevMode ? [{ debugName: "_userRoles" }] : /* istanbul ignore next */ []));
120
228
  // ─── Undo / redo ────────────────────────────────────────────────────────────
121
229
  //
122
- // History is a list of {config, rules} pairs, because the builder's state is two signals
123
- // and undoing one without the other would leave a rule pointing at a field that no longer
124
- // exists.
125
- //
126
- // Entries hold **references, not clones**. `mutate` builds a new config and `mutateField`
127
- // shares structure along the unchanged path, so every snapshot already refers to immutable
128
- // objects and unchanged subtrees are shared between entries. That also removes the usual
129
- // re-entrancy problem for free: undo puts the stored object back, so the recording effect
130
- // sees the exact reference already sitting at the cursor and skips it. No suppression flag
131
- // to get out of step.
132
- history = [];
133
- cursor = signal(-1, ...(ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []));
134
- historyLength = signal(0, ...(ngDevMode ? [{ debugName: "historyLength" }] : /* istanbul ignore next */ []));
135
- lastEditAt = 0;
136
- /** Consecutive edits closer together than this fold into one undo step. */
137
- static COALESCE_MS = 400;
138
- canUndo = computed(() => this.cursor() > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
139
- canRedo = computed(() => this.cursor() < this.historyLength() - 1, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
140
- /**
141
- * Fold the current state into history.
142
- *
143
- * Called explicitly by the write paths rather than from an `effect`. An effect would have
144
- * been fewer call sites, but it requires an injection context: `BuilderStore` had no
145
- * constructor, so `new BuilderStore()` was legal, and adding one broke every caller that
146
- * did it with NG0203. Explicit calls also make the timing deterministic — a test can
147
- * assert straight after an edit instead of flushing effects first.
148
- *
149
- * Calling it twice for one operation is harmless: the second call sees the same signal
150
- * references already at the cursor and returns.
151
- *
152
- * `setFieldLabel` is bound to a keystroke, so recording every emission would make undo
153
- * walk back one character at a time. Two consecutive edits merge when they land inside
154
- * `COALESCE_MS` *and* the structure is unchanged — a rename coalesces, while adding,
155
- * removing or moving anything always earns its own step however fast it is clicked.
156
- */
230
+ // History stores {config, rules} pairs, because the builder's state is two signals and
231
+ // undoing one without the other would leave a rule pointing at a field that no longer
232
+ // exists. Write paths call `record()` explicitly rather than from an `effect`, which would
233
+ // need an injection context and break `new BuilderStore()`.
234
+ history = new BuilderHistory(shapeOf, (a, b) => a.config === b.config && a.rules === b.rules);
235
+ canUndo = this.history.canUndo;
236
+ canRedo = this.history.canRedo;
157
237
  record() {
158
- const config = this._config();
159
- const rules = this._rules();
160
- const at = this.cursor();
161
- const top = at >= 0 ? this.history[at] : undefined;
162
- if (top && top.config === config && top.rules === rules)
163
- return;
164
- const now = Date.now();
165
- const snapshot = { config, rules, shape: this.shapeOf(config, rules) };
166
- if (top &&
167
- now - this.lastEditAt < BuilderStore.COALESCE_MS &&
168
- top.shape === snapshot.shape &&
169
- at === this.history.length - 1) {
170
- this.history[at] = snapshot;
171
- this.lastEditAt = now;
172
- return;
173
- }
174
- // A new edit after an undo discards the redo branch, which is what every editor does.
175
- this.history.length = at + 1;
176
- this.history.push(snapshot);
177
- // Bounded, because a builder session is long and every keystroke that does not coalesce
178
- // earns an entry. Snapshots hold references and share unchanged subtrees, so an entry is
179
- // cheap — but unbounded is unbounded, and the oldest steps are the ones nobody walks
180
- // back to. Dropping from the front keeps the most recent 200.
181
- if (this.history.length > BuilderStore.MAX_HISTORY) {
182
- this.history.splice(0, this.history.length - BuilderStore.MAX_HISTORY);
183
- }
184
- this.cursor.set(this.history.length - 1);
185
- this.historyLength.set(this.history.length);
186
- this.lastEditAt = now;
187
- }
188
- /** How many undo steps are kept. */
189
- static MAX_HISTORY = 200;
190
- /** Counts that distinguish a structural edit from a value edit. */
191
- shapeOf(config, rules) {
192
- let fields = 0;
193
- let tabs = 0;
194
- const walkFields = (list) => {
195
- for (const f of list ?? []) {
196
- fields += 1;
197
- walkFields(f.children);
198
- }
199
- };
200
- const walkTabs = (list) => {
201
- for (const t of list ?? []) {
202
- tabs += 1;
203
- walkFields(t.fields);
204
- walkTabs(t.children);
205
- }
206
- };
207
- walkTabs(config.tabs);
208
- return `${tabs}:${fields}:${rules.length}`;
238
+ this.history.record({ config: this._config(), rules: this._rules() });
209
239
  }
210
240
  /** Start history again from the state just loaded. Nothing before it is undoable. */
211
241
  resetHistory() {
212
- this.history.length = 0;
213
- this.history.push({
214
- config: this._config(),
215
- rules: this._rules(),
216
- shape: this.shapeOf(this._config(), this._rules()),
217
- });
218
- this.cursor.set(0);
219
- this.historyLength.set(1);
220
- this.lastEditAt = 0;
242
+ this.history.reset({ config: this._config(), rules: this._rules() });
221
243
  }
222
244
  undo() {
223
- if (!this.canUndo())
224
- return;
225
- this.applyHistory(this.cursor() - 1);
245
+ this.restore(this.history.undo());
226
246
  }
227
247
  redo() {
228
- if (!this.canRedo())
229
- return;
230
- this.applyHistory(this.cursor() + 1);
248
+ this.restore(this.history.redo());
231
249
  }
232
- applyHistory(index) {
233
- const snapshot = this.history[index];
250
+ restore(snapshot) {
234
251
  if (!snapshot)
235
252
  return;
236
- this.cursor.set(index);
237
- // Order matters only in that both land before the effect runs; it then sees references
238
- // identical to this entry and records nothing.
239
253
  this._config.set(snapshot.config);
240
254
  this._rules.set(snapshot.rules);
241
255
  this._isDirty.set(true);
@@ -255,11 +269,8 @@ class BuilderStore {
255
269
  /**
256
270
  * Every field the config declares, sub-tabs included.
257
271
  *
258
- * This used to stop at top-level tabs, which quietly broke three things at once: nine of
259
- * `insuranceClaims`'s twenty-eight fields never appeared on the canvas, the entity-reference
260
- * picker could not offer a nested field as a source, and the drift check looked a nested
261
- * field up here, got nothing, and returned without checking. The store's structural
262
- * operations already walked the whole tree — only this view did not.
272
+ * The canvas, the entity-reference source picker and the drift check all read this, so it
273
+ * has to reach nested tabs just as the store's structural operations do.
263
274
  *
264
275
  * It deliberately does not descend into a field's own `children`: `group` and `array`
265
276
  * children are rendered by the row that owns them, not as rows of their own.
@@ -389,10 +400,9 @@ class BuilderStore {
389
400
  /**
390
401
  * Every tab in the tree, flattened.
391
402
  *
392
- * The structural operations below used to iterate `draft.tabs` directly, which is only
393
- * the top level. A field on a sub-tab could be selected and edited but never removed,
394
- * duplicated, moved or reordered — the builder could author nested configs it was then
395
- * unable to restructure, which is the worst possible state for an authoring tool.
403
+ * The structural operations below go through this rather than `draft.tabs`, which is only
404
+ * the top level — otherwise a field on a sub-tab could be edited but never removed,
405
+ * duplicated, moved or reordered.
396
406
  */
397
407
  flattenTabs(tabs = []) {
398
408
  const list = [];
@@ -533,20 +543,16 @@ class BuilderStore {
533
543
  *
534
544
  * Ids are the wiring between fields — rules, cascades, patches and `showWhen` all address
535
545
  * fields by id — so a rename that only touches `field.id` silently breaks that wiring.
536
- * That was survivable while renaming was a rare manual act; label-derived ids make it
537
- * routine, so the references move with it.
538
- */
539
- /**
546
+ * Label-derived ids make renaming routine, so the references move with it.
547
+ *
540
548
  * `key` addresses the field to rename; `oldId` is the bare id every *reference* to it
541
- * carries. They were one parameter, which worked only while an id was unique across the
542
- * config: resolving needs the path, while `showWhen`, `parentField`, `autoPatch` targets
543
- * and rules are all keyed by bare id and never match a path.
549
+ * carries. They are separate because resolving needs the path, while `showWhen`,
550
+ * `parentField`, `autoPatch` targets and rules are all keyed by bare id.
544
551
  */
545
552
  applyRename(key, oldId, newId) {
546
553
  // Selection is held as whatever the caller passed — `addField` stores a bare id while a
547
- // canvas click stores a path — so comparing the two strings said "not selected" for a
548
- // field that plainly was, and the inspector emptied itself mid-rename. Compare the
549
- // fields the two keys resolve to instead.
554
+ // canvas click stores a path — so compare the fields the two keys resolve to, not the
555
+ // strings.
550
556
  const selectedKey = this._selectedFieldId();
551
557
  const tabsNow = this._config().tabs;
552
558
  const wasSelected = !!selectedKey && this.findFieldInTabs(tabsNow, selectedKey) === this.findFieldInTabs(tabsNow, key);
@@ -831,6 +837,12 @@ class BuilderStore {
831
837
  if (field.validators.pattern === LEGACY_EMAIL_PATTERN)
832
838
  delete field.validators.pattern;
833
839
  }
840
+ else if (validator === 'url' || validator === 'phone') {
841
+ if (on)
842
+ field.validators[validator] = true;
843
+ else
844
+ delete field.validators[validator];
845
+ }
834
846
  });
835
847
  }
836
848
  setParamValidator(id, validator, value) {
@@ -870,11 +882,12 @@ class BuilderStore {
870
882
  if (validator === 'required')
871
883
  return !!field.validators?.required;
872
884
  if (validator === 'email') {
873
- // This used to report *any* pattern as email, so writing a custom regex silently
874
- // ticked the box. The legacy encoding is still recognised so a config authored by the
875
- // old builder keeps showing Email as ticked.
885
+ // Only the legacy email regex counts, never any pattern: a custom regex must not tick
886
+ // the box. Recognising it keeps older configs showing Email as ticked.
876
887
  return !!field.validators?.email || field.validators?.pattern === LEGACY_EMAIL_PATTERN;
877
888
  }
889
+ if (validator === 'url' || validator === 'phone')
890
+ return !!field.validators?.[validator];
878
891
  return false;
879
892
  }
880
893
  // ─── Options (dropdown / multiSelect / radio) ────────────────────────────────
@@ -941,7 +954,7 @@ class BuilderStore {
941
954
  }
942
955
  });
943
956
  }
944
- /** Link a field to a field defined in another entity configuration (Phase 8). */
957
+ /** Link a field to a field defined in another entity configuration */
945
958
  linkReferencedField(fieldId, sourceEntityKey, sourceField) {
946
959
  this.mutate(draft => {
947
960
  const field = this.findFieldInTabs(draft.tabs, fieldId);
@@ -1670,10 +1683,10 @@ class BuilderStore {
1670
1683
  }
1671
1684
  return problems;
1672
1685
  }
1673
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1674
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderStore });
1686
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1687
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore });
1675
1688
  }
1676
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderStore, decorators: [{
1689
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore, decorators: [{
1677
1690
  type: Injectable
1678
1691
  }] });
1679
1692
 
@@ -1976,9 +1989,9 @@ const DEFAULT_BUILDER_TEXT = {
1976
1989
  hintInLanguage: 'Help text ({language})',
1977
1990
  hintHint: 'An info icon beside the label, shown on hover and while the field has focus. Read out with the field.',
1978
1991
  defaultValue: 'Default value',
1992
+ sliderStep: 'Step',
1993
+ sliderStepHint: 'How far one notch moves the slider. Leave empty for 1.',
1979
1994
  // Field inspector — section headings and the layout control.
1980
- // `validation` used to be the heading above the validator checkboxes; the inspector's
1981
- // sections now own their headings, so `sectionValidation` replaces it.
1982
1995
  sectionBasics: 'Basics',
1983
1996
  sectionLayout: 'Layout',
1984
1997
  sectionValidation: 'Validation',
@@ -2175,10 +2188,10 @@ class BuilderTextService {
2175
2188
  text(key, params) {
2176
2189
  return resolveUiText(this.overrides, DEFAULT_BUILDER_TEXT, key, this.language(), params);
2177
2190
  }
2178
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2179
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, providedIn: 'root' });
2191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2192
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, providedIn: 'root' });
2180
2193
  }
2181
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, decorators: [{
2194
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, decorators: [{
2182
2195
  type: Injectable,
2183
2196
  args: [{ providedIn: 'root' }]
2184
2197
  }] });
@@ -2268,8 +2281,8 @@ class EntityReferenceConfigComponent {
2268
2281
  this.filtersParseError.set('Invalid JSON.');
2269
2282
  }
2270
2283
  }
2271
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityReferenceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2272
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
2284
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2285
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
2273
2286
  @if (field(); as f) {
2274
2287
  <div class="deb-entity-ref">
2275
2288
  <span class="deb-section-title">{{ ui.text('entityReference') }}</span>
@@ -2429,7 +2442,7 @@ class EntityReferenceConfigComponent {
2429
2442
  }
2430
2443
  `, isInline: true, styles: [".deb-entity-ref{display:flex;flex-direction:column;gap:12px}.deb-option-row>mat-form-field{flex:1}.deb-error-hint{color:#b91c1c}\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: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { 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: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2431
2444
  }
2432
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityReferenceConfigComponent, decorators: [{
2445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceConfigComponent, decorators: [{
2433
2446
  type: Component,
2434
2447
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-entity-reference-config', standalone: true, imports: [
2435
2448
  FormsModule,
@@ -2619,6 +2632,8 @@ class RuleFormComponent {
2619
2632
  priority: 1,
2620
2633
  };
2621
2634
  save = new EventEmitter();
2635
+ // Published API since 1.x: renaming it would break every host bound to `(cancel)`.
2636
+ // eslint-disable-next-line @angular-eslint/no-output-native
2622
2637
  cancel = new EventEmitter();
2623
2638
  operators = [
2624
2639
  'EQUAL',
@@ -2684,8 +2699,8 @@ class RuleFormComponent {
2684
2699
  removeCondition(index) {
2685
2700
  this.rule.conditions.splice(index, 1);
2686
2701
  }
2687
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2688
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RuleFormComponent, isStandalone: true, selector: "ngx-rule-form", inputs: { rule: "rule" }, outputs: { save: "save", cancel: "cancel" }, ngImport: i0, template: `
2702
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2703
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RuleFormComponent, isStandalone: true, selector: "ngx-rule-form", inputs: { rule: "rule" }, outputs: { save: "save", cancel: "cancel" }, ngImport: i0, template: `
2689
2704
  <div class="deb-rule-form">
2690
2705
  <h3 class="deb-rule-form__title">{{ ui.text(rule.id ? 'editRuleTitle' : 'newRuleTitle') }}</h3>
2691
2706
 
@@ -2766,7 +2781,7 @@ class RuleFormComponent {
2766
2781
  </div>
2767
2782
  `, isInline: true, styles: [".deb-rule-form{display:flex;flex-direction:column;gap:12px;background:#fafafa;border:1px solid #e0e0e0;border-radius:8px;padding:16px}.deb-rule-form__title{margin:0 0 8px;font-size:16px;font-weight:600}.deb-full{width:100%}.deb-rule-path{color:#888;font-size:11px;margin-left:6px}.deb-rule-section{display:flex;flex-direction:column;gap:8px}.deb-rule-section h4{margin:0;font-size:13px;font-weight:600;color:#555}.deb-rule-row{display:flex;gap:8px;align-items:center}.deb-rule-row>*{flex:1}.deb-rule-form__actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: 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: 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2768
2783
  }
2769
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, decorators: [{
2784
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleFormComponent, decorators: [{
2770
2785
  type: Component,
2771
2786
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-form', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule], template: `
2772
2787
  <div class="deb-rule-form">
@@ -2910,8 +2925,8 @@ class FieldRulesListComponent {
2910
2925
  : `${rule.action.type}: ${rule.action.value}`;
2911
2926
  return `${rule.fieldId} ${cond} → ${action}`;
2912
2927
  }
2913
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldRulesListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2914
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
2928
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRulesListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2929
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
2915
2930
  <div class="deb-rules">
2916
2931
  <div class="deb-rules__head">
2917
2932
  <span class="deb-section-title">{{ ui.text('rules') }}</span>
@@ -2994,7 +3009,7 @@ class FieldRulesListComponent {
2994
3009
  </div>
2995
3010
  `, isInline: true, styles: [".deb-rules{display:flex;flex-direction:column;gap:8px}.deb-rules__head{display:flex;align-items:center;justify-content:space-between}.deb-rule-item{display:flex;align-items:center;justify-content:space-between;gap:8px;border:1px solid #e5e7eb;border-radius:8px;padding:8px 10px}.deb-rule-item--off{opacity:.55}.deb-rule-item__body{display:flex;flex-direction:column;min-width:0}.deb-rule-item__summary{font-size:13px;font-weight:600;overflow-wrap:anywhere}.deb-rule-item__meta{font-size:11px;color:#6b7280}.deb-rule-item__actions{display:flex;align-items:center;flex:0 0 auto}\n"], dependencies: [{ 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: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { 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: RuleFormComponent, selector: "ngx-rule-form", inputs: ["rule"], outputs: ["save", "cancel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2996
3011
  }
2997
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldRulesListComponent, decorators: [{
3012
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRulesListComponent, decorators: [{
2998
3013
  type: Component,
2999
3014
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
3000
3015
  <div class="deb-rules">
@@ -3120,10 +3135,8 @@ class ReferencedFieldConfigComponent {
3120
3135
  }
3121
3136
  }
3122
3137
  /**
3123
- * Both callers pass the id of the field they just edited. This used to ignore that
3124
- * argument and read `store.selectedField()` instead, so whenever the edited field was not
3125
- * the selected one it checked drift against the wrong field's source entity — or bailed
3126
- * out entirely if nothing was selected.
3138
+ * Takes the id of the field just edited rather than reading `store.selectedField()`, which
3139
+ * need not be the same field — or any field at all.
3127
3140
  */
3128
3141
  async checkDriftForField(fieldId) {
3129
3142
  const f = this.store.fields().find(field => field.id === fieldId);
@@ -3172,8 +3185,8 @@ class ReferencedFieldConfigComponent {
3172
3185
  }
3173
3186
  return undefined;
3174
3187
  }
3175
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ReferencedFieldConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3176
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ReferencedFieldConfigComponent, isStandalone: true, selector: "ngx-referenced-field-config", ngImport: i0, template: `
3188
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ReferencedFieldConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3189
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ReferencedFieldConfigComponent, isStandalone: true, selector: "ngx-referenced-field-config", ngImport: i0, template: `
3177
3190
  @if (field(); as f) {
3178
3191
  <div class="deb-referenced-field" style="margin-top: 16px;">
3179
3192
  <div class="deb-row" style="justify-content: space-between; align-items: center; margin-bottom: 8px;">
@@ -3234,7 +3247,7 @@ class ReferencedFieldConfigComponent {
3234
3247
  }
3235
3248
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: "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: "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: 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3236
3249
  }
3237
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ReferencedFieldConfigComponent, decorators: [{
3250
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ReferencedFieldConfigComponent, decorators: [{
3238
3251
  type: Component,
3239
3252
  args: [{
3240
3253
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -3376,6 +3389,11 @@ class FieldInspectorComponent {
3376
3389
  ];
3377
3390
  return flags.filter(Boolean).length;
3378
3391
  }
3392
+ /** A step must be above zero; anything else clears it, and the slider uses 1. */
3393
+ positiveOrUnset(value) {
3394
+ const n = this.toNum(value);
3395
+ return n !== null && n > 0 ? n : undefined;
3396
+ }
3379
3397
  toNum(value) {
3380
3398
  if (value === '' || value === null || value === undefined)
3381
3399
  return null;
@@ -3443,9 +3461,7 @@ class FieldInspectorComponent {
3443
3461
  }
3444
3462
  /**
3445
3463
  * Seeds a new condition with the first field not already watched.
3446
- *
3447
- * It used to seed the literal string `field`, which is not a field id at all — so a new
3448
- * condition started out referencing nothing and silently hid the field until it was edited.
3464
+ * A seed that names no real field would silently hide this one until edited.
3449
3465
  */
3450
3466
  addShowWhen(field) {
3451
3467
  const next = { ...(field.showWhen ?? {}) };
@@ -3533,7 +3549,7 @@ class FieldInspectorComponent {
3533
3549
  { label: 'Slug', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
3534
3550
  ];
3535
3551
  supportsPattern(type) {
3536
- return ['text', 'textarea', 'markdown', 'email', 'password'].includes(type);
3552
+ return ['text', 'textarea', 'markdown', 'email', 'password', 'url', 'phone'].includes(type);
3537
3553
  }
3538
3554
  applyPreset(field, pattern) {
3539
3555
  this.store.setPatternValidator(this.store.keyOf(field), pattern);
@@ -3560,10 +3576,10 @@ class FieldInspectorComponent {
3560
3576
  if (key)
3561
3577
  this.store.selectField(key);
3562
3578
  }
3563
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3564
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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" }] });
3579
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3580
+ 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" }] });
3565
3581
  }
3566
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, decorators: [{
3582
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, decorators: [{
3567
3583
  type: Component,
3568
3584
  args: [{ selector: 'ngx-field-inspector', standalone: true, imports: [
3569
3585
  FormsModule,
@@ -3579,7 +3595,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3579
3595
  EntityReferenceConfigComponent,
3580
3596
  FieldRulesListComponent,
3581
3597
  ReferencedFieldConfigComponent,
3582
- ], 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"] }]
3598
+ ], 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"] }]
3583
3599
  }] });
3584
3600
 
3585
3601
  /**
@@ -3598,12 +3614,18 @@ const GROUP_OF = {
3598
3614
  currency: 'basic',
3599
3615
  email: 'basic',
3600
3616
  password: 'basic',
3617
+ url: 'basic',
3618
+ phone: 'basic',
3619
+ slider: 'basic',
3620
+ color: 'basic',
3601
3621
  dropdown: 'choice',
3602
3622
  radio: 'choice',
3603
3623
  multiSelect: 'choice',
3604
3624
  checkbox: 'choice',
3605
3625
  boolean: 'choice',
3606
3626
  'entity-ref': 'choice',
3627
+ rating: 'choice',
3628
+ tags: 'choice',
3607
3629
  date: 'datetime',
3608
3630
  datetime: 'datetime',
3609
3631
  time: 'datetime',
@@ -3626,11 +3648,8 @@ const GROUP_TEXT_KEY = {
3626
3648
  /**
3627
3649
  * FieldPaletteComponent — the list of buildable field types.
3628
3650
  *
3629
- * Two things changed the day this stopped being a flat grid of twenty-two buttons. The labels
3630
- * were clipped mid-word — "Boolean Toggl", "Entity Referen", "File Attachmen" — because the
3631
- * grid forced two equal columns and the button refused to wrap, so a third of the palette
3632
- * could not be read. And with no order to them, finding "Month & Year" meant scanning the
3633
- * whole list: the grouping and the filter are what make a palette this size navigable.
3651
+ * Grouped and filterable, because a flat list of twenty-two types is not navigable, and
3652
+ * tiles wrap their labels so names like "Entity Reference" are never clipped.
3634
3653
  */
3635
3654
  class FieldPaletteComponent {
3636
3655
  /** Builder chrome, overridable via BUILDER_TEXT. */
@@ -3659,8 +3678,8 @@ class FieldPaletteComponent {
3659
3678
  })).filter(section => section.items.length > 0);
3660
3679
  }, ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
3661
3680
  pick = new EventEmitter();
3662
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3663
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FieldPaletteComponent, isStandalone: true, selector: "ngx-field-palette", outputs: { pick: "pick" }, ngImport: i0, template: `
3681
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3682
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldPaletteComponent, isStandalone: true, selector: "ngx-field-palette", outputs: { pick: "pick" }, ngImport: i0, template: `
3664
3683
  <div class="deb-palette">
3665
3684
  <div class="deb-palette__search">
3666
3685
  <mat-icon aria-hidden="true">search</mat-icon>
@@ -3710,7 +3729,7 @@ class FieldPaletteComponent {
3710
3729
  </div>
3711
3730
  `, isInline: true, styles: [".deb-palette{display:flex;flex-direction:column;gap:14px}.deb-palette__search{display:flex;align-items:center;gap:8px;padding:0 10px;height:36px;border:1px solid var(--deb-border, #e2e8f0);border-radius:8px;background:#f8fafc}.deb-palette__search:focus-within{border-color:var(--deb-accent, #6366f1);background:#fff;box-shadow:0 0 0 3px #6366f129}.deb-palette__search mat-icon{font-size:17px;width:17px;height:17px;color:var(--deb-muted, #64748b);flex:none}.deb-palette__search input{flex:1;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:13px;color:inherit}.deb-palette__search input::-webkit-search-cancel-button{display:none}.deb-palette__clear{appearance:none;border:0;background:none;padding:0;cursor:pointer;display:flex;color:var(--deb-muted, #64748b)}.deb-palette__clear:hover{color:#0f172a}.deb-palette__section{display:flex;flex-direction:column;gap:6px}.deb-palette__grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}.deb-palette__item{justify-content:flex-start!important;height:auto!important;min-height:38px;line-height:1.25!important;border-radius:8px!important;border:1px solid var(--deb-border, #e2e8f0)!important;padding:7px 10px!important;font-family:var(--font-heading, inherit)!important;font-weight:600!important;font-size:12.5px!important;transition:all .18s cubic-bezier(.16,1,.3,1)!important;background:#fff!important;text-align:left!important}.deb-palette__item:hover{border-color:#6366f1!important;background:#f5f3ff!important;color:#4f46e5!important;transform:translateY(-1px);box-shadow:0 4px 12px #6366f126!important}.deb-palette__item mat-icon{color:#4338ca;margin-right:7px;font-size:17px;width:17px;height:17px;flex:none}.deb-palette__item span{white-space:normal;overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.deb-palette__item{transition:none!important}.deb-palette__item:hover{transform:none}}\n"], dependencies: [{ 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: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3712
3731
  }
3713
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldPaletteComponent, decorators: [{
3732
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldPaletteComponent, decorators: [{
3714
3733
  type: Component,
3715
3734
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-palette', standalone: true, imports: [MatButtonModule, MatIconModule, MatTooltipModule], template: `
3716
3735
  <div class="deb-palette">
@@ -3792,8 +3811,8 @@ class TabManagerComponent {
3792
3811
  }
3793
3812
  return true;
3794
3813
  }
3795
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TabManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3796
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
3814
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TabManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3815
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
3797
3816
  <div class="deb-tabs">
3798
3817
  <div class="deb-tabs__head">
3799
3818
  <span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
@@ -3939,7 +3958,7 @@ class TabManagerComponent {
3939
3958
  </div>
3940
3959
  `, isInline: true, styles: [".deb-tabs__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.deb-tab-card{border:1px solid var(--deb-border, #e5e7eb);border-radius:8px;padding:10px;margin-bottom:10px;background:var(--deb-bg-surface, #ffffff)}.deb-tab-card--system{border-left:4px solid var(--deb-accent, #6366f1)}.deb-tabs__row{display:flex;align-items:center;gap:4px}.deb-tab-settings{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;margin-top:8px;font-size:13px}.deb-tab-module{margin-top:8px}.deb-subtabs{margin-top:8px;padding-left:16px;border-left:2px dashed var(--deb-border, #e5e7eb)}.deb-subtabs__title{font-size:12px;font-weight:600;color:var(--deb-muted, #6b7280)}.deb-subtab-row{margin-top:6px}.deb-grow{flex:1}\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: 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: 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: MatSlideToggleModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3941
3960
  }
3942
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TabManagerComponent, decorators: [{
3961
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TabManagerComponent, decorators: [{
3943
3962
  type: Component,
3944
3963
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-tab-manager', standalone: true, imports: [
3945
3964
  FormsModule,
@@ -4114,8 +4133,8 @@ class EntityBuilderTreeNodeComponent {
4114
4133
  const lang = this.store.activeLanguage();
4115
4134
  return resolveLabel(field.label, lang) || field.id;
4116
4135
  }
4117
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4118
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityBuilderTreeNodeComponent, isStandalone: true, selector: "ngx-entity-builder-tree-node", inputs: { field: "field", index: "index", totalCount: "totalCount" }, ngImport: i0, template: `
4136
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4137
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderTreeNodeComponent, isStandalone: true, selector: "ngx-entity-builder-tree-node", inputs: { field: "field", index: "index", totalCount: "totalCount" }, ngImport: i0, template: `
4119
4138
  <div
4120
4139
  cdkDrag
4121
4140
  class="deb-field-row"
@@ -4231,7 +4250,7 @@ class EntityBuilderTreeNodeComponent {
4231
4250
  }
4232
4251
  `, isInline: true, dependencies: [{ kind: "component", type: EntityBuilderTreeNodeComponent, selector: "ngx-entity-builder-tree-node", inputs: ["field", "index", "totalCount"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4233
4252
  }
4234
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderTreeNodeComponent, decorators: [{
4253
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderTreeNodeComponent, decorators: [{
4235
4254
  type: Component,
4236
4255
  args: [{
4237
4256
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4383,8 +4402,8 @@ class EntityBuilderCanvasComponent {
4383
4402
  onDrop(tabId, event) {
4384
4403
  this.store.reorderField(event.previousIndex, event.currentIndex, tabId);
4385
4404
  }
4386
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4387
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityBuilderCanvasComponent, isStandalone: true, selector: "ngx-entity-builder-canvas", outputs: { collapse: "collapse" }, ngImport: i0, template: `
4405
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4406
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderCanvasComponent, isStandalone: true, selector: "ngx-entity-builder-canvas", outputs: { collapse: "collapse" }, ngImport: i0, template: `
4388
4407
  <mat-card class="deb-card deb-card--canvas">
4389
4408
  <mat-card-header>
4390
4409
  <mat-card-title class="deb-card__title">
@@ -4409,10 +4428,9 @@ class EntityBuilderCanvasComponent {
4409
4428
  @if (store.fields().length === 0) {
4410
4429
  <div class="deb-empty">
4411
4430
  <!--
4412
- Decoration, and marked as such. The sentence below says everything this glyph
4413
- says, so a reader hearing "widgets" before it learns nothing; and a faint
4414
- illustration cannot meet a text contrast ratio without ceasing to be faint.
4415
- Hidden from the accessibility tree, it is neither announced nor measured.
4431
+ Decoration, and marked as such: the sentence below says everything this glyph
4432
+ says, so it is hidden from the accessibility tree. axe still measures its
4433
+ contrast, so its colour passes AA like any other text.
4416
4434
  -->
4417
4435
  <mat-icon aria-hidden="true">widgets</mat-icon>
4418
4436
  <p>{{ ui.text('canvasEmpty') }}</p>
@@ -4445,7 +4463,7 @@ class EntityBuilderCanvasComponent {
4445
4463
  </mat-card>
4446
4464
  `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "ngmodule", type: MatButtonModule }, { 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: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { 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: EntityBuilderTreeNodeComponent, selector: "ngx-entity-builder-tree-node", inputs: ["field", "index", "totalCount"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4447
4465
  }
4448
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderCanvasComponent, decorators: [{
4466
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderCanvasComponent, decorators: [{
4449
4467
  type: Component,
4450
4468
  args: [{
4451
4469
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4485,10 +4503,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
4485
4503
  @if (store.fields().length === 0) {
4486
4504
  <div class="deb-empty">
4487
4505
  <!--
4488
- Decoration, and marked as such. The sentence below says everything this glyph
4489
- says, so a reader hearing "widgets" before it learns nothing; and a faint
4490
- illustration cannot meet a text contrast ratio without ceasing to be faint.
4491
- Hidden from the accessibility tree, it is neither announced nor measured.
4506
+ Decoration, and marked as such: the sentence below says everything this glyph
4507
+ says, so it is hidden from the accessibility tree. axe still measures its
4508
+ contrast, so its colour passes AA like any other text.
4492
4509
  -->
4493
4510
  <mat-icon aria-hidden="true">widgets</mat-icon>
4494
4511
  <p>{{ ui.text('canvasEmpty') }}</p>
@@ -4573,8 +4590,8 @@ class RuleDependencyGraphComponent {
4573
4590
  this.dismiss.emit();
4574
4591
  }
4575
4592
  }
4576
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleDependencyGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4577
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: RuleDependencyGraphComponent, isStandalone: true, selector: "ngx-rule-dependency-graph", outputs: { dismiss: "dismiss", selectField: "selectField" }, ngImport: i0, template: `
4593
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleDependencyGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4594
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RuleDependencyGraphComponent, isStandalone: true, selector: "ngx-rule-dependency-graph", outputs: { dismiss: "dismiss", selectField: "selectField" }, ngImport: i0, template: `
4578
4595
  <div class="deb-graph-backdrop" (click)="onBackdropClick($event)">
4579
4596
  <div class="deb-graph-dialog" role="dialog" aria-modal="true" data-testid="rule-graph-dialog">
4580
4597
  <div class="deb-graph-header">
@@ -4743,7 +4760,7 @@ class RuleDependencyGraphComponent {
4743
4760
  </div>
4744
4761
  `, isInline: true, styles: [".deb-graph-backdrop{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:1000;padding:24px;animation:debFadeIn .15s ease-out}@keyframes debFadeIn{0%{opacity:0}to{opacity:1}}.deb-graph-dialog{background:var(--deb-surface, #ffffff);border:1px solid var(--deb-border, #e2e8f0);border-radius:14px;width:100%;max-width:960px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 25px 50px -12px #00000040;overflow:hidden}.deb-graph-header{display:flex;align-items:flex-start;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--deb-border, #e2e8f0)}.deb-graph-header__title-row{display:flex;align-items:center;gap:10px}.deb-graph-icon{color:var(--deb-accent, #6366f1);font-size:26px;width:26px;height:26px}.deb-graph-title{font-size:18px;font-weight:700;color:#0f172a;margin:0}.deb-graph-subtitle{font-size:13px;color:var(--deb-muted, #64748b);margin:4px 0 0}.deb-graph-metrics{display:flex;gap:12px;padding:12px 24px;background:#f8fafc;border-bottom:1px solid var(--deb-border, #e2e8f0);flex-wrap:wrap}.deb-metric-pill{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:9999px;font-size:12px;font-weight:600;border:1px solid transparent}.deb-metric-pill mat-icon{font-size:16px;width:16px;height:16px}.deb-metric-pill--rule{background:#eef2ff;color:#4f46e5;border-color:#c7d2fe}.deb-metric-pill--show-when{background:#f0fdf4;color:#16a34a;border-color:#bbf7d0}.deb-metric-pill--cascade{background:#fefce8;color:#ca8a04;border-color:#fef08a}.deb-metric-pill--patch{background:#fdf2f8;color:#db2777;border-color:#fbcfe8}.deb-graph-controls{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 24px;border-bottom:1px solid var(--deb-border, #e2e8f0);flex-wrap:wrap}.deb-graph-search{flex:1;min-width:240px}.deb-graph-filter-types{display:flex;gap:6px;flex-wrap:wrap}.deb-filter-btn{padding:5px 12px;border-radius:6px;border:1px solid var(--deb-border, #e2e8f0);background:#fff;font-size:12px;font-weight:500;color:#475569;cursor:pointer;transition:all .15s ease}.deb-filter-btn:hover{background:#f1f5f9;color:#0f172a}.deb-filter-btn--active{background:var(--deb-accent, #6366f1);color:#fff;border-color:var(--deb-accent, #6366f1)}.deb-filter-btn--active:hover{background:var(--deb-accent-hover, #4f46e5)}.deb-graph-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:12px}.deb-graph-empty{text-align:center;padding:48px 16px;color:var(--deb-muted, #64748b)}.deb-graph-empty mat-icon{font-size:40px;width:40px;height:40px;margin-bottom:8px;opacity:.5}.deb-edge-card{display:grid;grid-template-columns:220px 1fr 220px;align-items:center;gap:16px;padding:12px 16px;background:#fff;border:1px solid var(--deb-border, #e2e8f0);border-radius:10px;transition:box-shadow .15s ease,border-color .15s ease}.deb-edge-card:hover{border-color:#cbd5e1;box-shadow:0 4px 6px -1px #0000000d}.deb-node-chip{display:flex;flex-direction:column;align-items:flex-start;padding:8px 12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;text-align:left;width:100%;transition:background .15s ease,border-color .15s ease}.deb-node-chip:hover{background:#f1f5f9;border-color:var(--deb-accent, #6366f1)}.deb-node-chip__icon{font-size:16px;width:16px;height:16px;color:var(--deb-accent, #6366f1);margin-bottom:2px}.deb-node-chip__label{font-size:13px;font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}.deb-node-chip__key{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;color:var(--deb-muted, #64748b);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}.deb-edge-flow{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;text-align:center}.deb-edge-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 8px;border-radius:4px}.deb-edge-badge--rule{background:#eef2ff;color:#4f46e5}.deb-edge-badge--show-when{background:#f0fdf4;color:#16a34a}.deb-edge-badge--cascade{background:#fefce8;color:#a16207}.deb-edge-badge--patch{background:#fdf2f8;color:#be185d}.deb-edge-arrow{display:inline-flex;align-items:center;gap:6px;color:#475569}.deb-edge-summary{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;background:#f1f5f9;padding:2px 8px;border-radius:4px}.deb-edge-arrow-icon{font-size:16px;width:16px;height:16px;color:var(--deb-muted, #64748b)}.deb-edge-desc{font-size:11px;color:var(--deb-muted, #64748b)}.deb-graph-footer{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-top:1px solid var(--deb-border, #e2e8f0);background:#f8fafc}.deb-graph-count{font-size:13px;color:var(--deb-muted, #64748b)}@media(max-width:768px){.deb-edge-card{grid-template-columns:1fr;gap:8px}}\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: MatChipsModule }, { 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.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { 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: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4745
4762
  }
4746
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleDependencyGraphComponent, decorators: [{
4763
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleDependencyGraphComponent, decorators: [{
4747
4764
  type: Component,
4748
4765
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-dependency-graph', standalone: true, imports: [
4749
4766
  FormsModule,
@@ -5171,10 +5188,10 @@ class EntityBuilderComponent {
5171
5188
  .filter(Boolean)));
5172
5189
  this.store.setPermission(action, roles);
5173
5190
  }
5174
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5175
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", 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>&nbsp;{{ 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:#cbd5e1}.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 });
5191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5192
+ 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>&nbsp;{{ 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 });
5176
5193
  }
5177
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, decorators: [{
5194
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, decorators: [{
5178
5195
  type: Component,
5179
5196
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-entity-builder', standalone: true, providers: [BuilderStore], imports: [
5180
5197
  FormsModule,
@@ -5195,7 +5212,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
5195
5212
  FieldInspectorComponent,
5196
5213
  EntityBuilderCanvasComponent,
5197
5214
  RuleDependencyGraphComponent,
5198
- ], 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>&nbsp;{{ 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:#cbd5e1}.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"] }]
5215
+ ], 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>&nbsp;{{ 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"] }]
5199
5216
  }], ctorParameters: () => [], propDecorators: { config: [{
5200
5217
  type: Input
5201
5218
  }], rules: [{