@ng-forge/dynamic-form-mcp 0.9.0-next.9 → 0.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.
@@ -780,7 +780,7 @@ var VALIDATORS = [
780
780
  {
781
781
  type: "custom",
782
782
  category: "custom",
783
- description: "Custom synchronous validator using registered function or expression. IMPORTANT: The validator returns { kind: 'errorKind' } on failure. The actual error MESSAGE is defined in 'validationMessages' at the FIELD level, NOT in the validator config.",
783
+ description: "Custom synchronous validator using registered function or expression. IMPORTANT: The validator returns { kind: 'errorKind' } on failure. The actual error MESSAGE is defined in 'validationMessages' at the FIELD level, NOT in the validator config. NOTE: TypeScript-authored configs may also use an inline `fn: CustomValidator` instead of `functionName` (XOR with `functionName`, code-only \u2014 not JSON-serializable); MCP-generated configs should use `functionName`.",
784
784
  parameters: {
785
785
  functionName: {
786
786
  name: "functionName",
@@ -832,7 +832,7 @@ var VALIDATORS = [
832
832
  {
833
833
  type: "async",
834
834
  category: "async",
835
- description: "Async validator using registered async function (resource-based). Register the function via customFnConfig.asyncValidators.",
835
+ description: "Async validator using registered async function (resource-based). Register the function via customFnConfig.asyncValidators. NOTE: TypeScript-authored configs may also use an inline `fn: AsyncCustomValidator` instead of `functionName` (XOR with `functionName`, code-only \u2014 not JSON-serializable); MCP-generated configs should use `functionName`.",
836
836
  parameters: {
837
837
  functionName: {
838
838
  name: "functionName",
@@ -855,7 +855,7 @@ var VALIDATORS = [
855
855
  {
856
856
  type: "http",
857
857
  category: "http",
858
- description: "HTTP validator \u2014 supports two modes: (1) Declarative (fully JSON-serializable, no function registration) using http + responseMapping properties, or (2) Function-based using functionName to reference a registered HTTP validator function. Query param values and (optionally) body values are expressions evaluated against the form context. Response mapping uses validWhen (truthy = valid) and errorKind (maps to field-level validationMessages).",
858
+ description: "HTTP validator \u2014 supports two modes: (1) Declarative (fully JSON-serializable, no function registration) using http + responseMapping properties, or (2) Function-based using functionName to reference a registered HTTP validator function. Query param values and (optionally) body values are expressions evaluated against the form context. Response mapping uses validWhen (truthy = valid) and errorKind (maps to field-level validationMessages). NOTE: TypeScript-authored configs may also use an inline `fn: HttpCustomValidator` instead of `functionName` (XOR with `functionName`, code-only \u2014 not JSON-serializable); MCP-generated configs should use `functionName` or declarative mode.",
859
859
  parameters: {
860
860
  functionName: {
861
861
  name: "functionName",
@@ -1402,7 +1402,7 @@ var DOCUMENTATION = [
1402
1402
  // packages/dynamic-form-mcp/src/registry/wrappers.ts
1403
1403
  var WRAPPER_AUTHORING_CONTRACT = `All wrapper components MUST:
1404
1404
 
1405
- 1. Implement \`FieldWrapperContract\` from \`@ng-forge/dynamic-forms\`.
1405
+ 1. Implement \`FieldWrapper\` from \`@ng-forge/dynamic-forms\`.
1406
1406
  2. Expose \`readonly fieldComponent = viewChild.required('fieldComponent', { read: ViewContainerRef });\`
1407
1407
  \u2014 this is the slot where the inner wrapper (or the field) is rendered
1408
1408
  imperatively by the outlet.
@@ -1553,6 +1553,419 @@ const tagItemTemplate = { key: 'tag', type: 'input', label: 'Tag' } as const sat
1553
1553
  }
1554
1554
  ];
1555
1555
 
1556
+ // packages/dynamic-form-mcp/src/registry/addons.ts
1557
+ var UNIVERSAL_BASE_PROPS = {
1558
+ slot: {
1559
+ name: "slot",
1560
+ type: "'prefix' | 'suffix' | string",
1561
+ description: "Slot to render this addon in. `prefix` and `suffix` are universal; adapters may add more.",
1562
+ required: true
1563
+ },
1564
+ className: {
1565
+ name: "className",
1566
+ type: "string",
1567
+ description: "CSS class applied to the addon host element.",
1568
+ required: false
1569
+ },
1570
+ hidden: {
1571
+ name: "hidden",
1572
+ type: "boolean | Signal<boolean> | Observable<boolean>",
1573
+ description: "Reactive visibility \u2014 `true` removes the addon from layout. JSON-safe when boolean.",
1574
+ required: false
1575
+ },
1576
+ disabled: {
1577
+ name: "disabled",
1578
+ type: "boolean | Signal<boolean> | Observable<boolean>",
1579
+ description: "Reactive disabled state \u2014 primarily relevant for interactive kinds.",
1580
+ required: false
1581
+ }
1582
+ };
1583
+ var ADDON_KINDS = [
1584
+ // ─── Core-shipped universal kinds ────────────────────────────────────────
1585
+ {
1586
+ kind: "text",
1587
+ category: "core",
1588
+ package: "@ng-forge/dynamic-forms",
1589
+ adapter: null,
1590
+ description: "Renders inline text content (currency symbol, unit label, character counter, etc.).",
1591
+ jsonSafe: true,
1592
+ props: {
1593
+ ...UNIVERSAL_BASE_PROPS,
1594
+ text: {
1595
+ name: "text",
1596
+ type: "DynamicText",
1597
+ description: "Text content. Supports plain strings, observables, signals, and i18n keys.",
1598
+ required: true
1599
+ }
1600
+ },
1601
+ example: `{ slot: 'prefix', kind: 'text', text: '$' }`,
1602
+ minimalExample: `{ slot: 'prefix', kind: 'text', text: '$' }`
1603
+ },
1604
+ {
1605
+ kind: "template",
1606
+ category: "core",
1607
+ package: "@ng-forge/dynamic-forms",
1608
+ adapter: null,
1609
+ description: "Renders a named `<ng-template>` projected into <df-dynamic-form>. JSON-safe \u2014 backend ships the key, FE supplies the template.",
1610
+ jsonSafe: true,
1611
+ props: {
1612
+ ...UNIVERSAL_BASE_PROPS,
1613
+ templateKey: {
1614
+ name: "templateKey",
1615
+ type: "string",
1616
+ description: 'Name of an `<ng-template dfTemplate="...">` projected into the host form.',
1617
+ required: true
1618
+ }
1619
+ },
1620
+ example: `{ slot: 'suffix', kind: 'template', templateKey: 'searchIcon' }`,
1621
+ minimalExample: `{ slot: 'suffix', kind: 'template', templateKey: 'mySlot' }`
1622
+ },
1623
+ {
1624
+ kind: "component",
1625
+ category: "core",
1626
+ package: "@ng-forge/dynamic-forms",
1627
+ adapter: null,
1628
+ description: "Renders an arbitrary Angular component. The `component` loader is a function; this kind is dropped from JSON-derived configs by the validator.",
1629
+ jsonSafe: false,
1630
+ props: {
1631
+ ...UNIVERSAL_BASE_PROPS,
1632
+ component: {
1633
+ name: "component",
1634
+ type: "() => Promise<Type<unknown> | { default: Type<unknown> }>",
1635
+ description: "Lazy loader for the Angular component to render.",
1636
+ required: true
1637
+ },
1638
+ inputs: {
1639
+ name: "inputs",
1640
+ type: "Record<string, unknown>",
1641
+ description: "Inputs forwarded to the rendered component.",
1642
+ required: false
1643
+ }
1644
+ },
1645
+ example: `{
1646
+ slot: 'prefix',
1647
+ kind: 'component',
1648
+ component: () => import('./my-component').then(m => m.MyComponent),
1649
+ inputs: { foo: 'bar' },
1650
+ }`,
1651
+ minimalExample: `{ slot: 'prefix', kind: 'component', component: () => import('./x') }`
1652
+ },
1653
+ // ─── PrimeNG-shipped kinds ───────────────────────────────────────────────
1654
+ {
1655
+ kind: "prime-icon",
1656
+ category: "adapter",
1657
+ package: "@ng-forge/dynamic-forms-primeng",
1658
+ adapter: "primeng",
1659
+ description: 'Renders `<i class="pi pi-{icon}">` \u2014 bare PrimeIcons name.',
1660
+ jsonSafe: true,
1661
+ props: {
1662
+ ...UNIVERSAL_BASE_PROPS,
1663
+ icon: {
1664
+ name: "icon",
1665
+ type: "string",
1666
+ description: 'PrimeIcons name without the `pi-` prefix (e.g., "search", "times").',
1667
+ required: true
1668
+ },
1669
+ ariaLabel: {
1670
+ name: "ariaLabel",
1671
+ type: "DynamicText",
1672
+ description: "Accessible label for icons that convey meaning. Decorative icons omit this.",
1673
+ required: false
1674
+ }
1675
+ },
1676
+ example: `{ slot: 'prefix', kind: 'prime-icon', icon: 'search', ariaLabel: 'Search' }`,
1677
+ minimalExample: `{ slot: 'prefix', kind: 'prime-icon', icon: 'search' }`
1678
+ },
1679
+ {
1680
+ kind: "prime-button",
1681
+ category: "adapter",
1682
+ package: "@ng-forge/dynamic-forms-primeng",
1683
+ adapter: "primeng",
1684
+ description: "Renders `<p-button>` with optional icon, label, severity, reactive loading, and exactly one click handler (preset, actionRef, or action).",
1685
+ jsonSafe: true,
1686
+ props: {
1687
+ ...UNIVERSAL_BASE_PROPS,
1688
+ icon: { name: "icon", type: "string", description: "PrimeIcons name without the `pi-` prefix.", required: false },
1689
+ label: { name: "label", type: "DynamicText", description: "Visible button label.", required: false },
1690
+ ariaLabel: {
1691
+ name: "ariaLabel",
1692
+ type: "DynamicText",
1693
+ description: "Required when icon-only (no `label`). Used as `aria-label`.",
1694
+ required: false
1695
+ },
1696
+ severity: {
1697
+ name: "severity",
1698
+ type: "'primary' | 'secondary' | 'success' | 'info' | 'warn' | 'danger' | 'help' | 'contrast'",
1699
+ description: "PrimeNG button colour variant.",
1700
+ required: false
1701
+ },
1702
+ loading: {
1703
+ name: "loading",
1704
+ type: "boolean | Signal<boolean> | Observable<boolean>",
1705
+ description: "Reactive loading state \u2014 renders the native PrimeNG spinner.",
1706
+ required: false
1707
+ },
1708
+ preset: {
1709
+ name: "preset",
1710
+ type: "'clear' | 'reset' | 'paste' | 'copy' | 'toggle-password-visibility' | string",
1711
+ description: "Built-in preset action. Mutually exclusive with `actionRef` and `action`. Adapters may extend the preset union.",
1712
+ required: false
1713
+ },
1714
+ actionRef: {
1715
+ name: "actionRef",
1716
+ type: "string",
1717
+ description: "Typed string handle to a handler registered via `provideAddonActions(...)`. Mutually exclusive with `preset` and `action`. JSON-safe.",
1718
+ required: false
1719
+ },
1720
+ action: {
1721
+ name: "action",
1722
+ type: "(ctx: AddonActionContext) => void",
1723
+ description: "Inline click handler (code-only \u2014 dropped from JSON-derived configs). Mutually exclusive with `preset` and `actionRef`.",
1724
+ required: false
1725
+ }
1726
+ },
1727
+ example: `{ slot: 'suffix', kind: 'prime-button', icon: 'times', ariaLabel: 'Clear', preset: 'clear' }`,
1728
+ minimalExample: `{ slot: 'suffix', kind: 'prime-button', icon: 'times', ariaLabel: 'Clear', preset: 'clear' }`
1729
+ },
1730
+ // ─── Material-shipped kinds ──────────────────────────────────────────────
1731
+ {
1732
+ kind: "mat-icon",
1733
+ category: "adapter",
1734
+ package: "@ng-forge/dynamic-forms-material",
1735
+ adapter: "material",
1736
+ description: "Renders `<mat-icon>{icon}</mat-icon>` \u2014 Material Icons ligature name.",
1737
+ jsonSafe: true,
1738
+ props: {
1739
+ ...UNIVERSAL_BASE_PROPS,
1740
+ icon: {
1741
+ name: "icon",
1742
+ type: "string",
1743
+ description: 'Material Icons ligature (e.g., "search", "close", "visibility").',
1744
+ required: true
1745
+ },
1746
+ ariaLabel: {
1747
+ name: "ariaLabel",
1748
+ type: "DynamicText",
1749
+ description: "Accessible label for icons that convey meaning. Decorative icons omit this.",
1750
+ required: false
1751
+ }
1752
+ },
1753
+ example: `{ slot: 'prefix', kind: 'mat-icon', icon: 'search', ariaLabel: 'Search' }`,
1754
+ minimalExample: `{ slot: 'prefix', kind: 'mat-icon', icon: 'search' }`
1755
+ },
1756
+ {
1757
+ kind: "mat-button",
1758
+ category: "adapter",
1759
+ package: "@ng-forge/dynamic-forms-material",
1760
+ adapter: "material",
1761
+ description: "Renders `<button mat-icon-button>` (icon-only) or `<button mat-button>` (labeled) with optional color, reactive loading, and exactly one click handler (preset, actionRef, or action).",
1762
+ jsonSafe: true,
1763
+ props: {
1764
+ ...UNIVERSAL_BASE_PROPS,
1765
+ icon: { name: "icon", type: "string", description: "Material Icons ligature.", required: false },
1766
+ label: { name: "label", type: "DynamicText", description: "Visible button label.", required: false },
1767
+ ariaLabel: {
1768
+ name: "ariaLabel",
1769
+ type: "DynamicText",
1770
+ description: "REQUIRED when icon-only (no `label`). Used as `aria-label`.",
1771
+ required: false
1772
+ },
1773
+ color: {
1774
+ name: "color",
1775
+ type: "'primary' | 'accent' | 'warn'",
1776
+ description: "Material button color variant.",
1777
+ required: false
1778
+ },
1779
+ loading: {
1780
+ name: "loading",
1781
+ type: "boolean | Signal<boolean> | Observable<boolean>",
1782
+ description: "Reactive loading state \u2014 disables the button when truthy.",
1783
+ required: false
1784
+ },
1785
+ preset: {
1786
+ name: "preset",
1787
+ type: "'clear' | 'reset' | 'paste' | 'copy' | 'toggle-password-visibility' | string",
1788
+ description: "Built-in preset action. Mutually exclusive with `actionRef` and `action`.",
1789
+ required: false
1790
+ },
1791
+ actionRef: {
1792
+ name: "actionRef",
1793
+ type: "string",
1794
+ description: "Typed handle to a handler registered via `provideAddonActions(...)`. JSON-safe.",
1795
+ required: false
1796
+ },
1797
+ action: {
1798
+ name: "action",
1799
+ type: "(ctx: AddonActionContext) => void",
1800
+ description: "Inline click handler (code-only \u2014 dropped from JSON-derived configs).",
1801
+ required: false
1802
+ }
1803
+ },
1804
+ example: `{ slot: 'suffix', kind: 'mat-button', icon: 'close', ariaLabel: 'Clear', preset: 'clear' }`,
1805
+ minimalExample: `{ slot: 'suffix', kind: 'mat-button', icon: 'close', ariaLabel: 'Clear', preset: 'clear' }`
1806
+ },
1807
+ // ─── Bootstrap-shipped kinds ─────────────────────────────────────────────
1808
+ {
1809
+ kind: "bs-icon",
1810
+ category: "adapter",
1811
+ package: "@ng-forge/dynamic-forms-bootstrap",
1812
+ adapter: "bootstrap",
1813
+ description: 'Renders `<i class="bi bi-{icon}">` \u2014 Bootstrap Icons name without the `bi-` prefix.',
1814
+ jsonSafe: true,
1815
+ props: {
1816
+ ...UNIVERSAL_BASE_PROPS,
1817
+ icon: {
1818
+ name: "icon",
1819
+ type: "string",
1820
+ description: 'Bootstrap Icons name without the `bi-` prefix (e.g., "search", "x", "eye").',
1821
+ required: true
1822
+ },
1823
+ ariaLabel: {
1824
+ name: "ariaLabel",
1825
+ type: "DynamicText",
1826
+ description: "Accessible label for icons that convey meaning. Decorative icons omit this.",
1827
+ required: false
1828
+ }
1829
+ },
1830
+ example: `{ slot: 'prefix', kind: 'bs-icon', icon: 'search', ariaLabel: 'Search' }`,
1831
+ minimalExample: `{ slot: 'prefix', kind: 'bs-icon', icon: 'search' }`
1832
+ },
1833
+ {
1834
+ kind: "bs-button",
1835
+ category: "adapter",
1836
+ package: "@ng-forge/dynamic-forms-bootstrap",
1837
+ adapter: "bootstrap",
1838
+ description: 'Renders `<button class="btn btn-outline-{severity}">` with optional icon, label, severity, and reactive loading (swaps to a spinner).',
1839
+ jsonSafe: true,
1840
+ props: {
1841
+ ...UNIVERSAL_BASE_PROPS,
1842
+ icon: { name: "icon", type: "string", description: "Bootstrap Icons name without the `bi-` prefix.", required: false },
1843
+ label: { name: "label", type: "DynamicText", description: "Visible button label.", required: false },
1844
+ ariaLabel: {
1845
+ name: "ariaLabel",
1846
+ type: "DynamicText",
1847
+ description: "REQUIRED when icon-only (no `label`). Used as `aria-label`.",
1848
+ required: false
1849
+ },
1850
+ severity: {
1851
+ name: "severity",
1852
+ type: "'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark'",
1853
+ description: "Bootstrap button variant (mapped to `btn-outline-{severity}`).",
1854
+ required: false
1855
+ },
1856
+ loading: {
1857
+ name: "loading",
1858
+ type: "boolean | Signal<boolean> | Observable<boolean>",
1859
+ description: "Reactive loading state \u2014 swaps the button content for a spinner.",
1860
+ required: false
1861
+ },
1862
+ preset: {
1863
+ name: "preset",
1864
+ type: "'clear' | 'reset' | 'paste' | 'copy' | 'toggle-password-visibility' | string",
1865
+ description: "Built-in preset action. Mutually exclusive with `actionRef` and `action`.",
1866
+ required: false
1867
+ },
1868
+ actionRef: {
1869
+ name: "actionRef",
1870
+ type: "string",
1871
+ description: "Typed handle to a handler registered via `provideAddonActions(...)`. JSON-safe.",
1872
+ required: false
1873
+ },
1874
+ action: {
1875
+ name: "action",
1876
+ type: "(ctx: AddonActionContext) => void",
1877
+ description: "Inline click handler (code-only \u2014 dropped from JSON-derived configs).",
1878
+ required: false
1879
+ }
1880
+ },
1881
+ example: `{ slot: 'suffix', kind: 'bs-button', icon: 'x', ariaLabel: 'Clear', preset: 'clear' }`,
1882
+ minimalExample: `{ slot: 'suffix', kind: 'bs-button', icon: 'x', ariaLabel: 'Clear', preset: 'clear' }`
1883
+ },
1884
+ // ─── Ionic-shipped kinds ─────────────────────────────────────────────────
1885
+ {
1886
+ kind: "ion-icon",
1887
+ category: "adapter",
1888
+ package: "@ng-forge/dynamic-forms-ionic",
1889
+ adapter: "ionic",
1890
+ description: 'Renders `<ion-icon name="{icon}">` \u2014 Ionicons name.',
1891
+ jsonSafe: true,
1892
+ props: {
1893
+ ...UNIVERSAL_BASE_PROPS,
1894
+ icon: {
1895
+ name: "icon",
1896
+ type: "string",
1897
+ description: 'Ionicons name (e.g., "search-outline", "close-outline", "eye-outline").',
1898
+ required: true
1899
+ },
1900
+ ariaLabel: {
1901
+ name: "ariaLabel",
1902
+ type: "DynamicText",
1903
+ description: "Accessible label for icons that convey meaning. Decorative icons omit this.",
1904
+ required: false
1905
+ }
1906
+ },
1907
+ example: `{ slot: 'prefix', kind: 'ion-icon', icon: 'search-outline', ariaLabel: 'Search' }`,
1908
+ minimalExample: `{ slot: 'prefix', kind: 'ion-icon', icon: 'search-outline' }`
1909
+ },
1910
+ {
1911
+ kind: "ion-button",
1912
+ category: "adapter",
1913
+ package: "@ng-forge/dynamic-forms-ionic",
1914
+ adapter: "ionic",
1915
+ description: 'Renders `<ion-button>` with optional icon, label, color, fill, and reactive loading (renders an `<ion-spinner>`). `ion-button` addons render INSIDE `<ion-input>` as native `<ion-button slot="start|end">` via an attribute-selector component so Ionic\'s `::slotted(ion-button[slot=start|end])` shadow CSS sizes them natively.',
1916
+ jsonSafe: true,
1917
+ props: {
1918
+ ...UNIVERSAL_BASE_PROPS,
1919
+ icon: { name: "icon", type: "string", description: "Ionicons name.", required: false },
1920
+ label: { name: "label", type: "DynamicText", description: "Visible button label.", required: false },
1921
+ ariaLabel: {
1922
+ name: "ariaLabel",
1923
+ type: "DynamicText",
1924
+ description: "REQUIRED when icon-only (no `label`). Used as `aria-label`.",
1925
+ required: false
1926
+ },
1927
+ color: {
1928
+ name: "color",
1929
+ type: "'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger' | 'light' | 'medium' | 'dark'",
1930
+ description: "Ionic button color.",
1931
+ required: false
1932
+ },
1933
+ fill: {
1934
+ name: "fill",
1935
+ type: "'clear' | 'outline' | 'solid' | 'default'",
1936
+ description: "Ionic button fill style.",
1937
+ required: false
1938
+ },
1939
+ loading: {
1940
+ name: "loading",
1941
+ type: "boolean | Signal<boolean> | Observable<boolean>",
1942
+ description: "Reactive loading state \u2014 renders an `<ion-spinner>`.",
1943
+ required: false
1944
+ },
1945
+ preset: {
1946
+ name: "preset",
1947
+ type: "'clear' | 'reset' | 'paste' | 'copy' | 'toggle-password-visibility' | string",
1948
+ description: "Built-in preset action. Mutually exclusive with `actionRef` and `action`.",
1949
+ required: false
1950
+ },
1951
+ actionRef: {
1952
+ name: "actionRef",
1953
+ type: "string",
1954
+ description: "Typed handle to a handler registered via `provideAddonActions(...)`. JSON-safe.",
1955
+ required: false
1956
+ },
1957
+ action: {
1958
+ name: "action",
1959
+ type: "(ctx: AddonActionContext) => void",
1960
+ description: "Inline click handler (code-only \u2014 dropped from JSON-derived configs).",
1961
+ required: false
1962
+ }
1963
+ },
1964
+ example: `{ slot: 'suffix', kind: 'ion-button', icon: 'close-outline', ariaLabel: 'Clear', preset: 'clear' }`,
1965
+ minimalExample: `{ slot: 'suffix', kind: 'ion-button', icon: 'close-outline', ariaLabel: 'Clear', preset: 'clear' }`
1966
+ }
1967
+ ];
1968
+
1556
1969
  // packages/dynamic-form-mcp/src/registry/index.ts
1557
1970
  function getFieldTypes() {
1558
1971
  return FIELD_TYPES;
@@ -1596,6 +2009,12 @@ function getDocPage(id) {
1596
2009
  function getDocPagesByCategory(category) {
1597
2010
  return DOCUMENTATION.filter((d) => d.category === category);
1598
2011
  }
2012
+ function getAddonKinds() {
2013
+ return ADDON_KINDS;
2014
+ }
2015
+ function getAddonKind(kind) {
2016
+ return ADDON_KINDS.find((k) => k.kind === kind);
2017
+ }
1599
2018
 
1600
2019
  // packages/dynamic-form-mcp/src/resources/field-types.resource.ts
1601
2020
  function registerFieldTypesResource(server) {
@@ -2142,7 +2561,9 @@ const config: FormConfig = {
2142
2561
 
2143
2562
  Every field MUST have:
2144
2563
 
2145
- - \`key\`: Unique identifier (except for \`row\` type which is layout-only)
2564
+ - \`key\`: Identifier, unique within its group scope (the same leaf key may repeat inside different groups, e.g. \`createA.name\` and \`createB.name\`). Page/row/array containers do not introduce a scope. Buttons (which don't bind to form values) don't need unique keys.
2565
+ - **DOM id / data-testid composition:** keys are joined with \`_\` through group ancestors to form the rendered DOM id. A leaf \`street\` inside group \`address\` renders as \`id="address_street"\`. Inside an array item, the item index is appended after: \`id="address_street_0"\`. Use this exact form for \`for=\`, \`aria-describedby\`, \`aria-labelledby\`, or CSS selectors that target group-nested fields.
2566
+ - **Avoid \`_\` in keys when they could collide with a group-prefixed id.** A top-level key \`foo_bar\` and a leaf \`bar\` inside group \`foo\` would both render as \`id="foo_bar"\`. The validator catches this collision at boot, but it's easier to just not put \`_\` in keys that share a name with an existing group + leaf pair.
2146
2567
  - \`type\`: One of the registered field types
2147
2568
  - \`label\`: Human-readable label (for accessibility)
2148
2569
 
@@ -2299,12 +2720,13 @@ Available shorthands: \`required\`, \`email\`, \`min\`, \`max\`, \`minLength\`,
2299
2720
 
2300
2721
  ### Nullable Values
2301
2722
 
2302
- Value fields accept a \`nullable?: boolean\` flag. When \`true\`:
2723
+ Value fields and checked fields (\`checkbox\`, \`toggle\`) accept a \`nullable?: boolean\` flag. When \`true\`:
2303
2724
 
2304
2725
  - \`value\` accepts \`null\` in addition to the field's normal value type.
2305
- - An omitted \`value\` resolves to \`null\` (instead of the type-specific empty default like \`''\`, \`NaN\`, or \`[]\`).
2726
+ - An omitted \`value\` resolves to \`null\` (instead of the type-specific empty default like \`''\`, \`NaN\`, \`false\`, or \`[]\`).
2306
2727
  - Orthogonal to \`required\`. \`nullable\` declares that the **model** accepts \`null\`; \`required\` is a **validation** constraint. Angular's \`Validators.required\` treats \`null\` as invalid, so a field that is both \`nullable\` and \`required\` will fail required-validation when the value is \`null\` \u2014 the two flags describe different layers (data shape vs. validation), not conflicting semantics.
2307
2728
  - Map OpenAPI \`nullable: true\` (3.0) or \`type: [T, null]\` (3.1) to this flag.
2729
+ - Container fields (\`group\`, \`array\`, \`row\`, \`page\`) do NOT support \`nullable\` \u2014 their null semantics are ambiguous.
2308
2730
 
2309
2731
  \`\`\`typescript
2310
2732
  {
@@ -2314,6 +2736,14 @@ Value fields accept a \`nullable?: boolean\` flag. When \`true\`:
2314
2736
  nullable: true,
2315
2737
  value: null, // valid; default-resolves to null
2316
2738
  }
2739
+
2740
+ {
2741
+ key: 'active',
2742
+ type: 'checkbox',
2743
+ label: 'Active',
2744
+ nullable: true,
2745
+ value: null, // undecided checkbox state \u2014 model permits null, UI renders unchecked
2746
+ }
2317
2747
  \`\`\`
2318
2748
 
2319
2749
  Read-side caveat: a user clearing a text input reads back as \`""\`, not \`null\` \u2014 this is a DOM/Web IDL contract, identical to classic Reactive Forms. \`nullable\` is a contract for accepted values, not a guarantee of emitted ones.
@@ -2409,6 +2839,8 @@ Invokes a registered custom function by name:
2409
2839
 
2410
2840
  Register functions via \`customFnConfig.customFunctions\`.
2411
2841
 
2842
+ > **TypeScript-authored configs:** the same surface also accepts an inline \`fn: CustomFunction\` instead of \`functionName\` (XOR with \`functionName\`, code-only \u2014 not JSON-serializable). The MCP server emits \`functionName\` exclusively; consumers writing configs in TS may prefer \`fn\` when they don't need JSON round-tripping. Same caveat applies to async conditions (\`asyncFn\`), function derivations (\`fn\`), async function derivations (\`asyncFn\`), and validators (\`fn\`).
2843
+
2412
2844
  #### Async Custom Condition
2413
2845
 
2414
2846
  Invokes a registered async function that returns \`Promise<boolean>\` or \`Observable<boolean>\`:
@@ -2482,6 +2914,53 @@ Available adapters:
2482
2914
  - \`@ng-forge/dynamic-forms-primeng\` - PrimeNG
2483
2915
  - \`@ng-forge/dynamic-forms-ionic\` - Ionic
2484
2916
 
2917
+ ### Style Defaults (opt-in)
2918
+
2919
+ Each adapter ships an opinionated Sass partial with host-framework colors/typography. Library SCSS uses \`var(--df-name, structural-fallback)\` so forms render usefully without any opt-in \u2014 only opt in if you want the adapter-themed look.
2920
+
2921
+ \`\`\`scss
2922
+ @use '@ng-forge/dynamic-forms-material/styles/defaults' as df;
2923
+
2924
+ :root {
2925
+ @include df.apply-material-form-defaults;
2926
+ }
2927
+ \`\`\`
2928
+
2929
+ Substitute \`material\` for \`bootstrap\`, \`primeng\`, or \`ionic\`. Material also ships \`form-material-defaults-dark\` for dark-mode hint/error colors. Ionic ships \`apply-ionic-legacy-error-styles\` that pairs with \`withLegacyStatusClasses()\` (see below).
2930
+
2931
+ ## Provider Features
2932
+
2933
+ \`provideDynamicForm()\` accepts optional \`with*\` feature configurators alongside the adapter field bundle:
2934
+
2935
+ ### \`withLegacyStatusClasses()\` \u2014 opt into legacy ng-* CSS classes
2936
+
2937
+ By default, Angular Signal Forms uses its modern class strategy. If your CSS targets \`.ng-touched\`, \`.ng-invalid\`, \`.ng-dirty\`, etc. on form-bound elements, add this feature to wire up the compat-class strategy:
2938
+
2939
+ \`\`\`typescript
2940
+ import { provideDynamicForm, withLegacyStatusClasses } from '@ng-forge/dynamic-forms';
2941
+ import { withMaterialFields } from '@ng-forge/dynamic-forms-material';
2942
+
2943
+ export const appConfig = {
2944
+ providers: [
2945
+ provideDynamicForm(
2946
+ ...withMaterialFields(),
2947
+ withLegacyStatusClasses(),
2948
+ ),
2949
+ ],
2950
+ };
2951
+ \`\`\`
2952
+
2953
+ Recommended when consuming Ionic's \`apply-ionic-legacy-error-styles\` mixin, when using Bootstrap themes that style \`is-invalid.ng-touched\`, or any custom CSS that depends on the \`ng-*\` classes.
2954
+
2955
+ ### Other built-in features
2956
+
2957
+ - \`withLoggerConfig(options?)\` \u2014 configure or disable the dynamic-form logger
2958
+ - \`withEventFormValue()\` \u2014 attach the current form value to every dispatched event
2959
+ - \`withValueExclusionDefaults(options)\` \u2014 set form-wide value exclusion defaults
2960
+ - \`withValidationExecutionDefaults(options)\` \u2014 set form-wide validation execution defaults (e.g. defer until touched)
2961
+
2962
+ All features are exported from \`@ng-forge/dynamic-forms\`.
2963
+
2485
2964
  ## Common Patterns
2486
2965
 
2487
2966
  ### Always End with Submit Button
@@ -2516,7 +2995,7 @@ Available adapters:
2516
2995
 
2517
2996
  1. **Missing labels** - Always include labels for accessibility
2518
2997
  2. **Missing validation messages** - Users need clear feedback
2519
- 3. **Duplicate keys** - Each key must be unique within its scope
2998
+ 3. **Duplicate keys within a group scope** - Keys must be unique within the same group. Same key in different groups is fine (different scoped paths). Buttons are exempt.
2520
2999
  4. **Select without options** - Select/radio fields must have options
2521
3000
  5. **Array without fields** - Array fields need a fields template
2522
3001
  6. **Group without fields** - Group fields need child fields
@@ -9378,6 +9857,7 @@ function getTopicList() {
9378
9857
  const fieldTypes = ["input", "select", "slider", "radio", "checkbox", "textarea", "datepicker", "toggle", "text", "hidden"];
9379
9858
  const containers = ["group", "row", "array", "simplified-array", "page"];
9380
9859
  const wrappers = ["wrappers", ...getWrappers().map((w) => w.type)];
9860
+ const addons = ["addons", ...getAddonKinds().map((k) => k.kind)];
9381
9861
  const concepts = [
9382
9862
  "validation",
9383
9863
  "validation-messages",
@@ -9416,6 +9896,9 @@ ${containers.map(formatTopicItem).join("\n")}
9416
9896
  ## Wrappers
9417
9897
  ${wrappers.map(formatTopicItem).join("\n")}
9418
9898
 
9899
+ ## Addons
9900
+ ${addons.map(formatTopicItem).join("\n")}
9901
+
9419
9902
  ## Concepts
9420
9903
  ${concepts.map(formatTopicItem).join("\n")}
9421
9904
 
@@ -9584,7 +10067,120 @@ const config = {
9584
10067
  lines.push(wrappers[0]?.contract ?? "");
9585
10068
  return lines.join("\n");
9586
10069
  }
10070
+ function formatAddonInfo(addon, depth) {
10071
+ if (depth === "brief") {
10072
+ return [
10073
+ `# ${addon.kind} addon`,
10074
+ "",
10075
+ `${addon.description.split(".")[0]}.`,
10076
+ "",
10077
+ "```typescript",
10078
+ addon.minimalExample,
10079
+ "```",
10080
+ addon.jsonSafe ? "" : "**Code-only** \u2014 dropped by the lenient validator when the config came from JSON."
10081
+ ].filter(Boolean).join("\n");
10082
+ }
10083
+ const lines = [];
10084
+ lines.push(`# ${addon.kind} addon`);
10085
+ lines.push("");
10086
+ lines.push(`**Category:** ${addon.category}`);
10087
+ lines.push(`**Package:** \`${addon.package}\``);
10088
+ if (addon.adapter) lines.push(`**Adapter:** ${addon.adapter}`);
10089
+ lines.push(`**JSON-safe:** ${addon.jsonSafe ? "Yes" : "No (code-only)"}`);
10090
+ lines.push(`**Description:** ${addon.description}`);
10091
+ if (Object.keys(addon.props).length > 0) {
10092
+ lines.push("");
10093
+ lines.push("## Config Props");
10094
+ for (const prop of Object.values(addon.props)) {
10095
+ const req = prop.required ? " **(required)**" : "";
10096
+ lines.push(`- \`${prop.name}\`: ${prop.type}${req} \u2014 ${prop.description}`);
10097
+ }
10098
+ }
10099
+ lines.push("");
10100
+ lines.push("## Minimal Example");
10101
+ lines.push("```typescript");
10102
+ lines.push(addon.minimalExample);
10103
+ lines.push("```");
10104
+ lines.push("");
10105
+ lines.push("## Full Example");
10106
+ lines.push("```typescript");
10107
+ lines.push(addon.example);
10108
+ lines.push("```");
10109
+ return lines.join("\n");
10110
+ }
10111
+ function formatAddonsOverview() {
10112
+ const kinds = getAddonKinds();
10113
+ const lines = [];
10114
+ lines.push("# Addons");
10115
+ lines.push("");
10116
+ lines.push(
10117
+ "Addons render inside a field slot \u2014 icons, buttons, inline text, custom templates, or custom components. Multiple addons in the same slot render left-to-right (mirrored in RTL). `hidden` and `disabled` are reactive (signals, observables, or `FormStateCondition`)."
10118
+ );
10119
+ lines.push("");
10120
+ lines.push("## Applying an addon");
10121
+ lines.push("");
10122
+ lines.push("```typescript");
10123
+ lines.push(`{
10124
+ key: 'q',
10125
+ type: 'input',
10126
+ label: 'Search',
10127
+ addons: [
10128
+ { slot: 'prefix', kind: 'mat-icon', icon: 'search', ariaLabel: 'Search' },
10129
+ { slot: 'suffix', kind: 'mat-button', icon: 'close', ariaLabel: 'Clear', preset: 'clear' },
10130
+ ],
10131
+ }`);
10132
+ lines.push("```");
10133
+ lines.push("");
10134
+ lines.push("## Registered Kinds");
10135
+ lines.push("");
10136
+ const core = kinds.filter((k) => k.category === "core");
10137
+ const adapter = kinds.filter((k) => k.category === "adapter");
10138
+ if (core.length > 0) {
10139
+ lines.push("### Core (universal \u2014 ship from `@ng-forge/dynamic-forms`)");
10140
+ for (const k of core) {
10141
+ const json = k.jsonSafe ? "" : " _(code-only)_";
10142
+ lines.push(`- **${k.kind}**${json} \u2014 ${k.description.split(".")[0]}.`);
10143
+ }
10144
+ lines.push("");
10145
+ }
10146
+ if (adapter.length > 0) {
10147
+ lines.push("### Adapter (provided by UI integration packages)");
10148
+ for (const k of adapter) {
10149
+ lines.push(`- **${k.kind}** (${k.adapter}, \`${k.package}\`) \u2014 ${k.description.split(".")[0]}.`);
10150
+ }
10151
+ lines.push("");
10152
+ }
10153
+ lines.push('Use `ngforge_lookup topic="<kind>"` (e.g., `mat-button`, `pi-icon`) for full details on any individual addon kind.');
10154
+ lines.push("");
10155
+ lines.push("## Click variants (mutually exclusive on button-like addons)");
10156
+ lines.push("");
10157
+ lines.push("- `preset`: built-in semantics \u2014 `clear` / `reset` / `paste` / `copy` / `toggle-password-visibility`.");
10158
+ lines.push("- `actionRef`: named handler resolved via `provideAddonActions({ ... })`.");
10159
+ lines.push("- `action`: inline function (code-only, dropped by JSON validator).");
10160
+ lines.push("");
10161
+ lines.push("Setting more than one logs a warning and applies the first (precedence: `preset` > `actionRef` > `action`).");
10162
+ return lines.join("\n");
10163
+ }
9587
10164
  async function resolveTopicContent(resolvedTopic, depth, uiIntegration) {
10165
+ if (resolvedTopic === "addons" || resolvedTopic === "addon" || resolvedTopic === "addon-kinds") {
10166
+ if (depth === "brief") {
10167
+ const kinds = getAddonKinds();
10168
+ return [
10169
+ "**Addons** render inside a field slot (prefix / suffix / adapter-specific): icons, buttons, inline text, templates, components.",
10170
+ "",
10171
+ "Registered kinds:",
10172
+ ...kinds.map((k) => `- \`${k.kind}\` (${k.category}${k.adapter ? `, ${k.adapter}` : ""})${k.jsonSafe ? "" : " \u2014 code-only"}`),
10173
+ "",
10174
+ 'Use `addons: [{ slot: "suffix", kind: "<kind>", ... }]` on an addon-supporting field. See `ngforge_lookup topic="addons"` for full docs.'
10175
+ ].join("\n");
10176
+ }
10177
+ return formatAddonsOverview();
10178
+ }
10179
+ const addonInfo = getAddonKind(resolvedTopic);
10180
+ const collidesWithFieldOrWrapper = resolvedTopic === "text" || !!getWrapper(resolvedTopic);
10181
+ if (addonInfo && !collidesWithFieldOrWrapper) {
10182
+ return formatAddonInfo(addonInfo, depth);
10183
+ }
9588
10184
  if (resolvedTopic === "wrappers" || resolvedTopic === "wrapper") {
9589
10185
  if (depth === "brief") {
9590
10186
  const wrappers = getWrappers();
@@ -9735,6 +10331,8 @@ Containers: group, row, array, simplified-array, page
9735
10331
 
9736
10332
  Wrappers: wrappers (overview), css (shipping), section, arraySection (demo-only)
9737
10333
 
10334
+ Addons: addons (overview), text, template, component (core), mat-icon, mat-button, bs-icon, bs-button, pi-icon, pi-button, ion-icon, ion-button
10335
+
9738
10336
  Concepts: validation, conditional, derivation, property-derivation, options-format, expression-variables, async-validators
9739
10337
 
9740
10338
  Patterns: field-placement, logic-matrix, context-api, containers, multi-page-gotchas
@@ -13192,6 +13790,18 @@ function buildIndex() {
13192
13790
  toolCall: `ngforge_lookup topic="${wrapper.type}"`
13193
13791
  });
13194
13792
  }
13793
+ for (const addon of getAddonKinds()) {
13794
+ const subcategory = addon.adapter ? `Addon (${addon.adapter})` : `Addon (${addon.category})`;
13795
+ entries.push({
13796
+ id: addon.kind,
13797
+ category: "addon",
13798
+ subcategory,
13799
+ description: addon.description,
13800
+ aliases: [],
13801
+ content: `${addon.kind} addon ${addon.description} ${addon.package}`.toLowerCase(),
13802
+ toolCall: `ngforge_lookup topic="${addon.kind}"`
13803
+ });
13804
+ }
13195
13805
  searchIndex = entries;
13196
13806
  return entries;
13197
13807
  }