ngx-dynamic-entity-builder 1.8.1 → 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';
@@ -1553,6 +1553,221 @@ function withExistingOptions(options, current) {
1553
1553
  return [...options, ...extra];
1554
1554
  }
1555
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
+
1556
1771
  /**
1557
1772
  * EntityReferenceConfigComponent — authors the `entityReference` block of an entity-ref field:
1558
1773
  * registry key, display fields, static filters, and the parent→child cascade.
@@ -1561,6 +1776,8 @@ function withExistingOptions(options, current) {
1561
1776
  * selected reference returns.
1562
1777
  */
1563
1778
  class EntityReferenceConfigComponent {
1779
+ /** Builder chrome, overridable via BUILDER_TEXT. */
1780
+ ui = inject(BuilderTextService);
1564
1781
  store = inject(BuilderStore);
1565
1782
  field = this.store.selectedField;
1566
1783
  /** Sibling fields that can drive a cascade — anything but this field itself. */
@@ -1640,32 +1857,32 @@ class EntityReferenceConfigComponent {
1640
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: `
1641
1858
  @if (field(); as f) {
1642
1859
  <div class="deb-entity-ref">
1643
- <span class="deb-section-title">Entity reference</span>
1860
+ <span class="deb-section-title">{{ ui.text('entityReference') }}</span>
1644
1861
 
1645
1862
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1646
- <mat-label>Registry key</mat-label>
1863
+ <mat-label>{{ ui.text('registryKey') }}</mat-label>
1647
1864
  <input
1648
1865
  matInput
1649
1866
  data-testid="entity-ref-key"
1650
1867
  [ngModel]="ref(f).linkedEntityKey ?? ''"
1651
1868
  (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1652
1869
  />
1653
- <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1870
+ <mat-hint>{{ ui.text('registryKeyHint') }}</mat-hint>
1654
1871
  </mat-form-field>
1655
1872
 
1656
1873
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1657
- <mat-label>Display fields</mat-label>
1874
+ <mat-label>{{ ui.text('displayFields') }}</mat-label>
1658
1875
  <input
1659
1876
  matInput
1660
1877
  data-testid="entity-ref-display-fields"
1661
1878
  [ngModel]="displayFieldsText(f)"
1662
1879
  (ngModelChange)="setDisplayFields(f, $event)"
1663
1880
  />
1664
- <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1881
+ <mat-hint>{{ ui.text('displayFieldsHint') }}</mat-hint>
1665
1882
  </mat-form-field>
1666
1883
 
1667
1884
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1668
- <mat-label>Static filters (JSON)</mat-label>
1885
+ <mat-label>{{ ui.text('staticFilters') }}</mat-label>
1669
1886
  <input
1670
1887
  matInput
1671
1888
  data-testid="entity-ref-filters"
@@ -1675,52 +1892,52 @@ class EntityReferenceConfigComponent {
1675
1892
  @if (filtersError()) {
1676
1893
  <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1677
1894
  } @else {
1678
- <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1895
+ <mat-hint>{{ ui.text('staticFiltersHint') }}</mat-hint>
1679
1896
  }
1680
1897
  </mat-form-field>
1681
1898
 
1682
1899
  <mat-divider></mat-divider>
1683
1900
 
1684
1901
  <!-- Cascade -->
1685
- <span class="deb-section-title">Cascade</span>
1902
+ <span class="deb-section-title">{{ ui.text('cascade') }}</span>
1686
1903
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1687
- <mat-label>Parent field</mat-label>
1904
+ <mat-label>{{ ui.text('parentField') }}</mat-label>
1688
1905
  <mat-select
1689
1906
  data-testid="entity-ref-parent"
1690
1907
  [ngModel]="ref(f).parentField ?? ''"
1691
1908
  (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1692
1909
  >
1693
- <mat-option value="">None — load all options</mat-option>
1910
+ <mat-option value="">{{ ui.text('parentFieldNone') }}</mat-option>
1694
1911
  @for (option of parentOptions(); track option.value) {
1695
1912
  <mat-option [value]="option.value">
1696
1913
  {{ option.label }} <span class="deb-path-hint">{{ option.path }}</span>
1697
1914
  </mat-option>
1698
1915
  }
1699
1916
  </mat-select>
1700
- <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1917
+ <mat-hint>{{ ui.text('cascadeHint') }}</mat-hint>
1701
1918
  </mat-form-field>
1702
1919
 
1703
1920
  @if (ref(f).parentField) {
1704
1921
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1705
- <mat-label>Lookup filter path</mat-label>
1922
+ <mat-label>{{ ui.text('lookupFilterPath') }}</mat-label>
1706
1923
  <input
1707
1924
  matInput
1708
1925
  data-testid="entity-ref-lookup-filter"
1709
1926
  [ngModel]="ref(f).lookupFilter ?? ''"
1710
1927
  (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1711
1928
  />
1712
- <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1929
+ <mat-hint>{{ ui.text('lookupFilterPathHint') }}</mat-hint>
1713
1930
  </mat-form-field>
1714
1931
 
1715
1932
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1716
- <mat-label>Lookup path (nested options)</mat-label>
1933
+ <mat-label>{{ ui.text('lookupPath') }}</mat-label>
1717
1934
  <input
1718
1935
  matInput
1719
1936
  data-testid="entity-ref-lookup-path"
1720
1937
  [ngModel]="ref(f).lookupPath ?? ''"
1721
1938
  (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1722
1939
  />
1723
- <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1940
+ <mat-hint>{{ ui.text('lookupPathHint') }}</mat-hint>
1724
1941
  </mat-form-field>
1725
1942
  }
1726
1943
 
@@ -1728,20 +1945,20 @@ class EntityReferenceConfigComponent {
1728
1945
 
1729
1946
  <!-- autoPatch -->
1730
1947
  <div class="deb-row deb-row--split">
1731
- <span class="deb-section-title">Auto-patch on select</span>
1948
+ <span class="deb-section-title">{{ ui.text('autoPatchOnSelect') }}</span>
1732
1949
  <button
1733
1950
  mat-stroked-button
1734
1951
  type="button"
1735
1952
  data-testid="add-auto-patch"
1736
1953
  (click)="store.addAutoPatchMapping(store.keyOf(f))"
1737
1954
  >
1738
- <mat-icon>add</mat-icon> Mapping
1955
+ <mat-icon>add</mat-icon> {{ ui.text('mapping') }}
1739
1956
  </button>
1740
1957
  </div>
1741
1958
 
1742
1959
  @if (f.autoPatch) {
1743
1960
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1744
- <mat-label>Target tab</mat-label>
1961
+ <mat-label>{{ ui.text('targetTab') }}</mat-label>
1745
1962
  <mat-select
1746
1963
  data-testid="auto-patch-tab"
1747
1964
  [ngModel]="f.autoPatch.targetTab"
@@ -1756,7 +1973,7 @@ class EntityReferenceConfigComponent {
1756
1973
  @for (mapping of f.autoPatch.mappings; track $index) {
1757
1974
  <div class="deb-option-row">
1758
1975
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1759
- <mat-label>Source (linked record)</mat-label>
1976
+ <mat-label>{{ ui.text('sourceLinkedRecord') }}</mat-label>
1760
1977
  <input
1761
1978
  matInput
1762
1979
  [ngModel]="mapping.source"
@@ -1764,7 +1981,7 @@ class EntityReferenceConfigComponent {
1764
1981
  />
1765
1982
  </mat-form-field>
1766
1983
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1767
- <mat-label>Target field</mat-label>
1984
+ <mat-label>{{ ui.text('targetField') }}</mat-label>
1768
1985
  <mat-select
1769
1986
  data-testid="auto-patch-target"
1770
1987
  [ngModel]="mapping.target"
@@ -1781,7 +1998,7 @@ class EntityReferenceConfigComponent {
1781
1998
  mat-icon-button
1782
1999
  type="button"
1783
2000
  color="warn"
1784
- matTooltip="Remove mapping"
2001
+ [matTooltip]="ui.text('removeMapping')"
1785
2002
  (click)="store.removeAutoPatchMapping(store.keyOf(f), $index)"
1786
2003
  >
1787
2004
  <mat-icon>delete</mat-icon>
@@ -1790,7 +2007,7 @@ class EntityReferenceConfigComponent {
1790
2007
  }
1791
2008
  } @else {
1792
2009
  <p class="deb-hint">
1793
- No auto-patch configured — selecting a reference will not copy any values.
2010
+ {{ ui.text('noAutoPatch') }}
1794
2011
  </p>
1795
2012
  }
1796
2013
  </div>
@@ -1812,32 +2029,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1812
2029
  ], template: `
1813
2030
  @if (field(); as f) {
1814
2031
  <div class="deb-entity-ref">
1815
- <span class="deb-section-title">Entity reference</span>
2032
+ <span class="deb-section-title">{{ ui.text('entityReference') }}</span>
1816
2033
 
1817
2034
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1818
- <mat-label>Registry key</mat-label>
2035
+ <mat-label>{{ ui.text('registryKey') }}</mat-label>
1819
2036
  <input
1820
2037
  matInput
1821
2038
  data-testid="entity-ref-key"
1822
2039
  [ngModel]="ref(f).linkedEntityKey ?? ''"
1823
2040
  (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1824
2041
  />
1825
- <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
2042
+ <mat-hint>{{ ui.text('registryKeyHint') }}</mat-hint>
1826
2043
  </mat-form-field>
1827
2044
 
1828
2045
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1829
- <mat-label>Display fields</mat-label>
2046
+ <mat-label>{{ ui.text('displayFields') }}</mat-label>
1830
2047
  <input
1831
2048
  matInput
1832
2049
  data-testid="entity-ref-display-fields"
1833
2050
  [ngModel]="displayFieldsText(f)"
1834
2051
  (ngModelChange)="setDisplayFields(f, $event)"
1835
2052
  />
1836
- <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
2053
+ <mat-hint>{{ ui.text('displayFieldsHint') }}</mat-hint>
1837
2054
  </mat-form-field>
1838
2055
 
1839
2056
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1840
- <mat-label>Static filters (JSON)</mat-label>
2057
+ <mat-label>{{ ui.text('staticFilters') }}</mat-label>
1841
2058
  <input
1842
2059
  matInput
1843
2060
  data-testid="entity-ref-filters"
@@ -1847,52 +2064,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1847
2064
  @if (filtersError()) {
1848
2065
  <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1849
2066
  } @else {
1850
- <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
2067
+ <mat-hint>{{ ui.text('staticFiltersHint') }}</mat-hint>
1851
2068
  }
1852
2069
  </mat-form-field>
1853
2070
 
1854
2071
  <mat-divider></mat-divider>
1855
2072
 
1856
2073
  <!-- Cascade -->
1857
- <span class="deb-section-title">Cascade</span>
2074
+ <span class="deb-section-title">{{ ui.text('cascade') }}</span>
1858
2075
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1859
- <mat-label>Parent field</mat-label>
2076
+ <mat-label>{{ ui.text('parentField') }}</mat-label>
1860
2077
  <mat-select
1861
2078
  data-testid="entity-ref-parent"
1862
2079
  [ngModel]="ref(f).parentField ?? ''"
1863
2080
  (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1864
2081
  >
1865
- <mat-option value="">None — load all options</mat-option>
2082
+ <mat-option value="">{{ ui.text('parentFieldNone') }}</mat-option>
1866
2083
  @for (option of parentOptions(); track option.value) {
1867
2084
  <mat-option [value]="option.value">
1868
2085
  {{ option.label }} <span class="deb-path-hint">{{ option.path }}</span>
1869
2086
  </mat-option>
1870
2087
  }
1871
2088
  </mat-select>
1872
- <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
2089
+ <mat-hint>{{ ui.text('cascadeHint') }}</mat-hint>
1873
2090
  </mat-form-field>
1874
2091
 
1875
2092
  @if (ref(f).parentField) {
1876
2093
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1877
- <mat-label>Lookup filter path</mat-label>
2094
+ <mat-label>{{ ui.text('lookupFilterPath') }}</mat-label>
1878
2095
  <input
1879
2096
  matInput
1880
2097
  data-testid="entity-ref-lookup-filter"
1881
2098
  [ngModel]="ref(f).lookupFilter ?? ''"
1882
2099
  (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1883
2100
  />
1884
- <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
2101
+ <mat-hint>{{ ui.text('lookupFilterPathHint') }}</mat-hint>
1885
2102
  </mat-form-field>
1886
2103
 
1887
2104
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1888
- <mat-label>Lookup path (nested options)</mat-label>
2105
+ <mat-label>{{ ui.text('lookupPath') }}</mat-label>
1889
2106
  <input
1890
2107
  matInput
1891
2108
  data-testid="entity-ref-lookup-path"
1892
2109
  [ngModel]="ref(f).lookupPath ?? ''"
1893
2110
  (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1894
2111
  />
1895
- <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
2112
+ <mat-hint>{{ ui.text('lookupPathHint') }}</mat-hint>
1896
2113
  </mat-form-field>
1897
2114
  }
1898
2115
 
@@ -1900,20 +2117,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1900
2117
 
1901
2118
  <!-- autoPatch -->
1902
2119
  <div class="deb-row deb-row--split">
1903
- <span class="deb-section-title">Auto-patch on select</span>
2120
+ <span class="deb-section-title">{{ ui.text('autoPatchOnSelect') }}</span>
1904
2121
  <button
1905
2122
  mat-stroked-button
1906
2123
  type="button"
1907
2124
  data-testid="add-auto-patch"
1908
2125
  (click)="store.addAutoPatchMapping(store.keyOf(f))"
1909
2126
  >
1910
- <mat-icon>add</mat-icon> Mapping
2127
+ <mat-icon>add</mat-icon> {{ ui.text('mapping') }}
1911
2128
  </button>
1912
2129
  </div>
1913
2130
 
1914
2131
  @if (f.autoPatch) {
1915
2132
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1916
- <mat-label>Target tab</mat-label>
2133
+ <mat-label>{{ ui.text('targetTab') }}</mat-label>
1917
2134
  <mat-select
1918
2135
  data-testid="auto-patch-tab"
1919
2136
  [ngModel]="f.autoPatch.targetTab"
@@ -1928,7 +2145,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1928
2145
  @for (mapping of f.autoPatch.mappings; track $index) {
1929
2146
  <div class="deb-option-row">
1930
2147
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1931
- <mat-label>Source (linked record)</mat-label>
2148
+ <mat-label>{{ ui.text('sourceLinkedRecord') }}</mat-label>
1932
2149
  <input
1933
2150
  matInput
1934
2151
  [ngModel]="mapping.source"
@@ -1936,7 +2153,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1936
2153
  />
1937
2154
  </mat-form-field>
1938
2155
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
1939
- <mat-label>Target field</mat-label>
2156
+ <mat-label>{{ ui.text('targetField') }}</mat-label>
1940
2157
  <mat-select
1941
2158
  data-testid="auto-patch-target"
1942
2159
  [ngModel]="mapping.target"
@@ -1953,7 +2170,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1953
2170
  mat-icon-button
1954
2171
  type="button"
1955
2172
  color="warn"
1956
- matTooltip="Remove mapping"
2173
+ [matTooltip]="ui.text('removeMapping')"
1957
2174
  (click)="store.removeAutoPatchMapping(store.keyOf(f), $index)"
1958
2175
  >
1959
2176
  <mat-icon>delete</mat-icon>
@@ -1962,7 +2179,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1962
2179
  }
1963
2180
  } @else {
1964
2181
  <p class="deb-hint">
1965
- No auto-patch configured — selecting a reference will not copy any values.
2182
+ {{ ui.text('noAutoPatch') }}
1966
2183
  </p>
1967
2184
  }
1968
2185
  </div>
@@ -1974,6 +2191,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
1974
2191
  * RuleFormComponent — dialog/panel form for creating or editing a FormRule.
1975
2192
  */
1976
2193
  class RuleFormComponent {
2194
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2195
+ ui = inject(BuilderTextService);
1977
2196
  store = inject(BuilderStore);
1978
2197
  rule = {
1979
2198
  formConfigId: 'form-1',
@@ -2029,8 +2248,7 @@ class RuleFormComponent {
2029
2248
  */
2030
2249
  targetValues() {
2031
2250
  const ids = this.rule.targets.filter(t => t.type === 'field').map(t => t.id);
2032
- const unchanged = ids.length === this.cachedTargets.length &&
2033
- ids.every((id, i) => id === this.cachedTargets[i]);
2251
+ const unchanged = ids.length === this.cachedTargets.length && ids.every((id, i) => id === this.cachedTargets[i]);
2034
2252
  if (!unchanged)
2035
2253
  this.cachedTargets = ids;
2036
2254
  return this.cachedTargets;
@@ -2054,11 +2272,11 @@ class RuleFormComponent {
2054
2272
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2055
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: `
2056
2274
  <div class="deb-rule-form">
2057
- <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>
2058
2276
 
2059
2277
  <!-- Trigger Field -->
2060
2278
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2061
- <mat-label>Trigger Field</mat-label>
2279
+ <mat-label>{{ ui.text('triggerField') }}</mat-label>
2062
2280
  <mat-select [(ngModel)]="rule.fieldId" data-testid="rule-trigger">
2063
2281
  @for (option of triggerOptions(); track option.value) {
2064
2282
  <mat-option [value]="option.value">
@@ -2070,13 +2288,8 @@ class RuleFormComponent {
2070
2288
 
2071
2289
  <!-- Targets: the fields this rule acts on -->
2072
2290
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2073
- <mat-label>Apply to fields</mat-label>
2074
- <mat-select
2075
- multiple
2076
- data-testid="rule-targets"
2077
- [ngModel]="targetValues()"
2078
- (ngModelChange)="setTargets($event)"
2079
- >
2291
+ <mat-label>{{ ui.text('applyToFields') }}</mat-label>
2292
+ <mat-select multiple data-testid="rule-targets" [ngModel]="targetValues()" (ngModelChange)="setTargets($event)">
2080
2293
  @for (option of targetOptions(); track option.value) {
2081
2294
  <mat-option [value]="option.value">
2082
2295
  {{ option.label }} <span class="deb-rule-path">{{ option.path }}</span>
@@ -2087,11 +2300,11 @@ class RuleFormComponent {
2087
2300
 
2088
2301
  <!-- Conditions -->
2089
2302
  <div class="deb-rule-section">
2090
- <h4>Conditions</h4>
2303
+ <h4>{{ ui.text('conditions') }}</h4>
2091
2304
  @for (cond of rule.conditions; track $index) {
2092
2305
  <div class="deb-rule-row">
2093
2306
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2094
- <mat-label>Operator</mat-label>
2307
+ <mat-label>{{ ui.text('operator') }}</mat-label>
2095
2308
  <mat-select [(ngModel)]="cond.operator">
2096
2309
  @for (op of operators; track op) {
2097
2310
  <mat-option [value]="op">{{ op }}</mat-option>
@@ -2099,8 +2312,8 @@ class RuleFormComponent {
2099
2312
  </mat-select>
2100
2313
  </mat-form-field>
2101
2314
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2102
- <mat-label>Value</mat-label>
2103
- <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')" />
2104
2317
  </mat-form-field>
2105
2318
  <button mat-icon-button color="warn" type="button" (click)="removeCondition($index)">
2106
2319
  <mat-icon>delete</mat-icon>
@@ -2108,53 +2321,45 @@ class RuleFormComponent {
2108
2321
  </div>
2109
2322
  }
2110
2323
  <button mat-stroked-button type="button" (click)="addCondition()">
2111
- <mat-icon>add</mat-icon> Add Condition
2324
+ <mat-icon>add</mat-icon> {{ ui.text('addCondition') }}
2112
2325
  </button>
2113
2326
  </div>
2114
2327
 
2115
2328
  <!-- Action -->
2116
2329
  <div class="deb-rule-section">
2117
- <h4>Action</h4>
2330
+ <h4>{{ ui.text('action') }}</h4>
2118
2331
  <div class="deb-rule-row">
2119
2332
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2120
- <mat-label>Action Type</mat-label>
2333
+ <mat-label>{{ ui.text('actionType') }}</mat-label>
2121
2334
  <mat-select [(ngModel)]="rule.action.type">
2122
- <mat-option value="visibility">Visibility (Show/Hide)</mat-option>
2123
- <mat-option value="validation">Validation Message</mat-option>
2124
- <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>
2125
2338
  </mat-select>
2126
2339
  </mat-form-field>
2127
2340
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2128
- <mat-label>Value / Message</mat-label>
2129
- <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')" />
2130
2343
  </mat-form-field>
2131
2344
  </div>
2132
2345
  </div>
2133
2346
 
2134
2347
  <div class="deb-rule-form__actions">
2135
- <button mat-button type="button" (click)="cancel.emit()">Cancel</button>
2136
- <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>
2137
2350
  </div>
2138
2351
  </div>
2139
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 });
2140
2353
  }
2141
2354
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: RuleFormComponent, decorators: [{
2142
2355
  type: Component,
2143
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-form', standalone: true, imports: [
2144
- CommonModule,
2145
- FormsModule,
2146
- MatButtonModule,
2147
- MatFormFieldModule,
2148
- MatIconModule,
2149
- MatInputModule,
2150
- MatSelectModule,
2151
- ], template: `
2356
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-form', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule], template: `
2152
2357
  <div class="deb-rule-form">
2153
- <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>
2154
2359
 
2155
2360
  <!-- Trigger Field -->
2156
2361
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2157
- <mat-label>Trigger Field</mat-label>
2362
+ <mat-label>{{ ui.text('triggerField') }}</mat-label>
2158
2363
  <mat-select [(ngModel)]="rule.fieldId" data-testid="rule-trigger">
2159
2364
  @for (option of triggerOptions(); track option.value) {
2160
2365
  <mat-option [value]="option.value">
@@ -2166,13 +2371,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2166
2371
 
2167
2372
  <!-- Targets: the fields this rule acts on -->
2168
2373
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2169
- <mat-label>Apply to fields</mat-label>
2170
- <mat-select
2171
- multiple
2172
- data-testid="rule-targets"
2173
- [ngModel]="targetValues()"
2174
- (ngModelChange)="setTargets($event)"
2175
- >
2374
+ <mat-label>{{ ui.text('applyToFields') }}</mat-label>
2375
+ <mat-select multiple data-testid="rule-targets" [ngModel]="targetValues()" (ngModelChange)="setTargets($event)">
2176
2376
  @for (option of targetOptions(); track option.value) {
2177
2377
  <mat-option [value]="option.value">
2178
2378
  {{ option.label }} <span class="deb-rule-path">{{ option.path }}</span>
@@ -2183,11 +2383,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2183
2383
 
2184
2384
  <!-- Conditions -->
2185
2385
  <div class="deb-rule-section">
2186
- <h4>Conditions</h4>
2386
+ <h4>{{ ui.text('conditions') }}</h4>
2187
2387
  @for (cond of rule.conditions; track $index) {
2188
2388
  <div class="deb-rule-row">
2189
2389
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2190
- <mat-label>Operator</mat-label>
2390
+ <mat-label>{{ ui.text('operator') }}</mat-label>
2191
2391
  <mat-select [(ngModel)]="cond.operator">
2192
2392
  @for (op of operators; track op) {
2193
2393
  <mat-option [value]="op">{{ op }}</mat-option>
@@ -2195,8 +2395,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2195
2395
  </mat-select>
2196
2396
  </mat-form-field>
2197
2397
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2198
- <mat-label>Value</mat-label>
2199
- <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')" />
2200
2400
  </mat-form-field>
2201
2401
  <button mat-icon-button color="warn" type="button" (click)="removeCondition($index)">
2202
2402
  <mat-icon>delete</mat-icon>
@@ -2204,32 +2404,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2204
2404
  </div>
2205
2405
  }
2206
2406
  <button mat-stroked-button type="button" (click)="addCondition()">
2207
- <mat-icon>add</mat-icon> Add Condition
2407
+ <mat-icon>add</mat-icon> {{ ui.text('addCondition') }}
2208
2408
  </button>
2209
2409
  </div>
2210
2410
 
2211
2411
  <!-- Action -->
2212
2412
  <div class="deb-rule-section">
2213
- <h4>Action</h4>
2413
+ <h4>{{ ui.text('action') }}</h4>
2214
2414
  <div class="deb-rule-row">
2215
2415
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2216
- <mat-label>Action Type</mat-label>
2416
+ <mat-label>{{ ui.text('actionType') }}</mat-label>
2217
2417
  <mat-select [(ngModel)]="rule.action.type">
2218
- <mat-option value="visibility">Visibility (Show/Hide)</mat-option>
2219
- <mat-option value="validation">Validation Message</mat-option>
2220
- <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>
2221
2421
  </mat-select>
2222
2422
  </mat-form-field>
2223
2423
  <mat-form-field appearance="outline" subscriptSizing="dynamic">
2224
- <mat-label>Value / Message</mat-label>
2225
- <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')" />
2226
2426
  </mat-form-field>
2227
2427
  </div>
2228
2428
  </div>
2229
2429
 
2230
2430
  <div class="deb-rule-form__actions">
2231
- <button mat-button type="button" (click)="cancel.emit()">Cancel</button>
2232
- <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>
2233
2433
  </div>
2234
2434
  </div>
2235
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"] }]
@@ -2246,6 +2446,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2246
2446
  * enable/disable, edit, delete. `RuleFormComponent` authors one rule; this owns the set.
2247
2447
  */
2248
2448
  class FieldRulesListComponent {
2449
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2450
+ ui = inject(BuilderTextService);
2249
2451
  store = inject(BuilderStore);
2250
2452
  /** The rule currently open in the editor — a new draft or a copy of an existing rule. */
2251
2453
  editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
@@ -2297,7 +2499,7 @@ class FieldRulesListComponent {
2297
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: `
2298
2500
  <div class="deb-rules">
2299
2501
  <div class="deb-rules__head">
2300
- <span class="deb-section-title">Rules</span>
2502
+ <span class="deb-section-title">{{ ui.text('rules') }}</span>
2301
2503
  <button
2302
2504
  mat-stroked-button
2303
2505
  type="button"
@@ -2305,7 +2507,7 @@ class FieldRulesListComponent {
2305
2507
  [disabled]="!store.selectedFieldId()"
2306
2508
  (click)="startCreate()"
2307
2509
  >
2308
- <mat-icon>add</mat-icon> Rule
2510
+ <mat-icon>add</mat-icon> {{ ui.text('rule') }}
2309
2511
  </button>
2310
2512
  </div>
2311
2513
 
@@ -2314,7 +2516,7 @@ class FieldRulesListComponent {
2314
2516
  }
2315
2517
 
2316
2518
  @if (store.rulesForSelectedField().length === 0 && !editing()) {
2317
- <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>
2318
2520
  }
2319
2521
 
2320
2522
  @for (rule of store.rulesForSelectedField(); track rule.id) {
@@ -2322,19 +2524,20 @@ class FieldRulesListComponent {
2322
2524
  <div class="deb-rule-item__body">
2323
2525
  <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
2324
2526
  <span class="deb-rule-item__meta">
2325
- priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
2527
+ {{ ui.text('rulePriority', { priority: rule.priority, targets: rule.targets.length }) }}
2326
2528
  </span>
2327
2529
  </div>
2328
2530
  <div class="deb-rule-item__actions">
2329
2531
  <mat-slide-toggle
2330
2532
  [checked]="rule.enabled"
2331
- matTooltip="Enable / disable"
2533
+ [matTooltip]="ui.text('toggleRule')"
2332
2534
  (change)="store.toggleRule(rule.id!, $event.checked)"
2333
2535
  />
2334
2536
  <button
2335
2537
  mat-icon-button
2336
2538
  type="button"
2337
- matTooltip="Move up" aria-label="Move rule up"
2539
+ [matTooltip]="ui.text('moveUp')"
2540
+ [attr.aria-label]="ui.text('moveRuleUp')"
2338
2541
  [attr.data-testid]="'rule-up-' + rule.id"
2339
2542
  (click)="store.moveRule(rule.id!, -1)"
2340
2543
  >
@@ -2343,7 +2546,8 @@ class FieldRulesListComponent {
2343
2546
  <button
2344
2547
  mat-icon-button
2345
2548
  type="button"
2346
- matTooltip="Move down" aria-label="Move rule down"
2549
+ [matTooltip]="ui.text('moveDown')"
2550
+ [attr.aria-label]="ui.text('moveRuleDown')"
2347
2551
  [attr.data-testid]="'rule-down-' + rule.id"
2348
2552
  (click)="store.moveRule(rule.id!, 1)"
2349
2553
  >
@@ -2352,7 +2556,7 @@ class FieldRulesListComponent {
2352
2556
  <button
2353
2557
  mat-icon-button
2354
2558
  type="button"
2355
- matTooltip="Edit rule"
2559
+ [matTooltip]="ui.text('editRule')"
2356
2560
  [attr.data-testid]="'rule-edit-' + rule.id"
2357
2561
  (click)="startEdit(rule)"
2358
2562
  >
@@ -2362,7 +2566,7 @@ class FieldRulesListComponent {
2362
2566
  mat-icon-button
2363
2567
  type="button"
2364
2568
  color="warn"
2365
- matTooltip="Delete rule"
2569
+ [matTooltip]="ui.text('deleteRule')"
2366
2570
  [attr.data-testid]="'rule-delete-' + rule.id"
2367
2571
  (click)="store.removeRule(rule.id!)"
2368
2572
  >
@@ -2379,7 +2583,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2379
2583
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
2380
2584
  <div class="deb-rules">
2381
2585
  <div class="deb-rules__head">
2382
- <span class="deb-section-title">Rules</span>
2586
+ <span class="deb-section-title">{{ ui.text('rules') }}</span>
2383
2587
  <button
2384
2588
  mat-stroked-button
2385
2589
  type="button"
@@ -2387,7 +2591,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2387
2591
  [disabled]="!store.selectedFieldId()"
2388
2592
  (click)="startCreate()"
2389
2593
  >
2390
- <mat-icon>add</mat-icon> Rule
2594
+ <mat-icon>add</mat-icon> {{ ui.text('rule') }}
2391
2595
  </button>
2392
2596
  </div>
2393
2597
 
@@ -2396,7 +2600,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2396
2600
  }
2397
2601
 
2398
2602
  @if (store.rulesForSelectedField().length === 0 && !editing()) {
2399
- <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>
2400
2604
  }
2401
2605
 
2402
2606
  @for (rule of store.rulesForSelectedField(); track rule.id) {
@@ -2404,19 +2608,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2404
2608
  <div class="deb-rule-item__body">
2405
2609
  <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
2406
2610
  <span class="deb-rule-item__meta">
2407
- priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
2611
+ {{ ui.text('rulePriority', { priority: rule.priority, targets: rule.targets.length }) }}
2408
2612
  </span>
2409
2613
  </div>
2410
2614
  <div class="deb-rule-item__actions">
2411
2615
  <mat-slide-toggle
2412
2616
  [checked]="rule.enabled"
2413
- matTooltip="Enable / disable"
2617
+ [matTooltip]="ui.text('toggleRule')"
2414
2618
  (change)="store.toggleRule(rule.id!, $event.checked)"
2415
2619
  />
2416
2620
  <button
2417
2621
  mat-icon-button
2418
2622
  type="button"
2419
- matTooltip="Move up" aria-label="Move rule up"
2623
+ [matTooltip]="ui.text('moveUp')"
2624
+ [attr.aria-label]="ui.text('moveRuleUp')"
2420
2625
  [attr.data-testid]="'rule-up-' + rule.id"
2421
2626
  (click)="store.moveRule(rule.id!, -1)"
2422
2627
  >
@@ -2425,7 +2630,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2425
2630
  <button
2426
2631
  mat-icon-button
2427
2632
  type="button"
2428
- matTooltip="Move down" aria-label="Move rule down"
2633
+ [matTooltip]="ui.text('moveDown')"
2634
+ [attr.aria-label]="ui.text('moveRuleDown')"
2429
2635
  [attr.data-testid]="'rule-down-' + rule.id"
2430
2636
  (click)="store.moveRule(rule.id!, 1)"
2431
2637
  >
@@ -2434,7 +2640,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2434
2640
  <button
2435
2641
  mat-icon-button
2436
2642
  type="button"
2437
- matTooltip="Edit rule"
2643
+ [matTooltip]="ui.text('editRule')"
2438
2644
  [attr.data-testid]="'rule-edit-' + rule.id"
2439
2645
  (click)="startEdit(rule)"
2440
2646
  >
@@ -2444,7 +2650,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2444
2650
  mat-icon-button
2445
2651
  type="button"
2446
2652
  color="warn"
2447
- matTooltip="Delete rule"
2653
+ [matTooltip]="ui.text('deleteRule')"
2448
2654
  [attr.data-testid]="'rule-delete-' + rule.id"
2449
2655
  (click)="store.removeRule(rule.id!)"
2450
2656
  >
@@ -2458,6 +2664,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2458
2664
  }] });
2459
2665
 
2460
2666
  class ReferencedFieldConfigComponent {
2667
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2668
+ ui = inject(BuilderTextService);
2461
2669
  store = inject(BuilderStore);
2462
2670
  configSource = inject(ConfigSourceService, { optional: true });
2463
2671
  field = computed(() => this.store.selectedField(), ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
@@ -2536,56 +2744,54 @@ class ReferencedFieldConfigComponent {
2536
2744
  @if (field(); as f) {
2537
2745
  <div class="deb-referenced-field" style="margin-top: 16px;">
2538
2746
  <div class="deb-row" style="justify-content: space-between; align-items: center; margin-bottom: 8px;">
2539
- <span class="deb-section-title" style="margin:0;">Referenced Field</span>
2747
+ <span class="deb-section-title" style="margin:0;">{{ ui.text('referencedField') }}</span>
2540
2748
  <mat-slide-toggle
2541
2749
  data-testid="toggle-referenced"
2542
2750
  [ngModel]="f.isReferenced ?? false"
2543
2751
  (ngModelChange)="toggleReferenced(f, $event)"
2544
2752
  >
2545
- Link
2753
+ {{ ui.text('link') }}
2546
2754
  </mat-slide-toggle>
2547
2755
  </div>
2548
2756
 
2549
2757
  @if (f.isReferenced) {
2550
2758
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2551
- <mat-label>Source Entity Key</mat-label>
2759
+ <mat-label>{{ ui.text('sourceEntityKey') }}</mat-label>
2552
2760
  <input
2553
2761
  matInput
2554
2762
  data-testid="referenced-entity-key"
2555
2763
  [ngModel]="f.referencedEntityKey ?? ''"
2556
2764
  (ngModelChange)="updateEntityKey(f, $event)"
2557
- placeholder="e.g. individuals"
2765
+ [placeholder]="ui.text('sourceEntityKeyPlaceholder')"
2558
2766
  />
2559
2767
  </mat-form-field>
2560
2768
 
2561
2769
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2562
- <mat-label>Source Field ID</mat-label>
2770
+ <mat-label>{{ ui.text('sourceFieldId') }}</mat-label>
2563
2771
  <input
2564
2772
  matInput
2565
2773
  data-testid="referenced-field-id"
2566
2774
  [ngModel]="f.referencedFieldId ?? ''"
2567
2775
  (ngModelChange)="updateFieldId(f, $event)"
2568
- placeholder="e.g. firstName"
2776
+ [placeholder]="ui.text('sourceFieldIdPlaceholder')"
2569
2777
  />
2570
2778
  </mat-form-field>
2571
2779
 
2572
2780
  @if (f.hasDrift) {
2573
- <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
+ >
2574
2786
  <div class="deb-row" style="gap: 8px; align-items: center; color: #e65100; font-weight: 500;">
2575
2787
  <mat-icon style="font-size: 18px; width:18px; height:18px;">warning</mat-icon>
2576
- <span>Source field definition has drifted!</span>
2788
+ <span>{{ ui.text('driftHeading') }}</span>
2577
2789
  </div>
2578
2790
  <p style="margin: 4px 0 8px 0; font-size: 12px; color: #666;">
2579
- The upstream field configuration in "{{ f.referencedEntityKey }}" has evolved.
2791
+ {{ ui.text('driftBody', { entity: f.referencedEntityKey ?? '' }) }}
2580
2792
  </p>
2581
- <button
2582
- mat-flat-button
2583
- color="warn"
2584
- type="button"
2585
- data-testid="sync-source-btn"
2586
- (click)="syncWithSource(f)"
2587
- >
2588
- Sync with Source
2793
+ <button mat-flat-button color="warn" type="button" data-testid="sync-source-btn" (click)="syncWithSource(f)">
2794
+ {{ ui.text('syncWithSource') }}
2589
2795
  </button>
2590
2796
  </div>
2591
2797
  }
@@ -2614,56 +2820,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2614
2820
  @if (field(); as f) {
2615
2821
  <div class="deb-referenced-field" style="margin-top: 16px;">
2616
2822
  <div class="deb-row" style="justify-content: space-between; align-items: center; margin-bottom: 8px;">
2617
- <span class="deb-section-title" style="margin:0;">Referenced Field</span>
2823
+ <span class="deb-section-title" style="margin:0;">{{ ui.text('referencedField') }}</span>
2618
2824
  <mat-slide-toggle
2619
2825
  data-testid="toggle-referenced"
2620
2826
  [ngModel]="f.isReferenced ?? false"
2621
2827
  (ngModelChange)="toggleReferenced(f, $event)"
2622
2828
  >
2623
- Link
2829
+ {{ ui.text('link') }}
2624
2830
  </mat-slide-toggle>
2625
2831
  </div>
2626
2832
 
2627
2833
  @if (f.isReferenced) {
2628
2834
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2629
- <mat-label>Source Entity Key</mat-label>
2835
+ <mat-label>{{ ui.text('sourceEntityKey') }}</mat-label>
2630
2836
  <input
2631
2837
  matInput
2632
2838
  data-testid="referenced-entity-key"
2633
2839
  [ngModel]="f.referencedEntityKey ?? ''"
2634
2840
  (ngModelChange)="updateEntityKey(f, $event)"
2635
- placeholder="e.g. individuals"
2841
+ [placeholder]="ui.text('sourceEntityKeyPlaceholder')"
2636
2842
  />
2637
2843
  </mat-form-field>
2638
2844
 
2639
2845
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full" style="margin-bottom: 8px;">
2640
- <mat-label>Source Field ID</mat-label>
2846
+ <mat-label>{{ ui.text('sourceFieldId') }}</mat-label>
2641
2847
  <input
2642
2848
  matInput
2643
2849
  data-testid="referenced-field-id"
2644
2850
  [ngModel]="f.referencedFieldId ?? ''"
2645
2851
  (ngModelChange)="updateFieldId(f, $event)"
2646
- placeholder="e.g. firstName"
2852
+ [placeholder]="ui.text('sourceFieldIdPlaceholder')"
2647
2853
  />
2648
2854
  </mat-form-field>
2649
2855
 
2650
2856
  @if (f.hasDrift) {
2651
- <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
+ >
2652
2862
  <div class="deb-row" style="gap: 8px; align-items: center; color: #e65100; font-weight: 500;">
2653
2863
  <mat-icon style="font-size: 18px; width:18px; height:18px;">warning</mat-icon>
2654
- <span>Source field definition has drifted!</span>
2864
+ <span>{{ ui.text('driftHeading') }}</span>
2655
2865
  </div>
2656
2866
  <p style="margin: 4px 0 8px 0; font-size: 12px; color: #666;">
2657
- The upstream field configuration in "{{ f.referencedEntityKey }}" has evolved.
2867
+ {{ ui.text('driftBody', { entity: f.referencedEntityKey ?? '' }) }}
2658
2868
  </p>
2659
- <button
2660
- mat-flat-button
2661
- color="warn"
2662
- type="button"
2663
- data-testid="sync-source-btn"
2664
- (click)="syncWithSource(f)"
2665
- >
2666
- Sync with Source
2869
+ <button mat-flat-button color="warn" type="button" data-testid="sync-source-btn" (click)="syncWithSource(f)">
2870
+ {{ ui.text('syncWithSource') }}
2667
2871
  </button>
2668
2872
  </div>
2669
2873
  }
@@ -2678,6 +2882,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2678
2882
  * FieldInspectorComponent — edits every property of the currently selected field.
2679
2883
  */
2680
2884
  class FieldInspectorComponent {
2885
+ /** Builder chrome, overridable via BUILDER_TEXT. */
2886
+ ui = inject(BuilderTextService);
2681
2887
  store = inject(BuilderStore);
2682
2888
  field = this.store.selectedField;
2683
2889
  meta = computed(() => {
@@ -2814,7 +3020,7 @@ class FieldInspectorComponent {
2814
3020
  return String(value);
2815
3021
  }
2816
3022
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2817
- 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" }] });
2818
3024
  }
2819
3025
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, decorators: [{
2820
3026
  type: Component,
@@ -2832,7 +3038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2832
3038
  EntityReferenceConfigComponent,
2833
3039
  FieldRulesListComponent,
2834
3040
  ReferencedFieldConfigComponent,
2835
- ], 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"] }]
2836
3042
  }] });
2837
3043
 
2838
3044
  /**
@@ -2887,8 +3093,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
2887
3093
  * TabManagerComponent — add / rename / reorder / remove tabs & sub-tabs with systemDefault protection.
2888
3094
  */
2889
3095
  class TabManagerComponent {
3096
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3097
+ ui = inject(BuilderTextService);
2890
3098
  store = inject(BuilderStore);
2891
- canEditSystemDefaultsFn = inject(SYSTEM_DEFAULT_CAN_EDIT, { optional: true });
3099
+ canEditSystemDefaultsFn = inject(SYSTEM_DEFAULT_CAN_EDIT, {
3100
+ optional: true,
3101
+ });
2892
3102
  lang() {
2893
3103
  return this.store.activeLanguage();
2894
3104
  }
@@ -2910,21 +3120,21 @@ class TabManagerComponent {
2910
3120
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
2911
3121
  <div class="deb-tabs">
2912
3122
  <div class="deb-tabs__head">
2913
- <span class="deb-section-title">Tabs Manager</span>
3123
+ <span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
2914
3124
  <button mat-stroked-button type="button" (click)="store.addTab()">
2915
- <mat-icon>add</mat-icon> Add Tab
3125
+ <mat-icon>add</mat-icon> {{ ui.text('addTab') }}
2916
3126
  </button>
2917
3127
  </div>
2918
3128
 
2919
3129
  @if (store.tabs().length === 0) {
2920
- <p class="deb-hint">No tabs — all fields render in a single section.</p>
3130
+ <p class="deb-hint">{{ ui.text('noTabs') }}</p>
2921
3131
  }
2922
3132
 
2923
3133
  @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2924
3134
  <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2925
3135
  <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2926
3136
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2927
- <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
3137
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? ui.text('systemTag') : '' }}</mat-label>
2928
3138
  <input
2929
3139
  matInput
2930
3140
  [disabled]="!canEditTab(tab)"
@@ -2937,7 +3147,8 @@ class TabManagerComponent {
2937
3147
  type="button"
2938
3148
  [disabled]="i === 0 || !canEditTab(tab)"
2939
3149
  (click)="store.moveTab(tab.id, -1)"
2940
- matTooltip="Move up" aria-label="Move tab up"
3150
+ [matTooltip]="ui.text('moveUp')"
3151
+ [attr.aria-label]="ui.text('moveTabUp')"
2941
3152
  >
2942
3153
  <mat-icon>arrow_upward</mat-icon>
2943
3154
  </button>
@@ -2946,7 +3157,8 @@ class TabManagerComponent {
2946
3157
  type="button"
2947
3158
  [disabled]="i === count - 1 || !canEditTab(tab)"
2948
3159
  (click)="store.moveTab(tab.id, 1)"
2949
- matTooltip="Move down" aria-label="Move tab down"
3160
+ [matTooltip]="ui.text('moveDown')"
3161
+ [attr.aria-label]="ui.text('moveTabDown')"
2950
3162
  >
2951
3163
  <mat-icon>arrow_downward</mat-icon>
2952
3164
  </button>
@@ -2956,7 +3168,8 @@ class TabManagerComponent {
2956
3168
  color="warn"
2957
3169
  [disabled]="!canEditTab(tab)"
2958
3170
  (click)="store.removeTab(tab.id)"
2959
- matTooltip="Remove tab" aria-label="Remove tab"
3171
+ [matTooltip]="ui.text('removeTab')"
3172
+ [attr.aria-label]="ui.text('removeTab')"
2960
3173
  >
2961
3174
  <mat-icon>delete</mat-icon>
2962
3175
  </button>
@@ -2969,47 +3182,41 @@ class TabManagerComponent {
2969
3182
  [ngModel]="tab.flatData"
2970
3183
  (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2971
3184
  >
2972
- Flat Data
3185
+ {{ ui.text('flatData') }}
2973
3186
  </mat-checkbox>
2974
3187
  <mat-checkbox
2975
3188
  [disabled]="!canEditTab(tab)"
2976
3189
  [ngModel]="tab.isPrimaryTab"
2977
3190
  (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2978
3191
  >
2979
- Primary Tab
3192
+ {{ ui.text('primaryTab') }}
2980
3193
  </mat-checkbox>
2981
3194
  <mat-checkbox
2982
3195
  [disabled]="!canEditTab(tab)"
2983
3196
  [ngModel]="tab.maskData"
2984
3197
  (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2985
3198
  >
2986
- Mask Tab Data
3199
+ {{ ui.text('maskTabData') }}
2987
3200
  </mat-checkbox>
2988
3201
  <mat-checkbox
2989
3202
  [disabled]="!canEditTab(tab)"
2990
3203
  [ngModel]="tab.systemDefault"
2991
3204
  (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2992
3205
  >
2993
- System Default
3206
+ {{ ui.text('systemDefault') }}
2994
3207
  </mat-checkbox>
2995
- <button
2996
- mat-button
2997
- type="button"
2998
- color="primary"
2999
- [disabled]="!canEditTab(tab)"
3000
- (click)="store.addSubTab(tab.id)"
3001
- >
3002
- + Sub-tab
3208
+ <button mat-button type="button" color="primary" [disabled]="!canEditTab(tab)" (click)="store.addSubTab(tab.id)">
3209
+ {{ ui.text('addSubTab') }}
3003
3210
  </button>
3004
3211
  </div>
3005
3212
 
3006
3213
  <!-- Module tab configuration -->
3007
3214
  <div class="deb-tab-module">
3008
3215
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
3009
- <mat-label>Consumer Module Name (Optional)</mat-label>
3216
+ <mat-label>{{ ui.text('consumerModuleName') }}</mat-label>
3010
3217
  <input
3011
3218
  matInput
3012
- placeholder="e.g. documents-view"
3219
+ [placeholder]="ui.text('consumerModulePlaceholder')"
3013
3220
  [disabled]="!canEditTab(tab)"
3014
3221
  [ngModel]="tab.moduleName"
3015
3222
  (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
@@ -3020,7 +3227,7 @@ class TabManagerComponent {
3020
3227
  <!-- Sub-tabs recursive render (Level 2) -->
3021
3228
  @if (tab.children && tab.children.length > 0) {
3022
3229
  <div class="deb-subtabs">
3023
- <span class="deb-subtabs__title">Sub-tabs:</span>
3230
+ <span class="deb-subtabs__title">{{ ui.text('subTabs') }}</span>
3024
3231
  @for (sub of tab.children; track sub.id) {
3025
3232
  <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
3026
3233
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
@@ -3038,7 +3245,8 @@ class TabManagerComponent {
3038
3245
  color="warn"
3039
3246
  [disabled]="!canEditTab(sub)"
3040
3247
  (click)="store.removeTab(sub.id)"
3041
- matTooltip="Remove sub-tab" aria-label="Remove sub-tab"
3248
+ [matTooltip]="ui.text('removeSubTab')"
3249
+ [attr.aria-label]="ui.text('removeSubTab')"
3042
3250
  >
3043
3251
  <mat-icon>delete</mat-icon>
3044
3252
  </button>
@@ -3065,21 +3273,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3065
3273
  ], template: `
3066
3274
  <div class="deb-tabs">
3067
3275
  <div class="deb-tabs__head">
3068
- <span class="deb-section-title">Tabs Manager</span>
3276
+ <span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
3069
3277
  <button mat-stroked-button type="button" (click)="store.addTab()">
3070
- <mat-icon>add</mat-icon> Add Tab
3278
+ <mat-icon>add</mat-icon> {{ ui.text('addTab') }}
3071
3279
  </button>
3072
3280
  </div>
3073
3281
 
3074
3282
  @if (store.tabs().length === 0) {
3075
- <p class="deb-hint">No tabs — all fields render in a single section.</p>
3283
+ <p class="deb-hint">{{ ui.text('noTabs') }}</p>
3076
3284
  }
3077
3285
 
3078
3286
  @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
3079
3287
  <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
3080
3288
  <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
3081
3289
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
3082
- <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
3290
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? ui.text('systemTag') : '' }}</mat-label>
3083
3291
  <input
3084
3292
  matInput
3085
3293
  [disabled]="!canEditTab(tab)"
@@ -3092,7 +3300,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3092
3300
  type="button"
3093
3301
  [disabled]="i === 0 || !canEditTab(tab)"
3094
3302
  (click)="store.moveTab(tab.id, -1)"
3095
- matTooltip="Move up" aria-label="Move tab up"
3303
+ [matTooltip]="ui.text('moveUp')"
3304
+ [attr.aria-label]="ui.text('moveTabUp')"
3096
3305
  >
3097
3306
  <mat-icon>arrow_upward</mat-icon>
3098
3307
  </button>
@@ -3101,7 +3310,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3101
3310
  type="button"
3102
3311
  [disabled]="i === count - 1 || !canEditTab(tab)"
3103
3312
  (click)="store.moveTab(tab.id, 1)"
3104
- matTooltip="Move down" aria-label="Move tab down"
3313
+ [matTooltip]="ui.text('moveDown')"
3314
+ [attr.aria-label]="ui.text('moveTabDown')"
3105
3315
  >
3106
3316
  <mat-icon>arrow_downward</mat-icon>
3107
3317
  </button>
@@ -3111,7 +3321,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3111
3321
  color="warn"
3112
3322
  [disabled]="!canEditTab(tab)"
3113
3323
  (click)="store.removeTab(tab.id)"
3114
- matTooltip="Remove tab" aria-label="Remove tab"
3324
+ [matTooltip]="ui.text('removeTab')"
3325
+ [attr.aria-label]="ui.text('removeTab')"
3115
3326
  >
3116
3327
  <mat-icon>delete</mat-icon>
3117
3328
  </button>
@@ -3124,47 +3335,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3124
3335
  [ngModel]="tab.flatData"
3125
3336
  (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
3126
3337
  >
3127
- Flat Data
3338
+ {{ ui.text('flatData') }}
3128
3339
  </mat-checkbox>
3129
3340
  <mat-checkbox
3130
3341
  [disabled]="!canEditTab(tab)"
3131
3342
  [ngModel]="tab.isPrimaryTab"
3132
3343
  (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
3133
3344
  >
3134
- Primary Tab
3345
+ {{ ui.text('primaryTab') }}
3135
3346
  </mat-checkbox>
3136
3347
  <mat-checkbox
3137
3348
  [disabled]="!canEditTab(tab)"
3138
3349
  [ngModel]="tab.maskData"
3139
3350
  (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
3140
3351
  >
3141
- Mask Tab Data
3352
+ {{ ui.text('maskTabData') }}
3142
3353
  </mat-checkbox>
3143
3354
  <mat-checkbox
3144
3355
  [disabled]="!canEditTab(tab)"
3145
3356
  [ngModel]="tab.systemDefault"
3146
3357
  (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
3147
3358
  >
3148
- System Default
3359
+ {{ ui.text('systemDefault') }}
3149
3360
  </mat-checkbox>
3150
- <button
3151
- mat-button
3152
- type="button"
3153
- color="primary"
3154
- [disabled]="!canEditTab(tab)"
3155
- (click)="store.addSubTab(tab.id)"
3156
- >
3157
- + Sub-tab
3361
+ <button mat-button type="button" color="primary" [disabled]="!canEditTab(tab)" (click)="store.addSubTab(tab.id)">
3362
+ {{ ui.text('addSubTab') }}
3158
3363
  </button>
3159
3364
  </div>
3160
3365
 
3161
3366
  <!-- Module tab configuration -->
3162
3367
  <div class="deb-tab-module">
3163
3368
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
3164
- <mat-label>Consumer Module Name (Optional)</mat-label>
3369
+ <mat-label>{{ ui.text('consumerModuleName') }}</mat-label>
3165
3370
  <input
3166
3371
  matInput
3167
- placeholder="e.g. documents-view"
3372
+ [placeholder]="ui.text('consumerModulePlaceholder')"
3168
3373
  [disabled]="!canEditTab(tab)"
3169
3374
  [ngModel]="tab.moduleName"
3170
3375
  (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
@@ -3175,7 +3380,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3175
3380
  <!-- Sub-tabs recursive render (Level 2) -->
3176
3381
  @if (tab.children && tab.children.length > 0) {
3177
3382
  <div class="deb-subtabs">
3178
- <span class="deb-subtabs__title">Sub-tabs:</span>
3383
+ <span class="deb-subtabs__title">{{ ui.text('subTabs') }}</span>
3179
3384
  @for (sub of tab.children; track sub.id) {
3180
3385
  <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
3181
3386
  <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
@@ -3193,7 +3398,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3193
3398
  color="warn"
3194
3399
  [disabled]="!canEditTab(sub)"
3195
3400
  (click)="store.removeTab(sub.id)"
3196
- matTooltip="Remove sub-tab" aria-label="Remove sub-tab"
3401
+ [matTooltip]="ui.text('removeSubTab')"
3402
+ [attr.aria-label]="ui.text('removeSubTab')"
3197
3403
  >
3198
3404
  <mat-icon>delete</mat-icon>
3199
3405
  </button>
@@ -3208,6 +3414,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3208
3414
  }] });
3209
3415
 
3210
3416
  class EntityBuilderTreeNodeComponent {
3417
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3418
+ ui = inject(BuilderTextService);
3211
3419
  store = inject(BuilderStore);
3212
3420
  field;
3213
3421
  index;
@@ -3235,58 +3443,89 @@ class EntityBuilderTreeNodeComponent {
3235
3443
  role="button"
3236
3444
  tabindex="0"
3237
3445
  [attr.aria-pressed]="store.isSelected(field)"
3238
- [attr.aria-label]="'Select field ' + fieldLabel(field)"
3446
+ [attr.aria-label]="ui.text('selectField', { field: fieldLabel(field) })"
3239
3447
  >
3240
- <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
+ >
3241
3451
  <mat-icon class="deb-field-type" [matTooltip]="fieldTypeLabel(field.type)">{{ fieldTypeIcon(field.type) }}</mat-icon>
3242
3452
  <div class="deb-field-meta">
3243
3453
  <div class="deb-row" style="gap:6px">
3244
3454
  <span class="deb-field-label" [attr.data-testid]="'row-label-' + field.id">{{ fieldLabel(field) }}</span>
3245
3455
  <span class="deb-type-badge">{{ field.type }}</span>
3246
3456
  @if (field.isReferenced) {
3247
- <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
+ >
3248
3462
  }
3249
3463
  @if (field.hasDrift) {
3250
- <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
+ >
3251
3470
  }
3252
3471
  </div>
3253
3472
  <span class="deb-field-id" [attr.data-testid]="'row-id-' + field.id">{{ field.id }}</span>
3254
3473
  </div>
3255
- @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
+ }
3256
3477
 
3257
- <button mat-icon-button type="button" [disabled]="index === 0"
3478
+ <button
3479
+ mat-icon-button
3480
+ type="button"
3481
+ [disabled]="index === 0"
3258
3482
  [attr.data-testid]="'row-up-' + field.id"
3259
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
3260
- (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
+ >
3261
3487
  <mat-icon>arrow_upward</mat-icon>
3262
3488
  </button>
3263
- <button mat-icon-button type="button" [disabled]="index === totalCount - 1"
3489
+ <button
3490
+ mat-icon-button
3491
+ type="button"
3492
+ [disabled]="index === totalCount - 1"
3264
3493
  [attr.data-testid]="'row-down-' + field.id"
3265
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
3266
- (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
+ >
3267
3498
  <mat-icon>arrow_downward</mat-icon>
3268
3499
  </button>
3269
- <button mat-icon-button type="button"
3500
+ <button
3501
+ mat-icon-button
3502
+ type="button"
3270
3503
  [attr.data-testid]="'row-duplicate-' + field.id"
3271
- [attr.aria-label]="'Duplicate ' + fieldLabel(field)"
3272
- (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
+ >
3273
3508
  <mat-icon>content_copy</mat-icon>
3274
3509
  </button>
3275
- <button mat-icon-button type="button" color="warn"
3510
+ <button
3511
+ mat-icon-button
3512
+ type="button"
3513
+ color="warn"
3276
3514
  [attr.data-testid]="'row-delete-' + field.id"
3277
- (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
+ >
3278
3518
  <mat-icon>delete</mat-icon>
3279
3519
  </button>
3280
3520
  </div>
3281
3521
 
3282
3522
  @if (field.children && field.children.length > 0) {
3283
- <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
+ >
3284
3527
  @for (child of field.children; track child.id; let ci = $index, cCount = $count) {
3285
- <ngx-entity-builder-tree-node
3286
- [field]="child"
3287
- [index]="ci"
3288
- [totalCount]="cCount"
3289
- />
3528
+ <ngx-entity-builder-tree-node [field]="child" [index]="ci" [totalCount]="cCount" />
3290
3529
  }
3291
3530
  </div>
3292
3531
  }
@@ -3298,13 +3537,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3298
3537
  changeDetection: ChangeDetectionStrategy.OnPush,
3299
3538
  selector: 'ngx-entity-builder-tree-node',
3300
3539
  standalone: true,
3301
- imports: [
3302
- CommonModule,
3303
- DragDropModule,
3304
- MatButtonModule,
3305
- MatIconModule,
3306
- MatTooltipModule,
3307
- ],
3540
+ imports: [CommonModule, DragDropModule, MatButtonModule, MatIconModule, MatTooltipModule],
3308
3541
  template: `
3309
3542
  <div
3310
3543
  cdkDrag
@@ -3317,58 +3550,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3317
3550
  role="button"
3318
3551
  tabindex="0"
3319
3552
  [attr.aria-pressed]="store.isSelected(field)"
3320
- [attr.aria-label]="'Select field ' + fieldLabel(field)"
3553
+ [attr.aria-label]="ui.text('selectField', { field: fieldLabel(field) })"
3321
3554
  >
3322
- <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
+ >
3323
3558
  <mat-icon class="deb-field-type" [matTooltip]="fieldTypeLabel(field.type)">{{ fieldTypeIcon(field.type) }}</mat-icon>
3324
3559
  <div class="deb-field-meta">
3325
3560
  <div class="deb-row" style="gap:6px">
3326
3561
  <span class="deb-field-label" [attr.data-testid]="'row-label-' + field.id">{{ fieldLabel(field) }}</span>
3327
3562
  <span class="deb-type-badge">{{ field.type }}</span>
3328
3563
  @if (field.isReferenced) {
3329
- <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
+ >
3330
3569
  }
3331
3570
  @if (field.hasDrift) {
3332
- <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
+ >
3333
3577
  }
3334
3578
  </div>
3335
3579
  <span class="deb-field-id" [attr.data-testid]="'row-id-' + field.id">{{ field.id }}</span>
3336
3580
  </div>
3337
- @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
+ }
3338
3584
 
3339
- <button mat-icon-button type="button" [disabled]="index === 0"
3585
+ <button
3586
+ mat-icon-button
3587
+ type="button"
3588
+ [disabled]="index === 0"
3340
3589
  [attr.data-testid]="'row-up-' + field.id"
3341
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
3342
- (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
+ >
3343
3594
  <mat-icon>arrow_upward</mat-icon>
3344
3595
  </button>
3345
- <button mat-icon-button type="button" [disabled]="index === totalCount - 1"
3596
+ <button
3597
+ mat-icon-button
3598
+ type="button"
3599
+ [disabled]="index === totalCount - 1"
3346
3600
  [attr.data-testid]="'row-down-' + field.id"
3347
- [attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
3348
- (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
+ >
3349
3605
  <mat-icon>arrow_downward</mat-icon>
3350
3606
  </button>
3351
- <button mat-icon-button type="button"
3607
+ <button
3608
+ mat-icon-button
3609
+ type="button"
3352
3610
  [attr.data-testid]="'row-duplicate-' + field.id"
3353
- [attr.aria-label]="'Duplicate ' + fieldLabel(field)"
3354
- (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
+ >
3355
3615
  <mat-icon>content_copy</mat-icon>
3356
3616
  </button>
3357
- <button mat-icon-button type="button" color="warn"
3617
+ <button
3618
+ mat-icon-button
3619
+ type="button"
3620
+ color="warn"
3358
3621
  [attr.data-testid]="'row-delete-' + field.id"
3359
- (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
+ >
3360
3625
  <mat-icon>delete</mat-icon>
3361
3626
  </button>
3362
3627
  </div>
3363
3628
 
3364
3629
  @if (field.children && field.children.length > 0) {
3365
- <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
+ >
3366
3634
  @for (child of field.children; track child.id; let ci = $index, cCount = $count) {
3367
- <ngx-entity-builder-tree-node
3368
- [field]="child"
3369
- [index]="ci"
3370
- [totalCount]="cCount"
3371
- />
3635
+ <ngx-entity-builder-tree-node [field]="child" [index]="ci" [totalCount]="cCount" />
3372
3636
  }
3373
3637
  </div>
3374
3638
  }
@@ -3386,6 +3650,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3386
3650
  }] } });
3387
3651
 
3388
3652
  class EntityBuilderCanvasComponent {
3653
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3654
+ ui = inject(BuilderTextService);
3389
3655
  store = inject(BuilderStore);
3390
3656
  /**
3391
3657
  * Headings only earn their space once the fields are actually split across tabs. A
@@ -3404,13 +3670,13 @@ class EntityBuilderCanvasComponent {
3404
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: `
3405
3671
  <mat-card class="deb-canvas">
3406
3672
  <mat-card-header>
3407
- <mat-card-title>Fields ({{ store.fields().length }})</mat-card-title>
3673
+ <mat-card-title>{{ ui.text('fieldsHeading', { count: store.fields().length }) }}</mat-card-title>
3408
3674
  </mat-card-header>
3409
3675
  <mat-card-content>
3410
3676
  @if (store.fields().length === 0) {
3411
3677
  <div class="deb-empty">
3412
3678
  <mat-icon>widgets</mat-icon>
3413
- <p>No fields yet. Pick a type from <strong>Add field</strong> to get started.</p>
3679
+ <p>{{ ui.text('canvasEmpty') }}</p>
3414
3680
  </div>
3415
3681
  }
3416
3682
 
@@ -3432,11 +3698,7 @@ class EntityBuilderCanvasComponent {
3432
3698
  (cdkDropListDropped)="onDrop(group.tabId, $event)"
3433
3699
  >
3434
3700
  @for (f of group.fields; track f.id; let i = $index, count = $count) {
3435
- <ngx-entity-builder-tree-node
3436
- [field]="f"
3437
- [index]="i"
3438
- [totalCount]="count"
3439
- />
3701
+ <ngx-entity-builder-tree-node [field]="f" [index]="i" [totalCount]="count" />
3440
3702
  }
3441
3703
  </div>
3442
3704
  }
@@ -3450,23 +3712,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3450
3712
  changeDetection: ChangeDetectionStrategy.OnPush,
3451
3713
  selector: 'ngx-entity-builder-canvas',
3452
3714
  standalone: true,
3453
- imports: [
3454
- CommonModule,
3455
- DragDropModule,
3456
- MatCardModule,
3457
- MatIconModule,
3458
- EntityBuilderTreeNodeComponent,
3459
- ],
3715
+ imports: [CommonModule, DragDropModule, MatCardModule, MatIconModule, EntityBuilderTreeNodeComponent],
3460
3716
  template: `
3461
3717
  <mat-card class="deb-canvas">
3462
3718
  <mat-card-header>
3463
- <mat-card-title>Fields ({{ store.fields().length }})</mat-card-title>
3719
+ <mat-card-title>{{ ui.text('fieldsHeading', { count: store.fields().length }) }}</mat-card-title>
3464
3720
  </mat-card-header>
3465
3721
  <mat-card-content>
3466
3722
  @if (store.fields().length === 0) {
3467
3723
  <div class="deb-empty">
3468
3724
  <mat-icon>widgets</mat-icon>
3469
- <p>No fields yet. Pick a type from <strong>Add field</strong> to get started.</p>
3725
+ <p>{{ ui.text('canvasEmpty') }}</p>
3470
3726
  </div>
3471
3727
  }
3472
3728
 
@@ -3488,11 +3744,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3488
3744
  (cdkDropListDropped)="onDrop(group.tabId, $event)"
3489
3745
  >
3490
3746
  @for (f of group.fields; track f.id; let i = $index, count = $count) {
3491
- <ngx-entity-builder-tree-node
3492
- [field]="f"
3493
- [index]="i"
3494
- [totalCount]="count"
3495
- />
3747
+ <ngx-entity-builder-tree-node [field]="f" [index]="i" [totalCount]="count" />
3496
3748
  }
3497
3749
  </div>
3498
3750
  }
@@ -3510,6 +3762,8 @@ const EMPTY_ROLES = Object.freeze([]);
3510
3762
  * EntityBuilderComponent — the top-level visual builder for an EntityFormConfig.
3511
3763
  */
3512
3764
  class EntityBuilderComponent {
3765
+ /** Builder chrome, overridable via BUILDER_TEXT. */
3766
+ ui = inject(BuilderTextService);
3513
3767
  store = inject(BuilderStore);
3514
3768
  /**
3515
3769
  * Optional: only apps that registered a CONFIG_SOURCE have one. Used to drop the cached
@@ -3520,6 +3774,16 @@ class EntityBuilderComponent {
3520
3774
  config;
3521
3775
  /** Languages available for label/placeholder editing. First entry is the default. */
3522
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
+ }
3523
3787
  /** Optional role list — enables multi-select role pickers for RBAC instead of free text. */
3524
3788
  availableRoles = [];
3525
3789
  /**
@@ -3609,7 +3873,7 @@ class EntityBuilderComponent {
3609
3873
  this.store.setPermission(action, roles);
3610
3874
  }
3611
3875
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3612
- 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 });
3613
3877
  }
3614
3878
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: EntityBuilderComponent, decorators: [{
3615
3879
  type: Component,
@@ -3630,11 +3894,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3630
3894
  TabManagerComponent,
3631
3895
  FieldInspectorComponent,
3632
3896
  EntityBuilderCanvasComponent,
3633
- ], 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"] }]
3634
3898
  }], ctorParameters: () => [], propDecorators: { config: [{
3635
3899
  type: Input
3636
3900
  }], languages: [{
3637
3901
  type: Input
3902
+ }], uiLanguage: [{
3903
+ type: Input
3638
3904
  }], availableRoles: [{
3639
3905
  type: Input
3640
3906
  }], userRoles: [{
@@ -3656,5 +3922,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
3656
3922
  * Generated bundle index. Do not edit.
3657
3923
  */
3658
3924
 
3659
- 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 };
3660
3926
  //# sourceMappingURL=ngx-dynamic-entity-builder.mjs.map