ngx-dynamic-entity-builder 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, inject, ChangeDetectionStrategy, Component, EventEmitter, Output, Input, effect, HostListener } from '@angular/core';
2
+ import { signal, computed, Injectable, InjectionToken, inject, ChangeDetectionStrategy, Component, EventEmitter, Output, Input, effect, HostListener } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { FormsModule } from '@angular/forms';
5
5
  import * as i2 from '@angular/material/button';
@@ -26,7 +26,7 @@ import * as i4$2 from '@angular/material/tooltip';
26
26
  import { MatTooltipModule } from '@angular/material/tooltip';
27
27
  import { collectFieldScopes, assignFieldRefs, normalizeConfigOptions, getFieldTypeMeta, createFieldConfig, labelToId, createFieldSnapshot, computeFieldDrift, toRefToken, humanizeId, findTab, fieldRefFor, parseFieldRef, refOf, resolveLabel, resolveOptionLabel, FIELD_TYPE_CATALOG, COMMON_MODULES } from '@dynamic-entity/core';
28
28
  export { FIELD_TYPE_CATALOG, createFieldConfig, getFieldTypeMeta, humanizeId } from '@dynamic-entity/core';
29
- import { ConfigSourceService, SYSTEM_DEFAULT_CAN_EDIT } from 'ngx-dynamic-entity';
29
+ import { resolveUiText, ConfigSourceService, SYSTEM_DEFAULT_CAN_EDIT } from 'ngx-dynamic-entity';
30
30
  export { SYSTEM_DEFAULT_CAN_EDIT } from 'ngx-dynamic-entity';
31
31
  import * as i3 from '@angular/material/divider';
32
32
  import { MatDividerModule } from '@angular/material/divider';
@@ -1415,6 +1415,14 @@ class BuilderStore {
1415
1415
  }
1416
1416
  validate(config) {
1417
1417
  const problems = [];
1418
+ // Core calls a config with no tabs an error — nothing can render — and the builder used
1419
+ // to report only a warning and leave Save enabled. That let the builder save a config
1420
+ // `dynamic-entity validate` then rejected in CI: the mirror of the bug where it refused
1421
+ // one core accepted. The builder defers to core here, as it already does for the scope
1422
+ // rule, and the message is core's so the two read the same in both places.
1423
+ if (!config.tabs?.length) {
1424
+ problems.push({ level: 'error', message: 'At least one tab is required.' });
1425
+ }
1418
1426
  if (!config.entity || !config.entity.trim()) {
1419
1427
  problems.push({ level: 'error', message: 'Entity name is required.' });
1420
1428
  }
@@ -1545,6 +1553,221 @@ function withExistingOptions(options, current) {
1545
1553
  return [...options, ...extra];
1546
1554
  }
1547
1555
 
1556
+ /**
1557
+ * Every word the builder puts on screen that is not part of the config being authored.
1558
+ *
1559
+ * The renderer has its own list (`DEFAULT_UI_TEXT`) and the two are deliberately separate:
1560
+ * an application that ships only the renderer should not see a hundred and fifty builder
1561
+ * keys in completion, and the two vocabularies share no strings worth deduplicating. What
1562
+ * they do share is the resolution rule, which comes from `resolveUiText`.
1563
+ *
1564
+ * Keys are grouped by panel, because a translator works screen by screen.
1565
+ */
1566
+ const DEFAULT_BUILDER_TEXT = {
1567
+ // Toolbar and page chrome
1568
+ builderTitle: 'Entity Builder',
1569
+ undo: 'Undo',
1570
+ undoTooltip: 'Undo (Ctrl+Z)',
1571
+ redo: 'Redo',
1572
+ redoTooltip: 'Redo (Ctrl+Shift+Z)',
1573
+ issuesTooltip: '{count} issue(s) — hover items in the list',
1574
+ copyJson: 'Copy JSON',
1575
+ save: 'Save',
1576
+ // Entity settings panel
1577
+ entityName: 'Entity name',
1578
+ entityNamePlaceholder: 'e.g. clients',
1579
+ editingLanguage: 'Editing language',
1580
+ maskEntity: 'Mask this entity (RBAC)',
1581
+ permissions: 'Permissions',
1582
+ actionRoles: '{action} roles',
1583
+ actionRolesPlaceholder: '{action} roles (comma-separated)',
1584
+ permissionsHint: 'Empty = no restriction for that action.',
1585
+ addField: 'Add field',
1586
+ fieldProperties: 'Field properties',
1587
+ configJson: 'Config JSON',
1588
+ // Canvas
1589
+ fieldsHeading: 'Fields ({count})',
1590
+ canvasEmpty: 'No fields yet. Pick a type from Add field to get started.',
1591
+ // Field rows in the tree
1592
+ selectField: 'Select field {field}',
1593
+ dragToReorder: 'Drag to reorder (or use the move buttons)',
1594
+ referencedFieldTooltip: 'Referenced field (linked to {entity})',
1595
+ driftDetected: 'Drift detected: source field has evolved',
1596
+ required: 'Required',
1597
+ moveUp: 'Move up',
1598
+ moveDown: 'Move down',
1599
+ duplicate: 'Duplicate',
1600
+ delete: 'Delete',
1601
+ moveFieldUp: 'Move {field} up',
1602
+ moveFieldDown: 'Move {field} down',
1603
+ duplicateField: 'Duplicate {field}',
1604
+ // Field inspector — identity
1605
+ inspectorEmpty: 'Select a field to edit its properties.',
1606
+ fieldId: 'Field id',
1607
+ fieldIdFixedHint: 'The stored data key for this saved field — fixed.',
1608
+ fieldIdDerivedHint: 'Derived from the label. Used as the data key.',
1609
+ tab: 'Tab',
1610
+ tabMoveHint: 'Moving the field rewrites its path and repoints the rules that named it.',
1611
+ labelInLanguage: 'Label ({language})',
1612
+ placeholderInLanguage: 'Placeholder ({language})',
1613
+ defaultValue: 'Default value',
1614
+ validation: 'Validation',
1615
+ // Field inspector — options
1616
+ dataSource: 'Data source',
1617
+ optionsComeFrom: 'Options come from',
1618
+ optionsSourceNone: 'Nothing yet',
1619
+ optionsSourceInline: 'Options authored here',
1620
+ optionsSourceList: 'A named list',
1621
+ listName: 'List name',
1622
+ listNameHint: 'The consuming app resolves this name through its lookup registry. Options are not authored here.',
1623
+ options: 'Options',
1624
+ option: 'Option',
1625
+ optionInLanguage: 'Option ({language})',
1626
+ removeOption: 'Remove option',
1627
+ noOptionsYet: 'No options yet — add at least one.',
1628
+ // Field inspector — display flags
1629
+ display: 'Display',
1630
+ visible: 'Visible',
1631
+ tableColumn: 'Table column',
1632
+ readOnly: 'Read-only',
1633
+ disabled: 'Disabled',
1634
+ maskData: 'Mask data (RBAC)',
1635
+ criticalField: 'Critical field',
1636
+ criticalFieldTooltip: 'Renders locked; editing it announces a change against the session baseline.',
1637
+ showInSummary: 'Show in summary',
1638
+ showInSummaryTooltip: "Shown in the record editor's summary panel.",
1639
+ // Field inspector — conditional visibility
1640
+ showWhen: 'Show when',
1641
+ condition: 'Condition',
1642
+ watchedField: 'Watched field',
1643
+ equals: 'Equals',
1644
+ removeCondition: 'Remove condition',
1645
+ alwaysVisibleHint: 'Always visible. Add a condition to show this field only for certain values.',
1646
+ // Field inspector — copy on true
1647
+ copyOnTrue: 'Copy on true',
1648
+ mapping: 'Mapping',
1649
+ fromField: 'From field',
1650
+ toField: 'To field',
1651
+ removeMapping: 'Remove mapping',
1652
+ copyOnTrueHint: "When this flips to true, copy one field's value into another.",
1653
+ // Rules list
1654
+ rules: 'Rules',
1655
+ rule: 'Rule',
1656
+ noRulesOnField: 'No rules on this field.',
1657
+ rulePriority: 'priority {priority} · {targets} target(s)',
1658
+ toggleRule: 'Enable / disable',
1659
+ moveRuleUp: 'Move rule up',
1660
+ moveRuleDown: 'Move rule down',
1661
+ editRule: 'Edit rule',
1662
+ deleteRule: 'Delete rule',
1663
+ // Rule form
1664
+ newRuleTitle: 'New Form Rule',
1665
+ editRuleTitle: 'Edit Rule',
1666
+ triggerField: 'Trigger Field',
1667
+ applyToFields: 'Apply to fields',
1668
+ conditions: 'Conditions',
1669
+ operator: 'Operator',
1670
+ value: 'Value',
1671
+ conditionValuePlaceholder: 'Condition value',
1672
+ addCondition: 'Add Condition',
1673
+ action: 'Action',
1674
+ actionType: 'Action Type',
1675
+ actionVisibility: 'Visibility (Show/Hide)',
1676
+ actionValidationMessage: 'Validation Message',
1677
+ actionInfoBanner: 'Info Banner',
1678
+ valueOrMessage: 'Value / Message',
1679
+ valueOrMessagePlaceholder: 'false to hide, or message text',
1680
+ cancel: 'Cancel',
1681
+ saveRule: 'Save Rule',
1682
+ // Tab manager
1683
+ tabsManager: 'Tabs Manager',
1684
+ systemTag: '(System)',
1685
+ addTab: 'Add Tab',
1686
+ noTabs: 'No tabs — all fields render in a single section.',
1687
+ flatData: 'Flat Data',
1688
+ primaryTab: 'Primary Tab',
1689
+ maskTabData: 'Mask Tab Data',
1690
+ systemDefault: 'System Default',
1691
+ addSubTab: '+ Sub-tab',
1692
+ consumerModuleName: 'Consumer Module Name (Optional)',
1693
+ consumerModulePlaceholder: 'e.g. documents-view',
1694
+ subTabs: 'Sub-tabs:',
1695
+ moveTabUp: 'Move tab up',
1696
+ moveTabDown: 'Move tab down',
1697
+ removeTab: 'Remove tab',
1698
+ removeSubTab: 'Remove sub-tab',
1699
+ // Entity reference panel
1700
+ entityReference: 'Entity reference',
1701
+ registryKey: 'Registry key',
1702
+ registryKeyHint: 'Key the consumer registered a loader under. Defaults to the field id.',
1703
+ displayFields: 'Display fields',
1704
+ displayFieldsHint: 'Comma-separated record paths used to build each option label.',
1705
+ staticFilters: 'Static filters (JSON)',
1706
+ staticFiltersHint: 'Passed to the loader, e.g. {"isEmployee": false}.',
1707
+ cascade: 'Cascade',
1708
+ parentField: 'Parent field',
1709
+ parentFieldNone: 'None — load all options',
1710
+ cascadeHint: "This field's options reload whenever the parent changes.",
1711
+ lookupFilterPath: 'Lookup filter path',
1712
+ lookupFilterPathHint: 'Keep options whose record matches the parent value at this path.',
1713
+ lookupPath: 'Lookup path (nested options)',
1714
+ lookupPathHint: "Take options from this array on the selected parent's record instead.",
1715
+ autoPatchOnSelect: 'Auto-patch on select',
1716
+ targetTab: 'Target tab',
1717
+ sourceLinkedRecord: 'Source (linked record)',
1718
+ targetField: 'Target field',
1719
+ noAutoPatch: 'No auto-patch configured — selecting a reference will not copy any values.',
1720
+ // Referenced-field panel
1721
+ referencedField: 'Referenced Field',
1722
+ link: 'Link',
1723
+ sourceEntityKey: 'Source Entity Key',
1724
+ sourceEntityKeyPlaceholder: 'e.g. individuals',
1725
+ sourceFieldId: 'Source Field ID',
1726
+ sourceFieldIdPlaceholder: 'e.g. firstName',
1727
+ driftHeading: 'Source field definition has drifted!',
1728
+ driftBody: 'The upstream field configuration in "{entity}" has evolved.',
1729
+ syncWithSource: 'Sync with Source',
1730
+ };
1731
+ /**
1732
+ * Overrides for the builder's own chrome.
1733
+ *
1734
+ * Accepts what `UI_TEXT` accepts: a partial map whose values are strings or `LocalizedText`
1735
+ * (`{ en, de }`), or a resolver `(key, defaultText, language) => string` for a host with an
1736
+ * existing i18n layer. `DEFAULT_BUILDER_TEXT` is the full key list with its English source
1737
+ * strings; anything left out keeps its default.
1738
+ *
1739
+ * @example
1740
+ * { provide: BUILDER_TEXT, useValue: { save: { en: 'Save', de: 'Speichern' } } }
1741
+ */
1742
+ const BUILDER_TEXT = new InjectionToken('BUILDER_TEXT');
1743
+ /**
1744
+ * Resolves the builder's own UI text.
1745
+ *
1746
+ * The language lives on the service rather than travelling through every template call,
1747
+ * because the builder already has a *different* language on screen: `activeLanguage()`
1748
+ * selects the `LocalizedText` entry being **authored**. Passing that one to the chrome
1749
+ * would flip the builder's own interface every time an author switched the label language
1750
+ * they were editing, which is the opposite of what it means.
1751
+ *
1752
+ * Root-provided and therefore shared: two builders mounted at once show one chrome
1753
+ * language. `EntityBuilderComponent`'s `uiLanguage` input is what sets it.
1754
+ */
1755
+ class BuilderTextService {
1756
+ overrides = inject(BUILDER_TEXT, { optional: true });
1757
+ /** The chrome language. Not the authoring language — see the class comment. */
1758
+ language = signal('en', ...(ngDevMode ? [{ debugName: "language" }] : /* istanbul ignore next */ []));
1759
+ /** One label by key. `params` fill the `{placeholder}` slots. */
1760
+ text(key, params) {
1761
+ return resolveUiText(this.overrides, DEFAULT_BUILDER_TEXT, key, this.language(), params);
1762
+ }
1763
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1764
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, providedIn: 'root' });
1765
+ }
1766
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: BuilderTextService, decorators: [{
1767
+ type: Injectable,
1768
+ args: [{ providedIn: 'root' }]
1769
+ }] });
1770
+
1548
1771
  /**
1549
1772
  * EntityReferenceConfigComponent — authors the `entityReference` block of an entity-ref field:
1550
1773
  * registry key, display fields, static filters, and the parent→child cascade.
@@ -1553,6 +1776,8 @@ function withExistingOptions(options, current) {
1553
1776
  * selected reference returns.
1554
1777
  */
1555
1778
  class EntityReferenceConfigComponent {
1779
+ /** Builder chrome, overridable via BUILDER_TEXT. */
1780
+ ui = inject(BuilderTextService);
1556
1781
  store = inject(BuilderStore);
1557
1782
  field = this.store.selectedField;
1558
1783
  /** Sibling fields that can drive a cascade — anything but this field itself. */
@@ -1632,32 +1857,32 @@ class EntityReferenceConfigComponent {
1632
1857
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
1633
1858
  @if (field(); as f) {
1634
1859
  <div class="deb-entity-ref">
1635
- <span class="deb-section-title">Entity reference</span>
1860
+ <span class="deb-section-title">{{ ui.text('entityReference') }}</span>
1636
1861
 
1637
1862
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1638
- <mat-label>Registry key</mat-label>
1863
+ <mat-label>{{ ui.text('registryKey') }}</mat-label>
1639
1864
  <input
1640
1865
  matInput
1641
1866
  data-testid="entity-ref-key"
1642
1867
  [ngModel]="ref(f).linkedEntityKey ?? ''"
1643
1868
  (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1644
1869
  />
1645
- <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1870
+ <mat-hint>{{ ui.text('registryKeyHint') }}</mat-hint>
1646
1871
  </mat-form-field>
1647
1872
 
1648
1873
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1649
- <mat-label>Display fields</mat-label>
1874
+ <mat-label>{{ ui.text('displayFields') }}</mat-label>
1650
1875
  <input
1651
1876
  matInput
1652
1877
  data-testid="entity-ref-display-fields"
1653
1878
  [ngModel]="displayFieldsText(f)"
1654
1879
  (ngModelChange)="setDisplayFields(f, $event)"
1655
1880
  />
1656
- <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1881
+ <mat-hint>{{ ui.text('displayFieldsHint') }}</mat-hint>
1657
1882
  </mat-form-field>
1658
1883
 
1659
1884
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1660
- <mat-label>Static filters (JSON)</mat-label>
1885
+ <mat-label>{{ ui.text('staticFilters') }}</mat-label>
1661
1886
  <input
1662
1887
  matInput
1663
1888
  data-testid="entity-ref-filters"
@@ -1667,52 +1892,52 @@ class EntityReferenceConfigComponent {
1667
1892
  @if (filtersError()) {
1668
1893
  <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1669
1894
  } @else {
1670
- <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1895
+ <mat-hint>{{ ui.text('staticFiltersHint') }}</mat-hint>
1671
1896
  }
1672
1897
  </mat-form-field>
1673
1898
 
1674
1899
  <mat-divider></mat-divider>
1675
1900
 
1676
1901
  <!-- Cascade -->
1677
- <span class="deb-section-title">Cascade</span>
1902
+ <span class="deb-section-title">{{ ui.text('cascade') }}</span>
1678
1903
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1679
- <mat-label>Parent field</mat-label>
1904
+ <mat-label>{{ ui.text('parentField') }}</mat-label>
1680
1905
  <mat-select
1681
1906
  data-testid="entity-ref-parent"
1682
1907
  [ngModel]="ref(f).parentField ?? ''"
1683
1908
  (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1684
1909
  >
1685
- <mat-option value="">None — load all options</mat-option>
1910
+ <mat-option value="">{{ ui.text('parentFieldNone') }}</mat-option>
1686
1911
  @for (option of parentOptions(); track option.value) {
1687
1912
  <mat-option [value]="option.value">
1688
1913
  {{ option.label }} <span class="deb-path-hint">{{ option.path }}</span>
1689
1914
  </mat-option>
1690
1915
  }
1691
1916
  </mat-select>
1692
- <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1917
+ <mat-hint>{{ ui.text('cascadeHint') }}</mat-hint>
1693
1918
  </mat-form-field>
1694
1919
 
1695
1920
  @if (ref(f).parentField) {
1696
1921
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1697
- <mat-label>Lookup filter path</mat-label>
1922
+ <mat-label>{{ ui.text('lookupFilterPath') }}</mat-label>
1698
1923
  <input
1699
1924
  matInput
1700
1925
  data-testid="entity-ref-lookup-filter"
1701
1926
  [ngModel]="ref(f).lookupFilter ?? ''"
1702
1927
  (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1703
1928
  />
1704
- <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1929
+ <mat-hint>{{ ui.text('lookupFilterPathHint') }}</mat-hint>
1705
1930
  </mat-form-field>
1706
1931
 
1707
1932
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1708
- <mat-label>Lookup path (nested options)</mat-label>
1933
+ <mat-label>{{ ui.text('lookupPath') }}</mat-label>
1709
1934
  <input
1710
1935
  matInput
1711
1936
  data-testid="entity-ref-lookup-path"
1712
1937
  [ngModel]="ref(f).lookupPath ?? ''"
1713
1938
  (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1714
1939
  />
1715
- <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1940
+ <mat-hint>{{ ui.text('lookupPathHint') }}</mat-hint>
1716
1941
  </mat-form-field>
1717
1942
  }
1718
1943
 
@@ -1720,20 +1945,20 @@ class EntityReferenceConfigComponent {
1720
1945
 
1721
1946
  <!-- autoPatch -->
1722
1947
  <div class="deb-row deb-row--split">
1723
- <span class="deb-section-title">Auto-patch on select</span>
1948
+ <span class="deb-section-title">{{ ui.text('autoPatchOnSelect') }}</span>
1724
1949
  <button
1725
1950
  mat-stroked-button
1726
1951
  type="button"
1727
1952
  data-testid="add-auto-patch"
1728
1953
  (click)="store.addAutoPatchMapping(store.keyOf(f))"
1729
1954
  >
1730
- <mat-icon>add</mat-icon> Mapping
1955
+ <mat-icon>add</mat-icon> {{ ui.text('mapping') }}
1731
1956
  </button>
1732
1957
  </div>
1733
1958
 
1734
1959
  @if (f.autoPatch) {
1735
1960
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1736
- <mat-label>Target tab</mat-label>
1961
+ <mat-label>{{ ui.text('targetTab') }}</mat-label>
1737
1962
  <mat-select
1738
1963
  data-testid="auto-patch-tab"
1739
1964
  [ngModel]="f.autoPatch.targetTab"
@@ -1748,7 +1973,7 @@ class EntityReferenceConfigComponent {
1748
1973
  @for (mapping of f.autoPatch.mappings; track $index) {
1749
1974
  <div class="deb-option-row">
1750
1975
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1751
- <mat-label>Source (linked record)</mat-label>
1976
+ <mat-label>{{ ui.text('sourceLinkedRecord') }}</mat-label>
1752
1977
  <input
1753
1978
  matInput
1754
1979
  [ngModel]="mapping.source"
@@ -1756,7 +1981,7 @@ class EntityReferenceConfigComponent {
1756
1981
  />
1757
1982
  </mat-form-field>
1758
1983
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1759
- <mat-label>Target field</mat-label>
1984
+ <mat-label>{{ ui.text('targetField') }}</mat-label>
1760
1985
  <mat-select
1761
1986
  data-testid="auto-patch-target"
1762
1987
  [ngModel]="mapping.target"
@@ -1773,7 +1998,7 @@ class EntityReferenceConfigComponent {
1773
1998
  mat-icon-button
1774
1999
  type="button"
1775
2000
  color="warn"
1776
- matTooltip="Remove mapping"
2001
+ [matTooltip]="ui.text('removeMapping')"
1777
2002
  (click)="store.removeAutoPatchMapping(store.keyOf(f), $index)"
1778
2003
  >
1779
2004
  <mat-icon>delete</mat-icon>
@@ -1782,7 +2007,7 @@ class EntityReferenceConfigComponent {
1782
2007
  }
1783
2008
  } @else {
1784
2009
  <p class="deb-hint">
1785
- No auto-patch configured — selecting a reference will not copy any values.
2010
+ {{ ui.text('noAutoPatch') }}
1786
2011
  </p>
1787
2012
  }
1788
2013
  </div>
@@ -1804,32 +2029,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1804
2029
  ], template: `
1805
2030
  @if (field(); as f) {
1806
2031
  <div class="deb-entity-ref">
1807
- <span class="deb-section-title">Entity reference</span>
2032
+ <span class="deb-section-title">{{ ui.text('entityReference') }}</span>
1808
2033
 
1809
2034
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1810
- <mat-label>Registry key</mat-label>
2035
+ <mat-label>{{ ui.text('registryKey') }}</mat-label>
1811
2036
  <input
1812
2037
  matInput
1813
2038
  data-testid="entity-ref-key"
1814
2039
  [ngModel]="ref(f).linkedEntityKey ?? ''"
1815
2040
  (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1816
2041
  />
1817
- <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
2042
+ <mat-hint>{{ ui.text('registryKeyHint') }}</mat-hint>
1818
2043
  </mat-form-field>
1819
2044
 
1820
2045
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1821
- <mat-label>Display fields</mat-label>
2046
+ <mat-label>{{ ui.text('displayFields') }}</mat-label>
1822
2047
  <input
1823
2048
  matInput
1824
2049
  data-testid="entity-ref-display-fields"
1825
2050
  [ngModel]="displayFieldsText(f)"
1826
2051
  (ngModelChange)="setDisplayFields(f, $event)"
1827
2052
  />
1828
- <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
2053
+ <mat-hint>{{ ui.text('displayFieldsHint') }}</mat-hint>
1829
2054
  </mat-form-field>
1830
2055
 
1831
2056
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1832
- <mat-label>Static filters (JSON)</mat-label>
2057
+ <mat-label>{{ ui.text('staticFilters') }}</mat-label>
1833
2058
  <input
1834
2059
  matInput
1835
2060
  data-testid="entity-ref-filters"
@@ -1839,52 +2064,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1839
2064
  @if (filtersError()) {
1840
2065
  <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1841
2066
  } @else {
1842
- <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
2067
+ <mat-hint>{{ ui.text('staticFiltersHint') }}</mat-hint>
1843
2068
  }
1844
2069
  </mat-form-field>
1845
2070
 
1846
2071
  <mat-divider></mat-divider>
1847
2072
 
1848
2073
  <!-- Cascade -->
1849
- <span class="deb-section-title">Cascade</span>
2074
+ <span class="deb-section-title">{{ ui.text('cascade') }}</span>
1850
2075
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1851
- <mat-label>Parent field</mat-label>
2076
+ <mat-label>{{ ui.text('parentField') }}</mat-label>
1852
2077
  <mat-select
1853
2078
  data-testid="entity-ref-parent"
1854
2079
  [ngModel]="ref(f).parentField ?? ''"
1855
2080
  (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1856
2081
  >
1857
- <mat-option value="">None — load all options</mat-option>
2082
+ <mat-option value="">{{ ui.text('parentFieldNone') }}</mat-option>
1858
2083
  @for (option of parentOptions(); track option.value) {
1859
2084
  <mat-option [value]="option.value">
1860
2085
  {{ option.label }} <span class="deb-path-hint">{{ option.path }}</span>
1861
2086
  </mat-option>
1862
2087
  }
1863
2088
  </mat-select>
1864
- <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
2089
+ <mat-hint>{{ ui.text('cascadeHint') }}</mat-hint>
1865
2090
  </mat-form-field>
1866
2091
 
1867
2092
  @if (ref(f).parentField) {
1868
2093
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1869
- <mat-label>Lookup filter path</mat-label>
2094
+ <mat-label>{{ ui.text('lookupFilterPath') }}</mat-label>
1870
2095
  <input
1871
2096
  matInput
1872
2097
  data-testid="entity-ref-lookup-filter"
1873
2098
  [ngModel]="ref(f).lookupFilter ?? ''"
1874
2099
  (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1875
2100
  />
1876
- <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
2101
+ <mat-hint>{{ ui.text('lookupFilterPathHint') }}</mat-hint>
1877
2102
  </mat-form-field>
1878
2103
 
1879
2104
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1880
- <mat-label>Lookup path (nested options)</mat-label>
2105
+ <mat-label>{{ ui.text('lookupPath') }}</mat-label>
1881
2106
  <input
1882
2107
  matInput
1883
2108
  data-testid="entity-ref-lookup-path"
1884
2109
  [ngModel]="ref(f).lookupPath ?? ''"
1885
2110
  (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1886
2111
  />
1887
- <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
2112
+ <mat-hint>{{ ui.text('lookupPathHint') }}</mat-hint>
1888
2113
  </mat-form-field>
1889
2114
  }
1890
2115
 
@@ -1892,20 +2117,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1892
2117
 
1893
2118
  <!-- autoPatch -->
1894
2119
  <div class="deb-row deb-row--split">
1895
- <span class="deb-section-title">Auto-patch on select</span>
2120
+ <span class="deb-section-title">{{ ui.text('autoPatchOnSelect') }}</span>
1896
2121
  <button
1897
2122
  mat-stroked-button
1898
2123
  type="button"
1899
2124
  data-testid="add-auto-patch"
1900
2125
  (click)="store.addAutoPatchMapping(store.keyOf(f))"
1901
2126
  >
1902
- <mat-icon>add</mat-icon> Mapping
2127
+ <mat-icon>add</mat-icon> {{ ui.text('mapping') }}
1903
2128
  </button>
1904
2129
  </div>
1905
2130
 
1906
2131
  @if (f.autoPatch) {
1907
2132
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1908
- <mat-label>Target tab</mat-label>
2133
+ <mat-label>{{ ui.text('targetTab') }}</mat-label>
1909
2134
  <mat-select
1910
2135
  data-testid="auto-patch-tab"
1911
2136
  [ngModel]="f.autoPatch.targetTab"
@@ -1920,7 +2145,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1920
2145
  @for (mapping of f.autoPatch.mappings; track $index) {
1921
2146
  <div class="deb-option-row">
1922
2147
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1923
- <mat-label>Source (linked record)</mat-label>
2148
+ <mat-label>{{ ui.text('sourceLinkedRecord') }}</mat-label>
1924
2149
  <input
1925
2150
  matInput
1926
2151
  [ngModel]="mapping.source"
@@ -1928,7 +2153,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1928
2153
  />
1929
2154
  </mat-form-field>
1930
2155
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1931
- <mat-label>Target field</mat-label>
2156
+ <mat-label>{{ ui.text('targetField') }}</mat-label>
1932
2157
  <mat-select
1933
2158
  data-testid="auto-patch-target"
1934
2159
  [ngModel]="mapping.target"
@@ -1945,7 +2170,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1945
2170
  mat-icon-button
1946
2171
  type="button"
1947
2172
  color="warn"
1948
- matTooltip="Remove mapping"
2173
+ [matTooltip]="ui.text('removeMapping')"
1949
2174
  (click)="store.removeAutoPatchMapping(store.keyOf(f), $index)"
1950
2175
  >
1951
2176
  <mat-icon>delete</mat-icon>
@@ -1954,7 +2179,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1954
2179
  }
1955
2180
  } @else {
1956
2181
  <p class="deb-hint">
1957
- No auto-patch configured — selecting a reference will not copy any values.
2182
+ {{ ui.text('noAutoPatch') }}
1958
2183
  </p>
1959
2184
  }
1960
2185
  </div>
@@ -1966,6 +2191,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1966
2191
  * RuleFormComponent — dialog/panel form for creating or editing a FormRule.
1967
2192
  */
1968
2193
  class RuleFormComponent {
2194
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2195
+ ui = inject(BuilderTextService);
1969
2196
  store = inject(BuilderStore);
1970
2197
  rule = {
1971
2198
  formConfigId: 'form-1',
@@ -2021,8 +2248,7 @@ class RuleFormComponent {
2021
2248
  */
2022
2249
  targetValues() {
2023
2250
  const ids = this.rule.targets.filter(t => t.type === 'field').map(t => t.id);
2024
- const unchanged = ids.length === this.cachedTargets.length &&
2025
- ids.every((id, i) => id === this.cachedTargets[i]);
2251
+ const unchanged = ids.length === this.cachedTargets.length && ids.every((id, i) => id === this.cachedTargets[i]);
2026
2252
  if (!unchanged)
2027
2253
  this.cachedTargets = ids;
2028
2254
  return this.cachedTargets;
@@ -2046,11 +2272,11 @@ class RuleFormComponent {
2046
2272
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2047
2273
  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: `
2048
2274
  <div class="deb-rule-form">
2049
- <h3 class="deb-rule-form__title">{{ rule.id ? 'Edit Rule' : 'New Form Rule' }}</h3>
2275
+ <h3 class="deb-rule-form__title">{{ ui.text(rule.id ? 'editRuleTitle' : 'newRuleTitle') }}</h3>
2050
2276
 
2051
2277
  <!-- Trigger Field -->
2052
2278
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2053
- <mat-label>Trigger Field</mat-label>
2279
+ <mat-label>{{ ui.text('triggerField') }}</mat-label>
2054
2280
  <mat-select [(ngModel)]="rule.fieldId" data-testid="rule-trigger">
2055
2281
  @for (option of triggerOptions(); track option.value) {
2056
2282
  <mat-option [value]="option.value">
@@ -2062,13 +2288,8 @@ class RuleFormComponent {
2062
2288
 
2063
2289
  <!-- Targets: the fields this rule acts on -->
2064
2290
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2065
- <mat-label>Apply to fields</mat-label>
2066
- <mat-select
2067
- multiple
2068
- data-testid="rule-targets"
2069
- [ngModel]="targetValues()"
2070
- (ngModelChange)="setTargets($event)"
2071
- >
2291
+ <mat-label>{{ ui.text('applyToFields') }}</mat-label>
2292
+ <mat-select multiple data-testid="rule-targets" [ngModel]="targetValues()" (ngModelChange)="setTargets($event)">
2072
2293
  @for (option of targetOptions(); track option.value) {
2073
2294
  <mat-option [value]="option.value">
2074
2295
  {{ option.label }} <span class="deb-rule-path">{{ option.path }}</span>
@@ -2079,11 +2300,11 @@ class RuleFormComponent {
2079
2300
 
2080
2301
  <!-- Conditions -->
2081
2302
  <div class="deb-rule-section">
2082
- <h4>Conditions</h4>
2303
+ <h4>{{ ui.text('conditions') }}</h4>
2083
2304
  @for (cond of rule.conditions; track $index) {
2084
2305
  <div class="deb-rule-row">
2085
2306
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2086
- <mat-label>Operator</mat-label>
2307
+ <mat-label>{{ ui.text('operator') }}</mat-label>
2087
2308
  <mat-select [(ngModel)]="cond.operator">
2088
2309
  @for (op of operators; track op) {
2089
2310
  <mat-option [value]="op">{{ op }}</mat-option>
@@ -2091,8 +2312,8 @@ class RuleFormComponent {
2091
2312
  </mat-select>
2092
2313
  </mat-form-field>
2093
2314
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2094
- <mat-label>Value</mat-label>
2095
- <input matInput [(ngModel)]="cond.value" placeholder="Condition value" />
2315
+ <mat-label>{{ ui.text('value') }}</mat-label>
2316
+ <input matInput [(ngModel)]="cond.value" [placeholder]="ui.text('conditionValuePlaceholder')" />
2096
2317
  </mat-form-field>
2097
2318
  <button mat-icon-button color="warn" type="button" (click)="removeCondition($index)">
2098
2319
  <mat-icon>delete</mat-icon>
@@ -2100,53 +2321,45 @@ class RuleFormComponent {
2100
2321
  </div>
2101
2322
  }
2102
2323
  <button mat-stroked-button type="button" (click)="addCondition()">
2103
- <mat-icon>add</mat-icon> Add Condition
2324
+ <mat-icon>add</mat-icon> {{ ui.text('addCondition') }}
2104
2325
  </button>
2105
2326
  </div>
2106
2327
 
2107
2328
  <!-- Action -->
2108
2329
  <div class="deb-rule-section">
2109
- <h4>Action</h4>
2330
+ <h4>{{ ui.text('action') }}</h4>
2110
2331
  <div class="deb-rule-row">
2111
2332
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2112
- <mat-label>Action Type</mat-label>
2333
+ <mat-label>{{ ui.text('actionType') }}</mat-label>
2113
2334
  <mat-select [(ngModel)]="rule.action.type">
2114
- <mat-option value="visibility">Visibility (Show/Hide)</mat-option>
2115
- <mat-option value="validation">Validation Message</mat-option>
2116
- <mat-option value="info">Info Banner</mat-option>
2335
+ <mat-option value="visibility">{{ ui.text('actionVisibility') }}</mat-option>
2336
+ <mat-option value="validation">{{ ui.text('actionValidationMessage') }}</mat-option>
2337
+ <mat-option value="info">{{ ui.text('actionInfoBanner') }}</mat-option>
2117
2338
  </mat-select>
2118
2339
  </mat-form-field>
2119
2340
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2120
- <mat-label>Value / Message</mat-label>
2121
- <input matInput [(ngModel)]="rule.action.value" placeholder="false to hide, or message text" />
2341
+ <mat-label>{{ ui.text('valueOrMessage') }}</mat-label>
2342
+ <input matInput [(ngModel)]="rule.action.value" [placeholder]="ui.text('valueOrMessagePlaceholder')" />
2122
2343
  </mat-form-field>
2123
2344
  </div>
2124
2345
  </div>
2125
2346
 
2126
2347
  <div class="deb-rule-form__actions">
2127
- <button mat-button type="button" (click)="cancel.emit()">Cancel</button>
2128
- <button mat-raised-button color="primary" type="button" (click)="save.emit(rule)">Save Rule</button>
2348
+ <button mat-button type="button" (click)="cancel.emit()">{{ ui.text('cancel') }}</button>
2349
+ <button mat-raised-button color="primary" type="button" (click)="save.emit(rule)">{{ ui.text('saveRule') }}</button>
2129
2350
  </div>
2130
2351
  </div>
2131
2352
  `, 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: i5.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: i8.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: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2132
2353
  }
2133
2354
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, decorators: [{
2134
2355
  type: Component,
2135
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-form', standalone: true, imports: [
2136
- CommonModule,
2137
- FormsModule,
2138
- MatButtonModule,
2139
- MatFormFieldModule,
2140
- MatIconModule,
2141
- MatInputModule,
2142
- MatSelectModule,
2143
- ], template: `
2356
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-form', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule], template: `
2144
2357
  <div class="deb-rule-form">
2145
- <h3 class="deb-rule-form__title">{{ rule.id ? 'Edit Rule' : 'New Form Rule' }}</h3>
2358
+ <h3 class="deb-rule-form__title">{{ ui.text(rule.id ? 'editRuleTitle' : 'newRuleTitle') }}</h3>
2146
2359
 
2147
2360
  <!-- Trigger Field -->
2148
2361
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2149
- <mat-label>Trigger Field</mat-label>
2362
+ <mat-label>{{ ui.text('triggerField') }}</mat-label>
2150
2363
  <mat-select [(ngModel)]="rule.fieldId" data-testid="rule-trigger">
2151
2364
  @for (option of triggerOptions(); track option.value) {
2152
2365
  <mat-option [value]="option.value">
@@ -2158,13 +2371,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2158
2371
 
2159
2372
  <!-- Targets: the fields this rule acts on -->
2160
2373
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2161
- <mat-label>Apply to fields</mat-label>
2162
- <mat-select
2163
- multiple
2164
- data-testid="rule-targets"
2165
- [ngModel]="targetValues()"
2166
- (ngModelChange)="setTargets($event)"
2167
- >
2374
+ <mat-label>{{ ui.text('applyToFields') }}</mat-label>
2375
+ <mat-select multiple data-testid="rule-targets" [ngModel]="targetValues()" (ngModelChange)="setTargets($event)">
2168
2376
  @for (option of targetOptions(); track option.value) {
2169
2377
  <mat-option [value]="option.value">
2170
2378
  {{ option.label }} <span class="deb-rule-path">{{ option.path }}</span>
@@ -2175,11 +2383,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2175
2383
 
2176
2384
  <!-- Conditions -->
2177
2385
  <div class="deb-rule-section">
2178
- <h4>Conditions</h4>
2386
+ <h4>{{ ui.text('conditions') }}</h4>
2179
2387
  @for (cond of rule.conditions; track $index) {
2180
2388
  <div class="deb-rule-row">
2181
2389
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2182
- <mat-label>Operator</mat-label>
2390
+ <mat-label>{{ ui.text('operator') }}</mat-label>
2183
2391
  <mat-select [(ngModel)]="cond.operator">
2184
2392
  @for (op of operators; track op) {
2185
2393
  <mat-option [value]="op">{{ op }}</mat-option>
@@ -2187,8 +2395,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2187
2395
  </mat-select>
2188
2396
  </mat-form-field>
2189
2397
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2190
- <mat-label>Value</mat-label>
2191
- <input matInput [(ngModel)]="cond.value" placeholder="Condition value" />
2398
+ <mat-label>{{ ui.text('value') }}</mat-label>
2399
+ <input matInput [(ngModel)]="cond.value" [placeholder]="ui.text('conditionValuePlaceholder')" />
2192
2400
  </mat-form-field>
2193
2401
  <button mat-icon-button color="warn" type="button" (click)="removeCondition($index)">
2194
2402
  <mat-icon>delete</mat-icon>
@@ -2196,32 +2404,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2196
2404
  </div>
2197
2405
  }
2198
2406
  <button mat-stroked-button type="button" (click)="addCondition()">
2199
- <mat-icon>add</mat-icon> Add Condition
2407
+ <mat-icon>add</mat-icon> {{ ui.text('addCondition') }}
2200
2408
  </button>
2201
2409
  </div>
2202
2410
 
2203
2411
  <!-- Action -->
2204
2412
  <div class="deb-rule-section">
2205
- <h4>Action</h4>
2413
+ <h4>{{ ui.text('action') }}</h4>
2206
2414
  <div class="deb-rule-row">
2207
2415
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2208
- <mat-label>Action Type</mat-label>
2416
+ <mat-label>{{ ui.text('actionType') }}</mat-label>
2209
2417
  <mat-select [(ngModel)]="rule.action.type">
2210
- <mat-option value="visibility">Visibility (Show/Hide)</mat-option>
2211
- <mat-option value="validation">Validation Message</mat-option>
2212
- <mat-option value="info">Info Banner</mat-option>
2418
+ <mat-option value="visibility">{{ ui.text('actionVisibility') }}</mat-option>
2419
+ <mat-option value="validation">{{ ui.text('actionValidationMessage') }}</mat-option>
2420
+ <mat-option value="info">{{ ui.text('actionInfoBanner') }}</mat-option>
2213
2421
  </mat-select>
2214
2422
  </mat-form-field>
2215
2423
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2216
- <mat-label>Value / Message</mat-label>
2217
- <input matInput [(ngModel)]="rule.action.value" placeholder="false to hide, or message text" />
2424
+ <mat-label>{{ ui.text('valueOrMessage') }}</mat-label>
2425
+ <input matInput [(ngModel)]="rule.action.value" [placeholder]="ui.text('valueOrMessagePlaceholder')" />
2218
2426
  </mat-form-field>
2219
2427
  </div>
2220
2428
  </div>
2221
2429
 
2222
2430
  <div class="deb-rule-form__actions">
2223
- <button mat-button type="button" (click)="cancel.emit()">Cancel</button>
2224
- <button mat-raised-button color="primary" type="button" (click)="save.emit(rule)">Save Rule</button>
2431
+ <button mat-button type="button" (click)="cancel.emit()">{{ ui.text('cancel') }}</button>
2432
+ <button mat-raised-button color="primary" type="button" (click)="save.emit(rule)">{{ ui.text('saveRule') }}</button>
2225
2433
  </div>
2226
2434
  </div>
2227
2435
  `, 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"] }]
@@ -2238,6 +2446,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2238
2446
  * enable/disable, edit, delete. `RuleFormComponent` authors one rule; this owns the set.
2239
2447
  */
2240
2448
  class FieldRulesListComponent {
2449
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2450
+ ui = inject(BuilderTextService);
2241
2451
  store = inject(BuilderStore);
2242
2452
  /** The rule currently open in the editor — a new draft or a copy of an existing rule. */
2243
2453
  editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
@@ -2289,7 +2499,7 @@ class FieldRulesListComponent {
2289
2499
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
2290
2500
  <div class="deb-rules">
2291
2501
  <div class="deb-rules__head">
2292
- <span class="deb-section-title">Rules</span>
2502
+ <span class="deb-section-title">{{ ui.text('rules') }}</span>
2293
2503
  <button
2294
2504
  mat-stroked-button
2295
2505
  type="button"
@@ -2297,7 +2507,7 @@ class FieldRulesListComponent {
2297
2507
  [disabled]="!store.selectedFieldId()"
2298
2508
  (click)="startCreate()"
2299
2509
  >
2300
- <mat-icon>add</mat-icon> Rule
2510
+ <mat-icon>add</mat-icon> {{ ui.text('rule') }}
2301
2511
  </button>
2302
2512
  </div>
2303
2513
 
@@ -2306,7 +2516,7 @@ class FieldRulesListComponent {
2306
2516
  }
2307
2517
 
2308
2518
  @if (store.rulesForSelectedField().length === 0 && !editing()) {
2309
- <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
2519
+ <p class="deb-hint" data-testid="rules-empty">{{ ui.text('noRulesOnField') }}</p>
2310
2520
  }
2311
2521
 
2312
2522
  @for (rule of store.rulesForSelectedField(); track rule.id) {
@@ -2314,19 +2524,20 @@ class FieldRulesListComponent {
2314
2524
  <div class="deb-rule-item__body">
2315
2525
  <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
2316
2526
  <span class="deb-rule-item__meta">
2317
- priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
2527
+ {{ ui.text('rulePriority', { priority: rule.priority, targets: rule.targets.length }) }}
2318
2528
  </span>
2319
2529
  </div>
2320
2530
  <div class="deb-rule-item__actions">
2321
2531
  <mat-slide-toggle
2322
2532
  [checked]="rule.enabled"
2323
- matTooltip="Enable / disable"
2533
+ [matTooltip]="ui.text('toggleRule')"
2324
2534
  (change)="store.toggleRule(rule.id!, $event.checked)"
2325
2535
  />
2326
2536
  <button
2327
2537
  mat-icon-button
2328
2538
  type="button"
2329
- matTooltip="Move up" aria-label="Move rule up"
2539
+ [matTooltip]="ui.text('moveUp')"
2540
+ [attr.aria-label]="ui.text('moveRuleUp')"
2330
2541
  [attr.data-testid]="'rule-up-' + rule.id"
2331
2542
  (click)="store.moveRule(rule.id!, -1)"
2332
2543
  >
@@ -2335,7 +2546,8 @@ class FieldRulesListComponent {
2335
2546
  <button
2336
2547
  mat-icon-button
2337
2548
  type="button"
2338
- matTooltip="Move down" aria-label="Move rule down"
2549
+ [matTooltip]="ui.text('moveDown')"
2550
+ [attr.aria-label]="ui.text('moveRuleDown')"
2339
2551
  [attr.data-testid]="'rule-down-' + rule.id"
2340
2552
  (click)="store.moveRule(rule.id!, 1)"
2341
2553
  >
@@ -2344,7 +2556,7 @@ class FieldRulesListComponent {
2344
2556
  <button
2345
2557
  mat-icon-button
2346
2558
  type="button"
2347
- matTooltip="Edit rule"
2559
+ [matTooltip]="ui.text('editRule')"
2348
2560
  [attr.data-testid]="'rule-edit-' + rule.id"
2349
2561
  (click)="startEdit(rule)"
2350
2562
  >
@@ -2354,7 +2566,7 @@ class FieldRulesListComponent {
2354
2566
  mat-icon-button
2355
2567
  type="button"
2356
2568
  color="warn"
2357
- matTooltip="Delete rule"
2569
+ [matTooltip]="ui.text('deleteRule')"
2358
2570
  [attr.data-testid]="'rule-delete-' + rule.id"
2359
2571
  (click)="store.removeRule(rule.id!)"
2360
2572
  >
@@ -2371,7 +2583,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2371
2583
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
2372
2584
  <div class="deb-rules">
2373
2585
  <div class="deb-rules__head">
2374
- <span class="deb-section-title">Rules</span>
2586
+ <span class="deb-section-title">{{ ui.text('rules') }}</span>
2375
2587
  <button
2376
2588
  mat-stroked-button
2377
2589
  type="button"
@@ -2379,7 +2591,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2379
2591
  [disabled]="!store.selectedFieldId()"
2380
2592
  (click)="startCreate()"
2381
2593
  >
2382
- <mat-icon>add</mat-icon> Rule
2594
+ <mat-icon>add</mat-icon> {{ ui.text('rule') }}
2383
2595
  </button>
2384
2596
  </div>
2385
2597
 
@@ -2388,7 +2600,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2388
2600
  }
2389
2601
 
2390
2602
  @if (store.rulesForSelectedField().length === 0 && !editing()) {
2391
- <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
2603
+ <p class="deb-hint" data-testid="rules-empty">{{ ui.text('noRulesOnField') }}</p>
2392
2604
  }
2393
2605
 
2394
2606
  @for (rule of store.rulesForSelectedField(); track rule.id) {
@@ -2396,19 +2608,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2396
2608
  <div class="deb-rule-item__body">
2397
2609
  <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
2398
2610
  <span class="deb-rule-item__meta">
2399
- priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
2611
+ {{ ui.text('rulePriority', { priority: rule.priority, targets: rule.targets.length }) }}
2400
2612
  </span>
2401
2613
  </div>
2402
2614
  <div class="deb-rule-item__actions">
2403
2615
  <mat-slide-toggle
2404
2616
  [checked]="rule.enabled"
2405
- matTooltip="Enable / disable"
2617
+ [matTooltip]="ui.text('toggleRule')"
2406
2618
  (change)="store.toggleRule(rule.id!, $event.checked)"
2407
2619
  />
2408
2620
  <button
2409
2621
  mat-icon-button
2410
2622
  type="button"
2411
- matTooltip="Move up" aria-label="Move rule up"
2623
+ [matTooltip]="ui.text('moveUp')"
2624
+ [attr.aria-label]="ui.text('moveRuleUp')"
2412
2625
  [attr.data-testid]="'rule-up-' + rule.id"
2413
2626
  (click)="store.moveRule(rule.id!, -1)"
2414
2627
  >
@@ -2417,7 +2630,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2417
2630
  <button
2418
2631
  mat-icon-button
2419
2632
  type="button"
2420
- matTooltip="Move down" aria-label="Move rule down"
2633
+ [matTooltip]="ui.text('moveDown')"
2634
+ [attr.aria-label]="ui.text('moveRuleDown')"
2421
2635
  [attr.data-testid]="'rule-down-' + rule.id"
2422
2636
  (click)="store.moveRule(rule.id!, 1)"
2423
2637
  >
@@ -2426,7 +2640,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2426
2640
  <button
2427
2641
  mat-icon-button
2428
2642
  type="button"
2429
- matTooltip="Edit rule"
2643
+ [matTooltip]="ui.text('editRule')"
2430
2644
  [attr.data-testid]="'rule-edit-' + rule.id"
2431
2645
  (click)="startEdit(rule)"
2432
2646
  >
@@ -2436,7 +2650,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2436
2650
  mat-icon-button
2437
2651
  type="button"
2438
2652
  color="warn"
2439
- matTooltip="Delete rule"
2653
+ [matTooltip]="ui.text('deleteRule')"
2440
2654
  [attr.data-testid]="'rule-delete-' + rule.id"
2441
2655
  (click)="store.removeRule(rule.id!)"
2442
2656
  >
@@ -2450,6 +2664,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2450
2664
  }] });
2451
2665
 
2452
2666
  class ReferencedFieldConfigComponent {
2667
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2668
+ ui = inject(BuilderTextService);
2453
2669
  store = inject(BuilderStore);
2454
2670
  configSource = inject(ConfigSourceService, { optional: true });
2455
2671
  field = computed(() => this.store.selectedField(), ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
@@ -2528,56 +2744,54 @@ class ReferencedFieldConfigComponent {
2528
2744
  @if (field(); as f) {
2529
2745
  <div class="deb-referenced-field" style="margin-top: 16px;">
2530
2746
  <div class="deb-row" style="justify-content: space-between; align-items: center; margin-bottom: 8px;">
2531
- <span class="deb-section-title" style="margin:0;">Referenced Field</span>
2747
+ <span class="deb-section-title" style="margin:0;">{{ ui.text('referencedField') }}</span>
2532
2748
  <mat-slide-toggle
2533
2749
  data-testid="toggle-referenced"
2534
2750
  [ngModel]="f.isReferenced ?? false"
2535
2751
  (ngModelChange)="toggleReferenced(f, $event)"
2536
2752
  >
2537
- Link
2753
+ {{ ui.text('link') }}
2538
2754
  </mat-slide-toggle>
2539
2755
  </div>
2540
2756
 
2541
2757
  @if (f.isReferenced) {
2542
2758
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2543
- <mat-label>Source Entity Key</mat-label>
2759
+ <mat-label>{{ ui.text('sourceEntityKey') }}</mat-label>
2544
2760
  <input
2545
2761
  matInput
2546
2762
  data-testid="referenced-entity-key"
2547
2763
  [ngModel]="f.referencedEntityKey ?? ''"
2548
2764
  (ngModelChange)="updateEntityKey(f, $event)"
2549
- placeholder="e.g. individuals"
2765
+ [placeholder]="ui.text('sourceEntityKeyPlaceholder')"
2550
2766
  />
2551
2767
  </mat-form-field>
2552
2768
 
2553
2769
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2554
- <mat-label>Source Field ID</mat-label>
2770
+ <mat-label>{{ ui.text('sourceFieldId') }}</mat-label>
2555
2771
  <input
2556
2772
  matInput
2557
2773
  data-testid="referenced-field-id"
2558
2774
  [ngModel]="f.referencedFieldId ?? ''"
2559
2775
  (ngModelChange)="updateFieldId(f, $event)"
2560
- placeholder="e.g. firstName"
2776
+ [placeholder]="ui.text('sourceFieldIdPlaceholder')"
2561
2777
  />
2562
2778
  </mat-form-field>
2563
2779
 
2564
2780
  @if (f.hasDrift) {
2565
- <div class="deb-drift-banner" data-testid="drift-banner" style="background: #fff3e0; border: 1px solid #ffe0b2; padding: 8px 12px; border-radius: 4px; margin-top: 8px;">
2781
+ <div
2782
+ class="deb-drift-banner"
2783
+ data-testid="drift-banner"
2784
+ style="background: #fff3e0; border: 1px solid #ffe0b2; padding: 8px 12px; border-radius: 4px; margin-top: 8px;"
2785
+ >
2566
2786
  <div class="deb-row" style="gap: 8px; align-items: center; color: #e65100; font-weight: 500;">
2567
2787
  <mat-icon style="font-size: 18px; width:18px; height:18px;">warning</mat-icon>
2568
- <span>Source field definition has drifted!</span>
2788
+ <span>{{ ui.text('driftHeading') }}</span>
2569
2789
  </div>
2570
2790
  <p style="margin: 4px 0 8px 0; font-size: 12px; color: #666;">
2571
- The upstream field configuration in "{{ f.referencedEntityKey }}" has evolved.
2791
+ {{ ui.text('driftBody', { entity: f.referencedEntityKey ?? '' }) }}
2572
2792
  </p>
2573
- <button
2574
- mat-flat-button
2575
- color="warn"
2576
- type="button"
2577
- data-testid="sync-source-btn"
2578
- (click)="syncWithSource(f)"
2579
- >
2580
- Sync with Source
2793
+ <button mat-flat-button color="warn" type="button" data-testid="sync-source-btn" (click)="syncWithSource(f)">
2794
+ {{ ui.text('syncWithSource') }}
2581
2795
  </button>
2582
2796
  </div>
2583
2797
  }
@@ -2606,56 +2820,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2606
2820
  @if (field(); as f) {
2607
2821
  <div class="deb-referenced-field" style="margin-top: 16px;">
2608
2822
  <div class="deb-row" style="justify-content: space-between; align-items: center; margin-bottom: 8px;">
2609
- <span class="deb-section-title" style="margin:0;">Referenced Field</span>
2823
+ <span class="deb-section-title" style="margin:0;">{{ ui.text('referencedField') }}</span>
2610
2824
  <mat-slide-toggle
2611
2825
  data-testid="toggle-referenced"
2612
2826
  [ngModel]="f.isReferenced ?? false"
2613
2827
  (ngModelChange)="toggleReferenced(f, $event)"
2614
2828
  >
2615
- Link
2829
+ {{ ui.text('link') }}
2616
2830
  </mat-slide-toggle>
2617
2831
  </div>
2618
2832
 
2619
2833
  @if (f.isReferenced) {
2620
2834
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2621
- <mat-label>Source Entity Key</mat-label>
2835
+ <mat-label>{{ ui.text('sourceEntityKey') }}</mat-label>
2622
2836
  <input
2623
2837
  matInput
2624
2838
  data-testid="referenced-entity-key"
2625
2839
  [ngModel]="f.referencedEntityKey ?? ''"
2626
2840
  (ngModelChange)="updateEntityKey(f, $event)"
2627
- placeholder="e.g. individuals"
2841
+ [placeholder]="ui.text('sourceEntityKeyPlaceholder')"
2628
2842
  />
2629
2843
  </mat-form-field>
2630
2844
 
2631
2845
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2632
- <mat-label>Source Field ID</mat-label>
2846
+ <mat-label>{{ ui.text('sourceFieldId') }}</mat-label>
2633
2847
  <input
2634
2848
  matInput
2635
2849
  data-testid="referenced-field-id"
2636
2850
  [ngModel]="f.referencedFieldId ?? ''"
2637
2851
  (ngModelChange)="updateFieldId(f, $event)"
2638
- placeholder="e.g. firstName"
2852
+ [placeholder]="ui.text('sourceFieldIdPlaceholder')"
2639
2853
  />
2640
2854
  </mat-form-field>
2641
2855
 
2642
2856
  @if (f.hasDrift) {
2643
- <div class="deb-drift-banner" data-testid="drift-banner" style="background: #fff3e0; border: 1px solid #ffe0b2; padding: 8px 12px; border-radius: 4px; margin-top: 8px;">
2857
+ <div
2858
+ class="deb-drift-banner"
2859
+ data-testid="drift-banner"
2860
+ style="background: #fff3e0; border: 1px solid #ffe0b2; padding: 8px 12px; border-radius: 4px; margin-top: 8px;"
2861
+ >
2644
2862
  <div class="deb-row" style="gap: 8px; align-items: center; color: #e65100; font-weight: 500;">
2645
2863
  <mat-icon style="font-size: 18px; width:18px; height:18px;">warning</mat-icon>
2646
- <span>Source field definition has drifted!</span>
2864
+ <span>{{ ui.text('driftHeading') }}</span>
2647
2865
  </div>
2648
2866
  <p style="margin: 4px 0 8px 0; font-size: 12px; color: #666;">
2649
- The upstream field configuration in "{{ f.referencedEntityKey }}" has evolved.
2867
+ {{ ui.text('driftBody', { entity: f.referencedEntityKey ?? '' }) }}
2650
2868
  </p>
2651
- <button
2652
- mat-flat-button
2653
- color="warn"
2654
- type="button"
2655
- data-testid="sync-source-btn"
2656
- (click)="syncWithSource(f)"
2657
- >
2658
- Sync with Source
2869
+ <button mat-flat-button color="warn" type="button" data-testid="sync-source-btn" (click)="syncWithSource(f)">
2870
+ {{ ui.text('syncWithSource') }}
2659
2871
  </button>
2660
2872
  </div>
2661
2873
  }
@@ -2670,6 +2882,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2670
2882
  * FieldInspectorComponent — edits every property of the currently selected field.
2671
2883
  */
2672
2884
  class FieldInspectorComponent {
2885
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2886
+ ui = inject(BuilderTextService);
2673
2887
  store = inject(BuilderStore);
2674
2888
  field = this.store.selectedField;
2675
2889
  meta = computed(() => {
@@ -2806,7 +3020,7 @@ class FieldInspectorComponent {
2806
3020
  return String(value);
2807
3021
  }
2808
3022
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2809
- 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 <div class=\"deb-inspector\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Field id</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\n } @else {\n <mat-hint>Derived from the label. Used as the data key.</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>Tab</mat-label>\n <mat-select data-testid=\"field-tab\"\n [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>Moving the field rewrites its path and repoints the rules that named it.</mat-hint>\n </mat-form-field>\n }\n\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Label ({{ 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>Placeholder ({{ 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 @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Default value</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\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">Validation</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox>\n }\n </div>\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 }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\n <span class=\"deb-section-title\">Data source</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Options come from</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\">Nothing yet</mat-option>\n <mat-option value=\"manual\">Options authored here</mat-option>\n <mat-option value=\"lookup\">A named list</mat-option>\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>List name</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 The consuming app resolves this name through its lookup registry. Options are not\n authored here.\n </p>\n }\n\n <!-- Inline options -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\n <span class=\"deb-section-title\">Options</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(store.keyOf(f))\">\n <mat-icon>add</mat-icon> Option\n </button>\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>Option ({{ 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 <button mat-icon-button type=\"button\" color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\" matTooltip=\"Remove option\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">Display</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >Visible</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >Table column</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >Read-only</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >Disabled</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >Mask data (RBAC)</mat-slide-toggle>\n <mat-slide-toggle\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\n >Critical field</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n matTooltip=\"Shown in the record editor's summary panel.\"\n >Show in summary</mat-slide-toggle>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Show when</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> 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>Watched field</mat-label>\n <mat-select data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\">\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>Equals</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\n (click)=\"removeShowWhen(f, entry.key)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Copy on true</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\">\n <mat-icon>add</mat-icon> 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>From field</mat-label>\n <mat-select data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\">\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>To field</mat-label>\n <mat-select data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\">\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 mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>Select a field to edit its properties.</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{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.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-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\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$1.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: "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: i5.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: i8.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: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.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" }] });
3023
+ 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 <div class=\"deb-inspector\">\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 @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\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">{{ ui.text('validation') }}</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\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 }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\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\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && 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 (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(store.keyOf(f))\">\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\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 <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\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">{{ ui.text('display') }}</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\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 [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 [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 [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 [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 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 [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\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\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\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\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\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\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{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.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-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\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$1.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: "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: i5.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: i8.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: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.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" }] });
2810
3024
  }
2811
3025
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, decorators: [{
2812
3026
  type: Component,
@@ -2824,7 +3038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2824
3038
  EntityReferenceConfigComponent,
2825
3039
  FieldRulesListComponent,
2826
3040
  ReferencedFieldConfigComponent,
2827
- ], template: "@if (field(); as f) {\n <div class=\"deb-inspector\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Field id</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\n } @else {\n <mat-hint>Derived from the label. Used as the data key.</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>Tab</mat-label>\n <mat-select data-testid=\"field-tab\"\n [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>Moving the field rewrites its path and repoints the rules that named it.</mat-hint>\n </mat-form-field>\n }\n\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Label ({{ 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>Placeholder ({{ 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 @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Default value</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\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">Validation</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox>\n }\n </div>\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 }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\n <span class=\"deb-section-title\">Data source</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Options come from</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\">Nothing yet</mat-option>\n <mat-option value=\"manual\">Options authored here</mat-option>\n <mat-option value=\"lookup\">A named list</mat-option>\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>List name</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 The consuming app resolves this name through its lookup registry. Options are not\n authored here.\n </p>\n }\n\n <!-- Inline options -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\n <span class=\"deb-section-title\">Options</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(store.keyOf(f))\">\n <mat-icon>add</mat-icon> Option\n </button>\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>Option ({{ 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 <button mat-icon-button type=\"button\" color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\" matTooltip=\"Remove option\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">Display</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >Visible</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >Table column</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >Read-only</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >Disabled</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >Mask data (RBAC)</mat-slide-toggle>\n <mat-slide-toggle\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\n >Critical field</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n matTooltip=\"Shown in the record editor's summary panel.\"\n >Show in summary</mat-slide-toggle>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Show when</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> 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>Watched field</mat-label>\n <mat-select data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\">\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>Equals</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\n (click)=\"removeShowWhen(f, entry.key)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Copy on true</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\">\n <mat-icon>add</mat-icon> 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>From field</mat-label>\n <mat-select data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\">\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>To field</mat-label>\n <mat-select data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\">\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 mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>Select a field to edit its properties.</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{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.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-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\n"] }]
3041
+ ], template: "@if (field(); as f) {\n <div class=\"deb-inspector\">\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 @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\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">{{ ui.text('validation') }}</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\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 }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\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\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && 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 (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(store.keyOf(f))\">\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\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 <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\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">{{ ui.text('display') }}</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\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 [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 [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 [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 [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 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 [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\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\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\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\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\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\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{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.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-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\n"] }]
2828
3042
  }] });
2829
3043
 
2830
3044
  /**
@@ -2879,8 +3093,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2879
3093
  * TabManagerComponent — add / rename / reorder / remove tabs & sub-tabs with systemDefault protection.
2880
3094
  */
2881
3095
  class TabManagerComponent {
3096
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3097
+ ui = inject(BuilderTextService);
2882
3098
  store = inject(BuilderStore);
2883
- canEditSystemDefaultsFn = inject(SYSTEM_DEFAULT_CAN_EDIT, { optional: true });
3099
+ canEditSystemDefaultsFn = inject(SYSTEM_DEFAULT_CAN_EDIT, {
3100
+ optional: true,
3101
+ });
2884
3102
  lang() {
2885
3103
  return this.store.activeLanguage();
2886
3104
  }
@@ -2902,21 +3120,21 @@ class TabManagerComponent {
2902
3120
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
2903
3121
  <div class="deb-tabs">
2904
3122
  <div class="deb-tabs__head">
2905
- <span class="deb-section-title">Tabs Manager</span>
3123
+ <span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
2906
3124
  <button mat-stroked-button type="button" (click)="store.addTab()">
2907
- <mat-icon>add</mat-icon> Add Tab
3125
+ <mat-icon>add</mat-icon> {{ ui.text('addTab') }}
2908
3126
  </button>
2909
3127
  </div>
2910
3128
 
2911
3129
  @if (store.tabs().length === 0) {
2912
- <p class="deb-hint">No tabs — all fields render in a single section.</p>
3130
+ <p class="deb-hint">{{ ui.text('noTabs') }}</p>
2913
3131
  }
2914
3132
 
2915
3133
  @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2916
3134
  <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2917
3135
  <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2918
3136
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2919
- <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
3137
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? ui.text('systemTag') : '' }}</mat-label>
2920
3138
  <input
2921
3139
  matInput
2922
3140
  [disabled]="!canEditTab(tab)"
@@ -2929,7 +3147,8 @@ class TabManagerComponent {
2929
3147
  type="button"
2930
3148
  [disabled]="i === 0 || !canEditTab(tab)"
2931
3149
  (click)="store.moveTab(tab.id, -1)"
2932
- matTooltip="Move up" aria-label="Move tab up"
3150
+ [matTooltip]="ui.text('moveUp')"
3151
+ [attr.aria-label]="ui.text('moveTabUp')"
2933
3152
  >
2934
3153
  <mat-icon>arrow_upward</mat-icon>
2935
3154
  </button>
@@ -2938,7 +3157,8 @@ class TabManagerComponent {
2938
3157
  type="button"
2939
3158
  [disabled]="i === count - 1 || !canEditTab(tab)"
2940
3159
  (click)="store.moveTab(tab.id, 1)"
2941
- matTooltip="Move down" aria-label="Move tab down"
3160
+ [matTooltip]="ui.text('moveDown')"
3161
+ [attr.aria-label]="ui.text('moveTabDown')"
2942
3162
  >
2943
3163
  <mat-icon>arrow_downward</mat-icon>
2944
3164
  </button>
@@ -2948,7 +3168,8 @@ class TabManagerComponent {
2948
3168
  color="warn"
2949
3169
  [disabled]="!canEditTab(tab)"
2950
3170
  (click)="store.removeTab(tab.id)"
2951
- matTooltip="Remove tab" aria-label="Remove tab"
3171
+ [matTooltip]="ui.text('removeTab')"
3172
+ [attr.aria-label]="ui.text('removeTab')"
2952
3173
  >
2953
3174
  <mat-icon>delete</mat-icon>
2954
3175
  </button>
@@ -2961,47 +3182,41 @@ class TabManagerComponent {
2961
3182
  [ngModel]="tab.flatData"
2962
3183
  (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2963
3184
  >
2964
- Flat Data
3185
+ {{ ui.text('flatData') }}
2965
3186
  </mat-checkbox>
2966
3187
  <mat-checkbox
2967
3188
  [disabled]="!canEditTab(tab)"
2968
3189
  [ngModel]="tab.isPrimaryTab"
2969
3190
  (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2970
3191
  >
2971
- Primary Tab
3192
+ {{ ui.text('primaryTab') }}
2972
3193
  </mat-checkbox>
2973
3194
  <mat-checkbox
2974
3195
  [disabled]="!canEditTab(tab)"
2975
3196
  [ngModel]="tab.maskData"
2976
3197
  (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2977
3198
  >
2978
- Mask Tab Data
3199
+ {{ ui.text('maskTabData') }}
2979
3200
  </mat-checkbox>
2980
3201
  <mat-checkbox
2981
3202
  [disabled]="!canEditTab(tab)"
2982
3203
  [ngModel]="tab.systemDefault"
2983
3204
  (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2984
3205
  >
2985
- System Default
3206
+ {{ ui.text('systemDefault') }}
2986
3207
  </mat-checkbox>
2987
- <button
2988
- mat-button
2989
- type="button"
2990
- color="primary"
2991
- [disabled]="!canEditTab(tab)"
2992
- (click)="store.addSubTab(tab.id)"
2993
- >
2994
- + Sub-tab
3208
+ <button mat-button type="button" color="primary" [disabled]="!canEditTab(tab)" (click)="store.addSubTab(tab.id)">
3209
+ {{ ui.text('addSubTab') }}
2995
3210
  </button>
2996
3211
  </div>
2997
3212
 
2998
3213
  <!-- Module tab configuration -->
2999
3214
  <div class="deb-tab-module">
3000
3215
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
3001
- <mat-label>Consumer Module Name (Optional)</mat-label>
3216
+ <mat-label>{{ ui.text('consumerModuleName') }}</mat-label>
3002
3217
  <input
3003
3218
  matInput
3004
- placeholder="e.g. documents-view"
3219
+ [placeholder]="ui.text('consumerModulePlaceholder')"
3005
3220
  [disabled]="!canEditTab(tab)"
3006
3221
  [ngModel]="tab.moduleName"
3007
3222
  (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
@@ -3012,7 +3227,7 @@ class TabManagerComponent {
3012
3227
  <!-- Sub-tabs recursive render (Level 2) -->
3013
3228
  @if (tab.children && tab.children.length > 0) {
3014
3229
  <div class="deb-subtabs">
3015
- <span class="deb-subtabs__title">Sub-tabs:</span>
3230
+ <span class="deb-subtabs__title">{{ ui.text('subTabs') }}</span>
3016
3231
  @for (sub of tab.children; track sub.id) {
3017
3232
  <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
3018
3233
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
@@ -3030,7 +3245,8 @@ class TabManagerComponent {
3030
3245
  color="warn"
3031
3246
  [disabled]="!canEditTab(sub)"
3032
3247
  (click)="store.removeTab(sub.id)"
3033
- matTooltip="Remove sub-tab" aria-label="Remove sub-tab"
3248
+ [matTooltip]="ui.text('removeSubTab')"
3249
+ [attr.aria-label]="ui.text('removeSubTab')"
3034
3250
  >
3035
3251
  <mat-icon>delete</mat-icon>
3036
3252
  </button>
@@ -3057,21 +3273,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3057
3273
  ], template: `
3058
3274
  <div class="deb-tabs">
3059
3275
  <div class="deb-tabs__head">
3060
- <span class="deb-section-title">Tabs Manager</span>
3276
+ <span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
3061
3277
  <button mat-stroked-button type="button" (click)="store.addTab()">
3062
- <mat-icon>add</mat-icon> Add Tab
3278
+ <mat-icon>add</mat-icon> {{ ui.text('addTab') }}
3063
3279
  </button>
3064
3280
  </div>
3065
3281
 
3066
3282
  @if (store.tabs().length === 0) {
3067
- <p class="deb-hint">No tabs — all fields render in a single section.</p>
3283
+ <p class="deb-hint">{{ ui.text('noTabs') }}</p>
3068
3284
  }
3069
3285
 
3070
3286
  @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
3071
3287
  <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
3072
3288
  <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
3073
3289
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
3074
- <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
3290
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? ui.text('systemTag') : '' }}</mat-label>
3075
3291
  <input
3076
3292
  matInput
3077
3293
  [disabled]="!canEditTab(tab)"
@@ -3084,7 +3300,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3084
3300
  type="button"
3085
3301
  [disabled]="i === 0 || !canEditTab(tab)"
3086
3302
  (click)="store.moveTab(tab.id, -1)"
3087
- matTooltip="Move up" aria-label="Move tab up"
3303
+ [matTooltip]="ui.text('moveUp')"
3304
+ [attr.aria-label]="ui.text('moveTabUp')"
3088
3305
  >
3089
3306
  <mat-icon>arrow_upward</mat-icon>
3090
3307
  </button>
@@ -3093,7 +3310,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3093
3310
  type="button"
3094
3311
  [disabled]="i === count - 1 || !canEditTab(tab)"
3095
3312
  (click)="store.moveTab(tab.id, 1)"
3096
- matTooltip="Move down" aria-label="Move tab down"
3313
+ [matTooltip]="ui.text('moveDown')"
3314
+ [attr.aria-label]="ui.text('moveTabDown')"
3097
3315
  >
3098
3316
  <mat-icon>arrow_downward</mat-icon>
3099
3317
  </button>
@@ -3103,7 +3321,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3103
3321
  color="warn"
3104
3322
  [disabled]="!canEditTab(tab)"
3105
3323
  (click)="store.removeTab(tab.id)"
3106
- matTooltip="Remove tab" aria-label="Remove tab"
3324
+ [matTooltip]="ui.text('removeTab')"
3325
+ [attr.aria-label]="ui.text('removeTab')"
3107
3326
  >
3108
3327
  <mat-icon>delete</mat-icon>
3109
3328
  </button>
@@ -3116,47 +3335,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3116
3335
  [ngModel]="tab.flatData"
3117
3336
  (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
3118
3337
  >
3119
- Flat Data
3338
+ {{ ui.text('flatData') }}
3120
3339
  </mat-checkbox>
3121
3340
  <mat-checkbox
3122
3341
  [disabled]="!canEditTab(tab)"
3123
3342
  [ngModel]="tab.isPrimaryTab"
3124
3343
  (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
3125
3344
  >
3126
- Primary Tab
3345
+ {{ ui.text('primaryTab') }}
3127
3346
  </mat-checkbox>
3128
3347
  <mat-checkbox
3129
3348
  [disabled]="!canEditTab(tab)"
3130
3349
  [ngModel]="tab.maskData"
3131
3350
  (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
3132
3351
  >
3133
- Mask Tab Data
3352
+ {{ ui.text('maskTabData') }}
3134
3353
  </mat-checkbox>
3135
3354
  <mat-checkbox
3136
3355
  [disabled]="!canEditTab(tab)"
3137
3356
  [ngModel]="tab.systemDefault"
3138
3357
  (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
3139
3358
  >
3140
- System Default
3359
+ {{ ui.text('systemDefault') }}
3141
3360
  </mat-checkbox>
3142
- <button
3143
- mat-button
3144
- type="button"
3145
- color="primary"
3146
- [disabled]="!canEditTab(tab)"
3147
- (click)="store.addSubTab(tab.id)"
3148
- >
3149
- + Sub-tab
3361
+ <button mat-button type="button" color="primary" [disabled]="!canEditTab(tab)" (click)="store.addSubTab(tab.id)">
3362
+ {{ ui.text('addSubTab') }}
3150
3363
  </button>
3151
3364
  </div>
3152
3365
 
3153
3366
  <!-- Module tab configuration -->
3154
3367
  <div class="deb-tab-module">
3155
3368
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
3156
- <mat-label>Consumer Module Name (Optional)</mat-label>
3369
+ <mat-label>{{ ui.text('consumerModuleName') }}</mat-label>
3157
3370
  <input
3158
3371
  matInput
3159
- placeholder="e.g. documents-view"
3372
+ [placeholder]="ui.text('consumerModulePlaceholder')"
3160
3373
  [disabled]="!canEditTab(tab)"
3161
3374
  [ngModel]="tab.moduleName"
3162
3375
  (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
@@ -3167,7 +3380,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3167
3380
  <!-- Sub-tabs recursive render (Level 2) -->
3168
3381
  @if (tab.children && tab.children.length > 0) {
3169
3382
  <div class="deb-subtabs">
3170
- <span class="deb-subtabs__title">Sub-tabs:</span>
3383
+ <span class="deb-subtabs__title">{{ ui.text('subTabs') }}</span>
3171
3384
  @for (sub of tab.children; track sub.id) {
3172
3385
  <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
3173
3386
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
@@ -3185,7 +3398,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3185
3398
  color="warn"
3186
3399
  [disabled]="!canEditTab(sub)"
3187
3400
  (click)="store.removeTab(sub.id)"
3188
- matTooltip="Remove sub-tab" aria-label="Remove sub-tab"
3401
+ [matTooltip]="ui.text('removeSubTab')"
3402
+ [attr.aria-label]="ui.text('removeSubTab')"
3189
3403
  >
3190
3404
  <mat-icon>delete</mat-icon>
3191
3405
  </button>
@@ -3200,6 +3414,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3200
3414
  }] });
3201
3415
 
3202
3416
  class EntityBuilderTreeNodeComponent {
3417
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3418
+ ui = inject(BuilderTextService);
3203
3419
  store = inject(BuilderStore);
3204
3420
  field;
3205
3421
  index;
@@ -3227,58 +3443,89 @@ class EntityBuilderTreeNodeComponent {
3227
3443
  role="button"
3228
3444
  tabindex="0"
3229
3445
  [attr.aria-pressed]="store.isSelected(field)"
3230
- [attr.aria-label]="'Select field ' + fieldLabel(field)"
3446
+ [attr.aria-label]="ui.text('selectField', { field: fieldLabel(field) })"
3231
3447
  >
3232
- <mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" title="Drag to reorder (or use the move buttons)">drag_indicator</mat-icon>
3448
+ <mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" [title]="ui.text('dragToReorder')"
3449
+ >drag_indicator</mat-icon
3450
+ >
3233
3451
  <mat-icon class="deb-field-type" [matTooltip]="fieldTypeLabel(field.type)">{{ fieldTypeIcon(field.type) }}</mat-icon>
3234
3452
  <div class="deb-field-meta">
3235
3453
  <div class="deb-row" style="gap:6px">
3236
3454
  <span class="deb-field-label" [attr.data-testid]="'row-label-' + field.id">{{ fieldLabel(field) }}</span>
3237
3455
  <span class="deb-type-badge">{{ field.type }}</span>
3238
3456
  @if (field.isReferenced) {
3239
- <mat-icon style="font-size: 16px; width: 16px; height: 16px; color: #1976d2;" matTooltip="Referenced field (linked to {{ field.referencedEntityKey }})">link</mat-icon>
3457
+ <mat-icon
3458
+ style="font-size: 16px; width: 16px; height: 16px; color: #1976d2;"
3459
+ [matTooltip]="ui.text('referencedFieldTooltip', { entity: field.referencedEntityKey ?? '' })"
3460
+ >link</mat-icon
3461
+ >
3240
3462
  }
3241
3463
  @if (field.hasDrift) {
3242
- <mat-icon style="font-size: 16px; width: 16px; height: 16px; color: #d32f2f;" data-testid="drift-warning-icon" matTooltip="Drift detected: source field has evolved">sync_problem</mat-icon>
3464
+ <mat-icon
3465
+ style="font-size: 16px; width: 16px; height: 16px; color: #d32f2f;"
3466
+ data-testid="drift-warning-icon"
3467
+ [matTooltip]="ui.text('driftDetected')"
3468
+ >sync_problem</mat-icon
3469
+ >
3243
3470
  }
3244
3471
  </div>
3245
3472
  <span class="deb-field-id" [attr.data-testid]="'row-id-' + field.id">{{ field.id }}</span>
3246
3473
  </div>
3247
- @if (field.validators?.required) { <span class="deb-req" title="Required">*</span> }
3474
+ @if (field.validators?.required) {
3475
+ <span class="deb-req" [title]="ui.text('required')">*</span>
3476
+ }
3248
3477
 
3249
- <button mat-icon-button type="button" [disabled]="index === 0"
3478
+ <button
3479
+ mat-icon-button
3480
+ type="button"
3481
+ [disabled]="index === 0"
3250
3482
  [attr.data-testid]="'row-up-' + field.id"
3251
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
3252
- (click)="store.moveField(store.keyOf(field), -1); $event.stopPropagation()" matTooltip="Move up">
3483
+ [attr.aria-label]="ui.text('moveFieldUp', { field: fieldLabel(field) })"
3484
+ (click)="store.moveField(store.keyOf(field), -1); $event.stopPropagation()"
3485
+ [matTooltip]="ui.text('moveUp')"
3486
+ >
3253
3487
  <mat-icon>arrow_upward</mat-icon>
3254
3488
  </button>
3255
- <button mat-icon-button type="button" [disabled]="index === totalCount - 1"
3489
+ <button
3490
+ mat-icon-button
3491
+ type="button"
3492
+ [disabled]="index === totalCount - 1"
3256
3493
  [attr.data-testid]="'row-down-' + field.id"
3257
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
3258
- (click)="store.moveField(store.keyOf(field), 1); $event.stopPropagation()" matTooltip="Move down">
3494
+ [attr.aria-label]="ui.text('moveFieldDown', { field: fieldLabel(field) })"
3495
+ (click)="store.moveField(store.keyOf(field), 1); $event.stopPropagation()"
3496
+ [matTooltip]="ui.text('moveDown')"
3497
+ >
3259
3498
  <mat-icon>arrow_downward</mat-icon>
3260
3499
  </button>
3261
- <button mat-icon-button type="button"
3500
+ <button
3501
+ mat-icon-button
3502
+ type="button"
3262
3503
  [attr.data-testid]="'row-duplicate-' + field.id"
3263
- [attr.aria-label]="'Duplicate ' + fieldLabel(field)"
3264
- (click)="store.duplicateField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Duplicate">
3504
+ [attr.aria-label]="ui.text('duplicateField', { field: fieldLabel(field) })"
3505
+ (click)="store.duplicateField(store.keyOf(field)); $event.stopPropagation()"
3506
+ [matTooltip]="ui.text('duplicate')"
3507
+ >
3265
3508
  <mat-icon>content_copy</mat-icon>
3266
3509
  </button>
3267
- <button mat-icon-button type="button" color="warn"
3510
+ <button
3511
+ mat-icon-button
3512
+ type="button"
3513
+ color="warn"
3268
3514
  [attr.data-testid]="'row-delete-' + field.id"
3269
- (click)="store.removeField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Delete">
3515
+ (click)="store.removeField(store.keyOf(field)); $event.stopPropagation()"
3516
+ [matTooltip]="ui.text('delete')"
3517
+ >
3270
3518
  <mat-icon>delete</mat-icon>
3271
3519
  </button>
3272
3520
  </div>
3273
3521
 
3274
3522
  @if (field.children && field.children.length > 0) {
3275
- <div class="deb-tree-children" style="padding-left: 20px; border-left: 2px solid rgba(0,0,0,0.08); margin-left: 12px; margin-top: 4px;">
3523
+ <div
3524
+ class="deb-tree-children"
3525
+ style="padding-left: 20px; border-left: 2px solid rgba(0,0,0,0.08); margin-left: 12px; margin-top: 4px;"
3526
+ >
3276
3527
  @for (child of field.children; track child.id; let ci = $index, cCount = $count) {
3277
- <ngx-entity-builder-tree-node
3278
- [field]="child"
3279
- [index]="ci"
3280
- [totalCount]="cCount"
3281
- />
3528
+ <ngx-entity-builder-tree-node [field]="child" [index]="ci" [totalCount]="cCount" />
3282
3529
  }
3283
3530
  </div>
3284
3531
  }
@@ -3290,13 +3537,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3290
3537
  changeDetection: ChangeDetectionStrategy.OnPush,
3291
3538
  selector: 'ngx-entity-builder-tree-node',
3292
3539
  standalone: true,
3293
- imports: [
3294
- CommonModule,
3295
- DragDropModule,
3296
- MatButtonModule,
3297
- MatIconModule,
3298
- MatTooltipModule,
3299
- ],
3540
+ imports: [CommonModule, DragDropModule, MatButtonModule, MatIconModule, MatTooltipModule],
3300
3541
  template: `
3301
3542
  <div
3302
3543
  cdkDrag
@@ -3309,58 +3550,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3309
3550
  role="button"
3310
3551
  tabindex="0"
3311
3552
  [attr.aria-pressed]="store.isSelected(field)"
3312
- [attr.aria-label]="'Select field ' + fieldLabel(field)"
3553
+ [attr.aria-label]="ui.text('selectField', { field: fieldLabel(field) })"
3313
3554
  >
3314
- <mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" title="Drag to reorder (or use the move buttons)">drag_indicator</mat-icon>
3555
+ <mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" [title]="ui.text('dragToReorder')"
3556
+ >drag_indicator</mat-icon
3557
+ >
3315
3558
  <mat-icon class="deb-field-type" [matTooltip]="fieldTypeLabel(field.type)">{{ fieldTypeIcon(field.type) }}</mat-icon>
3316
3559
  <div class="deb-field-meta">
3317
3560
  <div class="deb-row" style="gap:6px">
3318
3561
  <span class="deb-field-label" [attr.data-testid]="'row-label-' + field.id">{{ fieldLabel(field) }}</span>
3319
3562
  <span class="deb-type-badge">{{ field.type }}</span>
3320
3563
  @if (field.isReferenced) {
3321
- <mat-icon style="font-size: 16px; width: 16px; height: 16px; color: #1976d2;" matTooltip="Referenced field (linked to {{ field.referencedEntityKey }})">link</mat-icon>
3564
+ <mat-icon
3565
+ style="font-size: 16px; width: 16px; height: 16px; color: #1976d2;"
3566
+ [matTooltip]="ui.text('referencedFieldTooltip', { entity: field.referencedEntityKey ?? '' })"
3567
+ >link</mat-icon
3568
+ >
3322
3569
  }
3323
3570
  @if (field.hasDrift) {
3324
- <mat-icon style="font-size: 16px; width: 16px; height: 16px; color: #d32f2f;" data-testid="drift-warning-icon" matTooltip="Drift detected: source field has evolved">sync_problem</mat-icon>
3571
+ <mat-icon
3572
+ style="font-size: 16px; width: 16px; height: 16px; color: #d32f2f;"
3573
+ data-testid="drift-warning-icon"
3574
+ [matTooltip]="ui.text('driftDetected')"
3575
+ >sync_problem</mat-icon
3576
+ >
3325
3577
  }
3326
3578
  </div>
3327
3579
  <span class="deb-field-id" [attr.data-testid]="'row-id-' + field.id">{{ field.id }}</span>
3328
3580
  </div>
3329
- @if (field.validators?.required) { <span class="deb-req" title="Required">*</span> }
3581
+ @if (field.validators?.required) {
3582
+ <span class="deb-req" [title]="ui.text('required')">*</span>
3583
+ }
3330
3584
 
3331
- <button mat-icon-button type="button" [disabled]="index === 0"
3585
+ <button
3586
+ mat-icon-button
3587
+ type="button"
3588
+ [disabled]="index === 0"
3332
3589
  [attr.data-testid]="'row-up-' + field.id"
3333
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
3334
- (click)="store.moveField(store.keyOf(field), -1); $event.stopPropagation()" matTooltip="Move up">
3590
+ [attr.aria-label]="ui.text('moveFieldUp', { field: fieldLabel(field) })"
3591
+ (click)="store.moveField(store.keyOf(field), -1); $event.stopPropagation()"
3592
+ [matTooltip]="ui.text('moveUp')"
3593
+ >
3335
3594
  <mat-icon>arrow_upward</mat-icon>
3336
3595
  </button>
3337
- <button mat-icon-button type="button" [disabled]="index === totalCount - 1"
3596
+ <button
3597
+ mat-icon-button
3598
+ type="button"
3599
+ [disabled]="index === totalCount - 1"
3338
3600
  [attr.data-testid]="'row-down-' + field.id"
3339
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
3340
- (click)="store.moveField(store.keyOf(field), 1); $event.stopPropagation()" matTooltip="Move down">
3601
+ [attr.aria-label]="ui.text('moveFieldDown', { field: fieldLabel(field) })"
3602
+ (click)="store.moveField(store.keyOf(field), 1); $event.stopPropagation()"
3603
+ [matTooltip]="ui.text('moveDown')"
3604
+ >
3341
3605
  <mat-icon>arrow_downward</mat-icon>
3342
3606
  </button>
3343
- <button mat-icon-button type="button"
3607
+ <button
3608
+ mat-icon-button
3609
+ type="button"
3344
3610
  [attr.data-testid]="'row-duplicate-' + field.id"
3345
- [attr.aria-label]="'Duplicate ' + fieldLabel(field)"
3346
- (click)="store.duplicateField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Duplicate">
3611
+ [attr.aria-label]="ui.text('duplicateField', { field: fieldLabel(field) })"
3612
+ (click)="store.duplicateField(store.keyOf(field)); $event.stopPropagation()"
3613
+ [matTooltip]="ui.text('duplicate')"
3614
+ >
3347
3615
  <mat-icon>content_copy</mat-icon>
3348
3616
  </button>
3349
- <button mat-icon-button type="button" color="warn"
3617
+ <button
3618
+ mat-icon-button
3619
+ type="button"
3620
+ color="warn"
3350
3621
  [attr.data-testid]="'row-delete-' + field.id"
3351
- (click)="store.removeField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Delete">
3622
+ (click)="store.removeField(store.keyOf(field)); $event.stopPropagation()"
3623
+ [matTooltip]="ui.text('delete')"
3624
+ >
3352
3625
  <mat-icon>delete</mat-icon>
3353
3626
  </button>
3354
3627
  </div>
3355
3628
 
3356
3629
  @if (field.children && field.children.length > 0) {
3357
- <div class="deb-tree-children" style="padding-left: 20px; border-left: 2px solid rgba(0,0,0,0.08); margin-left: 12px; margin-top: 4px;">
3630
+ <div
3631
+ class="deb-tree-children"
3632
+ style="padding-left: 20px; border-left: 2px solid rgba(0,0,0,0.08); margin-left: 12px; margin-top: 4px;"
3633
+ >
3358
3634
  @for (child of field.children; track child.id; let ci = $index, cCount = $count) {
3359
- <ngx-entity-builder-tree-node
3360
- [field]="child"
3361
- [index]="ci"
3362
- [totalCount]="cCount"
3363
- />
3635
+ <ngx-entity-builder-tree-node [field]="child" [index]="ci" [totalCount]="cCount" />
3364
3636
  }
3365
3637
  </div>
3366
3638
  }
@@ -3378,6 +3650,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3378
3650
  }] } });
3379
3651
 
3380
3652
  class EntityBuilderCanvasComponent {
3653
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3654
+ ui = inject(BuilderTextService);
3381
3655
  store = inject(BuilderStore);
3382
3656
  /**
3383
3657
  * Headings only earn their space once the fields are actually split across tabs. A
@@ -3396,13 +3670,13 @@ class EntityBuilderCanvasComponent {
3396
3670
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityBuilderCanvasComponent, isStandalone: true, selector: "ngx-entity-builder-canvas", ngImport: i0, template: `
3397
3671
  <mat-card class="deb-canvas">
3398
3672
  <mat-card-header>
3399
- <mat-card-title>Fields ({{ store.fields().length }})</mat-card-title>
3673
+ <mat-card-title>{{ ui.text('fieldsHeading', { count: store.fields().length }) }}</mat-card-title>
3400
3674
  </mat-card-header>
3401
3675
  <mat-card-content>
3402
3676
  @if (store.fields().length === 0) {
3403
3677
  <div class="deb-empty">
3404
3678
  <mat-icon>widgets</mat-icon>
3405
- <p>No fields yet. Pick a type from <strong>Add field</strong> to get started.</p>
3679
+ <p>{{ ui.text('canvasEmpty') }}</p>
3406
3680
  </div>
3407
3681
  }
3408
3682
 
@@ -3424,11 +3698,7 @@ class EntityBuilderCanvasComponent {
3424
3698
  (cdkDropListDropped)="onDrop(group.tabId, $event)"
3425
3699
  >
3426
3700
  @for (f of group.fields; track f.id; let i = $index, count = $count) {
3427
- <ngx-entity-builder-tree-node
3428
- [field]="f"
3429
- [index]="i"
3430
- [totalCount]="count"
3431
- />
3701
+ <ngx-entity-builder-tree-node [field]="f" [index]="i" [totalCount]="count" />
3432
3702
  }
3433
3703
  </div>
3434
3704
  }
@@ -3442,23 +3712,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3442
3712
  changeDetection: ChangeDetectionStrategy.OnPush,
3443
3713
  selector: 'ngx-entity-builder-canvas',
3444
3714
  standalone: true,
3445
- imports: [
3446
- CommonModule,
3447
- DragDropModule,
3448
- MatCardModule,
3449
- MatIconModule,
3450
- EntityBuilderTreeNodeComponent,
3451
- ],
3715
+ imports: [CommonModule, DragDropModule, MatCardModule, MatIconModule, EntityBuilderTreeNodeComponent],
3452
3716
  template: `
3453
3717
  <mat-card class="deb-canvas">
3454
3718
  <mat-card-header>
3455
- <mat-card-title>Fields ({{ store.fields().length }})</mat-card-title>
3719
+ <mat-card-title>{{ ui.text('fieldsHeading', { count: store.fields().length }) }}</mat-card-title>
3456
3720
  </mat-card-header>
3457
3721
  <mat-card-content>
3458
3722
  @if (store.fields().length === 0) {
3459
3723
  <div class="deb-empty">
3460
3724
  <mat-icon>widgets</mat-icon>
3461
- <p>No fields yet. Pick a type from <strong>Add field</strong> to get started.</p>
3725
+ <p>{{ ui.text('canvasEmpty') }}</p>
3462
3726
  </div>
3463
3727
  }
3464
3728
 
@@ -3480,11 +3744,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3480
3744
  (cdkDropListDropped)="onDrop(group.tabId, $event)"
3481
3745
  >
3482
3746
  @for (f of group.fields; track f.id; let i = $index, count = $count) {
3483
- <ngx-entity-builder-tree-node
3484
- [field]="f"
3485
- [index]="i"
3486
- [totalCount]="count"
3487
- />
3747
+ <ngx-entity-builder-tree-node [field]="f" [index]="i" [totalCount]="count" />
3488
3748
  }
3489
3749
  </div>
3490
3750
  }
@@ -3502,6 +3762,8 @@ const EMPTY_ROLES = Object.freeze([]);
3502
3762
  * EntityBuilderComponent — the top-level visual builder for an EntityFormConfig.
3503
3763
  */
3504
3764
  class EntityBuilderComponent {
3765
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3766
+ ui = inject(BuilderTextService);
3505
3767
  store = inject(BuilderStore);
3506
3768
  /**
3507
3769
  * Optional: only apps that registered a CONFIG_SOURCE have one. Used to drop the cached
@@ -3512,6 +3774,16 @@ class EntityBuilderComponent {
3512
3774
  config;
3513
3775
  /** Languages available for label/placeholder editing. First entry is the default. */
3514
3776
  languages = ['en'];
3777
+ /**
3778
+ * Language for the builder's **own** chrome, resolved through `BUILDER_TEXT`.
3779
+ *
3780
+ * Deliberately separate from `languages` and `store.activeLanguage()`, which choose the
3781
+ * `LocalizedText` entry being authored: switching the label language you are editing
3782
+ * should not translate the interface around it.
3783
+ */
3784
+ set uiLanguage(value) {
3785
+ this.ui.language.set(value || 'en');
3786
+ }
3515
3787
  /** Optional role list — enables multi-select role pickers for RBAC instead of free text. */
3516
3788
  availableRoles = [];
3517
3789
  /**
@@ -3601,7 +3873,7 @@ class EntityBuilderComponent {
3601
3873
  this.store.setPermission(action, roles);
3602
3874
  }
3603
3875
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3604
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: "config", languages: "languages", availableRoles: "availableRoles", userRoles: "userRoles", commonModules: "commonModules" }, outputs: { configChange: "configChange", save: "save" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\n <mat-icon>dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">Entity Builder</span>\n <span class=\"deb-spacer\"></span>\n\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matTooltip]=\"store.problems().length + ' issue(s) \u2014 hover items in the list'\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ store.errors().length }} / {{ store.problems().length }}\n </button>\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=\"Undo (Ctrl+Z)\"\n aria-label=\"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=\"Redo (Ctrl+Shift+Z)\"\n aria-label=\"Redo\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button mat-button type=\"button\" (click)=\"copyJson()\">\n <mat-icon>content_copy</mat-icon> Copy JSON\n </button>\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> Save\n </button>\n</mat-toolbar>\n\n<div class=\"deb-body\">\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--left\">\n <mat-card>\n <mat-card-content>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Entity name</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n placeholder=\"e.g. clients\"\n />\n </mat-form-field>\n\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\n <mat-label>Editing language</mat-label>\n <mat-select\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 <mat-slide-toggle\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >Mask this entity (RBAC)</mat-slide-toggle>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>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>{{ action }} roles</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>{{ action }} roles (comma-separated)</mat-label>\n <input\n matInput\n [ngModel]=\"rolesFor(action).join(', ')\"\n (ngModelChange)=\"setRolesFromText(action, $event)\"\n />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">Empty = no restriction for that action.</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card>\n <mat-card-header><mat-card-title>Add field</mat-card-title></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>\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 \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\">\n <ngx-entity-builder-canvas />\n </div>\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--right\">\n <mat-card>\n <mat-card-header><mat-card-title>Field properties</mat-card-title></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: JSON + optional live preview \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;Config JSON</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n\n <div class=\"deb-preview-slot\">\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </div>\n</div>\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#fff}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;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:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px;max-height:calc(100vh - 40px);overflow:auto}@media(max-width:1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;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-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.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:#ef4444;font-weight:700}.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{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{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)}.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: i2$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i2$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i2$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i2$1.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: i5.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: i8.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: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.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: i10.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" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3876
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: "config", languages: "languages", uiLanguage: "uiLanguage", availableRoles: "availableRoles", userRoles: "userRoles", commonModules: "commonModules" }, outputs: { configChange: "configChange", save: "save" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\n <mat-icon>dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-spacer\"></span>\n\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ store.errors().length }} / {{ store.problems().length }}\n </button>\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 mat-button type=\"button\" (click)=\"copyJson()\"><mat-icon>content_copy</mat-icon> {{ ui.text('copyJson') }}</button>\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</mat-toolbar>\n\n<div class=\"deb-body\">\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--left\">\n <mat-card>\n <mat-card-content>\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 <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\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 <mat-slide-toggle [ngModel]=\"store.config().maskData ?? false\" (ngModelChange)=\"store.setMaskData($event)\">{{\n ui.text('maskEntity')\n }}</mat-slide-toggle>\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>\n <mat-card-header\n ><mat-card-title>{{ ui.text('addField') }}</mat-card-title></mat-card-header\n >\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>\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 \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\">\n <ngx-entity-builder-canvas />\n </div>\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--right\">\n <mat-card>\n <mat-card-header\n ><mat-card-title>{{ ui.text('fieldProperties') }}</mat-card-title></mat-card-header\n >\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: JSON + optional live preview \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\n <div class=\"deb-preview-slot\">\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </div>\n</div>\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#fff}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;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:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px;max-height:calc(100vh - 40px);overflow:auto}@media(max-width:1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;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-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.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:#ef4444;font-weight:700}.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{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{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)}.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: i2$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i2$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i2$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i2$1.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: i5.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: i8.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: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.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: i10.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" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3605
3877
  }
3606
3878
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, decorators: [{
3607
3879
  type: Component,
@@ -3622,11 +3894,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3622
3894
  TabManagerComponent,
3623
3895
  FieldInspectorComponent,
3624
3896
  EntityBuilderCanvasComponent,
3625
- ], template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\n <mat-icon>dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">Entity Builder</span>\n <span class=\"deb-spacer\"></span>\n\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matTooltip]=\"store.problems().length + ' issue(s) \u2014 hover items in the list'\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ store.errors().length }} / {{ store.problems().length }}\n </button>\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=\"Undo (Ctrl+Z)\"\n aria-label=\"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=\"Redo (Ctrl+Shift+Z)\"\n aria-label=\"Redo\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button mat-button type=\"button\" (click)=\"copyJson()\">\n <mat-icon>content_copy</mat-icon> Copy JSON\n </button>\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> Save\n </button>\n</mat-toolbar>\n\n<div class=\"deb-body\">\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--left\">\n <mat-card>\n <mat-card-content>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Entity name</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n placeholder=\"e.g. clients\"\n />\n </mat-form-field>\n\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\n <mat-label>Editing language</mat-label>\n <mat-select\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 <mat-slide-toggle\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >Mask this entity (RBAC)</mat-slide-toggle>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>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>{{ action }} roles</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>{{ action }} roles (comma-separated)</mat-label>\n <input\n matInput\n [ngModel]=\"rolesFor(action).join(', ')\"\n (ngModelChange)=\"setRolesFromText(action, $event)\"\n />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">Empty = no restriction for that action.</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card>\n <mat-card-header><mat-card-title>Add field</mat-card-title></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>\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 \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\">\n <ngx-entity-builder-canvas />\n </div>\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--right\">\n <mat-card>\n <mat-card-header><mat-card-title>Field properties</mat-card-title></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: JSON + optional live preview \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;Config JSON</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n\n <div class=\"deb-preview-slot\">\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </div>\n</div>\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#fff}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;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:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px;max-height:calc(100vh - 40px);overflow:auto}@media(max-width:1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;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-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.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:#ef4444;font-weight:700}.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{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{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)}.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"] }]
3897
+ ], template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\n <mat-icon>dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-spacer\"></span>\n\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ store.errors().length }} / {{ store.problems().length }}\n </button>\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 mat-button type=\"button\" (click)=\"copyJson()\"><mat-icon>content_copy</mat-icon> {{ ui.text('copyJson') }}</button>\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</mat-toolbar>\n\n<div class=\"deb-body\">\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--left\">\n <mat-card>\n <mat-card-content>\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 <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\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 <mat-slide-toggle [ngModel]=\"store.config().maskData ?? false\" (ngModelChange)=\"store.setMaskData($event)\">{{\n ui.text('maskEntity')\n }}</mat-slide-toggle>\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>\n <mat-card-header\n ><mat-card-title>{{ ui.text('addField') }}</mat-card-title></mat-card-header\n >\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>\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 \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\">\n <ngx-entity-builder-canvas />\n </div>\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--right\">\n <mat-card>\n <mat-card-header\n ><mat-card-title>{{ ui.text('fieldProperties') }}</mat-card-title></mat-card-header\n >\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: JSON + optional live preview \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\n <div class=\"deb-preview-slot\">\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </div>\n</div>\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#fff}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;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:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px;max-height:calc(100vh - 40px);overflow:auto}@media(max-width:1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;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-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.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:#ef4444;font-weight:700}.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{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{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)}.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"] }]
3626
3898
  }], ctorParameters: () => [], propDecorators: { config: [{
3627
3899
  type: Input
3628
3900
  }], languages: [{
3629
3901
  type: Input
3902
+ }], uiLanguage: [{
3903
+ type: Input
3630
3904
  }], availableRoles: [{
3631
3905
  type: Input
3632
3906
  }], userRoles: [{
@@ -3648,5 +3922,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3648
3922
  * Generated bundle index. Do not edit.
3649
3923
  */
3650
3924
 
3651
- export { BuilderStore, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, TabManagerComponent, fieldPathOptions, withExistingOptions };
3925
+ export { BUILDER_TEXT, BuilderStore, BuilderTextService, DEFAULT_BUILDER_TEXT, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, TabManagerComponent, fieldPathOptions, withExistingOptions };
3652
3926
  //# sourceMappingURL=ngx-dynamic-entity-builder.mjs.map