feat(ms-ai-architect): surfaces adopt page-header + key-stats (4 surfaces)
Steg 8 i v1.10.0-loepet. Wrappe alle 4 surfaces (Onboarding, Home, Catalog,
Project) med renderPageShell({eyebrow, title, lede, verdict, keyStats}, body):
- Onboarding: eyebrow ONBOARDING, lede tilpasset for 20-felts onboarding
- Home: dynamisk "Hei, {orgName | venn}", keyStats {PROSJEKTER, AKTIVE RAPPORTER}
- Catalog: keyStats {KOMMANDOER 24, AGENTER 12, SKILLS 5}
- Project: title=project.name, lede=description, verdict via inferProjectVerdict
(block > go-with-conditions > approved > n-a), keyStats {RAPPORTER, SIST OPPDATERT}
Project-surface utvidet med .screen-tabs (A4 Tier 3): Oversikt / Rapporter /
Kontekst / Eksport. Rapporter er primaer (eksisterende category-tabs+panels);
andre skjermer er stub i Sesjon 2 og fylles ut i Sesjon 3-6. Screen-tabs CSS
inline i playground-style-blokk per scope-regel (plugin standalone).
Per R8: ingen .page__meta chips. Action-buttons (Tilbake/Slett) flyttet under
page-shell-headeren (verdict-slot tar ikke arbitrary HTML).
Helpers lagt til:
- inferProjectVerdict(project) — aggregert verdict, tom reports -> n-a
- inferProjectLastUpdated(project) — siste report.updatedAt eller createdAt
- ACTIONS['project-screen'] — toggle screen-tabs uten full re-render
Verify: 4/4 surfaces kaller renderPageShell. Tester: 215 statiske, 240 playground,
7 migrations PASS.
This commit is contained in:
parent
8be04e3a21
commit
6f1631a32f
1 changed files with 221 additions and 60 deletions
|
|
@ -176,6 +176,15 @@
|
|||
.key-stat[data-modifier="high"] .key-stat__value { color: var(--color-severity-high); }
|
||||
.key-stat[data-modifier="medium"] .key-stat__value { color: var(--color-severity-medium); }
|
||||
.key-stat[data-modifier="low"] .key-stat__value { color: var(--color-severity-low); }
|
||||
|
||||
/* Tier 3 A4 — Screen-tabs (segmented). Inline her per scope-regel
|
||||
(plugin standalone). Kandidat for hoisting til shared/ i Sesjon 6. */
|
||||
.screen-tabs { display: flex; gap: var(--space-1); padding: 4px; background: var(--color-bg-soft); border-radius: var(--radius-md); width: fit-content; margin: 0 0 var(--space-4) 0; }
|
||||
.screen-tab { padding: var(--space-2) var(--space-3); font-size: var(--font-size-sm); font-weight: var(--font-weight-medium); background: transparent; border: none; border-radius: var(--radius-sm); cursor: pointer; color: var(--color-text-secondary); font-family: inherit; }
|
||||
.screen-tab:hover { color: var(--color-text-primary); }
|
||||
.screen-tab[aria-current="true"] { background: var(--color-surface); color: var(--color-text-primary); box-shadow: var(--shadow-sm); }
|
||||
.screen { display: none; }
|
||||
.screen[data-active="true"] { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -1565,6 +1574,45 @@
|
|||
return count;
|
||||
}
|
||||
|
||||
// Aggregert verdict for project-surface page-shell. Henter parsed.verdict
|
||||
// fra alle reports og kollapser til én pille: block > go-with-conditions
|
||||
// > approved > n-a. Tom reports{} -> 'n-a' per Sesjon 2-risk-flagg.
|
||||
function inferProjectVerdict(project) {
|
||||
const reports = (project && project.reports) || {};
|
||||
const verdicts = [];
|
||||
for (const k in reports) {
|
||||
const v = reports[k] && reports[k].parsed && reports[k].parsed.verdict;
|
||||
if (v) verdicts.push(String(v).toLowerCase());
|
||||
}
|
||||
if (verdicts.length === 0) return 'n-a';
|
||||
for (let i = 0; i < verdicts.length; i++) {
|
||||
if (verdicts[i] === 'block' || verdicts[i] === 'failed') return 'block';
|
||||
}
|
||||
for (let i = 0; i < verdicts.length; i++) {
|
||||
const v = verdicts[i];
|
||||
if (v === 'go-with-conditions' || v === 'warning') return 'go-with-conditions';
|
||||
}
|
||||
let allGo = true;
|
||||
for (let i = 0; i < verdicts.length; i++) {
|
||||
const v = verdicts[i];
|
||||
if (v !== 'go' && v !== 'approved' && v !== 'allow') { allGo = false; break; }
|
||||
}
|
||||
return allGo ? 'approved' : 'n-a';
|
||||
}
|
||||
|
||||
function inferProjectLastUpdated(project) {
|
||||
const reports = (project && project.reports) || {};
|
||||
let latest = null;
|
||||
for (const k in reports) {
|
||||
const r = reports[k];
|
||||
if (r && r.updatedAt) {
|
||||
if (!latest || r.updatedAt > latest) latest = r.updatedAt;
|
||||
}
|
||||
}
|
||||
const ts = latest || (project && project.createdAt) || '';
|
||||
return ts ? String(ts).slice(0, 10) : '–';
|
||||
}
|
||||
|
||||
function projectMeterBand(filled, total) {
|
||||
if (total === 0) return '4'; // tom = "krever oppmerksomhet"
|
||||
const pct = filled / total;
|
||||
|
|
@ -1647,17 +1695,20 @@
|
|||
})();
|
||||
|
||||
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 = (
|
||||
const activeReportCount = projects.reduce(function (a, p) { return a + projectReportCount(p); }, 0);
|
||||
const homeShell = renderPageShell({
|
||||
eyebrow: 'HJEM',
|
||||
title: 'Hei, ' + (orgName || 'venn'),
|
||||
lede: orgName
|
||||
? 'Velg arbeidsspor eller utforsk eksisterende prosjekter. Felles state er aktiv og forhåndsutfyller skjemaer.'
|
||||
: 'Single-file arkitektur-rådgivning for Microsoft AI-stakken. Start med onboarding for å aktivere felles state.',
|
||||
verdict: 'n-a',
|
||||
keyStats: [
|
||||
{ label: 'PROSJEKTER', value: projects.length },
|
||||
{ label: 'AKTIVE RAPPORTER', value: activeReportCount }
|
||||
]
|
||||
},
|
||||
tracksHtml +
|
||||
'<section class="home-projects">' +
|
||||
'<div class="home-section-head">' +
|
||||
'<h2>Mine prosjekter</h2>' +
|
||||
|
|
@ -1671,9 +1722,7 @@
|
|||
root.innerHTML = (
|
||||
renderTopbar('Hjem') +
|
||||
'<div class="app-shell">' +
|
||||
heroHtml +
|
||||
tracksHtml +
|
||||
projectsSection +
|
||||
homeShell +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
|
@ -1709,6 +1758,11 @@
|
|||
];
|
||||
|
||||
let currentProjectTab = 'regulatory';
|
||||
// Screen-tabs på project-surface (A4 Tier 3): Oversikt / Rapporter /
|
||||
// Kontekst / Eksport. Default 'rapporter' = primær arbeidsflate (eksisterende
|
||||
// category-tabs + panels). Andre skjermer er stub i Sesjon 2 og fylles ut
|
||||
// i senere sesjoner.
|
||||
let currentProjectScreen = 'rapporter';
|
||||
|
||||
function findProject(id) {
|
||||
const list = store.state.projects || [];
|
||||
|
|
@ -1734,6 +1788,7 @@
|
|||
store.state.projects.push(project);
|
||||
store.state.activeProjectId = id;
|
||||
currentProjectTab = 'regulatory';
|
||||
currentProjectScreen = 'rapporter';
|
||||
return project;
|
||||
}
|
||||
|
||||
|
|
@ -1931,30 +1986,36 @@
|
|||
const reportTotal = CATALOG.commands.filter(function (c) { return c.produces_report; }).length;
|
||||
const reportFilled = projectReportCount(project);
|
||||
|
||||
const scenarioChips = (project.scenarios || []).map(function (sid) {
|
||||
const s = SCENARIOS.find(function (x) { return x.id === sid; });
|
||||
return '<span class="project-header__chip">' + escapeHtml(s ? s.name : sid) + '</span>';
|
||||
}).join('');
|
||||
const dateChip = '<span class="project-header__chip">opprettet ' + escapeHtml((project.createdAt || '').slice(0, 10)) + '</span>';
|
||||
const progressChip = '<span class="project-header__chip">' + reportFilled + '/' + reportTotal + ' rapporter</span>';
|
||||
|
||||
const headerHtml = (
|
||||
'<header class="project-header">' +
|
||||
'<div class="project-header__top">' +
|
||||
'<div>' +
|
||||
'<h1 class="project-header__title">' + escapeHtml(project.name) + '</h1>' +
|
||||
(project.description ? '<p style="color: var(--color-text-secondary); margin-top: var(--space-2); max-width: 70ch;">' + escapeHtml(project.description) + '</p>' : '') +
|
||||
'</div>' +
|
||||
'<div style="display:flex; gap: var(--space-2); flex-shrink: 0;">' +
|
||||
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-home">← Tilbake</button>' +
|
||||
'<button type="button" class="btn btn--secondary btn--sm" data-action="delete-project" data-project-id="' + escapeAttr(project.id) + '">Slett</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="project-header__meta">' + dateChip + progressChip + scenarioChips + '</div>' +
|
||||
'</header>'
|
||||
// Action-bar (Tilbake / Slett) flyttet under page-shell-headeren —
|
||||
// page__header har dedikert verdict-slot som ikke tar arbitrary HTML.
|
||||
const actionBar = (
|
||||
'<div class="onboarding-actions" style="justify-content: flex-end; margin-bottom: var(--space-4);">' +
|
||||
'<button type="button" class="btn btn--ghost btn--sm" data-action="goto-home">← Tilbake</button>' +
|
||||
'<button type="button" class="btn btn--secondary btn--sm" data-action="delete-project" data-project-id="' + escapeAttr(project.id) + '">Slett</button>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
// Tabs per CATALOG.categories
|
||||
// Screen-tabs (A4 Tier 3): Oversikt / Rapporter / Kontekst / Eksport.
|
||||
// Sesjon 2: Rapporter er primær; andre er stub-skjermer som fylles ut
|
||||
// i Sesjon 3-6.
|
||||
const SCREENS = [
|
||||
{ id: 'oversikt', label: 'Oversikt' },
|
||||
{ id: 'rapporter', label: 'Rapporter' },
|
||||
{ id: 'kontekst', label: 'Kontekst' },
|
||||
{ id: 'eksport', label: 'Eksport' }
|
||||
];
|
||||
const screenTabsHtml = '<nav class="screen-tabs" role="tablist" aria-label="Prosjekt-skjermer">' + SCREENS.map(function (s) {
|
||||
const isActive = currentProjectScreen === s.id;
|
||||
return (
|
||||
'<button type="button" class="screen-tab" role="tab"' +
|
||||
' aria-current="' + (isActive ? 'true' : 'false') + '"' +
|
||||
' data-action="project-screen" data-screen="' + escapeAttr(s.id) + '">' +
|
||||
escapeHtml(s.label) +
|
||||
'</button>'
|
||||
);
|
||||
}).join('') + '</nav>';
|
||||
|
||||
// Tabs per CATALOG.categories — kun synlig under "Rapporter"-skjermen.
|
||||
const tabsHtml = '<div class="project-tabs" role="tablist">' + CATALOG.categories.map(function (cat) {
|
||||
const isActive = currentProjectTab === cat.id;
|
||||
return (
|
||||
|
|
@ -1982,12 +2043,79 @@
|
|||
);
|
||||
}).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="screen" data-active="' + (currentProjectScreen === 'oversikt' ? 'true' : 'false') + '" data-screen-id="oversikt">' +
|
||||
'<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>' +
|
||||
(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>Sesjon 3+: aggregerte verdict-pillen, recommended-next-actions og top-risks vises her.</em></p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
const rapporterHtml = (
|
||||
'<div class="screen" data-active="' + (currentProjectScreen === 'rapporter' ? 'true' : 'false') + '" data-screen-id="rapporter">' +
|
||||
tabsHtml +
|
||||
panelsHtml +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
const kontekstHtml = (
|
||||
'<div class="screen" data-active="' + (currentProjectScreen === 'kontekst' ? 'true' : 'false') + '" data-screen-id="kontekst">' +
|
||||
'<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>Sesjon 3+: snapshot av de 20 fellesfeltene og hva som er prefilled per command vises her.</em></p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
const eksportHtml = (
|
||||
'<div class="screen" data-active="' + (currentProjectScreen === 'eksport' ? 'true' : 'false') + '" data-screen-id="eksport">' +
|
||||
'<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 topbar for hele state. Per-prosjekt eksport (PDF/Markdown) kommer i Sesjon 6.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
const projectShell = renderPageShell({
|
||||
eyebrow: 'PROSJEKT',
|
||||
title: project.name,
|
||||
lede: project.description || '',
|
||||
verdict: inferProjectVerdict(project),
|
||||
keyStats: [
|
||||
{ label: 'RAPPORTER', value: reportFilled + '/' + reportTotal },
|
||||
{ label: 'SIST OPPDATERT', value: inferProjectLastUpdated(project) }
|
||||
]
|
||||
},
|
||||
actionBar +
|
||||
screenTabsHtml +
|
||||
oversiktHtml +
|
||||
rapporterHtml +
|
||||
kontekstHtml +
|
||||
eksportHtml
|
||||
);
|
||||
|
||||
root.innerHTML = (
|
||||
renderTopbar('Prosjekt: ' + escapeHtml(project.name)) +
|
||||
'<div class="app-shell app-shell--wide">' +
|
||||
headerHtml +
|
||||
tabsHtml +
|
||||
panelsHtml +
|
||||
projectShell +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
|
@ -2092,18 +2220,27 @@
|
|||
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) + '»' : '');
|
||||
const catalogShell = renderPageShell({
|
||||
eyebrow: 'KATALOG',
|
||||
title: 'Command-katalog',
|
||||
lede: '24 kommandoer i 5 fagområder. Filtrer for å finne det du trenger, åpne skjema for å bygge en pipeline-streng.',
|
||||
verdict: 'n-a',
|
||||
keyStats: [
|
||||
{ label: 'KOMMANDOER', value: 24 },
|
||||
{ label: 'AGENTER', value: 12 },
|
||||
{ label: 'SKILLS', value: 5 }
|
||||
]
|
||||
},
|
||||
'<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>'
|
||||
);
|
||||
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>' +
|
||||
catalogShell +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
|
@ -3873,20 +4010,26 @@
|
|||
'</div>'
|
||||
);
|
||||
|
||||
const onboardingShell = renderPageShell({
|
||||
eyebrow: 'ONBOARDING',
|
||||
title: 'Bli kjent med oss',
|
||||
lede: 'Oppgi virksomhetskontekst slik at vi kan tilpasse arkitekturråd til din situasjon. 20 felles felter gjenbrukes på tvers av alle commands.',
|
||||
verdict: 'n-a',
|
||||
keyStats: []
|
||||
},
|
||||
'<div class="onboarding-layout">' +
|
||||
sidebar +
|
||||
'<div class="onboarding-main">' +
|
||||
errorSummary +
|
||||
'<div class="onboarding-groups">' + groupsHtml + '</div>' +
|
||||
actionBar +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
root.innerHTML = (
|
||||
'<div class="app-shell">' +
|
||||
'<div class="onboarding-layout">' +
|
||||
sidebar +
|
||||
'<div class="onboarding-main">' +
|
||||
'<header class="onboarding-header">' +
|
||||
'<h1>Velkommen til ms-ai-architect</h1>' +
|
||||
'<p>Fyll inn 20 felles felter — de gjenbrukes på tvers av alle commands og forhåndsutfyller skjemaer per prosjekt.</p>' +
|
||||
'</header>' +
|
||||
errorSummary +
|
||||
'<div class="onboarding-groups">' + groupsHtml + '</div>' +
|
||||
actionBar +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
onboardingShell +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
|
@ -4137,6 +4280,24 @@
|
|||
});
|
||||
};
|
||||
|
||||
ACTIONS['project-screen'] = function (ev, el) {
|
||||
const screen = el.dataset.screen;
|
||||
if (!screen) return;
|
||||
currentProjectScreen = screen;
|
||||
// Toggle aria-current på screen-tabs + data-active på .screen-paneler
|
||||
// uten full re-render (bevarer evt textarea-input i panels).
|
||||
const root = getSurfaceEl('project');
|
||||
if (!root) return;
|
||||
const tabs = root.querySelectorAll('.screen-tab');
|
||||
tabs.forEach(function (t) {
|
||||
t.setAttribute('aria-current', t.dataset.screen === screen ? 'true' : 'false');
|
||||
});
|
||||
const screens = root.querySelectorAll('.screen[data-screen-id]');
|
||||
screens.forEach(function (s) {
|
||||
s.setAttribute('data-active', s.dataset.screenId === screen ? 'true' : 'false');
|
||||
});
|
||||
};
|
||||
|
||||
ACTIONS['parse'] = function (ev, el) {
|
||||
const commandId = el.dataset.command;
|
||||
if (!commandId) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue