@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
@@ -0,0 +1,673 @@
1
+ var AqMcpConfig_1;
2
+ import { __decorate, __metadata } from "tslib";
3
+ /**
4
+ * `<aq-mcp-config>` — root element. Owns the descriptor + values + validation
5
+ * state, renders ordered sections (§12), and emits intent for the host:
6
+ * - `aq-mcp-change` — on any field edit `{ attribute, value, values }`
7
+ * - `aq-mcp-submit` — per-section save `{ sectionKey, submitTool, values }` (§13/§14)
8
+ * - `aq-mcp-action` — button/action `{ tool, args, confirm, field, action }` (§5).
9
+ * `action` is the specific `FieldAction` clicked for a multi-action control's
10
+ * own toolbar (e.g. `properties-table`'s "Download"/"Edit Configuration"),
11
+ * undefined for the classic single-action `button` control.
12
+ * - `aq-mcp-options-request` — a `select`/`radio` field with `tool` set has no
13
+ * options yet `{ attribute, tool, args }` (§9.6); host resolves + feeds them
14
+ * back via the `dynamicOptions` property (see below), one request per attribute.
15
+ * It performs NO network I/O and holds no secrets (D-4): it renders, validates,
16
+ * and emits; the host executes tools. A host driving its OWN "submit
17
+ * everything" button (bypassing per-section footers) can call the public
18
+ * `validateAll()` method directly on the element to get the library's real
19
+ * validation + inline field-error display instead of just the raw `values`.
20
+ * Every field is also validated live on its own edits (`onFieldInput`), not
21
+ * just on submit — its error updates the instant its value changes.
22
+ * `sectionsLayout` ('stacked' default, or 'tabs' with `tabsPlacement` for
23
+ * orientation) controls how sections are arranged — everything else
24
+ * (validation, submit, footers) behaves identically in either layout.
25
+ * `tabsShowErrorIndicator` (default true, tabs mode only) puts a red dot on a
26
+ * tab whose section has a required-but-empty or invalid field, computed live
27
+ * — not just after a failed submit.
28
+ *
29
+ * Renders into LIGHT DOM (`createRenderRoot()` returns `this`, no shadow
30
+ * root) on all three elements in this package, deliberately: it lets Nile's
31
+ * CSS custom-property tokens (`--ng-*`/`--nile-*`) and `mcp-ui-render.css`
32
+ * cascade in from the host's own stylesheet the same way any other page
33
+ * content does, with no per-shadow-root style injection needed. The
34
+ * trade-off is zero style encapsulation — a host's global CSS can affect
35
+ * `.aq-mcp-*` classes and vice versa — and it means `mcp-ui-render.css` MUST
36
+ * be loaded globally by the host; without it these elements render
37
+ * structurally correct but entirely unstyled, with no error or warning
38
+ * (see USAGE.md "Styling & light DOM").
39
+ */
40
+ import { LitElement, html } from 'lit';
41
+ import { customElement, property, state } from 'lit/decorators.js';
42
+ import { fieldApplies, seedDefaults } from '../engine/values.js';
43
+ import { validateField } from '../engine/validate.js';
44
+ import { collectSubmit } from '../engine/submit.js';
45
+ import { sectionStatus } from '../engine/status.js';
46
+ import './aq-mcp-section.js';
47
+ /**
48
+ * Header glyphs for the section keys the bootstrap contract itself defines
49
+ * (§2): `authorization` is who the connector signs in AS, `tenantAttributes`
50
+ * is where it reads and writes. Any other key falls through to
51
+ * `FALLBACK_SECTION_ICON`, so an unknown section is never iconless, and a
52
+ * host can override any of it via `sectionIcons`.
53
+ */
54
+ const DEFAULT_SECTION_ICONS = {
55
+ authorization: 'ng-user-01',
56
+ tenantAttributes: 'ng-server',
57
+ };
58
+ const FALLBACK_SECTION_ICON = 'ng-settings-01';
59
+ /** Highest descriptor contract MAJOR this build understands (§9.5). */
60
+ const SUPPORTED_MAJOR = 1;
61
+ /** Parse the MAJOR from a `MAJOR.MINOR` version string; NaN when malformed. */
62
+ function majorOf(version) {
63
+ return Number.parseInt(String(version ?? '').split('.')[0], 10);
64
+ }
65
+ let AqMcpConfig = AqMcpConfig_1 = class AqMcpConfig extends LitElement {
66
+ constructor() {
67
+ super(...arguments);
68
+ /** The `bootstrap` response to render. */
69
+ this.descriptor = null;
70
+ /** Current values keyed by `attribute`. */
71
+ this.values = {};
72
+ /**
73
+ * Host-fed options for `select`/`radio` fields whose list is resolved dynamically
74
+ * (`field.tool` set, e.g. `listFtpServers`) rather than declared statically as
75
+ * `field.options` (§9.6) — keyed by `field.attribute`. This library performs no
76
+ * network I/O itself (D-4): it only ASKS, via `aq-mcp-options-request`; the host
77
+ * calls the tool and sets the result back in here (immutably — a new object, not
78
+ * a mutated one, so Lit's property-changed check fires):
79
+ *
80
+ * el.addEventListener('aq-mcp-options-request', async (e) => {
81
+ * const { attribute, tool, args } = e.detail;
82
+ * const options = await host.callTool(tool, args); // e.g. FTP server list
83
+ * el.dynamicOptions = { ...el.dynamicOptions, [attribute]: options };
84
+ * });
85
+ *
86
+ * Takes priority over `field.options` when both are present for the same
87
+ * attribute. A field is requested at most once per attribute for this
88
+ * element's lifetime — a host that needs to force a refresh re-triggers it
89
+ * itself (out of scope here, same "host decides" split as `validateAll()`).
90
+ */
91
+ this.dynamicOptions = {};
92
+ /** Master switch for every section's footer (§13/§14) — the default "Save
93
+ * <label>" button, AND any `registerSectionFooter` override. Default true;
94
+ * set false to hide every section's footer at once, e.g. when the host
95
+ * drives submission itself via its own button(s). */
96
+ this.showSectionFooters = true;
97
+ /** How sections are laid out. `'stacked'` (default) is today's behavior —
98
+ * every section as its own card, one after another. `'tabs'` renders one
99
+ * `nile-nav-tab` per section (nav) with the section's own card as that
100
+ * tab's panel — orientation controlled by `tabsPlacement`. `'accordion'`
101
+ * renders one `nile-accordion` per section, single-open, with a completion
102
+ * summary in each collapsed header (field count + status chip) and a
103
+ * two-column, label-above-control body — see `renderAccordion`. */
104
+ this.sectionsLayout = 'stacked';
105
+ /** Tab orientation when `sectionsLayout === 'tabs'` — passed straight
106
+ * through to `nile-nav-tab-group`'s own `placement`. Ignored in
107
+ * `'stacked'` mode. `'start'` gives a left-hand vertical tab list. */
108
+ this.tabsPlacement = 'top';
109
+ /** In `'tabs'` mode, show a small red dot next to a tab's label when that
110
+ * section has a required-but-empty field or a live validation error — lets
111
+ * the user spot which tab needs attention without opening it. Default
112
+ * true (this is a pure addition with no prior equivalent, so there's
113
+ * nothing to stay backward-compatible with); set false to opt out.
114
+ * Ignored in `'stacked'` mode, where inline field errors are already
115
+ * visible without needing a tab-level summary. */
116
+ this.tabsShowErrorIndicator = true;
117
+ /** Which section starts expanded in `'accordion'` mode. Left unset
118
+ * (`null`), the library picks: the first section with a required-but-empty
119
+ * field, else the first visible section — so a form always opens on the
120
+ * part that still needs work. A section `key` pins that section instead;
121
+ * the empty string starts every section collapsed. Only consulted when the
122
+ * descriptor (or this property) changes — never re-evaluated on a value
123
+ * edit, which would slam the card the user is typing in shut. Ignored in
124
+ * the other layouts. */
125
+ this.accordionOpenSection = null;
126
+ /** Show the open section's save footer in `'accordion'` mode. Default
127
+ * FALSE, unlike the other two layouts: an accordion is a whole-form
128
+ * overview, and the host normally drives one submit for all of it via
129
+ * `validateAll()`. Set true to get the per-section "Save <label>" button
130
+ * here too; `showSectionFooters` still overrides both to off. Ignored in
131
+ * the other layouts. */
132
+ this.accordionShowFooters = false;
133
+ /** How many columns the open section's field grid uses in `'accordion'`
134
+ * mode. `2` (default) is the side-by-side layout; `1` stacks every field
135
+ * full width, which suits a narrow container, a long-label descriptor, or
136
+ * simply a host that prefers a single reading column.
137
+ *
138
+ * This is a CEILING, not a fixed count: at `2` the grid still collapses to
139
+ * one column when the container is too narrow to give each cell a usable
140
+ * width (see `.aq-mcp-section__body--dense`), so `1` means "never two",
141
+ * not "always exactly one regardless of space". Ignored in the other
142
+ * layouts, whose rows are single-column by construction. */
143
+ this.accordionColumns = 2;
144
+ /** Host override for section header icons in `'accordion'` mode, keyed by
145
+ * `section.key` → `nile-glyph` name. Highest priority in a three-step
146
+ * fallback: this map, then the descriptor's own `section.icon`, then
147
+ * `DEFAULT_SECTION_ICONS` below. The host wins because it knows the
148
+ * product's iconography; the descriptor only knows the connector. */
149
+ this.sectionIcons = {};
150
+ this.errors = {};
151
+ /** The single expanded section's `key` in `'accordion'` mode, or null when
152
+ * all are collapsed. Holding ONE key is what makes the accordion
153
+ * single-open: setting it re-renders every sibling with `open=false`. */
154
+ this.openKey = null;
155
+ this.pendingAction = null;
156
+ /** Attributes already asked about via `aq-mcp-options-request` — a plain dedupe
157
+ * guard, not rendered state, so mutated in place rather than reassigned (no
158
+ * need to trigger a Lit update for it). */
159
+ this.requestedOptions = new Set();
160
+ }
161
+ createRenderRoot() {
162
+ return this;
163
+ }
164
+ willUpdate(changed) {
165
+ // Re-seed on a `values` change too, not just `descriptor` — a field that was
166
+ // inapplicable at initial load (e.g. gated behind `dependencyAttribute`) only
167
+ // gets its own `defaultValue` seeded once the user's edit makes it apply, and
168
+ // that edit lands as a `values` change, never a `descriptor` one. `seedDefaults`
169
+ // only ever fills in a currently-`undefined` attribute, so comparing key counts
170
+ // is enough to tell "did this seeding pass actually add anything" — guards
171
+ // against reassigning (and re-triggering `willUpdate`) every single time.
172
+ if ((changed.has('descriptor') || changed.has('values')) && this.descriptor) {
173
+ const seeded = seedDefaults(this.descriptor, this.values);
174
+ if (Object.keys(seeded).length !== Object.keys(this.values).length) {
175
+ this.values = seeded;
176
+ }
177
+ }
178
+ // Which accordion card starts open is decided from the descriptor, NOT
179
+ // re-decided on every edit — see `accordionOpenSection`'s doc comment.
180
+ // Runs after the seeding above so `resolveOpenKey`'s completeness check
181
+ // sees defaults that were just filled in, not the pre-seed blanks.
182
+ if (changed.has('descriptor') || changed.has('accordionOpenSection') || changed.has('sectionsLayout')) {
183
+ this.openKey = this.resolveOpenKey();
184
+ }
185
+ }
186
+ /** The section to expand first: an explicit `accordionOpenSection` wins
187
+ * (including `''` for "all collapsed"), else the first section still
188
+ * missing a required value, else the first visible one. */
189
+ resolveOpenKey() {
190
+ if (this.accordionOpenSection != null)
191
+ return this.accordionOpenSection || null;
192
+ const visible = this.visibleSections(this.sortedSections());
193
+ const needsAttention = visible.find((s) => sectionStatus(s, this.values).state === 'incomplete');
194
+ return (needsAttention ?? visible[0])?.key ?? null;
195
+ }
196
+ /** Sections in render order (§12) — `order` ascending, missing treated as 0. */
197
+ sortedSections() {
198
+ return [...(this.descriptor?.sections ?? [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
199
+ }
200
+ updated(changed) {
201
+ if (changed.has('descriptor') || changed.has('values') || changed.has('dynamicOptions')) {
202
+ this.requestPendingOptions();
203
+ }
204
+ }
205
+ /** Emits `aq-mcp-options-request` for every currently-applicable `select`/`radio`
206
+ * field that declares `tool` but has no options yet — neither a static
207
+ * `field.options` nor an already-fed `dynamicOptions` entry — and hasn't been
208
+ * asked about before. Skipped for a field that's statically hidden or whose
209
+ * `dependencyAttribute` condition isn't currently met (§12) — no point asking
210
+ * for FTP servers before the user has even chosen SFTP storage. */
211
+ requestPendingOptions() {
212
+ for (const section of this.descriptor?.sections ?? []) {
213
+ for (const field of section.fields ?? []) {
214
+ if (!field.tool)
215
+ continue;
216
+ if (field['ui-component'] !== 'select' && field['ui-component'] !== 'radio')
217
+ continue;
218
+ if (field.hidden || !fieldApplies(field, this.values))
219
+ continue;
220
+ if (this.requestedOptions.has(field.attribute))
221
+ continue;
222
+ const hasOptions = (this.dynamicOptions[field.attribute] ?? field.options ?? []).length > 0;
223
+ if (hasOptions)
224
+ continue;
225
+ this.requestedOptions.add(field.attribute);
226
+ this.dispatchEvent(new CustomEvent('aq-mcp-options-request', {
227
+ detail: { attribute: field.attribute, tool: field.tool, args: field.args ?? {} },
228
+ bubbles: true,
229
+ composed: true,
230
+ }));
231
+ }
232
+ }
233
+ }
234
+ /** Look up a field's own schema by `attribute` — needed to re-validate it
235
+ * live on every edit (`onFieldInput`), since the event only carries the
236
+ * new value, not the field definition itself. */
237
+ fieldByAttribute(attribute) {
238
+ for (const s of this.descriptor?.sections ?? []) {
239
+ const f = (s.fields ?? []).find((field) => field.attribute === attribute);
240
+ if (f)
241
+ return f;
242
+ }
243
+ return undefined;
244
+ }
245
+ /** Whether a field is currently in play for validation — skips
246
+ * `datatype: "none"` (buttons/reports never hold a value), statically
247
+ * hidden fields, and fields whose `dependencyAttribute` condition isn't
248
+ * currently met. Shared by `computeErrors`, live on-change validation, and
249
+ * the tab error-indicator, so all three agree on what "applicable" means. */
250
+ fieldNeedsValidation(f) {
251
+ return f.datatype !== 'none' && !f.hidden && fieldApplies(f, this.values);
252
+ }
253
+ // ---- field intent ----
254
+ onFieldInput(e) {
255
+ const { attribute, value } = e.detail;
256
+ this.values = { ...this.values, [attribute]: value };
257
+ // Live-validate on every change (not just clear-and-wait-for-submit): an
258
+ // edited field's error updates immediately to reflect its NEW value —
259
+ // shows the instant it's typed, clears the instant it's actually fixed.
260
+ const field = this.fieldByAttribute(attribute);
261
+ const msg = field && this.fieldNeedsValidation(field) ? validateField(field, value) : null;
262
+ this.errors = { ...this.errors, [attribute]: msg };
263
+ this.dispatchEvent(new CustomEvent('aq-mcp-change', {
264
+ detail: { attribute, value, values: this.values },
265
+ bubbles: true,
266
+ composed: true,
267
+ }));
268
+ }
269
+ onFieldAction(e) {
270
+ const { field, action } = e.detail;
271
+ const confirm = action?.confirm ?? field.confirm;
272
+ if (confirm) {
273
+ this.pendingAction = { field, action }; // honour confirm before invoking (§5)
274
+ return;
275
+ }
276
+ this.emitAction(field, action);
277
+ }
278
+ onSectionSubmit(e) {
279
+ const { sectionKey } = e.detail;
280
+ const section = (this.descriptor?.sections ?? []).find((s) => s.key === sectionKey);
281
+ if (section)
282
+ this.submitSection(section);
283
+ }
284
+ emitAction(field, action) {
285
+ this.dispatchEvent(new CustomEvent('aq-mcp-action', {
286
+ detail: {
287
+ tool: action?.tool ?? field.tool,
288
+ args: action?.args ?? field.args ?? {},
289
+ confirm: action?.confirm ?? field.confirm,
290
+ field,
291
+ action,
292
+ },
293
+ bubbles: true,
294
+ composed: true,
295
+ }));
296
+ }
297
+ // ---- submit ----
298
+ /** Validate one batch of fields against the current `values` — shared by
299
+ * `validateSection` (one section) and the public `validateAll` (every
300
+ * section), so both paths compute errors identically. */
301
+ computeErrors(fields) {
302
+ const errs = {};
303
+ let ok = true;
304
+ for (const f of fields) {
305
+ if (!this.fieldNeedsValidation(f))
306
+ continue;
307
+ const msg = validateField(f, this.values[f.attribute]);
308
+ errs[f.attribute] = msg;
309
+ if (msg)
310
+ ok = false;
311
+ }
312
+ return { errs, ok };
313
+ }
314
+ /** Does this section have an actual, currently-recorded validation error —
315
+ * i.e. a field present in `this.errors` with a message. Deliberately reads
316
+ * `this.errors`, NOT a fresh re-validation of every field: a required field
317
+ * the user simply hasn't reached yet is incomplete, not invalid, and
318
+ * shouldn't flag its tab before the user has done anything. An error only
319
+ * lands in `this.errors` once a field has actually been edited (live
320
+ * validation) or the section/form has been submitted at least once —
321
+ * exactly the same "has this field's error actually surfaced" signal the
322
+ * inline per-field error display already uses. */
323
+ sectionHasError(section) {
324
+ return (section.fields ?? []).some((f) => !!this.errors[f.attribute]);
325
+ }
326
+ validateSection(section) {
327
+ const { errs, ok } = this.computeErrors(section.fields ?? []);
328
+ this.errors = { ...this.errors, ...errs };
329
+ return ok;
330
+ }
331
+ submitSection(section) {
332
+ if (!this.validateSection(section))
333
+ return;
334
+ this.dispatchEvent(new CustomEvent('aq-mcp-submit', {
335
+ detail: collectSubmit(section, this.values),
336
+ bubbles: true,
337
+ composed: true,
338
+ }));
339
+ }
340
+ /** Validate every section's applicable fields at once and return the full
341
+ * form state. Pushes results into this element's own `errors` state — the
342
+ * same thing a section's Save button does via `validateSection`, just
343
+ * across ALL sections at once — so a host-driven "submit everything"
344
+ * button (bypassing per-section footers entirely) still gets the
345
+ * library's real validation and inline field-error display, not just the
346
+ * raw `values`. Call it imperatively on the element (e.g. an Angular
347
+ * template ref: `<aq-mcp-config #cfg>` then `cfg.validateAll()`) — there's
348
+ * no natural event to hang this off, since the host decides what "submit
349
+ * everything" even means for its own data shape. */
350
+ validateAll() {
351
+ const allFields = (this.descriptor?.sections ?? []).flatMap((s) => s.fields ?? []);
352
+ const { errs, ok } = this.computeErrors(allFields);
353
+ this.errors = errs;
354
+ return { valid: ok, values: this.values };
355
+ }
356
+ render() {
357
+ const d = this.descriptor;
358
+ if (!d)
359
+ return html ``;
360
+ const sections = this.sortedSections();
361
+ const major = majorOf(d.version);
362
+ // MAJOR ahead of what we support → best-effort render on known keys + a notice (§9.5).
363
+ const versionAhead = Number.isFinite(major) && major > SUPPORTED_MAJOR;
364
+ return html `
365
+ <div
366
+ class="aq-mcp ${this.sectionsLayout === 'accordion' ? 'aq-mcp--accordion' : ''}"
367
+ @field-input=${this.onFieldInput}
368
+ @field-action=${this.onFieldAction}
369
+ @section-submit=${this.onSectionSubmit}
370
+ >
371
+ ${versionAhead
372
+ ? html `<nile-section-message
373
+ variant="warning"
374
+ heading="Unsupported configuration version"
375
+ description="This form was built for an older version (v${SUPPORTED_MAJOR}.x). Some fields may not render as intended."
376
+ ></nile-section-message>`
377
+ : html ``}
378
+ ${this.renderSections(sections)}
379
+ ${this.renderConfirm()}
380
+ </div>
381
+ `;
382
+ }
383
+ /** Dispatch to the layout the host asked for. `'stacked'` — today's
384
+ * default — is the fallback for any unrecognised value, so a typo'd
385
+ * attribute renders a usable form rather than nothing. */
386
+ renderSections(sections) {
387
+ if (this.sectionsLayout === 'tabs')
388
+ return this.renderTabs(sections);
389
+ if (this.sectionsLayout === 'accordion')
390
+ return this.renderAccordion(sections);
391
+ return sections.map((s) => this.renderSection(s));
392
+ }
393
+ /** A section whose every field is statically hidden has nothing to show
394
+ * (§7, §12) — in 'stacked' mode `renderSection` just renders nothing for
395
+ * it; in 'tabs' mode it must not get a tab at all (an empty tab is worse
396
+ * than no tab), so this filters the nav list up front. */
397
+ visibleSections(sections) {
398
+ return sections.filter((s) => {
399
+ const fields = s.fields ?? [];
400
+ return !(fields.length > 0 && fields.every((f) => f.hidden));
401
+ });
402
+ }
403
+ renderTabs(sections) {
404
+ const visible = this.visibleSections(sections);
405
+ if (visible.length === 0)
406
+ return html ``;
407
+ // 'start'/'end' are the vertical placements — nile-nav-tab-group's own
408
+ // `width` attribute fixes each tab item's width (its documented purpose:
409
+ // preventing layout shift when switching tabs), which for a vertical nav
410
+ // list is exactly the nav column's width; `indicator-placement="left"`
411
+ // is Nile's own documented pairing for vertical placement.
412
+ const vertical = this.tabsPlacement === 'start' || this.tabsPlacement === 'end';
413
+ return html `
414
+ <nile-nav-tab-group
415
+ class="aq-mcp-tabs"
416
+ placement=${this.tabsPlacement}
417
+ indicator-placement=${vertical ? 'left' : ''}
418
+ width=${vertical ? '220px' : ''}
419
+ >
420
+ ${visible.map((s) => html `<nile-nav-tab slot="nav" panel=${s.key}>
421
+ <span class="aq-mcp-tab-label" title=${s.label}>${s.label}</span>
422
+ ${this.tabsShowErrorIndicator && this.sectionHasError(s)
423
+ ? html `<span
424
+ slot="suffix"
425
+ class="aq-mcp-tab-error-dot"
426
+ role="img"
427
+ aria-label="${s.label} has a validation error"
428
+ ></span>`
429
+ : ''}
430
+ </nile-nav-tab>`)}
431
+ ${visible.map((s) => html `<nile-nav-tab-panel name=${s.key}>${this.renderSection(s, true)}</nile-nav-tab-panel>`)}
432
+ </nile-nav-tab-group>
433
+ `;
434
+ }
435
+ /**
436
+ * Is this `nile-show`/`nile-hide` the ACCORDION's own, or one that bubbled up
437
+ * from a descendant?
438
+ *
439
+ * Every Nile component emits with `bubbles: true, composed: true`
440
+ * (`internal/nile-element.ts`), and `nile-tooltip` emits the same
441
+ * `nile-show`/`nile-hide` names an accordion does. A field's help tooltip
442
+ * lives inside the open card, so without this guard simply hovering an info
443
+ * icon opened the section and dismissing the tooltip COLLAPSED it — the
444
+ * card slamming shut under the user's cursor.
445
+ *
446
+ * `currentTarget` is the `nile-accordion` the listener sits on;
447
+ * `target` is whatever emitted. Comparing them keeps a descendant's
448
+ * identically-named event from driving this element's open state, and stays
449
+ * correct for any future nested Nile component that reuses these names.
450
+ */
451
+ static isOwnDisclosure(e) {
452
+ return e.target === e.currentTarget;
453
+ }
454
+ /**
455
+ * `sectionsLayout: 'accordion'` — one `nile-accordion` per visible section,
456
+ * single-open, in §12 order.
457
+ *
458
+ * Single-open falls out of holding ONE key: `nile-accordion` owns its own
459
+ * `open` flag, so both directions are wired — `?open` down from `openKey`,
460
+ * `nile-show`/`nile-hide` back up. Setting `openKey` on a show re-renders
461
+ * every sibling with `open=false`, which closes them; there's no need to
462
+ * imperatively call `hide()` on the others.
463
+ */
464
+ /** Host map first, then the built-in map for the contract's own section
465
+ * keys, then a generic glyph. The descriptor has no icon field and its
466
+ * shape is fixed, so the host is the only place a product can override
467
+ * this — which is the right place anyway: iconography is the app's
468
+ * decision, not the connector's. */
469
+ iconFor(section) {
470
+ return this.sectionIcons[section.key] ?? DEFAULT_SECTION_ICONS[section.key] ?? FALLBACK_SECTION_ICON;
471
+ }
472
+ /**
473
+ * The header's completion readout, for a section the user may never open.
474
+ *
475
+ * A mark and a NUMBER, no sentence. The count is descriptor-derived (how
476
+ * many `isRequired` fields still hold no value); the check and the dot are
477
+ * symbols. An earlier version read "Complete" and "N required left", which
478
+ * is the renderer narrating state in words the descriptor never supplied.
479
+ *
480
+ * The trade-off is real and worth naming: a bare dot-and-count is terser
481
+ * than "2 required left", and leans on the reader connecting it to the `*`
482
+ * marks inside the card. `title` carries the count on hover and to assistive
483
+ * tech using the descriptor's own key name rather than invented prose.
484
+ */
485
+ renderSectionStatus(status) {
486
+ if (status.state === 'neutral')
487
+ return html ``;
488
+ if (status.state === 'incomplete') {
489
+ return html `<span
490
+ class="aq-mcp-acc__state aq-mcp-acc__state--incomplete"
491
+ title="isRequired: ${status.requiredEmpty}"
492
+ >
493
+ <span class="aq-mcp-acc__dot"></span>${status.requiredEmpty}
494
+ </span>`;
495
+ }
496
+ return html `<span class="aq-mcp-acc__state aq-mcp-acc__state--complete">
497
+ <nile-glyph name="ng-check" method="stroke" size="18" color="currentColor"></nile-glyph>
498
+ </span>`;
499
+ }
500
+ /**
501
+ * `sectionsLayout: 'accordion'` — one `nile-accordion` per visible section,
502
+ * single-open, in §12 order. Each header is an icon tile, the section
503
+ * `label`, its `authType` badge, and a completion status beside the chevron,
504
+ * so a collapsed section says what it is and whether it still needs work
505
+ * without being opened.
506
+ *
507
+ * Every word of connector-describing text here is a descriptor VALUE
508
+ * rendered verbatim — `section.label` and `section.authType`. An earlier
509
+ * version composed a sentence around them ("Basic authentication · saved by
510
+ * the platform"), which read as the connector's own description of itself
511
+ * but was the renderer putting words in its mouth: only "Basic" came from
512
+ * the descriptor, and "saved by the platform" was inferred from a key being
513
+ * ABSENT. The `bootstrap` format carries no prose field, so rather than
514
+ * invent one, nothing is written here that the descriptor did not supply.
515
+ *
516
+ * Single-open falls out of holding ONE key: `nile-accordion` owns its own
517
+ * `open` flag, so both directions are wired — `?open` down from `openKey`,
518
+ * `nile-show`/`nile-hide` back up. Setting `openKey` on a show re-renders
519
+ * every sibling with `open=false`, which closes them; there's no need to
520
+ * imperatively call `hide()` on the others.
521
+ */
522
+ renderAccordion(sections) {
523
+ const visible = this.visibleSections(sections);
524
+ if (visible.length === 0)
525
+ return html ``;
526
+ return html `<div class="aq-mcp-acc-list">
527
+ ${visible.map((s) => {
528
+ const status = sectionStatus(s, this.values);
529
+ return html `<nile-accordion
530
+ class="aq-mcp-acc"
531
+ data-section=${s.key}
532
+ data-state=${status.state}
533
+ expandIconPlacement="right"
534
+ size="lg"
535
+ ?open=${this.openKey === s.key}
536
+ @nile-show=${(e) => {
537
+ if (!AqMcpConfig_1.isOwnDisclosure(e))
538
+ return;
539
+ this.openKey = s.key;
540
+ }}
541
+ @nile-hide=${(e) => {
542
+ if (!AqMcpConfig_1.isOwnDisclosure(e))
543
+ return;
544
+ // Only clear if THIS section is the one recorded as open — a hide
545
+ // fired by a sibling being re-rendered closed must not wipe the
546
+ // key that was just set by the section the user actually opened.
547
+ if (this.openKey === s.key)
548
+ this.openKey = null;
549
+ }}
550
+ >
551
+ <div slot="summary" class="aq-mcp-acc__summary">
552
+ <span class="aq-mcp-acc__icon" aria-hidden="true">
553
+ <nile-glyph name=${this.iconFor(s)} method="stroke" size="20" color="currentColor"></nile-glyph>
554
+ </span>
555
+ <span class="aq-mcp-acc__heading">
556
+ <span class="aq-mcp-acc__label">${s.label}</span>
557
+ ${s.authType ? html `<nile-badge variant="info">${s.authType}</nile-badge>` : ''}
558
+ </span>
559
+ ${this.renderSectionStatus(status)}
560
+ </div>
561
+ ${this.renderSection(s, false, true)}
562
+ </nile-accordion>`;
563
+ })}
564
+ </div>`;
565
+ }
566
+ /** `compact` drops the per-section card's own border + title (the tab nav
567
+ * already shows the label) when nested inside a tab panel — the authType
568
+ * badge and save-target caption still show either way. `dense` is the
569
+ * accordion equivalent: the card AND the header go (the accordion's own
570
+ * summary carries all of it), and the body switches to the two-column,
571
+ * label-above-control grid. They're separate flags, not one enum, because
572
+ * each drops a different amount of chrome. */
573
+ renderSection(section, compact = false, dense = false) {
574
+ const fields = section.fields ?? [];
575
+ // Section whose every field is hidden renders nothing (§7, §12).
576
+ if (fields.length > 0 && fields.every((f) => f.hidden))
577
+ return html ``;
578
+ // Accordion mode hides the per-section footer unless the host opts back
579
+ // in (`accordionShowFooters`) — `showSectionFooters` still vetoes both.
580
+ const showFooter = this.showSectionFooters && (!dense || this.accordionShowFooters);
581
+ return html `<aq-mcp-section
582
+ .section=${section}
583
+ .values=${this.values}
584
+ .errors=${this.errors}
585
+ .showFooter=${showFooter}
586
+ .compact=${compact}
587
+ .dense=${dense}
588
+ .columns=${this.accordionColumns}
589
+ .dynamicOptions=${this.dynamicOptions}
590
+ ></aq-mcp-section>`;
591
+ }
592
+ renderConfirm() {
593
+ const pending = this.pendingAction;
594
+ if (!pending)
595
+ return html ``;
596
+ const { field, action } = pending;
597
+ const cancel = () => {
598
+ this.pendingAction = null;
599
+ };
600
+ const proceed = () => {
601
+ this.pendingAction = null;
602
+ this.emitAction(field, action);
603
+ };
604
+ return html `
605
+ <nile-dialog open label="Please confirm" @nile-close=${cancel} @nile-hide=${cancel}>
606
+ <p>${action?.confirm ?? field.confirm}</p>
607
+ <nile-button slot="footer" variant="ghost" @click=${cancel}>Cancel</nile-button>
608
+ <nile-button slot="footer" variant="primary" @click=${proceed}>Continue</nile-button>
609
+ </nile-dialog>
610
+ `;
611
+ }
612
+ };
613
+ __decorate([
614
+ property({ attribute: false }),
615
+ __metadata("design:type", Object)
616
+ ], AqMcpConfig.prototype, "descriptor", void 0);
617
+ __decorate([
618
+ property({ attribute: false }),
619
+ __metadata("design:type", Object)
620
+ ], AqMcpConfig.prototype, "values", void 0);
621
+ __decorate([
622
+ property({ attribute: false }),
623
+ __metadata("design:type", Object)
624
+ ], AqMcpConfig.prototype, "dynamicOptions", void 0);
625
+ __decorate([
626
+ property({ type: Boolean }),
627
+ __metadata("design:type", Object)
628
+ ], AqMcpConfig.prototype, "showSectionFooters", void 0);
629
+ __decorate([
630
+ property({ type: String }),
631
+ __metadata("design:type", String)
632
+ ], AqMcpConfig.prototype, "sectionsLayout", void 0);
633
+ __decorate([
634
+ property({ type: String }),
635
+ __metadata("design:type", String)
636
+ ], AqMcpConfig.prototype, "tabsPlacement", void 0);
637
+ __decorate([
638
+ property({ type: Boolean }),
639
+ __metadata("design:type", Object)
640
+ ], AqMcpConfig.prototype, "tabsShowErrorIndicator", void 0);
641
+ __decorate([
642
+ property({ type: String }),
643
+ __metadata("design:type", Object)
644
+ ], AqMcpConfig.prototype, "accordionOpenSection", void 0);
645
+ __decorate([
646
+ property({ type: Boolean }),
647
+ __metadata("design:type", Object)
648
+ ], AqMcpConfig.prototype, "accordionShowFooters", void 0);
649
+ __decorate([
650
+ property({ type: Number }),
651
+ __metadata("design:type", Number)
652
+ ], AqMcpConfig.prototype, "accordionColumns", void 0);
653
+ __decorate([
654
+ property({ attribute: false }),
655
+ __metadata("design:type", Object)
656
+ ], AqMcpConfig.prototype, "sectionIcons", void 0);
657
+ __decorate([
658
+ state(),
659
+ __metadata("design:type", Object)
660
+ ], AqMcpConfig.prototype, "errors", void 0);
661
+ __decorate([
662
+ state(),
663
+ __metadata("design:type", Object)
664
+ ], AqMcpConfig.prototype, "openKey", void 0);
665
+ __decorate([
666
+ state(),
667
+ __metadata("design:type", Object)
668
+ ], AqMcpConfig.prototype, "pendingAction", void 0);
669
+ AqMcpConfig = AqMcpConfig_1 = __decorate([
670
+ customElement('aq-mcp-config')
671
+ ], AqMcpConfig);
672
+ export { AqMcpConfig };
673
+ //# sourceMappingURL=aq-mcp-config.js.map