feat(llm-security): playground v7.6.2-dev — prosjekt-surface opprydding + topbar-splitt [skip-docs]

- renderCommandSubCard: collapsed-by-default + click-to-expand uten remount
- renderProjectSurface: stub-screens (Oversikt/Kontekst/Eksport) fjernet, kun Rapporter-tab
- renderTopbar: split-pattern (primær nav venstre / state-IO høyre)
This commit is contained in:
Kjell Tore Guttormsen 2026-05-18 12:23:57 +02:00
commit 86d6ecdc50

View file

@ -78,6 +78,55 @@
.command-cards { display: flex; flex-direction: column; gap: var(--space-4); }
.sub-zone { border-top: 1px solid var(--color-border-subtle); padding-top: var(--space-3); }
.sub-zone__heading { font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-tertiary); margin: 0 0 var(--space-2); }
/* Collapsible command sub-cards (Rapporter-tab) */
.command-subcard { padding: 0; overflow: hidden; }
.command-subcard .card__head--toggle {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
width: 100%;
margin: 0;
padding: var(--space-4) var(--space-5);
background: transparent;
border: 0;
cursor: pointer;
text-align: left;
font-family: inherit;
color: inherit;
}
.command-subcard .card__head--toggle:hover { background: var(--color-bg-soft); }
.command-subcard .card__head--toggle:focus-visible { outline: 2px solid var(--color-primary-500); outline-offset: -2px; }
.command-subcard .card__head-text { flex: 1; min-width: 0; }
.command-subcard .card__head-meta { display: flex; flex-direction: column; gap: 6px; align-items: flex-end; flex-shrink: 0; }
.command-subcard .subcard-chev {
display: inline-block;
font-size: 0.875rem;
color: var(--color-text-tertiary);
transform: rotate(-90deg);
transition: transform 0.15s ease;
align-self: center;
flex-shrink: 0;
width: 1em;
text-align: center;
}
.command-subcard .card__head--toggle[aria-expanded="true"] .subcard-chev { transform: rotate(0deg); }
.command-subcard .subcard-body {
padding: 0 var(--space-5) var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
/* App header — split nav groups */
.app-header__nav-group { display: flex; align-items: center; gap: var(--space-2); }
.app-header__nav-sep {
width: 1px;
align-self: stretch;
background: var(--color-border-subtle);
margin: 0 var(--space-2);
}
.paste-import-row { display: flex; flex-direction: column; gap: var(--space-2); }
.paste-import-row__actions { display: flex; gap: var(--space-2); align-items: center; }
.form-zone-placeholder { padding: var(--space-3); background: var(--color-bg-soft); border-radius: var(--radius-sm); font-size: var(--font-size-sm); color: var(--color-text-tertiary); font-style: italic; }
@ -6532,15 +6581,20 @@
breadcrumbHtml +
'<div class="app-header__spacer"></div>' +
'<div class="app-header__actions" role="group" aria-label="Hovednavigasjon">' +
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-home">Hjem</button>' +
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-catalog">Katalog</button>' +
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-onboarding">Re-onboard</button>' +
'<button type="button" class="btn btn--secondary btn--sm" data-action="export-state" aria-label="Eksporter state til JSON">Eksporter</button>' +
'<button type="button" class="btn btn--secondary btn--sm" data-action="import-state" aria-label="Importer state fra JSON">Importer</button>' +
'<input type="file" accept="application/json,.json" data-import-input hidden>' +
'<button type="button" class="theme-toggle" data-action="toggle-theme" aria-label="Bytt til ' + themeNext + ' modus">' +
'<span data-theme-label>' + themeLabel + '</span>' +
'</button>' +
'<div class="app-header__nav-group" role="group" aria-label="Primær navigasjon">' +
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-home">Hjem</button>' +
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-catalog">Katalog</button>' +
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-onboarding">Re-onboard</button>' +
'</div>' +
'<span class="app-header__nav-sep" aria-hidden="true"></span>' +
'<div class="app-header__nav-group" role="group" aria-label="State og tema">' +
'<button type="button" class="btn btn--secondary btn--sm" data-action="export-state" aria-label="Eksporter state til JSON">Eksporter</button>' +
'<button type="button" class="btn btn--secondary btn--sm" data-action="import-state" aria-label="Importer state fra JSON">Importer</button>' +
'<input type="file" accept="application/json,.json" data-import-input hidden>' +
'<button type="button" class="theme-toggle" data-action="toggle-theme" aria-label="Bytt til ' + themeNext + ' modus">' +
'<span data-theme-label>' + themeLabel + '</span>' +
'</button>' +
'</div>' +
'</div>' +
'</header>'
);
@ -7029,12 +7083,19 @@
// PROJECT SURFACE (stub i Fase 1 — full report-render i Fase 2/3)
// ============================================================
let currentProjectTab = 'discover';
let currentProjectScreen = 'rapporter';
// Tracks which sub-cards are expanded — key: projectId + '::' + cmdId.
// Persists across re-renders so paste-import etc. doesn't collapse them.
const expandedSubcards = new Set();
function subcardKey(projectId, cmdId) { return projectId + '::' + cmdId; }
function renderCommandSubCard(cmd, projectId) {
const project = findProject(projectId);
const report = project && project.reports && project.reports[cmd.id];
const hasReport = !!(report && report.parsed);
const isExpanded = expandedSubcards.has(subcardKey(projectId, cmd.id));
const bodyId = 'subcard-body-' + cmd.id.replace(/[^a-zA-Z0-9_-]/g, '_');
const formZone = (
'<div class="sub-zone">' +
@ -7075,23 +7136,26 @@
}
return (
'<article class="card" data-command-subcard data-command-id="' + escapeAttr(cmd.id) + '">' +
'<div class="card__head">' +
'<div>' +
'<article class="card command-subcard" data-command-subcard data-command-id="' + escapeAttr(cmd.id) + '">' +
'<button type="button" class="card__head card__head--toggle" data-action="toggle-subcard" data-command="' + escapeAttr(cmd.id) + '" data-project-id="' + escapeAttr(projectId) + '" aria-expanded="' + (isExpanded ? 'true' : 'false') + '" aria-controls="' + escapeAttr(bodyId) + '">' +
'<div class="card__head-text">' +
'<h3 class="card__title">' + escapeHtml(cmd.label) + '</h3>' +
'<p class="card__desc">' + escapeHtml(cmd.description) + '</p>' +
'</div>' +
'<div style="display:flex; flex-direction:column; gap:6px; align-items:flex-end;">' +
'<div class="card__head-meta">' +
'<span class="badge badge--scope-security">llm-security</span>' +
(cmd.produces_report
? '<span class="card__pill">' + (hasReport ? '✓ Rapport' : 'Rapport') + '</span>'
: '<span class="card__pill">Verktøy</span>'
) +
'</div>' +
'<span class="subcard-chev" aria-hidden="true"></span>' +
'</button>' +
'<div class="subcard-body" id="' + escapeAttr(bodyId) + '"' + (isExpanded ? '' : ' hidden') + '>' +
formZone +
pasteZone +
reportZone +
'</div>' +
formZone +
pasteZone +
reportZone +
'</article>'
);
}
@ -7112,17 +7176,6 @@
'</div>'
);
const SCREENS = [
{ id: 'oversikt', label: 'Oversikt' },
{ id: 'rapporter', label: 'Rapporter' },
{ id: 'kontekst', label: 'Kontekst' },
{ id: 'eksport', label: 'Eksport' }
];
const screenTabsHtml = '<nav class="tab-list" role="tablist" aria-label="Prosjekt-skjermer">' + SCREENS.map(function (s) {
const isActive = currentProjectScreen === s.id;
return '<button type="button" class="tab" role="tab" aria-current="' + (isActive ? 'true' : 'false') + '" data-action="project-screen" data-screen="' + escapeAttr(s.id) + '">' + escapeHtml(s.label) + '</button>';
}).join('') + '</nav>';
const tabsHtml = '<div class="project-tabs" role="tablist">' + CATALOG.categories.map(function (cat) {
const isActive = currentProjectTab === cat.id;
return '<button type="button" class="project-tab" role="tab"' + (isActive ? ' aria-current="true"' : '') + ' data-action="project-tab" data-tab="' + escapeAttr(cat.id) + '">' + escapeHtml(cat.label) + '<span class="project-tab__count">' + cat.count + '</span></button>';
@ -7134,53 +7187,6 @@
return '<div class="command-cards" role="tabpanel" data-tab-panel="' + escapeAttr(cat.id) + '"' + (isActive ? '' : ' hidden') + '>' + cards + '</div>';
}).join('');
const scenarioChipsList = (project.scenarios || []).map(function (sid) {
const s = SCENARIOS.find(function (x) { return x.id === sid; });
return '<li>' + escapeHtml(s ? s.name : sid) + '</li>';
}).join('');
const oversiktHtml = (
'<div class="tab-panel" data-screen-id="oversikt"' + (currentProjectScreen === 'oversikt' ? '' : ' hidden') + '>' +
'<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">Oversikt</h3>' +
'<p class="guide-panel__text">Opprettet ' + escapeHtml((project.createdAt || '').slice(0, 10)) + '. ' + reportFilled + ' av ' + reportTotal + ' rapporter generert.</p>' +
'<p class="guide-panel__text" style="margin-top: var(--space-2);">Target: <code>' + escapeHtml(project.target_path || '—') + '</code> (<em>' + escapeHtml(project.target_type || 'codebase') + '</em>)</p>' +
(scenarioChipsList ? '<p class="guide-panel__text" style="margin-top: var(--space-2);"><strong>Scenarioer:</strong></p><ul style="margin: 0; padding-left: var(--space-4); color: var(--color-text-secondary);">' + scenarioChipsList + '</ul>' : '') +
'<p class="guide-panel__text" style="margin-top: var(--space-3);"><em>Fase 2-3: aggregert verdict-pille, top-funn på tvers av rapporter, og recommended-next-actions vises her.</em></p>' +
'</div>' +
'</div>' +
'</div>'
);
const rapporterHtml = '<div class="tab-panel" data-screen-id="rapporter"' + (currentProjectScreen === 'rapporter' ? '' : ' hidden') + '>' + tabsHtml + panelsHtml + '</div>';
const kontekstHtml = (
'<div class="tab-panel" data-screen-id="kontekst"' + (currentProjectScreen === 'kontekst' ? '' : ' hidden') + '>' +
'<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">Kontekst</h3>' +
'<p class="guide-panel__text">Fellesfeltene fra onboarding gjenbrukes automatisk i alle command-skjemaer. Bruk <button type="button" class="btn btn--ghost btn--sm" data-action="goto-onboarding" style="display:inline;">Re-onboard</button> for å oppdatere.</p>' +
'<p class="guide-panel__text" style="margin-top: var(--space-2);"><em>Fase 2-3: snapshot av de 5 fellesgruppene og hvilke felt som prefilles per kommando vises her.</em></p>' +
'</div>' +
'</div>' +
'</div>'
);
const eksportHtml = (
'<div class="tab-panel" data-screen-id="eksport"' + (currentProjectScreen === 'eksport' ? '' : ' hidden') + '>' +
'<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">Eksport</h3>' +
'<p class="guide-panel__text">Bruk <strong>Eksporter</strong> i toppmenyen for hele state. Per-prosjekt PDF/Markdown-eksport kommer i Fase 3.</p>' +
'</div>' +
'</div>' +
'</div>'
);
const projectShell = renderPageShell({
eyebrow: 'PROSJEKT · ' + escapeHtml((project.target_type || 'codebase').toUpperCase()),
title: project.name,
@ -7197,7 +7203,7 @@
{ label: 'TARGET', value: (project.target_type || 'codebase') }
]
},
'<div class="stack-lg">' + actionBar + screenTabsHtml + oversiktHtml + rapporterHtml + kontekstHtml + eksportHtml + '</div>'
'<div class="stack-lg">' + actionBar + tabsHtml + panelsHtml + '</div>'
);
root.innerHTML = renderTopbar('Prosjekt: ' + escapeHtml(project.name)) +
@ -10572,22 +10578,37 @@
}
// Project tabs
if (action === 'project-screen') {
currentProjectScreen = target.dataset.screen;
scheduleRender();
return;
}
if (action === 'project-tab') {
currentProjectTab = target.dataset.tab;
scheduleRender();
return;
}
// Sub-card toggle (Rapporter-tab) — direct DOM manipulation to preserve form-field state
if (action === 'toggle-subcard') {
const cmdId = target.dataset.command;
const projectId = target.dataset.projectId;
const article = target.closest('[data-command-subcard]');
const body = article ? article.querySelector('.subcard-body') : null;
if (!body) return;
const key = projectId + '::' + cmdId;
const willExpand = body.hasAttribute('hidden');
if (willExpand) {
expandedSubcards.add(key);
body.removeAttribute('hidden');
target.setAttribute('aria-expanded', 'true');
} else {
expandedSubcards.delete(key);
body.setAttribute('hidden', '');
target.setAttribute('aria-expanded', 'false');
}
return;
}
// Project lifecycle
if (action === 'open-project') {
const pid = target.dataset.projectId;
store.state.activeProjectId = pid;
currentProjectScreen = 'rapporter';
currentProjectTab = 'discover';
navigate('project');
return;
@ -10633,7 +10654,6 @@
};
store.state.projects.push(project);
store.state.activeProjectId = project.id;
currentProjectScreen = 'rapporter';
currentProjectTab = 'discover';
closeModal();
navigate('project');