feat(llm-security): playground v7.6.2-dev — katalog list-view + builder-pane [skip-docs]

- renderCatalogSurface rewritten to list-view (1 rad per kommando),
  filter-chips (Alle/Rapport/Verktoy + 6 kategori-chips) + sok
- Builder-pane (modal) med live-preview: pipeline-strengen oppdateres
  mens skjema fylles ut. Kopier-knapp er primaer CTA med clipboard API +
  textarea-fallback for file:// (allerede eksisterende).
- Smart prefill fra store.state.shared via 'from: shared' fields i
  renderCommandForm. Pane-state skriver ikke tilbake til shared (scope
  'cat', ingen project-save). Felles-felt markert med 'felles'-badge.
- Forstegangsbesok lander pa home (fjernet onboarding auto-redirect).
  Re-onboard tilgjengelig via topbar.

Sesjon 1 av 5 i v7.7.0-lopet. CSS-additioner: catalog-filter-chips,
catalog-chip, catalog-list, catalog-row, builder-modal.

Tester: 1822/1822 gronne. Static JS-parse OK. Browser-walkthrough
gjenstar — verifiseres manuelt for v7.7.0 release. Docs oppdateres ved
v7.7.0-release i Sesjon 5 (samlet commit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-18 11:56:44 +02:00
commit 0dc7ff485f

View file

@ -112,7 +112,7 @@
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
/* Catalog */
.catalog-search { width: 100%; max-width: 480px; margin-bottom: var(--space-5); }
.catalog-search { width: 100%; max-width: 480px; margin-bottom: 0; }
.catalog-cards-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--space-3); margin-top: var(--space-3); }
.catalog-tool-notice { padding: 8px 12px; background: var(--color-bg-soft); border-left: 3px solid var(--color-state-info, var(--color-primary-300)); font-size: var(--font-size-xs); color: var(--color-text-secondary); border-radius: var(--radius-sm); }
/* Expansion-body: playground-markup mangler .expansion__body-inner-wrapping
@ -121,6 +121,42 @@
.expansion__body { padding: 0 var(--space-4) var(--space-4); border-top: 1px solid var(--color-border-subtle); }
.expansion[aria-expanded="false"] .expansion__body { display: none; }
/* Catalog v7.6.2: filter-chips + list-view + builder-pane */
.catalog-filter-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; }
.catalog-chip { font-family: inherit; font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); padding: 6px 12px; border-radius: var(--radius-pill); border: 1px solid var(--color-border-moderate); background: var(--color-surface); color: var(--color-text-secondary); cursor: pointer; transition: background 120ms ease, border-color 120ms ease, color 120ms ease; display: inline-flex; align-items: center; gap: 6px; }
.catalog-chip:hover { border-color: var(--color-border-strong); color: var(--color-text-primary); }
.catalog-chip:focus-visible { outline: 2px solid var(--color-scope-security, var(--color-primary-500)); outline-offset: 2px; }
.catalog-chip--active { background: var(--color-scope-security, var(--color-primary-500)); border-color: var(--color-scope-security, var(--color-primary-500)); color: var(--color-text-on-primary, #fff); }
.catalog-chip--active:hover { color: var(--color-text-on-primary, #fff); }
.catalog-chip__count { font-size: 10px; opacity: 0.85; padding: 1px 6px; border-radius: var(--radius-pill); background: rgba(0,0,0,0.08); }
.catalog-chip--active .catalog-chip__count { background: rgba(255,255,255,0.18); }
.catalog-list { display: flex; flex-direction: column; gap: 1px; background: var(--color-border-subtle); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); overflow: hidden; }
.catalog-row { display: flex; align-items: stretch; gap: var(--space-4); padding: var(--space-3) var(--space-4); background: var(--color-surface); border: 0; text-align: left; font-family: inherit; cursor: pointer; transition: background 120ms ease; width: 100%; }
.catalog-row:hover { background: var(--color-bg-soft); }
.catalog-row:focus-visible { outline: 2px solid var(--color-scope-security, var(--color-primary-500)); outline-offset: -2px; }
.catalog-row__main { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.catalog-row__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2); }
.catalog-row__id { font-family: var(--font-family-mono); font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); color: var(--color-text-primary); }
.catalog-row__label { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.catalog-row__desc { font-size: var(--font-size-xs); color: var(--color-text-tertiary); line-height: var(--line-height-snug); }
.catalog-row__hint { font-family: var(--font-family-mono); font-size: 11px; color: var(--color-text-tertiary); background: var(--color-bg-soft); padding: 1px 6px; border-radius: var(--radius-sm); }
.catalog-row__meta { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; font-size: var(--font-size-xs); }
.catalog-row__category { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); font-weight: var(--font-weight-semibold); }
.catalog-row__fields { color: var(--color-text-tertiary); }
.catalog-empty { padding: var(--space-5); text-align: center; color: var(--color-text-tertiary); border: 1px dashed var(--color-border-subtle); border-radius: var(--radius-md); }
/* Builder-pane: bigger modal, layout-tuned for live-preview workflow */
.builder-modal { max-width: 880px; }
.builder-modal__lede { color: var(--color-text-secondary); margin: 0; font-size: var(--font-size-sm); }
.builder-modal .form-preview { background: var(--color-bg-soft); border: 1px solid var(--color-border-subtle); }
.builder-modal__hint { font-size: var(--font-size-xs); color: var(--color-text-tertiary); margin: 0; }
@media (max-width: 720px) {
.catalog-row { flex-direction: column; gap: var(--space-2); }
.catalog-row__meta { flex-direction: row; align-items: center; }
}
/* Modal (playground-only — DS har ikke modal-pattern enda) */
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; padding: var(--space-5); }
.modal { background: var(--color-surface); border-radius: var(--radius-md); max-width: 720px; width: 100%; max-height: 90vh; overflow: auto; padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); }
@ -5694,10 +5730,9 @@
window.__store = store;
window.__persistence = persistence;
// Initial-surface heuristikk
const orgName = store.state.shared && store.state.shared.organization && store.state.shared.organization.name;
if (!orgName) store.state.activeSurface = 'onboarding';
else if (!store.state.activeSurface) store.state.activeSurface = 'home';
// Initial-surface heuristikk: førstegangsbesøk lander på home.
// Re-onboard er tilgjengelig via topbar når brukeren ønsker det.
if (!store.state.activeSurface) store.state.activeSurface = 'home';
scheduleRender();
}
@ -6875,6 +6910,7 @@
// CATALOG SURFACE
// ============================================================
let catalogSearchQuery = '';
let catalogFilter = 'all'; // 'all' | 'report' | 'tool' | <category-id>
function catalogMatches(cmd, q) {
if (!q) return true;
@ -6882,68 +6918,88 @@
return hay.indexOf(q) >= 0;
}
function renderCatalogCardHtml(cmd) {
function categoryLabelById(id) {
const c = (CATALOG.categories || []).find(function (x) { return x.id === id; });
return c ? c.label : id;
}
function filteredCatalogCommands() {
const q = catalogSearchQuery.toLowerCase().trim();
return (CATALOG.commands || []).filter(function (c) {
if (!catalogMatches(c, q)) return false;
if (catalogFilter === 'all') return true;
if (catalogFilter === 'report') return !!c.produces_report;
if (catalogFilter === 'tool') return !c.produces_report;
return c.category === catalogFilter;
});
}
function renderCatalogRowHtml(cmd) {
const isVerktoy = !cmd.produces_report;
const pill = isVerktoy ? '<span class="card__pill">Verktøy</span>' : '<span class="card__pill">Rapport</span>';
const hintHtml = cmd.argument_hint ? '<span class="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>' : '';
const hintHtml = cmd.argument_hint ? ' <code class="catalog-row__hint">' + escapeHtml(cmd.argument_hint) + '</code>' : '';
const fieldCount = (cmd.input_fields || []).length;
const fieldLabel = fieldCount + ' felt' + (fieldCount === 1 ? '' : 'er');
return (
'<article class="card" data-command-card data-command-id="' + escapeAttr(cmd.id) + '">' +
'<div class="card__head">' +
'<div>' +
'<h3 class="card__title">' + escapeHtml(cmd.label) + '</h3>' +
'<p class="card__desc">' + escapeHtml(cmd.description) + '</p>' +
'<button type="button" class="catalog-row" data-action="catalog-open-form" data-command="' + escapeAttr(cmd.id) + '" aria-label="Åpne builder for ' + escapeAttr(cmd.label) + '">' +
'<span class="catalog-row__main">' +
'<span class="catalog-row__head">' +
'<span class="catalog-row__id">/security:' + escapeHtml(cmd.id) + '</span>' +
'<span class="catalog-row__label">' + escapeHtml(cmd.label) + '</span>' +
hintHtml +
'</div>' +
'<div style="display:flex; flex-direction:column; gap:6px; align-items:flex-end;">' +
'<span class="badge badge--scope-security">llm-security</span>' +
pill +
'</div>' +
'</div>' +
verktoyNotice +
'<div class="card__actions">' +
'<button type="button" class="btn btn--primary btn--sm" data-action="catalog-open-form" data-command="' + escapeAttr(cmd.id) + '">Åpne skjema</button>' +
'<span style="font-size: var(--font-size-xs); color: var(--color-text-tertiary);">' + (cmd.input_fields || []).length + ' felter</span>' +
'</div>' +
'</article>'
'</span>' +
'<span class="catalog-row__desc">' + escapeHtml(cmd.description) + '</span>' +
'</span>' +
'<span class="catalog-row__meta">' +
'<span class="catalog-row__category">' + escapeHtml(categoryLabelById(cmd.category)) + '</span>' +
pill +
'<span class="catalog-row__fields">' + fieldLabel + '</span>' +
'</span>' +
'</button>'
);
}
function renderCatalogGroupsHtml() {
const q = catalogSearchQuery.toLowerCase().trim();
return CATALOG.categories.map(function (cat) {
const cmds = CATALOG.commands.filter(function (c) { return c.category === cat.id && catalogMatches(c, q); });
if (cmds.length === 0 && q) return ''; // skjul tomme grupper ved aktiv søk
const isOpen = q !== '' || cat.id === 'discover'; // discover åpen som default
const cardsHtml = cmds.length > 0
? '<div class="catalog-cards-grid">' + cmds.map(renderCatalogCardHtml).join('') + '</div>'
: '<p style="color: var(--color-text-tertiary); margin: var(--space-3) 0;">Ingen kommandoer i denne kategorien.</p>';
function renderCatalogChipsHtml() {
const total = (CATALOG.commands || []).length;
const reportCount = (CATALOG.commands || []).filter(function (c) { return c.produces_report; }).length;
const toolCount = total - reportCount;
const baseChips = [
{ id: 'all', label: 'Alle', count: total },
{ id: 'report', label: 'Rapport-produserende', count: reportCount },
{ id: 'tool', label: 'Verktøy', count: toolCount }
];
const categoryChips = (CATALOG.categories || []).map(function (cat) {
return { id: cat.id, label: cat.label, count: cat.count };
});
return baseChips.concat(categoryChips).map(function (chip) {
const active = catalogFilter === chip.id;
return (
'<div class="expansion" aria-expanded="' + (isOpen ? 'true' : 'false') + '">' +
'<button type="button" class="expansion__head" data-action="catalog-toggle-group" data-group="' + escapeAttr(cat.id) + '">' +
'<span class="expansion__title">' +
'<span class="expansion__title-main">' + escapeHtml(cat.label) + '</span>' +
'<span class="expansion__title-sub">' + cmds.length + ' av ' + cat.count + ' kommandoer' + (q ? ' (filtrert)' : '') + '</span>' +
'</span>' +
'<span class="expansion__chev" aria-hidden="true"></span>' +
'</button>' +
'<div class="expansion__body">' + cardsHtml + '</div>' +
'</div>'
'<button type="button" class="catalog-chip' + (active ? ' catalog-chip--active' : '') + '" data-action="catalog-filter" data-filter="' + escapeAttr(chip.id) + '" aria-pressed="' + (active ? 'true' : 'false') + '">' +
escapeHtml(chip.label) + ' <span class="catalog-chip__count">' + chip.count + '</span>' +
'</button>'
);
}).join('');
}
function renderCatalogListBodyHtml() {
const cmds = filteredCatalogCommands();
if (cmds.length === 0) {
return '<div class="catalog-empty">Ingen treff. Prøv et annet søk eller filter.</div>';
}
return '<div class="catalog-list">' + cmds.map(renderCatalogRowHtml).join('') + '</div>';
}
function renderCatalogSurface() {
const root = getSurfaceEl('catalog');
if (!root) return;
const total = CATALOG.commands.length;
const reportCount = CATALOG.commands.filter(function (c) { return c.produces_report; }).length;
const total = (CATALOG.commands || []).length;
const reportCount = (CATALOG.commands || []).filter(function (c) { return c.produces_report; }).length;
const toolCount = total - reportCount;
const catalogShell = renderPageShell({
eyebrow: 'KATALOG',
title: 'Command-katalog',
lede: 'Alle ' + total + ' kommandoer gruppert på kategori. Bygg pipeline-strenger uten et aktivt prosjekt.',
lede: 'Alle ' + total + ' kommandoer. Søk, filtrer, klikk en rad for å bygge kommandostrengen.',
verdict: 'n-a',
meta: [
total + ' kommandoer',
@ -6958,7 +7014,8 @@
},
'<div class="stack-lg">' +
'<input type="search" class="input catalog-search" placeholder="Søk i kommandoer (id, label, beskrivelse, argument-hint) …" data-catalog-search value="' + escapeAttr(catalogSearchQuery) + '" aria-label="Søk i kommando-katalogen">' +
'<div data-catalog-groups>' + renderCatalogGroupsHtml() + '</div>' +
'<div class="catalog-filter-chips" role="group" aria-label="Filtre">' + renderCatalogChipsHtml() + '</div>' +
'<div data-catalog-list>' + renderCatalogListBodyHtml() + '</div>' +
'</div>'
);
@ -6966,12 +7023,6 @@
renderTopbar('Katalog') +
'<div class="app-shell">' + catalogShell + '</div>'
);
// Bevarer fokus i søkefeltet under re-render
const searchEl = root.querySelector('[data-catalog-search]');
if (searchEl && document.activeElement !== searchEl && catalogSearchQuery) {
// Ikke stjel fokus med mindre brukeren akkurat skrev — håndteres i action handler
}
}
// ============================================================
@ -10406,13 +10457,20 @@
function renderCatalogFormModal(cmd) {
const formHtml = renderCommandForm(cmd.id, { scope: 'cat' });
const sharedCount = (cmd.input_fields || []).filter(function (f) { return f.from === 'shared'; }).length;
const sharedHint = sharedCount > 0
? '<p class="builder-modal__hint">Felt merket <span class="field-from-tag" style="cursor:default;">felles</span> er forhåndsutfylt fra onboarding (' + sharedCount + ' av ' + (cmd.input_fields || []).length + '). Endringer her påvirker ikke onboarding-state.</p>'
: '<p class="builder-modal__hint">Fyll ut argumenter — pipeline-strengen oppdateres mens du skriver.</p>';
return (
'<div class="modal" role="dialog" aria-labelledby="cf-title">' +
'<div class="modal builder-modal" role="dialog" aria-labelledby="cf-title" data-builder-pane>' +
'<div class="modal__head">' +
'<h2 id="cf-title" class="modal__title">' + escapeHtml(cmd.label) + '</h2>' +
'<div>' +
'<h2 id="cf-title" class="modal__title">' + escapeHtml(cmd.label) + ' <span style="font-family: var(--font-family-mono); font-size: var(--font-size-md); color: var(--color-text-tertiary); font-weight: var(--font-weight-regular);">/security:' + escapeHtml(cmd.id) + '</span></h2>' +
'</div>' +
'<button type="button" class="modal__close" data-action="close-modal" aria-label="Lukk">×</button>' +
'</div>' +
'<p style="color: var(--color-text-secondary); margin: 0;">' + escapeHtml(cmd.description) + '</p>' +
'<p class="builder-modal__lede">' + escapeHtml(cmd.description) + '</p>' +
sharedHint +
formHtml +
'</div>'
);
@ -10589,18 +10647,36 @@
}
// Catalog
if (action === 'catalog-toggle-group') {
const grp = target.dataset.group;
const exp = target.closest('.expansion');
if (exp) {
const open = exp.getAttribute('aria-expanded') === 'true';
exp.setAttribute('aria-expanded', open ? 'false' : 'true');
if (action === 'catalog-filter') {
const f = target.dataset.filter || 'all';
if (catalogFilter === f) return;
catalogFilter = f;
// Re-render in-place: chips (active state) + list body
const root = getSurfaceEl('catalog');
if (root) {
const chipsEl = root.querySelector('.catalog-filter-chips');
if (chipsEl) chipsEl.innerHTML = renderCatalogChipsHtml();
const listEl = root.querySelector('[data-catalog-list]');
if (listEl) listEl.innerHTML = renderCatalogListBodyHtml();
}
return;
}
if (action === 'catalog-open-form') {
const cmd = (CATALOG.commands || []).find(function (c) { return c.id === cmdId; });
if (cmd) openModal(renderCatalogFormModal(cmd));
if (!cmd) return;
openModal(renderCatalogFormModal(cmd));
// Initial live-preview: vis pipeline-streng med shared-prefill
const formEl = document.querySelector('[data-builder-pane] form.command-form[data-command-form="' + CSS.escape(cmd.id) + '"]');
if (formEl) {
const data = readCommandFormValues(formEl);
const str = buildCommand(cmd.id, data);
showCommandPreview(formEl, str);
// Auto-fokus første input for keyboard-flow
const firstInput = formEl.querySelector('input:not([type="hidden"]), select, textarea');
if (firstInput) {
try { firstInput.focus(); } catch (e) { /* ignore */ }
}
}
return;
}
@ -10677,14 +10753,24 @@
if (ev.key === 'Escape') closeModal();
});
// Catalog search
// Catalog search + live builder-pane preview
document.addEventListener('input', function (ev) {
if (ev.target && ev.target.matches && ev.target.matches('[data-catalog-search]')) {
catalogSearchQuery = ev.target.value;
const groupsEl = document.querySelector('[data-catalog-groups]');
if (groupsEl) groupsEl.innerHTML = renderCatalogGroupsHtml();
const listEl = document.querySelector('[data-catalog-list]');
if (listEl) listEl.innerHTML = renderCatalogListBodyHtml();
return;
}
// Live preview inside builder-pane (catalog modal)
if (ev.target && ev.target.matches && ev.target.matches('[data-builder-pane] [data-cf-field]')) {
const formEl = ev.target.closest('form.command-form');
if (formEl) {
const data = readCommandFormValues(formEl);
const str = buildCommand(formEl.dataset.commandForm, data);
showCommandPreview(formEl, str);
}
// Fall through to onboarding handling below in case selector overlaps
}
// Onboarding fields persist on input (debounced via throttledWriter)
if (ev.target && ev.target.matches && ev.target.matches('[data-onboarding-field]')) {
const path = ev.target.dataset.cfField;
@ -10716,6 +10802,15 @@
scheduleRender();
}
}
// Builder-pane: select/checkbox change → live preview
if (ev.target && ev.target.matches && ev.target.matches('[data-builder-pane] [data-cf-field]')) {
const formEl = ev.target.closest('form.command-form');
if (formEl) {
const data = readCommandFormValues(formEl);
const str = buildCommand(formEl.dataset.commandForm, data);
showCommandPreview(formEl, str);
}
}
});
// Import file picker