feat(ms-ai-architect): playground v3 catalog surface with search + 5 expansion groups [skip-docs]

Step 9 of v3 plan. Replaces renderCatalogStub with full
renderCatalogSurface — search-input + 5 .expansion-grupper (en per
CATALOG.categories) + per-command-card with "Åpne skjema"-button. Klikk
åpner modal med renderCommandForm (samme generic renderer som
prosjekt-detalj fra Step 8).

Søk: input-event oppdaterer modul-lokal catalogSearchQuery og kaller
refreshCatalogResults() som re-rendrer kun groups-containeren — bevarer
fokus + cursor i søkefeltet (full re-render ville flyttet caret).
Filtrerer på id+label+description+argument_hint. Når query er aktiv
forces alle expansions med treff åpne; ellers er 'regulatory' åpen som
default (mest brukt entry-point).

Verktøy-commands får .catalog-card__pill="Verktøy" + .catalog-tool-notice
("Verktøy — ingen rapport-import"). Modalen viser samme advarsel via
.guide-panel--info-banner. Rapport-produserende får "Rapport"-pill.

Verifisert via vm-sandbox med activeSurface='catalog':
- data-command-card === 24 (Step 9 verify-assert ✓)
- 5 expansion-grupper (data-catalog-group)
- 24 open-catalog-form-knapper
- 17 Rapport-pills + 7 Verktøy-notices (matcher CATALOG.commands.filter
  produces_report)
- refreshCatalogResults() med query='classify' kjører feilfritt
This commit is contained in:
Kjell Tore Guttormsen 2026-05-03 18:35:44 +02:00
commit 3750bee48b

View file

@ -1440,7 +1440,7 @@
if (active === 'onboarding') renderOnboardingSurface();
else if (active === 'home') renderHomeSurface();
else if (active === 'project') renderProjectSurface();
else if (active === 'catalog') renderCatalogStub();
else if (active === 'catalog') renderCatalogSurface();
}
function navigate(surface) {
@ -1920,10 +1920,164 @@
);
}
function renderCatalogStub() {
// ============================================================
// CATALOG SURFACE (Step 9)
// ============================================================
//
// 24 commands gruppert i 5 .expansion-grupper (CATALOG.categories) med
// søke-input som filtrerer på id+label+description+argument_hint.
// Hver kategori-expansion rendrer en .catalog-cards-grid med kort.
// "Åpne skjema" på et kort åpner renderCommandForm() i modal.
//
// Søk: input-event oppdaterer modul-lokal catalogSearchQuery og
// re-rendrer kun groups-containeren (bevarer fokus/cursor i søkefeltet).
// Når query er ikke-tom forces alle expansions åpne. I tom-state er
// 'regulatory' åpen som standard (mest brukt entry-point).
//
// Verktøy-commands får .catalog-tool-notice "Verktøy"-pill + samme
// skjema-modal — ingen rapport-import (parser/renderer hopper dem over).
let catalogSearchQuery = '';
function catalogMatches(cmd, q) {
if (!q) return true;
const hay = (
(cmd.id || '') + ' ' +
(cmd.label || '') + ' ' +
(cmd.description || '') + ' ' +
(cmd.argument_hint || '')
).toLowerCase();
return hay.indexOf(q) >= 0;
}
function renderCatalogCardHtml(cmd) {
const isVerktoy = !cmd.produces_report;
const pill = isVerktoy
? '<span class="catalog-card__pill">Verktøy</span>'
: '<span class="catalog-card__pill">Rapport</span>';
const hintHtml = cmd.argument_hint
? '<span class="catalog-card__hint">' + escapeHtml(cmd.argument_hint) + '</span>'
: '';
const verktoyNotice = isVerktoy
? '<div class="catalog-tool-notice">Verktøy — ingen rapport-import. Skjema bygger pipeline-streng som kjøres i terminalen.</div>'
: '';
return (
'<article class="catalog-card" data-command-card data-command-id="' + escapeAttr(cmd.id) + '">' +
'<div class="catalog-card__head">' +
'<div>' +
'<h3 class="catalog-card__title">' + escapeHtml(cmd.label) + '</h3>' +
'<p class="catalog-card__desc">' + escapeHtml(cmd.description) + '</p>' +
'</div>' +
pill +
'</div>' +
'<div class="catalog-card__meta">' +
'<span class="command-card__id">/architect:' + escapeHtml(cmd.id) + '</span>' +
hintHtml +
'</div>' +
verktoyNotice +
'<div class="catalog-card__actions">' +
'<button type="button" class="btn btn--secondary btn--sm" data-action="open-catalog-form" data-command="' + escapeAttr(cmd.id) + '">Åpne skjema</button>' +
'</div>' +
'</article>'
);
}
function renderCatalogGroupsHtml() {
const q = (catalogSearchQuery || '').trim().toLowerCase();
return CATALOG.categories.map(function (cat) {
const cmds = CATALOG.commands.filter(function (c) {
return c.category === cat.id && catalogMatches(c, q);
});
const cardsHtml = cmds.map(renderCatalogCardHtml).join('');
// Force-open når aktiv søk-query har treff. Ellers: 'regulatory' åpen som default.
const expanded = q ? (cmds.length > 0 ? 'true' : 'false') : (cat.id === 'regulatory' ? 'true' : 'false');
const subLabel = cmds.length === cat.count
? cat.count + ' commands'
: cmds.length + ' / ' + cat.count + ' commands';
const body = cmds.length > 0
? '<div class="catalog-cards">' + cardsHtml + '</div>'
: '<p class="command-form__hint" style="padding: var(--space-2) 0;">Ingen treff i denne kategorien.</p>';
return (
'<section class="expansion" aria-expanded="' + expanded + '" data-catalog-group="' + escapeAttr(cat.id) + '">' +
'<button type="button" class="expansion__head" data-action="catalog-toggle-group">' +
'<span class="expansion__title">' +
'<span class="expansion__title-main">' + escapeHtml(cat.label) + '</span>' +
'<span class="expansion__title-sub">' + subLabel + '</span>' +
'</span>' +
'<span class="expansion__chev" aria-hidden="true"></span>' +
'</button>' +
'<div class="expansion__body">' +
'<div class="expansion__body-inner">' + body + '</div>' +
'</div>' +
'</section>'
);
}).join('');
}
function renderCatalogSurface() {
const root = getSurfaceEl('catalog');
if (!root) return;
root.innerHTML = renderTopbar('Katalog') + '<div class="app-shell"><p>Command-katalog fylles i Step 9.</p></div>';
const q = (catalogSearchQuery || '').trim().toLowerCase();
const totalMatches = CATALOG.commands.filter(function (c) { return catalogMatches(c, q); }).length;
const countText = totalMatches + ' av ' + CATALOG.commands.length + ' treff' + (q ? ' for «' + escapeHtml(catalogSearchQuery) + '»' : '');
root.innerHTML = (
renderTopbar('Katalog') +
'<div class="app-shell app-shell--wide">' +
'<header class="catalog-header">' +
'<h1>Command-katalog</h1>' +
'<p>24 commands gruppert i 5 kategorier. Åpne skjema for å bygge en pipeline-streng som kopieres til terminalen og kjøres med Claude Code.</p>' +
'</header>' +
'<div class="catalog-toolbar">' +
'<input type="search" class="input" placeholder="Søk på navn, beskrivelse eller argument-hint…" value="' + escapeAttr(catalogSearchQuery) + '" data-catalog-search aria-label="Søk i katalog">' +
'<span class="catalog-toolbar__count" data-catalog-count>' + countText + '</span>' +
'</div>' +
'<div class="catalog-groups" data-catalog-groups>' + renderCatalogGroupsHtml() + '</div>' +
'</div>'
);
}
function refreshCatalogResults() {
const root = getSurfaceEl('catalog');
if (!root) return;
const groupsEl = root.querySelector('[data-catalog-groups]');
if (groupsEl) groupsEl.innerHTML = renderCatalogGroupsHtml();
const countEl = root.querySelector('[data-catalog-count]');
if (countEl) {
const q = (catalogSearchQuery || '').trim().toLowerCase();
const totalMatches = CATALOG.commands.filter(function (c) { return catalogMatches(c, q); }).length;
countEl.textContent = totalMatches + ' av ' + CATALOG.commands.length + ' treff' + (q ? ' for «' + catalogSearchQuery + '»' : '');
}
}
function renderCatalogFormModalHtml(cmd) {
const formHtml = renderCommandForm(cmd.id, { context: 'modal', scope: 'm' });
const verktoyBanner = !cmd.produces_report
? (
'<div class="guide-panel guide-panel--info">' +
'<div class="guide-panel__icon" aria-hidden="true">i</div>' +
'<div class="guide-panel__body">' +
'<h3 class="guide-panel__title">Verktøy</h3>' +
'<p class="guide-panel__text">Dette er et verktøy. Skjemaet bygger en pipeline-streng — ingen rapport-import.</p>' +
'</div>' +
'</div>'
)
: '';
return (
'<div class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="cf-modal-title">' +
'<div class="modal modal--wide">' +
'<div>' +
'<h2 class="modal__title" id="cf-modal-title">' + escapeHtml(cmd.label) + '</h2>' +
'<p class="catalog-card__desc" style="margin-top: var(--space-2);">' + escapeHtml(cmd.description) + '</p>' +
'<span class="command-card__id">/architect:' + escapeHtml(cmd.id) + '</span>' +
'</div>' +
verktoyBanner +
'<div>' + formHtml + '</div>' +
'<div class="modal__actions">' +
'<button type="button" class="btn btn--ghost" data-action="modal-cancel">Lukk</button>' +
'</div>' +
'</div>' +
'</div>'
);
}
// ---- Paste-import stub (Step 12 erstatter med faktisk routing) ----
@ -2457,6 +2611,31 @@
showCommandPreview(formEl, buildCommand(commandId, data));
};
// ---- Step 9: catalog actions ----
ACTIONS['open-catalog-form'] = function (ev, el) {
const commandId = el.dataset.command;
if (!commandId) return;
const cmd = (CATALOG.commands || []).find(function (c) { return c.id === commandId; });
if (!cmd) return;
mountModal(renderCatalogFormModalHtml(cmd));
};
ACTIONS['catalog-toggle-group'] = function (ev, el) {
const exp = el.closest('.expansion');
if (!exp) return;
const open = exp.getAttribute('aria-expanded') === 'true';
exp.setAttribute('aria-expanded', open ? 'false' : 'true');
};
// Søk-input: input-event oppdaterer query og re-rendrer kun groups-containeren
// (bevarer fokus/cursor i selve søke-feltet — full re-render ville flyttet caret).
document.addEventListener('input', function (ev) {
if (!ev.target.matches || !ev.target.matches('[data-catalog-search]')) return;
catalogSearchQuery = ev.target.value || '';
refreshCatalogResults();
});
// Eksponer for Verify-asserts og Step 8/9/12.
window.__SCENARIOS = SCENARIOS;
window.__createProject = createProject;
@ -2468,6 +2647,8 @@
window.__renderCommandForm = renderCommandForm;
window.__readCommandFormValues = readCommandFormValues;
window.__resolveSharedPath = resolveSharedPath;
window.__renderCatalogSurface = renderCatalogSurface;
window.__refreshCatalogResults = refreshCatalogResults;
ACTIONS['export-state'] = function () {
try { exportState(); }