feat(ms-ai-architect): playground v3 home surface + project list [skip-docs]

Step 6/17 av Playground v3-leveransen (Session 2, Wave 2).

Hjem-skjerm med 3-track entry-pattern (.tracks__card--guided/explore/expert):
  - Onboard / Re-onboard
  - Nytt prosjekt
  - Command-katalog

Prosjekt-liste under tracks: .fleet-grid med .fleet-tile per prosjekt
(navn + scenario-chip + meter med rapport-fremdrift). Tom-state vises
som .guide-panel--info med 'Opprett første prosjekt'-knapp.

Topbar (renderTopbar) med brand + nav + eksport/import-knapper synlig
på home/catalog/project. Onboarding holdes uten topbar for full-fokus
første-flyt. import-input change-handler ruter via window.__importState
fra Step 3 og kjører scheduleRender etter import.

Verifisert via vm sandbox:
  - 21 tracks__card-treff (3 cards med modifier-klasser)
  - guided/explore/expert-modifiers alle til stede
  - empty-state guide-panel--info når projects=[]
  - fleet-grid suppressed når projects=[]

Stub-actions for new-project (Step 7 erstatter med modal-åpning).
README/CLAUDE.md-update deferred til Step 17 (Session 5).
This commit is contained in:
Kjell Tore Guttormsen 2026-05-03 18:19:22 +02:00
commit ff99a51d1d

View file

@ -1179,22 +1179,174 @@
scheduleRender();
}
// Stubber for surfaces som fylles i Steps 6, 7, 9. Holder renderActive
// total uten å kreve at de finnes.
// Topbar — gjenbrukes på home, catalog, project. Onboarding viser ingen topbar
// (full-fokus førstegangs-flyt). Eksport/import-knapper wires opp til
// __exportState/__importState fra Step 3.
function renderTopbar(crumb) {
const orgName = (store.state.shared.organization && store.state.shared.organization.name) || '';
const crumbHtml = (orgName || crumb)
? '<span class="topbar__crumb">' + (orgName ? escapeHtml(orgName) : '') + (orgName && crumb ? ' · ' : '') + (crumb || '') + '</span>'
: '';
return (
'<header class="topbar">' +
'<div class="topbar__brand">' +
'<span class="topbar__brand-mark" aria-hidden="true">M</span>' +
'<span>ms-ai-architect</span>' +
crumbHtml +
'</div>' +
'<nav class="topbar__nav" 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>' +
'</nav>' +
'</header>'
);
}
// ============================================================
// HOME SURFACE (Step 6)
// ============================================================
//
// 3 entry-tracks (.tracks med .tracks__card--guided/explore/expert) som
// første-valg på home. Under: prosjekt-liste i .fleet-grid med .fleet-tile
// per prosjekt. Tom-state: .guide-panel--info. "Nytt prosjekt"-knapp
// åpner modal (modal-handler i Step 7 — Step 6 har stub).
function projectReportCount(p) {
if (!p || !p.reports) return 0;
let count = 0;
for (const k in p.reports) {
if (p.reports[k] && p.reports[k].parsed) count++;
}
return count;
}
function projectMeterBand(filled, total) {
if (total === 0) return '4'; // tom = "krever oppmerksomhet"
const pct = filled / total;
if (pct >= 0.8) return '1';
if (pct >= 0.5) return '2';
if (pct >= 0.2) return '3';
return '4';
}
function renderHomeSurface() {
const root = getSurfaceEl('home');
if (!root) return;
root.innerHTML = '<div class="app-shell"><p>Hjem-skjerm fylles i Step 6.</p></div>';
const projects = store.state.projects || [];
const reportTotal = CATALOG.commands.filter(function (c) { return c.produces_report; }).length;
const tracksHtml = (
'<div class="tracks">' +
'<button type="button" class="tracks__card tracks__card--guided" data-action="goto-onboarding">' +
'<span class="tracks__card-icon" aria-hidden="true">⚙︎</span>' +
'<h3 class="tracks__card-title">Onboard / Re-onboard</h3>' +
'<p class="tracks__card-desc">Oppdater de 18 felles feltene som forhåndsutfyller alle command-skjemaer.</p>' +
'<span class="tracks__card-meta"><span>Felles state</span><span class="tracks__card-cta">Åpne →</span></span>' +
'</button>' +
'<button type="button" class="tracks__card tracks__card--explore" data-action="new-project">' +
'<span class="tracks__card-icon" aria-hidden="true"></span>' +
'<h3 class="tracks__card-title">Nytt prosjekt</h3>' +
'<p class="tracks__card-desc">Start et nytt arkitektur-prosjekt. Hvert prosjekt holder sine egne ROS, DPIA, AI Act-klassifisering osv.</p>' +
'<span class="tracks__card-meta"><span>Per-prosjekt state</span><span class="tracks__card-cta">Opprett →</span></span>' +
'</button>' +
'<button type="button" class="tracks__card tracks__card--expert" data-action="goto-catalog">' +
'<span class="tracks__card-icon" aria-hidden="true"></span>' +
'<h3 class="tracks__card-title">Command-katalog</h3>' +
'<p class="tracks__card-desc">Bla i alle 24 commands gruppert på kategori. Generer pipeline-strenger uten et prosjekt.</p>' +
'<span class="tracks__card-meta"><span>' + CATALOG.commands.length + ' commands</span><span class="tracks__card-cta">Bla →</span></span>' +
'</button>' +
'</div>'
);
const projectListHtml = (function () {
if (projects.length === 0) {
return (
'<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">Du har ingen prosjekter ennå</h3>' +
'<p class="guide-panel__text">Opprett ditt første for å starte ROS-, DPIA- og AI Act-arbeid. Felles felter du fylte ut i onboarding gjenbrukes automatisk.</p>' +
'<div class="guide-panel__action">' +
'<button type="button" class="btn btn--primary" data-action="new-project">Opprett første prosjekt</button>' +
'</div>' +
'</div>' +
'</div>'
);
}
const tiles = projects.map(function (p) {
const filled = projectReportCount(p);
const band = projectMeterBand(filled, reportTotal);
const pct = reportTotal ? Math.round(100 * filled / reportTotal) : 0;
const scenarios = Array.isArray(p.scenarios) ? p.scenarios : [];
const chip = scenarios.length > 0
? '<span class="fleet-tile__chip">' + escapeHtml(scenarios[0]) + (scenarios.length > 1 ? ' +' + (scenarios.length - 1) : '') + '</span>'
: '<span class="fleet-tile__chip">Uten scenario</span>';
return (
'<button type="button" class="fleet-tile" data-action="open-project" data-project-id="' + escapeAttr(p.id) + '">' +
'<div class="fleet-tile__row">' +
'<span class="fleet-tile__name" title="' + escapeAttr(p.name) + '">' + escapeHtml(p.name) + '</span>' +
chip +
'</div>' +
'<div class="fleet-tile__meter" aria-label="Rapport-fremdrift">' +
'<span class="fleet-tile__meter-fill" data-band="' + band + '" style="width:' + Math.max(pct, 4) + '%"></span>' +
'</div>' +
'<div class="fleet-tile__meta">' +
'<span>' + filled + '/' + reportTotal + ' rapporter</span>' +
'<span class="fleet-tile__trend--stable">' + pct + '%</span>' +
'</div>' +
'</button>'
);
}).join('');
return '<div class="fleet-grid">' + tiles + '</div>';
})();
const orgName = (store.state.shared.organization && store.state.shared.organization.name) || '';
const heroHtml = (
'<section class="home-hero">' +
'<h1>' + (orgName ? 'Hei, ' + escapeHtml(orgName) : 'ms-ai-architect') + '</h1>' +
'<p>' + (orgName
? 'Velg hvor du vil starte. Felles state er aktiv og forhåndsutfyller skjemaer.'
: 'Single-file arkitektur-rådgivning for Microsoft AI-stakken. Start med onboarding for å aktivere felles state.'
) + '</p>' +
'</section>'
);
const projectsSection = (
'<section class="home-projects">' +
'<div class="home-section-head">' +
'<h2>Mine prosjekter</h2>' +
'<span class="home-section-meta">' + projects.length + ' prosjekt' + (projects.length === 1 ? '' : 'er') + ' · maks ' + reportTotal + ' rapporter per prosjekt</span>' +
'</div>' +
projectListHtml +
(projects.length > 0 ? '<div class="onboarding-actions" style="margin-top: var(--space-4);"><button type="button" class="btn btn--primary" data-action="new-project">Nytt prosjekt</button></div>' : '') +
'</section>'
);
root.innerHTML = (
renderTopbar('Hjem') +
'<div class="app-shell">' +
heroHtml +
tracksHtml +
projectsSection +
'</div>'
);
}
// Stub for project + catalog som fylles i Steps 7, 9.
function renderProjectSurface() {
const root = getSurfaceEl('project');
if (!root) return;
root.innerHTML = '<div class="app-shell"><p>Prosjekt-overflate fylles i Step 7.</p></div>';
root.innerHTML = renderTopbar('Prosjekt') + '<div class="app-shell"><p>Prosjekt-detaljvisning fylles i Step 7.</p></div>';
}
function renderCatalogStub() {
const root = getSurfaceEl('catalog');
if (!root) return;
root.innerHTML = '<div class="app-shell"><p>Command-katalog fylles i Step 9.</p></div>';
root.innerHTML = renderTopbar('Katalog') + '<div class="app-shell"><p>Command-katalog fylles i Step 9.</p></div>';
}
// ============================================================
@ -1593,6 +1745,57 @@
if (input) input.focus();
};
// ============================================================
// NAV + EXPORT/IMPORT ACTIONS (Step 6)
// ============================================================
ACTIONS['goto-home'] = function () { navigate('home'); };
ACTIONS['goto-catalog'] = function () { navigate('catalog'); };
ACTIONS['goto-onboarding'] = function () { navigate('onboarding'); };
ACTIONS['open-project'] = function (ev, el) {
const id = el.dataset.projectId;
if (!id) return;
store.state.activeProjectId = id;
navigate('project');
};
// Stub — Step 7 erstatter med modal-åpning.
ACTIONS['new-project'] = function () {
console.log('[playground v3] new-project: modal kommer i Step 7');
};
ACTIONS['export-state'] = function () {
try { exportState(); }
catch (err) { console.error('[playground v3] export feilet:', err); alert('Eksport feilet: ' + err.message); }
};
ACTIONS['import-state'] = function (ev, el) {
const topbar = el.closest('.topbar');
if (!topbar) return;
const input = topbar.querySelector('[data-import-input]');
if (!input) return;
input.value = ''; // tillat samme fil to ganger
input.click();
};
// File-input change handler (én gang for hele dokumentet — input genereres
// fortløpende via renderTopbar, men endringen bobler).
document.addEventListener('change', function (ev) {
if (!ev.target.matches || !ev.target.matches('[data-import-input]')) return;
const file = ev.target.files && ev.target.files[0];
if (!file) return;
importState(file)
.then(function () {
scheduleRender();
alert('Import fullført. Nåværende state er erstattet av filens innhold.');
})
.catch(function (err) {
console.error('[playground v3] import feilet:', err);
alert('Import feilet: ' + err.message);
});
});
// Eksponer for Verify-asserts og Steps 6-9.
window.__navigate = navigate;
window.__scheduleRender = scheduleRender;