Two changes in one commit because they were prepared together and the component demos depend on the new self-hosted fonts.css. Tier 3 wave 2 — 12 new components --------------------------------- Adds components-tier3-supplement.css (886 lines) and 12 isolated demo HTML pages under shared/playground-examples/components/: toxic-flow chain, fleet-overview, kanban Keep/Review/Remove, maturity-ladder, classify-and-transform, cycle-ribbon, persistent-antipattern, suppressed-signals, ExpansionCard, ReadMore, FormProgress, Aspirational-vs-Committed. Reuses existing tokens — no new CSS custom properties. Honors the Phase 1 feedback rules: no large pink areas for body text, severity-red distinct from failure-red, dark mode via existing [data-theme="dark"]. Provenance: components-tier3-supplement.css and the 12 demo bodies were authored by claude.ai/design (separate Anthropic instance) on 2026-05-03. This commit only integrates them — path rewrites, font swap, generic name substitution in fleet-overview demo data, README updates. base.css from the export was deliberately NOT taken in because it reverted the inline-message contrast fix from v0.1. Self-hosted fonts (Inter, JetBrains Mono, Source Serif 4) --------------------------------------------------------- Replaces all fonts.googleapis.com / fonts.gstatic.com requests with .woff2 files bundled at shared/playground-design-system/fonts/. Why: - No data leaked to Google about end-user IPs and User-Agents. - GDPR-safe for Norwegian public-sector deployments. - Works offline / behind air-gapped firewalls. - Forkers downloading the marketplace get a complete bundle. All three families are SIL Open Font License 1.1 — license texts included alongside the woff2 files. Source Serif 4 woff2 generated locally from the upstream OTF release using fonttools ttLib.woff2 compress; Inter and JetBrains Mono are unmodified upstream webfont releases. Total bundle: 9 woff2 files, ~940 KB. New fonts.css declares all @font-face rules with font-display: swap. All 6 example HTMLs and 12 new component demos load it via a single relative path. Verified -------- - Privacy grep returns empty across plugins/ and shared/ - Google Fonts grep returns empty across shared/*.html - Smoke test via python -m http.server: HTML + 7 stylesheets + Inter-Regular.woff2 all return 200 Doc updates ----------- - shared/playground-design-system/README.md: file tree updated, Quick start snippet shows fonts.css link, "Self-hosted fonts" section added - shared/playground-design-system/fonts/LICENSES.md: combined attribution - README.md (root): Tier 3 wave 1+2 component list, Privacy-first bullet - CLAUDE.md (root): tree entry expanded for new components + fonts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
144 lines
7 KiB
HTML
144 lines
7 KiB
HTML
<!doctype html>
|
|
<html lang="nb">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Kanban · Keep/Review/Remove · Tier 3 supp</title>
|
|
<link rel="stylesheet" href="../../playground-design-system/tokens.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/base.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/components.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/components-tier2.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/components-tier3-supplement.css" />
|
|
<link rel="stylesheet" href="../../playground-design-system/fonts.css" />
|
|
<style>
|
|
.modal-bg { position: fixed; inset: 0; background: var(--color-overlay); display: none; align-items: center; justify-content: center; z-index: 100; padding: var(--space-4); }
|
|
.modal-bg[data-open="true"] { display: flex; }
|
|
.modal { background: var(--color-surface); border-radius: var(--radius-lg); padding: var(--space-5); max-width: 540px; width: 100%; box-shadow: var(--shadow-lg); max-height: 90vh; overflow: auto; }
|
|
.checklist { list-style: none; padding: 0; margin: var(--space-3) 0; display: flex; flex-direction: column; gap: 6px; }
|
|
.checklist li { display: flex; gap: 8px; font-size: var(--font-size-sm); }
|
|
.checklist .ok { color: var(--color-state-success); }
|
|
.checklist .no { color: var(--color-severity-critical); }
|
|
.checklist .un { color: var(--color-text-tertiary); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="app-header">
|
|
<a href="../index.html" class="app-header__brand"><span class="app-header__brand-mark">P</span><span>Playground</span></a>
|
|
<span class="app-header__breadcrumb">/ Komponenter / Kanban: Keep/Review/Remove</span>
|
|
</header>
|
|
|
|
<main class="container container--wide" style="padding: var(--space-8) 0;">
|
|
<div style="margin-bottom: var(--space-6);">
|
|
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-scope-security); font-weight: var(--font-weight-semibold);">llm-security · /security plugin-audit</span>
|
|
<h1 style="margin: 4px 0 6px;">Kanban: Behold / Vurder / Fjern</h1>
|
|
<p class="text-secondary" style="max-width: 65ch;">Klassifisér installerte plugins/MCP-servere etter trust. Klikk-flytt mellom kolonner.</p>
|
|
</div>
|
|
|
|
<div class="kanban-board" id="board"></div>
|
|
</main>
|
|
|
|
<div class="modal-bg" id="modal" onclick="if(event.target===this) closeModal()">
|
|
<div class="modal" id="modalBody"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const board = {
|
|
keep: { title: "Behold", items: [
|
|
{ name: "anthropic/claude-code-mcp", verdict: "trusted", meta: "Sist auditert 2026-04-15" },
|
|
{ name: "github/copilot-chat", verdict: "trusted", meta: "Sist auditert 2026-04-12" },
|
|
{ name: "lier-kommune/internal-mcp", verdict: "trusted", meta: "Sist auditert 2026-04-30" },
|
|
{ name: "digdir/auth-mcp", verdict: "trusted", meta: "Sist auditert 2026-05-01" },
|
|
]},
|
|
review: { title: "Vurder", items: [
|
|
{ name: "thirdparty/web-search", verdict: "unknown", meta: "Audit due 2026-06-01" },
|
|
{ name: "community/markdown-tools", verdict: "unknown", meta: "Audit due 2026-05-20" },
|
|
]},
|
|
remove: { title: "Fjern", items: [
|
|
{ name: "evil-project-health@1.2.3", verdict: "BLOCK", reason: "85 funn (24 critical), Unicode-steganografi, exfil-flow" },
|
|
]},
|
|
};
|
|
|
|
const checklists = {
|
|
trusted: [
|
|
{ok:'ok', text:'Source repo verifisert (signed commits)'},
|
|
{ok:'ok', text:'Maintainer kjent og aktiv'},
|
|
{ok:'ok', text:'Ingen kritiske funn siste audit'},
|
|
{ok:'ok', text:'Capabilities dokumentert og minst-mulig'},
|
|
{ok:'ok', text:'Ingen exfil-flow detektert'},
|
|
{ok:'ok', text:'Lisens kompatibel med offentlig bruk'},
|
|
{ok:'ok', text:'Versjon pinnet i lockfile'},
|
|
{ok:'ok', text:'Endringslogg konsistent med kode'},
|
|
{ok:'ok', text:'Trust-skår > 80'},
|
|
],
|
|
unknown: [
|
|
{ok:'un', text:'Source repo verifisert'},
|
|
{ok:'ok', text:'Maintainer kjent'},
|
|
{ok:'un', text:'Audit ikke utført siste 90 d'},
|
|
{ok:'ok', text:'Capabilities dokumentert'},
|
|
{ok:'un', text:'Exfil-analyse ikke kjørt'},
|
|
{ok:'ok', text:'Lisens OK'},
|
|
{ok:'ok', text:'Versjon pinnet'},
|
|
{ok:'un', text:'Endringslogg ufullstendig'},
|
|
{ok:'un', text:'Trust-skår ikke beregnet'},
|
|
],
|
|
BLOCK: [
|
|
{ok:'no', text:'Unicode-tag-injeksjon i README (steganografi)'},
|
|
{ok:'no', text:'Exfil til webhook.site/abc123 detektert'},
|
|
{ok:'no', text:'24 kritiske TFA-funn'},
|
|
{ok:'no', text:'Maintainer ikke verifiserbar'},
|
|
{ok:'no', text:'Source-repo nylig opprettet (typosquat?)'},
|
|
{ok:'no', text:'Bash + filsystem + nett uten begrensning'},
|
|
{ok:'no', text:'Lisens uklar'},
|
|
{ok:'no', text:'Versjon ikke pinnet'},
|
|
{ok:'no', text:'Trust-skår: 4'},
|
|
],
|
|
};
|
|
|
|
function render() {
|
|
document.getElementById('board').innerHTML = ['keep','review','remove'].map(b => `
|
|
<div class="kanban-col" data-bucket="${b}">
|
|
<div class="kanban-col__head">
|
|
<span class="kanban-col__title">${board[b].title}</span>
|
|
<span class="kanban-col__count">${board[b].items.length}</span>
|
|
</div>
|
|
${board[b].items.length ? board[b].items.map((it, i) => `
|
|
<div class="kanban-card" data-verdict="${it.verdict}" onclick="openModal('${b}', ${i})">
|
|
<span class="kanban-card__name">${it.name}</span>
|
|
${it.meta ? `<span class="kanban-card__meta">${it.meta}</span>` : ''}
|
|
${it.reason ? `<span class="kanban-card__reason">${it.reason}</span>` : ''}
|
|
<div class="kanban-actions" onclick="event.stopPropagation()">
|
|
${b !== 'keep' ? `<button onclick="move('${b}','keep',${i})">→ Behold</button>` : ''}
|
|
${b !== 'review' ? `<button onclick="move('${b}','review',${i})">→ Vurder</button>` : ''}
|
|
${b !== 'remove' ? `<button onclick="move('${b}','remove',${i})">→ Fjern</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('') : `<div class="kanban-col__empty">Ingen i denne bøtten ennå.<br><button class="btn btn--secondary btn--sm" style="margin-top:8px;">+ Legg til</button></div>`}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function move(from, to, i) {
|
|
const item = board[from].items.splice(i, 1)[0];
|
|
board[to].items.push(item);
|
|
render();
|
|
}
|
|
|
|
function openModal(b, i) {
|
|
const it = board[b].items[i];
|
|
const cl = checklists[it.verdict] || checklists.unknown;
|
|
const sym = { ok: '✓', no: '✗', un: '?' };
|
|
document.getElementById('modalBody').innerHTML = `
|
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
|
<h3 style="margin:0;">${it.name}</h3>
|
|
<button class="btn btn--ghost btn--sm" onclick="closeModal()">Lukk</button>
|
|
</div>
|
|
<p class="text-secondary" style="font-size:var(--font-size-sm); margin:6px 0 0;">Trust-vurdering · ${it.verdict.toUpperCase()}</p>
|
|
<ul class="checklist">${cl.map(c => `<li><span class="${c.ok}">${sym[c.ok]}</span><span>${c.text}</span></li>`).join('')}</ul>
|
|
`;
|
|
document.getElementById('modal').setAttribute('data-open', 'true');
|
|
}
|
|
function closeModal() { document.getElementById('modal').setAttribute('data-open', 'false'); }
|
|
render();
|
|
</script>
|
|
</body>
|
|
</html>
|