ngx-dynamic-entity-builder 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,11 +8,11 @@ import * as i3$3 from '@angular/material/card';
8
8
  import { MatCardModule } from '@angular/material/card';
9
9
  import * as i3$2 from '@angular/material/checkbox';
10
10
  import { MatCheckboxModule } from '@angular/material/checkbox';
11
- import * as i4$2 from '@angular/material/expansion';
11
+ import * as i4$3 from '@angular/material/expansion';
12
12
  import { MatExpansionModule } from '@angular/material/expansion';
13
13
  import * as i4 from '@angular/material/form-field';
14
14
  import { MatFormFieldModule } from '@angular/material/form-field';
15
- import * as i5 from '@angular/material/icon';
15
+ import * as i4$1 from '@angular/material/icon';
16
16
  import { MatIconModule } from '@angular/material/icon';
17
17
  import * as i6 from '@angular/material/input';
18
18
  import { MatInputModule } from '@angular/material/input';
@@ -24,7 +24,7 @@ import * as i3$1 from '@angular/material/slide-toggle';
24
24
  import { MatSlideToggleModule } from '@angular/material/slide-toggle';
25
25
  import * as i11 from '@angular/material/toolbar';
26
26
  import { MatToolbarModule } from '@angular/material/toolbar';
27
- import * as i4$1 from '@angular/material/tooltip';
27
+ import * as i4$2 from '@angular/material/tooltip';
28
28
  import { MatTooltipModule } from '@angular/material/tooltip';
29
29
  import { parseFieldRef, toRefToken, collectFieldScopes, assignFieldRefs, normalizeConfigOptions, getFieldTypeMeta, createFieldConfig, labelToId, createFieldSnapshot, computeFieldDrift, OPTION_KEY, optionKeyOf, resolveLabel, humanizeId, findTab, fieldRefFor, refOf, resolveOptionLabel, FIELD_TYPE_CATALOG, COMMON_MODULES } from '@dynamic-entity/core';
30
30
  export { FIELD_TYPE_CATALOG, createFieldConfig, getFieldTypeMeta, humanizeId } from '@dynamic-entity/core';
@@ -35,6 +35,7 @@ import { MatDividerModule } from '@angular/material/divider';
35
35
  import { CommonModule } from '@angular/common';
36
36
  import * as i1$1 from '@angular/cdk/drag-drop';
37
37
  import { DragDropModule } from '@angular/cdk/drag-drop';
38
+ import { MatChipsModule } from '@angular/material/chips';
38
39
 
39
40
  /**
40
41
  * field-catalog.ts — re-exports shared field catalog from @dynamic-entity/core.
@@ -55,6 +56,95 @@ function deepClone(value) {
55
56
  return JSON.parse(JSON.stringify(value));
56
57
  }
57
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
+
58
148
  const clone = deepClone;
59
149
  const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
60
150
  /**
@@ -65,6 +155,26 @@ const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
65
155
  * un-ticked. Nothing writes it any more.
66
156
  */
67
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
+ }
68
178
  /**
69
179
  * Follow a field rename through a rule reference, in either spelling.
70
180
  *
@@ -112,129 +222,34 @@ class BuilderStore {
112
222
  /**
113
223
  * Roles of the person using the builder — distinct from `availableRoles`, which is the
114
224
  * vocabulary a schema may reference. Needed so the SYSTEM_DEFAULT_CAN_EDIT predicate can
115
- * be asked a real question; it was previously called with a hardcoded empty array, which
116
- * made any role-checking predicate answer false for everyone.
225
+ * be asked a real question rather than answering for an empty role list.
117
226
  */
118
227
  _userRoles = signal([], ...(ngDevMode ? [{ debugName: "_userRoles" }] : /* istanbul ignore next */ []));
119
228
  // ─── Undo / redo ────────────────────────────────────────────────────────────
120
229
  //
121
- // History is a list of {config, rules} pairs, because the builder's state is two signals
122
- // and undoing one without the other would leave a rule pointing at a field that no longer
123
- // exists.
124
- //
125
- // Entries hold **references, not clones**. `mutate` builds a new config and `mutateField`
126
- // shares structure along the unchanged path, so every snapshot already refers to immutable
127
- // objects and unchanged subtrees are shared between entries. That also removes the usual
128
- // re-entrancy problem for free: undo puts the stored object back, so the recording effect
129
- // sees the exact reference already sitting at the cursor and skips it. No suppression flag
130
- // to get out of step.
131
- history = [];
132
- cursor = signal(-1, ...(ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []));
133
- historyLength = signal(0, ...(ngDevMode ? [{ debugName: "historyLength" }] : /* istanbul ignore next */ []));
134
- lastEditAt = 0;
135
- /** Consecutive edits closer together than this fold into one undo step. */
136
- static COALESCE_MS = 400;
137
- canUndo = computed(() => this.cursor() > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
138
- canRedo = computed(() => this.cursor() < this.historyLength() - 1, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
139
- /**
140
- * Fold the current state into history.
141
- *
142
- * Called explicitly by the write paths rather than from an `effect`. An effect would have
143
- * been fewer call sites, but it requires an injection context: `BuilderStore` had no
144
- * constructor, so `new BuilderStore()` was legal, and adding one broke every caller that
145
- * did it with NG0203. Explicit calls also make the timing deterministic — a test can
146
- * assert straight after an edit instead of flushing effects first.
147
- *
148
- * Calling it twice for one operation is harmless: the second call sees the same signal
149
- * references already at the cursor and returns.
150
- *
151
- * `setFieldLabel` is bound to a keystroke, so recording every emission would make undo
152
- * walk back one character at a time. Two consecutive edits merge when they land inside
153
- * `COALESCE_MS` *and* the structure is unchanged — a rename coalesces, while adding,
154
- * removing or moving anything always earns its own step however fast it is clicked.
155
- */
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;
156
237
  record() {
157
- const config = this._config();
158
- const rules = this._rules();
159
- const at = this.cursor();
160
- const top = at >= 0 ? this.history[at] : undefined;
161
- if (top && top.config === config && top.rules === rules)
162
- return;
163
- const now = Date.now();
164
- const snapshot = { config, rules, shape: this.shapeOf(config, rules) };
165
- if (top &&
166
- now - this.lastEditAt < BuilderStore.COALESCE_MS &&
167
- top.shape === snapshot.shape &&
168
- at === this.history.length - 1) {
169
- this.history[at] = snapshot;
170
- this.lastEditAt = now;
171
- return;
172
- }
173
- // A new edit after an undo discards the redo branch, which is what every editor does.
174
- this.history.length = at + 1;
175
- this.history.push(snapshot);
176
- // Bounded, because a builder session is long and every keystroke that does not coalesce
177
- // earns an entry. Snapshots hold references and share unchanged subtrees, so an entry is
178
- // cheap — but unbounded is unbounded, and the oldest steps are the ones nobody walks
179
- // back to. Dropping from the front keeps the most recent 200.
180
- if (this.history.length > BuilderStore.MAX_HISTORY) {
181
- this.history.splice(0, this.history.length - BuilderStore.MAX_HISTORY);
182
- }
183
- this.cursor.set(this.history.length - 1);
184
- this.historyLength.set(this.history.length);
185
- this.lastEditAt = now;
186
- }
187
- /** How many undo steps are kept. */
188
- static MAX_HISTORY = 200;
189
- /** Counts that distinguish a structural edit from a value edit. */
190
- shapeOf(config, rules) {
191
- let fields = 0;
192
- let tabs = 0;
193
- const walkFields = (list) => {
194
- for (const f of list ?? []) {
195
- fields += 1;
196
- walkFields(f.children);
197
- }
198
- };
199
- const walkTabs = (list) => {
200
- for (const t of list ?? []) {
201
- tabs += 1;
202
- walkFields(t.fields);
203
- walkTabs(t.children);
204
- }
205
- };
206
- walkTabs(config.tabs);
207
- return `${tabs}:${fields}:${rules.length}`;
238
+ this.history.record({ config: this._config(), rules: this._rules() });
208
239
  }
209
240
  /** Start history again from the state just loaded. Nothing before it is undoable. */
210
241
  resetHistory() {
211
- this.history.length = 0;
212
- this.history.push({
213
- config: this._config(),
214
- rules: this._rules(),
215
- shape: this.shapeOf(this._config(), this._rules()),
216
- });
217
- this.cursor.set(0);
218
- this.historyLength.set(1);
219
- this.lastEditAt = 0;
242
+ this.history.reset({ config: this._config(), rules: this._rules() });
220
243
  }
221
244
  undo() {
222
- if (!this.canUndo())
223
- return;
224
- this.applyHistory(this.cursor() - 1);
245
+ this.restore(this.history.undo());
225
246
  }
226
247
  redo() {
227
- if (!this.canRedo())
228
- return;
229
- this.applyHistory(this.cursor() + 1);
248
+ this.restore(this.history.redo());
230
249
  }
231
- applyHistory(index) {
232
- const snapshot = this.history[index];
250
+ restore(snapshot) {
233
251
  if (!snapshot)
234
252
  return;
235
- this.cursor.set(index);
236
- // Order matters only in that both land before the effect runs; it then sees references
237
- // identical to this entry and records nothing.
238
253
  this._config.set(snapshot.config);
239
254
  this._rules.set(snapshot.rules);
240
255
  this._isDirty.set(true);
@@ -254,11 +269,8 @@ class BuilderStore {
254
269
  /**
255
270
  * Every field the config declares, sub-tabs included.
256
271
  *
257
- * This used to stop at top-level tabs, which quietly broke three things at once: nine of
258
- * `insuranceClaims`'s twenty-eight fields never appeared on the canvas, the entity-reference
259
- * picker could not offer a nested field as a source, and the drift check looked a nested
260
- * field up here, got nothing, and returned without checking. The store's structural
261
- * 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.
262
274
  *
263
275
  * It deliberately does not descend into a field's own `children`: `group` and `array`
264
276
  * children are rendered by the row that owns them, not as rows of their own.
@@ -388,10 +400,9 @@ class BuilderStore {
388
400
  /**
389
401
  * Every tab in the tree, flattened.
390
402
  *
391
- * The structural operations below used to iterate `draft.tabs` directly, which is only
392
- * the top level. A field on a sub-tab could be selected and edited but never removed,
393
- * duplicated, moved or reordered — the builder could author nested configs it was then
394
- * 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.
395
406
  */
396
407
  flattenTabs(tabs = []) {
397
408
  const list = [];
@@ -532,20 +543,16 @@ class BuilderStore {
532
543
  *
533
544
  * Ids are the wiring between fields — rules, cascades, patches and `showWhen` all address
534
545
  * fields by id — so a rename that only touches `field.id` silently breaks that wiring.
535
- * That was survivable while renaming was a rare manual act; label-derived ids make it
536
- * routine, so the references move with it.
537
- */
538
- /**
546
+ * Label-derived ids make renaming routine, so the references move with it.
547
+ *
539
548
  * `key` addresses the field to rename; `oldId` is the bare id every *reference* to it
540
- * carries. They were one parameter, which worked only while an id was unique across the
541
- * config: resolving needs the path, while `showWhen`, `parentField`, `autoPatch` targets
542
- * 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.
543
551
  */
544
552
  applyRename(key, oldId, newId) {
545
553
  // Selection is held as whatever the caller passed — `addField` stores a bare id while a
546
- // canvas click stores a path — so comparing the two strings said "not selected" for a
547
- // field that plainly was, and the inspector emptied itself mid-rename. Compare the
548
- // 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.
549
556
  const selectedKey = this._selectedFieldId();
550
557
  const tabsNow = this._config().tabs;
551
558
  const wasSelected = !!selectedKey && this.findFieldInTabs(tabsNow, selectedKey) === this.findFieldInTabs(tabsNow, key);
@@ -846,6 +853,21 @@ class BuilderStore {
846
853
  }
847
854
  });
848
855
  }
856
+ setPatternValidator(id, pattern) {
857
+ this.mutate(draft => {
858
+ const field = this.findFieldInTabs(draft.tabs, id);
859
+ if (!field)
860
+ return;
861
+ field.validators = field.validators ?? {};
862
+ const trimmed = pattern?.trim();
863
+ if (!trimmed) {
864
+ delete field.validators.pattern;
865
+ }
866
+ else {
867
+ field.validators.pattern = trimmed;
868
+ }
869
+ });
870
+ }
849
871
  getParamValidator(field, validator) {
850
872
  const val = field.validators?.[validator];
851
873
  return typeof val === 'number' ? val : null;
@@ -854,9 +876,8 @@ class BuilderStore {
854
876
  if (validator === 'required')
855
877
  return !!field.validators?.required;
856
878
  if (validator === 'email') {
857
- // This used to report *any* pattern as email, so writing a custom regex silently
858
- // ticked the box. The legacy encoding is still recognised so a config authored by the
859
- // old builder keeps showing Email as ticked.
879
+ // Only the legacy email regex counts, never any pattern: a custom regex must not tick
880
+ // the box. Recognising it keeps older configs showing Email as ticked.
860
881
  return !!field.validators?.email || field.validators?.pattern === LEGACY_EMAIL_PATTERN;
861
882
  }
862
883
  return false;
@@ -925,7 +946,7 @@ class BuilderStore {
925
946
  }
926
947
  });
927
948
  }
928
- /** Link a field to a field defined in another entity configuration (Phase 8). */
949
+ /** Link a field to a field defined in another entity configuration */
929
950
  linkReferencedField(fieldId, sourceEntityKey, sourceField) {
930
951
  this.mutate(draft => {
931
952
  const field = this.findFieldInTabs(draft.tabs, fieldId);
@@ -1654,10 +1675,10 @@ class BuilderStore {
1654
1675
  }
1655
1676
  return problems;
1656
1677
  }
1657
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1658
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderStore });
1678
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1679
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore });
1659
1680
  }
1660
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderStore, decorators: [{
1681
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore, decorators: [{
1661
1682
  type: Injectable
1662
1683
  }] });
1663
1684
 
@@ -1699,6 +1720,177 @@ function withExistingOptions(options, current) {
1699
1720
  return [...options, ...extra];
1700
1721
  }
1701
1722
 
1723
+ /**
1724
+ * Computes all dependency edges (rules, showWhen, cascades, patchOnTrue, autoPatch)
1725
+ * across an EntityFormConfig and its rules.
1726
+ */
1727
+ function computeRuleDependencies(config, rules = [], language = 'en') {
1728
+ const edges = [];
1729
+ let edgeIdSeq = 0;
1730
+ const scopes = collectFieldScopes(config);
1731
+ const byKey = new Map();
1732
+ const byId = new Map();
1733
+ const byToken = new Map();
1734
+ for (const entry of scopes) {
1735
+ const key = refOf(entry.field, entry.scope);
1736
+ const token = toRefToken(key);
1737
+ const label = resolveLabel(entry.field.label, language) || entry.field.id;
1738
+ const meta = { key, id: entry.field.id, label, field: entry.field };
1739
+ byKey.set(key, meta);
1740
+ byId.set(entry.field.id, meta);
1741
+ byToken.set(token, meta);
1742
+ }
1743
+ function resolveRef(refStr) {
1744
+ if (!refStr)
1745
+ return { key: '', label: '—', valid: false };
1746
+ const parsed = parseFieldRef(refStr);
1747
+ const val = parsed.value;
1748
+ const meta = byKey.get(refStr) ??
1749
+ byKey.get(val) ??
1750
+ byId.get(val) ??
1751
+ byToken.get(refStr) ??
1752
+ byToken.get(toRefToken(val));
1753
+ if (meta) {
1754
+ return { key: meta.key, label: meta.label, valid: true };
1755
+ }
1756
+ return { key: val || refStr, label: val || refStr, valid: false };
1757
+ }
1758
+ let totalRules = 0;
1759
+ let totalShowWhen = 0;
1760
+ let totalCascades = 0;
1761
+ let totalPatches = 0;
1762
+ // 1. Form Rules
1763
+ for (const rule of rules) {
1764
+ if (!rule.fieldId)
1765
+ continue;
1766
+ totalRules++;
1767
+ const source = resolveRef(rule.fieldId);
1768
+ const targets = rule.targets ?? [];
1769
+ const actionType = rule.action?.type ?? 'action';
1770
+ let actionSummary = actionType;
1771
+ if (actionType === 'visibility') {
1772
+ actionSummary = rule.action?.value === false || rule.action?.value === 'false' ? 'hide' : 'show';
1773
+ }
1774
+ else if (actionType === 'validation' || actionType === 'validationMessage') {
1775
+ actionSummary = 'validate';
1776
+ }
1777
+ else if (actionType === 'info' || actionType === 'infoBanner') {
1778
+ actionSummary = 'info';
1779
+ }
1780
+ const conditionsText = rule.conditions && rule.conditions.length > 0
1781
+ ? rule.conditions.map(c => `${c.operator ?? 'EQUAL'} ${c.value ?? ''}`.trim()).join(' & ')
1782
+ : 'always';
1783
+ for (const target of targets) {
1784
+ const targetResolved = resolveRef(target.id);
1785
+ edges.push({
1786
+ id: `rule-${++edgeIdSeq}`,
1787
+ sourceKey: source.key,
1788
+ sourceLabel: source.label,
1789
+ targetKey: targetResolved.key,
1790
+ targetLabel: targetResolved.label,
1791
+ type: 'rule',
1792
+ actionSummary,
1793
+ description: `${actionSummary.toUpperCase()} when ${conditionsText}`,
1794
+ sourceValid: source.valid,
1795
+ targetValid: targetResolved.valid,
1796
+ });
1797
+ }
1798
+ }
1799
+ // 2. Field-level features: showWhen, cascade, patchOnTrue, autoPatch
1800
+ for (const entry of scopes) {
1801
+ const field = entry.field;
1802
+ const fieldKey = refOf(field, entry.scope);
1803
+ const fieldResolved = resolveRef(fieldKey);
1804
+ // showWhen
1805
+ if (field.showWhen && typeof field.showWhen === 'object') {
1806
+ for (const [watchedRef, expectedVal] of Object.entries(field.showWhen)) {
1807
+ totalShowWhen++;
1808
+ const source = resolveRef(watchedRef);
1809
+ const valStr = typeof expectedVal === 'object' ? JSON.stringify(expectedVal) : String(expectedVal);
1810
+ edges.push({
1811
+ id: `showWhen-${++edgeIdSeq}`,
1812
+ sourceKey: source.key,
1813
+ sourceLabel: source.label,
1814
+ targetKey: fieldResolved.key,
1815
+ targetLabel: fieldResolved.label,
1816
+ type: 'showWhen',
1817
+ actionSummary: `= ${valStr}`,
1818
+ description: `Visible when ${source.label} = ${valStr}`,
1819
+ sourceValid: source.valid,
1820
+ targetValid: fieldResolved.valid,
1821
+ });
1822
+ }
1823
+ }
1824
+ // cascade parentField
1825
+ if (field.entityReference?.parentField) {
1826
+ totalCascades++;
1827
+ const source = resolveRef(field.entityReference.parentField);
1828
+ edges.push({
1829
+ id: `cascade-${++edgeIdSeq}`,
1830
+ sourceKey: source.key,
1831
+ sourceLabel: source.label,
1832
+ targetKey: fieldResolved.key,
1833
+ targetLabel: fieldResolved.label,
1834
+ type: 'cascade',
1835
+ actionSummary: 'filters options',
1836
+ description: `Reloads options when ${source.label} changes`,
1837
+ sourceValid: source.valid,
1838
+ targetValid: fieldResolved.valid,
1839
+ });
1840
+ }
1841
+ // patchOnTrue
1842
+ if (Array.isArray(field.patchOnTrue)) {
1843
+ for (const mapping of field.patchOnTrue) {
1844
+ if (!mapping.to)
1845
+ continue;
1846
+ totalPatches++;
1847
+ const toResolved = resolveRef(mapping.to);
1848
+ const fromResolved = resolveRef(mapping.from);
1849
+ edges.push({
1850
+ id: `patch-${++edgeIdSeq}`,
1851
+ sourceKey: fieldResolved.key,
1852
+ sourceLabel: fieldResolved.label,
1853
+ targetKey: toResolved.key,
1854
+ targetLabel: toResolved.label,
1855
+ type: 'patchOnTrue',
1856
+ actionSummary: `copy ${fromResolved.label} → ${toResolved.label}`,
1857
+ description: `When true, copies ${fromResolved.label} to ${toResolved.label}`,
1858
+ sourceValid: fieldResolved.valid,
1859
+ targetValid: toResolved.valid,
1860
+ });
1861
+ }
1862
+ }
1863
+ // autoPatch
1864
+ if (field.autoPatch && Array.isArray(field.autoPatch.mappings)) {
1865
+ for (const mapping of field.autoPatch.mappings) {
1866
+ if (!mapping.target)
1867
+ continue;
1868
+ totalPatches++;
1869
+ const targetResolved = resolveRef(mapping.target);
1870
+ edges.push({
1871
+ id: `autopatch-${++edgeIdSeq}`,
1872
+ sourceKey: fieldResolved.key,
1873
+ sourceLabel: fieldResolved.label,
1874
+ targetKey: targetResolved.key,
1875
+ targetLabel: targetResolved.label,
1876
+ type: 'autoPatch',
1877
+ actionSummary: `patch → ${targetResolved.label}`,
1878
+ description: `Selection copies ${mapping.source} to ${targetResolved.label}`,
1879
+ sourceValid: fieldResolved.valid,
1880
+ targetValid: targetResolved.valid,
1881
+ });
1882
+ }
1883
+ }
1884
+ }
1885
+ return {
1886
+ edges,
1887
+ totalRules,
1888
+ totalShowWhen,
1889
+ totalCascades,
1890
+ totalPatches,
1891
+ };
1892
+ }
1893
+
1702
1894
  /**
1703
1895
  * Every word the builder puts on screen that is not part of the config being authored.
1704
1896
  *
@@ -1790,8 +1982,6 @@ const DEFAULT_BUILDER_TEXT = {
1790
1982
  hintHint: 'An info icon beside the label, shown on hover and while the field has focus. Read out with the field.',
1791
1983
  defaultValue: 'Default value',
1792
1984
  // Field inspector — section headings and the layout control.
1793
- // `validation` used to be the heading above the validator checkboxes; the inspector's
1794
- // sections now own their headings, so `sectionValidation` replaces it.
1795
1985
  sectionBasics: 'Basics',
1796
1986
  sectionLayout: 'Layout',
1797
1987
  sectionValidation: 'Validation',
@@ -1801,6 +1991,7 @@ const DEFAULT_BUILDER_TEXT = {
1801
1991
  sectionAutomation: 'Automation',
1802
1992
  sectionRules: 'Rules',
1803
1993
  sectionReference: 'Reference',
1994
+ sectionDependencies: 'Dependencies',
1804
1995
  width: 'Width',
1805
1996
  widthQuarter: '¼',
1806
1997
  widthThird: '⅓',
@@ -1927,6 +2118,33 @@ const DEFAULT_BUILDER_TEXT = {
1927
2118
  driftHeading: 'Source field definition has drifted!',
1928
2119
  driftBody: 'The upstream field configuration in "{entity}" has evolved.',
1929
2120
  syncWithSource: 'Sync with Source',
2121
+ // Validation pattern playground
2122
+ pattern: 'Validation Pattern (Regex)',
2123
+ patternHint: 'Regular expression for format validation.',
2124
+ testPattern: 'Test Pattern',
2125
+ testPatternPlaceholder: 'Type sample input to test…',
2126
+ patternMatch: 'Pattern matches',
2127
+ patternNoMatch: 'Pattern does not match',
2128
+ patternInvalid: 'Invalid regex syntax',
2129
+ patternEmptyPrompt: 'Enter regex above to test sample input',
2130
+ quickPresets: 'Presets',
2131
+ // Field dependencies
2132
+ dependsOn: 'Depends on',
2133
+ affects: 'Affects',
2134
+ noDependencies: 'No active rule or field dependencies for this field.',
2135
+ // Rule graph
2136
+ ruleGraph: 'Rule Graph',
2137
+ ruleGraphTitle: 'Rule & Dependency Graph',
2138
+ ruleGraphSubtitle: 'Schema-wide rule triggers, conditional visibility, and cascades',
2139
+ filterDependencies: 'Filter dependencies…',
2140
+ noDependenciesFound: 'No dependencies matching current filter.',
2141
+ jumpToField: 'Select {field}',
2142
+ closeDialog: 'Close',
2143
+ dependencyTypeRule: 'Rule',
2144
+ dependencyTypeShowWhen: 'Show When',
2145
+ dependencyTypeCascade: 'Cascade',
2146
+ dependencyTypePatch: 'Copy on True',
2147
+ totalDependencies: '{count} dependencies',
1930
2148
  };
1931
2149
  /**
1932
2150
  * Overrides for the builder's own chrome.
@@ -1960,10 +2178,10 @@ class BuilderTextService {
1960
2178
  text(key, params) {
1961
2179
  return resolveUiText(this.overrides, DEFAULT_BUILDER_TEXT, key, this.language(), params);
1962
2180
  }
1963
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1964
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, providedIn: 'root' });
2181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2182
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, providedIn: 'root' });
1965
2183
  }
1966
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, decorators: [{
2184
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, decorators: [{
1967
2185
  type: Injectable,
1968
2186
  args: [{ providedIn: 'root' }]
1969
2187
  }] });
@@ -2053,8 +2271,8 @@ class EntityReferenceConfigComponent {
2053
2271
  this.filtersParseError.set('Invalid JSON.');
2054
2272
  }
2055
2273
  }
2056
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityReferenceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2057
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
2274
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2275
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
2058
2276
  @if (field(); as f) {
2059
2277
  <div class="deb-entity-ref">
2060
2278
  <span class="deb-section-title">{{ ui.text('entityReference') }}</span>
@@ -2212,9 +2430,9 @@ class EntityReferenceConfigComponent {
2212
2430
  }
2213
2431
  </div>
2214
2432
  }
2215
- `, 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: i5.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$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2433
+ `, 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 });
2216
2434
  }
2217
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityReferenceConfigComponent, decorators: [{
2435
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceConfigComponent, decorators: [{
2218
2436
  type: Component,
2219
2437
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-entity-reference-config', standalone: true, imports: [
2220
2438
  FormsModule,
@@ -2404,6 +2622,8 @@ class RuleFormComponent {
2404
2622
  priority: 1,
2405
2623
  };
2406
2624
  save = new EventEmitter();
2625
+ // Published API since 1.x: renaming it would break every host bound to `(cancel)`.
2626
+ // eslint-disable-next-line @angular-eslint/no-output-native
2407
2627
  cancel = new EventEmitter();
2408
2628
  operators = [
2409
2629
  'EQUAL',
@@ -2469,8 +2689,8 @@ class RuleFormComponent {
2469
2689
  removeCondition(index) {
2470
2690
  this.rule.conditions.splice(index, 1);
2471
2691
  }
2472
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2473
- 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: `
2692
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2693
+ 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: `
2474
2694
  <div class="deb-rule-form">
2475
2695
  <h3 class="deb-rule-form__title">{{ ui.text(rule.id ? 'editRuleTitle' : 'newRuleTitle') }}</h3>
2476
2696
 
@@ -2549,9 +2769,9 @@ class RuleFormComponent {
2549
2769
  <button mat-raised-button color="primary" type="button" (click)="save.emit(rule)">{{ ui.text('saveRule') }}</button>
2550
2770
  </div>
2551
2771
  </div>
2552
- `, 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: i5.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 });
2772
+ `, 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 });
2553
2773
  }
2554
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, decorators: [{
2774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleFormComponent, decorators: [{
2555
2775
  type: Component,
2556
2776
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-form', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule], template: `
2557
2777
  <div class="deb-rule-form">
@@ -2695,8 +2915,8 @@ class FieldRulesListComponent {
2695
2915
  : `${rule.action.type}: ${rule.action.value}`;
2696
2916
  return `${rule.fieldId} ${cond} → ${action}`;
2697
2917
  }
2698
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldRulesListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2699
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
2918
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRulesListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2919
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
2700
2920
  <div class="deb-rules">
2701
2921
  <div class="deb-rules__head">
2702
2922
  <span class="deb-section-title">{{ ui.text('rules') }}</span>
@@ -2777,9 +2997,9 @@ class FieldRulesListComponent {
2777
2997
  </div>
2778
2998
  }
2779
2999
  </div>
2780
- `, 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: i5.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$1.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 });
3000
+ `, 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 });
2781
3001
  }
2782
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldRulesListComponent, decorators: [{
3002
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRulesListComponent, decorators: [{
2783
3003
  type: Component,
2784
3004
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
2785
3005
  <div class="deb-rules">
@@ -2905,10 +3125,8 @@ class ReferencedFieldConfigComponent {
2905
3125
  }
2906
3126
  }
2907
3127
  /**
2908
- * Both callers pass the id of the field they just edited. This used to ignore that
2909
- * argument and read `store.selectedField()` instead, so whenever the edited field was not
2910
- * the selected one it checked drift against the wrong field's source entity — or bailed
2911
- * out entirely if nothing was selected.
3128
+ * Takes the id of the field just edited rather than reading `store.selectedField()`, which
3129
+ * need not be the same field — or any field at all.
2912
3130
  */
2913
3131
  async checkDriftForField(fieldId) {
2914
3132
  const f = this.store.fields().find(field => field.id === fieldId);
@@ -2957,8 +3175,8 @@ class ReferencedFieldConfigComponent {
2957
3175
  }
2958
3176
  return undefined;
2959
3177
  }
2960
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ReferencedFieldConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2961
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: ReferencedFieldConfigComponent, isStandalone: true, selector: "ngx-referenced-field-config", ngImport: i0, template: `
3178
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ReferencedFieldConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3179
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ReferencedFieldConfigComponent, isStandalone: true, selector: "ngx-referenced-field-config", ngImport: i0, template: `
2962
3180
  @if (field(); as f) {
2963
3181
  <div class="deb-referenced-field" style="margin-top: 16px;">
2964
3182
  <div class="deb-row" style="justify-content: space-between; align-items: center; margin-bottom: 8px;">
@@ -3017,9 +3235,9 @@ class ReferencedFieldConfigComponent {
3017
3235
  }
3018
3236
  </div>
3019
3237
  }
3020
- `, 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: i5.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 });
3238
+ `, 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 });
3021
3239
  }
3022
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: ReferencedFieldConfigComponent, decorators: [{
3240
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ReferencedFieldConfigComponent, decorators: [{
3023
3241
  type: Component,
3024
3242
  args: [{
3025
3243
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -3228,9 +3446,7 @@ class FieldInspectorComponent {
3228
3446
  }
3229
3447
  /**
3230
3448
  * Seeds a new condition with the first field not already watched.
3231
- *
3232
- * It used to seed the literal string `field`, which is not a field id at all — so a new
3233
- * condition started out referencing nothing and silently hid the field until it was edited.
3449
+ * A seed that names no real field would silently hide this one until edited.
3234
3450
  */
3235
3451
  addShowWhen(field) {
3236
3452
  const next = { ...(field.showWhen ?? {}) };
@@ -3290,10 +3506,65 @@ class FieldInspectorComponent {
3290
3506
  return JSON.stringify(value);
3291
3507
  return String(value);
3292
3508
  }
3293
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3294
- 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 What is open by default is what an author sets when they add a field \u2014 name it, size it,\n mark it required, give a choice field its options. The advanced half starts closed, but\n opens itself as soon as *this* field uses it: a field with two rules is not a field whose\n rules are advanced, and hiding what is already configured is how a setting gets forgotten\n about. A closed section still reports itself through the count in its heading.\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 @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\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 </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\" [attr.open]=\"displayFlagCount(f) ? '' : null\">\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\" [attr.open]=\"showWhenEntries(f).length ? '' : null\">\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\" [attr.open]=\"meta()?.isEntityRef || (f.patchOnTrue ?? []).length ? '' : null\">\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\" [attr.open]=\"store.rulesForSelectedField().length ? '' : null\">\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 <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" [attr.open]=\"f.isReferenced || f.hasDrift ? '' : null\">\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}\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: i5.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$1.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" }] });
3509
+ // ─── Pattern Playground ────────────────────────────────────────────────────
3510
+ testSample = signal('', ...(ngDevMode ? [{ debugName: "testSample" }] : /* istanbul ignore next */ []));
3511
+ regexEvaluation = computed(() => {
3512
+ const f = this.field();
3513
+ const pattern = f?.validators?.pattern?.trim();
3514
+ if (!pattern)
3515
+ return { status: 'empty' };
3516
+ let regex;
3517
+ try {
3518
+ regex = new RegExp(pattern);
3519
+ }
3520
+ catch (e) {
3521
+ return { status: 'invalid', error: e.message };
3522
+ }
3523
+ const sample = this.testSample();
3524
+ if (!sample)
3525
+ return { status: 'idle' };
3526
+ return { status: regex.test(sample) ? 'match' : 'no-match' };
3527
+ }, ...(ngDevMode ? [{ debugName: "regexEvaluation" }] : /* istanbul ignore next */ []));
3528
+ patternPresets = [
3529
+ { label: 'Alpha', pattern: '^[a-zA-Z]+$' },
3530
+ { label: 'Alphanumeric', pattern: '^[a-zA-Z0-9]+$' },
3531
+ { label: 'Digits', pattern: '^\\d+$' },
3532
+ { label: 'Postal Code', pattern: '^\\d{5}(-\\d{4})?$' },
3533
+ { label: 'Phone', pattern: '^\\+?[1-9]\\d{1,14}$' },
3534
+ { label: 'Slug', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
3535
+ ];
3536
+ supportsPattern(type) {
3537
+ return ['text', 'textarea', 'markdown', 'email', 'password'].includes(type);
3538
+ }
3539
+ applyPreset(field, pattern) {
3540
+ this.store.setPatternValidator(this.store.keyOf(field), pattern);
3541
+ }
3542
+ // ─── Dependencies ──────────────────────────────────────────────────────────
3543
+ allDependencies = computed(() => computeRuleDependencies(this.store.config(), this.store.rules(), this.lang()), ...(ngDevMode ? [{ debugName: "allDependencies" }] : /* istanbul ignore next */ []));
3544
+ incomingDependencies = computed(() => {
3545
+ const f = this.field();
3546
+ if (!f)
3547
+ return [];
3548
+ const key = this.store.keyOf(f);
3549
+ const id = f.id;
3550
+ return this.allDependencies().edges.filter(e => e.targetKey === key || e.targetKey === id);
3551
+ }, ...(ngDevMode ? [{ debugName: "incomingDependencies" }] : /* istanbul ignore next */ []));
3552
+ outgoingDependencies = computed(() => {
3553
+ const f = this.field();
3554
+ if (!f)
3555
+ return [];
3556
+ const key = this.store.keyOf(f);
3557
+ const id = f.id;
3558
+ return this.allDependencies().edges.filter(e => e.sourceKey === key || e.sourceKey === id);
3559
+ }, ...(ngDevMode ? [{ debugName: "outgoingDependencies" }] : /* istanbul ignore next */ []));
3560
+ selectField(key) {
3561
+ if (key)
3562
+ this.store.selectField(key);
3563
+ }
3564
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3565
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('condition') }}\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('mapping') }}\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: EntityReferenceConfigComponent, selector: "ngx-entity-reference-config" }, { kind: "component", type: FieldRulesListComponent, selector: "ngx-field-rules-list" }, { kind: "component", type: ReferencedFieldConfigComponent, selector: "ngx-referenced-field-config" }] });
3295
3566
  }
3296
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, decorators: [{
3567
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, decorators: [{
3297
3568
  type: Component,
3298
3569
  args: [{ selector: 'ngx-field-inspector', standalone: true, imports: [
3299
3570
  FormsModule,
@@ -3309,7 +3580,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3309
3580
  EntityReferenceConfigComponent,
3310
3581
  FieldRulesListComponent,
3311
3582
  ReferencedFieldConfigComponent,
3312
- ], 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 What is open by default is what an author sets when they add a field \u2014 name it, size it,\n mark it required, give a choice field its options. The advanced half starts closed, but\n opens itself as soon as *this* field uses it: a field with two rules is not a field whose\n rules are advanced, and hiding what is already configured is how a setting gets forgotten\n about. A closed section still reports itself through the count in its heading.\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 @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\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 </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\" [attr.open]=\"displayFlagCount(f) ? '' : null\">\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\" [attr.open]=\"showWhenEntries(f).length ? '' : null\">\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\" [attr.open]=\"meta()?.isEntityRef || (f.patchOnTrue ?? []).length ? '' : null\">\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\" [attr.open]=\"store.rulesForSelectedField().length ? '' : null\">\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 <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" [attr.open]=\"f.isReferenced || f.hasDrift ? '' : null\">\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}\n"] }]
3583
+ ], template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('condition') }}\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('mapping') }}\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"] }]
3313
3584
  }] });
3314
3585
 
3315
3586
  /**
@@ -3356,11 +3627,8 @@ const GROUP_TEXT_KEY = {
3356
3627
  /**
3357
3628
  * FieldPaletteComponent — the list of buildable field types.
3358
3629
  *
3359
- * Two things changed the day this stopped being a flat grid of twenty-two buttons. The labels
3360
- * were clipped mid-word — "Boolean Toggl", "Entity Referen", "File Attachmen" — because the
3361
- * grid forced two equal columns and the button refused to wrap, so a third of the palette
3362
- * could not be read. And with no order to them, finding "Month & Year" meant scanning the
3363
- * whole list: the grouping and the filter are what make a palette this size navigable.
3630
+ * Grouped and filterable, because a flat list of twenty-two types is not navigable, and
3631
+ * tiles wrap their labels so names like "Entity Reference" are never clipped.
3364
3632
  */
3365
3633
  class FieldPaletteComponent {
3366
3634
  /** Builder chrome, overridable via BUILDER_TEXT. */
@@ -3389,8 +3657,8 @@ class FieldPaletteComponent {
3389
3657
  })).filter(section => section.items.length > 0);
3390
3658
  }, ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
3391
3659
  pick = new EventEmitter();
3392
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3393
- 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: `
3660
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3661
+ 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: `
3394
3662
  <div class="deb-palette">
3395
3663
  <div class="deb-palette__search">
3396
3664
  <mat-icon aria-hidden="true">search</mat-icon>
@@ -3438,9 +3706,9 @@ class FieldPaletteComponent {
3438
3706
  <p class="deb-hint" data-testid="palette-no-match">{{ ui.text('paletteNoMatch', { query: query() }) }}</p>
3439
3707
  }
3440
3708
  </div>
3441
- `, 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: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3709
+ `, 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 });
3442
3710
  }
3443
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldPaletteComponent, decorators: [{
3711
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldPaletteComponent, decorators: [{
3444
3712
  type: Component,
3445
3713
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-palette', standalone: true, imports: [MatButtonModule, MatIconModule, MatTooltipModule], template: `
3446
3714
  <div class="deb-palette">
@@ -3522,8 +3790,8 @@ class TabManagerComponent {
3522
3790
  }
3523
3791
  return true;
3524
3792
  }
3525
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TabManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3526
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
3793
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TabManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3794
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
3527
3795
  <div class="deb-tabs">
3528
3796
  <div class="deb-tabs__head">
3529
3797
  <span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
@@ -3667,9 +3935,9 @@ class TabManagerComponent {
3667
3935
  </div>
3668
3936
  }
3669
3937
  </div>
3670
- `, 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: i5.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$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3938
+ `, 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 });
3671
3939
  }
3672
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: TabManagerComponent, decorators: [{
3940
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TabManagerComponent, decorators: [{
3673
3941
  type: Component,
3674
3942
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-tab-manager', standalone: true, imports: [
3675
3943
  FormsModule,
@@ -3844,8 +4112,8 @@ class EntityBuilderTreeNodeComponent {
3844
4112
  const lang = this.store.activeLanguage();
3845
4113
  return resolveLabel(field.label, lang) || field.id;
3846
4114
  }
3847
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3848
- 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: `
4115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4116
+ 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: `
3849
4117
  <div
3850
4118
  cdkDrag
3851
4119
  class="deb-field-row"
@@ -3959,9 +4227,9 @@ class EntityBuilderTreeNodeComponent {
3959
4227
  }
3960
4228
  </div>
3961
4229
  }
3962
- `, 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: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4230
+ `, 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 });
3963
4231
  }
3964
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderTreeNodeComponent, decorators: [{
4232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderTreeNodeComponent, decorators: [{
3965
4233
  type: Component,
3966
4234
  args: [{
3967
4235
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4113,8 +4381,8 @@ class EntityBuilderCanvasComponent {
4113
4381
  onDrop(tabId, event) {
4114
4382
  this.store.reorderField(event.previousIndex, event.currentIndex, tabId);
4115
4383
  }
4116
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4117
- 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: `
4384
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4385
+ 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: `
4118
4386
  <mat-card class="deb-card deb-card--canvas">
4119
4387
  <mat-card-header>
4120
4388
  <mat-card-title class="deb-card__title">
@@ -4139,10 +4407,9 @@ class EntityBuilderCanvasComponent {
4139
4407
  @if (store.fields().length === 0) {
4140
4408
  <div class="deb-empty">
4141
4409
  <!--
4142
- Decoration, and marked as such. The sentence below says everything this glyph
4143
- says, so a reader hearing "widgets" before it learns nothing; and a faint
4144
- illustration cannot meet a text contrast ratio without ceasing to be faint.
4145
- Hidden from the accessibility tree, it is neither announced nor measured.
4410
+ Decoration, and marked as such: the sentence below says everything this glyph
4411
+ says, so it is hidden from the accessibility tree. axe still measures its
4412
+ contrast, so its colour passes AA like any other text.
4146
4413
  -->
4147
4414
  <mat-icon aria-hidden="true">widgets</mat-icon>
4148
4415
  <p>{{ ui.text('canvasEmpty') }}</p>
@@ -4173,9 +4440,9 @@ class EntityBuilderCanvasComponent {
4173
4440
  }
4174
4441
  </mat-card-content>
4175
4442
  </mat-card>
4176
- `, 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: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$1.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 });
4443
+ `, 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 });
4177
4444
  }
4178
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderCanvasComponent, decorators: [{
4445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderCanvasComponent, decorators: [{
4179
4446
  type: Component,
4180
4447
  args: [{
4181
4448
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -4215,10 +4482,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
4215
4482
  @if (store.fields().length === 0) {
4216
4483
  <div class="deb-empty">
4217
4484
  <!--
4218
- Decoration, and marked as such. The sentence below says everything this glyph
4219
- says, so a reader hearing "widgets" before it learns nothing; and a faint
4220
- illustration cannot meet a text contrast ratio without ceasing to be faint.
4221
- Hidden from the accessibility tree, it is neither announced nor measured.
4485
+ Decoration, and marked as such: the sentence below says everything this glyph
4486
+ says, so it is hidden from the accessibility tree. axe still measures its
4487
+ contrast, so its colour passes AA like any other text.
4222
4488
  -->
4223
4489
  <mat-icon aria-hidden="true">widgets</mat-icon>
4224
4490
  <p>{{ ui.text('canvasEmpty') }}</p>
@@ -4255,6 +4521,408 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
4255
4521
  type: Output
4256
4522
  }] } });
4257
4523
 
4524
+ class RuleDependencyGraphComponent {
4525
+ ui = inject(BuilderTextService);
4526
+ store = inject(BuilderStore);
4527
+ dismiss = new EventEmitter();
4528
+ selectField = new EventEmitter();
4529
+ filterQuery = signal('', ...(ngDevMode ? [{ debugName: "filterQuery" }] : /* istanbul ignore next */ []));
4530
+ selectedType = signal('all', ...(ngDevMode ? [{ debugName: "selectedType" }] : /* istanbul ignore next */ []));
4531
+ graphData = computed(() => computeRuleDependencies(this.store.config(), this.store.rules(), this.ui.language()), ...(ngDevMode ? [{ debugName: "graphData" }] : /* istanbul ignore next */ []));
4532
+ filteredEdges = computed(() => {
4533
+ const q = this.filterQuery().trim().toLowerCase();
4534
+ const type = this.selectedType();
4535
+ return this.graphData().edges.filter(edge => {
4536
+ if (type !== 'all' && edge.type !== type)
4537
+ return false;
4538
+ if (!q)
4539
+ return true;
4540
+ return (edge.sourceLabel.toLowerCase().includes(q) ||
4541
+ edge.sourceKey.toLowerCase().includes(q) ||
4542
+ edge.targetLabel.toLowerCase().includes(q) ||
4543
+ edge.targetKey.toLowerCase().includes(q) ||
4544
+ edge.actionSummary.toLowerCase().includes(q) ||
4545
+ edge.description.toLowerCase().includes(q));
4546
+ });
4547
+ }, ...(ngDevMode ? [{ debugName: "filteredEdges" }] : /* istanbul ignore next */ []));
4548
+ typeName(type) {
4549
+ switch (type) {
4550
+ case 'rule':
4551
+ return this.ui.text('dependencyTypeRule');
4552
+ case 'showWhen':
4553
+ return this.ui.text('dependencyTypeShowWhen');
4554
+ case 'cascade':
4555
+ return this.ui.text('dependencyTypeCascade');
4556
+ case 'patchOnTrue':
4557
+ case 'autoPatch':
4558
+ return this.ui.text('dependencyTypePatch');
4559
+ }
4560
+ }
4561
+ onNodeClick(key) {
4562
+ if (!key)
4563
+ return;
4564
+ this.selectField.emit(key);
4565
+ this.dismiss.emit();
4566
+ }
4567
+ onBackdropClick(event) {
4568
+ if (event.target.classList.contains('deb-graph-backdrop')) {
4569
+ this.dismiss.emit();
4570
+ }
4571
+ }
4572
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleDependencyGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4573
+ 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: `
4574
+ <div class="deb-graph-backdrop" (click)="onBackdropClick($event)">
4575
+ <div class="deb-graph-dialog" role="dialog" aria-modal="true" data-testid="rule-graph-dialog">
4576
+ <div class="deb-graph-header">
4577
+ <div class="deb-graph-header__titles">
4578
+ <div class="deb-graph-header__title-row">
4579
+ <mat-icon class="deb-graph-icon">hub</mat-icon>
4580
+ <h2 class="deb-graph-title">{{ ui.text('ruleGraphTitle') }}</h2>
4581
+ </div>
4582
+ <p class="deb-graph-subtitle">{{ ui.text('ruleGraphSubtitle') }}</p>
4583
+ </div>
4584
+ <button
4585
+ mat-icon-button
4586
+ type="button"
4587
+ data-testid="close-rule-graph"
4588
+ [matTooltip]="ui.text('closeDialog')"
4589
+ (click)="dismiss.emit()"
4590
+ >
4591
+ <mat-icon>close</mat-icon>
4592
+ </button>
4593
+ </div>
4594
+
4595
+ <div class="deb-graph-metrics">
4596
+ <span class="deb-metric-pill deb-metric-pill--rule">
4597
+ <mat-icon>rule</mat-icon>
4598
+ {{ graphData().totalRules }} {{ ui.text('dependencyTypeRule') }}
4599
+ </span>
4600
+ <span class="deb-metric-pill deb-metric-pill--show-when">
4601
+ <mat-icon>visibility</mat-icon>
4602
+ {{ graphData().totalShowWhen }} {{ ui.text('dependencyTypeShowWhen') }}
4603
+ </span>
4604
+ <span class="deb-metric-pill deb-metric-pill--cascade">
4605
+ <mat-icon>account_tree</mat-icon>
4606
+ {{ graphData().totalCascades }} {{ ui.text('dependencyTypeCascade') }}
4607
+ </span>
4608
+ <span class="deb-metric-pill deb-metric-pill--patch">
4609
+ <mat-icon>content_copy</mat-icon>
4610
+ {{ graphData().totalPatches }} {{ ui.text('dependencyTypePatch') }}
4611
+ </span>
4612
+ </div>
4613
+
4614
+ <div class="deb-graph-controls">
4615
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-graph-search">
4616
+ <mat-icon matPrefix>search</mat-icon>
4617
+ <input
4618
+ matInput
4619
+ data-testid="filter-dependencies-input"
4620
+ [placeholder]="ui.text('filterDependencies')"
4621
+ [ngModel]="filterQuery()"
4622
+ (ngModelChange)="filterQuery.set($event)"
4623
+ />
4624
+ @if (filterQuery()) {
4625
+ <button mat-icon-button matSuffix type="button" (click)="filterQuery.set('')">
4626
+ <mat-icon>clear</mat-icon>
4627
+ </button>
4628
+ }
4629
+ </mat-form-field>
4630
+
4631
+ <div class="deb-graph-filter-types">
4632
+ <button
4633
+ type="button"
4634
+ class="deb-filter-btn"
4635
+ [class.deb-filter-btn--active]="selectedType() === 'all'"
4636
+ (click)="selectedType.set('all')"
4637
+ >
4638
+ All ({{ graphData().edges.length }})
4639
+ </button>
4640
+ <button
4641
+ type="button"
4642
+ class="deb-filter-btn"
4643
+ [class.deb-filter-btn--active]="selectedType() === 'rule'"
4644
+ (click)="selectedType.set('rule')"
4645
+ >
4646
+ {{ ui.text('dependencyTypeRule') }}
4647
+ </button>
4648
+ <button
4649
+ type="button"
4650
+ class="deb-filter-btn"
4651
+ [class.deb-filter-btn--active]="selectedType() === 'showWhen'"
4652
+ (click)="selectedType.set('showWhen')"
4653
+ >
4654
+ {{ ui.text('dependencyTypeShowWhen') }}
4655
+ </button>
4656
+ <button
4657
+ type="button"
4658
+ class="deb-filter-btn"
4659
+ [class.deb-filter-btn--active]="selectedType() === 'cascade'"
4660
+ (click)="selectedType.set('cascade')"
4661
+ >
4662
+ {{ ui.text('dependencyTypeCascade') }}
4663
+ </button>
4664
+ <button
4665
+ type="button"
4666
+ class="deb-filter-btn"
4667
+ [class.deb-filter-btn--active]="selectedType() === 'patchOnTrue'"
4668
+ (click)="selectedType.set('patchOnTrue')"
4669
+ >
4670
+ {{ ui.text('dependencyTypePatch') }}
4671
+ </button>
4672
+ </div>
4673
+ </div>
4674
+
4675
+ <div class="deb-graph-body" data-testid="rule-graph-edges">
4676
+ @if (filteredEdges().length === 0) {
4677
+ <div class="deb-graph-empty" data-testid="no-dependencies-found">
4678
+ <mat-icon>search_off</mat-icon>
4679
+ <p>{{ ui.text('noDependenciesFound') }}</p>
4680
+ </div>
4681
+ }
4682
+
4683
+ @for (edge of filteredEdges(); track edge.id) {
4684
+ <div class="deb-edge-card" [attr.data-testid]="'edge-card-' + edge.id">
4685
+ <!-- Source -->
4686
+ <button
4687
+ type="button"
4688
+ class="deb-node-chip deb-node-chip--source"
4689
+ [matTooltip]="ui.text('jumpToField', { field: edge.sourceLabel })"
4690
+ (click)="onNodeClick(edge.sourceKey)"
4691
+ >
4692
+ <mat-icon class="deb-node-chip__icon">tune</mat-icon>
4693
+ <span class="deb-node-chip__label">{{ edge.sourceLabel }}</span>
4694
+ <span class="deb-node-chip__key">{{ edge.sourceKey }}</span>
4695
+ </button>
4696
+
4697
+ <!-- Connection details -->
4698
+ <div class="deb-edge-flow">
4699
+ <span
4700
+ class="deb-edge-badge"
4701
+ [class.deb-edge-badge--rule]="edge.type === 'rule'"
4702
+ [class.deb-edge-badge--show-when]="edge.type === 'showWhen'"
4703
+ [class.deb-edge-badge--cascade]="edge.type === 'cascade'"
4704
+ [class.deb-edge-badge--patch]="edge.type === 'patchOnTrue' || edge.type === 'autoPatch'"
4705
+ >
4706
+ {{ typeName(edge.type) }}
4707
+ </span>
4708
+ <span class="deb-edge-arrow">
4709
+ <span class="deb-edge-summary">{{ edge.actionSummary }}</span>
4710
+ <mat-icon class="deb-edge-arrow-icon">arrow_forward</mat-icon>
4711
+ </span>
4712
+ <span class="deb-edge-desc">{{ edge.description }}</span>
4713
+ </div>
4714
+
4715
+ <!-- Target -->
4716
+ <button
4717
+ type="button"
4718
+ class="deb-node-chip deb-node-chip--target"
4719
+ [matTooltip]="ui.text('jumpToField', { field: edge.targetLabel })"
4720
+ (click)="onNodeClick(edge.targetKey)"
4721
+ >
4722
+ <mat-icon class="deb-node-chip__icon">output</mat-icon>
4723
+ <span class="deb-node-chip__label">{{ edge.targetLabel }}</span>
4724
+ <span class="deb-node-chip__key">{{ edge.targetKey }}</span>
4725
+ </button>
4726
+ </div>
4727
+ }
4728
+ </div>
4729
+
4730
+ <div class="deb-graph-footer">
4731
+ <span class="deb-graph-count">
4732
+ {{ ui.text('totalDependencies', { count: filteredEdges().length }) }}
4733
+ </span>
4734
+ <button mat-flat-button color="primary" type="button" (click)="dismiss.emit()">
4735
+ {{ ui.text('closeDialog') }}
4736
+ </button>
4737
+ </div>
4738
+ </div>
4739
+ </div>
4740
+ `, 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 });
4741
+ }
4742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleDependencyGraphComponent, decorators: [{
4743
+ type: Component,
4744
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-dependency-graph', standalone: true, imports: [
4745
+ FormsModule,
4746
+ MatButtonModule,
4747
+ MatChipsModule,
4748
+ MatFormFieldModule,
4749
+ MatIconModule,
4750
+ MatInputModule,
4751
+ MatTooltipModule,
4752
+ ], template: `
4753
+ <div class="deb-graph-backdrop" (click)="onBackdropClick($event)">
4754
+ <div class="deb-graph-dialog" role="dialog" aria-modal="true" data-testid="rule-graph-dialog">
4755
+ <div class="deb-graph-header">
4756
+ <div class="deb-graph-header__titles">
4757
+ <div class="deb-graph-header__title-row">
4758
+ <mat-icon class="deb-graph-icon">hub</mat-icon>
4759
+ <h2 class="deb-graph-title">{{ ui.text('ruleGraphTitle') }}</h2>
4760
+ </div>
4761
+ <p class="deb-graph-subtitle">{{ ui.text('ruleGraphSubtitle') }}</p>
4762
+ </div>
4763
+ <button
4764
+ mat-icon-button
4765
+ type="button"
4766
+ data-testid="close-rule-graph"
4767
+ [matTooltip]="ui.text('closeDialog')"
4768
+ (click)="dismiss.emit()"
4769
+ >
4770
+ <mat-icon>close</mat-icon>
4771
+ </button>
4772
+ </div>
4773
+
4774
+ <div class="deb-graph-metrics">
4775
+ <span class="deb-metric-pill deb-metric-pill--rule">
4776
+ <mat-icon>rule</mat-icon>
4777
+ {{ graphData().totalRules }} {{ ui.text('dependencyTypeRule') }}
4778
+ </span>
4779
+ <span class="deb-metric-pill deb-metric-pill--show-when">
4780
+ <mat-icon>visibility</mat-icon>
4781
+ {{ graphData().totalShowWhen }} {{ ui.text('dependencyTypeShowWhen') }}
4782
+ </span>
4783
+ <span class="deb-metric-pill deb-metric-pill--cascade">
4784
+ <mat-icon>account_tree</mat-icon>
4785
+ {{ graphData().totalCascades }} {{ ui.text('dependencyTypeCascade') }}
4786
+ </span>
4787
+ <span class="deb-metric-pill deb-metric-pill--patch">
4788
+ <mat-icon>content_copy</mat-icon>
4789
+ {{ graphData().totalPatches }} {{ ui.text('dependencyTypePatch') }}
4790
+ </span>
4791
+ </div>
4792
+
4793
+ <div class="deb-graph-controls">
4794
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-graph-search">
4795
+ <mat-icon matPrefix>search</mat-icon>
4796
+ <input
4797
+ matInput
4798
+ data-testid="filter-dependencies-input"
4799
+ [placeholder]="ui.text('filterDependencies')"
4800
+ [ngModel]="filterQuery()"
4801
+ (ngModelChange)="filterQuery.set($event)"
4802
+ />
4803
+ @if (filterQuery()) {
4804
+ <button mat-icon-button matSuffix type="button" (click)="filterQuery.set('')">
4805
+ <mat-icon>clear</mat-icon>
4806
+ </button>
4807
+ }
4808
+ </mat-form-field>
4809
+
4810
+ <div class="deb-graph-filter-types">
4811
+ <button
4812
+ type="button"
4813
+ class="deb-filter-btn"
4814
+ [class.deb-filter-btn--active]="selectedType() === 'all'"
4815
+ (click)="selectedType.set('all')"
4816
+ >
4817
+ All ({{ graphData().edges.length }})
4818
+ </button>
4819
+ <button
4820
+ type="button"
4821
+ class="deb-filter-btn"
4822
+ [class.deb-filter-btn--active]="selectedType() === 'rule'"
4823
+ (click)="selectedType.set('rule')"
4824
+ >
4825
+ {{ ui.text('dependencyTypeRule') }}
4826
+ </button>
4827
+ <button
4828
+ type="button"
4829
+ class="deb-filter-btn"
4830
+ [class.deb-filter-btn--active]="selectedType() === 'showWhen'"
4831
+ (click)="selectedType.set('showWhen')"
4832
+ >
4833
+ {{ ui.text('dependencyTypeShowWhen') }}
4834
+ </button>
4835
+ <button
4836
+ type="button"
4837
+ class="deb-filter-btn"
4838
+ [class.deb-filter-btn--active]="selectedType() === 'cascade'"
4839
+ (click)="selectedType.set('cascade')"
4840
+ >
4841
+ {{ ui.text('dependencyTypeCascade') }}
4842
+ </button>
4843
+ <button
4844
+ type="button"
4845
+ class="deb-filter-btn"
4846
+ [class.deb-filter-btn--active]="selectedType() === 'patchOnTrue'"
4847
+ (click)="selectedType.set('patchOnTrue')"
4848
+ >
4849
+ {{ ui.text('dependencyTypePatch') }}
4850
+ </button>
4851
+ </div>
4852
+ </div>
4853
+
4854
+ <div class="deb-graph-body" data-testid="rule-graph-edges">
4855
+ @if (filteredEdges().length === 0) {
4856
+ <div class="deb-graph-empty" data-testid="no-dependencies-found">
4857
+ <mat-icon>search_off</mat-icon>
4858
+ <p>{{ ui.text('noDependenciesFound') }}</p>
4859
+ </div>
4860
+ }
4861
+
4862
+ @for (edge of filteredEdges(); track edge.id) {
4863
+ <div class="deb-edge-card" [attr.data-testid]="'edge-card-' + edge.id">
4864
+ <!-- Source -->
4865
+ <button
4866
+ type="button"
4867
+ class="deb-node-chip deb-node-chip--source"
4868
+ [matTooltip]="ui.text('jumpToField', { field: edge.sourceLabel })"
4869
+ (click)="onNodeClick(edge.sourceKey)"
4870
+ >
4871
+ <mat-icon class="deb-node-chip__icon">tune</mat-icon>
4872
+ <span class="deb-node-chip__label">{{ edge.sourceLabel }}</span>
4873
+ <span class="deb-node-chip__key">{{ edge.sourceKey }}</span>
4874
+ </button>
4875
+
4876
+ <!-- Connection details -->
4877
+ <div class="deb-edge-flow">
4878
+ <span
4879
+ class="deb-edge-badge"
4880
+ [class.deb-edge-badge--rule]="edge.type === 'rule'"
4881
+ [class.deb-edge-badge--show-when]="edge.type === 'showWhen'"
4882
+ [class.deb-edge-badge--cascade]="edge.type === 'cascade'"
4883
+ [class.deb-edge-badge--patch]="edge.type === 'patchOnTrue' || edge.type === 'autoPatch'"
4884
+ >
4885
+ {{ typeName(edge.type) }}
4886
+ </span>
4887
+ <span class="deb-edge-arrow">
4888
+ <span class="deb-edge-summary">{{ edge.actionSummary }}</span>
4889
+ <mat-icon class="deb-edge-arrow-icon">arrow_forward</mat-icon>
4890
+ </span>
4891
+ <span class="deb-edge-desc">{{ edge.description }}</span>
4892
+ </div>
4893
+
4894
+ <!-- Target -->
4895
+ <button
4896
+ type="button"
4897
+ class="deb-node-chip deb-node-chip--target"
4898
+ [matTooltip]="ui.text('jumpToField', { field: edge.targetLabel })"
4899
+ (click)="onNodeClick(edge.targetKey)"
4900
+ >
4901
+ <mat-icon class="deb-node-chip__icon">output</mat-icon>
4902
+ <span class="deb-node-chip__label">{{ edge.targetLabel }}</span>
4903
+ <span class="deb-node-chip__key">{{ edge.targetKey }}</span>
4904
+ </button>
4905
+ </div>
4906
+ }
4907
+ </div>
4908
+
4909
+ <div class="deb-graph-footer">
4910
+ <span class="deb-graph-count">
4911
+ {{ ui.text('totalDependencies', { count: filteredEdges().length }) }}
4912
+ </span>
4913
+ <button mat-flat-button color="primary" type="button" (click)="dismiss.emit()">
4914
+ {{ ui.text('closeDialog') }}
4915
+ </button>
4916
+ </div>
4917
+ </div>
4918
+ </div>
4919
+ `, 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"] }]
4920
+ }], propDecorators: { dismiss: [{
4921
+ type: Output
4922
+ }], selectField: [{
4923
+ type: Output
4924
+ }] } });
4925
+
4258
4926
  /** RBAC actions surfaced in the settings panel. */
4259
4927
  const RBAC_ACTIONS = ['view', 'edit', 'delete'];
4260
4928
  /** Shared stable empty array — never allocate a fresh [] per change-detection (mat-select loops). */
@@ -4351,6 +5019,17 @@ class EntityBuilderComponent {
4351
5019
  togglePreview() {
4352
5020
  this.previewOpen.update(open => !open);
4353
5021
  }
5022
+ ruleGraphOpen = signal(false, ...(ngDevMode ? [{ debugName: "ruleGraphOpen" }] : /* istanbul ignore next */ []));
5023
+ openRuleGraph() {
5024
+ this.ruleGraphOpen.set(true);
5025
+ }
5026
+ closeRuleGraph() {
5027
+ this.ruleGraphOpen.set(false);
5028
+ }
5029
+ onSelectFieldFromGraph(key) {
5030
+ this.store.selectField(key);
5031
+ this.ruleGraphOpen.set(false);
5032
+ }
4354
5033
  /*
4355
5034
  * Collapsing from inside a panel destroys the button that was just pressed, and expanding
4356
5035
  * from a rail destroys the rail. Either way the browser drops focus onto `<body>`, which
@@ -4488,10 +5167,10 @@ class EntityBuilderComponent {
4488
5167
  .filter(Boolean)));
4489
5168
  this.store.setPermission(action, roles);
4490
5169
  }
4491
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4492
- 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-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", 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$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$2.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: i5.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$1.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
5170
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5171
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: false, isRequired: false, transformFunction: null }, rules: { classPropertyName: "rules", publicName: "rules", isSignal: false, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: false, isRequired: false, transformFunction: null }, uiLanguage: { classPropertyName: "uiLanguage", publicName: "uiLanguage", isSignal: false, isRequired: false, transformFunction: null }, availableRoles: { classPropertyName: "availableRoles", publicName: "availableRoles", isSignal: false, isRequired: false, transformFunction: null }, userRoles: { classPropertyName: "userRoles", publicName: "userRoles", isSignal: false, isRequired: false, transformFunction: null }, commonModules: { classPropertyName: "commonModules", publicName: "commonModules", isSignal: false, isRequired: false, transformFunction: null }, leftSidebarOpen: { classPropertyName: "leftSidebarOpen", publicName: "leftSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, rightSidebarOpen: { classPropertyName: "rightSidebarOpen", publicName: "rightSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, fieldsOpen: { classPropertyName: "fieldsOpen", publicName: "fieldsOpen", isSignal: true, isRequired: false, transformFunction: null }, previewOpen: { classPropertyName: "previewOpen", publicName: "previewOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", rulesChange: "rulesChange", save: "save", leftSidebarOpen: "leftSidebarOpenChange", rightSidebarOpen: "rightSidebarOpenChange", fieldsOpen: "fieldsOpenChange", previewOpen: "previewOpenChange" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('permissions') }}</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRoles', { action }) }}</mat-label>\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: the exported JSON \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon>&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 });
4493
5172
  }
4494
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, decorators: [{
5173
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, decorators: [{
4495
5174
  type: Component,
4496
5175
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-entity-builder', standalone: true, providers: [BuilderStore], imports: [
4497
5176
  FormsModule,
@@ -4511,7 +5190,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
4511
5190
  TabManagerComponent,
4512
5191
  FieldInspectorComponent,
4513
5192
  EntityBuilderCanvasComponent,
4514
- ], 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-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", 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"] }]
5193
+ RuleDependencyGraphComponent,
5194
+ ], encapsulation: ViewEncapsulation.None, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('permissions') }}</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRoles', { action }) }}</mat-label>\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: the exported JSON \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon>&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"] }]
4515
5195
  }], ctorParameters: () => [], propDecorators: { config: [{
4516
5196
  type: Input
4517
5197
  }], rules: [{
@@ -4543,5 +5223,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
4543
5223
  * Generated bundle index. Do not edit.
4544
5224
  */
4545
5225
 
4546
- export { BUILDER_TEXT, BuilderStore, BuilderTextService, DEFAULT_BUILDER_TEXT, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, TabManagerComponent, fieldPathOptions, withExistingOptions };
5226
+ export { BUILDER_TEXT, BuilderStore, BuilderTextService, DEFAULT_BUILDER_TEXT, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleDependencyGraphComponent, RuleFormComponent, TabManagerComponent, computeRuleDependencies, fieldPathOptions, withExistingOptions };
4547
5227
  //# sourceMappingURL=ngx-dynamic-entity-builder.mjs.map