@aquera/mcp-ui-render 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +437 -0
  2. package/CONFIGURATION.md +178 -0
  3. package/LICENSE +21 -0
  4. package/README.md +237 -0
  5. package/RELEASE-NOTES.md +77 -0
  6. package/TEST-PLAN.md +72 -0
  7. package/USAGE.md +100 -0
  8. package/package.json +52 -0
  9. package/src/controls.d.ts +1 -0
  10. package/src/controls.js +572 -0
  11. package/src/controls.js.map +1 -0
  12. package/src/elements/aq-mcp-config.d.ts +289 -0
  13. package/src/elements/aq-mcp-config.js +673 -0
  14. package/src/elements/aq-mcp-config.js.map +1 -0
  15. package/src/elements/aq-mcp-field.d.ts +92 -0
  16. package/src/elements/aq-mcp-field.js +275 -0
  17. package/src/elements/aq-mcp-field.js.map +1 -0
  18. package/src/elements/aq-mcp-section.d.ts +101 -0
  19. package/src/elements/aq-mcp-section.js +261 -0
  20. package/src/elements/aq-mcp-section.js.map +1 -0
  21. package/src/engine/status.d.ts +31 -0
  22. package/src/engine/status.js +44 -0
  23. package/src/engine/status.js.map +1 -0
  24. package/src/engine/submit.d.ts +26 -0
  25. package/src/engine/submit.js +37 -0
  26. package/src/engine/submit.js.map +1 -0
  27. package/src/engine/validate.d.ts +24 -0
  28. package/src/engine/validate.js +144 -0
  29. package/src/engine/validate.js.map +1 -0
  30. package/src/engine/values.d.ts +20 -0
  31. package/src/engine/values.js +42 -0
  32. package/src/engine/values.js.map +1 -0
  33. package/src/index.d.ts +18 -0
  34. package/src/index.js +20 -0
  35. package/src/index.js.map +1 -0
  36. package/src/mcp-ui-render.css +811 -0
  37. package/src/registry.d.ts +63 -0
  38. package/src/registry.js +39 -0
  39. package/src/registry.js.map +1 -0
  40. package/src/types.d.ts +239 -0
  41. package/src/types.js +8 -0
  42. package/src/types.js.map +1 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,437 @@
1
+ # Changelog — pre-npm development history (Verdaccio)
2
+
3
+ > **This file is history, not the release record.** Everything below documents the package's
4
+ > development against the **private Verdaccio registry**, where it reached `0.5.4` published and
5
+ > `0.6.0` in-repo. None of these versions exist on public npm.
6
+ >
7
+ > **For published releases, see [`RELEASE-NOTES.md`](./RELEASE-NOTES.md)** — the public npm line
8
+ > starts at `0.0.1`, which is the same code as the internal `0.6.0` build.
9
+ >
10
+ > It is kept rather than deleted because the reasoning in these entries — why a control behaves
11
+ > the way it does, what an interim version got wrong — is the only record of those decisions, and
12
+ > that context outlives the registry it was written against.
13
+
14
+ All notable changes to `@aquera/mcp-ui-render` are documented here.
15
+ Format loosely follows [Keep a Changelog](https://keepachangelog.com/); this package
16
+ follows semver (MAJOR.MINOR.PATCH) once past 0.x.
17
+
18
+ ## [0.6.0] - 2026-08-26
19
+
20
+ ### Added
21
+
22
+ - **`sectionsLayout: 'accordion'` — a third section arrangement.** Alongside `'stacked'` (a card
23
+ per section) and `'tabs'`, sections can now render as a single-open stack of `nile-accordion`
24
+ cards. The point is the *collapsed* state: a section that isn't open still reports what's inside
25
+ it, so a multi-section form is scannable without opening every card.
26
+ - **Header.** An icon tile, the section label with a describing line under it, and a completion
27
+ status beside the chevron — **✓ Complete** or **N required left** — so a collapsed section says
28
+ both what it is and whether it still needs work. Status is plain text with a mark rather than a
29
+ filled pill: it is a readout, not a control, and colour is never the only signal.
30
+ - **The header writes no connector-describing text of its own.** `section.label` and
31
+ `section.authType` are rendered verbatim — the label as the title, the authType as the same
32
+ `nile-badge` the stacked and tabs layouts already use. Nothing is composed around them and
33
+ nothing is inferred from a key being absent. An interim version rendered
34
+ "Basic authentication · saved by the platform" under the title, which read as the connector
35
+ describing itself but wasn't: only "Basic" came from the descriptor, and "saved by the
36
+ platform" was the renderer turning a MISSING `submitTool` into a sentence. The `bootstrap`
37
+ format carries no prose field and its shape is fixed, so rather than add a key or invent copy,
38
+ the header now shows only values the descriptor supplied.
39
+ - **`sectionIcons`** (a host map keyed by section key) sets the header tile, over a built-in
40
+ default for the contract's own `authorization` and `tenantAttributes` keys and a generic
41
+ fallback. The override is host-side because the format carries no icon field — and because
42
+ iconography is the application's decision, not the connector's.
43
+ - **Dense field layout.** The open card's body is a grid of label-**above**-control cells rather
44
+ than the label-left/control-right rows the other layouts use, with each field's `help` as hint
45
+ text beneath the control — the same `renderHelp()` (and the same "View more/less" truncation)
46
+ every other layout uses, only repositioned, so one description reads identically everywhere.
47
+ An empty field shows "Not set" as its placeholder, and a boolean renders as a labelled toggle
48
+ pill. Booleans keep their declared `order` — styled differently, not reordered.
49
+ - **Field zoning.** The open card splits into three: fields with no `defaultValue` (what you must
50
+ actually decide) full width; fields that already carry a working default grouped two-up under a
51
+ "defaults you can tune" rule; and booleans last, as full-width rows carrying their own
52
+ description. This DOES reorder relative to the descriptor — the only layout that does — because
53
+ an "everything you must set" block reads better than defaults interleaved with required fields.
54
+ Order is preserved within each zone.
55
+ - **The required marker is now conditional.** `Needs a value` shows only while a required field is
56
+ still empty, instead of an asterisk on every required field forever. A permanent mark on
57
+ finished fields is noise that hides the ones that still mean something; this makes the marker a
58
+ to-do list, matching what the header's "N required left" counts.
59
+ - **A stored `writeOnce` secret reads as a state**, not a masked value: a lock and "Saved and
60
+ encrypted" rather than a row of dots, since the host holds the real secret and this element
61
+ never sees it (§7) — dots imply there is something here to reveal.
62
+ - **Dense help is never truncated.** The `View more` threshold is tuned for the stacked layout's
63
+ narrow label column; as a full-width hint line the same text already fits.
64
+ - **`accordionColumns`** (`1 | 2`, default `2`) sets how many columns the defaults group uses. It is a
65
+ ceiling, not a fixed count: at `2` the grid still collapses to one column when its CONTAINER
66
+ is too narrow (not the viewport — the same form is 1180px wide on its own page and ~400px
67
+ beside a docked panel at an identical viewport), so `1` means "never two".
68
+ - **`accordionOpenSection`** (default `null`) picks the initially-expanded section: unset, the
69
+ library opens the first section still missing a required value, else the first visible one — so
70
+ the form opens on the part that needs work. A section `key` pins it; `''` starts all collapsed.
71
+ Only re-evaluated on a descriptor change, never on a value edit, so the card being typed in
72
+ never slams shut.
73
+ - **`accordionShowFooters`** (default **`false`**, unlike the other layouts) — an accordion is a
74
+ whole-form overview, and the host normally drives one submit via `validateAll()`. Set it true
75
+ for per-section "Save `<label>`" buttons here too; `showSectionFooters: false` still wins.
76
+ - An unrecognised `sectionsLayout` value now falls back to `'stacked'` rather than rendering an
77
+ empty form.
78
+
79
+ - **`sectionStatus()`** (`engine/status.ts`, exported) — the pure function behind that attention
80
+ state, usable standalone by a host that wants the same "is this section done" summary elsewhere. It counts applicable, non-hidden, value-bearing fields
81
+ (§12/§7; `datatype: "none"` excluded — buttons and reports hold no value the user supplies) and
82
+ treats a stored write-once secret (§7) as set.
83
+
84
+ It is deliberately **not** the same question as the existing per-tab error dot: a required field
85
+ the user simply hasn't reached yet is *incomplete*, not *invalid*. `sectionStatus` reports
86
+ completeness; the error machinery keeps reporting validity, and neither replaces the other.
87
+
88
+ - **`isEmpty()`** is now exported from `engine/validate.js`. It was already the private rule behind
89
+ `isRequired`; exporting it is what lets `sectionStatus` mean exactly the same thing by "empty" —
90
+ two independent emptiness notions would let a section read "Complete" while a field inside it
91
+ still failed validation.
92
+
93
+ - **`ControlContext.dense`** — an optional, purely advisory flag telling a control its row is being
94
+ laid out label-above-control in a narrow grid cell. Every built-in renders correctly either way
95
+ and a custom control that ignores it is not broken; the built-ins use it for one thing, showing
96
+ "Not set" as the placeholder of an empty field that has no `defaultValue` to advertise instead.
97
+
98
+ ### Fixed
99
+
100
+ - **A field's help tooltip no longer collapses the section it lives in.** `nile-tooltip` emits
101
+ `nile-show`/`nile-hide` — the same event names `nile-accordion` uses — and every Nile component
102
+ emits `bubbles: true, composed: true` (`internal/nile-element.ts`). The accordion's own handlers
103
+ therefore saw a tooltip opening as "open this section" and its dismissal as "close it", so the
104
+ card slammed shut under the user's cursor. The handlers now ignore any disclosure event whose
105
+ target isn't the accordion itself, which also covers any future nested Nile component reusing
106
+ those names.
107
+
108
+ ### Notes
109
+
110
+ - `nile-accordion` is registered by `@aquera/nile-elements`' root bundle (what a host imports), but
111
+ the package ships no per-element export subpath for it — the same gap the browser suite already
112
+ documents for `nile-file-upload`. Hosts importing the whole package are unaffected.
113
+ - No change to `types.ts`: layout stays a host-side element property and never enters the
114
+ `bootstrap` descriptor, same as `'tabs'`.
115
+
116
+ ## [0.5.5] - 2026-08-08
117
+
118
+ ### Added
119
+
120
+ - **`datatype: "object"` now has a real editable control** — `validateField` has always had
121
+ proper object validation (`requiredKeys`, per-value regex, §8), but no `ui-component` actually
122
+ rendered an object usably: `input`/`textarea` both did `String(value)`, which for a real object
123
+ produces the literal text `"[object Object]"` — not editable, not even legible. `input` and
124
+ `textarea` now display an object value as pretty-printed JSON, and parse the typed text back
125
+ into a real object on input; while the JSON is mid-typing and not yet valid/complete (the
126
+ normal state for anything beyond a bare `{}`), the raw text is kept as an interim value instead
127
+ of being dropped or corrupted — `validateField`'s object branch already treats a non-object
128
+ value as an empty object rather than crashing, so this degrades to an ordinary "missing
129
+ required key(s)" validation message, never a throw, until the JSON is valid again.
130
+ `DATATYPE_DEFAULT['object']` changed from `input` to `textarea` (a single line is a poor fit
131
+ for JSON; `textarea` is also what real descriptors already pair `object` with, e.g.
132
+ `flatfile-mcp-server`'s `tenantAttributes.schemas`). Found — like the `0.5.4` defaults fix —
133
+ while validating a real `flatfile-mcp-server` bootstrap descriptor end to end; not currently
134
+ exercised by any hidden field in that descriptor, but a real gap for any connector that ever
135
+ ships a non-hidden `object` field.
136
+
137
+ ## [0.5.4] - 2026-08-08
138
+
139
+ ### Fixed
140
+
141
+ - **A dependent field's own `defaultValue` was never seeded when it became newly applicable
142
+ mid-session — only at initial descriptor load.** `<aq-mcp-config>`'s `willUpdate` only called
143
+ `seedDefaults()` when the `descriptor` property itself changed. A field gated behind
144
+ `dependencyAttribute`/`dependencyValue` (or `dependsOn`) that's inapplicable at first render —
145
+ e.g. a "Storage Type" radio that only applies once "Source Connection Type" is set to "Flat
146
+ File" — becomes applicable via a `values` change (the user picking something elsewhere), not a
147
+ `descriptor` change, so its `defaultValue` was silently never seeded: the field rendered with
148
+ nothing selected despite the descriptor declaring a default, and — worse, for a non-required
149
+ field — that default would never be submitted at all, since `collectSubmit` only submits keys
150
+ already present in `values`. Found while validating a real `flatfile-mcp-server` bootstrap
151
+ descriptor end to end. Fixed by re-running `seedDefaults` on a `values` change too, guarded by a
152
+ key-count comparison (`seedDefaults` only ever fills in a currently-`undefined` attribute, so a
153
+ key-count mismatch is a cheap, correct signal that seeding actually added something) —
154
+ reassigning `this.values` only when seeding truly changed it avoids re-triggering `willUpdate`
155
+ forever.
156
+
157
+ ## [0.5.3] - 2026-08-08
158
+
159
+ ### Fixed
160
+
161
+ - **`.aq-mcp-section`, `.aq-mcp-tabs`, and the selected permissions-report filter chip kept a
162
+ white background under the dark theme.** All three used `--ng-color-base-white`, a raw Nile
163
+ palette token that's `#FFFFFF` unconditionally in both light AND dark (it never flips) —
164
+ the theme-aware surface token is `--ng-colors-bg-primary`, which resolves to
165
+ `--ng-color-base-white` in light and `--ng-color-graydarkmode-950` in dark. Every text token in
166
+ the sheet was already correct (`--ng-colors-text-primary-900`/`-secondary-700`/`-tertiary-600`
167
+ all flip), so under dark mode section headings and help text turned near-white against a card
168
+ that stayed white — headings effectively invisible, `nile-input` controls rendering as black
169
+ boxes on a white card. `.aq-mcp-section__footer` uses `--ng-colors-bg-secondary` and DOES flip,
170
+ so the footer strip went dark inside a still-white card. Fixed by swapping all three to
171
+ `--ng-colors-bg-primary` (light mode unaffected — it resolves to the same white there).
172
+ - **The selected permissions-report filter chip's text color was never theme-aware at all.**
173
+ `--ng-colors-brand-600` isn't a real Nile token (checked every `nxtgen-*.css` shipped) — it
174
+ silently fell back to the hardcoded `#155eef` in both themes, every time. Swapped for
175
+ `--ng-componentcolors-utility-brand-600`, the real semantic token, which resolves to the exact
176
+ same `#155EEF` in light mode (zero visual change there) and to a lighter `#528BFF` in dark mode
177
+ — the old `#155eef`-on-`--ng-colors-bg-primary` combination in dark mode measured ~3.6:1
178
+ contrast, borderline-failing for text; the fixed token measures noticeably better.
179
+ **Known, left as-is:** `.aq-mcp-field__help-toggle`'s "View more"/"View less" link has the same
180
+ nonexistent `--ng-colors-brand-600` token, falling back to `#7f56d9` — Aquera's actual brand
181
+ purple, not Nile's blue. No theme-flipping purple token exists in Nile (every "brand" text
182
+ token there resolves to the blue `bluedark` family), and the literal purple already reads
183
+ ~3.9:1 against the new dark surface, so swapping it would trade a working purple for an
184
+ unintended blue rather than fix anything — left alone.
185
+
186
+ ## [0.5.2] - 2026-08-07
187
+
188
+ ### Fixed
189
+
190
+ - **`validateFile` crashed on any non-`File` value, not just the `WRITE_ONCE_UNCHANGED`
191
+ sentinel fixed in `0.5.1`.** That fix stopped the sentinel specifically from reaching
192
+ `validateFile`, but `validateFile`/`acceptMatches` still unconditionally assumed
193
+ `value` was a real `File` — any OTHER non-`File` value (stale data left over from
194
+ before a field became a `file`-upload type, or any other malformed value a host
195
+ hands back) still crashed the same way: `file.name.toLowerCase()` on a value with no
196
+ `.name`. Fixed by guarding `validateFile` itself with an `instanceof File` check —
197
+ fails safe (returns the field's validation message, i.e. "invalid") instead of
198
+ throwing, for ANY non-`File` value, not just the one sentinel string. This is the
199
+ correct chokepoint: every `file`-datatype value flows through `validateFile`
200
+ regardless of how it got there, so this closes the crash for good rather than
201
+ special-casing another known-bad value.
202
+
203
+ ## [0.5.1] - 2026-08-07
204
+
205
+ ### Fixed
206
+
207
+ - **`validateField` crashed on a stored `writeOnce` value for `file` fields, silently killing the whole submit.**
208
+ A `writeOnce` field's stored value is the `WRITE_ONCE_UNCHANGED` sentinel string (§7), never the real
209
+ value — `validateField` never checked for it before running datatype-specific checks. For a `string`
210
+ field this "worked" by accident (`String(sentinel)` against the field's own regex is harmless, if
211
+ meaningless). For a `file` field it crashed for real: the `file`-control branch reads `value.name`/
212
+ `value.size` off what it assumes is a real `File`, and a plain string has neither — `TypeError` thrown
213
+ mid-`computeErrors()`, which both `validateSection` (section Save) and `AqMcpConfig.validateAll()` call
214
+ over *every* field in the section, not just the one being edited. Net effect: any connector with a
215
+ stored file-type credential field (e.g. ADP's client certificate) was unsaveable — editing ANY other
216
+ field in that section threw before ever emitting `aq-mcp-submit`, no request ever sent, Save stuck
217
+ indefinitely. Fixed by short-circuiting `validateField` to `null` the moment `value === WRITE_ONCE_UNCHANGED`,
218
+ before any datatype-specific check runs — the same "sentinel means stored, don't touch" rule
219
+ `<aq-mcp-field>`'s own `isSetWriteOnce()` already applies on the display side.
220
+
221
+ ## [0.5.0] - 2026-08-06
222
+
223
+ ### Added
224
+
225
+ - **`radio` control** — a new built-in `ui-component`, mapped to `nile-radio-group`/
226
+ `nile-radio` (previously an unregistered `ui-component` silently fell back to the
227
+ datatype default, e.g. `input` for `string` — no radio buttons, no options shown,
228
+ no warning). Needs `options` like `select` does; an empty list logs the same
229
+ "descriptor defect (§15)" warning and renders disabled. Gets the same accessible-name
230
+ wiring as `input`/`textarea`/`password`/`select`: the field's label is passed into
231
+ Nile's own `label` property (real `aria-labelledby` association on the group's
232
+ `<fieldset>`), then visually hidden via `::part(form-control-label)` since this
233
+ library already shows the label in `<aq-mcp-field>`'s own left column.
234
+ - **`select`/`radio` accept `{ value, label }` options**, alongside the original plain
235
+ primitive form (`value === display text`) — e.g. `{ value: 'sftpStorage', label: 'SFTP
236
+ Storage' }` shows "SFTP Storage" to the user while still emitting/submitting the raw
237
+ code `sftpStorage`. Fully backward compatible: a plain string/number option renders
238
+ exactly as before.
239
+ - **`dynamicOptions` property + `aq-mcp-options-request` event (§9.6)** — a way to feed a
240
+ `select`/`radio` field's `options` from a host-executed tool call (e.g. `listFtpServers`),
241
+ since this library still performs no network I/O itself (D-4). A field with `tool` set and
242
+ no options yet emits `aq-mcp-options-request` (`{ attribute, tool, args }`) once it's
243
+ actually applicable (respects `dependencyAttribute`) and at most once per attribute ever;
244
+ the host calls the tool and sets the result into `<aq-mcp-config>.dynamicOptions` (keyed by
245
+ attribute), which takes priority over the field's own static `options`. A field pending a
246
+ fetch renders disabled WITHOUT the §15 "descriptor defect" warning — that warning is now
247
+ reserved for a field with no options and no `tool` either, i.e. genuinely no way to ever get
248
+ any.
249
+ - **`properties-table` control** — a new built-in `ui-component`, `datatype: "none"` (never
250
+ validated/submitted, same contract as `test-connection-report`). Renders a read-only
251
+ Property/Value `<table>` from a `PropertiesTableRow[]` list — for surfacing a handful of
252
+ attributes' values (typically read-only/hidden tenant attributes worth seeing but not worth
253
+ exposing as disabled form fields) without duplicating them into the editable form. Two ways
254
+ to supply the rows, same priority `select`/`radio` already have between `options` and
255
+ `dynamicOptions`: **static**, declared directly on the field as `field.rows` (for data the
256
+ connector already knows when it builds the `bootstrap` response — no host computation
257
+ needed), or **dynamic**, host-supplied via `values[attribute]` (wins over `field.rows` when
258
+ both are present). Formats `null`/`undefined` as an em dash, an array by joining with commas,
259
+ and truncates a long value with an ellipsis (the full value stays available via the cell's
260
+ `title`). Not mapped to a Nile element — Nile has no read-only data-table primitive this
261
+ simple case needs. A plain (non-array) object value is `JSON.stringify`'d rather than
262
+ `String()`'d (so it never renders the literal `[object Object]`), and property names over 24
263
+ characters truncate with an ellipsis + hover `title`, same treatment as values.
264
+ - **`Field.dependsOn?: { attribute: string; value: string | string[] }[]`** — a compound,
265
+ multi-attribute OR dependency, alongside the existing single-attribute
266
+ `dependencyAttribute`/`dependencyValue`. A field applies if `dependencyAttribute` matches OR
267
+ any `dependsOn` entry matches (all OR'd together, never AND'd) — for a field relevant when
268
+ either of two DIFFERENT attributes hits a value, which the single-attribute form can't
269
+ express. Fully backward compatible: a field using only `dependencyAttribute` behaves exactly
270
+ as before.
271
+ - **Collapsible "View more"/"View less" help text** (`<aq-mcp-field>`) — `field.help` over 100
272
+ characters now renders truncated with a toggle instead of always showing in full; help at or
273
+ under the threshold is unaffected. Purely a display convenience with local component state,
274
+ no `Field`/`Values` model change.
275
+ - **`Field.actions` — a multi-action toolbar for a control, alongside the existing single-action
276
+ `button` control.** `properties-table` now renders its own card header (title from
277
+ `field.label`, subtitle from `field.help`, and a right-aligned toolbar built from
278
+ `field.actions`) instead of relying on `<aq-mcp-field>`'s usual label-left/control-right split
279
+ — matching the live reference's "Generated Configuration" card exactly (a download icon
280
+ button + an "Edit Configuration" button, each independently wired to its own tool). An
281
+ `actions` entry with `icon` set renders as an icon-only `nile-icon-button`; without `icon`, a
282
+ labeled `nile-button`. Clicking one calls the control's `onAction` with that specific
283
+ `FieldAction`, and `aq-mcp-action`'s event detail now includes `action` (undefined for the
284
+ classic single-`button` case) so a host can tell which of several actions fired. Each action
285
+ carries its own `tool`/`args`/`confirm`, independent of the field's own (still supported)
286
+ top-level `tool`/`args`/`confirm`.
287
+ - **`properties-table` is now a `FULL_WIDTH_CONTROLS` entry** (`<aq-mcp-field>`) — it spans the
288
+ whole field row instead of sitting in the standard two-column label-left/control-right split,
289
+ since it now renders its own title/subtitle/toolbar. Purely a layout change; `field.rows` /
290
+ `values[attribute]` / empty-value behavior is unchanged, and the field's label still always
291
+ renders somewhere (now in the control's own header) even when there are no rows yet.
292
+ - **`permissions-report` control** — a new `datatype: "none"` read-only control, distinct from
293
+ `test-connection-report`: that one models a list of PASS/FAIL endpoint probes, this one models
294
+ a scope/permission GRANT table (a validation banner + a running "N Granted" count + an
295
+ All/Success filter + a Resource Type/Permission Type/Access table) — matching the live
296
+ reference's own "Credentials" tab Test Connection result exactly. The granted count is always
297
+ derived from the current `grants` array (never a separately-suppliable, possibly-inconsistent
298
+ number). The All/Success filter is pure CSS (native radio inputs + a general-sibling rule in
299
+ `mcp-ui-render.css`) — no Lit reactive state, same precedent `test-connection-report`'s
300
+ mitigation `<details>` already set. New types: `PermissionGrant`, `PermissionsValidationReport`.
301
+
302
+ ### Fixed
303
+
304
+ - **A `test-connection-report`/`permissions-report` field with no value yet still rendered its
305
+ own `<aq-mcp-field>` row** — the control itself correctly rendered nothing, but the field's
306
+ label (e.g. "Validation Report") still showed in the left column next to a blank right column,
307
+ since `<aq-mcp-section>`'s applicability filter only checked `hidden`/`dependencyAttribute`, not
308
+ whether the control had anything to show. Fixed by skipping the field entirely (no
309
+ `<aq-mcp-field>` element at all, not just an empty control body) when one of these two controls
310
+ has a `null`/`undefined` value — `properties-table` is unaffected (it always shows its own
311
+ header/toolbar by design, even with zero rows).
312
+ - **`file` fields always showed `nile-file-upload`'s hardcoded image-upload hint**
313
+ ("PNG, JPG or SVG (upto 1MB) | 1:1 ratio") regardless of the field's own `accept`/`maxSizeKB` —
314
+ those props only fed the control's drop/pick VALIDATION, never its displayed `subtitle` text,
315
+ so a non-image field (a PEM certificate, say) still told the user to drop a PNG. Fixed by
316
+ deriving the hint from `field.accept` (extension-like entries only, e.g. `.pem`/`.crt`; bare
317
+ MIME types are skipped) and `field.maxSizeKB`, e.g. `"PEM, CRT or CER (up to 64KB)"`.
318
+
319
+ ## [0.4.1] - 2026-08-05
320
+
321
+ ### Fixed
322
+
323
+ - **Rendered form controls had no accessible name at all for assistive technology.**
324
+ `<aq-mcp-field>` shows the field's label VISIBLY in its own left-column `<div>` (§12
325
+ two-column layout), but that div was never programmatically associated with the
326
+ control next to it (no `for`/`aria-labelledby`/`aria-label`) — and the `input`/
327
+ `textarea`/`password`/`select` controls never populated Nile's own `label` property
328
+ either, which is what actually drives each control's real accessible-name wiring
329
+ (`for="input"` on `nile-input`/`nile-textarea`, `aria-labelledby="label"` on
330
+ `nile-select`; each is `aria-hidden` when `label` is empty). Net effect: every
331
+ rendered field had zero accessible name for a screen reader. Fixed by passing the
332
+ field's label into Nile's `.label` property on those four controls, then visually
333
+ hiding Nile's own internal label element (`::part(form-control-label)`, the
334
+ standard clip-rect "hidden but announced" pattern — not `display:none`, which would
335
+ also drop it from the accessibility tree) so nothing appears twice on screen.
336
+ Confirmed directly against the live accessibility tree (`aria-hidden="false"`, label
337
+ text matches, zero visual footprint) — not just asserted from source reading.
338
+ - **Known, separate gap — not fixed here:** `checkbox` (`nile-slide-toggle`) has no
339
+ equivalent wiring in Nile itself; its internal `<input type="checkbox">` has no
340
+ `for`/`aria-label`/`aria-labelledby` connecting it to its own `label`/`sublabel`
341
+ properties at all (confirmed by reading its full render template). This is a gap in
342
+ Nile's own component, not something fixable from this library's side — flagged for
343
+ the Nile team, not silently worked around.
344
+
345
+ ## [0.4.0] - 2026-08-04
346
+
347
+ ### Added
348
+
349
+ - **`sectionsLayout: 'stacked' | 'tabs'`** (`<aq-mcp-config>`, default `'stacked'`) — an
350
+ alternative to the default one-card-per-section layout. `'tabs'` renders one
351
+ [`nile-nav-tab`](https://nile.aqueralabs.com/2.0.1/nav-tab) per section, with the
352
+ section's own card as that tab's panel (rendered `compact`: no border/title, since the
353
+ tab label already shows it). Validation, submit, and footers behave identically in
354
+ either layout — only the arrangement changes.
355
+ - **`tabsPlacement: 'top' | 'bottom' | 'start' | 'end'`** (`<aq-mcp-config>`, default
356
+ `'top'`) — tab orientation, only meaningful in `sectionsLayout: 'tabs'`. `'start'`/`'end'`
357
+ give a fixed-width vertical tab list (label truncates with an ellipsis + native `title`
358
+ tooltip past 170px, using `nile-nav-tab-group`'s own `width` attribute to prevent layout
359
+ shift between tabs, per its documented purpose).
360
+ - **`tabsShowErrorIndicator`** (`<aq-mcp-config>`, default `true`) — a small red dot next to
361
+ a tab's label once that section has an actual recorded validation error (present in the
362
+ form's live `errors` state) — never on a pristine, untouched required field. Rendered via
363
+ `nile-nav-tab`'s documented `suffix` slot (not the default label slot, which gets
364
+ squeezed to zero width in vertical placement). Set `false` to opt out.
365
+ - **`compact`** (`<aq-mcp-section>`, default `false`) — drops a section's own card
366
+ border/title when it's already framed by something else (set automatically by
367
+ `<aq-mcp-config>` in tabs mode; available for a host embedding `<aq-mcp-section>`
368
+ standalone inside its own tabbed/framed UI).
369
+ - New `mcp-render-demo` example: a "Sections: Stacked / Tabs (horizontal) / Tabs
370
+ (vertical)" cycling control wired to `sectionsLayout`/`tabsPlacement`.
371
+ - **`CONFIGURATION.md`** — a dedicated reference documenting every property, method, and
372
+ event on `<aq-mcp-config>` / `<aq-mcp-section>` / `<aq-mcp-field>`, plus the
373
+ `registerControl`/`registerSectionFooter` extensibility registries, linked from
374
+ `README.md`.
375
+
376
+ ## [0.3.1] - 2026-08-04
377
+
378
+ ### Fixed
379
+
380
+ - **`writeOnce` password fields (e.g. `datatype: "password"` with `writeOnce: true`) discarded
381
+ keystrokes and flipped back to the masked "••••••• Replace" view after the very first
382
+ character typed into a blank field.** Two compounding bugs:
383
+ - The `password` control (`controls.ts`) hardcoded `<nile-input>`'s `.value` to `''` on
384
+ every render instead of binding the actual current `value` — so every re-render
385
+ (triggered by each keystroke) forcibly reset the input back to empty.
386
+ - `AqMcpField.isSetWriteOnce()` treated **any non-empty string** as "a value is already
387
+ stored" (`typeof this.value === 'string' && this.value !== ''`), rather than checking
388
+ specifically for the `WRITE_ONCE_UNCHANGED` sentinel. So the moment a user typed even one
389
+ character into a genuinely blank field, that keystroke itself satisfied "non-empty
390
+ string" and flipped the field to the masked/Replace view mid-type, discarding everything
391
+ typed after the first character.
392
+ - Both fixed: the control now binds `.value` like every other control, and
393
+ `isSetWriteOnce()` now checks `this.value === WRITE_ONCE_UNCHANGED` specifically. The
394
+ genuine "already-stored value → click Replace → type a new one" flow is unaffected and
395
+ still masks correctly.
396
+
397
+ ## [0.3.0] - 2026-08-04
398
+
399
+ ### Added
400
+
401
+ - **`showFooter` / `showSectionFooters`** boolean properties (`<aq-mcp-section>` /
402
+ `<aq-mcp-config>`, default `true`) — a master switch that hides a section's footer
403
+ entirely, whether it's the library's default "Save `<label>`" button **or** a
404
+ `registerSectionFooter` override. Lets a host that drives submission itself (its own
405
+ external button) turn footers off with one property instead of registering an empty
406
+ footer for every section key.
407
+ - **`AqMcpConfig.validateAll(): { valid: boolean; values: Values }`** — a public method a
408
+ host can call imperatively (e.g. via an Angular template ref) to validate every
409
+ section's applicable fields at once and get the full form state back. Pushes results
410
+ into the element's own `errors` state — the same mechanism a section's own Save button
411
+ uses via `validateSection` — so a host-driven "submit everything" button gets real
412
+ validation and inline field-error display, not just raw `values`.
413
+ - **Live on-change validation** — editing a field now re-validates its actual new value
414
+ immediately (`onFieldInput`), instead of just optimistically clearing its error and
415
+ waiting for the next submit. An error appears the instant a field goes invalid and
416
+ clears the instant it's genuinely fixed.
417
+
418
+ ### Changed
419
+
420
+ - Internally refactored the per-field validation loop into a shared `computeErrors()`
421
+ helper, used by both `validateSection` (one section) and `validateAll` (every section),
422
+ so both paths compute errors identically.
423
+
424
+ ## [0.2.0] - 2026-07-31
425
+
426
+ Published externally with no corresponding source changes in this repo — identical
427
+ content to 0.1.0. Recorded here only so the version history has no unexplained gap; see
428
+ 0.1.0 below for what it actually contains.
429
+
430
+ ## [0.1.0] - 2026-07-29
431
+
432
+ ### Added
433
+
434
+ - Initial scaffold: framework-agnostic Lit 3 web components (`<aq-mcp-config>`,
435
+ `<aq-mcp-section>`, `<aq-mcp-field>`) rendering a `bootstrap` tenant-configuration
436
+ descriptor via Nile, with the `aq-mcp-change` / `aq-mcp-submit` / `aq-mcp-action` event
437
+ contract and the `registerControl` / `registerSectionFooter` extensibility registry.
@@ -0,0 +1,178 @@
1
+ # Configuration Reference
2
+
3
+ Every property, method, event, and extensibility hook the three custom elements
4
+ (`<aq-mcp-config>`, `<aq-mcp-section>`, `<aq-mcp-field>`) expose. See [`USAGE.md`](./USAGE.md)
5
+ for framework integration examples and [`README.md`](./README.md) for the public API export
6
+ table (`index.ts`).
7
+
8
+ All properties are plain Lit reactive properties set via JS (`el.propName = value`) or, in
9
+ Angular/JSX, via property binding (`[propName]="value"`) — not HTML attributes, except where
10
+ noted (`sectionsLayout`, `tabsPlacement`, `showSectionFooters`, `tabsShowErrorIndicator`,
11
+ `showFooter`, `compact` reflect as attributes too since they're primitives).
12
+
13
+ ## `<aq-mcp-config>` — the root element
14
+
15
+ This is the element a host actually mounts. It owns the descriptor + values + validation state,
16
+ renders every section, and is the only element that emits the public event contract.
17
+
18
+ ### Properties
19
+
20
+ | Property | Type | Default | Description |
21
+ |---|---|---|---|
22
+ | `descriptor` | `Descriptor \| null` | `null` | The `bootstrap` tool's response — required to render anything. |
23
+ | `values` | `Values` (`Record<string, unknown>`) | `{}` | Current form state, keyed by `attribute`. One-way in: the host passes it down; edits flow back out via the `aq-mcp-change` event's `detail.values`, not by mutating this property in place. |
24
+ | `showSectionFooters` | `boolean` | `true` | Master switch for every section's footer — the default "Save `<label>`" button, **and** any `registerSectionFooter` override. `false` hides every footer at once, e.g. when the host drives submission itself via its own button(s). |
25
+ | `sectionsLayout` | `'stacked' \| 'tabs' \| 'accordion'` | `'stacked'` | How sections are arranged. `'stacked'` is every section as its own card, one after another (unchanged legacy behavior). `'tabs'` renders one `nile-nav-tab` per section, with the section's own card as that tab's panel. `'accordion'` renders one `nile-accordion` per section — single-open, with a label-above-control body laid out in one or two columns (`accordionColumns`) and each field's `help` shown as hint text beneath it; a card whose section still has a required field empty tints its edge, so a collapsed section that needs work is findable. Validation and submit behave identically in every layout; footers default to hidden in `'accordion'` (see `accordionShowFooters`). An unrecognised value falls back to `'stacked'`. |
26
+ | `tabsPlacement` | `'top' \| 'bottom' \| 'start' \| 'end'` | `'top'` | Tab orientation, only meaningful when `sectionsLayout === 'tabs'`. Passed straight through to `nile-nav-tab-group`'s own `placement`. `'start'` gives a left-hand vertical tab list; `'end'` a right-hand one. |
27
+ | `tabsShowErrorIndicator` | `boolean` | `true` | Tabs mode only. Shows a small red dot next to a tab's label once that section has an **actual recorded** validation error (i.e. present in the internal `errors` state) — not merely a required field the user hasn't reached yet. An error lands there only after a live edit is validated or the section/whole form has been submitted at least once. `false` opts out entirely. |
28
+ | `accordionOpenSection` | `string \| null` | `null` | Accordion mode only. Which section starts expanded. Left `null`, the library picks the first section with a required-but-empty field, else the first visible section — so the form opens on the part that still needs work. A section `key` pins that section; `''` starts every section collapsed. Only re-evaluated when the descriptor (or this property) changes, never on a value edit — otherwise the card being typed in would slam shut. |
29
+ | `accordionShowFooters` | `boolean` | `false` | Accordion mode only. Shows the open section's "Save `<label>`" footer. Defaults to **false**, unlike the other layouts: an accordion is a whole-form overview, and the host normally drives one submit for all of it via `validateAll()`. `showSectionFooters: false` still overrides this to off. |
30
+ | `sectionIcons` | `Record<string, string>` | `{}` | Accordion mode only. Header icon per `section.key` → `nile-glyph` name. Host map first, then a built-in default for `authorization`/`tenantAttributes`, then a generic glyph. Host-side only — the `bootstrap` format carries no icon field. |
31
+ | `accordionColumns` | `1 \| 2` | `2` | Accordion mode only. How many columns the "defaults you can tune" group uses. A **ceiling, not a fixed count**: at `2` the grid still collapses to one column when the container is too narrow to give each cell a usable width, so `1` means "never two", not "always exactly one". |
32
+ | `dynamicOptions` | `Record<string, unknown[]>` | `{}` | Host-fed options for `select`/`radio` fields resolved dynamically (§9.6) — keyed by `field.attribute`, takes priority over that field's own static `options` when both are present. The library asks for a list via `aq-mcp-options-request`; the host calls the named tool and sets the result back in here (a NEW object each time — `{ ...cfg.dynamicOptions, [attr]: list }` — not a mutated one, so the property-changed check fires). See "Common recipes" below for the full round-trip. |
33
+
34
+ ### Methods
35
+
36
+ | Method | Signature | Description |
37
+ |---|---|---|
38
+ | `validateAll()` | `(): { valid: boolean; values: Values }` | Validates every section's applicable fields at once and pushes the results into the element's own internal `errors` state — the same mechanism a section's own Save button uses (`validateSection`), just across every section. Call it imperatively on the element (e.g. an Angular template ref: `<aq-mcp-config #cfg>` then `cfg.validateAll()`) when the host drives its own "submit everything" button instead of using per-section footers — this is what makes that external button still get real validation + inline field-error display, not just the raw `values`. There is no event for this: the host decides what "submit everything" even means for its own data shape, so it must call the method directly. |
39
+
40
+ ### Events
41
+
42
+ All three bubble and are composed (`bubbles: true, composed: true`), so a host listening on any
43
+ ancestor element (or via `addEventListener` on `document`) still receives them.
44
+
45
+ | Event | `detail` shape | Fires when |
46
+ |---|---|---|
47
+ | `aq-mcp-change` | `{ attribute: string; value: unknown; values: Values }` | Any field is edited. `values` is the **entire current form state** across every section (not scoped to the edited field's section) — this is the only way the host's own copy of `values` stays in sync, since `[values]` is a one-way-in property; write `detail.values` back into whatever the host passed in as `values` if it needs to read the form state later (e.g. from a host-driven submit button). |
48
+ | `aq-mcp-submit` | `{ sectionKey: string; submitTool: string \| null; values: Values }` (`SubmitPayload`) | A section's Save action succeeds validation. `values` here is scoped to **that one section only**, and pre-processed: hidden/`datatype:"none"` fields dropped, inapplicable fields (failed a `dependencyAttribute`/`dependsOn` check, §9.7) dropped, numbers coerced, and a blank/unchanged `writeOnce`/secret field omitted entirely (so "keep current value" doesn't overwrite anything). `submitTool` is the connector tool to call, or `null` meaning the platform default (`applications_update`). Never fires if validation fails — nothing is emitted, the section's fields just show inline errors. |
49
+ | `aq-mcp-action` | `{ tool: string; args: Record<string, unknown>; confirm?: string; field: Field; action?: FieldAction }` | A `datatype: "none"` button field is invoked, OR one entry in a control's own `field.actions` toolbar is clicked (§5, e.g. `properties-table`'s "Download"/"Edit Configuration") — after the user confirms, if a `confirm` message is declared (on the action itself, or on the field, checked in that order). `tool`/`args`/`confirm` resolve from the clicked `action` when present, else from the field's own top-level `tool`/`args`/`confirm`. `action` is the specific `FieldAction` clicked, undefined for the classic single-`button` case. No `values` on this event at all. |
50
+ | `aq-mcp-options-request` | `{ attribute: string; tool: string; args: Record<string, unknown> }` | A `select`/`radio` field declares `tool` and currently has no options (§9.6) — neither a static `field.options` nor an existing `dynamicOptions` entry — AND currently applies (passes its `dependencyAttribute`/`dependsOn` check, if any, §9.7; a field gated behind another field's value is never requested before that value makes it applicable). Fires **at most once per `attribute`** for this element's lifetime; feed the result back via the `dynamicOptions` property. |
51
+
52
+ The library performs no network I/O and holds no secrets: it renders, validates, and emits
53
+ intent; the host is responsible for actually calling the connector/platform tool.
54
+
55
+ ## `<aq-mcp-section>` — one section's card
56
+
57
+ Normally only used internally by `<aq-mcp-config>` (one instance per section), but it's a
58
+ public, independently-usable custom element if a host ever wants to render a single section on
59
+ its own outside the full form.
60
+
61
+ ### Properties
62
+
63
+ | Property | Type | Default | Description |
64
+ |---|---|---|---|
65
+ | `section` | `Section` | *(required)* | The section to render — one entry from `descriptor.sections`. |
66
+ | `values` | `Values` | `{}` | Current form state (the whole form's, not just this section's — fields reference other sections' values via `dependencyAttribute`). |
67
+ | `errors` | `Record<string, string \| null>` | `{}` | Per-field validation messages, keyed by `attribute`. |
68
+ | `showFooter` | `boolean` | `true` | Master switch for **this** section's footer — the default "Save `<label>`" button, AND any `registerSectionFooter` override for this section's key. `false` hides the footer entirely regardless of what's registered. |
69
+ | `compact` | `boolean` | `false` | Set automatically by `<aq-mcp-config>` when this section is rendered as a tab panel (`sectionsLayout: 'tabs'`) — drops the card's own border and title line (the tab nav already shows the label), but keeps the `authType` badge and save-target caption. Set it yourself only if embedding `<aq-mcp-section>` standalone inside some other tabbed/framed UI. |
70
+ | `dynamicOptions` | `Record<string, unknown[]>` | `{}` | Passed straight through to every `<aq-mcp-field>` this section renders — see `<aq-mcp-config>`'s own `dynamicOptions` for the full contract. **Note:** `aq-mcp-options-request` is only ever emitted by `<aq-mcp-config>` (it owns the "which field needs options" scan); `<aq-mcp-section>` used standalone never fires it, so a host embedding it without `<aq-mcp-config>` must resolve any `tool`-backed field's options itself before setting this property — same "more on the host" trade-off already true for standalone validation/submit below. |
71
+
72
+ Emits the internal `section-submit` event (`{ detail: { sectionKey } }`) when its footer's Save
73
+ button is clicked — this is consumed by `<aq-mcp-config>`, not part of the public host contract;
74
+ a host embedding `<aq-mcp-section>` standalone needs to listen for it directly and call its own
75
+ validation/submit logic (there's no `<aq-mcp-config>` there to do it automatically).
76
+
77
+ ## `<aq-mcp-field>` — one field's row
78
+
79
+ Independent of `<aq-mcp-config>`/`<aq-mcp-section>` entirely — the simplest way to render a
80
+ single control (e.g. dropping a `test-connection-report` result into an arbitrary div; see
81
+ `README.md`'s "Rendering a single `ui-component`" section for a worked example).
82
+
83
+ ### Properties
84
+
85
+ | Property | Type | Default | Description |
86
+ |---|---|---|---|
87
+ | `field` | `Field` | *(required)* | The field definition to render (`attribute`, `datatype`, `ui-component`, `isRequired`, `validation`, etc. — see the descriptor contract). |
88
+ | `value` | `unknown` | `undefined` | The field's current value. |
89
+ | `error` | `string \| null` | `null` | Validation message to show, or `null` when valid. |
90
+ | `dynamicOptions` | `Record<string, unknown[]>` | `{}` | Forwarded to the `select`/`radio` control renderer as `ControlContext.dynamicOptions` (§9.6) — see `<aq-mcp-config>`'s own `dynamicOptions` for the full contract. `<aq-mcp-field>` never emits `aq-mcp-options-request` itself (that scan lives in `<aq-mcp-config>`); a standalone `<aq-mcp-field>` for a `tool`-backed field needs this set directly by the host up front. |
91
+
92
+ A `writeOnce` field (e.g. a password) whose `value` is exactly the `WRITE_ONCE_UNCHANGED`
93
+ sentinel (exported from `index.ts`) renders masked + a "Replace" link instead of an input, until
94
+ Replace is clicked — this is a stored-value indicator, not a truthy-string check, so typing into
95
+ an otherwise-empty `writeOnce` field never gets misread as "already set."
96
+
97
+ A `file` field's `accept`/`maxSizeKB` validation is **client-side only, advisory** — this library
98
+ has no network I/O (D-4), so it can't be the real gate. A connector must still enforce file
99
+ type/size server-side; treat the in-browser check as a fast first pass for the user, not a
100
+ security boundary.
101
+
102
+ Emits the internal `field-input` (`{ detail: { attribute, value } }`) and `field-action`
103
+ (`{ detail: { field } }`) events — again consumed by `<aq-mcp-config>`/`<aq-mcp-section>`, not
104
+ part of the public contract for a standalone embed.
105
+
106
+ ## Extensibility registries (`index.ts` exports)
107
+
108
+ Two global, side-effecting registries — call these once at app bootstrap, before the relevant
109
+ `<aq-mcp-*>` element first renders.
110
+
111
+ | Function | Signature | Description |
112
+ |---|---|---|
113
+ | `registerControl` | `(name: string, renderer: ControlRenderer) => void` | Register or override the Lit renderer for a `ui-component` string. An unknown `ui-component` always falls back to the field's datatype default (`registry.ts`'s `defaultControlFor`) — a field is never silently dropped. |
114
+ | `resolveControl` | `(uiComponent: string) => ControlRenderer \| undefined` | Look up a registered control renderer directly (used internally by `<aq-mcp-field>`; also usable by a host bypassing `<aq-mcp-field>` entirely — see README). |
115
+ | `registerSectionFooter` | `(key: string, renderer: SectionFooterRenderer) => void` | Register or override the footer for a section, keyed by `section.key`. Replaces the library's default "Save `<label>`" button entirely for that key — the renderer owns the markup and must call `ctx.onSubmit()` itself to actually trigger validation + `aq-mcp-submit`. This override always takes precedence over `showSectionFooters`/`showFooter` being `true`, but **not** over them being `false` — the boolean is a master kill-switch checked first (see `AqMcpSection.renderFooter`). |
116
+ | `resolveSectionFooter` | `(key: string) => SectionFooterRenderer \| undefined` | Look up a registered footer renderer directly. |
117
+
118
+ `ControlContext` (what a control renderer receives): `{ field, value, error, onInput, onAction?, dynamicOptions? }`.
119
+ `SectionFooterContext` (what a footer renderer receives): `{ section, values, onSubmit }`.
120
+
121
+ ## Common recipes
122
+
123
+ **Hide every section's default footer, drive submission from the host's own button:**
124
+ ```ts
125
+ cfg.showSectionFooters = false;
126
+ // ...
127
+ const { valid, values } = cfg.validateAll(); // real validation, not just raw values
128
+ ```
129
+
130
+ **Tabs, vertical, with the error dot:**
131
+ ```html
132
+ <aq-mcp-config
133
+ [descriptor]="descriptor" [values]="values"
134
+ sectionsLayout="tabs" tabsPlacement="start"
135
+ ></aq-mcp-config>
136
+ ```
137
+
138
+ **Feed a `select`/`radio` field's options from a tool call (§9.6):**
139
+ ```ts
140
+ cfg.addEventListener('aq-mcp-options-request', async (e) => {
141
+ const { attribute, tool, args } = e.detail; // e.g. tool: 'listFtpServers'
142
+ const options = await host.callTool(tool, args); // string[] or {value,label}[]
143
+ cfg.dynamicOptions = { ...cfg.dynamicOptions, [attribute]: options };
144
+ });
145
+ ```
146
+
147
+ **Replace one section's footer, keep the rest default:**
148
+ ```ts
149
+ import { registerSectionFooter } from '@aquera/mcp-ui-render';
150
+ import { html } from 'lit';
151
+
152
+ registerSectionFooter('authorization', ({ onSubmit }) => html`
153
+ <div class="aq-mcp-section__footer">
154
+ <nile-button variant="primary" @click=${onSubmit}>Save & encrypt credentials</nile-button>
155
+ </div>
156
+ `);
157
+ ```
158
+
159
+ **Customize `properties-table`'s action toolbar (`Field.actions`) beyond the built-in
160
+ icon/labeled-button default** — there's no separate action-renderer hook; `registerControl`
161
+ overrides the whole control, toolbar included, same as customizing any other built-in:
162
+ ```ts
163
+ import { registerControl } from '@aquera/mcp-ui-render';
164
+ import { html } from 'lit';
165
+
166
+ registerControl('properties-table', ({ field, value, onAction }) => {
167
+ const rows = value ?? field.rows ?? [];
168
+ return html`
169
+ <div class="my-config-card">
170
+ <h3>${field.label}</h3>
171
+ <nile-dropdown>
172
+ ${(field.actions ?? []).map((a) => html`<nile-menu-item @click=${() => onAction?.(a)}>${a.label}</nile-menu-item>`)}
173
+ </nile-dropdown>
174
+ <!-- ... your own row rendering ... -->
175
+ </div>
176
+ `;
177
+ });
178
+ ```